Files
agent/tests/unit_tests/rag/test_docs_prompt_layer.py

174 lines
6.9 KiB
Python

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"