Improve agent UX and spec sync
This commit is contained in:
@@ -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))},
|
||||
|
||||
@@ -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."
|
||||
|
||||
Reference in New Issue
Block a user