# V2RetrievalPolicyResolver Architecture ## 1. Роль компонента `V2RetrievalPolicyResolver` это deterministic bridge между `V2IntentRouter` и docs-RAG retrieval. Компонент работает поверх уже готового `V2RouteResult` и не делает повторную интерпретацию пользовательского текста: - не вызывает LLM; - не меняет `intent` и `subintent`; - не ранжирует документы; - не собирает evidence. Его задача: собрать один `RetrievalPlan` с полями: - `profile` - `layers` - `limit` - `filters` ## 2. Зависимости Актуальная реализация опирается на: - `src/app/core/agent/processes/v2/retrieval/policy_resolver.py` - `src/app/core/agent/processes/v2/anchor_signals.py` - `src/app/core/agent/processes/v2/models.py` - `src/app/core/rag/contracts/enums.py` - `src/app/core/agent/processes/v2/retrieval/v2_rag_adapter.py` - `src/app/core/rag/retrieval/session_retriever.py` - `src/app/core/rag/persistence/repository.py` - `src/app/core/rag/persistence/query_repository.py` - `src/app/core/rag/persistence/retrieval_statement_builder.py` ## 3. Входной контракт Resolver использует: - `route.intent` - `route.subintent` - `route.anchors.entity_names` - `route.anchors.file_names` - `route.anchors.endpoint_paths` - `route.anchors.target_doc_hints` - `route.anchors.matched_aliases` - `route.anchors.process_domain` - `route.anchors.process_subdomain` `route.target_terms` в текущей реализации profile/filter branching не влияет. ## 4. Верхнеуровневый branching `resolve(route)` имеет три ветки: 1. `GENERAL_QA` -> `general_qa_grounded_summary` 2. `FIND_FILES` -> `file_lookup` 3. иначе -> docs summary branch Инварианты: - `GENERAL_QA` всегда остаётся general profile; - `FIND_FILES` всегда остаётся `file_lookup`; - resolver всегда возвращает один валидный `RetrievalPlan`. ## 5. Внутренняя декомпозиция Текущая реализация разбита на два helper-класса. ### `_AnchorTermCollector` Собирает термы для `prefer_like_patterns`. Источники: - basename из `target_doc_hints` - `endpoint_paths` - `file_names` - `entity_names` - `matched_aliases` - `process_domain` - `process_subdomain` Все значения нормализуются в lower-case и превращаются в SQL-like patterns вида `"%term%"`. Для `FIND_FILES` действует отдельное правило: - если есть `target_doc_hints`, `prefer_like_patterns` строится только по basename hints; - иначе используется общий набор collected terms. ### `_RouteFilterBuilder` Собирает `filters` для трёх веток: - `general_filters(route)` - `summary_filters(route)` - `find_files_filters(route)` Дополнительно содержит path selection: - `_summary_prefixes(route)` - `_find_files_prefixes(route)` - `_find_files_prefer_prefixes(route)` ## 6. Signal detection Summary profile и часть path preferences зависят от `anchor_signal_types(route)`. Сигналы вычисляются так: - `FIND_FILES` - если `route.subintent == FIND_FILES` - `API_ENDPOINT` - если есть `endpoint_paths` - или в `target_doc_hints` / `file_names` / `matched_aliases` встречаются маркеры `"/api/"`, `"api"`, `"endpoint"` - `ARCHITECTURE` - если в `target_doc_hints` / `file_names` / `matched_aliases` встречаются `"/architecture/"`, `"architecture"`, `"arch"` - `LOGIC_FLOW` - если в `target_doc_hints` / `file_names` / `matched_aliases` встречаются `"/logic/"`, `"logic"`, `"workflow"`, `"flow"`, `"process"` - `DOMAIN_ENTITY` - если есть `entity_names` - или в `target_doc_hints` / `file_names` / `matched_aliases` встречаются `"/domains/"`, `"domain"`, `"entity"`, `"component"` Важно: - `process_domain` и `process_subdomain` сейчас **не участвуют** в signal detection; - они влияют только на filters и `prefer_like_patterns`. ## 7. Summary profile selection Метод `_summary_profile(route)` использует: - `meaningful = anchor_signal_types(route) - {FIND_FILES}` Правило: - если meaningful signal не ровно один -> `docs_summary_generic` - если ровно один: - `API_ENDPOINT` -> `docs_summary_api_endpoint` - `ARCHITECTURE` -> `docs_summary_architecture` - `LOGIC_FLOW` -> `docs_summary_logic_flow` - `DOMAIN_ENTITY` -> `docs_summary_domain_entity` Следствие: - конфликт API + architecture -> generic; - API + entity -> generic; - weak/no signals -> generic. ## 8. Profiles, layers, limits ### `general_qa_grounded_summary` - condition: `route.intent == GENERAL_QA` - layers: `[D1_DOCUMENT_CATALOG, D0_DOC_CHUNKS]` - limit: `8` ### `file_lookup` - condition: `route.subintent == FIND_FILES` - layers: `[D1_DOCUMENT_CATALOG, D3_ENTITY_CATALOG]` - limit: `12` ### `docs_summary_api_endpoint` - layers: `[D1_DOCUMENT_CATALOG, D2_FACT_INDEX, D0_DOC_CHUNKS]` - limit: `8` ### `docs_summary_logic_flow` - layers: `[D4_WORKFLOW_INDEX, D1_DOCUMENT_CATALOG, D0_DOC_CHUNKS]` - limit: `8` ### `docs_summary_domain_entity` - layers: `[D3_ENTITY_CATALOG, D1_DOCUMENT_CATALOG, D0_DOC_CHUNKS]` - limit: `8` ### `docs_summary_architecture` - layers: `[D1_DOCUMENT_CATALOG, D5_RELATION_GRAPH, D0_DOC_CHUNKS]` - limit: `8` ### `docs_summary_generic` - layers: `[D1_DOCUMENT_CATALOG, D0_DOC_CHUNKS]` - limit: `8` ## 9. Filters by branch ### General branch `general_filters(route)` возвращает: - `prefer_path_prefixes = ["docs/architecture/", "docs/"]` - `prefer_like_patterns = ["%readme.md%", "%overview%"]` - `target_doc_hints = list(route.anchors.target_doc_hints)` Это обзорный, но не узкий plan: hard `path_prefixes` здесь нет. ### Summary branch `summary_filters(route)` всегда включает: - `target_doc_hints` - `metadata.domain`, если есть `process_domain` - `metadata.subdomain`, если есть `process_subdomain` - `prefer_path_prefixes` - `prefer_like_patterns` Дополнительно: - если есть `API_ENDPOINT` signal, добавляется hard `path_prefixes = ["docs/api/", "docs/"]` `prefer_path_prefixes` для summary: - API -> `["docs/api/", "docs/"]` - ARCHITECTURE -> `["docs/architecture/", "docs/"]` - LOGIC_FLOW -> `["docs/logic/", "docs/architecture/", "docs/"]` - DOMAIN_ENTITY -> `["docs/domains/", "docs/", "docs/api/"]` - empty signals -> `["docs/"]` Если сигналов несколько, prefixes объединяются и dedupe-ятся с сохранением порядка. ### FIND_FILES branch `find_files_filters(route)` всегда включает: - `target_doc_hints` - `metadata.domain`, если есть `process_domain` - `metadata.subdomain`, если есть `process_subdomain` - `path_prefixes` - `prefer_path_prefixes` - `prefer_like_patterns` `path_prefixes` для `FIND_FILES` выбираются по приоритету: 1. директории из `target_doc_hints` 2. директории из `file_names`, если путь начинается с `docs/` 3. signal-based fallback: - API -> `["docs/api/", "docs/"]` - ARCHITECTURE -> `["docs/architecture/", "docs/"]` - LOGIC_FLOW -> `["docs/logic/", "docs/"]` - DOMAIN_ENTITY -> `["docs/domains/", "docs/"]` 4. default -> `["docs/"]` `prefer_path_prefixes` для `FIND_FILES`: - начинается с `path_prefixes` - если есть `process_domain` или `process_subdomain`, дополнительно добавляет: - `"docs/domains/"` - `"docs/logic/"` ## 10. Hard и soft сигналы в текущей реализации В терминах текущего кода: Hard-ish / narrowing filters: - `path_prefixes` - `metadata.domain` - `metadata.subdomain` Soft preferences: - `prefer_path_prefixes` - `prefer_like_patterns` Отдельно: - `target_doc_hints` всегда сохраняются в `RetrievalPlan.filters`, но **не маппятся напрямую** в `RagRepository.retrieve(...)` как SQL hard filter. То есть сейчас `target_doc_hints` это не прямой DB filter, а downstream anchor для других шагов пайплайна и для deterministic exact-doc seeding logic. ## 11. Интеграция с retrieval stack Следующий слой после resolver теперь исполняет plan не напрямую в `V2Process`, а через `V2RagRetrievalAdapter`. `V2RagRetrievalAdapter.fetch_rows(...)` использует `RetrievalPlan` так: - читает `filters["target_doc_hints"]` из самого плана; - делает exact-path seed через `retrieve_exact_files(...)`; - для missing hints делает substring fallback через `retrieve_chunks_by_path_substrings(...)`; - затем делает обычный semantic retrieve через `RagSessionRetriever.retrieve(...)`; - объединяет exact / substring / semantic rows через dedupe merge. Это важный сдвиг: execution strategy теперь зависит от **контракта `RetrievalPlan`**, а не от скрытой route-specific логики внутри `V2Process`. `RagSessionRetriever._map_filters()` прокидывает в `RagRepository.retrieve(...)`: - `path_prefixes` - `exclude_path_prefixes` - `exclude_like_patterns` - `prefer_path_prefixes` - `prefer_like_patterns` - `prefer_non_tests` - `metadata_domain` из `filters["metadata.domain"]` - `metadata_subdomain` из `filters["metadata.subdomain"]` `RetrievalStatementBuilder.build_retrieve(...)` добавляет SQL predicates: - `lower(metadata_json->>'domain') = :metadata_domain` - `lower(metadata_json->>'subdomain') = :metadata_subdomain` Таким образом: - `process_domain/process_subdomain` реально участвуют в retrieval query; - `target_doc_hints` реально участвуют в retrieval execution strategy на уровне adapter; - `V2RetrievalPolicyResolver` определяет plan contract, а следующий шаг исполняет этот contract более буквально. ## 12. Актуальные ограничения - Логика полностью deterministic. - `target_terms` сейчас не участвуют в branching resolver. - `process_domain/process_subdomain` не влияют на summary profile selection. - API signal добавляет `path_prefixes` даже в generic summary, если среди конфликтующих сигналов присутствует API. - `target_doc_hints` не являются прямым SQL filter внутри обычного `retrieve`, но используются adapter-уровнем для exact-path / substring seeding до semantic retrieval.