From c00a1f1d24d26f713c323b5613bc5ef86e24ae3e Mon Sep 17 00:00:00 2001 From: kylemhicks Date: Sun, 3 May 2026 02:13:46 -0700 Subject: [PATCH] fix(atak): mirror local agent chat read-only --- .../plugin/TeraPlanClient.java | 7 +++ llm_dev_kmh/app.py | 46 ++++++++++++++++++- llm_dev_kmh/static/app.js | 35 ++++++++++++-- llm_dev_kmh/static/index.html | 4 +- llm_dev_kmh/static/styles.css | 26 +++++++++++ tests/test_llm_dev_stream.py | 24 ++++++++++ 6 files changed, 133 insertions(+), 9 deletions(-) diff --git a/atak/plugin/app/src/main/java/TacticalEdgeRouteAgent/plugin/TeraPlanClient.java b/atak/plugin/app/src/main/java/TacticalEdgeRouteAgent/plugin/TeraPlanClient.java index ca4bcb6..bd64b50 100644 --- a/atak/plugin/app/src/main/java/TacticalEdgeRouteAgent/plugin/TeraPlanClient.java +++ b/atak/plugin/app/src/main/java/TacticalEdgeRouteAgent/plugin/TeraPlanClient.java @@ -268,6 +268,13 @@ private static PromptResult parsePromptResult(int code, String body) { } return new PromptResult(true, summary.toString()); } + if (code >= 200 && code < 300 && json.has("response")) { + String response = json.optString("response", "").trim(); + if (!response.isEmpty()) { + return new PromptResult(true, response); + } + return new PromptResult(false, "Jetson returned an empty TERA response."); + } String detail = json.optString("detail", json.optString("message", "")); if (code >= 200 && code < 300) { return new PromptResult(false, diff --git a/llm_dev_kmh/app.py b/llm_dev_kmh/app.py index c808c13..f46c074 100644 --- a/llm_dev_kmh/app.py +++ b/llm_dev_kmh/app.py @@ -2587,11 +2587,30 @@ def _request_is_atak_mirror_candidate(request: PromptRequest) -> bool: def _mirror_source_for_request(request: PromptRequest) -> str: profile = (request.agent_profile or "").strip().lower() - if "atak" in profile: + if bool(JETSON_ATAK_MODE.get("active")) or "atak" in profile: return "atak-plugin" return "web-planner" +def _coerce_atak_prompt_request(request: PromptRequest) -> PromptRequest: + if not bool(JETSON_ATAK_MODE.get("active")): + return request + + profile = (request.agent_profile or "").strip().lower() + if "atak" in profile and (request.llm_provider or "").strip().lower() == "ollama": + return request + + return request.model_copy( + update={ + "agent_profile": str( + JETSON_ATAK_MODE.get("agent_profile") or TERA_ATAK_AGENT_PROFILE + ), + "llm_provider": "ollama", + "model": request.model or str(JETSON_ATAK_MODE.get("model") or TERA_ATAK_MODEL), + } + ) + + def _jetson_atak_response(detail: str | None = None) -> JetsonAtakModeResponse: return JetsonAtakModeResponse( active=bool(JETSON_ATAK_MODE.get("active")), @@ -7798,6 +7817,7 @@ async def create_package_cot( @app.post("/api/prompt", response_model=PromptResponse) async def prompt_ollama(request: PromptRequest) -> PromptResponse: + request = _coerce_atak_prompt_request(request) mirror = _request_is_atak_mirror_candidate(request) if mirror: _append_atak_mirror_event( @@ -7808,7 +7828,19 @@ async def prompt_ollama(request: PromptRequest) -> PromptResponse: provider=_request_llm_provider(request), direction="inbound", ) - result = await _post_prompt_with_fallback(request) + try: + result = await _post_prompt_with_fallback(request) + except HTTPException as exc: + if mirror: + _append_atak_mirror_event( + source="tera-agent", + role="assistant", + text=f"ERROR: {exc.detail}", + model=request.model or str(JETSON_ATAK_MODE.get("model") or TERA_ATAK_MODEL), + provider=_request_llm_provider(request), + direction="error", + ) + raise if mirror: _append_atak_mirror_event( source="tera-agent", @@ -7823,6 +7855,7 @@ async def prompt_ollama(request: PromptRequest) -> PromptResponse: @app.post("/api/prompt/stream") async def prompt_ollama_stream(request: PromptRequest) -> StreamingResponse: + request = _coerce_atak_prompt_request(request) mirror = _request_is_atak_mirror_candidate(request) if mirror: _append_atak_mirror_event( @@ -7986,6 +8019,15 @@ async def event_stream(): model=model, ) failures.append("ollama: " + " | ".join(errors)) + if mirror: + _append_atak_mirror_event( + source="tera-agent", + role="assistant", + text="ERROR: All model providers failed. " + " | ".join(failures), + model=model, + provider="ollama", + direction="error", + ) yield _sse_event( { "type": "error", diff --git a/llm_dev_kmh/static/app.js b/llm_dev_kmh/static/app.js index 2249e7c..272de11 100644 --- a/llm_dev_kmh/static/app.js +++ b/llm_dev_kmh/static/app.js @@ -2871,22 +2871,28 @@ function ensureLocalModelOption(model) { function renderAtakMirror(events = []) { els.atakMirrorLog.innerHTML = ""; - if (!events.length) { + const conversationEvents = events.filter((event) => ( + event.role === "operator" || event.role === "assistant" + )); + + if (!conversationEvents.length) { const empty = document.createElement("div"); empty.className = "atak-mirror-empty"; - empty.textContent = "No ATAK plugin conversation mirrored yet."; + empty.textContent = "No ATAK plugin conversation mirrored yet. Send from the Samsung TAK device."; els.atakMirrorLog.appendChild(empty); return; } - for (const event of events) { + for (const event of conversationEvents) { const item = document.createElement("article"); item.className = `atak-mirror-event ${event.role === "operator" ? "inbound" : "outbound"}`; const header = document.createElement("div"); header.className = "atak-mirror-event-header"; const source = document.createElement("span"); - source.textContent = `${event.source || "jetson"} / ${event.role || "event"}`; + source.textContent = event.role === "operator" + ? "ATAK device / operator" + : "TERA agent / Ollama"; const stamp = document.createElement("time"); stamp.dateTime = event.timestamp || ""; stamp.textContent = event.timestamp ? new Date(event.timestamp).toLocaleTimeString() : ""; @@ -2913,9 +2919,19 @@ function renderAtakMirror(events = []) { els.atakMirrorLog.scrollTop = els.atakMirrorLog.scrollHeight; } +function applyAtakMonitorMode() { + document.body.classList.toggle("atak-monitor-mode", state.atakAgentActive); + els.promptInput.disabled = state.atakAgentActive; + els.submitBtn.disabled = state.atakAgentActive; + if (state.atakAgentActive) { + els.requestStatus.textContent = "Read-only ATAK monitor active"; + } +} + function applyAtakAgentStatus(data) { state.atakAgentActive = Boolean(data.active); els.atakMirrorPanel.classList.toggle("hidden", !state.atakAgentActive); + applyAtakMonitorMode(); if (state.atakAgentActive) { const label = data.status === "active" ? "ATAK Local: ready" @@ -2924,10 +2940,14 @@ function applyAtakAgentStatus(data) { : "ATAK Local: starting"; const tone = data.status === "active" ? "good" : data.status === "error" ? "bad" : "warn"; setAtakAgentButton(label, tone); - els.atakMirrorStatus.textContent = data.detail || `Mirroring ${data.model || "local model"}`; + els.atakMirrorStatus.textContent = data.status === "active" + ? `Watching Samsung TAK -> ${data.model || "local Ollama"} -> Samsung TAK` + : data.detail || `Mirroring ${data.model || "local model"}`; renderAtakMirror(data.events || []); } else { setAtakAgentButton("ATAK Local", ""); + els.promptInput.disabled = false; + els.submitBtn.disabled = false; } } @@ -3276,6 +3296,11 @@ async function streamAssistantProvider({ async function submitPrompt(event) { event.preventDefault(); + if (state.atakAgentActive) { + els.requestStatus.textContent = "ATAK Local is read-only here. Send prompts from the Samsung TAK device."; + await refreshAtakMirror().catch(() => null); + return; + } els.submitBtn.disabled = true; const prompt = els.promptInput.value.trim(); diff --git a/llm_dev_kmh/static/index.html b/llm_dev_kmh/static/index.html index fae0501..6e437b7 100644 --- a/llm_dev_kmh/static/index.html +++ b/llm_dev_kmh/static/index.html @@ -315,8 +315,8 @@

Advisor conversation