atlasbot: route subjective queries to LLM

This commit is contained in:
Brad Stein 2026-01-27 20:02:09 -03:00
parent 18e543d95a
commit 9e06d7afc8
2 changed files with 42 additions and 20 deletions

View File

@ -16,7 +16,7 @@ spec:
labels: labels:
app: atlasbot app: atlasbot
annotations: annotations:
checksum/atlasbot-configmap: manual-atlasbot-68 checksum/atlasbot-configmap: manual-atlasbot-69
vault.hashicorp.com/agent-inject: "true" vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/role: "comms" vault.hashicorp.com/role: "comms"
vault.hashicorp.com/agent-inject-secret-turn-secret: "kv/data/atlas/comms/turn-shared-secret" vault.hashicorp.com/agent-inject-secret-turn-secret: "kv/data/atlas/comms/turn-shared-secret"

View File

@ -1911,19 +1911,6 @@ def cluster_answer(
history_lines: list[str] | None = None, history_lines: list[str] | None = None,
) -> str: ) -> str:
metrics_summary = snapshot_context(prompt, snapshot) metrics_summary = snapshot_context(prompt, snapshot)
if _is_insight_query(prompt):
candidates = _insight_candidates(inventory, snapshot)
used_keys = _recent_insight_keys(history_lines or [])
selected = _select_insight(prompt, candidates, used_keys=used_keys)
if selected:
key, raw_text, confidence = selected
formatted = _format_insight_text(key, raw_text)
if not formatted:
formatted = raw_text
prefix = _insight_prefix(prompt)
if prefix:
formatted = prefix + formatted
return _format_confidence(formatted, confidence)
structured = structured_answer( structured = structured_answer(
prompt, prompt,
inventory=inventory, inventory=inventory,
@ -2422,6 +2409,17 @@ def _history_payload_lines(history_payload: list[Any]) -> list[str]:
return [line for line in lines if line] return [line for line in lines if line]
def _append_history_context(context: str, history_lines: list[str]) -> str:
lines = [line.strip() for line in history_lines if isinstance(line, str) and line.strip()]
if not lines:
return context
snippet = "\n".join(lines[-6:])
combined = context + "\nRecent chat:\n" + snippet if context else "Recent chat:\n" + snippet
if len(combined) > MAX_CONTEXT_CHARS:
combined = combined[: MAX_CONTEXT_CHARS - 3].rstrip() + "..."
return combined
# Internal HTTP endpoint for cluster answers (website uses this). # Internal HTTP endpoint for cluster answers (website uses this).
class _AtlasbotHandler(BaseHTTPRequestHandler): class _AtlasbotHandler(BaseHTTPRequestHandler):
server_version = "AtlasbotHTTP/1.0" server_version = "AtlasbotHTTP/1.0"
@ -2493,15 +2491,25 @@ class _AtlasbotHandler(BaseHTTPRequestHandler):
) )
fallback = "I don't have enough data to answer that." fallback = "I don't have enough data to answer that."
if cluster_query: if cluster_query:
answer = cluster_answer( facts_answer = cluster_answer(
cleaned, cleaned,
inventory=inventory, inventory=inventory,
snapshot=snapshot, snapshot=snapshot,
workloads=workloads, workloads=workloads,
history_lines=history_lines, history_lines=history_lines,
) )
if not answer: open_ended = _is_subjective_query(cleaned) or _knowledge_intent(cleaned)
answer = fallback if open_ended:
llm_context = _append_history_context(context, history_lines)
answer = ollama_reply(
("http", "internal"),
cleaned,
context=llm_context,
fallback=facts_answer or fallback,
use_history=False,
)
else:
answer = facts_answer or fallback
else: else:
llm_prompt = cleaned llm_prompt = cleaned
answer = ollama_reply( answer = ollama_reply(
@ -2761,11 +2769,13 @@ def _ollama_call(hist_key, prompt: str, *, context: str, use_history: bool = Tru
"When a cluster snapshot is provided, never answer about unrelated meanings of 'Atlas' (maps, mythology, Apache Atlas, etc). " "When a cluster snapshot is provided, never answer about unrelated meanings of 'Atlas' (maps, mythology, Apache Atlas, etc). "
"Treat 'hottest' as highest utilization (CPU/RAM/NET/IO) rather than temperature. " "Treat 'hottest' as highest utilization (CPU/RAM/NET/IO) rather than temperature. "
"If you infer or synthesize, say 'Based on the snapshot' and keep it brief. " "If you infer or synthesize, say 'Based on the snapshot' and keep it brief. "
"For subjective prompts (interesting, favorite, unconventional), pick one or two observations from the context, explain why they stand out in 1-2 sentences, and avoid repeating the same observation as the last response if you can. "
"Prefer exact repo paths and Kubernetes resource names when relevant. " "Prefer exact repo paths and Kubernetes resource names when relevant. "
"Never include or request secret values. " "Never include or request secret values. "
"Do not suggest commands unless explicitly asked. " "Do not suggest commands unless explicitly asked. "
"Respond in plain sentences; do not return JSON or code fences unless explicitly asked. " "Respond in plain sentences; do not return JSON or code fences unless explicitly asked. "
"Translate metrics into natural language instead of echoing raw label/value pairs. " "Translate metrics into natural language instead of echoing raw label/value pairs. "
"Avoid bare lists unless the user asked for a list; weave numbers into sentences. "
"Do not answer by only listing runbooks; if the question is about Atlas/Othrys, summarize the cluster first and mention docs only if useful. " "Do not answer by only listing runbooks; if the question is about Atlas/Othrys, summarize the cluster first and mention docs only if useful. "
"If the question is not about Atlas/Othrys and no cluster context is provided, answer using general knowledge and say when you are unsure. " "If the question is not about Atlas/Othrys and no cluster context is provided, answer using general knowledge and say when you are unsure. "
"If the answer is not grounded in the provided context or tool data, say you do not know. " "If the answer is not grounded in the provided context or tool data, say you do not know. "
@ -2974,15 +2984,27 @@ def sync_loop(token: str, room_id: str):
fallback = "I don't have enough data to answer that." fallback = "I don't have enough data to answer that."
if cluster_query: if cluster_query:
reply = cluster_answer( facts_answer = cluster_answer(
cleaned_body, cleaned_body,
inventory=inventory, inventory=inventory,
snapshot=snapshot, snapshot=snapshot,
workloads=workloads, workloads=workloads,
history_lines=history[hist_key], history_lines=history[hist_key],
) )
if not reply: open_ended = _is_subjective_query(cleaned_body) or _knowledge_intent(cleaned_body)
reply = fallback if open_ended:
llm_context = _append_history_context(context, history[hist_key])
reply = ollama_reply_with_thinking(
token,
rid,
hist_key,
cleaned_body,
context=llm_context,
fallback=facts_answer or fallback,
use_history=False,
)
else:
reply = facts_answer or fallback
else: else:
llm_prompt = cleaned_body llm_prompt = cleaned_body
reply = ollama_reply_with_thinking( reply = ollama_reply_with_thinking(