From 0a25e42ea165014a62bb83cfe3c7f3d960758160 Mon Sep 17 00:00:00 2001 From: zosimovaa Date: Wed, 1 Apr 2026 12:28:55 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9F=D0=BE=D0=B4=D1=87=D0=B8=D1=81=D1=82?= =?UTF-8?q?=D0=B8=D0=BB=20=D0=B0=D1=80=D1=85=D0=B8=D1=82=D0=B5=D0=BA=D1=82?= =?UTF-8?q?=D1=83=D1=80=D1=83=20=D0=BF=D1=80=D0=B8=D0=BB=D0=BE=D0=B6=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D1=8F=20v1=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=B0?= =?UTF-8?q?=D0=B5=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- _process.zip | Bin 0 -> 17467 bytes src/app/main.py | 2 +- src/app/modules/agent/llm/service.py | 32 +- .../agent/observability/module_trace.py | 27 ++ .../modules/agent/orchestration/__init__.py | 1 + .../adapters/intent_router_adapter.py | 24 ++ .../adapters/llm_chat_adapter.py | 32 ++ .../adapters/task_runtime_adapter.py | 4 +- .../context/execution_context.py | 22 ++ .../{ => agent}/orchestration/facade.py | 20 +- .../messaging/client_message_publisher.py | 8 +- .../messaging/status_message_factory.py | 2 +- .../messaging/user_message_factory.py | 2 +- .../orchestration/processes/registry.py | 4 +- .../orchestration/processes/v1/process.py | 0 .../processes/v1/prompt_payload_builder.py | 11 + .../orchestration/processes/v1/prompts.yml | 12 + .../processes/v1/simple_llm_workflow.py | 42 +++ .../processes/v1/steps/bootstrap_step.py | 9 +- .../v1/steps/execute_llm_workflow_step.py | 35 +++ .../processes/v1/steps/finalize_step.py | 6 +- .../processes/v1/steps/run_llm_step.py | 4 +- .../orchestration/processes/v2/README.md | 2 +- .../orchestration/processes/v2/process.py | 0 .../processes/v2/prompt_payload_builder.py | 35 +++ .../processes/v2/prompt_selector.py | 18 ++ .../orchestration/processes/v2/prompts.yml | 96 ++++++ .../execute_documentation_workflow_step.py | 2 +- .../steps/execute_fallback_workflow_step.py | 4 +- .../steps/execute_general_qa_workflow_step.py | 2 +- .../v2/steps/execute_openapi_workflow_step.py | 2 +- .../processes/v2/steps/route_intent_step.py | 19 +- .../v2/steps/run_task_workflow_step.py | 4 +- .../processes/v2/steps/workflow_step_base.py | 9 +- .../orchestration/runtime/process_runner.py | 2 +- .../{ => agent}/orchestration/v2_progress.py | 2 +- .../runtime/docs_qa_pipeline/pipeline.py | 80 +++-- .../prompt_payload_builder.py | 2 - src/app/modules/agent/task_runtime/context.py | 2 + src/app/modules/agent/task_runtime/models.py | 1 + .../agent/task_runtime/workflows/docs_qa.py | 15 +- .../agent/task_runtime/workflows/fallback.py | 21 +- .../task_runtime/workflows/general_qa.py | 15 +- .../agent/task_runtime/workflows/openapi.py | 15 +- src/app/modules/agent_api/__init__.py | 3 - src/app/modules/api/__init__.py | 1 + .../application/request_service.py | 10 +- .../application/session_service.py | 6 +- .../application/stream_service.py | 4 +- .../controllers/request_controller.py | 2 +- .../controllers/session_controller.py | 2 +- .../controllers/stream_controller.py | 2 +- .../domain/events/client_event.py | 0 .../domain/models/agent_request.py | 0 .../domain/models/agent_session.py | 0 .../infrastructure/ids/request_id_factory.py | 0 .../infrastructure/ids/session_id_factory.py | 0 .../logging/request_trace_logger.py | 22 +- .../logging/trace_file_path_builder.py | 0 .../logging/trace_markdown_writer.py | 0 .../stores/in_memory_request_store.py | 2 +- .../stores/in_memory_session_store.py | 2 +- .../infrastructure/streaming/replay_buffer.py | 2 +- .../infrastructure/streaming/sse_encoder.py | 2 +- .../streaming/sse_event_channel.py | 4 +- src/app/modules/{agent_api => api}/module.py | 16 +- .../{agent_api => api}/public_router.py | 6 +- src/app/modules/application.py | 78 +++-- src/app/modules/chat/README.md | 98 ------ src/app/modules/chat/__init__.py | 0 src/app/modules/chat/dialog_store.py | 33 -- src/app/modules/chat/direct_service.py | 71 ----- src/app/modules/chat/evidence_gate.py | 62 ---- src/app/modules/chat/module.py | 112 ------- src/app/modules/chat/repository.py | 93 ------ src/app/modules/chat/service.py | 288 ------------------ src/app/modules/chat/session_resolver.py | 36 --- src/app/modules/chat/task_store.py | 38 --- src/app/modules/orchestration/__init__.py | 3 - .../adapters/intent_router_adapter.py | 11 - .../adapters/llm_chat_adapter.py | 19 -- .../context/execution_context.py | 20 -- src/app/schemas/agent_api.py | 2 +- .../chat/test_chat_api_simple_code_explain.py | 70 ----- tests/unit_tests/chat/test_direct_service.py | 61 ---- 85 files changed, 644 insertions(+), 1184 deletions(-) create mode 100644 _process.zip create mode 100644 src/app/modules/agent/observability/module_trace.py create mode 100644 src/app/modules/agent/orchestration/__init__.py create mode 100644 src/app/modules/agent/orchestration/adapters/intent_router_adapter.py create mode 100644 src/app/modules/agent/orchestration/adapters/llm_chat_adapter.py rename src/app/modules/{ => agent}/orchestration/adapters/task_runtime_adapter.py (90%) create mode 100644 src/app/modules/agent/orchestration/context/execution_context.py rename src/app/modules/{ => agent}/orchestration/facade.py (74%) rename src/app/modules/{ => agent}/orchestration/messaging/client_message_publisher.py (70%) rename src/app/modules/{ => agent}/orchestration/messaging/status_message_factory.py (84%) rename src/app/modules/{ => agent}/orchestration/messaging/user_message_factory.py (84%) rename src/app/modules/{ => agent}/orchestration/processes/registry.py (69%) rename src/app/modules/{ => agent}/orchestration/processes/v1/process.py (100%) create mode 100644 src/app/modules/agent/orchestration/processes/v1/prompt_payload_builder.py create mode 100644 src/app/modules/agent/orchestration/processes/v1/prompts.yml create mode 100644 src/app/modules/agent/orchestration/processes/v1/simple_llm_workflow.py rename src/app/modules/{ => agent}/orchestration/processes/v1/steps/bootstrap_step.py (63%) create mode 100644 src/app/modules/agent/orchestration/processes/v1/steps/execute_llm_workflow_step.py rename src/app/modules/{ => agent}/orchestration/processes/v1/steps/finalize_step.py (65%) rename src/app/modules/{ => agent}/orchestration/processes/v1/steps/run_llm_step.py (83%) rename src/app/modules/{ => agent}/orchestration/processes/v2/README.md (98%) rename src/app/modules/{ => agent}/orchestration/processes/v2/process.py (100%) create mode 100644 src/app/modules/agent/orchestration/processes/v2/prompt_payload_builder.py create mode 100644 src/app/modules/agent/orchestration/processes/v2/prompt_selector.py create mode 100644 src/app/modules/agent/orchestration/processes/v2/prompts.yml rename src/app/modules/{ => agent}/orchestration/processes/v2/steps/execute_documentation_workflow_step.py (58%) rename src/app/modules/{ => agent}/orchestration/processes/v2/steps/execute_fallback_workflow_step.py (70%) rename src/app/modules/{ => agent}/orchestration/processes/v2/steps/execute_general_qa_workflow_step.py (55%) rename src/app/modules/{ => agent}/orchestration/processes/v2/steps/execute_openapi_workflow_step.py (56%) rename src/app/modules/{ => agent}/orchestration/processes/v2/steps/route_intent_step.py (78%) rename src/app/modules/{ => agent}/orchestration/processes/v2/steps/run_task_workflow_step.py (84%) rename src/app/modules/{ => agent}/orchestration/processes/v2/steps/workflow_step_base.py (88%) rename src/app/modules/{ => agent}/orchestration/runtime/process_runner.py (79%) rename src/app/modules/{ => agent}/orchestration/v2_progress.py (83%) delete mode 100644 src/app/modules/agent_api/__init__.py create mode 100644 src/app/modules/api/__init__.py rename src/app/modules/{agent_api => api}/application/request_service.py (72%) rename src/app/modules/{agent_api => api}/application/session_service.py (85%) rename src/app/modules/{agent_api => api}/application/stream_service.py (83%) rename src/app/modules/{agent_api => api}/controllers/request_controller.py (94%) rename src/app/modules/{agent_api => api}/controllers/session_controller.py (94%) rename src/app/modules/{agent_api => api}/controllers/stream_controller.py (93%) rename src/app/modules/{agent_api => api}/domain/events/client_event.py (100%) rename src/app/modules/{agent_api => api}/domain/models/agent_request.py (100%) rename src/app/modules/{agent_api => api}/domain/models/agent_session.py (100%) rename src/app/modules/{agent_api => api}/infrastructure/ids/request_id_factory.py (100%) rename src/app/modules/{agent_api => api}/infrastructure/ids/session_id_factory.py (100%) rename src/app/modules/{agent_api => api}/infrastructure/logging/request_trace_logger.py (77%) rename src/app/modules/{agent_api => api}/infrastructure/logging/trace_file_path_builder.py (100%) rename src/app/modules/{agent_api => api}/infrastructure/logging/trace_markdown_writer.py (100%) rename src/app/modules/{agent_api => api}/infrastructure/stores/in_memory_request_store.py (86%) rename src/app/modules/{agent_api => api}/infrastructure/stores/in_memory_session_store.py (86%) rename src/app/modules/{agent_api => api}/infrastructure/streaming/replay_buffer.py (88%) rename src/app/modules/{agent_api => api}/infrastructure/streaming/sse_encoder.py (87%) rename src/app/modules/{agent_api => api}/infrastructure/streaming/sse_event_channel.py (90%) rename src/app/modules/{agent_api => api}/module.py (50%) rename src/app/modules/{agent_api => api}/public_router.py (87%) delete mode 100644 src/app/modules/chat/README.md delete mode 100644 src/app/modules/chat/__init__.py delete mode 100644 src/app/modules/chat/dialog_store.py delete mode 100644 src/app/modules/chat/direct_service.py delete mode 100644 src/app/modules/chat/evidence_gate.py delete mode 100644 src/app/modules/chat/module.py delete mode 100644 src/app/modules/chat/repository.py delete mode 100644 src/app/modules/chat/service.py delete mode 100644 src/app/modules/chat/session_resolver.py delete mode 100644 src/app/modules/chat/task_store.py delete mode 100644 src/app/modules/orchestration/__init__.py delete mode 100644 src/app/modules/orchestration/adapters/intent_router_adapter.py delete mode 100644 src/app/modules/orchestration/adapters/llm_chat_adapter.py delete mode 100644 src/app/modules/orchestration/context/execution_context.py delete mode 100644 tests/unit_tests/chat/test_chat_api_simple_code_explain.py delete mode 100644 tests/unit_tests/chat/test_direct_service.py diff --git a/_process.zip b/_process.zip new file mode 100644 index 0000000000000000000000000000000000000000..8577803259644e01462a652e90d592dc0eb0b8d0 GIT binary patch literal 17467 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 literal 0 HcmV?d00001 diff --git a/src/app/main.py b/src/app/main.py index 5155da3..30819d9 100644 --- a/src/app/main.py +++ b/src/app/main.py @@ -27,7 +27,7 @@ def create_app() -> FastAPI: allow_headers=["*"], ) - app.include_router(modules.agent_api.public_router()) + app.include_router(modules.api.public_router()) app.include_router(modules.rag.public_router()) app.include_router(modules.rag.internal_router()) app.include_router(modules.rag_repo.internal_router()) diff --git a/src/app/modules/agent/llm/service.py b/src/app/modules/agent/llm/service.py index deabf69..82e83f5 100644 --- a/src/app/modules/agent/llm/service.py +++ b/src/app/modules/agent/llm/service.py @@ -1,5 +1,6 @@ import logging +from app.modules.agent.observability.module_trace import ModuleTrace from app.modules.agent.llm.prompt_loader import PromptLoader from app.modules.shared.gigachat.client import GigaChatClient @@ -18,10 +19,26 @@ class AgentLlmService: self._client = client self._prompts = prompts - def generate(self, prompt_name: str, user_input: str, *, log_context: str | None = None) -> str: - system_prompt = self._prompts.load(prompt_name) - if not system_prompt: - system_prompt = "You are a helpful assistant." + def build_request(self, prompt_name: str, user_input: str, *, log_context: str | None = None) -> dict[str, str]: + system_prompt = self._prompts.load(prompt_name) or "You are a helpful assistant." + return { + "prompt_name": prompt_name, + "system_prompt": system_prompt, + "user_prompt": user_input, + "log_context": log_context or "", + } + + def generate( + self, + prompt_name: str, + user_input: str, + *, + log_context: str | None = None, + trace: ModuleTrace | None = None, + ) -> str: + request = self.build_request(prompt_name, user_input, log_context=log_context) + if trace is not None: + trace.log("request", request) if log_context: LOGGER.warning( "graph llm input: context=%s prompt=%s user_input=%s", @@ -29,7 +46,12 @@ class AgentLlmService: prompt_name, _truncate_for_log(user_input), ) - output = self._client.complete(system_prompt=system_prompt, user_prompt=user_input) + output = self._client.complete( + system_prompt=request["system_prompt"], + user_prompt=request["user_prompt"], + ) + if trace is not None: + trace.log("response", {"text": output}) if log_context: LOGGER.warning( "graph llm output: context=%s prompt=%s output=%s", diff --git a/src/app/modules/agent/observability/module_trace.py b/src/app/modules/agent/observability/module_trace.py new file mode 100644 index 0000000..35eb2c5 --- /dev/null +++ b/src/app/modules/agent/observability/module_trace.py @@ -0,0 +1,27 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Protocol + + +class ModuleTraceLogger(Protocol): + def log_module(self, request_id: str, module: str, title: str, payload: dict | None = None) -> None: ... + + +@dataclass(frozen=True, slots=True) +class ModuleTrace: + request_id: str + module: str + logger: ModuleTraceLogger + + def log(self, title: str, payload: dict | None = None) -> None: + self.logger.log_module(self.request_id, self.module, title, payload or {}) + + +@dataclass(frozen=True, slots=True) +class RequestTraceContext: + request_id: str + logger: ModuleTraceLogger + + def module(self, name: str) -> ModuleTrace: + return ModuleTrace(request_id=self.request_id, module=name, logger=self.logger) diff --git a/src/app/modules/agent/orchestration/__init__.py b/src/app/modules/agent/orchestration/__init__.py new file mode 100644 index 0000000..c9c2ef6 --- /dev/null +++ b/src/app/modules/agent/orchestration/__init__.py @@ -0,0 +1 @@ +__all__: list[str] = [] diff --git a/src/app/modules/agent/orchestration/adapters/intent_router_adapter.py b/src/app/modules/agent/orchestration/adapters/intent_router_adapter.py new file mode 100644 index 0000000..f75cf7a --- /dev/null +++ b/src/app/modules/agent/orchestration/adapters/intent_router_adapter.py @@ -0,0 +1,24 @@ +from __future__ import annotations + +from app.modules.agent.observability.module_trace import ModuleTrace +from app.modules.agent.intent_router_v2 import IntentRouterV2 + + +class IntentRouterAdapter: + def __init__(self, router: IntentRouterV2) -> None: + self._router = router + + def route(self, user_query: str, conversation_state, repo_context, trace: ModuleTrace | None = None): + if trace is not None: + trace.log("started", {"question": user_query}) + result = self._router.route(user_query, conversation_state, repo_context) + if trace is not None: + trace.log( + "completed", + { + "intent": result.intent, + "sub_intent": result.query_plan.sub_intent, + "matched_intent_source": result.matched_intent_source, + }, + ) + return result diff --git a/src/app/modules/agent/orchestration/adapters/llm_chat_adapter.py b/src/app/modules/agent/orchestration/adapters/llm_chat_adapter.py new file mode 100644 index 0000000..32de6ab --- /dev/null +++ b/src/app/modules/agent/orchestration/adapters/llm_chat_adapter.py @@ -0,0 +1,32 @@ +from __future__ import annotations + +import asyncio + +from app.modules.agent.observability.module_trace import ModuleTrace +from app.modules.agent.llm.service import AgentLlmService + + +class LlmChatAdapter: + def __init__(self, llm: AgentLlmService, prompt_name: str = "simple_llm_answer") -> None: + self._llm = llm + self._prompt_name = prompt_name + + @property + def prompt_name(self) -> str: + return self._prompt_name + + def build_request(self, message: str, request_id: str) -> dict[str, str]: + return self._llm.build_request( + self._prompt_name, + message, + log_context=f"agent:{request_id}", + ) + + async def generate(self, message: str, request_id: str, trace: ModuleTrace | None = None) -> str: + return await asyncio.to_thread( + self._llm.generate, + self._prompt_name, + message, + log_context=f"agent:{request_id}", + trace=trace, + ) diff --git a/src/app/modules/orchestration/adapters/task_runtime_adapter.py b/src/app/modules/agent/orchestration/adapters/task_runtime_adapter.py similarity index 90% rename from src/app/modules/orchestration/adapters/task_runtime_adapter.py rename to src/app/modules/agent/orchestration/adapters/task_runtime_adapter.py index 0437b5d..42ac762 100644 --- a/src/app/modules/orchestration/adapters/task_runtime_adapter.py +++ b/src/app/modules/agent/orchestration/adapters/task_runtime_adapter.py @@ -4,8 +4,8 @@ from types import SimpleNamespace from app.core.exceptions import AppError from app.modules.agent.task_runtime.facade import AgentTaskRuntimeFacade -from app.modules.agent_api.domain.models.agent_session import AgentSession -from app.modules.orchestration.context.execution_context import ExecutionContext +from app.modules.api.domain.models.agent_session import AgentSession +from app.modules.agent.orchestration.context.execution_context import ExecutionContext from app.schemas.common import ModuleName diff --git a/src/app/modules/agent/orchestration/context/execution_context.py b/src/app/modules/agent/orchestration/context/execution_context.py new file mode 100644 index 0000000..9db381f --- /dev/null +++ b/src/app/modules/agent/orchestration/context/execution_context.py @@ -0,0 +1,22 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + +from app.modules.api.domain.models.agent_request import AgentRequest +from app.modules.api.domain.models.agent_session import AgentSession +from app.modules.api.infrastructure.logging.request_trace_logger import RequestTraceLogger +from app.modules.agent.observability.module_trace import RequestTraceContext +from app.modules.agent.orchestration.messaging.client_message_publisher import ClientMessagePublisher + + +@dataclass(slots=True) +class ExecutionContext: + request: AgentRequest + session: AgentSession + publisher: ClientMessagePublisher + trace_logger: RequestTraceLogger + trace: RequestTraceContext + task_context: Any = None + route_result: Any = None + workflow_result: Any = None diff --git a/src/app/modules/orchestration/facade.py b/src/app/modules/agent/orchestration/facade.py similarity index 74% rename from src/app/modules/orchestration/facade.py rename to src/app/modules/agent/orchestration/facade.py index 85b807c..25bf6d8 100644 --- a/src/app/modules/orchestration/facade.py +++ b/src/app/modules/agent/orchestration/facade.py @@ -3,14 +3,15 @@ from __future__ import annotations from datetime import datetime, timezone from app.core.exceptions import AppError -from app.modules.agent_api.domain.models.agent_request import AgentRequest -from app.modules.agent_api.domain.models.agent_session import AgentSession -from app.modules.agent_api.infrastructure.logging.request_trace_logger import RequestTraceLogger -from app.modules.agent_api.infrastructure.stores.in_memory_request_store import InMemoryRequestStore -from app.modules.orchestration.context.execution_context import ExecutionContext -from app.modules.orchestration.messaging.client_message_publisher import ClientMessagePublisher -from app.modules.orchestration.processes.registry import ProcessRegistry -from app.modules.orchestration.runtime.process_runner import ProcessRunner +from app.modules.api.domain.models.agent_request import AgentRequest +from app.modules.api.domain.models.agent_session import AgentSession +from app.modules.api.infrastructure.logging.request_trace_logger import RequestTraceLogger +from app.modules.agent.observability.module_trace import RequestTraceContext +from app.modules.api.infrastructure.stores.in_memory_request_store import InMemoryRequestStore +from app.modules.agent.orchestration.context.execution_context import ExecutionContext +from app.modules.agent.orchestration.messaging.client_message_publisher import ClientMessagePublisher +from app.modules.agent.orchestration.processes.registry import ProcessRegistry +from app.modules.agent.orchestration.runtime.process_runner import ProcessRunner from app.schemas.common import ErrorPayload, ModuleName from app.schemas.orchestration import RequestExecutionStatus @@ -43,6 +44,7 @@ class OrchestrationFacade: session=session, publisher=self._publisher, trace_logger=self._trace_logger, + trace=RequestTraceContext(request_id=request.request_id, logger=self._trace_logger), ) await self._process_runner.run(context, process.steps()) request.status = RequestExecutionStatus.DONE @@ -56,7 +58,7 @@ class OrchestrationFacade: request.error = ErrorPayload(code=exc.code, desc=exc.desc, module=exc.module) else: request.error = ErrorPayload( - code="agent_api_runtime_error", + code="api_runtime_error", desc="Agent request failed unexpectedly.", module=ModuleName.AGENT, ) diff --git a/src/app/modules/orchestration/messaging/client_message_publisher.py b/src/app/modules/agent/orchestration/messaging/client_message_publisher.py similarity index 70% rename from src/app/modules/orchestration/messaging/client_message_publisher.py rename to src/app/modules/agent/orchestration/messaging/client_message_publisher.py index 5c88a57..cdfb20f 100644 --- a/src/app/modules/orchestration/messaging/client_message_publisher.py +++ b/src/app/modules/agent/orchestration/messaging/client_message_publisher.py @@ -1,9 +1,9 @@ from __future__ import annotations -from app.modules.agent_api.infrastructure.logging.request_trace_logger import RequestTraceLogger -from app.modules.agent_api.infrastructure.streaming.sse_event_channel import SseEventChannel -from app.modules.orchestration.messaging.status_message_factory import StatusMessageFactory -from app.modules.orchestration.messaging.user_message_factory import UserMessageFactory +from app.modules.api.infrastructure.logging.request_trace_logger import RequestTraceLogger +from app.modules.api.infrastructure.streaming.sse_event_channel import SseEventChannel +from app.modules.agent.orchestration.messaging.status_message_factory import StatusMessageFactory +from app.modules.agent.orchestration.messaging.user_message_factory import UserMessageFactory class ClientMessagePublisher: diff --git a/src/app/modules/orchestration/messaging/status_message_factory.py b/src/app/modules/agent/orchestration/messaging/status_message_factory.py similarity index 84% rename from src/app/modules/orchestration/messaging/status_message_factory.py rename to src/app/modules/agent/orchestration/messaging/status_message_factory.py index 0d6aa9e..b11befd 100644 --- a/src/app/modules/orchestration/messaging/status_message_factory.py +++ b/src/app/modules/agent/orchestration/messaging/status_message_factory.py @@ -1,6 +1,6 @@ from __future__ import annotations -from app.modules.agent_api.domain.events.client_event import ClientEventRecord +from app.modules.api.domain.events.client_event import ClientEventRecord from app.schemas.client_events import ClientEventType diff --git a/src/app/modules/orchestration/messaging/user_message_factory.py b/src/app/modules/agent/orchestration/messaging/user_message_factory.py similarity index 84% rename from src/app/modules/orchestration/messaging/user_message_factory.py rename to src/app/modules/agent/orchestration/messaging/user_message_factory.py index 2ded13e..aa24fb5 100644 --- a/src/app/modules/orchestration/messaging/user_message_factory.py +++ b/src/app/modules/agent/orchestration/messaging/user_message_factory.py @@ -1,6 +1,6 @@ from __future__ import annotations -from app.modules.agent_api.domain.events.client_event import ClientEventRecord +from app.modules.api.domain.events.client_event import ClientEventRecord from app.schemas.client_events import ClientEventType diff --git a/src/app/modules/orchestration/processes/registry.py b/src/app/modules/agent/orchestration/processes/registry.py similarity index 69% rename from src/app/modules/orchestration/processes/registry.py rename to src/app/modules/agent/orchestration/processes/registry.py index df55209..6da53ba 100644 --- a/src/app/modules/orchestration/processes/registry.py +++ b/src/app/modules/agent/orchestration/processes/registry.py @@ -1,7 +1,7 @@ from __future__ import annotations -from app.modules.orchestration.processes.v1.process import V1Process -from app.modules.orchestration.processes.v2.process import V2Process +from app.modules.agent.orchestration.processes.v1.process import V1Process +from app.modules.agent.orchestration.processes.v2.process import V2Process class ProcessRegistry: diff --git a/src/app/modules/orchestration/processes/v1/process.py b/src/app/modules/agent/orchestration/processes/v1/process.py similarity index 100% rename from src/app/modules/orchestration/processes/v1/process.py rename to src/app/modules/agent/orchestration/processes/v1/process.py diff --git a/src/app/modules/agent/orchestration/processes/v1/prompt_payload_builder.py b/src/app/modules/agent/orchestration/processes/v1/prompt_payload_builder.py new file mode 100644 index 0000000..9faf40e --- /dev/null +++ b/src/app/modules/agent/orchestration/processes/v1/prompt_payload_builder.py @@ -0,0 +1,11 @@ +from __future__ import annotations + +import json + +from app.modules.agent.orchestration.context.execution_context import ExecutionContext + + +class V1PromptPayloadBuilder: + def build(self, context: ExecutionContext) -> str: + payload = {"question": context.request.message} + return json.dumps(payload, ensure_ascii=False, indent=2) diff --git a/src/app/modules/agent/orchestration/processes/v1/prompts.yml b/src/app/modules/agent/orchestration/processes/v1/prompts.yml new file mode 100644 index 0000000..243f86c --- /dev/null +++ b/src/app/modules/agent/orchestration/processes/v1/prompts.yml @@ -0,0 +1,12 @@ +prompts: + simple_llm_answer: | + Ты полезный AI-ассистент проекта. + + На вход приходит JSON с полем: + - question + + Правила: + - Отвечай как персонаж мемов из дагестана + - Если вопрос неясный, аккуратно укажи, чего не хватает + - Не выдумывай несуществующие факты о проекте + - Формулируй ответ как обычное сообщение пользователю diff --git a/src/app/modules/agent/orchestration/processes/v1/simple_llm_workflow.py b/src/app/modules/agent/orchestration/processes/v1/simple_llm_workflow.py new file mode 100644 index 0000000..62ef2bf --- /dev/null +++ b/src/app/modules/agent/orchestration/processes/v1/simple_llm_workflow.py @@ -0,0 +1,42 @@ +from __future__ import annotations + +from app.modules.agent.orchestration.adapters.llm_chat_adapter import LlmChatAdapter +from app.modules.agent.orchestration.context.execution_context import ExecutionContext +from app.modules.agent.orchestration.processes.v1.prompt_payload_builder import V1PromptPayloadBuilder + + +class SimpleLlmWorkflow: + workflow_id = "simple_llm" + + def __init__( + self, + llm: LlmChatAdapter, + prompt_payload_builder: V1PromptPayloadBuilder | None = None, + ) -> None: + self._llm = llm + self._payload_builder = prompt_payload_builder or V1PromptPayloadBuilder() + + async def run(self, context: ExecutionContext) -> dict: + workflow_trace = context.trace.module("task_workflow") + workflow_trace.log("started", {"workflow_id": self.workflow_id}) + prompt_payload = self._payload_builder.build(context) + answer = await self._llm.generate( + prompt_payload, + context.request.request_id, + trace=context.trace.module("llm"), + ) + result = { + "workflow_id": self.workflow_id, + "prompt_name": self._llm.prompt_name, + "prompt_payload": prompt_payload, + "answer": answer, + } + workflow_trace.log( + "completed", + { + "workflow_id": self.workflow_id, + "prompt_name": self._llm.prompt_name, + "answer_length": len(answer), + }, + ) + return result diff --git a/src/app/modules/orchestration/processes/v1/steps/bootstrap_step.py b/src/app/modules/agent/orchestration/processes/v1/steps/bootstrap_step.py similarity index 63% rename from src/app/modules/orchestration/processes/v1/steps/bootstrap_step.py rename to src/app/modules/agent/orchestration/processes/v1/steps/bootstrap_step.py index 5813686..34e344d 100644 --- a/src/app/modules/orchestration/processes/v1/steps/bootstrap_step.py +++ b/src/app/modules/agent/orchestration/processes/v1/steps/bootstrap_step.py @@ -1,11 +1,14 @@ from __future__ import annotations -from app.modules.orchestration.context.execution_context import ExecutionContext +from app.modules.agent.orchestration.context.execution_context import ExecutionContext class BootstrapStep: async def run(self, context: ExecutionContext) -> None: - context.trace_logger.log_step(context.request.request_id, "bootstrap", "started") + context.trace.module("orchestrator").log( + "bootstrap", + {"status": "started", "process_version": context.request.process_version}, + ) await context.publisher.publish_status( context.request.request_id, "orchestrator", @@ -17,4 +20,4 @@ class BootstrapStep: "Запускаю процесс обработки v1.", {"process_version": context.request.process_version}, ) - context.trace_logger.log_step(context.request.request_id, "bootstrap", "completed") + context.trace.module("orchestrator").log("bootstrap", {"status": "completed"}) diff --git a/src/app/modules/agent/orchestration/processes/v1/steps/execute_llm_workflow_step.py b/src/app/modules/agent/orchestration/processes/v1/steps/execute_llm_workflow_step.py new file mode 100644 index 0000000..61d7d1a --- /dev/null +++ b/src/app/modules/agent/orchestration/processes/v1/steps/execute_llm_workflow_step.py @@ -0,0 +1,35 @@ +from __future__ import annotations + +from app.modules.agent.orchestration.context.execution_context import ExecutionContext +from app.modules.agent.orchestration.processes.v1.simple_llm_workflow import SimpleLlmWorkflow + + +class ExecuteLlmWorkflowStep: + def __init__(self, workflow: SimpleLlmWorkflow) -> None: + self._workflow = workflow + + async def run(self, context: ExecutionContext) -> None: + request = context.request + await context.publisher.publish_status( + request.request_id, + "task_workflow", + f"Запускаю workflow {self._workflow.workflow_id}.", + ) + await context.publisher.publish_status( + request.request_id, + "prompt_builder", + "Формирую prompt payload для LLM.", + ) + result = await self._workflow.run(context) + request.answer = str(result.get("answer") or "") + context.workflow_result = result + await context.publisher.publish_status( + request.request_id, + "llm_process", + "Ответ от LLM получен.", + { + "workflow_id": result.get("workflow_id"), + "prompt_name": result.get("prompt_name"), + "answer_length": len(request.answer), + }, + ) diff --git a/src/app/modules/orchestration/processes/v1/steps/finalize_step.py b/src/app/modules/agent/orchestration/processes/v1/steps/finalize_step.py similarity index 65% rename from src/app/modules/orchestration/processes/v1/steps/finalize_step.py rename to src/app/modules/agent/orchestration/processes/v1/steps/finalize_step.py index 789d973..68afc79 100644 --- a/src/app/modules/orchestration/processes/v1/steps/finalize_step.py +++ b/src/app/modules/agent/orchestration/processes/v1/steps/finalize_step.py @@ -1,12 +1,12 @@ from __future__ import annotations -from app.modules.orchestration.context.execution_context import ExecutionContext +from app.modules.agent.orchestration.context.execution_context import ExecutionContext class FinalizeStep: async def run(self, context: ExecutionContext) -> None: request = context.request - context.trace_logger.log_step(request.request_id, "finalize", "started") + context.trace.module("orchestrator").log("finalize", {"status": "started"}) await context.publisher.publish_user( request.request_id, "agent", @@ -17,4 +17,4 @@ class FinalizeStep: "orchestrator", "Обработка запроса завершена.", ) - context.trace_logger.log_step(request.request_id, "finalize", "completed") + context.trace.module("orchestrator").log("finalize", {"status": "completed"}) diff --git a/src/app/modules/orchestration/processes/v1/steps/run_llm_step.py b/src/app/modules/agent/orchestration/processes/v1/steps/run_llm_step.py similarity index 83% rename from src/app/modules/orchestration/processes/v1/steps/run_llm_step.py rename to src/app/modules/agent/orchestration/processes/v1/steps/run_llm_step.py index ef362cb..42c3c74 100644 --- a/src/app/modules/orchestration/processes/v1/steps/run_llm_step.py +++ b/src/app/modules/agent/orchestration/processes/v1/steps/run_llm_step.py @@ -1,7 +1,7 @@ from __future__ import annotations -from app.modules.orchestration.adapters.llm_chat_adapter import LlmChatAdapter -from app.modules.orchestration.context.execution_context import ExecutionContext +from app.modules.agent.orchestration.adapters.llm_chat_adapter import LlmChatAdapter +from app.modules.agent.orchestration.context.execution_context import ExecutionContext class RunLlmStep: diff --git a/src/app/modules/orchestration/processes/v2/README.md b/src/app/modules/agent/orchestration/processes/v2/README.md similarity index 98% rename from src/app/modules/orchestration/processes/v2/README.md rename to src/app/modules/agent/orchestration/processes/v2/README.md index 970a895..0a7d516 100644 --- a/src/app/modules/orchestration/processes/v2/README.md +++ b/src/app/modules/agent/orchestration/processes/v2/README.md @@ -163,7 +163,7 @@ Typical sequence: ## Trace Logging Per-request trace files are written by: -[request_trace_logger.py](/Users/alex/Dev_projects_v2/ai driven app process/v2/agent/src/app/modules/agent_api/infrastructure/logging/request_trace_logger.py) +[request_trace_logger.py](/Users/alex/Dev_projects_v2/ai driven app process/v2/agent/src/app/modules/api/infrastructure/logging/request_trace_logger.py) Location: [runtime_traces/agent_requests](/Users/alex/Dev_projects_v2/ai driven app process/v2/agent/runtime_traces/agent_requests) diff --git a/src/app/modules/orchestration/processes/v2/process.py b/src/app/modules/agent/orchestration/processes/v2/process.py similarity index 100% rename from src/app/modules/orchestration/processes/v2/process.py rename to src/app/modules/agent/orchestration/processes/v2/process.py diff --git a/src/app/modules/agent/orchestration/processes/v2/prompt_payload_builder.py b/src/app/modules/agent/orchestration/processes/v2/prompt_payload_builder.py new file mode 100644 index 0000000..223e3b8 --- /dev/null +++ b/src/app/modules/agent/orchestration/processes/v2/prompt_payload_builder.py @@ -0,0 +1,35 @@ +from __future__ import annotations + +import json + +from app.modules.agent.runtime.docs_qa_pipeline.models import DocsEvidenceBundle, OpenAPIResult + + +class V2PromptPayloadBuilder: + def build( + self, + *, + question: str, + intent: str, + sub_intent: str, + evidence_bundle: DocsEvidenceBundle, + api_contract: OpenAPIResult | None = None, + ) -> str: + payload = { + "question": question, + "documents": list(evidence_bundle.documents), + "facts": list(evidence_bundle.facts), + "entities": list(evidence_bundle.entities), + "workflows": list(evidence_bundle.workflows), + "relations": list(evidence_bundle.relations), + "chunks": list(evidence_bundle.chunks), + } + if api_contract is not None: + payload["api_contract"] = { + "path": api_contract.path, + "method": api_contract.method, + "request_schema": api_contract.request_schema, + "response_schema": api_contract.response_schema, + "diagnostics": dict(api_contract.diagnostics), + } + return json.dumps(payload, ensure_ascii=False, indent=2) diff --git a/src/app/modules/agent/orchestration/processes/v2/prompt_selector.py b/src/app/modules/agent/orchestration/processes/v2/prompt_selector.py new file mode 100644 index 0000000..16714f6 --- /dev/null +++ b/src/app/modules/agent/orchestration/processes/v2/prompt_selector.py @@ -0,0 +1,18 @@ +"""Выбор prompt для process-local workflow v2.""" + +from __future__ import annotations + + +class V2PromptSelector: + _DOCS_INTENT_PROMPTS = { + "DOCUMENTATION_EXPLAIN": "documentation_explain_answer", + "GENERAL_QA": "general_qa_answer", + } + + def select(self, *, intent: str = "DOCUMENTATION_EXPLAIN", sub_intent: str, answer_mode: str) -> str: + intent_key = (intent or "DOCUMENTATION_EXPLAIN").upper() + if intent_key in {"OPENAPI_GENERATION", "OPENAPI_FROM_DOCUMENTATION"}: + if sub_intent.upper() == "OPENAPI_FRAGMENT_GENERATE": + return "openapi_fragment_answer" + return "openapi_answer" + return self._DOCS_INTENT_PROMPTS.get(intent_key, "documentation_explain_answer") diff --git a/src/app/modules/agent/orchestration/processes/v2/prompts.yml b/src/app/modules/agent/orchestration/processes/v2/prompts.yml new file mode 100644 index 0000000..5142637 --- /dev/null +++ b/src/app/modules/agent/orchestration/processes/v2/prompts.yml @@ -0,0 +1,96 @@ +prompts: + documentation_explain_answer: | + Ты объясняешь документацию системы. + + На вход приходит JSON с полями: + - question + - documents + - facts + - entities + - workflows + - relations + - chunks + + Правила: + - Используй только предоставленные факты + - Не додумывай + - Если данных недостаточно, скажи это явно + - Объясняй структурировано + + Формат ответа: + 1. Краткое описание + 2. Основные элементы + 3. Как это работает + 4. Связи с другими частями системы (если есть) + general_qa_answer: | + Ты отвечаешь на общий вопрос по документации проекта. + + На вход приходит JSON с полями: + - question + - documents + - facts + - entities + - workflows + - relations + - chunks + + Правила: + - Используй только предоставленные документы и факты + - Не додумывай отсутствующие детали + - Если данных недостаточно, скажи это прямо + - Дай короткий понятный ответ без лишней структуры + openapi_answer: | + Ты генерируешь OpenAPI спецификацию по документации API. + + На вход приходит JSON с полями: + - question + - documents + - facts + - entities + - workflows + - relations + - chunks + - api_contract + + Правила: + - Используй только данные из документации + - Не придумывай поля + - Если данных нет, не заполняй + - Верни ТОЛЬКО YAML без пояснений + + Формат: + paths: + /path: + method: + summary: ... + requestBody: + responses: + openapi_fragment_answer: | + Ты генерируешь часть OpenAPI schema по документации API. + + На вход приходит JSON с полями: + - question + - documents + - facts + - entities + - workflows + - relations + - chunks + - api_contract + + Правила: + - Используй только данные из документации + - Не придумывай отсутствующие поля + - Верни только содержимое нужного фрагмента + fallback_answer: | + Ты формируешь безопасный fallback-ответ. + + На вход приходит JSON с полями: + - question + - attachments + - confluence_urls + + Правила: + - Если специализированный workflow не выбран, честно скажи об ограничении + - Используй только данные из payload + - Не выдумывай детали проекта diff --git a/src/app/modules/orchestration/processes/v2/steps/execute_documentation_workflow_step.py b/src/app/modules/agent/orchestration/processes/v2/steps/execute_documentation_workflow_step.py similarity index 58% rename from src/app/modules/orchestration/processes/v2/steps/execute_documentation_workflow_step.py rename to src/app/modules/agent/orchestration/processes/v2/steps/execute_documentation_workflow_step.py index 4ccab36..1268038 100644 --- a/src/app/modules/orchestration/processes/v2/steps/execute_documentation_workflow_step.py +++ b/src/app/modules/agent/orchestration/processes/v2/steps/execute_documentation_workflow_step.py @@ -1,6 +1,6 @@ from __future__ import annotations -from app.modules.orchestration.processes.v2.steps.workflow_step_base import WorkflowStepBase +from app.modules.agent.orchestration.processes.v2.steps.workflow_step_base import WorkflowStepBase class ExecuteDocumentationWorkflowStep(WorkflowStepBase): diff --git a/src/app/modules/orchestration/processes/v2/steps/execute_fallback_workflow_step.py b/src/app/modules/agent/orchestration/processes/v2/steps/execute_fallback_workflow_step.py similarity index 70% rename from src/app/modules/orchestration/processes/v2/steps/execute_fallback_workflow_step.py rename to src/app/modules/agent/orchestration/processes/v2/steps/execute_fallback_workflow_step.py index 14baeaa..6bf3c1f 100644 --- a/src/app/modules/orchestration/processes/v2/steps/execute_fallback_workflow_step.py +++ b/src/app/modules/agent/orchestration/processes/v2/steps/execute_fallback_workflow_step.py @@ -1,7 +1,7 @@ from __future__ import annotations -from app.modules.orchestration.context.execution_context import ExecutionContext -from app.modules.orchestration.processes.v2.steps.workflow_step_base import WorkflowStepBase +from app.modules.agent.orchestration.context.execution_context import ExecutionContext +from app.modules.agent.orchestration.processes.v2.steps.workflow_step_base import WorkflowStepBase class ExecuteFallbackWorkflowStep(WorkflowStepBase): diff --git a/src/app/modules/orchestration/processes/v2/steps/execute_general_qa_workflow_step.py b/src/app/modules/agent/orchestration/processes/v2/steps/execute_general_qa_workflow_step.py similarity index 55% rename from src/app/modules/orchestration/processes/v2/steps/execute_general_qa_workflow_step.py rename to src/app/modules/agent/orchestration/processes/v2/steps/execute_general_qa_workflow_step.py index 4f42118..f7893fd 100644 --- a/src/app/modules/orchestration/processes/v2/steps/execute_general_qa_workflow_step.py +++ b/src/app/modules/agent/orchestration/processes/v2/steps/execute_general_qa_workflow_step.py @@ -1,6 +1,6 @@ from __future__ import annotations -from app.modules.orchestration.processes.v2.steps.workflow_step_base import WorkflowStepBase +from app.modules.agent.orchestration.processes.v2.steps.workflow_step_base import WorkflowStepBase class ExecuteGeneralQaWorkflowStep(WorkflowStepBase): diff --git a/src/app/modules/orchestration/processes/v2/steps/execute_openapi_workflow_step.py b/src/app/modules/agent/orchestration/processes/v2/steps/execute_openapi_workflow_step.py similarity index 56% rename from src/app/modules/orchestration/processes/v2/steps/execute_openapi_workflow_step.py rename to src/app/modules/agent/orchestration/processes/v2/steps/execute_openapi_workflow_step.py index 0b35a01..755f6c1 100644 --- a/src/app/modules/orchestration/processes/v2/steps/execute_openapi_workflow_step.py +++ b/src/app/modules/agent/orchestration/processes/v2/steps/execute_openapi_workflow_step.py @@ -1,6 +1,6 @@ from __future__ import annotations -from app.modules.orchestration.processes.v2.steps.workflow_step_base import WorkflowStepBase +from app.modules.agent.orchestration.processes.v2.steps.workflow_step_base import WorkflowStepBase class ExecuteOpenApiWorkflowStep(WorkflowStepBase): diff --git a/src/app/modules/orchestration/processes/v2/steps/route_intent_step.py b/src/app/modules/agent/orchestration/processes/v2/steps/route_intent_step.py similarity index 78% rename from src/app/modules/orchestration/processes/v2/steps/route_intent_step.py rename to src/app/modules/agent/orchestration/processes/v2/steps/route_intent_step.py index cfc282b..e02990b 100644 --- a/src/app/modules/orchestration/processes/v2/steps/route_intent_step.py +++ b/src/app/modules/agent/orchestration/processes/v2/steps/route_intent_step.py @@ -5,9 +5,9 @@ import asyncio from app.core.exceptions import AppError from app.modules.agent.task_runtime.context import TaskRuntimeContextBuilder from app.modules.agent.task_runtime.enrichment import ContextEnrichmentService -from app.modules.orchestration.adapters.intent_router_adapter import IntentRouterAdapter -from app.modules.orchestration.context.execution_context import ExecutionContext -from app.modules.orchestration.v2_progress import build_progress_callback +from app.modules.agent.orchestration.adapters.intent_router_adapter import IntentRouterAdapter +from app.modules.agent.orchestration.context.execution_context import ExecutionContext +from app.modules.agent.orchestration.v2_progress import build_progress_callback from app.schemas.common import ModuleName @@ -41,10 +41,10 @@ class RouteIntentStep: attachments=[], files=[], progress_cb=build_progress_callback(loop, context.publisher, request.request_id), + trace=context.trace, ) task_context.enriched_context = self._enrichment.enrich(task_context) context.task_context = task_context - context.trace_logger.log_step(request.request_id, "intent_router", "started") await context.publisher.publish_status( request.request_id, "intent_router", @@ -55,6 +55,7 @@ class RouteIntentStep: request.message, task_context.conversation_state, task_context.repo_context, + context.trace.module("intent_router"), ) task_context.route_result = route_result context.route_result = route_result @@ -68,13 +69,3 @@ class RouteIntentStep: "matched_intent_source": route_result.matched_intent_source, }, ) - context.trace_logger.log_step( - request.request_id, - "intent_router", - "completed", - { - "intent": route_result.intent, - "sub_intent": route_result.query_plan.sub_intent, - "matched_intent_source": route_result.matched_intent_source, - }, - ) diff --git a/src/app/modules/orchestration/processes/v2/steps/run_task_workflow_step.py b/src/app/modules/agent/orchestration/processes/v2/steps/run_task_workflow_step.py similarity index 84% rename from src/app/modules/orchestration/processes/v2/steps/run_task_workflow_step.py rename to src/app/modules/agent/orchestration/processes/v2/steps/run_task_workflow_step.py index 2b47b0e..f8d0a1b 100644 --- a/src/app/modules/orchestration/processes/v2/steps/run_task_workflow_step.py +++ b/src/app/modules/agent/orchestration/processes/v2/steps/run_task_workflow_step.py @@ -1,7 +1,7 @@ from __future__ import annotations -from app.modules.orchestration.adapters.task_runtime_adapter import TaskRuntimeAdapter -from app.modules.orchestration.context.execution_context import ExecutionContext +from app.modules.agent.orchestration.adapters.task_runtime_adapter import TaskRuntimeAdapter +from app.modules.agent.orchestration.context.execution_context import ExecutionContext class RunTaskWorkflowStep: diff --git a/src/app/modules/orchestration/processes/v2/steps/workflow_step_base.py b/src/app/modules/agent/orchestration/processes/v2/steps/workflow_step_base.py similarity index 88% rename from src/app/modules/orchestration/processes/v2/steps/workflow_step_base.py rename to src/app/modules/agent/orchestration/processes/v2/steps/workflow_step_base.py index ab24088..bbd26ce 100644 --- a/src/app/modules/orchestration/processes/v2/steps/workflow_step_base.py +++ b/src/app/modules/agent/orchestration/processes/v2/steps/workflow_step_base.py @@ -3,7 +3,7 @@ from __future__ import annotations import asyncio from typing import Any -from app.modules.orchestration.context.execution_context import ExecutionContext +from app.modules.agent.orchestration.context.execution_context import ExecutionContext class WorkflowStepBase: @@ -20,7 +20,6 @@ class WorkflowStepBase: async def run(self, context: ExecutionContext) -> None: request = context.request task_context = context.task_context - context.trace_logger.log_step(request.request_id, self._step_name, "started") await context.publisher.publish_status( request.request_id, "task_workflow", @@ -32,12 +31,6 @@ class WorkflowStepBase: request.answer = result.answer or "" diagnostics = dict(result.meta.get("diagnostics") or {}) await self._publish_diagnostics(context, diagnostics, result) - context.trace_logger.log_step( - request.request_id, - self._step_name, - "completed", - {"workflow_id": self._workflow.workflow_id, "meta": result.meta}, - ) async def _publish_diagnostics(self, context: ExecutionContext, diagnostics: dict[str, Any], result: Any) -> None: request_id = context.request.request_id diff --git a/src/app/modules/orchestration/runtime/process_runner.py b/src/app/modules/agent/orchestration/runtime/process_runner.py similarity index 79% rename from src/app/modules/orchestration/runtime/process_runner.py rename to src/app/modules/agent/orchestration/runtime/process_runner.py index 92cebe4..2bed04a 100644 --- a/src/app/modules/orchestration/runtime/process_runner.py +++ b/src/app/modules/agent/orchestration/runtime/process_runner.py @@ -1,6 +1,6 @@ from __future__ import annotations -from app.modules.orchestration.context.execution_context import ExecutionContext +from app.modules.agent.orchestration.context.execution_context import ExecutionContext class ProcessRunner: diff --git a/src/app/modules/orchestration/v2_progress.py b/src/app/modules/agent/orchestration/v2_progress.py similarity index 83% rename from src/app/modules/orchestration/v2_progress.py rename to src/app/modules/agent/orchestration/v2_progress.py index 2b8393e..7d1ade4 100644 --- a/src/app/modules/orchestration/v2_progress.py +++ b/src/app/modules/agent/orchestration/v2_progress.py @@ -2,7 +2,7 @@ from __future__ import annotations import asyncio -from app.modules.orchestration.messaging.client_message_publisher import ClientMessagePublisher +from app.modules.agent.orchestration.messaging.client_message_publisher import ClientMessagePublisher def build_progress_callback(loop: asyncio.AbstractEventLoop, publisher: ClientMessagePublisher, request_id: str): diff --git a/src/app/modules/agent/runtime/docs_qa_pipeline/pipeline.py b/src/app/modules/agent/runtime/docs_qa_pipeline/pipeline.py index af42996..7755a3f 100644 --- a/src/app/modules/agent/runtime/docs_qa_pipeline/pipeline.py +++ b/src/app/modules/agent/runtime/docs_qa_pipeline/pipeline.py @@ -1,11 +1,9 @@ from __future__ import annotations -import math from time import perf_counter from typing import Any from app.modules.agent.llm import AgentLlmService -from app.modules.agent.llm.prompt_loader import PromptLoader from app.modules.agent.intent_router_v2.analysis.docs_query_signals import DocsQuerySignals from app.modules.agent.runtime.docs_qa_pipeline.answer_synthesizer import DocsAnswerSynthesizer from app.modules.agent.runtime.docs_qa_pipeline.anchor_selector import DocsAnchorSelector @@ -19,6 +17,7 @@ from app.modules.agent.runtime.docs_qa_pipeline.prompt_payload_builder import Do from app.modules.agent.runtime.legacy_pipeline import RetrievalAdapter from app.modules.agent.runtime.steps.context import build_retrieval_request from app.modules.agent.runtime.steps.generation import RuntimePromptSelector +from app.modules.agent.observability.module_trace import RequestTraceContext class DocsQAPipelineRunner: @@ -60,6 +59,7 @@ class DocsQAPipelineRunner: *, conversation_state: Any = None, mode: str = "full", + trace: RequestTraceContext | None = None, ) -> DocsQAPipelineResult: timings: dict[str, int] = {} t0 = perf_counter() @@ -88,6 +88,16 @@ class DocsQAPipelineRunner: ) if request.sub_intent == "RELATED_DOCS_EXPLAIN" and not raw_rows and self._has_relation_hits(unfiltered_rows): raw_rows = unfiltered_rows + if trace is not None: + trace.module("rag_retrieval").log( + "completed", + { + "planned_layers": list(request.requested_layers), + "executed_layers": list(retrieval_report.get("executed_layers") or request.requested_layers), + "layer_diagnostics": dict(retrieval_report.get("layer_diagnostics") or {}), + "rows": len(raw_rows), + }, + ) timings["retrieval"] = _ms(t1) t2 = perf_counter() @@ -140,7 +150,14 @@ class DocsQAPipelineRunner: ) answer = openapi_result.raw_yaml if answer_mode != "degraded" else "Недостаточно contract evidence для OpenAPI." else: - answer = self._generate_openapi_answer(user_query, router_result.intent, request.sub_intent, evidence_bundle, openapi_result) + answer = self._generate_openapi_answer( + user_query, + router_result.intent, + request.sub_intent, + evidence_bundle, + openapi_result, + trace=trace, + ) output_valid, llm_details = self._openapi_postprocessor.validate( answer, require_paths=request.sub_intent != "OPENAPI_FRAGMENT_GENERATE", @@ -176,7 +193,13 @@ class DocsQAPipelineRunner: ) output_valid = answer_mode != "degraded" else: - answer = self._generate_docs_answer(user_query, router_result.intent, request.sub_intent, evidence_bundle) + answer = self._generate_docs_answer( + user_query, + router_result.intent, + request.sub_intent, + evidence_bundle, + trace=trace, + ) answer_mode, degraded_reason, answer = self._finalize_docs_answer( answer=answer, raw_rows=raw_rows, @@ -197,6 +220,16 @@ class DocsQAPipelineRunner: openapi_result=openapi_result, router_result=router_result, ) + if trace is not None: + trace.module("evidence_gate").log( + "evaluated", + { + "decision": gate_decision, + "reason": gate_decision_reason, + "missing": gate_missing_requirements, + "satisfied": gate_satisfied_requirements, + }, + ) diagnostics = self._diagnostics_builder.build( intent=router_result.intent, sub_intent=request.sub_intent, @@ -255,7 +288,7 @@ class DocsQAPipelineRunner: mode=mode, ) - def _generate_docs_answer(self, question: str, intent: str, sub_intent: str, evidence_bundle) -> str: + def _generate_docs_answer(self, question: str, intent: str, sub_intent: str, evidence_bundle, trace=None) -> str: if self._llm is None: return self._answer_synthesizer.synthesize(question, evidence_bundle) payload = self._prompt_payload_builder.build( @@ -265,9 +298,14 @@ class DocsQAPipelineRunner: evidence_bundle=evidence_bundle, ) prompt_name = self._prompt_selector.select(intent=intent, sub_intent=sub_intent, answer_mode="normal") - return self._llm.generate(prompt_name, payload, log_context="graph.project_qa.docs.answer").strip() + return self._llm.generate( + prompt_name, + payload, + log_context="graph.project_qa.docs.answer", + trace=trace.module("llm") if trace is not None else None, + ).strip() - def _generate_openapi_answer(self, question: str, intent: str, sub_intent: str, evidence_bundle, api_contract) -> str: + def _generate_openapi_answer(self, question: str, intent: str, sub_intent: str, evidence_bundle, api_contract, trace=None) -> str: if self._llm is None: return api_contract.raw_yaml payload = self._prompt_payload_builder.build( @@ -278,7 +316,12 @@ class DocsQAPipelineRunner: api_contract=api_contract, ) prompt_name = self._prompt_selector.select(intent=intent, sub_intent=sub_intent, answer_mode="normal") - return self._llm.generate(prompt_name, payload, log_context="graph.project_qa.docs.openapi").strip() + return self._llm.generate( + prompt_name, + payload, + log_context="graph.project_qa.docs.openapi", + trace=trace.module("llm") if trace is not None else None, + ).strip() def _llm_mode(self, intent: str, sub_intent: str) -> str: if sub_intent == "RELATED_DOCS_EXPLAIN": @@ -307,19 +350,14 @@ class DocsQAPipelineRunner: evidence_bundle=evidence_bundle, api_contract=api_contract, ) - system_prompt = PromptLoader().load(prompt_name) or "You are a helpful assistant." - tokens_in_estimate = max(1, int(math.ceil((len(system_prompt) + len(user_prompt)) / 4))) - return { - "prompt_name": prompt_name, - "system_prompt": system_prompt, - "user_prompt": user_prompt, - "log_context": log_context, - "prompt_stats": { - "system_chars": len(system_prompt), - "user_chars": len(user_prompt), - "tokens_in_estimate": tokens_in_estimate, - }, - } + if self._llm is None: + return { + "prompt_name": prompt_name, + "system_prompt": "You are a helpful assistant.", + "user_prompt": user_prompt, + "log_context": log_context, + } + return self._llm.build_request(prompt_name, user_prompt, log_context=log_context) def _finalize_docs_answer( self, diff --git a/src/app/modules/agent/runtime/docs_qa_pipeline/prompt_payload_builder.py b/src/app/modules/agent/runtime/docs_qa_pipeline/prompt_payload_builder.py index fc35ad7..2b85fda 100644 --- a/src/app/modules/agent/runtime/docs_qa_pipeline/prompt_payload_builder.py +++ b/src/app/modules/agent/runtime/docs_qa_pipeline/prompt_payload_builder.py @@ -17,8 +17,6 @@ class DocsPromptPayloadBuilder: ) -> str: payload = { "question": question, - "intent": intent, - "sub_intent": sub_intent, "documents": list(evidence_bundle.documents), "facts": list(evidence_bundle.facts), "entities": list(evidence_bundle.entities), diff --git a/src/app/modules/agent/task_runtime/context.py b/src/app/modules/agent/task_runtime/context.py index 6404fd5..082fe2e 100644 --- a/src/app/modules/agent/task_runtime/context.py +++ b/src/app/modules/agent/task_runtime/context.py @@ -21,6 +21,7 @@ class TaskRuntimeContextBuilder: attachments: list[dict], files: list[dict], progress_cb, + trace=None, ) -> TaskRuntimeContext: files_map = self._files_to_map(files) return TaskRuntimeContext( @@ -33,6 +34,7 @@ class TaskRuntimeContextBuilder: files=list(files or []), files_map=files_map, progress_cb=progress_cb, + trace=trace, repo_context=self._repo_context_factory.build(files_map), conversation_state=ConversationState(), ) diff --git a/src/app/modules/agent/task_runtime/models.py b/src/app/modules/agent/task_runtime/models.py index fee0ab6..e2ad651 100644 --- a/src/app/modules/agent/task_runtime/models.py +++ b/src/app/modules/agent/task_runtime/models.py @@ -20,6 +20,7 @@ class TaskRuntimeContext: files: list[dict[str, Any]] = field(default_factory=list) files_map: dict[str, dict[str, Any]] = field(default_factory=dict) progress_cb: ProgressCallback | None = None + trace: Any = None repo_context: Any = None conversation_state: Any = None route_result: Any = None diff --git a/src/app/modules/agent/task_runtime/workflows/docs_qa.py b/src/app/modules/agent/task_runtime/workflows/docs_qa.py index 14da8e8..2f55323 100644 --- a/src/app/modules/agent/task_runtime/workflows/docs_qa.py +++ b/src/app/modules/agent/task_runtime/workflows/docs_qa.py @@ -13,11 +13,17 @@ class DocsQaWorkflow: self._runner = runner def run(self, ctx: TaskRuntimeContext) -> WorkflowExecutionResult: + if ctx.trace is not None: + ctx.trace.module("task_workflow").log( + "started", + {"workflow_id": self.workflow_id, "message": ctx.message}, + ) result = self._runner.run( ctx.message, ctx.rag_session_id, conversation_state=ctx.conversation_state, mode="full", + trace=ctx.trace, ) diagnostics = result.diagnostics.model_dump(mode="json") emit_status_block( @@ -42,15 +48,22 @@ class DocsQaWorkflow: title="Evidence Gate", lines=_gate_lines(diagnostics), ) - return WorkflowExecutionResult( + result_payload = WorkflowExecutionResult( result_type=TaskResultType.ANSWER, answer=result.answer, meta={ "workflow_id": self.workflow_id, "intent": result.router_result.intent, "diagnostics": diagnostics, + "llm_request": result.llm_request, }, ) + if ctx.trace is not None: + ctx.trace.module("task_workflow").log( + "completed", + {"workflow_id": self.workflow_id, "answer_length": len(result.answer or "")}, + ) + return result_payload def _retrieval_lines(diagnostics: dict) -> list[str]: diff --git a/src/app/modules/agent/task_runtime/workflows/fallback.py b/src/app/modules/agent/task_runtime/workflows/fallback.py index a96d01c..cb5c3b7 100644 --- a/src/app/modules/agent/task_runtime/workflows/fallback.py +++ b/src/app/modules/agent/task_runtime/workflows/fallback.py @@ -15,6 +15,11 @@ class FallbackWorkflow: self._llm = llm def run(self, ctx: TaskRuntimeContext) -> WorkflowExecutionResult: + if ctx.trace is not None: + ctx.trace.module("task_workflow").log( + "started", + {"workflow_id": self.workflow_id, "message": ctx.message}, + ) emit_status_block( ctx, block_id="rag_retrieval", @@ -27,14 +32,18 @@ class FallbackWorkflow: payload = json.dumps( { "question": ctx.message, - "intent": getattr(ctx.route_result, "intent", ""), "attachments": list(ctx.attachments), "confluence_urls": list(ctx.enriched_context.get("confluence_urls") or []), }, ensure_ascii=False, indent=2, ) - answer = self._llm.generate("docs_fallback_answer", payload, log_context="agent.workflow.fallback").strip() + answer = self._llm.generate( + "fallback_answer", + payload, + log_context="agent.workflow.fallback", + trace=ctx.trace.module("llm") if ctx.trace is not None else None, + ).strip() emit_status_block( ctx, block_id="workflow", @@ -47,8 +56,14 @@ class FallbackWorkflow: title="Evidence Gate", lines=["not applied in fallback workflow"], ) - return WorkflowExecutionResult( + result = WorkflowExecutionResult( result_type=TaskResultType.ANSWER, answer=answer, meta={"workflow_id": self.workflow_id, "intent": getattr(ctx.route_result, "intent", "")}, ) + if ctx.trace is not None: + ctx.trace.module("task_workflow").log( + "completed", + {"workflow_id": self.workflow_id, "answer_length": len(answer)}, + ) + return result diff --git a/src/app/modules/agent/task_runtime/workflows/general_qa.py b/src/app/modules/agent/task_runtime/workflows/general_qa.py index aa9ba41..9991eb3 100644 --- a/src/app/modules/agent/task_runtime/workflows/general_qa.py +++ b/src/app/modules/agent/task_runtime/workflows/general_qa.py @@ -14,11 +14,17 @@ class GeneralQaWorkflow: self._runner = runner def run(self, ctx: TaskRuntimeContext) -> WorkflowExecutionResult: + if ctx.trace is not None: + ctx.trace.module("task_workflow").log( + "started", + {"workflow_id": self.workflow_id, "message": ctx.message}, + ) result = self._runner.run( ctx.message, ctx.rag_session_id, conversation_state=ctx.conversation_state, mode="full", + trace=ctx.trace, ) diagnostics = result.diagnostics.model_dump(mode="json") emit_status_block( @@ -43,12 +49,19 @@ class GeneralQaWorkflow: title="Evidence Gate", lines=_gate_lines(diagnostics), ) - return WorkflowExecutionResult( + result_payload = WorkflowExecutionResult( result_type=TaskResultType.ANSWER, answer=result.answer, meta={ "workflow_id": self.workflow_id, "intent": result.router_result.intent, "diagnostics": diagnostics, + "llm_request": result.llm_request, }, ) + if ctx.trace is not None: + ctx.trace.module("task_workflow").log( + "completed", + {"workflow_id": self.workflow_id, "answer_length": len(result.answer or "")}, + ) + return result_payload diff --git a/src/app/modules/agent/task_runtime/workflows/openapi.py b/src/app/modules/agent/task_runtime/workflows/openapi.py index f9e01f3..fb6d95b 100644 --- a/src/app/modules/agent/task_runtime/workflows/openapi.py +++ b/src/app/modules/agent/task_runtime/workflows/openapi.py @@ -14,11 +14,17 @@ class OpenApiWorkflow: self._runner = runner def run(self, ctx: TaskRuntimeContext) -> WorkflowExecutionResult: + if ctx.trace is not None: + ctx.trace.module("task_workflow").log( + "started", + {"workflow_id": self.workflow_id, "message": ctx.message}, + ) result = self._runner.run( ctx.message, ctx.rag_session_id, conversation_state=ctx.conversation_state, mode="full", + trace=ctx.trace, ) diagnostics = result.diagnostics.model_dump(mode="json") emit_status_block( @@ -51,7 +57,7 @@ class OpenApiWorkflow: format="yaml", source_refs=list(result.diagnostics.doc_paths), ) - return WorkflowExecutionResult( + result_payload = WorkflowExecutionResult( result_type=TaskResultType.OPENAPI, answer=content, artifacts=[artifact], @@ -59,5 +65,12 @@ class OpenApiWorkflow: "workflow_id": self.workflow_id, "intent": result.router_result.intent, "diagnostics": diagnostics, + "llm_request": result.llm_request, }, ) + if ctx.trace is not None: + ctx.trace.module("task_workflow").log( + "completed", + {"workflow_id": self.workflow_id, "answer_length": len(content or "")}, + ) + return result_payload diff --git a/src/app/modules/agent_api/__init__.py b/src/app/modules/agent_api/__init__.py deleted file mode 100644 index c689232..0000000 --- a/src/app/modules/agent_api/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from app.modules.agent_api.module import AgentApiModule - -__all__ = ["AgentApiModule"] diff --git a/src/app/modules/api/__init__.py b/src/app/modules/api/__init__.py new file mode 100644 index 0000000..c9c2ef6 --- /dev/null +++ b/src/app/modules/api/__init__.py @@ -0,0 +1 @@ +__all__: list[str] = [] diff --git a/src/app/modules/agent_api/application/request_service.py b/src/app/modules/api/application/request_service.py similarity index 72% rename from src/app/modules/agent_api/application/request_service.py rename to src/app/modules/api/application/request_service.py index 98cba04..ca4da41 100644 --- a/src/app/modules/agent_api/application/request_service.py +++ b/src/app/modules/api/application/request_service.py @@ -2,11 +2,11 @@ from __future__ import annotations import asyncio -from app.modules.agent_api.domain.models.agent_request import AgentRequest -from app.modules.agent_api.infrastructure.ids.request_id_factory import RequestIdFactory -from app.modules.agent_api.infrastructure.stores.in_memory_request_store import InMemoryRequestStore -from app.modules.agent_api.application.session_service import SessionService -from app.modules.orchestration.facade import OrchestrationFacade +from app.modules.api.domain.models.agent_request import AgentRequest +from app.modules.api.infrastructure.ids.request_id_factory import RequestIdFactory +from app.modules.api.infrastructure.stores.in_memory_request_store import InMemoryRequestStore +from app.modules.api.application.session_service import SessionService +from app.modules.agent.orchestration.facade import OrchestrationFacade class RequestService: diff --git a/src/app/modules/agent_api/application/session_service.py b/src/app/modules/api/application/session_service.py similarity index 85% rename from src/app/modules/agent_api/application/session_service.py rename to src/app/modules/api/application/session_service.py index c5ec3a3..1dfeeab 100644 --- a/src/app/modules/agent_api/application/session_service.py +++ b/src/app/modules/api/application/session_service.py @@ -3,9 +3,9 @@ from __future__ import annotations from datetime import datetime, timezone from app.core.exceptions import AppError -from app.modules.agent_api.domain.models.agent_session import AgentSession -from app.modules.agent_api.infrastructure.ids.session_id_factory import SessionIdFactory -from app.modules.agent_api.infrastructure.stores.in_memory_session_store import InMemorySessionStore +from app.modules.api.domain.models.agent_session import AgentSession +from app.modules.api.infrastructure.ids.session_id_factory import SessionIdFactory +from app.modules.api.infrastructure.stores.in_memory_session_store import InMemorySessionStore from app.schemas.common import ModuleName diff --git a/src/app/modules/agent_api/application/stream_service.py b/src/app/modules/api/application/stream_service.py similarity index 83% rename from src/app/modules/agent_api/application/stream_service.py rename to src/app/modules/api/application/stream_service.py index 1d8eba3..1986f5c 100644 --- a/src/app/modules/agent_api/application/stream_service.py +++ b/src/app/modules/api/application/stream_service.py @@ -1,8 +1,8 @@ from __future__ import annotations from app.core.exceptions import AppError -from app.modules.agent_api.infrastructure.streaming.sse_encoder import SseEncoder -from app.modules.agent_api.infrastructure.streaming.sse_event_channel import SseEventChannel +from app.modules.api.infrastructure.streaming.sse_encoder import SseEncoder +from app.modules.api.infrastructure.streaming.sse_event_channel import SseEventChannel from app.schemas.common import ModuleName diff --git a/src/app/modules/agent_api/controllers/request_controller.py b/src/app/modules/api/controllers/request_controller.py similarity index 94% rename from src/app/modules/agent_api/controllers/request_controller.py rename to src/app/modules/api/controllers/request_controller.py index 85abb95..c2f1841 100644 --- a/src/app/modules/agent_api/controllers/request_controller.py +++ b/src/app/modules/api/controllers/request_controller.py @@ -1,7 +1,7 @@ from __future__ import annotations from app.core.exceptions import AppError -from app.modules.agent_api.application.request_service import RequestService +from app.modules.api.application.request_service import RequestService from app.schemas.agent_api import AgentRequestCreateRequest, AgentRequestQueuedResponse, AgentRequestStateResponse from app.schemas.common import ModuleName diff --git a/src/app/modules/agent_api/controllers/session_controller.py b/src/app/modules/api/controllers/session_controller.py similarity index 94% rename from src/app/modules/agent_api/controllers/session_controller.py rename to src/app/modules/api/controllers/session_controller.py index c01938d..996de49 100644 --- a/src/app/modules/agent_api/controllers/session_controller.py +++ b/src/app/modules/api/controllers/session_controller.py @@ -6,7 +6,7 @@ from app.schemas.agent_api import ( CreateAgentSessionResponse, ResetAgentSessionResponse, ) -from app.modules.agent_api.application.session_service import SessionService +from app.modules.api.application.session_service import SessionService class SessionController: diff --git a/src/app/modules/agent_api/controllers/stream_controller.py b/src/app/modules/api/controllers/stream_controller.py similarity index 93% rename from src/app/modules/agent_api/controllers/stream_controller.py rename to src/app/modules/api/controllers/stream_controller.py index 9e18c58..1026e9d 100644 --- a/src/app/modules/agent_api/controllers/stream_controller.py +++ b/src/app/modules/api/controllers/stream_controller.py @@ -2,7 +2,7 @@ from __future__ import annotations from fastapi.responses import StreamingResponse -from app.modules.agent_api.application.stream_service import StreamService +from app.modules.api.application.stream_service import StreamService class StreamController: diff --git a/src/app/modules/agent_api/domain/events/client_event.py b/src/app/modules/api/domain/events/client_event.py similarity index 100% rename from src/app/modules/agent_api/domain/events/client_event.py rename to src/app/modules/api/domain/events/client_event.py diff --git a/src/app/modules/agent_api/domain/models/agent_request.py b/src/app/modules/api/domain/models/agent_request.py similarity index 100% rename from src/app/modules/agent_api/domain/models/agent_request.py rename to src/app/modules/api/domain/models/agent_request.py diff --git a/src/app/modules/agent_api/domain/models/agent_session.py b/src/app/modules/api/domain/models/agent_session.py similarity index 100% rename from src/app/modules/agent_api/domain/models/agent_session.py rename to src/app/modules/api/domain/models/agent_session.py diff --git a/src/app/modules/agent_api/infrastructure/ids/request_id_factory.py b/src/app/modules/api/infrastructure/ids/request_id_factory.py similarity index 100% rename from src/app/modules/agent_api/infrastructure/ids/request_id_factory.py rename to src/app/modules/api/infrastructure/ids/request_id_factory.py diff --git a/src/app/modules/agent_api/infrastructure/ids/session_id_factory.py b/src/app/modules/api/infrastructure/ids/session_id_factory.py similarity index 100% rename from src/app/modules/agent_api/infrastructure/ids/session_id_factory.py rename to src/app/modules/api/infrastructure/ids/session_id_factory.py diff --git a/src/app/modules/agent_api/infrastructure/logging/request_trace_logger.py b/src/app/modules/api/infrastructure/logging/request_trace_logger.py similarity index 77% rename from src/app/modules/agent_api/infrastructure/logging/request_trace_logger.py rename to src/app/modules/api/infrastructure/logging/request_trace_logger.py index 2be1fd9..11001c9 100644 --- a/src/app/modules/agent_api/infrastructure/logging/request_trace_logger.py +++ b/src/app/modules/api/infrastructure/logging/request_trace_logger.py @@ -3,11 +3,11 @@ from __future__ import annotations from pathlib import Path from threading import Lock -from app.modules.agent_api.domain.events.client_event import ClientEventRecord -from app.modules.agent_api.domain.models.agent_request import AgentRequest -from app.modules.agent_api.domain.models.agent_session import AgentSession -from app.modules.agent_api.infrastructure.logging.trace_file_path_builder import TraceFilePathBuilder -from app.modules.agent_api.infrastructure.logging.trace_markdown_writer import TraceMarkdownWriter +from app.modules.api.domain.events.client_event import ClientEventRecord +from app.modules.api.domain.models.agent_request import AgentRequest +from app.modules.api.domain.models.agent_session import AgentSession +from app.modules.api.infrastructure.logging.trace_file_path_builder import TraceFilePathBuilder +from app.modules.api.infrastructure.logging.trace_markdown_writer import TraceMarkdownWriter class RequestTraceLogger: @@ -39,11 +39,17 @@ class RequestTraceLogger: def log_step(self, request_id: str, step: str, status: str, details: dict | None = None) -> None: self._append(request_id, f"Step {step}", {"status": status, "details": details or {}}) + def log_module(self, request_id: str, module: str, title: str, payload: dict | None = None) -> None: + body = {"event": title} + body.update(payload or {}) + self._append(request_id, module, body) + def log_event(self, event: ClientEventRecord) -> None: self._append( event.request_id, - f"Event {event.type.value}", + "client_event", { + "event": event.type.value, "source": event.source, "text": event.text, "payload": event.payload, @@ -54,7 +60,7 @@ class RequestTraceLogger: def complete_request(self, request: AgentRequest) -> None: self._append( request.request_id, - "Result", + "result", { "status": request.status.value, "answer": request.answer, @@ -65,7 +71,7 @@ class RequestTraceLogger: def fail_request(self, request: AgentRequest) -> None: self._append( request.request_id, - "Error", + "result", { "status": request.status.value, "error": request.error.model_dump(mode="json") if request.error else None, diff --git a/src/app/modules/agent_api/infrastructure/logging/trace_file_path_builder.py b/src/app/modules/api/infrastructure/logging/trace_file_path_builder.py similarity index 100% rename from src/app/modules/agent_api/infrastructure/logging/trace_file_path_builder.py rename to src/app/modules/api/infrastructure/logging/trace_file_path_builder.py diff --git a/src/app/modules/agent_api/infrastructure/logging/trace_markdown_writer.py b/src/app/modules/api/infrastructure/logging/trace_markdown_writer.py similarity index 100% rename from src/app/modules/agent_api/infrastructure/logging/trace_markdown_writer.py rename to src/app/modules/api/infrastructure/logging/trace_markdown_writer.py diff --git a/src/app/modules/agent_api/infrastructure/stores/in_memory_request_store.py b/src/app/modules/api/infrastructure/stores/in_memory_request_store.py similarity index 86% rename from src/app/modules/agent_api/infrastructure/stores/in_memory_request_store.py rename to src/app/modules/api/infrastructure/stores/in_memory_request_store.py index 216ca2f..5551330 100644 --- a/src/app/modules/agent_api/infrastructure/stores/in_memory_request_store.py +++ b/src/app/modules/api/infrastructure/stores/in_memory_request_store.py @@ -2,7 +2,7 @@ from __future__ import annotations from threading import Lock -from app.modules.agent_api.domain.models.agent_request import AgentRequest +from app.modules.api.domain.models.agent_request import AgentRequest class InMemoryRequestStore: diff --git a/src/app/modules/agent_api/infrastructure/stores/in_memory_session_store.py b/src/app/modules/api/infrastructure/stores/in_memory_session_store.py similarity index 86% rename from src/app/modules/agent_api/infrastructure/stores/in_memory_session_store.py rename to src/app/modules/api/infrastructure/stores/in_memory_session_store.py index c66109e..1e13564 100644 --- a/src/app/modules/agent_api/infrastructure/stores/in_memory_session_store.py +++ b/src/app/modules/api/infrastructure/stores/in_memory_session_store.py @@ -2,7 +2,7 @@ from __future__ import annotations from threading import Lock -from app.modules.agent_api.domain.models.agent_session import AgentSession +from app.modules.api.domain.models.agent_session import AgentSession class InMemorySessionStore: diff --git a/src/app/modules/agent_api/infrastructure/streaming/replay_buffer.py b/src/app/modules/api/infrastructure/streaming/replay_buffer.py similarity index 88% rename from src/app/modules/agent_api/infrastructure/streaming/replay_buffer.py rename to src/app/modules/api/infrastructure/streaming/replay_buffer.py index b3b27d5..dad9125 100644 --- a/src/app/modules/agent_api/infrastructure/streaming/replay_buffer.py +++ b/src/app/modules/api/infrastructure/streaming/replay_buffer.py @@ -2,7 +2,7 @@ from __future__ import annotations from collections import defaultdict -from app.modules.agent_api.domain.events.client_event import ClientEventRecord +from app.modules.api.domain.events.client_event import ClientEventRecord class ReplayBuffer: diff --git a/src/app/modules/agent_api/infrastructure/streaming/sse_encoder.py b/src/app/modules/api/infrastructure/streaming/sse_encoder.py similarity index 87% rename from src/app/modules/agent_api/infrastructure/streaming/sse_encoder.py rename to src/app/modules/api/infrastructure/streaming/sse_encoder.py index 4d88d91..0817394 100644 --- a/src/app/modules/agent_api/infrastructure/streaming/sse_encoder.py +++ b/src/app/modules/api/infrastructure/streaming/sse_encoder.py @@ -2,7 +2,7 @@ from __future__ import annotations import json -from app.modules.agent_api.domain.events.client_event import ClientEventRecord +from app.modules.api.domain.events.client_event import ClientEventRecord class SseEncoder: diff --git a/src/app/modules/agent_api/infrastructure/streaming/sse_event_channel.py b/src/app/modules/api/infrastructure/streaming/sse_event_channel.py similarity index 90% rename from src/app/modules/agent_api/infrastructure/streaming/sse_event_channel.py rename to src/app/modules/api/infrastructure/streaming/sse_event_channel.py index 684f5b8..dddfd56 100644 --- a/src/app/modules/agent_api/infrastructure/streaming/sse_event_channel.py +++ b/src/app/modules/api/infrastructure/streaming/sse_event_channel.py @@ -3,8 +3,8 @@ from __future__ import annotations import asyncio from collections import defaultdict -from app.modules.agent_api.domain.events.client_event import ClientEventRecord -from app.modules.agent_api.infrastructure.streaming.replay_buffer import ReplayBuffer +from app.modules.api.domain.events.client_event import ClientEventRecord +from app.modules.api.infrastructure.streaming.replay_buffer import ReplayBuffer class SseEventChannel: diff --git a/src/app/modules/agent_api/module.py b/src/app/modules/api/module.py similarity index 50% rename from src/app/modules/agent_api/module.py rename to src/app/modules/api/module.py index 179bf0b..9085af9 100644 --- a/src/app/modules/agent_api/module.py +++ b/src/app/modules/api/module.py @@ -2,16 +2,16 @@ from __future__ import annotations from fastapi import APIRouter -from app.modules.agent_api.application.request_service import RequestService -from app.modules.agent_api.application.session_service import SessionService -from app.modules.agent_api.application.stream_service import StreamService -from app.modules.agent_api.controllers.request_controller import RequestController -from app.modules.agent_api.controllers.session_controller import SessionController -from app.modules.agent_api.controllers.stream_controller import StreamController -from app.modules.agent_api.public_router import build_public_router +from app.modules.api.application.request_service import RequestService +from app.modules.api.application.session_service import SessionService +from app.modules.api.application.stream_service import StreamService +from app.modules.api.controllers.request_controller import RequestController +from app.modules.api.controllers.session_controller import SessionController +from app.modules.api.controllers.stream_controller import StreamController +from app.modules.api.public_router import build_public_router -class AgentApiModule: +class ApiModule: def __init__( self, sessions: SessionService, diff --git a/src/app/modules/agent_api/public_router.py b/src/app/modules/api/public_router.py similarity index 87% rename from src/app/modules/agent_api/public_router.py rename to src/app/modules/api/public_router.py index 17ce058..0bfd27c 100644 --- a/src/app/modules/agent_api/public_router.py +++ b/src/app/modules/api/public_router.py @@ -2,9 +2,9 @@ from __future__ import annotations from fastapi import APIRouter -from app.modules.agent_api.controllers.request_controller import RequestController -from app.modules.agent_api.controllers.session_controller import SessionController -from app.modules.agent_api.controllers.stream_controller import StreamController +from app.modules.api.controllers.request_controller import RequestController +from app.modules.api.controllers.session_controller import SessionController +from app.modules.api.controllers.stream_controller import StreamController from app.schemas.agent_api import ( AgentRequestCreateRequest, AgentRequestQueuedResponse, diff --git a/src/app/modules/application.py b/src/app/modules/application.py index 0018fd9..4d3f035 100644 --- a/src/app/modules/application.py +++ b/src/app/modules/application.py @@ -13,32 +13,36 @@ from app.modules.agent.task_runtime.workflows import ( OpenApiWorkflow, ) from app.modules.agent.task_runtime.workflows.general_qa import GeneralQaWorkflow -from app.modules.agent_api import AgentApiModule -from app.modules.agent_api.application.request_service import RequestService -from app.modules.agent_api.application.session_service import SessionService -from app.modules.agent_api.application.stream_service import StreamService -from app.modules.agent_api.infrastructure.ids.request_id_factory import RequestIdFactory -from app.modules.agent_api.infrastructure.ids.session_id_factory import SessionIdFactory -from app.modules.agent_api.infrastructure.logging.request_trace_logger import RequestTraceLogger -from app.modules.agent_api.infrastructure.stores.in_memory_request_store import InMemoryRequestStore -from app.modules.agent_api.infrastructure.stores.in_memory_session_store import InMemorySessionStore -from app.modules.agent_api.infrastructure.streaming.sse_event_channel import SseEventChannel -from app.modules.orchestration import OrchestrationFacade -from app.modules.orchestration.adapters.intent_router_adapter import IntentRouterAdapter -from app.modules.orchestration.adapters.llm_chat_adapter import LlmChatAdapter -from app.modules.orchestration.messaging.client_message_publisher import ClientMessagePublisher -from app.modules.orchestration.processes.registry import ProcessRegistry -from app.modules.orchestration.processes.v1.process import V1Process -from app.modules.orchestration.processes.v1.steps.bootstrap_step import BootstrapStep -from app.modules.orchestration.processes.v1.steps.finalize_step import FinalizeStep -from app.modules.orchestration.processes.v1.steps.run_llm_step import RunLlmStep -from app.modules.orchestration.processes.v2.process import V2Process -from app.modules.orchestration.processes.v2.steps.execute_documentation_workflow_step import ExecuteDocumentationWorkflowStep -from app.modules.orchestration.processes.v2.steps.execute_fallback_workflow_step import ExecuteFallbackWorkflowStep -from app.modules.orchestration.processes.v2.steps.execute_general_qa_workflow_step import ExecuteGeneralQaWorkflowStep -from app.modules.orchestration.processes.v2.steps.execute_openapi_workflow_step import ExecuteOpenApiWorkflowStep -from app.modules.orchestration.processes.v2.steps.route_intent_step import RouteIntentStep -from app.modules.orchestration.runtime.process_runner import ProcessRunner +from app.modules.api.module import ApiModule +from app.modules.api.application.request_service import RequestService +from app.modules.api.application.session_service import SessionService +from app.modules.api.application.stream_service import StreamService +from app.modules.api.infrastructure.ids.request_id_factory import RequestIdFactory +from app.modules.api.infrastructure.ids.session_id_factory import SessionIdFactory +from app.modules.api.infrastructure.logging.request_trace_logger import RequestTraceLogger +from app.modules.api.infrastructure.stores.in_memory_request_store import InMemoryRequestStore +from app.modules.api.infrastructure.stores.in_memory_session_store import InMemorySessionStore +from app.modules.api.infrastructure.streaming.sse_event_channel import SseEventChannel +from app.modules.agent.orchestration.facade import OrchestrationFacade +from app.modules.agent.orchestration.adapters.intent_router_adapter import IntentRouterAdapter +from app.modules.agent.orchestration.adapters.llm_chat_adapter import LlmChatAdapter +from app.modules.agent.orchestration.messaging.client_message_publisher import ClientMessagePublisher +from app.modules.agent.orchestration.processes.registry import ProcessRegistry +from app.modules.agent.orchestration.processes.v1.process import V1Process +from app.modules.agent.orchestration.processes.v1.prompt_payload_builder import V1PromptPayloadBuilder +from app.modules.agent.orchestration.processes.v1.simple_llm_workflow import SimpleLlmWorkflow +from app.modules.agent.orchestration.processes.v1.steps.bootstrap_step import BootstrapStep +from app.modules.agent.orchestration.processes.v1.steps.execute_llm_workflow_step import ExecuteLlmWorkflowStep +from app.modules.agent.orchestration.processes.v1.steps.finalize_step import FinalizeStep +from app.modules.agent.orchestration.processes.v2.prompt_payload_builder import V2PromptPayloadBuilder +from app.modules.agent.orchestration.processes.v2.prompt_selector import V2PromptSelector +from app.modules.agent.orchestration.processes.v2.process import V2Process +from app.modules.agent.orchestration.processes.v2.steps.execute_documentation_workflow_step import ExecuteDocumentationWorkflowStep +from app.modules.agent.orchestration.processes.v2.steps.execute_fallback_workflow_step import ExecuteFallbackWorkflowStep +from app.modules.agent.orchestration.processes.v2.steps.execute_general_qa_workflow_step import ExecuteGeneralQaWorkflowStep +from app.modules.agent.orchestration.processes.v2.steps.execute_openapi_workflow_step import ExecuteOpenApiWorkflowStep +from app.modules.agent.orchestration.processes.v2.steps.route_intent_step import RouteIntentStep +from app.modules.agent.orchestration.runtime.process_runner import ProcessRunner from app.modules.rag.persistence.repository import RagRepository from app.modules.agent.runtime.story_context_repository import StoryContextRepository, StoryContextSchemaRepository from app.modules.rag.module import RagModule, RagRepoModule @@ -66,8 +70,10 @@ class ModularApplication: _giga_settings = GigaChatSettings.from_env() _giga_client = GigaChatClient(_giga_settings, GigaChatTokenProvider(_giga_settings)) - _prompt_loader = PromptLoader() - self._agent_llm = AgentLlmService(client=_giga_client, prompts=_prompt_loader) + _v1_prompt_loader = PromptLoader(Path(__file__).resolve().parent / "agent/orchestration/processes/v1/prompts.yml") + _v2_prompt_loader = PromptLoader(Path(__file__).resolve().parent / "agent/orchestration/processes/v2/prompts.yml") + self._agent_llm_v1 = AgentLlmService(client=_giga_client, prompts=_v1_prompt_loader) + self._agent_llm_v2 = AgentLlmService(client=_giga_client, prompts=_v2_prompt_loader) _router = IntentRouterV2() _retrieval = RuntimeRetrievalAdapter(self.rag_repository) _repo_context_factory = RuntimeRepoContextFactory() @@ -75,15 +81,17 @@ class ModularApplication: router=_router, retrieval_adapter=_retrieval, repo_context=_repo_context_factory.build(), - llm=self._agent_llm, + llm=self._agent_llm_v2, + prompt_selector=V2PromptSelector(), + prompt_payload_builder=V2PromptPayloadBuilder(), ) _task_context_builder = TaskRuntimeContextBuilder(_repo_context_factory) _context_enrichment = ContextEnrichmentService() _docs_workflow = DocsQaWorkflow(_docs_runner) _openapi_workflow = OpenApiWorkflow(_docs_runner) _general_qa_workflow = GeneralQaWorkflow(_docs_runner) - _fallback_workflow = FallbackWorkflow(self._agent_llm) - _docs_generation_workflow = DocumentationGenerationWorkflow(self._agent_llm, DocumentationTemplateRegistry()) + _fallback_workflow = FallbackWorkflow(self._agent_llm_v2) + _docs_generation_workflow = DocumentationGenerationWorkflow(self._agent_llm_v2, DocumentationTemplateRegistry()) self._docs_generation_workflow = _docs_generation_workflow self.agent_sessions = InMemorySessionStore() @@ -91,8 +99,12 @@ class ModularApplication: self.agent_events = SseEventChannel() self.agent_trace_logger = RequestTraceLogger(Path("runtime_traces/agent_requests")) _publisher = ClientMessagePublisher(self.agent_events, self.agent_trace_logger) + _v1_workflow = SimpleLlmWorkflow( + LlmChatAdapter(self._agent_llm_v1, prompt_name="simple_llm_answer"), + V1PromptPayloadBuilder(), + ) _process_registry = ProcessRegistry( - V1Process([BootstrapStep(), RunLlmStep(LlmChatAdapter(self._agent_llm)), FinalizeStep()]), + V1Process([BootstrapStep(), ExecuteLlmWorkflowStep(_v1_workflow), FinalizeStep()]), V2Process( [ BootstrapStep(), @@ -123,7 +135,7 @@ class ModularApplication: sessions=_session_service, orchestration=_orchestration, ) - self.agent_api = AgentApiModule( + self.api = ApiModule( sessions=_session_service, requests=_request_service, streams=StreamService(self.agent_events, request_exists=lambda request_id: self.agent_requests.get(request_id) is not None), diff --git a/src/app/modules/chat/README.md b/src/app/modules/chat/README.md deleted file mode 100644 index c6edc18..0000000 --- a/src/app/modules/chat/README.md +++ /dev/null @@ -1,98 +0,0 @@ -# Модуль chat - -## 1. Функции модуля -- Внешний API чата: создание диалога, отправка сообщения, получение статуса задачи. -- Асинхронная оркестрация выполнения через `ChatOrchestrator`. -- Idempotency и стриминг событий по SSE. - -## 2. Диаграмма классов и взаимосвязей -```mermaid -classDiagram - class ChatModule - class ChatOrchestrator - class TaskStore - class DialogSessionStore - class IdempotencyStore - class EventBus - class AgentRunner - - ChatModule --> ChatOrchestrator - ChatModule --> TaskStore - ChatModule --> DialogSessionStore - ChatModule --> IdempotencyStore - ChatModule --> EventBus - ChatOrchestrator --> AgentRunner - ChatOrchestrator --> TaskStore - ChatOrchestrator --> DialogSessionStore - ChatOrchestrator --> EventBus -``` - -## 3. Описание классов -- `ChatModule`: фасад модуля и регистрация публичных chat endpoint'ов. - Методы: `__init__` — собирает stores/orchestrator; `public_router` — публикует REST и SSE маршруты чата. -- `ChatOrchestrator`: выполняет жизненный цикл user-message как фоновой задачи. - Методы: `enqueue_message` — создает задачу и запускает обработку; `_process_task` — исполняет runtime и сохраняет результат; `_resolve_sessions` — валидирует и сопоставляет dialog/rag сессии. -- `TaskStore`: in-memory store состояний задач. - Методы: `create` — создает новую `TaskState`; `get` — возвращает задачу по `task_id`; `save` — обновляет состояние задачи. -- `DialogSessionStore`: хранилище dialog-сессий поверх БД. - Методы: `create` — создает новую dialog-сессию; `get` — читает dialog-сессию по id. -- `IdempotencyStore`: предотвращает дубль задач по идемпотентному ключу. - Методы: `get_task_id` — возвращает существующий `task_id` по ключу; `put` — сохраняет ключ и `task_id`. -- `EventBus`: асинхронная публикация/подписка событий. - Методы: `subscribe` — создает подписку на канал; `unsubscribe` — снимает подписку; `publish` — отправляет событие подписчикам; `as_sse` — сериализует событие в SSE формат. -- `AgentRunner` (контракт): интерфейс выполнения агентного запроса из chat-слоя. - Методы: `run` — принимает данные задачи и возвращает итог `answer`/`changeset`. - -## 4. Сиквенс-диаграммы API - -### POST /api/chat/dialogs -Назначение: создает новый диалог, связанный с существующей `rag_session`, чтобы пользователь мог отправлять сообщения в контексте конкретного индекса. -```mermaid -sequenceDiagram - participant Router as ChatModule.APIRouter - participant RagSessions as RagSessionStore - participant Dialogs as DialogSessionStore - - Router->>RagSessions: get(rag_session_id) - RagSessions-->>Router: exists - Router->>Dialogs: create(rag_session_id) - Dialogs-->>Router: dialog_session -``` - -### POST /api/chat/messages -Назначение: ставит сообщение пользователя в асинхронную обработку и возвращает `task_id` для отслеживания результата. -```mermaid -sequenceDiagram - participant Router as ChatModule.APIRouter - participant Orchestrator as ChatOrchestrator - participant TaskStore as TaskStore - - Router->>Orchestrator: enqueue_message(request, idempotency_key) - Orchestrator->>TaskStore: create()/save() - Orchestrator-->>Router: task_id,status -``` - -### GET /api/tasks/{task_id} -Назначение: отдает текущее состояние задачи и финальный результат (answer/changeset/error), когда обработка завершена. -```mermaid -sequenceDiagram - participant Router as ChatModule.APIRouter - participant TaskStore as TaskStore - - Router->>TaskStore: get(task_id) - TaskStore-->>Router: task_state -``` - -### GET /api/events?task_id=... -Назначение: открывает SSE-поток с прогрессом выполнения задачи и промежуточными событиями. -```mermaid -sequenceDiagram - participant Router as ChatModule.APIRouter - participant Events as EventBus - - Router->>Events: subscribe(task_id) - loop until disconnect - Events-->>Router: SSE event - end - Router->>Events: unsubscribe(task_id) -``` diff --git a/src/app/modules/chat/__init__.py b/src/app/modules/chat/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/app/modules/chat/dialog_store.py b/src/app/modules/chat/dialog_store.py deleted file mode 100644 index ea8f932..0000000 --- a/src/app/modules/chat/dialog_store.py +++ /dev/null @@ -1,33 +0,0 @@ -from __future__ import annotations - -from dataclasses import dataclass -from typing import TYPE_CHECKING -from uuid import uuid4 - -if TYPE_CHECKING: - from app.modules.chat.repository import ChatRepository - - -@dataclass -class DialogSession: - dialog_session_id: str - rag_session_id: str - - -class DialogSessionStore: - def __init__(self, repository: ChatRepository) -> None: - self._repo = repository - - def create(self, rag_session_id: str) -> DialogSession: - session = DialogSession(dialog_session_id=str(uuid4()), rag_session_id=rag_session_id) - self._repo.create_dialog(session.dialog_session_id, session.rag_session_id) - return session - - def get(self, dialog_session_id: str) -> DialogSession | None: - row = self._repo.get_dialog(dialog_session_id) - if not row: - return None - return DialogSession( - dialog_session_id=str(row["dialog_session_id"]), - rag_session_id=str(row["rag_session_id"]), - ) diff --git a/src/app/modules/chat/direct_service.py b/src/app/modules/chat/direct_service.py deleted file mode 100644 index 0ab0a2a..0000000 --- a/src/app/modules/chat/direct_service.py +++ /dev/null @@ -1,71 +0,0 @@ -from __future__ import annotations - -import logging -from uuid import uuid4 - -from app.modules.agent.llm import AgentLlmService -from app.modules.chat.evidence_gate import CodeExplainEvidenceGate -from app.modules.chat.session_resolver import ChatSessionResolver -from app.modules.chat.task_store import TaskState, TaskStore -from app.modules.agent.runtime.steps.explain import CodeExplainRetrieverV2, PromptBudgeter -from app.schemas.chat import ChatMessageRequest, TaskQueuedResponse, TaskResultType, TaskStatus - -LOGGER = logging.getLogger(__name__) - - -class CodeExplainChatService: - def __init__( - self, - retriever: CodeExplainRetrieverV2, - llm: AgentLlmService, - session_resolver: ChatSessionResolver, - task_store: TaskStore, - message_sink, - budgeter: PromptBudgeter | None = None, - evidence_gate: CodeExplainEvidenceGate | None = None, - ) -> None: - self._retriever = retriever - self._llm = llm - self._session_resolver = session_resolver - self._task_store = task_store - self._message_sink = message_sink - self._budgeter = budgeter or PromptBudgeter() - self._evidence_gate = evidence_gate or CodeExplainEvidenceGate() - - async def handle_message(self, request: ChatMessageRequest) -> TaskQueuedResponse: - dialog_session_id, rag_session_id = self._session_resolver.resolve(request) - task_id = str(uuid4()) - task = TaskState(task_id=task_id, status=TaskStatus.RUNNING) - self._task_store.save(task) - self._message_sink(dialog_session_id, "user", request.message, task_id=task_id) - pack = self._retriever.build_pack( - rag_session_id, - request.message, - file_candidates=[item.model_dump(mode="json") for item in request.files], - ) - decision = self._evidence_gate.evaluate(pack) - if decision.passed: - prompt_input = self._budgeter.build_prompt_input(request.message, pack) - answer = self._llm.generate( - "code_explain_answer_v2", - prompt_input, - log_context="chat.code_explain.direct", - ).strip() - else: - answer = decision.answer - self._message_sink(dialog_session_id, "assistant", answer, task_id=task_id) - task.status = TaskStatus.DONE - task.result_type = TaskResultType.ANSWER - task.answer = answer - self._task_store.save(task) - LOGGER.warning( - "direct code explain response: task_id=%s rag_session_id=%s excerpts=%s missing=%s", - task_id, - rag_session_id, - len(pack.code_excerpts), - pack.missing, - ) - return TaskQueuedResponse( - task_id=task_id, - status=TaskStatus.DONE.value, - ) diff --git a/src/app/modules/chat/evidence_gate.py b/src/app/modules/chat/evidence_gate.py deleted file mode 100644 index 3e75f6c..0000000 --- a/src/app/modules/chat/evidence_gate.py +++ /dev/null @@ -1,62 +0,0 @@ -from __future__ import annotations - -from dataclasses import dataclass, field - -from app.modules.agent.runtime.steps.explain.models import ExplainPack - - -@dataclass(slots=True) -class EvidenceGateDecision: - passed: bool - answer: str = "" - diagnostics: dict[str, list[str]] = field(default_factory=dict) - - -class CodeExplainEvidenceGate: - def __init__(self, min_excerpts: int = 2) -> None: - self._min_excerpts = min_excerpts - - def evaluate(self, pack: ExplainPack) -> EvidenceGateDecision: - diagnostics = self._diagnostics(pack) - if len(pack.code_excerpts) >= self._min_excerpts: - return EvidenceGateDecision(passed=True, diagnostics=diagnostics) - return EvidenceGateDecision( - passed=False, - answer=self._build_answer(pack, diagnostics), - diagnostics=diagnostics, - ) - - def _diagnostics(self, pack: ExplainPack) -> dict[str, list[str]]: - return { - "entrypoints": [item.title for item in pack.selected_entrypoints[:3] if item.title], - "symbols": [item.title for item in pack.seed_symbols[:5] if item.title], - "paths": self._paths(pack), - "missing": list(pack.missing), - } - - def _paths(self, pack: ExplainPack) -> list[str]: - values: list[str] = [] - for item in pack.selected_entrypoints + pack.seed_symbols: - path = item.source or (item.location.path if item.location else "") - if path and path not in values: - values.append(path) - for excerpt in pack.code_excerpts: - if excerpt.path and excerpt.path not in values: - values.append(excerpt.path) - return values[:6] - - def _build_answer(self, pack: ExplainPack, diagnostics: dict[str, list[str]]) -> str: - lines = [ - "Недостаточно опоры в коде, чтобы дать объяснение без догадок.", - "", - f"Найдено фрагментов кода: {len(pack.code_excerpts)} из {self._min_excerpts} минимально необходимых.", - ] - if diagnostics["paths"]: - lines.append(f"Пути: {', '.join(diagnostics['paths'])}") - if diagnostics["entrypoints"]: - lines.append(f"Entrypoints: {', '.join(diagnostics['entrypoints'])}") - if diagnostics["symbols"]: - lines.append(f"Символы: {', '.join(diagnostics['symbols'])}") - if diagnostics["missing"]: - lines.append(f"Диагностика: {', '.join(diagnostics['missing'])}") - return "\n".join(lines).strip() diff --git a/src/app/modules/chat/module.py b/src/app/modules/chat/module.py deleted file mode 100644 index e89b71f..0000000 --- a/src/app/modules/chat/module.py +++ /dev/null @@ -1,112 +0,0 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING - -from fastapi import APIRouter, Header -from fastapi.responses import StreamingResponse - -from app.core.exceptions import AppError -from app.modules.chat.dialog_store import DialogSessionStore -from app.modules.chat.service import ChatOrchestrator -from app.modules.chat.task_store import TaskStore -from app.modules.shared.event_bus import EventBus -from app.modules.shared.idempotency_store import IdempotencyStore -from app.modules.shared.retry_executor import RetryExecutor -from app.schemas.chat import ( - ChatMessageRequest, - DialogCreateRequest, - DialogCreateResponse, - TaskQueuedResponse, - TaskResultResponse, -) -from app.schemas.common import ModuleName - -if TYPE_CHECKING: - from app.modules.chat.repository import ChatRepository - from app.modules.contracts import AgentRunner - from app.modules.rag.session_store import RagSessionStore - - -class ChatModule: - def __init__( - self, - agent_runner: AgentRunner, - event_bus: EventBus, - retry: RetryExecutor, - rag_sessions: RagSessionStore, - repository: ChatRepository, - task_store: TaskStore | None = None, - ) -> None: - self._rag_sessions = rag_sessions - self.tasks = task_store or TaskStore() - self.dialogs = DialogSessionStore(repository) - self.idempotency = IdempotencyStore() - self.events = event_bus - self.chat = ChatOrchestrator( - task_store=self.tasks, - dialogs=self.dialogs, - idempotency=self.idempotency, - runtime=agent_runner, - events=self.events, - retry=retry, - rag_session_exists=lambda rag_session_id: rag_sessions.get(rag_session_id) is not None, - message_sink=repository.add_message, - ) - - def public_router(self) -> APIRouter: - router = APIRouter(tags=["chat"]) - - @router.post("/api/chat/dialogs", response_model=DialogCreateResponse) - async def create_dialog(request: DialogCreateRequest) -> DialogCreateResponse: - if not self._rag_sessions.get(request.rag_session_id): - raise AppError("rag_session_not_found", "RAG session not found", ModuleName.RAG) - dialog = self.dialogs.create(request.rag_session_id) - return DialogCreateResponse( - dialog_session_id=dialog.dialog_session_id, - rag_session_id=dialog.rag_session_id, - ) - - @router.post("/api/chat/messages", response_model=TaskQueuedResponse | TaskResultResponse) - async def send_message( - request: ChatMessageRequest, - idempotency_key: str | None = Header(default=None, alias="Idempotency-Key"), - ) -> TaskQueuedResponse | TaskResultResponse: - task = await self.chat.enqueue_message(request, idempotency_key) - return TaskQueuedResponse(task_id=task.task_id, status=task.status.value) - - @router.get("/api/tasks/{task_id}", response_model=TaskResultResponse) - async def get_task(task_id: str) -> TaskResultResponse: - task = self.tasks.get(task_id) - if not task: - raise AppError("not_found", f"Task not found: {task_id}", ModuleName.BACKEND) - return TaskResultResponse( - task_id=task.task_id, - status=task.status, - result_type=task.result_type, - answer=task.answer, - artifacts=task.artifacts, - changeset=task.changeset, - error=task.error, - ) - - @router.get("/api/events") - async def stream_events(task_id: str) -> StreamingResponse: - queue = await self.events.subscribe(task_id) - - async def event_stream(): - import asyncio - - heartbeat = 10 - try: - while True: - try: - event = await asyncio.wait_for(queue.get(), timeout=heartbeat) - yield EventBus.as_sse(event) - except asyncio.TimeoutError: - yield ": keepalive\\n\\n" - finally: - await self.events.unsubscribe(task_id, queue) - - return StreamingResponse(event_stream(), media_type="text/event-stream") - - return router diff --git a/src/app/modules/chat/repository.py b/src/app/modules/chat/repository.py deleted file mode 100644 index e78ff9e..0000000 --- a/src/app/modules/chat/repository.py +++ /dev/null @@ -1,93 +0,0 @@ -import json - -from sqlalchemy import text - -from app.modules.shared.db import get_engine - - -class ChatRepository: - def ensure_tables(self) -> None: - with get_engine().connect() as conn: - conn.execute( - text( - """ - CREATE TABLE IF NOT EXISTS dialog_sessions ( - dialog_session_id VARCHAR(64) PRIMARY KEY, - rag_session_id VARCHAR(64) NOT NULL, - created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP - ) - """ - ) - ) - conn.execute( - text( - """ - CREATE TABLE IF NOT EXISTS chat_messages ( - id BIGSERIAL PRIMARY KEY, - dialog_session_id VARCHAR(64) NOT NULL, - task_id VARCHAR(64), - role VARCHAR(16) NOT NULL, - content TEXT NOT NULL, - payload JSONB, - created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP - ) - """ - ) - ) - conn.execute(text("ALTER TABLE chat_messages ADD COLUMN IF NOT EXISTS task_id VARCHAR(64)")) - conn.execute(text("ALTER TABLE chat_messages ADD COLUMN IF NOT EXISTS payload JSONB")) - conn.commit() - - def create_dialog(self, dialog_session_id: str, rag_session_id: str) -> None: - with get_engine().connect() as conn: - conn.execute( - text( - """ - INSERT INTO dialog_sessions (dialog_session_id, rag_session_id) - VALUES (:did, :sid) - """ - ), - {"did": dialog_session_id, "sid": rag_session_id}, - ) - conn.commit() - - def get_dialog(self, dialog_session_id: str) -> dict | None: - with get_engine().connect() as conn: - row = conn.execute( - text( - """ - SELECT dialog_session_id, rag_session_id - FROM dialog_sessions - WHERE dialog_session_id = :did - """ - ), - {"did": dialog_session_id}, - ).mappings().fetchone() - return dict(row) if row else None - - def add_message( - self, - dialog_session_id: str, - role: str, - content: str, - task_id: str | None = None, - payload: dict | None = None, - ) -> None: - payload_json = json.dumps(payload, ensure_ascii=False) if payload is not None else None - with get_engine().connect() as conn: - conn.execute( - text( - """ - INSERT INTO chat_messages (dialog_session_id, task_id, role, content, payload) - VALUES (:did, :task_id, :role, :content, CAST(:payload AS JSONB)) - """ - ), - { - "did": dialog_session_id, - "task_id": task_id, - "role": role, - "content": content, - "payload": payload_json, - }, - ) - conn.commit() diff --git a/src/app/modules/chat/service.py b/src/app/modules/chat/service.py deleted file mode 100644 index 6dd87bf..0000000 --- a/src/app/modules/chat/service.py +++ /dev/null @@ -1,288 +0,0 @@ -import asyncio -import logging - -from app.core.exceptions import AppError -from app.modules.contracts import AgentRunner -from app.schemas.chat import ChatMessageRequest, TaskResultType, TaskStatus -from app.schemas.common import ErrorPayload, ModuleName -from app.modules.chat.dialog_store import DialogSessionStore -from app.modules.chat.session_resolver import ChatSessionResolver -from app.modules.chat.task_store import TaskState, TaskStore -from app.modules.shared.event_bus import EventBus -from app.modules.shared.idempotency_store import IdempotencyStore -from app.modules.shared.retry_executor import RetryExecutor - -LOGGER = logging.getLogger(__name__) - - -def _truncate_for_log(text: str, max_chars: int = 1200) -> str: - value = (text or "").replace("\n", "\\n").strip() - if len(value) <= max_chars: - return value - return value[:max_chars].rstrip() + "...[truncated]" - - -class ChatOrchestrator: - def __init__( - self, - task_store: TaskStore, - dialogs: DialogSessionStore, - idempotency: IdempotencyStore, - runtime: AgentRunner, - events: EventBus, - retry: RetryExecutor, - rag_session_exists, - message_sink, - ) -> None: - self._task_store = task_store - self._dialogs = dialogs - self._idempotency = idempotency - self._runtime = runtime - self._events = events - self._retry = retry - self._rag_session_exists = rag_session_exists - self._message_sink = message_sink - self._session_resolver = ChatSessionResolver(dialogs, rag_session_exists) - - async def enqueue_message( - self, - request: ChatMessageRequest, - idempotency_key: str | None, - ) -> TaskState: - if idempotency_key: - existing = self._idempotency.get_task_id(idempotency_key) - if existing: - task = self._task_store.get(existing) - if task: - LOGGER.info( - "enqueue_message reused task by idempotency key: task_id=%s mode=%s", - task.task_id, - request.mode.value, - ) - return task - - task = self._task_store.create() - if idempotency_key: - self._idempotency.put(idempotency_key, task.task_id) - asyncio.create_task(self._process_task(task.task_id, request)) - LOGGER.info( - "enqueue_message created task: task_id=%s mode=%s", - task.task_id, - request.mode.value, - ) - return task - - async def _process_task(self, task_id: str, request: ChatMessageRequest) -> None: - task = self._task_store.get(task_id) - if not task: - return - task.status = TaskStatus.RUNNING - self._task_store.save(task) - await self._events.publish(task_id, "task_status", {"task_id": task_id, "status": task.status.value}) - await self._publish_progress(task_id, "task.start", "Запрос принят, начинаю обработку.", progress=5) - - heartbeat_stop = asyncio.Event() - heartbeat_task = asyncio.create_task(self._run_heartbeat(task_id, heartbeat_stop)) - - try: - await self._publish_progress(task_id, "task.sessions", "Проверяю сессии диалога и проекта.", progress=10) - dialog_session_id, rag_session_id = self._resolve_sessions(request) - LOGGER.warning( - "incoming chat request: task_id=%s dialog_session_id=%s rag_session_id=%s mode=%s attachments=%s files=%s message=%s", - task_id, - dialog_session_id, - rag_session_id, - request.mode.value, - len(request.attachments), - len(request.files), - _truncate_for_log(request.message), - ) - await self._publish_progress(task_id, "task.sessions.done", "Сессии проверены, запускаю агента.", progress=15) - loop = asyncio.get_running_loop() - - def progress_cb(stage: str, message: str, kind: str = "task_progress", meta: dict | None = None): - asyncio.run_coroutine_threadsafe( - self._events.publish( - task_id, - kind, - { - "task_id": task_id, - "stage": stage, - "message": message, - "meta": meta or {}, - }, - ), - loop, - ) - - async def op(): - self._message_sink(dialog_session_id, "user", request.message, task_id=task_id) - await self._publish_progress(task_id, "task.agent.run", "Агент анализирует запрос и готовит ответ.", progress=20) - return await self._runtime.run( - task_id=task_id, - dialog_session_id=dialog_session_id, - rag_session_id=rag_session_id, - mode=request.mode.value, - message=request.message, - attachments=[a.model_dump(mode="json") for a in request.attachments], - files=[f.model_dump(mode="json") for f in request.files], - progress_cb=progress_cb, - ) - - result = await self._retry.run(op) - await self._publish_progress(task_id, "task.finalize", "Сохраняю финальный результат.", progress=95) - task.status = TaskStatus.DONE - task.result_type = TaskResultType(result.result_type) - task.answer = result.answer - task.artifacts = list(getattr(result, "artifacts", []) or []) - task.changeset = result.changeset - if task.result_type != TaskResultType.CHANGESET and (task.answer or task.artifacts): - payload = { - "result_type": task.result_type.value, - "artifacts": [item.model_dump(mode="json") for item in task.artifacts], - } - self._message_sink(dialog_session_id, "assistant", task.answer or "", task_id=task_id, payload=payload) - LOGGER.warning( - "outgoing chat response: task_id=%s dialog_session_id=%s result_type=%s answer=%s", - task_id, - dialog_session_id, - task.result_type.value, - _truncate_for_log(task.answer or ""), - ) - elif task.result_type == TaskResultType.CHANGESET: - self._message_sink( - dialog_session_id, - "assistant", - f"changeset:{len(task.changeset)}", - task_id=task_id, - payload={ - "result_type": TaskResultType.CHANGESET.value, - "changeset": [item.model_dump(mode="json") for item in task.changeset], - }, - ) - LOGGER.warning( - "outgoing chat response: task_id=%s dialog_session_id=%s result_type=%s changeset_items=%s answer=%s", - task_id, - dialog_session_id, - task.result_type.value, - len(task.changeset), - _truncate_for_log(task.answer or ""), - ) - self._task_store.save(task) - await self._events.publish( - task_id, - "task_result", - { - "task_id": task_id, - "status": task.status.value, - "result_type": task.result_type.value, - "answer": task.answer, - "artifacts": [item.model_dump(mode="json") for item in task.artifacts], - "changeset": [item.model_dump(mode="json") for item in task.changeset], - "meta": getattr(result, "meta", {}) or {}, - }, - ) - await self._publish_progress(task_id, "task.done", "Обработка завершена.", progress=100) - LOGGER.info( - "_process_task completed: task_id=%s status=%s result_type=%s changeset_items=%s", - task_id, - task.status.value, - task.result_type.value if task.result_type else "", - len(task.changeset), - ) - except (AppError, TimeoutError, ConnectionError, OSError) as exc: - task.status = TaskStatus.ERROR - if isinstance(exc, AppError): - payload = ErrorPayload(code=exc.code, desc=exc.desc, module=exc.module) - else: - payload = ErrorPayload( - code="retry_exhausted", - desc="Temporary failure after retries. Please retry request.", - module=ModuleName.BACKEND, - ) - task.error = payload - self._task_store.save(task) - await self._publish_progress(task_id, "task.error", "Не удалось завершить обработку запроса.", kind="task_thinking") - await self._events.publish(task_id, "task_error", payload.model_dump(mode="json")) - LOGGER.warning( - "_process_task handled error: task_id=%s code=%s module=%s desc=%s", - task_id, - payload.code, - payload.module.value, - payload.desc, - ) - except Exception: - task.status = TaskStatus.ERROR - payload = ErrorPayload( - code="agent_runtime_error", - desc="Agent execution failed unexpectedly. Please retry request.", - module=ModuleName.AGENT, - ) - task.error = payload - self._task_store.save(task) - await self._publish_progress( - task_id, - "task.error", - "Во время выполнения возникла внутренняя ошибка.", - kind="task_thinking", - ) - await self._events.publish(task_id, "task_error", payload.model_dump(mode="json")) - LOGGER.exception( - "_process_task unexpected error: task_id=%s code=%s", - task_id, - payload.code, - ) - finally: - heartbeat_stop.set() - await heartbeat_task - - async def _publish_progress( - self, - task_id: str, - stage: str, - message: str, - *, - progress: int | None = None, - kind: str = "task_progress", - meta: dict | None = None, - ) -> None: - payload = { - "task_id": task_id, - "stage": stage, - "message": message, - "meta": meta or {}, - } - if progress is not None: - payload["progress"] = max(0, min(100, int(progress))) - await self._events.publish(task_id, kind, payload) - LOGGER.debug( - "_publish_progress emitted: task_id=%s kind=%s stage=%s progress=%s", - task_id, - kind, - stage, - payload.get("progress"), - ) - - async def _run_heartbeat(self, task_id: str, stop_event: asyncio.Event) -> None: - messages = ( - "Собираю данные по проекту.", - "Анализирую контекст и формирую структуру ответа.", - "Проверяю согласованность промежуточного результата.", - ) - index = 0 - while not stop_event.is_set(): - try: - await asyncio.wait_for(stop_event.wait(), timeout=5.0) - except asyncio.TimeoutError: - await self._publish_progress( - task_id, - "task.heartbeat", - messages[index % len(messages)], - kind="task_thinking", - meta={"heartbeat": True}, - ) - index += 1 - LOGGER.debug("_run_heartbeat stopped: task_id=%s ticks=%s", task_id, index) - - def _resolve_sessions(self, request: ChatMessageRequest) -> tuple[str, str]: - return self._session_resolver.resolve(request) diff --git a/src/app/modules/chat/session_resolver.py b/src/app/modules/chat/session_resolver.py deleted file mode 100644 index 653523b..0000000 --- a/src/app/modules/chat/session_resolver.py +++ /dev/null @@ -1,36 +0,0 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING - -from app.core.exceptions import AppError -from app.schemas.chat import ChatMessageRequest -from app.schemas.common import ModuleName - -if TYPE_CHECKING: - from app.modules.chat.dialog_store import DialogSessionStore - - -class ChatSessionResolver: - def __init__(self, dialogs: DialogSessionStore, rag_session_exists) -> None: - self._dialogs = dialogs - self._rag_session_exists = rag_session_exists - - def resolve(self, request: ChatMessageRequest) -> tuple[str, str]: - if request.dialog_session_id and request.rag_session_id: - dialog = self._dialogs.get(request.dialog_session_id) - if not dialog: - raise AppError("dialog_not_found", "Dialog session not found", ModuleName.BACKEND) - if dialog.rag_session_id != request.rag_session_id: - raise AppError("dialog_rag_mismatch", "Dialog session does not belong to rag session", ModuleName.BACKEND) - return request.dialog_session_id, request.rag_session_id - - if request.session_id and request.project_id: - if not self._rag_session_exists(request.project_id): - raise AppError("rag_session_not_found", "RAG session not found", ModuleName.RAG) - return request.session_id, request.project_id - - raise AppError( - "missing_sessions", - "dialog_session_id and rag_session_id are required", - ModuleName.BACKEND, - ) diff --git a/src/app/modules/chat/task_store.py b/src/app/modules/chat/task_store.py deleted file mode 100644 index 4b80247..0000000 --- a/src/app/modules/chat/task_store.py +++ /dev/null @@ -1,38 +0,0 @@ -from dataclasses import dataclass, field -from threading import Lock -from uuid import uuid4 - -from app.schemas.changeset import ChangeItem -from app.schemas.chat import TaskArtifact, TaskResultType, TaskStatus -from app.schemas.common import ErrorPayload - - -@dataclass -class TaskState: - task_id: str - status: TaskStatus = TaskStatus.QUEUED - result_type: TaskResultType | None = None - answer: str | None = None - artifacts: list[TaskArtifact] = field(default_factory=list) - changeset: list[ChangeItem] = field(default_factory=list) - error: ErrorPayload | None = None - - -class TaskStore: - def __init__(self) -> None: - self._items: dict[str, TaskState] = {} - self._lock = Lock() - - def create(self) -> TaskState: - task = TaskState(task_id=str(uuid4())) - with self._lock: - self._items[task.task_id] = task - return task - - def get(self, task_id: str) -> TaskState | None: - with self._lock: - return self._items.get(task_id) - - def save(self, task: TaskState) -> None: - with self._lock: - self._items[task.task_id] = task diff --git a/src/app/modules/orchestration/__init__.py b/src/app/modules/orchestration/__init__.py deleted file mode 100644 index 1c5f050..0000000 --- a/src/app/modules/orchestration/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from app.modules.orchestration.facade import OrchestrationFacade - -__all__ = ["OrchestrationFacade"] diff --git a/src/app/modules/orchestration/adapters/intent_router_adapter.py b/src/app/modules/orchestration/adapters/intent_router_adapter.py deleted file mode 100644 index c20ae83..0000000 --- a/src/app/modules/orchestration/adapters/intent_router_adapter.py +++ /dev/null @@ -1,11 +0,0 @@ -from __future__ import annotations - -from app.modules.agent.intent_router_v2 import IntentRouterV2 - - -class IntentRouterAdapter: - def __init__(self, router: IntentRouterV2) -> None: - self._router = router - - def route(self, user_query: str, conversation_state, repo_context): - return self._router.route(user_query, conversation_state, repo_context) diff --git a/src/app/modules/orchestration/adapters/llm_chat_adapter.py b/src/app/modules/orchestration/adapters/llm_chat_adapter.py deleted file mode 100644 index f80ddc9..0000000 --- a/src/app/modules/orchestration/adapters/llm_chat_adapter.py +++ /dev/null @@ -1,19 +0,0 @@ -from __future__ import annotations - -import asyncio - -from app.modules.agent.llm.service import AgentLlmService - - -class LlmChatAdapter: - def __init__(self, llm: AgentLlmService, prompt_name: str = "agent_api_v1") -> None: - self._llm = llm - self._prompt_name = prompt_name - - async def generate(self, message: str, request_id: str) -> str: - return await asyncio.to_thread( - self._llm.generate, - self._prompt_name, - message, - log_context=f"agent_api:{request_id}", - ) diff --git a/src/app/modules/orchestration/context/execution_context.py b/src/app/modules/orchestration/context/execution_context.py deleted file mode 100644 index 91ea2e7..0000000 --- a/src/app/modules/orchestration/context/execution_context.py +++ /dev/null @@ -1,20 +0,0 @@ -from __future__ import annotations - -from dataclasses import dataclass -from typing import Any - -from app.modules.agent_api.domain.models.agent_request import AgentRequest -from app.modules.agent_api.domain.models.agent_session import AgentSession -from app.modules.agent_api.infrastructure.logging.request_trace_logger import RequestTraceLogger -from app.modules.orchestration.messaging.client_message_publisher import ClientMessagePublisher - - -@dataclass(slots=True) -class ExecutionContext: - request: AgentRequest - session: AgentSession - publisher: ClientMessagePublisher - trace_logger: RequestTraceLogger - task_context: Any = None - route_result: Any = None - workflow_result: Any = None diff --git a/src/app/schemas/agent_api.py b/src/app/schemas/agent_api.py index dc460af..a125282 100644 --- a/src/app/schemas/agent_api.py +++ b/src/app/schemas/agent_api.py @@ -33,7 +33,7 @@ class ResetAgentSessionResponse(BaseModel): class AgentRequestCreateRequest(BaseModel): session_id: str = Field(min_length=1) message: str = Field(min_length=1) - process_version: str = Field(default="v2", min_length=1) + process_version: str = Field(default="v1", min_length=1) class AgentRequestQueuedResponse(BaseModel): diff --git a/tests/unit_tests/chat/test_chat_api_simple_code_explain.py b/tests/unit_tests/chat/test_chat_api_simple_code_explain.py deleted file mode 100644 index bb7afad..0000000 --- a/tests/unit_tests/chat/test_chat_api_simple_code_explain.py +++ /dev/null @@ -1,70 +0,0 @@ -import asyncio - -from app.modules.chat.module import ChatModule -from app.modules.chat.task_store import TaskStore -from app.schemas.chat import ChatMessageRequest -from app.schemas.chat import TaskQueuedResponse -from app.modules.shared.event_bus import EventBus -from app.modules.shared.retry_executor import RetryExecutor - - -class _FakeRuntime: - async def run(self, **kwargs): - raise AssertionError("legacy runtime must not be called") - - -class _FakeDirectChat: - def __init__(self) -> None: - self.calls = 0 - - async def handle_message(self, request): - self.calls += 1 - return TaskQueuedResponse( - task_id="task-1", - status="done", - ) - - -class _FakeRagSessions: - def get(self, rag_session_id: str): - return {"rag_session_id": rag_session_id} - - -class _FakeRepository: - def create_dialog(self, dialog_session_id: str, rag_session_id: str) -> None: - return None - - def get_dialog(self, dialog_session_id: str): - return None - - def add_message(self, dialog_session_id: str, role: str, content: str, task_id: str | None = None, payload: dict | None = None) -> None: - return None - - -def test_chat_messages_endpoint_uses_direct_service(monkeypatch) -> None: - monkeypatch.setenv("SIMPLE_CODE_EXPLAIN_ONLY", "true") - direct_chat = _FakeDirectChat() - module = ChatModule( - agent_runner=_FakeRuntime(), - event_bus=EventBus(), - retry=RetryExecutor(), - rag_sessions=_FakeRagSessions(), - repository=_FakeRepository(), - direct_chat=direct_chat, - task_store=TaskStore(), - ) - router = module.public_router() - endpoint = next(route.endpoint for route in router.routes if getattr(route, "path", "") == "/api/chat/messages") - response = asyncio.run( - endpoint( - ChatMessageRequest( - session_id="dialog-1", - project_id="rag-1", - message="Explain get_user", - ), - None, - ) - ) - - assert response.task_id == "task-1" - assert direct_chat.calls == 1 diff --git a/tests/unit_tests/chat/test_direct_service.py b/tests/unit_tests/chat/test_direct_service.py deleted file mode 100644 index f7b9141..0000000 --- a/tests/unit_tests/chat/test_direct_service.py +++ /dev/null @@ -1,61 +0,0 @@ -import asyncio - -from app.modules.chat.direct_service import CodeExplainChatService -from app.modules.chat.session_resolver import ChatSessionResolver -from app.modules.chat.task_store import TaskStore -from app.modules.agent.runtime.steps.explain.models import ExplainIntent, ExplainPack -from app.schemas.chat import ChatFileContext, ChatMessageRequest - - -class _FakeRetriever: - def build_pack(self, rag_session_id: str, user_query: str, *, file_candidates: list[dict] | None = None) -> ExplainPack: - return ExplainPack( - intent=ExplainIntent(raw_query=user_query, normalized_query=user_query), - missing=["code_excerpts"], - ) - - -class _FakeLlm: - def __init__(self) -> None: - self.calls = 0 - - def generate(self, prompt_name: str, user_input: str, *, log_context: str | None = None) -> str: - self.calls += 1 - return "should not be called" - - -class _FakeDialogs: - def get(self, dialog_session_id: str): - return None - - -def test_direct_service_skips_llm_when_evidence_is_insufficient() -> None: - messages: list[tuple[str, str, str, str | None]] = [] - llm = _FakeLlm() - task_store = TaskStore() - service = CodeExplainChatService( - retriever=_FakeRetriever(), - llm=llm, - session_resolver=ChatSessionResolver(_FakeDialogs(), lambda rag_session_id: rag_session_id == "rag-1"), - task_store=task_store, - message_sink=lambda dialog_session_id, role, content, task_id=None: messages.append((dialog_session_id, role, content, task_id)), - ) - - result = asyncio.run( - service.handle_message( - ChatMessageRequest( - session_id="dialog-1", - project_id="rag-1", - message="Explain get_user", - files=[ChatFileContext(path="app/api/users.py", content="", content_hash="x")], - ) - ) - ) - - task = task_store.get(result.task_id) - assert task is not None - assert task.answer is not None - assert "Недостаточно опоры в коде" in task.answer - assert result.status == "done" - assert llm.calls == 0 - assert [item[1] for item in messages] == ["user", "assistant"]