tasks 033 updated

This commit is contained in:
2026-06-30 19:05:17 +03:00
parent f1810090b6
commit e174c11d4a
23 changed files with 1976 additions and 213 deletions

View File

@@ -8,9 +8,12 @@
# from exceeding 400 lines and centralises risk classification in one place.
import asyncio
import json
import os
from collections.abc import AsyncGenerator
from typing import Any
from langchain_openai import ChatOpenAI
from src.agent._tool_resolver import (
_SAFE_AGENT_TOOLS,
_DANGEROUS_AGENT_TOOLS,
@@ -109,6 +112,80 @@ def confirmation_payload(conv_id: str, state, user_text: str) -> str:
# #endregion AgentChat.Confirmation.Payload
# #region AgentChat.Confirmation.FormatOutput [C:3] [TYPE Function] [SEMANTICS agent-chat,hitl,llm,formatting]
# @ingroup AgentChat
# @BRIEF Format tool output via LLM for a natural-language response, with fallback to
# prettified JSON. Yields streaming tokens.
# @POST Yields stream_token events with formatted text.
# @RELATION DEPENDS_ON -> [AgentChat.LangGraph.Setup]
# @RATIONALE Fast-path confirmation bypasses the LangGraph agent — the tool result is
# raw JSON. This function adds an LLM formatting layer so the user sees a
# readable response instead of raw data. Falls back to rule-based formatting
# when LLM is unavailable.
# @REJECTED Yielding raw JSON directly was rejected — users expect LLM-styled answers,
# not machine-readable data dumps.
async def _format_tool_output_via_llm(
tool_name: str, output: str,
) -> AsyncGenerator[str]:
from src.agent.langgraph_setup import _fetch_llm_config
from src.core.logger import logger
text = output.strip()
if not text:
yield json.dumps({
"content": "_(нет данных)_",
"metadata": {"type": "stream_token", "token": "_(нет данных)_"},
})
return
# ── Try LLM formatting ──
config = await _fetch_llm_config()
if config and config.get("configured"):
try:
llm = ChatOpenAI(
model=config.get("default_model", "gpt-4o-mini"),
base_url=config.get("base_url", "https://api.openai.com/v1"),
api_key=config["api_key"],
temperature=0,
max_tokens=1024,
)
prompt = (
f"Tool '{tool_name}' returned this data:\n\n{text}\n\n"
"Summarize this data in a concise, human-readable format. "
"Use bullet points or a short paragraph. "
"Keep it brief — under 5 sentences. "
"Answer in Russian unless the data is in English."
)
async for chunk in llm.astream(prompt):
if hasattr(chunk, "content") and chunk.content:
yield json.dumps({
"content": chunk.content,
"metadata": {"type": "stream_token", "token": chunk.content},
})
return
except Exception as exc:
logger.explore(
"LLM formatting failed, falling back to prettified output",
payload={"tool": tool_name}, error=str(exc),
extra={"src": "AgentChat.Confirmation.FormatOutput"},
)
# ── Fallback: prettified JSON or raw text ──
try:
data = json.loads(text)
pretty = json.dumps(data, indent=2, ensure_ascii=False)
yield json.dumps({
"content": pretty,
"metadata": {"type": "stream_token", "token": pretty},
})
except (json.JSONDecodeError, ValueError):
yield json.dumps({
"content": text,
"metadata": {"type": "stream_token", "token": text},
})
# #endregion AgentChat.Confirmation.FormatOutput
# #region AgentChat.Confirmation.HandleResume [C:4] [TYPE Function] [SEMANTICS agent-chat,hitl,resume,streaming]
# @ingroup AgentChat
# @BRIEF Resume from HITL checkpoint — execute confirmed tool or abort on deny.
@@ -179,10 +256,9 @@ async def handle_resume(
"content": f"{tool_name}",
"metadata": {"type": "tool_end", "tool": tool_name, "output": {"result": str(output)[:500]}},
})
yield json.dumps({
"content": str(output),
"metadata": {"type": "stream_token", "token": str(output)},
})
# Format tool output via LLM for a human-readable response
async for chunk in _format_tool_output_via_llm(tool_name, str(output)):
yield chunk
logger.reflect(
"Fast-path confirmation completed",
payload={"tool": tool_name},

View File

@@ -47,7 +47,7 @@ from src.agent.context import set_user_jwt
from src.agent.document_parser import parse_upload
from src.agent.langgraph_setup import create_agent
from src.agent.middleware import log_tool_event
from src.agent.tools import get_tools_for_query
from src.agent.tools import get_all_tools
from src.core.auth.jwt import decode_token
from src.core.cot_logger import seed_trace_id
from src.core.logger import logger
@@ -174,6 +174,11 @@ async def agent_handler( # noqa: C901 — intentionally complex C4 orchestratio
# ── Parse message ──
text = message.get("text", "") if isinstance(message, dict) else str(message)
# Preserve original user text for intent detection BEFORE any augmentation
# (truncation, file upload content, prefetch data). Substring-based keyword
# matching in get_tools_for_query / fast_confirmation_tool would otherwise
# match system-injected text (e.g. "tool" ⊂ "tools" in prefetch marker).
user_message_text = text
files = message.get("files", []) if isinstance(message, dict) else []
if not text.strip() and not files:
return
@@ -253,12 +258,12 @@ async def agent_handler( # noqa: C901 — intentionally complex C4 orchestratio
conv_id = conversation_id or str(uuid.uuid4())
_conv_locks[conv_id] = asyncio.Event()
fast_tool_name = fast_confirmation_tool(text)
fast_tool_name = fast_confirmation_tool(user_message_text)
if fast_tool_name:
_pending_confirmations[conv_id] = {
"tool_name": fast_tool_name,
"tool_args": {},
"user_text": text,
"user_text": user_message_text,
}
yield json.dumps({
"content": "⏸️ Требуется подтверждение",
@@ -267,18 +272,18 @@ async def agent_handler( # noqa: C901 — intentionally complex C4 orchestratio
return
# ── Pre-fetch dashboards ──
text_lower = text.lower()
prefetch_available = False
text_lower = user_message_text.lower()
if any(kw in text_lower for kw in ["дашборд", "dashboard", "dashboards", "дашборды"]):
try:
dash_data = await prefetch_dashboards(env_id or "")
if dash_data:
text += f"\n\n[PRE-FETCHED DATA — use this directly, do NOT call tools]\n{dash_data}\n[/PRE-FETCHED DATA]"
prefetch_available = True
except Exception:
pass
agent_tools = get_tools_for_query(text, prefetch_available=prefetch_available)
# All tools exposed — Gemma context window is now sufficient.
# Intent-based subset filtering (get_tools_for_query) retired.
agent_tools = get_all_tools()
agent = await create_agent(agent_tools, env_id)
config = {"configurable": {"thread_id": conv_id}}
@@ -333,7 +338,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, user_message_text)
return
elif not emitted_any:
yield json.dumps({

View File

@@ -184,7 +184,11 @@ async def create_agent(
# System prompt — env_id injected deterministically, not in user message
prompt = (
"You are a Superset Tools assistant. You have access to tools for searching "
"dashboards, checking health, listing environments, and checking task status. "
"dashboards, managing maintenance, running migrations and backups, "
"executing SQL and exploring databases, auditing permissions, "
"managing Git operations (branch/commit/deploy), running LLM validation "
"and documentation, creating and copying dashboards and datasets, "
"and checking system health, environments, and task status. "
"If the data you need is already provided in the user message, use that directly "
"rather than calling tools. Only call tools when the data is not present."
)