Improve agent UX and spec sync

This commit is contained in:
2026-07-03 16:47:10 +03:00
parent 500491c281
commit a99c1d6d01
22 changed files with 615 additions and 94 deletions

View File

@@ -55,6 +55,33 @@ from src.core.logger import logger
JWT_SECRET = os.environ["JWT_SECRET"] # @INVARIANT JWT_SECRET must be set in environment — crash-early, no default fallback
MAX_FILE_SIZE_BYTES = 10 * 1024 * 1024 # 10 MB
def _now_iso() -> str:
return datetime.now().astimezone().isoformat(timespec="seconds")
async def _build_agent_context(env_id: str | None) -> str:
"""Build hidden runtime context for the LLM without storing it as user text."""
parts = [
"[RUNTIME CONTEXT]",
f"Current datetime: {_now_iso()}",
"If the user asks to start/run something without an explicit start time, use Current datetime as start_time.",
"If the user gives a duration such as '15 minutes' or '2 hours', compute end_time from Current datetime.",
]
if env_id:
parts.append(f"Current environment_id: {env_id}")
dashboards = await prefetch_dashboards(env_id)
if dashboards:
parts.extend([
"",
"[PRE-FETCHED DASHBOARDS]",
dashboards,
"[/PRE-FETCHED DASHBOARDS]",
"Use the pre-fetched dashboards directly for dashboard name/id resolution.",
])
parts.append("[/RUNTIME CONTEXT]")
return "\n".join(parts)
# ── Session state ───────────────────────────────────────────────
# In-memory per-user lock (keyed by user_id)
_user_locks: dict[str, bool] = {}
@@ -262,6 +289,7 @@ async def agent_handler( # noqa: C901 — intentionally complex C4 orchestratio
text = text[:last_sentence_end + 1] + "\n[...truncated]"
else:
text = truncated + "\n[...truncated]"
visible_user_text = text
# ── File upload ──
file_storage_path: str | None = None
@@ -285,6 +313,11 @@ async def agent_handler( # noqa: C901 — intentionally complex C4 orchestratio
parsed = parse_upload(file_obj)
text = f"{text}\n\n--- Uploaded file content ---\n{parsed}"
runtime_context = await _build_agent_context(env_id)
agent_text = f"{visible_user_text}\n\n{runtime_context}"
if text != visible_user_text:
agent_text = f"{text}\n\n{runtime_context}"
# ── Yield file metadata for frontend download link ──
if file_storage_path and file_original_name:
yield json.dumps({
@@ -338,7 +371,7 @@ async def agent_handler( # noqa: C901 — intentionally complex C4 orchestratio
try:
emitted_any = False
async for event in agent.astream_events(
{"messages": [HumanMessage(content=text)]},
{"messages": [HumanMessage(content=agent_text)]},
config=config,
version="v2",
):
@@ -382,7 +415,7 @@ async def agent_handler( # noqa: C901 — intentionally complex C4 orchestratio
state = await agent.aget_state(config)
if getattr(state, "next", None):
emitted_any = True
yield confirmation_payload(conv_id, state, text)
yield confirmation_payload(conv_id, state, visible_user_text)
return
elif not emitted_any:
yield json.dumps({
@@ -408,7 +441,7 @@ async def agent_handler( # noqa: C901 — intentionally complex C4 orchestratio
"retryable": True,
},
})
await save_conversation(conv_id, text, user_id, assistant_text="")
await save_conversation(conv_id, visible_user_text, user_id, assistant_text="")
return
except (APITimeoutError, httpx.ReadTimeout) as exc:
@@ -426,7 +459,7 @@ async def agent_handler( # noqa: C901 — intentionally complex C4 orchestratio
"retryable": True,
},
})
await save_conversation(conv_id, text, user_id, assistant_text="")
await save_conversation(conv_id, visible_user_text, user_id, assistant_text="")
return
except AuthenticationError as exc:
@@ -444,7 +477,7 @@ async def agent_handler( # noqa: C901 — intentionally complex C4 orchestratio
"retryable": False,
},
})
await save_conversation(conv_id, text, user_id, assistant_text="")
await save_conversation(conv_id, visible_user_text, user_id, assistant_text="")
return
except OutputParserException as e:
@@ -472,7 +505,7 @@ async def agent_handler( # noqa: C901 — intentionally complex C4 orchestratio
try:
state = await agent.aget_state(config)
if getattr(state, "next", None):
yield confirmation_payload(conv_id, state, text)
yield confirmation_payload(conv_id, state, visible_user_text)
return
except Exception:
pass
@@ -480,12 +513,12 @@ async def agent_handler( # noqa: C901 — intentionally complex C4 orchestratio
"content": f"❌ Ошибка: {exc}",
"metadata": {"type": "error", "code": "PROCESSING_ERROR", "detail": str(exc)},
})
await save_conversation(conv_id, text, user_id, assistant_text="".join(assistant_parts))
await save_conversation(conv_id, visible_user_text, user_id, assistant_text="".join(assistant_parts))
return
await save_conversation(conv_id, text, user_id, assistant_text="".join(assistant_parts))
await save_conversation(conv_id, visible_user_text, user_id, assistant_text="".join(assistant_parts))
# Fire-and-forget: generate LLM title in background (best-effort)
asyncio.create_task(generate_llm_title(conv_id, text))
asyncio.create_task(generate_llm_title(conv_id, visible_user_text))
logger.reflect(
"Agent handler completed",
payload={"conv_id": conv_id, "assistant_len": len("".join(assistant_parts))},

View File

@@ -192,7 +192,12 @@ async def create_agent(
"You handle all intent detection — multi-intent queries, negations (\"don't run\"), "
"synonyms (\"панели\" = \"дашборды\"), and typos are your responsibility. "
"Call the right tool(s) for the job. If data is already provided in context, "
"use it directly rather than calling redundant tools."
"use it directly rather than calling redundant tools. "
"For maintenance requests, use the RUNTIME CONTEXT current datetime when the user says "
"\"start\", \"run\", \"now\", \"запусти\", or \"сейчас\" without an explicit start time. "
"Convert user durations into end_time. Do not ask for ISO datetime in that case. "
"If a user asks for dashboard maintenance, resolve the dashboard from provided context or tools, "
"then infer affected tables when possible; ask for table names only after resolution fails."
)
if env_id:
prompt += f"\n\nCurrent environment: '{env_id}'. When calling tools that accept env_id, use this value."