This commit is contained in:
2026-03-27 15:51:10 +03:00
parent 15586f9a8c
commit 51378c5d66
1234 changed files with 95644 additions and 543076 deletions
@@ -4,6 +4,7 @@ import math
from app.modules.agent.runtime import (
AgentRuntimeExecutor,
DocsQAPipelineRunner,
RuntimeRepoContextFactory,
RuntimeRetrievalAdapter,
)
@@ -11,6 +12,7 @@ from app.modules.agent.llm import AgentLlmService
from app.modules.agent.llm.prompt_loader import PromptLoader
from app.modules.agent.runtime.steps.context import build_retrieval_request, build_retrieval_result
from app.modules.agent.intent_router_v2 import ConversationState, IntentRouterV2
from app.modules.agent.intent_router_v2.factory import GigaChatIntentRouterFactory
from app.modules.shared.gigachat.client import GigaChatClient
from app.modules.shared.gigachat.settings import GigaChatSettings
from app.modules.shared.gigachat.token_provider import GigaChatTokenProvider
@@ -18,13 +20,17 @@ from tests.pipeline_setup_v3.core.models import ExecutionPayload, V3Case
class AgentRuntimeAdapter:
def __init__(self) -> None:
self._router = IntentRouterV2()
def __init__(self, *, pipeline_mode: str = "full", router_llm_mode: str = "deterministic") -> None:
self._pipeline_mode = pipeline_mode
self._router_llm_mode = router_llm_mode
self._router = self._build_router()
self._repo_context_factory = RuntimeRepoContextFactory()
self._retrieval = RuntimeRetrievalAdapter()
self._executor: AgentRuntimeExecutor | None = None
def execute(self, case: V3Case, rag_session_id: str | None) -> ExecutionPayload:
if self._is_docs_calibration_case(case):
return self._execute_docs_case(case, rag_session_id)
if case.mode == "router_only":
return self._run_router_only(case)
if not rag_session_id:
@@ -35,6 +41,25 @@ class AgentRuntimeAdapter:
return self._run_full_chain(case, rag_session_id)
raise ValueError(f"Unsupported mode: {case.mode}")
def _execute_docs_case(self, case: V3Case, rag_session_id: str | None) -> ExecutionPayload:
route = self._route(case.query)
if route.intent == "CODE_QA":
return self._stubbed_code_payload(case, route)
if case.mode == "router_only":
actual = self._actual_from_route(route)
actual["code_intents_stubbed"] = True
details = {
"query": case.query,
"router_result": route.model_dump(mode="json"),
"rag_rows": [],
"diagnostics": {"code_intents_stubbed": True},
"pipeline_steps": _router_only_steps(case.query, route),
}
return ExecutionPayload(actual=actual, details=details)
if not rag_session_id:
raise ValueError(f"Case '{case.case_id}' requires rag_session_id or repo_path")
return self._run_docs_chain(case, rag_session_id)
def _run_router_only(self, case: V3Case) -> ExecutionPayload:
route = self._route(case.query)
actual = self._actual_from_route(route)
@@ -42,6 +67,7 @@ class AgentRuntimeAdapter:
"query": case.query,
"router_result": route.model_dump(mode="json"),
"rag_rows": [],
"pipeline_steps": _router_only_steps(case.query, route),
}
return ExecutionPayload(actual=actual, details=details)
@@ -66,6 +92,14 @@ class AgentRuntimeAdapter:
"retrieval_result": retrieval_result.model_dump(mode="json"),
"rag_rows": raw_rows,
"symbol_resolution": symbol_resolution,
"pipeline_steps": _router_rag_steps(
query=case.query,
route=route.model_dump(mode="json"),
retrieval_request=request.model_dump(mode="json"),
retrieval_result=retrieval_result.model_dump(mode="json"),
rag_rows=raw_rows,
symbol_resolution=symbol_resolution,
),
}
return ExecutionPayload(actual=actual, details=details)
@@ -92,12 +126,23 @@ class AgentRuntimeAdapter:
"validation": result.validation.model_dump(mode="json"),
"token_usage": _token_usage(result),
"steps": list(result.runtime_trace),
"pipeline_steps": _full_chain_steps(
query=case.query,
route=route.model_dump(mode="json") if route else {},
retrieval_request=request.model_dump(mode="json") if request else {},
runtime_trace=list(result.runtime_trace),
),
}
return ExecutionPayload(actual=actual, details=details)
def _route(self, query: str):
return self._router.route(query, ConversationState(), self._repo_context_factory.build())
def _build_router(self) -> IntentRouterV2:
if self._router_llm_mode == "llm_disambiguation":
return GigaChatIntentRouterFactory().build()
return IntentRouterV2()
def _retrieve_rows(self, rag_session_id: str, request) -> list[dict]:
if request.sub_intent == "OPEN_FILE" and request.path_scope:
return self._retrieval.retrieve_exact_files(
@@ -162,6 +207,154 @@ class AgentRuntimeAdapter:
self._executor = AgentRuntimeExecutor(_build_llm())
return self._executor
def _docs_runner_instance(self) -> DocsQAPipelineRunner:
return DocsQAPipelineRunner(
router=self._router,
retrieval_adapter=self._retrieval,
repo_context=self._repo_context_factory.build(),
llm=None if self._pipeline_mode == "pre_llm_only" else _build_llm(),
)
def _run_docs_chain(self, case: V3Case, rag_session_id: str) -> ExecutionPayload:
result = self._docs_runner_instance().run(case.query, rag_session_id, mode=self._pipeline_mode)
actual = self._actual_from_docs_result(result)
details = {
"query": case.query,
"router_result": result.router_result.model_dump(mode="json"),
"retrieval_request": result.retrieval_request.model_dump(mode="json"),
"diagnostics": result.diagnostics.model_dump(mode="json"),
"llm_request": dict(result.llm_request or {}),
"rag_rows": list(result.raw_rows),
"pipeline_steps": [
_input_step(case.query),
{
"step": "router",
"input": {"query": case.query},
"output": {
"intent": result.router_result.intent,
"sub_intent": result.router_result.query_plan.sub_intent,
"graph_id": result.router_result.graph_id,
"conversation_mode": result.router_result.conversation_mode,
},
},
{
"step": "docs_pipeline",
"input": {"query": case.query},
"output": {
"answer_mode": result.answer_mode,
"prompt_name": result.prompt_name,
"llm_request": {
"prompt_name": result.llm_request.get("prompt_name"),
"log_context": result.llm_request.get("log_context"),
"prompt_stats": dict(result.llm_request.get("prompt_stats") or {}),
},
"degraded_reason": result.degraded_reason,
},
},
],
}
return ExecutionPayload(actual=actual, details=details)
def _actual_from_docs_result(self, result) -> dict:
route = result.router_result
request = result.retrieval_request
diagnostics = result.diagnostics
entity_candidates = tuple(diagnostics.query_entity_candidates or self._docs_entity_candidates(result.raw_rows))
doc_scope = tuple(diagnostics.doc_ids or self._docs_scope(result.raw_rows))
return {
"intent": route.intent,
"sub_intent": route.query_plan.sub_intent,
"graph_id": route.graph_id,
"conversation_mode": route.conversation_mode,
"rag_count": len(result.raw_rows),
"llm_answer": result.answer,
"answer_mode": _docs_answer_status(result.answer_mode, self._pipeline_mode),
"path_scope": tuple(getattr(request, "path_scope", []) or []),
"doc_scope": doc_scope,
"entity_candidates": entity_candidates,
"symbol_candidates": (),
"layers": tuple(request.requested_layers or []),
"filters": {
"doc_type": getattr(request.retrieval_spec.filters, "doc_type", None),
},
"pipeline_mode": self._pipeline_mode,
"gate_decision": diagnostics.gate_decision,
"prompt_used": result.prompt_name,
"llm_mode": diagnostics.llm_mode,
"degraded_reason": result.degraded_reason or None,
"code_intents_stubbed": True,
}
def _stubbed_code_payload(self, case: V3Case, route) -> ExecutionPayload:
actual = {
"intent": route.intent,
"sub_intent": route.query_plan.sub_intent,
"graph_id": route.graph_id,
"conversation_mode": route.conversation_mode,
"rag_count": 0,
"llm_answer": "",
"answer_mode": "stubbed",
"status": "stubbed",
"reason": "code intents are temporarily disabled during docs pipeline calibration",
"layers": (),
"symbol_candidates": (),
"code_intents_stubbed": True,
}
details = {
"query": case.query,
"router_result": route.model_dump(mode="json"),
"rag_rows": [],
"diagnostics": {
"code_intents_stubbed": True,
"degraded_reason": "code_intent_stubbed",
},
"pipeline_steps": [
_input_step(case.query),
{
"step": "router",
"input": {"query": case.query},
"output": {
"intent": route.intent,
"sub_intent": route.query_plan.sub_intent,
"graph_id": route.graph_id,
"conversation_mode": route.conversation_mode,
},
},
{
"step": "code_stub",
"input": {"query": case.query},
"output": {
"status": "stubbed",
"reason": "code intents are temporarily disabled during docs pipeline calibration",
},
},
],
}
return ExecutionPayload(actual=actual, details=details)
def _is_docs_calibration_case(self, case: V3Case) -> bool:
return "suite_03_docs" in case.source_file.as_posix()
def _docs_scope(self, rows: list[dict]) -> list[str]:
values: list[str] = []
for row in rows:
metadata = dict(row.get("metadata") or {})
for candidate in (metadata.get("document_id"), metadata.get("doc_id")):
value = str(candidate or "").strip()
if value and value not in values:
values.append(value)
return values
def _docs_entity_candidates(self, rows: list[dict]) -> list[str]:
values: list[str] = []
for row in rows:
if str(row.get("layer") or "") != "D3_ENTITY_CATALOG":
continue
title = str(row.get("title") or "").strip()
if title and title not in values:
values.append(title)
return values
def _build_llm() -> AgentLlmService:
settings = GigaChatSettings.from_env()
@@ -175,6 +368,16 @@ def _answer_status(answer_mode: str, llm_used: bool) -> str:
return answer_mode
def _docs_answer_status(answer_mode: str, pipeline_mode: str) -> str:
if pipeline_mode != "pre_llm_only":
return answer_mode
if answer_mode == "ready":
return "answered"
if answer_mode == "ready_partial":
return "structured_spec"
return answer_mode
def _token_usage(result) -> dict:
draft = result.draft_answer
if draft is None:
@@ -185,3 +388,108 @@ def _token_usage(result) -> dict:
"prompt_name": draft.prompt_name,
"tokens_in_estimate": tokens_in_estimate,
}
def _router_only_steps(query: str, route) -> list[dict]:
route_dump = route.model_dump(mode="json")
return [
_input_step(query),
{
"step": "router",
"input": {"query": query},
"output": {
"intent": route_dump.get("intent"),
"sub_intent": dict(route_dump.get("query_plan") or {}).get("sub_intent"),
"graph_id": route_dump.get("graph_id"),
"conversation_mode": route_dump.get("conversation_mode"),
},
},
]
def _router_rag_steps(
*,
query: str,
route: dict,
retrieval_request: dict,
retrieval_result: dict,
rag_rows: list[dict],
symbol_resolution: dict,
) -> list[dict]:
return [
_input_step(query),
{
"step": "router",
"input": {"query": query},
"output": {
"intent": route.get("intent"),
"sub_intent": dict(route.get("query_plan") or {}).get("sub_intent"),
"graph_id": route.get("graph_id"),
"conversation_mode": route.get("conversation_mode"),
},
},
{
"step": "retrieval_planning",
"input": {
"query": query,
"router_result": {
"intent": route.get("intent"),
"sub_intent": dict(route.get("query_plan") or {}).get("sub_intent"),
"graph_id": route.get("graph_id"),
},
},
"output": retrieval_request,
},
{
"step": "retrieval",
"input": retrieval_request,
"output": {
"rag_count": len(rag_rows),
"retrieval_result": retrieval_result,
"symbol_resolution": symbol_resolution,
},
},
]
def _full_chain_steps(*, query: str, route: dict, retrieval_request: dict, runtime_trace: list[dict]) -> list[dict]:
steps = [_input_step(query)]
for raw_step in runtime_trace:
step = dict(raw_step)
name = str(step.get("step") or "").strip()
if name == "router":
step.setdefault("input", {"query": query})
if name == "retrieval":
steps.append(
{
"step": "retrieval_planning",
"input": {
"query": query,
"router_result": {
"intent": route.get("intent"),
"sub_intent": dict(route.get("query_plan") or {}).get("sub_intent"),
"graph_id": route.get("graph_id"),
},
},
"output": retrieval_request,
}
)
step.setdefault("input", retrieval_request)
steps.append(
{
"step": name,
"status": step.get("status"),
"timings_ms": step.get("timings_ms") or {},
"input": step.get("input") or {},
"output": step.get("output") or {},
}
)
return steps
def _input_step(query: str) -> dict:
return {
"step": "input_query",
"input": {"query": query},
"output": {"query": query},
}