фиксирую состояние

This commit is contained in:
2026-04-07 21:41:27 +03:00
parent 7387e5cc51
commit f62fb678b8
52 changed files with 4073 additions and 316 deletions
@@ -78,3 +78,32 @@ def test_find_files_prefers_exact_path_match() -> None:
assert files[0].path == "docs/domains/runtime-health-entity.md"
assert files[0].match_reason in {"exact_path", "alias_match"}
def test_summary_ranking_penalizes_overview_doc_when_specific_api_doc_exists() -> None:
rows = [
{
"path": "docs/overview/health-overview.md",
"title": "Health overview",
"content": "",
"layer": "D1_DOCUMENT_CATALOG",
"metadata": {"summary_text": "Navigation page with related docs.", "document_id": "docs.health_overview"},
},
{
"path": "docs/api/health-endpoint.md",
"title": "Health endpoint",
"content": "",
"layer": "D1_DOCUMENT_CATALOG",
"metadata": {"summary_text": "GET /health returns runtime status.", "document_id": "api.health"},
},
]
route = _route(
hints=["health", "/health", "health endpoint"],
terms=["health"],
)
docs = DocsEvidenceAssembler().assemble_summaries(rows, route)
assert docs[0].path == "docs/api/health-endpoint.md"
assert docs[0].score_breakdown["specificity_boost"] > docs[1].score_breakdown["specificity_boost"]
assert docs[1].score_breakdown["generic_penalty"] < 0
@@ -96,3 +96,38 @@ def test_router_reduces_confidence_for_short_vague_query() -> None:
result = V2IntentRouter(llm=FakeLlm(_llm_response("GENERAL", "GENERAL_QA", "SUMMARY", confidence=0.8))).route("Что это?")
assert result.confidence < 0.8
def test_router_routes_doc_path_to_find_files() -> None:
result = V2IntentRouter(llm=FakeLlm(_llm_response("DOCS", "DOC_EXPLAIN", "SUMMARY"))).route("docs/api/health-endpoint.md")
assert result.subintent == "FIND_FILES"
assert result.anchors.file_names == ["docs/api/health-endpoint.md"]
assert result.anchors.endpoint_paths == []
def test_router_routes_file_token_to_find_files() -> None:
result = V2IntentRouter(llm=FakeLlm(_llm_response("DOCS", "DOC_EXPLAIN", "SUMMARY"))).route("health-endpoint.md")
assert result.subintent == "FIND_FILES"
assert result.anchors.file_names == ["health-endpoint.md"]
assert result.anchors.endpoint_paths == []
def test_router_promotes_api_method_query_to_endpoint_specific_docs_summary() -> None:
result = V2IntentRouter(llm=FakeLlm(_llm_response("DOCS", "DOC_EXPLAIN", "SUMMARY"))).route("Как работает метод health?")
assert result.intent == "DOC_EXPLAIN"
assert result.subintent == "SUMMARY"
assert result.anchors.endpoint_paths == ["/health"]
assert "docs/api/health-endpoint.md" in result.anchors.target_doc_hints
def test_router_keeps_short_api_like_token_as_strong_hint_without_explicit_path() -> None:
result = V2IntentRouter(llm=FakeLlm(_llm_response("DOCS", "DOC_EXPLAIN", "SUMMARY"))).route("Что делает health?")
assert result.intent == "DOC_EXPLAIN"
assert result.subintent == "SUMMARY"
assert result.anchors.endpoint_paths == []
assert "health endpoint" in result.anchors.target_doc_hints
assert "health" in result.target_terms
@@ -51,6 +51,7 @@ def test_file_names_accepts_real_doc_path() -> None:
anchors = V2AnchorExtractor().extract("docs/api/health.md", terms).anchors
assert anchors.file_names == ["docs/api/health.md"]
assert anchors.endpoint_paths == []
def test_file_names_rejects_endpoint_path() -> None:
@@ -60,8 +61,63 @@ def test_file_names_rejects_endpoint_path() -> None:
assert anchors.file_names == []
def test_target_terms_drop_noisy_english_file_words() -> None:
analysis = V2TargetTermsExtractor().extract("pls show doc for /health")
assert analysis.target_terms == ["/health"]
def test_doc_path_does_not_become_endpoint_path() -> None:
analysis = V2TargetTermsExtractor().extract("docs/api/health-endpoint.md")
assert analysis.endpoint_paths == []
def test_target_terms_drop_architecture_marker_words() -> None:
analysis = V2TargetTermsExtractor().extract("Объясни architecture overview сервиса уведомлений")
assert "объясни" not in analysis.target_terms
assert "architecture" not in analysis.target_terms
assert "overview" not in analysis.target_terms
def test_anchor_extractor_extracts_process_domain_and_subdomain() -> None:
terms = V2TargetTermsExtractor().extract("Объясни billing invoice process")
anchors = V2AnchorExtractor().extract("Объясни billing invoice process", terms).anchors
assert anchors.process_domain == "billing"
assert anchors.process_subdomain == "invoice"
def test_file_names_rejects_identifier_like_token() -> None:
terms = V2TargetTermsExtractor().extract("telegram_notify")
anchors = V2AnchorExtractor().extract("telegram_notify", terms).anchors
assert anchors.file_names == []
def test_target_terms_extracts_api_like_anchor_from_method_query() -> None:
analysis = V2TargetTermsExtractor().extract("Как работает метод health?")
assert analysis.target_terms == ["/health", "health"]
assert analysis.endpoint_paths == ["/health"]
assert analysis.api_like_terms == ["health"]
def test_anchor_extractor_builds_endpoint_hints_for_short_api_like_query() -> None:
terms = V2TargetTermsExtractor().extract("Что делает health?")
anchors = V2AnchorExtractor().extract("Что делает health?", terms).anchors
assert anchors.endpoint_paths == []
assert "health" in anchors.target_doc_hints
assert "/health" in anchors.target_doc_hints
assert "health endpoint" in anchors.target_doc_hints
def test_anchor_extractor_keeps_templated_endpoint_for_docs_query() -> None:
terms = V2TargetTermsExtractor().extract("Расскажи про endpoint /users/{id}")
anchors = V2AnchorExtractor().extract("Расскажи про endpoint /users/{id}", terms).anchors
assert anchors.endpoint_paths == ["/users/{id}"]
assert "/users/{id}" in anchors.target_doc_hints
assert "users endpoint" in anchors.target_doc_hints
+39
View File
@@ -284,3 +284,42 @@ def test_v2_process_can_disable_workflow_llm_for_general_summary() -> None:
assert "агрегированный статус runtime" in result.answer
assert llm.calls == []
def test_v2_process_prefers_canonical_health_doc_over_readme_for_method_query() -> None:
llm = FakeLlm("Health explanation.")
adapter = FakeRagAdapter(
summary_rows=[
{
"path": "docs/README.md",
"title": "README",
"content": "",
"layer": "D1_DOCUMENT_CATALOG",
"metadata": {"summary_text": "General documentation index.", "document_id": "docs.readme"},
},
{
"path": "docs/api/health-endpoint.md",
"title": "Health endpoint",
"content": "",
"layer": "D1_DOCUMENT_CATALOG",
"metadata": {
"summary_text": "GET /health returns aggregated runtime status.",
"document_id": "api.health",
},
},
],
file_rows=[],
)
process = _v2_process(llm, adapter)
runtime = _context("Как работает метод health?")
result = asyncio.run(process.run(runtime))
assert result.answer == "Health explanation."
assert llm.calls
assert "docs/api/health-endpoint.md" in llm.calls[0][1]
assert "docs/README.md" not in llm.calls[0][1]
pipeline_events = [payload for _, title, payload in runtime.trace.events if title == "retrieval_profile_selected"]
assert pipeline_events[0]["profile"] == "docs_api_method_explain"
evidence_events = [payload for _, title, payload in runtime.trace.events if title == "evidence_assembled"]
assert any(event.get("primary_doc") == "docs/api/health-endpoint.md" for event in evidence_events if isinstance(event, dict))
@@ -0,0 +1,81 @@
from __future__ import annotations
import asyncio
from app.core.agent.processes.v2.retrieval.v2_rag_adapter import V2RagRetrievalAdapter
from app.core.rag.retrieval.session_retriever import RetrievalPlan
class FakeRetriever:
def __init__(self) -> None:
self.calls: list[tuple[str, object]] = []
async def retrieve(self, _rag_session_id: str, _query_text: str, _plan: RetrievalPlan) -> list[dict]:
self.calls.append(("semantic", None))
return [
{
"path": "docs/api/health-endpoint.md",
"layer": "D1_DOCUMENT_CATALOG",
"metadata": {},
},
{
"path": "docs/api/secondary.md",
"layer": "D0_DOC_CHUNKS",
"metadata": {},
},
]
async def retrieve_exact_files(self, _rag_session_id: str, *, paths: list[str], layers=None, limit: int = 200) -> list[dict]:
del layers, limit
self.calls.append(("exact", list(paths)))
if "docs/api/health-endpoint.md" in paths:
return [
{
"path": "docs/api/health-endpoint.md",
"layer": "D1_DOCUMENT_CATALOG",
"metadata": {},
}
]
return []
async def retrieve_chunks_by_path_substrings(
self,
_rag_session_id: str,
*,
path_needles: list[str],
layers=None,
limit: int = 200,
) -> list[dict]:
del layers, limit
self.calls.append(("substring", list(path_needles)))
return []
def test_v2_rag_adapter_seeds_exact_rows_from_plan_hints() -> None:
adapter = V2RagRetrievalAdapter(FakeRetriever())
plan = RetrievalPlan(
profile="docs_summary_api_endpoint",
layers=["D1_DOCUMENT_CATALOG", "D2_FACT_INDEX", "D0_DOC_CHUNKS"],
limit=8,
filters={"target_doc_hints": ["docs/api/health-endpoint.md"]},
)
rows = asyncio.run(adapter.fetch_rows("rag-1", "explain /health", plan))
assert rows[0]["path"] == "docs/api/health-endpoint.md"
assert len(rows) == 2
def test_v2_rag_adapter_uses_substring_fallback_for_missing_hint() -> None:
retriever = FakeRetriever()
adapter = V2RagRetrievalAdapter(retriever)
plan = RetrievalPlan(
profile="file_lookup",
layers=["D1_DOCUMENT_CATALOG", "D3_ENTITY_CATALOG"],
limit=12,
filters={"target_doc_hints": ["docs/api/missing-health-endpoint.md"]},
)
asyncio.run(adapter.fetch_rows("rag-1", "find file", plan))
assert ("substring", ["missing-health-endpoint.md"]) in retriever.calls
@@ -4,46 +4,132 @@ from app.core.agent.processes.v2.models import V2Domain, V2Intent, V2RouteAnchor
from app.core.agent.processes.v2.retrieval.policy_resolver import V2RetrievalPolicyResolver
def _route(*, hints: list[str], endpoint_paths: list[str] | None = None, subintent: str = "SUMMARY", intent: str = "DOC_EXPLAIN") -> V2RouteResult:
def _route(
*,
intent: str = V2Intent.DOC_EXPLAIN,
subintent: str = V2Subintent.SUMMARY,
entity_names: list[str] | None = None,
file_names: list[str] | None = None,
endpoint_paths: list[str] | None = None,
target_doc_hints: list[str] | None = None,
matched_aliases: list[str] | None = None,
process_domain: str | None = None,
process_subdomain: str | None = None,
) -> V2RouteResult:
return V2RouteResult(
routing_domain=V2Domain.DOCS if intent == V2Intent.DOC_EXPLAIN else V2Domain.GENERAL,
intent=intent,
subintent=subintent,
user_query="q",
normalized_query="q",
anchors=V2RouteAnchors(target_doc_hints=hints, endpoint_paths=endpoint_paths or []),
anchors=V2RouteAnchors(
entity_names=entity_names or [],
file_names=file_names or [],
endpoint_paths=endpoint_paths or [],
target_doc_hints=target_doc_hints or [],
matched_aliases=matched_aliases or [],
process_domain=process_domain,
process_subdomain=process_subdomain,
),
)
def test_policy_prefers_api_docs_for_endpoint_queries() -> None:
def test_policy_maps_api_summary_to_fact_layers() -> None:
plan = V2RetrievalPolicyResolver().resolve(
_route(hints=["docs/api/health-endpoint.md"], endpoint_paths=["/health"])
_route(
endpoint_paths=["/health"],
target_doc_hints=["docs/api/health-endpoint.md"],
)
)
assert plan.profile == "docs_summary_api_endpoint"
assert plan.filters["path_prefixes"] == ["docs/api/", "docs/architecture/", "docs/"]
assert plan.filters["prefer_path_prefixes"][0] == "docs/api/"
assert plan.profile == "docs_api_method_explain"
assert plan.layers == ["D1_DOCUMENT_CATALOG", "D2_FACT_INDEX", "D0_DOC_CHUNKS"]
assert plan.filters["path_prefixes"] == [
"docs/api/",
"docs/endpoints/",
"docs/methods/",
"api/",
"endpoints/",
"methods/",
]
assert plan.filters["target_doc_hints"] == ["docs/api/health-endpoint.md"]
def test_policy_prefers_logic_docs_for_logic_queries() -> None:
plan = V2RetrievalPolicyResolver().resolve(_route(hints=["docs/logic/telegram-notification-loop.md"]))
def test_policy_maps_logic_summary_to_workflow_layers_and_metadata_filters() -> None:
plan = V2RetrievalPolicyResolver().resolve(
_route(
matched_aliases=["logic flow"],
process_domain="notifications",
process_subdomain="delivery_loop",
)
)
assert plan.profile == "docs_summary_logic_flow"
assert plan.layers == ["D4_WORKFLOW_INDEX", "D1_DOCUMENT_CATALOG", "D0_DOC_CHUNKS"]
assert plan.filters["metadata.domain"] == "notifications"
assert plan.filters["metadata.subdomain"] == "delivery_loop"
assert plan.filters["prefer_path_prefixes"][0] == "docs/logic/"
def test_policy_uses_deterministic_find_files_profile() -> None:
def test_policy_maps_entity_summary_to_entity_layers() -> None:
plan = V2RetrievalPolicyResolver().resolve(_route(entity_names=["RuntimeManager"]))
assert plan.profile == "docs_summary_domain_entity"
assert plan.layers == ["D3_ENTITY_CATALOG", "D1_DOCUMENT_CATALOG", "D0_DOC_CHUNKS"]
assert "%runtimemanager%" in plan.filters["prefer_like_patterns"]
def test_policy_keeps_api_method_profile_even_with_additional_entity_signal() -> None:
plan = V2RetrievalPolicyResolver().resolve(
_route(hints=["docs/api/health-endpoint.md"], endpoint_paths=["/health"], subintent=V2Subintent.FIND_FILES)
_route(
endpoint_paths=["/health"],
entity_names=["RuntimeManager"],
)
)
assert plan.profile == "docs_api_method_explain"
assert plan.layers == ["D1_DOCUMENT_CATALOG", "D2_FACT_INDEX", "D0_DOC_CHUNKS"]
def test_policy_uses_api_method_profile_for_endpoint_like_hints_without_explicit_path() -> None:
plan = V2RetrievalPolicyResolver().resolve(
_route(
target_doc_hints=["health", "/health", "health endpoint"],
)
)
assert plan.profile == "docs_api_method_explain"
assert "%health%" in plan.filters["prefer_like_patterns"]
def test_policy_uses_hard_and_soft_filters_for_find_files() -> None:
plan = V2RetrievalPolicyResolver().resolve(
_route(
subintent=V2Subintent.FIND_FILES,
file_names=["docs/workflows/manual-send.md"],
entity_names=["ManualSendWorker"],
matched_aliases=["manual send"],
process_domain="messaging",
process_subdomain="manual_send",
)
)
assert plan.profile == "file_lookup"
assert plan.layers == ["D1_DOCUMENT_CATALOG", "D3_ENTITY_CATALOG"]
assert "health-endpoint.md" in plan.filters["prefer_like_patterns"][0]
assert plan.filters["path_prefixes"] == ["docs/workflows/"]
assert plan.filters["metadata.domain"] == "messaging"
assert "%manualsendworker%" in plan.filters["prefer_like_patterns"]
def test_policy_uses_grounded_general_profile() -> None:
plan = V2RetrievalPolicyResolver().resolve(_route(hints=[], intent=V2Intent.GENERAL_QA))
def test_policy_keeps_general_routes_in_general_profile() -> None:
plan = V2RetrievalPolicyResolver().resolve(
_route(
intent=V2Intent.GENERAL_QA,
endpoint_paths=["/health"],
target_doc_hints=["docs/api/health-endpoint.md"],
)
)
assert plan.profile == "general_qa_grounded_summary"
assert plan.filters["prefer_path_prefixes"][0] == "docs/architecture/"
assert plan.layers == ["D1_DOCUMENT_CATALOG", "D0_DOC_CHUNKS"]
assert "path_prefixes" not in plan.filters
@@ -1,4 +1,8 @@
import logging
from app.core.rag.contracts.enums import RagLayer
from app.core.rag.indexing.docs.chunkers.markdown_chunker import SectionChunk
from app.core.rag.indexing.docs.integration_extractor import DocsIntegrationExtractor
from app.core.rag.indexing.docs.pipeline import DocsIndexingPipeline
@@ -153,3 +157,150 @@ Create invoice
assert integration_doc.metadata["target"] == "db.billing.invoices"
assert integration_doc.metadata["target_type"] == "db"
assert integration_doc.metadata["details"]["transaction"] == "required"
def test_docs_integration_extractor_keeps_valid_blocks() -> None:
extractor = DocsIntegrationExtractor()
sections = [
SectionChunk(
section_path="Details > Интеграции > Billing DB",
section_title="Billing DB",
content=(
"- target: db.billing.invoices\n"
"- target_type: db\n"
"- direction: outbound\n"
"- interaction: writes\n"
"- via: invoice repository\n"
"- purpose: persist created invoices\n"
"- details:\n"
" - transaction: required\n"
" - tables:\n"
" - invoices\n"
" - invoice_items\n"
),
order=0,
)
]
records = extractor.extract(sections, path="docs/billing/create_invoice.md")
assert len(records) == 1
assert records[0].target == "db.billing.invoices"
assert records[0].details["transaction"] == "required"
assert records[0].details["tables"] == ["invoices", "invoice_items"]
def test_docs_integration_extractor_soft_fails_on_markdown_like_yaml(caplog) -> None:
extractor = DocsIntegrationExtractor()
sections = [
SectionChunk(
section_path="Details > Интеграции > Runtime health provider",
section_title="Runtime health provider",
content=(
"- target: runtime.health_provider\n"
"- target_type: service\n"
"- direction: outbound\n"
"- interaction: depends_on\n"
"- via: async callback `health_provider()`\n"
"- purpose: получить агрегированный health runtime\n"
"- details:\n"
" - timeout_ms: 5000\n"
" - response_type: `HealthPayload`\n"
),
order=0,
)
]
with caplog.at_level(logging.WARNING):
records = extractor.extract(sections, path="docs/api/health-endpoint.md")
assert len(records) == 1
assert records[0].target == "runtime.health_provider"
assert records[0].via == "async callback `health_provider()`"
assert records[0].details == {}
assert "docs integration parse warning" in caplog.text
assert "docs/api/health-endpoint.md" in caplog.text
def test_docs_pipeline_keeps_other_layers_when_integration_block_is_invalid(caplog) -> None:
pipeline = DocsIndexingPipeline()
content = """---
id: api.runtime.health
type: api_method
doc_type: api_method
name: runtime_health
title: Runtime Health API
module: runtime
domain: platform
sub_domain: observability
layer: application
status: active
related_docs: []
links:
uses_logic:
- logic.runtime.health
---
# Runtime Health API
## Summary
Returns current runtime health.
## Details
### Описание
Возвращает агрегированное состояние runtime.
### Сценарий
**Название:**
Read health
**Предусловия:**
- runtime is running
**Триггер:**
- client calls health endpoint
**Основной сценарий:**
1. Read current state.
2. Return payload.
### Входные параметры
| field | type | required |
| --- | --- | --- |
| verbose | boolean | no |
### Интеграции
#### Runtime health provider
- target: runtime.health_provider
- target_type: service
- direction: outbound
- interaction: depends_on
- via: async callback `health_provider()`
- purpose: получить агрегированный health runtime
- details:
- timeout_ms: 5000
- response_type: `HealthPayload`
"""
with caplog.at_level(logging.WARNING):
docs = pipeline.index_file(
repo_id="acme/proj",
commit_sha="abc123",
path="docs/api/health-endpoint.md",
content=content,
)
layers = {doc.layer for doc in docs}
assert RagLayer.DOCS_DOCUMENT_CATALOG in layers
assert RagLayer.DOCS_DOC_CHUNKS in layers
assert RagLayer.DOCS_FACT_INDEX in layers
assert RagLayer.DOCS_WORKFLOW_INDEX in layers
assert RagLayer.DOCS_RELATION_GRAPH in layers
assert RagLayer.DOCS_INTEGRATION_INDEX in layers
assert "docs integration parse warning" in caplog.text
assert all(doc.source.path == "docs/api/health-endpoint.md" for doc in docs)
@@ -45,6 +45,23 @@ def test_retrieve_builder_adds_prefer_bonus_sorting() -> None:
assert params["prefer_like_0"] == "%/test\\_%.py"
def test_retrieve_builder_adds_metadata_filters() -> None:
builder = RetrievalStatementBuilder()
sql, params = builder.build_retrieve(
"rag-1",
[0.1, 0.2],
query_text="notification flow",
metadata_domain="notifications",
metadata_subdomain="delivery_loop",
)
assert "metadata_json->>'domain'" in sql
assert "metadata_json->>'subdomain'" in sql
assert params["metadata_domain"] == "notifications"
assert params["metadata_subdomain"] == "delivery_loop"
def test_lexical_builder_omits_test_filters_when_not_requested() -> None:
builder = RetrievalStatementBuilder()