from __future__ import annotations from app.core.agent.intent_router import IntentRouterV2 from app.core.agent.llm.prompt_loader import PromptLoader from app.core.agent.runtime.docs_qa_pipeline import DocsQAPipelineRunner from app.core.agent.runtime.docs_qa_pipeline.openapi_postprocessor import OpenAPIPostprocessor from app.core.agent.runtime.docs_qa_pipeline.prompt_payload_builder import DocsPromptPayloadBuilder from app.core.agent.orchestration.processes.v2.prompt_payload_builder import V2PromptPayloadBuilder from app.core.agent.runtime.steps.generation import RuntimePromptSelector from tests.docs_qa_eval.fixture_adapter import InMemoryDocsRetrievalAdapter from tests.unit_tests.rag.intent_router_testkit import repo_context class FakeLlm: def __init__(self, response: str) -> None: self.response = response self.calls: list[tuple[str, str]] = [] def generate(self, prompt_name: str, user_input: str, *, log_context: str | None = None) -> str: self.calls.append((prompt_name, user_input)) return self.response def test_docs_prompt_registry_contains_new_prompts() -> None: loader = PromptLoader() for name in ( "docs_explain_answer", "docs_general_answer", "docs_openapi_answer", "docs_openapi_fragment_answer", ): assert loader.load(name) def test_prompt_selector_uses_docs_prompts_only() -> None: selector = RuntimePromptSelector() assert selector.select(intent="DOCUMENTATION_EXPLAIN", sub_intent="COMPONENT_EXPLAIN", answer_mode="normal") == "docs_explain_answer" assert selector.select(intent="GENERAL_QA", sub_intent="GENERIC_QA", answer_mode="degraded") == "docs_general_answer" assert selector.select(intent="OPENAPI_GENERATION", sub_intent="OPENAPI_METHOD_GENERATE", answer_mode="normal") == "docs_openapi_answer" assert selector.select(intent="OPENAPI_GENERATION", sub_intent="OPENAPI_FRAGMENT_GENERATE", answer_mode="normal") == "docs_openapi_fragment_answer" def test_docs_prompt_payload_contains_required_contract() -> None: builder = DocsPromptPayloadBuilder() from app.core.agent.runtime.docs_qa_pipeline.models import DocsEvidenceBundle, OpenAPIResult payload = builder.build( question="Объясни billing", intent="DOCUMENTATION_EXPLAIN", sub_intent="COMPONENT_EXPLAIN", evidence_bundle=DocsEvidenceBundle( intent="DOCUMENTATION_EXPLAIN", sub_intent="COMPONENT_EXPLAIN", primary_documents=[{"title": "Billing"}], secondary_documents=[{"title": "Billing relation"}], documents=[{"title": "Billing"}], facts=[{"content": "Handles payments"}], relations=[{"title": "Billing -> Orders"}], ), api_contract=OpenAPIResult(path="/orders", method="post"), ) assert '"question": "Объясни billing"' in payload assert '"intent": "DOCUMENTATION_EXPLAIN"' in payload assert '"sub_intent": "COMPONENT_EXPLAIN"' in payload assert '"primary_documents"' in payload assert '"secondary_documents"' in payload assert '"documents"' in payload assert '"facts"' in payload assert '"relations"' in payload assert '"api_contract"' in payload def test_v2_prompt_payload_accepts_api_method_mode_fields() -> None: from app.core.agent.runtime.docs_qa_pipeline.models import DocsEvidenceBundle payload = V2PromptPayloadBuilder().build( question="Как работает метод health?", intent="DOCUMENTATION_EXPLAIN", sub_intent="API_METHOD_EXPLAIN", evidence_bundle=DocsEvidenceBundle(intent="DOCUMENTATION_EXPLAIN", sub_intent="API_METHOD_EXPLAIN"), api_method_answer_mode="indirect", target_endpoint_identity={ "anchor": "health", "normalized_path": "/health", "normalized_doc_id": "api.health_endpoint", }, direct_api_spec_found=False, ) assert '"api_method_answer_mode": "indirect"' in payload assert '"normalized_doc_id": "api.health_endpoint"' in payload assert '"direct_api_spec_found": false' in payload def test_openapi_postprocessor_requires_paths_for_full_spec() -> None: validator = OpenAPIPostprocessor() assert validator.validate("paths:\n /orders:\n post:\n responses: {}", require_paths=True)[0] is True assert validator.validate("type: object\nproperties:\n id: {}", require_paths=True)[0] is False assert validator.validate("type: object\nproperties:\n id: {}", require_paths=False)[0] is True def test_docs_pipeline_uses_llm_prompt_and_validates_openapi() -> None: rows = [ { "layer": "D2_FACT_INDEX", "path": "docs/api/orders-create.md", "title": "POST /orders", "content": "Create order", "metadata": { "endpoint": "/orders", "http_method": "post", "request_schema": {"type": "object", "properties": {"customer_id": {}}}, "response_schema": {"type": "object", "properties": {"order_id": {}}}, }, } ] llm = FakeLlm("not yaml") runner = DocsQAPipelineRunner( IntentRouterV2(), InMemoryDocsRetrievalAdapter(rows), repo_context=repo_context(), llm=llm, ) result = runner.run("Сгенерируй openapi spec для создания заказа", "docs-session") assert llm.calls assert llm.calls[0][0] == "docs_openapi_answer" assert result.prompt_name == "docs_openapi_answer" assert result.output_valid is False assert "paths:" in result.answer assert result.diagnostics.prompt_used == "docs_openapi_answer" assert result.diagnostics.llm_mode == "yaml" def test_pre_llm_mode_skips_llm_calls() -> None: rows = [ { "layer": "D2_FACT_INDEX", "path": "docs/api/orders-create.md", "title": "POST /orders", "content": "Create order", "metadata": { "endpoint": "/orders", "http_method": "post", "request_schema": {"type": "object", "properties": {"customer_id": {}}}, }, } ] llm = FakeLlm("unused") runner = DocsQAPipelineRunner( IntentRouterV2(), InMemoryDocsRetrievalAdapter(rows), repo_context=repo_context(), llm=llm, ) result = runner.run("Сгенерируй openapi для /orders", "docs-session", mode="pre_llm_only") assert llm.calls == [] assert result.answer assert result.mode == "pre_llm_only" assert result.diagnostics.gate_decision == "partial" assert result.answer_mode == "ready_partial" assert "paths:" in result.answer assert result.llm_request["prompt_name"] == "docs_openapi_answer" assert "user_prompt" in result.llm_request assert "system_prompt" in result.llm_request assert result.diagnostics.prompt["prompt_name"] == "docs_openapi_answer"