Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ static void requestPlan(String endpoint, String prompt, JSONObject mapContext,
PromptResult result = parsePromptResult(code, responseBody);
callback.onComplete(result.ok, result.message, result.ok ? planJson : null);
} catch (SocketTimeoutException e) {
callback.onComplete(false, "Jetson timed out. Check WiFi, port 8000, and whether the agent is running.", null);
callback.onComplete(false, "Jetson timed out. Check WiFi, the configured endpoint port, and whether the agent is running.", null);
} catch (Exception e) {
callback.onComplete(false, friendlyException(e), null);
} finally {
Expand Down Expand Up @@ -278,6 +278,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,
Expand Down
46 changes: 44 additions & 2 deletions llm_dev_kmh/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")),
Expand Down Expand Up @@ -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(
Expand All @@ -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",
Expand All @@ -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(
Expand Down Expand Up @@ -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",
Expand Down
35 changes: 30 additions & 5 deletions llm_dev_kmh/static/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() : "";
Expand All @@ -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"
Expand All @@ -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;
}
}

Expand Down Expand Up @@ -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();
Expand Down
4 changes: 2 additions & 2 deletions llm_dev_kmh/static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -315,8 +315,8 @@ <h2>Advisor conversation</h2>
<section id="atakMirrorPanel" class="atak-mirror-panel hidden" aria-live="polite">
<div class="atak-mirror-header">
<div>
<div class="section-subtitle">ATAK Mirror</div>
<div id="atakMirrorStatus" class="section-meta">Waiting for ATAK traffic</div>
<div class="section-subtitle">ATAK / Ollama Monitor</div>
<div id="atakMirrorStatus" class="section-meta">Read-only view of Samsung TAK requests and TERA replies</div>
</div>
<button id="atakMirrorRefreshBtn" class="ghost-button small-button" type="button">Refresh</button>
</div>
Expand Down
26 changes: 26 additions & 0 deletions llm_dev_kmh/static/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -1155,6 +1155,32 @@ textarea {
white-space: pre-wrap;
}

body.atak-monitor-mode #sourcePanel,
body.atak-monitor-mode #chatLog,
body.atak-monitor-mode #promptForm,
body.atak-monitor-mode .settings-shell,
body.atak-monitor-mode #settingsMenu {
display: none !important;
}

body.atak-monitor-mode .panel-chat-shell {
flex: 1 1 auto;
}

body.atak-monitor-mode .atak-mirror-panel {
flex: 1 1 auto;
min-height: 0;
max-height: none;
margin-top: 0;
padding-top: 0;
border-top: 0;
grid-template-rows: auto minmax(0, 1fr);
}

body.atak-monitor-mode .atak-mirror-log {
min-height: 0;
}

.chat-composer {
display: grid;
gap: var(--space-2);
Expand Down
24 changes: 24 additions & 0 deletions tests/test_llm_dev_stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -664,6 +664,30 @@ def test_atak_live_profile_uses_tactical_plugin_prompt() -> None:
assert "local imagery and geospatial data sourcing assistant" not in prompt


def test_atak_mode_coerces_prompt_requests_to_live_profile() -> None:
original_mode = kmh_app.JETSON_ATAK_MODE.copy()
try:
kmh_app.JETSON_ATAK_MODE.update(
{
"active": True,
"model": "gemma3:4b",
"agent_profile": "tera-atak-live",
}
)
request = kmh_app.PromptRequest(prompt="Hello from ATAK.")

coerced = kmh_app._coerce_atak_prompt_request(request)

assert coerced.llm_provider == "ollama"
assert coerced.model == "gemma3:4b"
assert coerced.agent_profile == "tera-atak-live"
assert kmh_app._mirror_source_for_request(coerced) == "atak-plugin"
assert "live ATAK plugin agent" in kmh_app._build_system_prompt(coerced)
finally:
kmh_app.JETSON_ATAK_MODE.clear()
kmh_app.JETSON_ATAK_MODE.update(original_mode)


def test_atak_activation_normalizes_gemma3_4_alias() -> None:
assert kmh_app._normalize_ollama_model_name("gemma3:4") == "gemma3:4b"
assert kmh_app._normalize_ollama_model_name("gemma3:4b") == "gemma3:4b"
Expand Down
Loading