From 8fb76bb33175a9d8703d9ad44381271c6535f418 Mon Sep 17 00:00:00 2001 From: zosimovaa Date: Tue, 7 Apr 2026 21:41:27 +0300 Subject: [PATCH] =?UTF-8?q?=D1=84=D0=B8=D0=BA=D1=81=D0=B8=D1=80=D1=83?= =?UTF-8?q?=D1=8E=20=D1=81=D0=BE=D1=81=D1=82=D0=BE=D1=8F=D0=BD=D0=B8=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 8 +- _plan.md | 4 + .../v2_intent_router_architecture.md | 277 ++-- ..._retrieval_policy_resolver_architecture.md | 316 +++++ .../20260407-175918-b17b76678614.md | 130 ++ .../20260407-175948-92e685261a86.md | 1048 +++++++++++++++ .../20260407-182058-3f56c69c7290.md | 622 +++++++++ .../20260407-183339-3284d16c6cb0.md | 1138 +++++++++++++++++ .../core/agent/processes/v2/anchor_signals.py | 23 +- .../agent/processes/v2/evidence/assembler.py | 90 +- .../processes/v2/intent_router/models.py | 1 + .../v2/intent_router/modules/anchors.py | 120 +- .../v2/intent_router/modules/target_terms.py | 152 ++- .../processes/v2/intent_router/router.py | 17 + .../routers/docs_subintent_resolver.py | 8 +- src/app/core/agent/processes/v2/process.py | 67 +- .../processes/v2/retrieval/policy_resolver.py | 294 ++++- .../processes/v2/retrieval/v2_rag_adapter.py | 85 +- .../indexing/docs/integration_extractor.py | 52 +- src/app/core/rag/indexing/docs/pipeline.py | 47 +- .../core/rag/persistence/query_repository.py | 52 + src/app/core/rag/persistence/repository.py | 19 + .../retrieval_statement_builder.py | 18 + .../core/rag/retrieval/session_retriever.py | 4 + tests/pipeline_setup_v4/README.md | 50 +- .../cases.yaml | 540 ++++++++ .../retrieval_policy_v1/20260407_144425.zip | Bin 0 -> 39589 bytes .../retrieval_policy_v2/20260407_145436.zip | Bin 0 -> 75279 bytes .../soft_observational_cases.yaml | 199 +++ .../strict_regression_cases.yaml | 206 +++ .../router_plus_policy_strict_soft_v1.zip | Bin 0 -> 128220 bytes .../20260407_155421.zip | Bin 0 -> 118150 bytes .../router_plus_policy_v1/20260407_150250.zip | Bin 0 -> 31693 bytes .../cases.yaml | 115 ++ .../20260407_155452.zip | Bin 0 -> 28966 bytes .../cases.yaml | 193 +++ .../20260407_162529.zip | Bin 0 -> 82592 bytes .../suite_07/process_v2_full_chain/cases.yaml | 180 +++ tests/pipeline_setup_v4/core/artifacts.py | 6 +- tests/pipeline_setup_v4/core/case_loader.py | 51 +- tests/pipeline_setup_v4/core/models.py | 32 +- tests/pipeline_setup_v4/core/validators.py | 240 +++- .../process_v2_full_chain_executor.py | 121 ++ .../process_v2_retrieval_policy_executor.py | 51 + .../executors/process_v2_router_executor.py | 16 +- .../process_v2_router_plus_policy_executor.py | 79 ++ ...cess_v2_router_plus_policy_rag_executor.py | 94 ++ tests/pipeline_setup_v4/executors/registry.py | 38 + .../agent/test_v2_evidence_ranking.py | 29 + .../unit_tests/agent/test_v2_intent_router.py | 35 + .../agent/test_v2_intent_router_extraction.py | 56 + tests/unit_tests/agent/test_v2_process.py | 39 + tests/unit_tests/agent/test_v2_rag_adapter.py | 81 ++ .../agent/test_v2_retrieval_policy.py | 116 +- .../rag/test_docs_indexing_pipeline.py | 151 +++ .../rag/test_retrieval_statement_builder.py | 17 + 56 files changed, 7011 insertions(+), 316 deletions(-) create mode 100644 _plan.md create mode 100644 _process/components/v2_retrieval_policy_resolver_architecture.md create mode 100644 runtime_traces/agent_requests/20260407-175918-b17b76678614.md create mode 100644 runtime_traces/agent_requests/20260407-175948-92e685261a86.md create mode 100644 runtime_traces/agent_requests/20260407-182058-3f56c69c7290.md create mode 100644 runtime_traces/agent_requests/20260407-183339-3284d16c6cb0.md create mode 100644 tests/pipeline_setup_v4/cases/suite_03/process_v2_retrieval_policy_resolver/cases.yaml create mode 100644 tests/pipeline_setup_v4/cases/suite_03/process_v2_retrieval_policy_resolver/test_runs/retrieval_policy_v1/20260407_144425.zip create mode 100644 tests/pipeline_setup_v4/cases/suite_03/process_v2_retrieval_policy_resolver/test_runs/retrieval_policy_v2/20260407_145436.zip create mode 100644 tests/pipeline_setup_v4/cases/suite_04/process_v2_router_plus_retrieval_policy/soft_observational_cases.yaml create mode 100644 tests/pipeline_setup_v4/cases/suite_04/process_v2_router_plus_retrieval_policy/strict_regression_cases.yaml create mode 100644 tests/pipeline_setup_v4/cases/suite_04/process_v2_router_plus_retrieval_policy/test_runs/router_plus_policy_strict_soft_v1.zip create mode 100644 tests/pipeline_setup_v4/cases/suite_04/process_v2_router_plus_retrieval_policy/test_runs/router_plus_policy_strict_soft_v3/20260407_155421.zip create mode 100644 tests/pipeline_setup_v4/cases/suite_04/process_v2_router_plus_retrieval_policy/test_runs/router_plus_policy_v1/20260407_150250.zip create mode 100644 tests/pipeline_setup_v4/cases/suite_05/process_v2_router_plus_retrieval_policy_quality_gate/cases.yaml create mode 100644 tests/pipeline_setup_v4/cases/suite_05/process_v2_router_plus_retrieval_policy_quality_gate/test_runs/router_plus_policy_qg_v3/20260407_155452.zip create mode 100644 tests/pipeline_setup_v4/cases/suite_06/process_v2_router_plus_retrieval_policy_rag/cases.yaml create mode 100644 tests/pipeline_setup_v4/cases/suite_06/process_v2_router_plus_retrieval_policy_rag/test_runs/router_plus_policy_rag_v1/20260407_162529.zip create mode 100644 tests/pipeline_setup_v4/cases/suite_07/process_v2_full_chain/cases.yaml create mode 100644 tests/pipeline_setup_v4/executors/process_v2_full_chain_executor.py create mode 100644 tests/pipeline_setup_v4/executors/process_v2_retrieval_policy_executor.py create mode 100644 tests/pipeline_setup_v4/executors/process_v2_router_plus_policy_executor.py create mode 100644 tests/pipeline_setup_v4/executors/process_v2_router_plus_policy_rag_executor.py create mode 100644 tests/unit_tests/agent/test_v2_rag_adapter.py diff --git a/.gitignore b/.gitignore index a35594d..d72a197 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,9 @@ .env .venv -__pycache__ \ No newline at end of file +__pycache__ + +# Pipeline harness: per-run artifacts (md/json from tests.pipeline_setup_v3/v4) +tests/**/test_runs/**/*.md +tests/**/test_runs/**/*.json +tests/**/test_results/**/*.md +tests/**/test_results/**/*.json \ No newline at end of file diff --git a/_plan.md b/_plan.md new file mode 100644 index 0000000..ce5247f --- /dev/null +++ b/_plan.md @@ -0,0 +1,4 @@ +# Запросы +1. Какие методы апи есть в проекте +2. Какие методы апи есть для healthcheck +3. Где документация на healthcheck \ No newline at end of file diff --git a/_process/components/v2_intent_router_architecture.md b/_process/components/v2_intent_router_architecture.md index 5ecb641..7982238 100644 --- a/_process/components/v2_intent_router_architecture.md +++ b/_process/components/v2_intent_router_architecture.md @@ -2,31 +2,44 @@ ## 1. Архитектура -Текущий `V2IntentRouter` состоит из следующих компонентов: +Текущий `V2IntentRouter` реализован как **LLM-first router**. +Deterministic-слой не выбирает маршрут по умолчанию и используется только для: + +- preprocessing +- validation ответа LLM +- fallback, если LLM не ответил или вернул невалидный маршрут + +Актуальные компоненты: - `router.py` - Главная точка входа и оркестратор. + Главная точка входа и оркестратор пайплайна. - `modules/normalizer.py` Нормализация текста запроса в `normalized_query`. - `modules/target_terms.py` - Извлечение `target_terms`, `endpoint_paths`, `matched_aliases`, `alias_docs`. + Извлечение retrieval-oriented `target_terms`, `endpoint_paths`, `matched_aliases`, `alias_docs`. - `modules/anchors.py` - Извлечение `anchors` и вспомогательных marker-сигналов. + Извлечение `anchors` и marker-сигналов для fallback и downstream retrieval. -- `routers/docs_subintent_resolver.py` - Определение `subintent`. - -- `routers/deterministic.py` - Детерминированное определение `routing_domain`, `intent`, `subintent`, `confidence`, `routing_mode`, `llm_router_used`, `reason_short`. +- `routers/route_catalog.py` + Каталог допустимых маршрутов (`allowed_routes`). - `routers/llm.py` - LLM-based определение `routing_domain`, `intent`, `subintent`, `confidence`, `reason_short`. + Основной LLM-router. Получает нормализованный запрос, `target_terms`, `anchors` и список допустимых маршрутов. + +- `routers/validator.py` + Deterministic validator для enum-значений, комбинации маршрута и базовой нормализации `confidence`. + +- `routers/confidence.py` + Пост-обработка confidence после ответа LLM. + +- `routers/fallback.py` + Fallback-маршрутизация, если LLM не ответил или ответ не прошёл validator. - `routers/prompts.yml` - Prompt для LLM-router. + Prompt-контракт для LLM-router. ## 2. Контракт @@ -53,7 +66,6 @@ `V2RouteAnchors`: - `entity_names: list[str]` -- `terms: list[str]` - `file_names: list[str]` - `endpoint_paths: list[str]` - `target_doc_hints: list[str]` @@ -78,35 +90,61 @@ - `SUMMARY` - `FIND_FILES` -### Поддерживаемые маршруты +### Допустимые маршруты - `GENERAL / GENERAL_QA / SUMMARY` - `DOCS / DOC_EXPLAIN / SUMMARY` - `DOCS / DOC_EXPLAIN / FIND_FILES` -## 4. Флоу обработки запроса +Эти маршруты централизованно заданы в `routers/route_catalog.py`. + +## 4. Актуальный флоу + +Пайплайн обработки запроса: 1. `router.py` принимает `user_query`. 2. `modules/normalizer.py` строит `normalized_query`. -3. `modules/target_terms.py` извлекает ключевые термы и alias-based сигналы. -4. `modules/anchors.py` строит `anchors` и marker-сигналы. +3. `modules/target_terms.py` извлекает: + - `target_terms` + - `endpoint_paths` + - `matched_aliases` + - `alias_docs` +4. `modules/anchors.py` строит: + - `anchors` + - `file_markers` + - `architecture_markers` + - `logic_markers` + - `domain_markers` + - `endpoint_markers` 5. `router.py` собирает `QueryFeatures`. -6. `routers/deterministic.py` пытается определить маршрут детерминированно. -7. Если deterministic route найден, он сразу возвращается. -8. Если deterministic route не найден, `router.py` вызывает `routers/llm.py`. -9. Если LLM вернул валидный маршрут, собирается `V2RouteResult` с `routing_mode="llm_assisted"`. -10. Если LLM недоступен или не вернул валидный маршрут, используется fallback: - `GENERAL / GENERAL_QA / SUMMARY` с `routing_mode="llm_fallback"`. +6. `routers/llm.py` вызывается как **основной селектор маршрута**. +7. `routers/validator.py` проверяет: + - что значения входят в допустимые enum + - что комбинация маршрута разрешена + - что `confidence` можно привести к `float` +8. `routers/confidence.py` корректирует confidence на основе силы сигналов. +9. Если ответ LLM валиден, возвращается `V2RouteResult` с `routing_mode="llm_default"`. +10. Если LLM не ответил, вернул сломанный JSON или невалидный маршрут, `routers/fallback.py` строит fallback route: + - `FIND_FILES`, если есть `file_markers` + - `DOCS / DOC_EXPLAIN / SUMMARY`, если есть docs-oriented anchors + - иначе `GENERAL / GENERAL_QA / SUMMARY` ## 5. Компоненты по флоу ### `router.py` - Задача - Собрать весь процесс роутинга в одной входной точке. + Оркестрировать полный routing pipeline. - Как решает - Последовательно вызывает normalizer, target terms extractor, anchors extractor, deterministic router и при необходимости LLM router. + Последовательно вызывает: + - normalizer + - target terms extractor + - anchor extractor + - LLM router + - validator + - confidence adjuster + - fallback router - Вход `user_query: str` @@ -117,7 +155,7 @@ ### `modules/normalizer.py` - Задача - Привести запрос к стабильной форме для дальнейшего анализа. + Привести запрос к стабильной форме для анализа. - Как решает Схлопывает лишние пробелы через `" ".join(...split())`. @@ -131,14 +169,29 @@ ### `modules/target_terms.py` - Задача - Выделить ключевые термы и retrieval-сигналы из запроса. + Построить **чистое retrieval-поле** `target_terms`. - Как решает - Использует: - - regex для path/entity-like фрагментов - - список stop-words - - alias rules с фразами и каноническими термами - - эвристику для `/health` + Использует позитивную модель отбора и включает в `target_terms` только: + - endpoint paths + - identifier-like tokens + - alias canonical terms + - domain terms + + Исключаются: + - question words + - intent words + - filler/noisy words + - marker words + - короткие токены `< 3`, если это не endpoint или alias + - битые path-like токены + + Дополнительно: + - lowercase + - trim punctuation по краям + - dedupe + - ограничение до `7` элементов + - приоритет: endpoints → identifiers → aliases → domain terms - Вход `normalized_query: str` @@ -153,117 +206,141 @@ ### `modules/anchors.py` - Задача - Построить полный набор `anchors` и doc-oriented marker-сигналов. + Построить `anchors` и marker-сигналы, не смешивая их с `target_terms`. - Как решает - Использует: - - regex для `entity_names` и `file_names` - - словари marker-фраз: - - file markers - - architecture markers - - logic markers - - domain markers - - endpoint markers - - map `endpoint -> target_doc_hint` - - alias docs из `TargetTermsAnalysis` + Извлекает: + - `entity_names` из PascalCase-like токенов + - `file_names` только по жёстким правилам: + - `*.md`, `*.yaml`, `*.yml`, `*.json` + - `docs/...`, `doc/...`, `documentation/...` + - `endpoint_paths` из `TargetTermsAnalysis` + - `target_doc_hints` из alias docs, endpoint map и marker-сигналов -- Вход - - `normalized_query: str` - - `TargetTermsAnalysis` - -- Выход - `AnchorAnalysis`: - - `anchors` + Marker-сигналы живут отдельно: - `file_markers` - `architecture_markers` - `logic_markers` - `domain_markers` - `endpoint_markers` -### `routers/docs_subintent_resolver.py` - -- Задача - Определить `subintent`. - -- Как решает - Эвристика: - - если есть `file_markers` -> `FIND_FILES` - - если есть doc-signals (`endpoint_paths`, `endpoint_markers`, `architecture_markers`, `logic_markers`, `domain_markers`, `target_doc_hints`) -> `SUMMARY` - - иначе `None` - - Вход - `QueryFeatures` + - `normalized_query: str` + - `TargetTermsAnalysis` - Выход - `subintent: str | None` + `AnchorAnalysis` -### `routers/deterministic.py` +### `routers/route_catalog.py` - Задача - Детерминированно определить маршрут без LLM там, где это возможно. + Держать один источник истины для допустимых маршрутов. - Как решает - Использует: - - `DocsSubintentResolver` - - проверку conflicting doc anchors - - список general markers - - Правила: - - `FIND_FILES` -> `DOCS / DOC_EXPLAIN / FIND_FILES` - - `subintent != None` и нет конфликта doc-signals -> `DOCS / DOC_EXPLAIN / SUMMARY` - - general marker -> `GENERAL / GENERAL_QA / SUMMARY` - -- Вход - - `user_query: str` - - `QueryFeatures` - - `anchors: V2RouteAnchors` - -- Выход - `V2RouteResult | None` + Возвращает: + - список `allowed_routes` для payload LLM + - проверку допустимости комбинации `routing_domain + intent + subintent` ### `routers/llm.py` - Задача - Определить маршрут через LLM, если deterministic routing не дал результата. + Выбрать маршрут через LLM как основной селектор. - Как решает Формирует JSON payload из: - - `user_query` - `normalized_query` - `target_terms` - `anchors` - - списка допустимых маршрутов + - `allowed_routes` Затем: - вызывает LLM - парсит JSON - - валидирует маршрут по whitelist - - нормализует `confidence` + - возвращает сырой candidate route без deterministic business-routing - Вход - - `user_query: str` - `normalized_query: str` - `target_terms: list[str]` - `anchors: dict` - Выход - `dict | None`: + `dict | None` + +### `routers/validator.py` + +- Задача + Deterministic validation ответа LLM. + +- Как решает + Проверяет: + - что `routing_domain`, `intent`, `subintent` заполнены + - что комбинация маршрута входит в `route_catalog` + - что `confidence` можно привести к числу + +- Вход + `dict | None` + +- Выход + Валидированный `dict | None` + +### `routers/confidence.py` + +- Задача + Сделать confidence осмысленным после ответа LLM. + +- Как решает + Корректирует confidence: + - `-0.1`, если нет strong anchors + - `-0.1`, если запрос короткий или vague + - `+0.05`, если есть явный signal (`file_markers`, `endpoint_paths`, `endpoint_markers`) + - затем clamp в диапазон `0.0..1.0` + +- Вход + - `confidence: float` + - `QueryFeatures` + +- Выход + `confidence: float` + +### `routers/fallback.py` + +- Задача + Построить deterministic fallback, если LLM невалиден. + +- Как решает + Правила: + - есть `file_markers` → `DOCS / DOC_EXPLAIN / FIND_FILES` + - есть docs-signals (`endpoint_paths`, `target_doc_hints`, `matched_aliases`, marker groups) → `DOCS / DOC_EXPLAIN / SUMMARY` + - иначе → `GENERAL / GENERAL_QA / SUMMARY` + +- Вход + - `user_query: str` + - `QueryFeatures` + - `anchors: V2RouteAnchors` + - `llm_attempted: bool` + +- Выход + `V2RouteResult` + +### `routers/prompts.yml` + +- Задача + Задать LLM-router контракт ответа и guidance по confidence. + +- Как решает + Ограничивает модель только `allowed_routes` и требует JSON с полями: - `routing_domain` - `intent` - `subintent` - `confidence` - `reason_short` -### `routers/prompts.yml` +## 6. Ключевые инварианты -- Задача - Задать LLM-router формальный контракт ответа. - -- Как решает - Описывает допустимые маршруты и требует вернуть только JSON. - -- Вход - Payload от `routers/llm.py` - -- Выход - Структурированный JSON-ответ LLM +- LLM является default router. +- Deterministic-слой не принимает основной routing decision. +- `target_terms` содержат только retrieval-useful terms. +- `anchors` не содержат `terms`. +- `/health` и другие endpoint paths не должны попадать в `file_names`, если это не файл с расширением. +- `file_names` содержат только реальные file/doc paths. +- Fallback используется только если LLM недоступен или вернул невалидный маршрут. diff --git a/_process/components/v2_retrieval_policy_resolver_architecture.md b/_process/components/v2_retrieval_policy_resolver_architecture.md new file mode 100644 index 0000000..f0f1948 --- /dev/null +++ b/_process/components/v2_retrieval_policy_resolver_architecture.md @@ -0,0 +1,316 @@ +# 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. diff --git a/runtime_traces/agent_requests/20260407-175918-b17b76678614.md b/runtime_traces/agent_requests/20260407-175918-b17b76678614.md new file mode 100644 index 0000000..2a4753f --- /dev/null +++ b/runtime_traces/agent_requests/20260407-175918-b17b76678614.md @@ -0,0 +1,130 @@ +# Runtime Trace: 20260407-175918-b17b76678614 + +- active_rag_session_id: 94851e51-1514-4a77-9570-b17b76678614 + +## request +```json +{ + "request_id": "req_d9dae665c88b476db700a3f7bd210370", + "session_id": "as_da5ddd4aacd94ec5b7078dd69e06c9c6", + "active_rag_session_id": "94851e51-1514-4a77-9570-b17b76678614", + "process_version": "v1", + "created_at": "2026-04-07T17:59:18.592170+00:00", + "message": "Ты тут?" +} +``` + +## workflow.v1 +```json +{ + "event": "workflow_started", + "workflow_id": "v1.flow_main" +} +``` + +## workflow.v1 +```json +{ + "event": "step_started", + "workflow_id": "v1.flow_main", + "step_id": "prepare_user_message", + "input": {} +} +``` + +## workflow.v1 +```json +{ + "event": "step_completed", + "workflow_id": "v1.flow_main", + "step_id": "prepare_user_message", + "output": { + "prepared_message_length": 7 + } +} +``` + +## workflow.v1 +```json +{ + "event": "step_started", + "workflow_id": "v1.flow_main", + "step_id": "generate_answer", + "input": { + "prompt_name": "v1_flow_main.answer", + "prepared_message_length": 7 + } +} +``` + +## workflow.v1.llm +```json +{ + "event": "request", + "prompt_name": "v1_flow_main.answer", + "system_prompt": "Ты полезный ассистент.\nОтветь на сообщение пользователя по существу.\nНе придумывай факты, если данных недостаточно.\nЕсли пользователь пишет по-русски, отвечай по-русски.", + "user_prompt": "Ты тут?", + "log_context": "agent:req_d9dae665c88b476db700a3f7bd210370" +} +``` + +## workflow.v1.llm +```json +{ + "event": "response", + "text": "Да, я здесь! Чем могу помочь?" +} +``` + +## workflow.v1 +```json +{ + "event": "step_completed", + "workflow_id": "v1.flow_main", + "step_id": "generate_answer", + "output": { + "answer_length": 29 + } +} +``` + +## workflow.v1 +```json +{ + "event": "step_started", + "workflow_id": "v1.flow_main", + "step_id": "finalize_answer", + "input": { + "answer_length_before_strip": 29 + } +} +``` + +## workflow.v1 +```json +{ + "event": "step_completed", + "workflow_id": "v1.flow_main", + "step_id": "finalize_answer", + "output": { + "answer_length": 29 + } +} +``` + +## workflow.v1 +```json +{ + "event": "workflow_completed", + "workflow_id": "v1.flow_main" +} +``` + +## result +```json +{ + "status": "done", + "answer": "Да, я здесь! Чем могу помочь?", + "completed_at": "2026-04-07T17:59:19.326182+00:00" +} +``` diff --git a/runtime_traces/agent_requests/20260407-175948-92e685261a86.md b/runtime_traces/agent_requests/20260407-175948-92e685261a86.md new file mode 100644 index 0000000..14bfe12 --- /dev/null +++ b/runtime_traces/agent_requests/20260407-175948-92e685261a86.md @@ -0,0 +1,1048 @@ +# Runtime Trace: 20260407-175948-92e685261a86 + +- active_rag_session_id: 965a992e-8be3-45cf-8a31-92e685261a86 + +## request +```json +{ + "request_id": "req_36c125fd77054be599054b080ea92d49", + "session_id": "as_21b7fe278b9548938d3a066eb02d5ed3", + "active_rag_session_id": "965a992e-8be3-45cf-8a31-92e685261a86", + "process_version": "v2", + "created_at": "2026-04-07T17:59:48.924496+00:00", + "message": "В какой файле документация на эндпоинт health?" +} +``` + +## process.v2 +```json +{ + "event": "intent_routed", + "routing_domain": "DOCS", + "intent": "DOC_EXPLAIN", + "subintent": "FIND_FILES", + "normalized_query": "В какой файле документация на эндпоинт health?", + "target_terms": [ + "эндпоинт", + "health" + ], + "anchors": { + "entity_names": [], + "file_names": [], + "endpoint_paths": [], + "target_doc_hints": [], + "matched_aliases": [], + "process_domain": null, + "process_subdomain": null, + "signal_types": [ + "FIND_FILES" + ] + }, + "confidence": 0.8500000000000001, + "routing_mode": "llm_default", + "llm_router_used": true, + "reason_short": "Запрос явно ищет файл с документацией.", + "rag_session_id": "965a992e-8be3-45cf-8a31-92e685261a86" +} +``` + +## process.v2.pipeline +```json +{ + "event": "router_resolved", + "domain": "DOCS", + "intent": "DOC_EXPLAIN", + "subintent": "FIND_FILES", + "confidence": 0.8500000000000001 +} +``` + +## process.v2.pipeline +```json +{ + "event": "anchors_extracted", + "signal_types": [ + "FIND_FILES" + ], + "endpoint_paths": [], + "target_doc_hints": [], + "matched_aliases": [], + "target_terms": [ + "эндпоинт", + "health" + ] +} +``` + +## process.v2.pipeline +```json +{ + "event": "alias_resolution", + "resolved_aliases": [], + "target_doc_hints": [] +} +``` + +## process.v2.retrieval_policy +```json +{ + "event": "retrieval_plan_resolved", + "profile": "file_lookup", + "layers": [ + "D1_DOCUMENT_CATALOG", + "D3_ENTITY_CATALOG" + ], + "limit": 12, + "filters": { + "target_doc_hints": [], + "path_prefixes": [ + "docs/" + ], + "prefer_path_prefixes": [ + "docs/" + ], + "prefer_like_patterns": [] + } +} +``` + +## process.v2.pipeline +```json +{ + "event": "retrieval_profile_selected", + "profile": "file_lookup", + "layers": [ + "D1_DOCUMENT_CATALOG", + "D3_ENTITY_CATALOG" + ], + "filters": { + "target_doc_hints": [], + "path_prefixes": [ + "docs/" + ], + "prefer_path_prefixes": [ + "docs/" + ], + "prefer_like_patterns": [] + } +} +``` + +## process.v2.rag_retrieval +```json +{ + "event": "rag_rows_fetched", + "profile": "file_lookup", + "row_count": 3, + "rows": [ + { + "layer": "D1_DOCUMENT_CATALOG", + "path": "docs/README.md", + "title": "Индекс технической документации test_echo_app", + "document_id": "index.test_echo_app_docs", + "entity_name": "", + "summary_text": "- Purpose: точка входа в техническую документацию сервиса `test_echo_app`.\n- Scope: архитектура, HTTP API control plane, цикл отправки уведомлений, health-модель и каталог ошибок.\n- Canonical structure: `docs/architecture`, `docs/api`, `docs/logic`, `docs/domains`, `docs/errors`.\n- Primary parent doc: [Архитектура Telegram Notify App](./architecture/telegram-notify-app-overview.md).\n- Navigation: ", + "section_path": "", + "content_preview": "- Purpose: точка входа в техническую документацию сервиса `test_echo_app`.\n- Scope: архитектура, HTTP API control plane, цикл отправки уведомлений, health-модель и каталог ошибок.\n- Canonical structure: `docs/architecture`, `docs/api`, `docs/logic`, `docs/domains`, `docs/errors`.\n- Primary parent doc: [Архитектура Telegram Notify App](./architecture/telegram-notify-app-overview.md).\n- Navigation: " + }, + { + "layer": "D1_DOCUMENT_CATALOG", + "path": "docs/architecture/telegram-notify-app-overview.md", + "title": "Архитектура Telegram Notify App", + "document_id": "architecture.telegram_notify_app", + "entity_name": "", + "summary_text": "- Purpose: сервис поднимает HTTP control plane и фоновый worker для отправки уведомлений в Telegram.\n- Entry point: `src/telegram_notify_app/main.py`.\n- Main components: `RuntimeManager`, `TelegramControlChannel`, `TelegramNotifyModule`, `TelegramNotifyWorker`, `TelegramSendService`.\n- Configuration: `config/config.yaml` или путь из `CONFIG_PATH`.\n- Related API: [`/health`](../api/health-endpoint.", + "section_path": "", + "content_preview": "- Purpose: сервис поднимает HTTP control plane и фоновый worker для отправки уведомлений в Telegram.\n- Entry point: `src/telegram_notify_app/main.py`.\n- Main components: `RuntimeManager`, `TelegramControlChannel`, `TelegramNotifyModule`, `TelegramNotifyWorker`, `TelegramSendService`.\n- Configuration: `config/config.yaml` или путь из `CONFIG_PATH`.\n- Related API: [`/health`](../api/health-endpoint." + }, + { + "layer": "D3_ENTITY_CATALOG", + "path": "docs/architecture/telegram-notify-app-overview.md", + "title": "TelegramNotifyModule", + "document_id": "architecture.telegram_notify_app", + "entity_name": "TelegramNotifyModule", + "summary_text": "", + "section_path": "", + "content_preview": "TelegramNotifyModule" + } + ] +} +``` + +## process.v2.pipeline +```json +{ + "event": "candidate_generation", + "query": "В какой файле документация на эндпоинт health?", + "profile": "file_lookup", + "details": { + "target_doc_hints": [], + "candidates_before_ranking": [ + "docs/README.md", + "docs/architecture/telegram-notify-app-overview.md", + "docs/architecture/telegram-notify-app-overview.md" + ] + }, + "resolved_aliases": [], + "target_doc_hints": [], + "candidate_docs_before_ranking": [ + { + "layer": "D1_DOCUMENT_CATALOG", + "path": "docs/README.md", + "title": "Индекс технической документации test_echo_app", + "document_id": "index.test_echo_app_docs", + "entity_name": "", + "summary_text": "- Purpose: точка входа в техническую документацию сервиса `test_echo_app`.\n- Scope: архитектура, HTTP API control plane, цикл отправки уведомлений, health-модель и каталог ошибок.\n- Canonical structure: `docs/architecture`, `docs/api`, `docs/logic`, `docs/domains`, `docs/errors`.\n- Primary parent doc: [Архитектура Telegram Notify App](./architecture/telegram-notify-app-overview.md).\n- Navigation: ", + "section_path": "", + "content_preview": "- Purpose: точка входа в техническую документацию сервиса `test_echo_app`.\n- Scope: архитектура, HTTP API control plane, цикл отправки уведомлений, health-модель и каталог ошибок.\n- Canonical structure: `docs/architecture`, `docs/api`, `docs/logic`, `docs/domains`, `docs/errors`.\n- Primary parent doc: [Архитектура Telegram Notify App](./architecture/telegram-notify-app-overview.md).\n- Navigation: " + }, + { + "layer": "D1_DOCUMENT_CATALOG", + "path": "docs/architecture/telegram-notify-app-overview.md", + "title": "Архитектура Telegram Notify App", + "document_id": "architecture.telegram_notify_app", + "entity_name": "", + "summary_text": "- Purpose: сервис поднимает HTTP control plane и фоновый worker для отправки уведомлений в Telegram.\n- Entry point: `src/telegram_notify_app/main.py`.\n- Main components: `RuntimeManager`, `TelegramControlChannel`, `TelegramNotifyModule`, `TelegramNotifyWorker`, `TelegramSendService`.\n- Configuration: `config/config.yaml` или путь из `CONFIG_PATH`.\n- Related API: [`/health`](../api/health-endpoint.", + "section_path": "", + "content_preview": "- Purpose: сервис поднимает HTTP control plane и фоновый worker для отправки уведомлений в Telegram.\n- Entry point: `src/telegram_notify_app/main.py`.\n- Main components: `RuntimeManager`, `TelegramControlChannel`, `TelegramNotifyModule`, `TelegramNotifyWorker`, `TelegramSendService`.\n- Configuration: `config/config.yaml` или путь из `CONFIG_PATH`.\n- Related API: [`/health`](../api/health-endpoint." + }, + { + "layer": "D3_ENTITY_CATALOG", + "path": "docs/architecture/telegram-notify-app-overview.md", + "title": "TelegramNotifyModule", + "document_id": "architecture.telegram_notify_app", + "entity_name": "TelegramNotifyModule", + "summary_text": "", + "section_path": "", + "content_preview": "TelegramNotifyModule" + } + ], + "sources": { + "seeded": [], + "metadata_lookup": [], + "semantic": [ + { + "layer": "D1_DOCUMENT_CATALOG", + "path": "docs/README.md", + "title": "Индекс технической документации test_echo_app", + "document_id": "index.test_echo_app_docs", + "entity_name": "", + "summary_text": "- Purpose: точка входа в техническую документацию сервиса `test_echo_app`.\n- Scope: архитектура, HTTP API control plane, цикл отправки уведомлений, health-модель и каталог ошибок.\n- Canonical structure: `docs/architecture`, `docs/api`, `docs/logic`, `docs/domains`, `docs/errors`.\n- Primary parent doc: [Архитектура Telegram Notify App](./architecture/telegram-notify-app-overview.md).\n- Navigation: ", + "section_path": "", + "content_preview": "- Purpose: точка входа в техническую документацию сервиса `test_echo_app`.\n- Scope: архитектура, HTTP API control plane, цикл отправки уведомлений, health-модель и каталог ошибок.\n- Canonical structure: `docs/architecture`, `docs/api`, `docs/logic`, `docs/domains`, `docs/errors`.\n- Primary parent doc: [Архитектура Telegram Notify App](./architecture/telegram-notify-app-overview.md).\n- Navigation: " + }, + { + "layer": "D1_DOCUMENT_CATALOG", + "path": "docs/architecture/telegram-notify-app-overview.md", + "title": "Архитектура Telegram Notify App", + "document_id": "architecture.telegram_notify_app", + "entity_name": "", + "summary_text": "- Purpose: сервис поднимает HTTP control plane и фоновый worker для отправки уведомлений в Telegram.\n- Entry point: `src/telegram_notify_app/main.py`.\n- Main components: `RuntimeManager`, `TelegramControlChannel`, `TelegramNotifyModule`, `TelegramNotifyWorker`, `TelegramSendService`.\n- Configuration: `config/config.yaml` или путь из `CONFIG_PATH`.\n- Related API: [`/health`](../api/health-endpoint.", + "section_path": "", + "content_preview": "- Purpose: сервис поднимает HTTP control plane и фоновый worker для отправки уведомлений в Telegram.\n- Entry point: `src/telegram_notify_app/main.py`.\n- Main components: `RuntimeManager`, `TelegramControlChannel`, `TelegramNotifyModule`, `TelegramNotifyWorker`, `TelegramSendService`.\n- Configuration: `config/config.yaml` или путь из `CONFIG_PATH`.\n- Related API: [`/health`](../api/health-endpoint." + }, + { + "layer": "D3_ENTITY_CATALOG", + "path": "docs/architecture/telegram-notify-app-overview.md", + "title": "TelegramNotifyModule", + "document_id": "architecture.telegram_notify_app", + "entity_name": "TelegramNotifyModule", + "summary_text": "", + "section_path": "", + "content_preview": "TelegramNotifyModule" + } + ] + } +} +``` + +## process.v2.pipeline +```json +{ + "event": "retrieval_executed", + "query": "В какой файле документация на эндпоинт health?", + "profile": "file_lookup", + "row_count": 3, + "target_doc_hints": [], + "top_results": [ + { + "layer": "D1_DOCUMENT_CATALOG", + "path": "docs/README.md", + "title": "Индекс технической документации test_echo_app", + "document_id": "index.test_echo_app_docs", + "entity_name": "", + "summary_text": "- Purpose: точка входа в техническую документацию сервиса `test_echo_app`.\n- Scope: архитектура, HTTP API control plane, цикл отправки уведомлений, health-модель и каталог ошибок.\n- Canonical structure: `docs/architecture`, `docs/api`, `docs/logic`, `docs/domains`, `docs/errors`.\n- Primary parent doc: [Архитектура Telegram Notify App](./architecture/telegram-notify-app-overview.md).\n- Navigation: ", + "section_path": "", + "content_preview": "- Purpose: точка входа в техническую документацию сервиса `test_echo_app`.\n- Scope: архитектура, HTTP API control plane, цикл отправки уведомлений, health-модель и каталог ошибок.\n- Canonical structure: `docs/architecture`, `docs/api`, `docs/logic`, `docs/domains`, `docs/errors`.\n- Primary parent doc: [Архитектура Telegram Notify App](./architecture/telegram-notify-app-overview.md).\n- Navigation: " + }, + { + "layer": "D1_DOCUMENT_CATALOG", + "path": "docs/architecture/telegram-notify-app-overview.md", + "title": "Архитектура Telegram Notify App", + "document_id": "architecture.telegram_notify_app", + "entity_name": "", + "summary_text": "- Purpose: сервис поднимает HTTP control plane и фоновый worker для отправки уведомлений в Telegram.\n- Entry point: `src/telegram_notify_app/main.py`.\n- Main components: `RuntimeManager`, `TelegramControlChannel`, `TelegramNotifyModule`, `TelegramNotifyWorker`, `TelegramSendService`.\n- Configuration: `config/config.yaml` или путь из `CONFIG_PATH`.\n- Related API: [`/health`](../api/health-endpoint.", + "section_path": "", + "content_preview": "- Purpose: сервис поднимает HTTP control plane и фоновый worker для отправки уведомлений в Telegram.\n- Entry point: `src/telegram_notify_app/main.py`.\n- Main components: `RuntimeManager`, `TelegramControlChannel`, `TelegramNotifyModule`, `TelegramNotifyWorker`, `TelegramSendService`.\n- Configuration: `config/config.yaml` или путь из `CONFIG_PATH`.\n- Related API: [`/health`](../api/health-endpoint." + }, + { + "layer": "D3_ENTITY_CATALOG", + "path": "docs/architecture/telegram-notify-app-overview.md", + "title": "TelegramNotifyModule", + "document_id": "architecture.telegram_notify_app", + "entity_name": "TelegramNotifyModule", + "summary_text": "", + "section_path": "", + "content_preview": "TelegramNotifyModule" + } + ] +} +``` + +## process.v2.evidence +```json +{ + "event": "evidence_assembled", + "mode": "find_files", + "file_count": 2, + "files": [ + "docs/README.md", + "docs/architecture/telegram-notify-app-overview.md" + ] +} +``` + +## process.v2.pipeline +```json +{ + "event": "evidence_assembled", + "mode": "find_files", + "primary_file": "docs/README.md", + "file_count": 2 +} +``` + +## process.v2.pipeline +```json +{ + "event": "ranking_explained", + "doc": "docs/README.md", + "score_breakdown": { + "semantic": 10, + "path_match": 0, + "filename_match": 0, + "alias_match": 0, + "anchor_boost": 120, + "target_doc_boost": 0, + "generic_penalty": 0 + }, + "score": 130, + "match_reason": "semantic_match" +} +``` + +## process.v2.pipeline +```json +{ + "event": "ranking_explained", + "doc": "docs/architecture/telegram-notify-app-overview.md", + "score_breakdown": { + "semantic": 10, + "path_match": 0, + "filename_match": 0, + "alias_match": 0, + "anchor_boost": 120, + "target_doc_boost": 0, + "generic_penalty": 0 + }, + "score": 130, + "match_reason": "semantic_match" +} +``` + +## process.v2.pipeline +```json +{ + "event": "ranking_explained", + "top_docs_after_ranking": [ + { + "doc": "docs/README.md", + "score": 130, + "match_reason": "semantic_match" + }, + { + "doc": "docs/architecture/telegram-notify-app-overview.md", + "score": 130, + "match_reason": "semantic_match" + } + ], + "ranking_score_breakdown": [ + { + "doc": "docs/README.md", + "score_breakdown": { + "semantic": 10, + "path_match": 0, + "filename_match": 0, + "alias_match": 0, + "anchor_boost": 120, + "target_doc_boost": 0, + "generic_penalty": 0 + } + }, + { + "doc": "docs/architecture/telegram-notify-app-overview.md", + "score_breakdown": { + "semantic": 10, + "path_match": 0, + "filename_match": 0, + "alias_match": 0, + "anchor_boost": 120, + "target_doc_boost": 0, + "generic_penalty": 0 + } + } + ] +} +``` + +## process.v2.pipeline +```json +{ + "event": "evidence_gate_checked", + "passed": false, + "reason": "low_confidence_shortlist", + "answer_mode": "deterministic" +} +``` + +## workflow.v2.find_files +```json +{ + "event": "workflow_started", + "workflow_id": "v2.docs_explain.find_files" +} +``` + +## workflow.v2.find_files +```json +{ + "event": "workflow_trace_flushed", + "workflow_id": "v2.docs_explain.find_files", + "steps": [ + { + "step_id": "finalize_find_files_answer", + "title": "Сборка списка файлов", + "input": {}, + "output": { + "answer_length": 64 + } + } + ] +} +``` + +## workflow.v2.find_files +```json +{ + "event": "workflow_completed", + "workflow_id": "v2.docs_explain.find_files" +} +``` + +## process.v2.pipeline +```json +{ + "event": "answer_generated", + "answer_mode": "deterministic", + "answer_length": 64 +} +``` + +## result +```json +{ + "status": "done", + "answer": "docs/README.md\ndocs/architecture/telegram-notify-app-overview.md", + "completed_at": "2026-04-07T17:59:50.263763+00:00" +} +``` + +## request +```json +{ + "request_id": "req_8345d97e397b482d9243764513180c4c", + "session_id": "as_21b7fe278b9548938d3a066eb02d5ed3", + "active_rag_session_id": "965a992e-8be3-45cf-8a31-92e685261a86", + "process_version": "v2", + "created_at": "2026-04-07T18:00:20.485493+00:00", + "message": "Как работает send_message_endpoint?" +} +``` + +## process.v2 +```json +{ + "event": "intent_routed", + "routing_domain": "DOCS", + "intent": "DOC_EXPLAIN", + "subintent": "SUMMARY", + "normalized_query": "Как работает send_message_endpoint?", + "target_terms": [ + "send_message_endpoint" + ], + "anchors": { + "entity_names": [], + "file_names": [], + "endpoint_paths": [], + "target_doc_hints": [], + "matched_aliases": [], + "process_domain": null, + "process_subdomain": null, + "signal_types": [] + }, + "confidence": 0.7500000000000001, + "routing_mode": "llm_default", + "llm_router_used": true, + "reason_short": "Запрос явно касается работы конкретного endpoint'а.", + "rag_session_id": "965a992e-8be3-45cf-8a31-92e685261a86" +} +``` + +## process.v2.pipeline +```json +{ + "event": "router_resolved", + "domain": "DOCS", + "intent": "DOC_EXPLAIN", + "subintent": "SUMMARY", + "confidence": 0.7500000000000001 +} +``` + +## process.v2.pipeline +```json +{ + "event": "anchors_extracted", + "signal_types": [], + "endpoint_paths": [], + "target_doc_hints": [], + "matched_aliases": [], + "target_terms": [ + "send_message_endpoint" + ] +} +``` + +## process.v2.pipeline +```json +{ + "event": "alias_resolution", + "resolved_aliases": [], + "target_doc_hints": [] +} +``` + +## process.v2.retrieval_policy +```json +{ + "event": "retrieval_plan_resolved", + "profile": "docs_summary_generic", + "layers": [ + "D1_DOCUMENT_CATALOG", + "D0_DOC_CHUNKS" + ], + "limit": 8, + "filters": { + "target_doc_hints": [], + "prefer_path_prefixes": [ + "docs/" + ], + "prefer_like_patterns": [] + } +} +``` + +## process.v2.pipeline +```json +{ + "event": "retrieval_profile_selected", + "profile": "docs_summary_generic", + "layers": [ + "D1_DOCUMENT_CATALOG", + "D0_DOC_CHUNKS" + ], + "filters": { + "target_doc_hints": [], + "prefer_path_prefixes": [ + "docs/" + ], + "prefer_like_patterns": [] + } +} +``` + +## process.v2.rag_retrieval +```json +{ + "event": "rag_rows_fetched", + "profile": "docs_summary_generic", + "row_count": 8, + "rows": [ + { + "layer": "D1_DOCUMENT_CATALOG", + "path": "docs/architecture/telegram-notify-app-overview.md", + "title": "Архитектура Telegram Notify App", + "document_id": "architecture.telegram_notify_app", + "entity_name": "", + "summary_text": "- Purpose: сервис поднимает HTTP control plane и фоновый worker для отправки уведомлений в Telegram.\n- Entry point: `src/telegram_notify_app/main.py`.\n- Main components: `RuntimeManager`, `TelegramControlChannel`, `TelegramNotifyModule`, `TelegramNotifyWorker`, `TelegramSendService`.\n- Configuration: `config/config.yaml` или путь из `CONFIG_PATH`.\n- Related API: [`/health`](../api/health-endpoint.", + "section_path": "", + "content_preview": "- Purpose: сервис поднимает HTTP control plane и фоновый worker для отправки уведомлений в Telegram.\n- Entry point: `src/telegram_notify_app/main.py`.\n- Main components: `RuntimeManager`, `TelegramControlChannel`, `TelegramNotifyModule`, `TelegramNotifyWorker`, `TelegramSendService`.\n- Configuration: `config/config.yaml` или путь из `CONFIG_PATH`.\n- Related API: [`/health`](../api/health-endpoint." + }, + { + "layer": "D1_DOCUMENT_CATALOG", + "path": "docs/README.md", + "title": "Индекс технической документации test_echo_app", + "document_id": "index.test_echo_app_docs", + "entity_name": "", + "summary_text": "- Purpose: точка входа в техническую документацию сервиса `test_echo_app`.\n- Scope: архитектура, HTTP API control plane, цикл отправки уведомлений, health-модель и каталог ошибок.\n- Canonical structure: `docs/architecture`, `docs/api`, `docs/logic`, `docs/domains`, `docs/errors`.\n- Primary parent doc: [Архитектура Telegram Notify App](./architecture/telegram-notify-app-overview.md).\n- Navigation: ", + "section_path": "", + "content_preview": "- Purpose: точка входа в техническую документацию сервиса `test_echo_app`.\n- Scope: архитектура, HTTP API control plane, цикл отправки уведомлений, health-модель и каталог ошибок.\n- Canonical structure: `docs/architecture`, `docs/api`, `docs/logic`, `docs/domains`, `docs/errors`.\n- Primary parent doc: [Архитектура Telegram Notify App](./architecture/telegram-notify-app-overview.md).\n- Navigation: " + }, + { + "layer": "D0_DOC_CHUNKS", + "path": "docs/architecture/telegram-notify-app-overview.md", + "title": "architecture.telegram_notify_app:Связанные документы", + "document_id": "architecture.telegram_notify_app", + "entity_name": "", + "summary_text": "", + "section_path": "Архитектура Telegram Notify App > Details > Связанные документы", + "content_preview": "- [API /health](../api/health-endpoint.md)\n- [API /actions/{action}](../api/control-actions-endpoint.md)\n- [API /send](../api/send-message-endpoint.md)\n- [Логика цикла отправки уведомлений](../logic/telegram-notification-loop.md)\n- [Доменная модель runtime health](../domains/runtime-health-entity.md)" + }, + { + "layer": "D0_DOC_CHUNKS", + "path": "docs/architecture/telegram-notify-app-overview.md", + "title": "architecture.telegram_notify_app:Telegram Bot API", + "document_id": "architecture.telegram_notify_app", + "entity_name": "", + "summary_text": "", + "section_path": "Архитектура Telegram Notify App > Details > Интеграции > Telegram Bot API", + "content_preview": "- target: ext.telegram_bot_api\n- target_type: external_system\n- direction: outbound\n- interaction: calls\n- via: HTTPS `POST /bot{token}/sendMessage`\n- purpose: доставка как плановых, так и ручных уведомлений\n- details:\n - producers:\n - logic.telegram_notification_loop\n - api.send_message_endpoint\n - timeout_seconds: 30" + }, + { + "layer": "D0_DOC_CHUNKS", + "path": "docs/README.md", + "title": "index.test_echo_app_docs:Навигация", + "document_id": "index.test_echo_app_docs", + "entity_name": "", + "summary_text": "", + "section_path": "Индекс технической документации test_echo_app > Details > Навигация", + "content_preview": "- [Архитектура Telegram Notify App](./architecture/telegram-notify-app-overview.md)\n- [API /health](./api/health-endpoint.md)\n- [API /actions/{action}](./api/control-actions-endpoint.md)\n- [API /send](./api/send-message-endpoint.md)\n- [Логика цикла отправки уведомлений](./logic/telegram-notification-loop.md)\n- [Доменная модель runtime health](./domains/runtime-health-entity.md)\n- [Каталог ошибок](" + }, + { + "layer": "D0_DOC_CHUNKS", + "path": "docs/architecture/telegram-notify-app-overview.md", + "title": "architecture.telegram_notify_app:Summary", + "document_id": "architecture.telegram_notify_app", + "entity_name": "", + "summary_text": "", + "section_path": "Архитектура Telegram Notify App > Summary", + "content_preview": "- Purpose: сервис поднимает HTTP control plane и фоновый worker для отправки уведомлений в Telegram.\n- Entry point: `src/telegram_notify_app/main.py`.\n- Main components: `RuntimeManager`, `TelegramControlChannel`, `TelegramNotifyModule`, `TelegramNotifyWorker`, `TelegramSendService`.\n- Configuration: `config/config.yaml` или путь из `CONFIG_PATH`.\n- Related API: [`/health`](../api/health-endpoint." + }, + { + "layer": "D0_DOC_CHUNKS", + "path": "docs/architecture/telegram-notify-app-overview.md", + "title": "architecture.telegram_notify_app:Связанный код", + "document_id": "architecture.telegram_notify_app", + "entity_name": "", + "summary_text": "", + "section_path": "Архитектура Telegram Notify App > Details > Связанный код", + "content_preview": "- `src/telegram_notify_app/main.py` - загрузка YAML, регистрация runtime и сигналов shutdown.\n- `src/telegram_notify_app/module.py` - регистрация worker и send service в runtime.\n- `src/telegram_notify_app/control_api.py` - HTTP control plane и endpoint `/send`.\n- `src/telegram_notify_app/worker.py` - фоновый workflow отправки и расчет health.\n- `src/telegram_notify_app/send_service.py` - разовая " + }, + { + "layer": "D0_DOC_CHUNKS", + "path": "docs/architecture/telegram-notify-app-overview.md", + "title": "architecture.telegram_notify_app:Интеграционные сценарии", + "document_id": "architecture.telegram_notify_app", + "entity_name": "", + "summary_text": "", + "section_path": "Архитектура Telegram Notify App > Details > Интеграционные сценарии", + "content_preview": "1. При старте `main()` загружает YAML-конфиг, извлекает host, port и интервал отправки, затем собирает runtime.\n2. `RuntimeManager` регистрирует `TelegramControlChannel` для HTTP control plane.\n3. `TelegramNotifyModule` добавляет `TelegramNotifyWorker` и `TelegramSendService` в runtime.\n4. Внешний клиент вызывает endpoint'ы control plane для health-check, lifecycle-операций или ручной отправки.\n5." + } + ] +} +``` + +## process.v2.pipeline +```json +{ + "event": "candidate_generation", + "query": "Как работает send_message_endpoint?", + "profile": "docs_summary_generic", + "details": { + "target_doc_hints": [], + "candidates_before_ranking": [ + "docs/architecture/telegram-notify-app-overview.md", + "docs/README.md", + "docs/architecture/telegram-notify-app-overview.md", + "docs/architecture/telegram-notify-app-overview.md", + "docs/README.md", + "docs/architecture/telegram-notify-app-overview.md", + "docs/architecture/telegram-notify-app-overview.md", + "docs/architecture/telegram-notify-app-overview.md" + ] + }, + "resolved_aliases": [], + "target_doc_hints": [], + "candidate_docs_before_ranking": [ + { + "layer": "D1_DOCUMENT_CATALOG", + "path": "docs/architecture/telegram-notify-app-overview.md", + "title": "Архитектура Telegram Notify App", + "document_id": "architecture.telegram_notify_app", + "entity_name": "", + "summary_text": "- Purpose: сервис поднимает HTTP control plane и фоновый worker для отправки уведомлений в Telegram.\n- Entry point: `src/telegram_notify_app/main.py`.\n- Main components: `RuntimeManager`, `TelegramControlChannel`, `TelegramNotifyModule`, `TelegramNotifyWorker`, `TelegramSendService`.\n- Configuration: `config/config.yaml` или путь из `CONFIG_PATH`.\n- Related API: [`/health`](../api/health-endpoint.", + "section_path": "", + "content_preview": "- Purpose: сервис поднимает HTTP control plane и фоновый worker для отправки уведомлений в Telegram.\n- Entry point: `src/telegram_notify_app/main.py`.\n- Main components: `RuntimeManager`, `TelegramControlChannel`, `TelegramNotifyModule`, `TelegramNotifyWorker`, `TelegramSendService`.\n- Configuration: `config/config.yaml` или путь из `CONFIG_PATH`.\n- Related API: [`/health`](../api/health-endpoint." + }, + { + "layer": "D1_DOCUMENT_CATALOG", + "path": "docs/README.md", + "title": "Индекс технической документации test_echo_app", + "document_id": "index.test_echo_app_docs", + "entity_name": "", + "summary_text": "- Purpose: точка входа в техническую документацию сервиса `test_echo_app`.\n- Scope: архитектура, HTTP API control plane, цикл отправки уведомлений, health-модель и каталог ошибок.\n- Canonical structure: `docs/architecture`, `docs/api`, `docs/logic`, `docs/domains`, `docs/errors`.\n- Primary parent doc: [Архитектура Telegram Notify App](./architecture/telegram-notify-app-overview.md).\n- Navigation: ", + "section_path": "", + "content_preview": "- Purpose: точка входа в техническую документацию сервиса `test_echo_app`.\n- Scope: архитектура, HTTP API control plane, цикл отправки уведомлений, health-модель и каталог ошибок.\n- Canonical structure: `docs/architecture`, `docs/api`, `docs/logic`, `docs/domains`, `docs/errors`.\n- Primary parent doc: [Архитектура Telegram Notify App](./architecture/telegram-notify-app-overview.md).\n- Navigation: " + }, + { + "layer": "D0_DOC_CHUNKS", + "path": "docs/architecture/telegram-notify-app-overview.md", + "title": "architecture.telegram_notify_app:Связанные документы", + "document_id": "architecture.telegram_notify_app", + "entity_name": "", + "summary_text": "", + "section_path": "Архитектура Telegram Notify App > Details > Связанные документы", + "content_preview": "- [API /health](../api/health-endpoint.md)\n- [API /actions/{action}](../api/control-actions-endpoint.md)\n- [API /send](../api/send-message-endpoint.md)\n- [Логика цикла отправки уведомлений](../logic/telegram-notification-loop.md)\n- [Доменная модель runtime health](../domains/runtime-health-entity.md)" + }, + { + "layer": "D0_DOC_CHUNKS", + "path": "docs/architecture/telegram-notify-app-overview.md", + "title": "architecture.telegram_notify_app:Telegram Bot API", + "document_id": "architecture.telegram_notify_app", + "entity_name": "", + "summary_text": "", + "section_path": "Архитектура Telegram Notify App > Details > Интеграции > Telegram Bot API", + "content_preview": "- target: ext.telegram_bot_api\n- target_type: external_system\n- direction: outbound\n- interaction: calls\n- via: HTTPS `POST /bot{token}/sendMessage`\n- purpose: доставка как плановых, так и ручных уведомлений\n- details:\n - producers:\n - logic.telegram_notification_loop\n - api.send_message_endpoint\n - timeout_seconds: 30" + }, + { + "layer": "D0_DOC_CHUNKS", + "path": "docs/README.md", + "title": "index.test_echo_app_docs:Навигация", + "document_id": "index.test_echo_app_docs", + "entity_name": "", + "summary_text": "", + "section_path": "Индекс технической документации test_echo_app > Details > Навигация", + "content_preview": "- [Архитектура Telegram Notify App](./architecture/telegram-notify-app-overview.md)\n- [API /health](./api/health-endpoint.md)\n- [API /actions/{action}](./api/control-actions-endpoint.md)\n- [API /send](./api/send-message-endpoint.md)\n- [Логика цикла отправки уведомлений](./logic/telegram-notification-loop.md)\n- [Доменная модель runtime health](./domains/runtime-health-entity.md)\n- [Каталог ошибок](" + }, + { + "layer": "D0_DOC_CHUNKS", + "path": "docs/architecture/telegram-notify-app-overview.md", + "title": "architecture.telegram_notify_app:Summary", + "document_id": "architecture.telegram_notify_app", + "entity_name": "", + "summary_text": "", + "section_path": "Архитектура Telegram Notify App > Summary", + "content_preview": "- Purpose: сервис поднимает HTTP control plane и фоновый worker для отправки уведомлений в Telegram.\n- Entry point: `src/telegram_notify_app/main.py`.\n- Main components: `RuntimeManager`, `TelegramControlChannel`, `TelegramNotifyModule`, `TelegramNotifyWorker`, `TelegramSendService`.\n- Configuration: `config/config.yaml` или путь из `CONFIG_PATH`.\n- Related API: [`/health`](../api/health-endpoint." + }, + { + "layer": "D0_DOC_CHUNKS", + "path": "docs/architecture/telegram-notify-app-overview.md", + "title": "architecture.telegram_notify_app:Связанный код", + "document_id": "architecture.telegram_notify_app", + "entity_name": "", + "summary_text": "", + "section_path": "Архитектура Telegram Notify App > Details > Связанный код", + "content_preview": "- `src/telegram_notify_app/main.py` - загрузка YAML, регистрация runtime и сигналов shutdown.\n- `src/telegram_notify_app/module.py` - регистрация worker и send service в runtime.\n- `src/telegram_notify_app/control_api.py` - HTTP control plane и endpoint `/send`.\n- `src/telegram_notify_app/worker.py` - фоновый workflow отправки и расчет health.\n- `src/telegram_notify_app/send_service.py` - разовая " + }, + { + "layer": "D0_DOC_CHUNKS", + "path": "docs/architecture/telegram-notify-app-overview.md", + "title": "architecture.telegram_notify_app:Интеграционные сценарии", + "document_id": "architecture.telegram_notify_app", + "entity_name": "", + "summary_text": "", + "section_path": "Архитектура Telegram Notify App > Details > Интеграционные сценарии", + "content_preview": "1. При старте `main()` загружает YAML-конфиг, извлекает host, port и интервал отправки, затем собирает runtime.\n2. `RuntimeManager` регистрирует `TelegramControlChannel` для HTTP control plane.\n3. `TelegramNotifyModule` добавляет `TelegramNotifyWorker` и `TelegramSendService` в runtime.\n4. Внешний клиент вызывает endpoint'ы control plane для health-check, lifecycle-операций или ручной отправки.\n5." + } + ], + "sources": { + "seeded": [], + "metadata_lookup": [], + "semantic": [ + { + "layer": "D1_DOCUMENT_CATALOG", + "path": "docs/architecture/telegram-notify-app-overview.md", + "title": "Архитектура Telegram Notify App", + "document_id": "architecture.telegram_notify_app", + "entity_name": "", + "summary_text": "- Purpose: сервис поднимает HTTP control plane и фоновый worker для отправки уведомлений в Telegram.\n- Entry point: `src/telegram_notify_app/main.py`.\n- Main components: `RuntimeManager`, `TelegramControlChannel`, `TelegramNotifyModule`, `TelegramNotifyWorker`, `TelegramSendService`.\n- Configuration: `config/config.yaml` или путь из `CONFIG_PATH`.\n- Related API: [`/health`](../api/health-endpoint.", + "section_path": "", + "content_preview": "- Purpose: сервис поднимает HTTP control plane и фоновый worker для отправки уведомлений в Telegram.\n- Entry point: `src/telegram_notify_app/main.py`.\n- Main components: `RuntimeManager`, `TelegramControlChannel`, `TelegramNotifyModule`, `TelegramNotifyWorker`, `TelegramSendService`.\n- Configuration: `config/config.yaml` или путь из `CONFIG_PATH`.\n- Related API: [`/health`](../api/health-endpoint." + }, + { + "layer": "D1_DOCUMENT_CATALOG", + "path": "docs/README.md", + "title": "Индекс технической документации test_echo_app", + "document_id": "index.test_echo_app_docs", + "entity_name": "", + "summary_text": "- Purpose: точка входа в техническую документацию сервиса `test_echo_app`.\n- Scope: архитектура, HTTP API control plane, цикл отправки уведомлений, health-модель и каталог ошибок.\n- Canonical structure: `docs/architecture`, `docs/api`, `docs/logic`, `docs/domains`, `docs/errors`.\n- Primary parent doc: [Архитектура Telegram Notify App](./architecture/telegram-notify-app-overview.md).\n- Navigation: ", + "section_path": "", + "content_preview": "- Purpose: точка входа в техническую документацию сервиса `test_echo_app`.\n- Scope: архитектура, HTTP API control plane, цикл отправки уведомлений, health-модель и каталог ошибок.\n- Canonical structure: `docs/architecture`, `docs/api`, `docs/logic`, `docs/domains`, `docs/errors`.\n- Primary parent doc: [Архитектура Telegram Notify App](./architecture/telegram-notify-app-overview.md).\n- Navigation: " + }, + { + "layer": "D0_DOC_CHUNKS", + "path": "docs/architecture/telegram-notify-app-overview.md", + "title": "architecture.telegram_notify_app:Связанные документы", + "document_id": "architecture.telegram_notify_app", + "entity_name": "", + "summary_text": "", + "section_path": "Архитектура Telegram Notify App > Details > Связанные документы", + "content_preview": "- [API /health](../api/health-endpoint.md)\n- [API /actions/{action}](../api/control-actions-endpoint.md)\n- [API /send](../api/send-message-endpoint.md)\n- [Логика цикла отправки уведомлений](../logic/telegram-notification-loop.md)\n- [Доменная модель runtime health](../domains/runtime-health-entity.md)" + }, + { + "layer": "D0_DOC_CHUNKS", + "path": "docs/architecture/telegram-notify-app-overview.md", + "title": "architecture.telegram_notify_app:Telegram Bot API", + "document_id": "architecture.telegram_notify_app", + "entity_name": "", + "summary_text": "", + "section_path": "Архитектура Telegram Notify App > Details > Интеграции > Telegram Bot API", + "content_preview": "- target: ext.telegram_bot_api\n- target_type: external_system\n- direction: outbound\n- interaction: calls\n- via: HTTPS `POST /bot{token}/sendMessage`\n- purpose: доставка как плановых, так и ручных уведомлений\n- details:\n - producers:\n - logic.telegram_notification_loop\n - api.send_message_endpoint\n - timeout_seconds: 30" + }, + { + "layer": "D0_DOC_CHUNKS", + "path": "docs/README.md", + "title": "index.test_echo_app_docs:Навигация", + "document_id": "index.test_echo_app_docs", + "entity_name": "", + "summary_text": "", + "section_path": "Индекс технической документации test_echo_app > Details > Навигация", + "content_preview": "- [Архитектура Telegram Notify App](./architecture/telegram-notify-app-overview.md)\n- [API /health](./api/health-endpoint.md)\n- [API /actions/{action}](./api/control-actions-endpoint.md)\n- [API /send](./api/send-message-endpoint.md)\n- [Логика цикла отправки уведомлений](./logic/telegram-notification-loop.md)\n- [Доменная модель runtime health](./domains/runtime-health-entity.md)\n- [Каталог ошибок](" + } + ] + } +} +``` + +## process.v2.pipeline +```json +{ + "event": "retrieval_executed", + "query": "Как работает send_message_endpoint?", + "profile": "docs_summary_generic", + "row_count": 8, + "target_doc_hints": [], + "top_results": [ + { + "layer": "D1_DOCUMENT_CATALOG", + "path": "docs/architecture/telegram-notify-app-overview.md", + "title": "Архитектура Telegram Notify App", + "document_id": "architecture.telegram_notify_app", + "entity_name": "", + "summary_text": "- Purpose: сервис поднимает HTTP control plane и фоновый worker для отправки уведомлений в Telegram.\n- Entry point: `src/telegram_notify_app/main.py`.\n- Main components: `RuntimeManager`, `TelegramControlChannel`, `TelegramNotifyModule`, `TelegramNotifyWorker`, `TelegramSendService`.\n- Configuration: `config/config.yaml` или путь из `CONFIG_PATH`.\n- Related API: [`/health`](../api/health-endpoint.", + "section_path": "", + "content_preview": "- Purpose: сервис поднимает HTTP control plane и фоновый worker для отправки уведомлений в Telegram.\n- Entry point: `src/telegram_notify_app/main.py`.\n- Main components: `RuntimeManager`, `TelegramControlChannel`, `TelegramNotifyModule`, `TelegramNotifyWorker`, `TelegramSendService`.\n- Configuration: `config/config.yaml` или путь из `CONFIG_PATH`.\n- Related API: [`/health`](../api/health-endpoint." + }, + { + "layer": "D1_DOCUMENT_CATALOG", + "path": "docs/README.md", + "title": "Индекс технической документации test_echo_app", + "document_id": "index.test_echo_app_docs", + "entity_name": "", + "summary_text": "- Purpose: точка входа в техническую документацию сервиса `test_echo_app`.\n- Scope: архитектура, HTTP API control plane, цикл отправки уведомлений, health-модель и каталог ошибок.\n- Canonical structure: `docs/architecture`, `docs/api`, `docs/logic`, `docs/domains`, `docs/errors`.\n- Primary parent doc: [Архитектура Telegram Notify App](./architecture/telegram-notify-app-overview.md).\n- Navigation: ", + "section_path": "", + "content_preview": "- Purpose: точка входа в техническую документацию сервиса `test_echo_app`.\n- Scope: архитектура, HTTP API control plane, цикл отправки уведомлений, health-модель и каталог ошибок.\n- Canonical structure: `docs/architecture`, `docs/api`, `docs/logic`, `docs/domains`, `docs/errors`.\n- Primary parent doc: [Архитектура Telegram Notify App](./architecture/telegram-notify-app-overview.md).\n- Navigation: " + }, + { + "layer": "D0_DOC_CHUNKS", + "path": "docs/architecture/telegram-notify-app-overview.md", + "title": "architecture.telegram_notify_app:Связанные документы", + "document_id": "architecture.telegram_notify_app", + "entity_name": "", + "summary_text": "", + "section_path": "Архитектура Telegram Notify App > Details > Связанные документы", + "content_preview": "- [API /health](../api/health-endpoint.md)\n- [API /actions/{action}](../api/control-actions-endpoint.md)\n- [API /send](../api/send-message-endpoint.md)\n- [Логика цикла отправки уведомлений](../logic/telegram-notification-loop.md)\n- [Доменная модель runtime health](../domains/runtime-health-entity.md)" + }, + { + "layer": "D0_DOC_CHUNKS", + "path": "docs/architecture/telegram-notify-app-overview.md", + "title": "architecture.telegram_notify_app:Telegram Bot API", + "document_id": "architecture.telegram_notify_app", + "entity_name": "", + "summary_text": "", + "section_path": "Архитектура Telegram Notify App > Details > Интеграции > Telegram Bot API", + "content_preview": "- target: ext.telegram_bot_api\n- target_type: external_system\n- direction: outbound\n- interaction: calls\n- via: HTTPS `POST /bot{token}/sendMessage`\n- purpose: доставка как плановых, так и ручных уведомлений\n- details:\n - producers:\n - logic.telegram_notification_loop\n - api.send_message_endpoint\n - timeout_seconds: 30" + }, + { + "layer": "D0_DOC_CHUNKS", + "path": "docs/README.md", + "title": "index.test_echo_app_docs:Навигация", + "document_id": "index.test_echo_app_docs", + "entity_name": "", + "summary_text": "", + "section_path": "Индекс технической документации test_echo_app > Details > Навигация", + "content_preview": "- [Архитектура Telegram Notify App](./architecture/telegram-notify-app-overview.md)\n- [API /health](./api/health-endpoint.md)\n- [API /actions/{action}](./api/control-actions-endpoint.md)\n- [API /send](./api/send-message-endpoint.md)\n- [Логика цикла отправки уведомлений](./logic/telegram-notification-loop.md)\n- [Доменная модель runtime health](./domains/runtime-health-entity.md)\n- [Каталог ошибок](" + } + ] +} +``` + +## process.v2.evidence +```json +{ + "event": "evidence_assembled", + "mode": "summary", + "document_count": 1, + "documents": [ + "docs/architecture/telegram-notify-app-overview.md" + ] +} +``` + +## process.v2.pipeline +```json +{ + "event": "evidence_assembled", + "mode": "summary", + "primary_doc": "docs/architecture/telegram-notify-app-overview.md", + "document_count": 1 +} +``` + +## process.v2.pipeline +```json +{ + "event": "ranking_explained", + "doc": "docs/architecture/telegram-notify-app-overview.md", + "score_breakdown": { + "semantic": 60, + "path_match": 0, + "filename_match": 0, + "alias_match": 0, + "anchor_boost": 0, + "target_doc_boost": 0, + "generic_penalty": 0 + }, + "score": 60, + "match_reason": "semantic_match" +} +``` + +## process.v2.pipeline +```json +{ + "event": "ranking_explained", + "top_docs_after_ranking": [ + { + "doc": "docs/architecture/telegram-notify-app-overview.md", + "score": 60, + "match_reason": "semantic_match" + } + ], + "ranking_score_breakdown": [ + { + "doc": "docs/architecture/telegram-notify-app-overview.md", + "score_breakdown": { + "semantic": 60, + "path_match": 0, + "filename_match": 0, + "alias_match": 0, + "anchor_boost": 0, + "target_doc_boost": 0, + "generic_penalty": 0 + } + } + ] +} +``` + +## process.v2.pipeline +```json +{ + "event": "evidence_gate_checked", + "passed": true, + "reason": "target_doc_found", + "answer_mode": "grounded_summary" +} +``` + +## workflow.v2.summary +```json +{ + "event": "workflow_started", + "workflow_id": "v2.docs_explain.summary" +} +``` + +## workflow.v2.summary.llm +```json +{ + "event": "request", + "prompt_name": "v2_docs_explain.summary_answer", + "system_prompt": "Ты объясняешь документацию только на основе найденных SUMMARY-блоков.\nИспользуй только факты из входного контекста.\nЕсли информации мало, прямо скажи об этом и не додумывай детали.\nВ конце перечисли файлы, на которые ты опирался.", + "user_prompt": "Запрос пользователя:\nКак работает send_message_endpoint?\n\nСигналы запроса:\n{\n \"entity_names\": [],\n \"file_names\": [],\n \"endpoint_paths\": [],\n \"target_doc_hints\": [],\n \"matched_aliases\": [],\n \"process_domain\": null,\n \"process_subdomain\": null,\n \"signal_types\": []\n}\n\nНайденные SUMMARY-блоки:\n\n1. path: docs/architecture/telegram-notify-app-overview.md\ntitle: Архитектура Telegram Notify App\nmatch_reason: semantic_match\nsummary: - Purpose: сервис поднимает HTTP control plane и фоновый worker для отправки уведомлений в Telegram.\n- Entry point: `src/telegram_notify_app/main.py`.\n- Main components: `RuntimeManager`, `TelegramControlChannel`, `TelegramNotifyModule`, `TelegramNotifyWorker`, `TelegramSendService`.\n- Configuration: `config/config.yaml` или путь из `CONFIG_PATH`.\n- Related API: [`/health`](../api/health-endpoint.md), [`/actions/{action}`](../api/control-actions-endpoint.md), [`/send`](../api/send-message-endpoint.md).\n- Related logic: [цикл отправки уведомлений](../logic/telegram-notification-loop.md).\n- Related domain: [runtime health](../domains/runtime-health-entity.md).", + "log_context": "agent:req_8345d97e397b482d9243764513180c4c" +} +``` + +## workflow.v2.summary.llm +```json +{ + "event": "response", + "text": "На основе представленного SUMMARY-блока, подробной информации о том, как именно работает endpoint `send_message_endpoint`, нет. Документ лишь указывает, что этот endpoint существует и ссылается на файл \"../api/send-message-endpoint.md\", который предположительно содержит более подробную информацию.\n\nФайлы, на которые я опирался:\n- docs/architecture/telegram-notify-app-overview.md" +} +``` + +## workflow.v2.summary +```json +{ + "event": "workflow_trace_flushed", + "workflow_id": "v2.docs_explain.summary", + "steps": [ + { + "step_id": "generate_summary_answer", + "title": "Сборка ответа по summary", + "input": {}, + "output": { + "answer_length": 381 + } + } + ] +} +``` + +## workflow.v2.summary +```json +{ + "event": "workflow_completed", + "workflow_id": "v2.docs_explain.summary" +} +``` + +## process.v2.pipeline +```json +{ + "event": "answer_generated", + "answer_mode": "grounded_summary", + "answer_length": 381 +} +``` + +## result +```json +{ + "status": "done", + "answer": "На основе представленного SUMMARY-блока, подробной информации о том, как именно работает endpoint `send_message_endpoint`, нет. Документ лишь указывает, что этот endpoint существует и ссылается на файл \"../api/send-message-endpoint.md\", который предположительно содержит более подробную информацию.\n\nФайлы, на которые я опирался:\n- docs/architecture/telegram-notify-app-overview.md", + "completed_at": "2026-04-07T18:00:23.497821+00:00" +} +``` diff --git a/runtime_traces/agent_requests/20260407-182058-3f56c69c7290.md b/runtime_traces/agent_requests/20260407-182058-3f56c69c7290.md new file mode 100644 index 0000000..d35141c --- /dev/null +++ b/runtime_traces/agent_requests/20260407-182058-3f56c69c7290.md @@ -0,0 +1,622 @@ +# Runtime Trace: 20260407-182058-3f56c69c7290 + +- active_rag_session_id: c8b893cc-cb13-4493-a6d1-3f56c69c7290 + +## request +```json +{ + "request_id": "req_bab9c8812ac94847bb102cba68516f10", + "session_id": "as_4fdccc9c55c549faad8f3ef379371129", + "active_rag_session_id": "c8b893cc-cb13-4493-a6d1-3f56c69c7290", + "process_version": "v2", + "created_at": "2026-04-07T18:20:58.679614+00:00", + "message": "Как работает метод health?" +} +``` + +## process.v2 +```json +{ + "event": "intent_routed", + "routing_domain": "DOCS", + "intent": "DOC_EXPLAIN", + "subintent": "SUMMARY", + "normalized_query": "Как работает метод health?", + "target_terms": [ + "метод", + "health" + ], + "anchors": { + "entity_names": [], + "file_names": [], + "endpoint_paths": [], + "target_doc_hints": [], + "matched_aliases": [], + "process_domain": null, + "process_subdomain": null, + "signal_types": [] + }, + "confidence": 0.75, + "routing_mode": "llm_default", + "llm_router_used": true, + "reason_short": "Запрос на понимание работы конкретного метода \"health\".", + "rag_session_id": "c8b893cc-cb13-4493-a6d1-3f56c69c7290" +} +``` + +## process.v2.pipeline +```json +{ + "event": "router_resolved", + "domain": "DOCS", + "intent": "DOC_EXPLAIN", + "subintent": "SUMMARY", + "confidence": 0.75 +} +``` + +## process.v2.pipeline +```json +{ + "event": "anchors_extracted", + "signal_types": [], + "endpoint_paths": [], + "target_doc_hints": [], + "matched_aliases": [], + "target_terms": [ + "метод", + "health" + ] +} +``` + +## process.v2.pipeline +```json +{ + "event": "alias_resolution", + "resolved_aliases": [], + "target_doc_hints": [] +} +``` + +## process.v2.retrieval_policy +```json +{ + "event": "retrieval_plan_resolved", + "profile": "docs_summary_generic", + "layers": [ + "D1_DOCUMENT_CATALOG", + "D0_DOC_CHUNKS" + ], + "limit": 8, + "filters": { + "target_doc_hints": [], + "prefer_path_prefixes": [ + "docs/" + ], + "prefer_like_patterns": [] + } +} +``` + +## process.v2.pipeline +```json +{ + "event": "retrieval_profile_selected", + "profile": "docs_summary_generic", + "layers": [ + "D1_DOCUMENT_CATALOG", + "D0_DOC_CHUNKS" + ], + "filters": { + "target_doc_hints": [], + "prefer_path_prefixes": [ + "docs/" + ], + "prefer_like_patterns": [] + } +} +``` + +## process.v2.rag_retrieval +```json +{ + "event": "rag_rows_fetched", + "profile": "docs_summary_generic", + "row_count": 8, + "rows": [ + { + "layer": "D1_DOCUMENT_CATALOG", + "path": "docs/architecture/telegram-notify-app-overview.md", + "title": "Архитектура Telegram Notify App", + "document_id": "architecture.telegram_notify_app", + "entity_name": "", + "summary_text": "- Purpose: сервис поднимает HTTP control plane и фоновый worker для отправки уведомлений в Telegram.\n- Entry point: `src/telegram_notify_app/main.py`.\n- Main components: `RuntimeManager`, `TelegramControlChannel`, `TelegramNotifyModule`, `TelegramNotifyWorker`, `TelegramSendService`.\n- Configuration: `config/config.yaml` или путь из `CONFIG_PATH`.\n- Related API: [`/health`](../api/health-endpoint.", + "section_path": "", + "content_preview": "- Purpose: сервис поднимает HTTP control plane и фоновый worker для отправки уведомлений в Telegram.\n- Entry point: `src/telegram_notify_app/main.py`.\n- Main components: `RuntimeManager`, `TelegramControlChannel`, `TelegramNotifyModule`, `TelegramNotifyWorker`, `TelegramSendService`.\n- Configuration: `config/config.yaml` или путь из `CONFIG_PATH`.\n- Related API: [`/health`](../api/health-endpoint." + }, + { + "layer": "D1_DOCUMENT_CATALOG", + "path": "docs/README.md", + "title": "Индекс технической документации test_echo_app", + "document_id": "index.test_echo_app_docs", + "entity_name": "", + "summary_text": "- Purpose: точка входа в техническую документацию сервиса `test_echo_app`.\n- Scope: архитектура, HTTP API control plane, цикл отправки уведомлений, health-модель и каталог ошибок.\n- Canonical structure: `docs/architecture`, `docs/api`, `docs/logic`, `docs/domains`, `docs/errors`.\n- Primary parent doc: [Архитектура Telegram Notify App](./architecture/telegram-notify-app-overview.md).\n- Navigation: ", + "section_path": "", + "content_preview": "- Purpose: точка входа в техническую документацию сервиса `test_echo_app`.\n- Scope: архитектура, HTTP API control plane, цикл отправки уведомлений, health-модель и каталог ошибок.\n- Canonical structure: `docs/architecture`, `docs/api`, `docs/logic`, `docs/domains`, `docs/errors`.\n- Primary parent doc: [Архитектура Telegram Notify App](./architecture/telegram-notify-app-overview.md).\n- Navigation: " + }, + { + "layer": "D0_DOC_CHUNKS", + "path": "docs/architecture/telegram-notify-app-overview.md", + "title": "architecture.telegram_notify_app:Операторские и мониторинговые клиенты", + "document_id": "architecture.telegram_notify_app", + "entity_name": "", + "summary_text": "", + "section_path": "Архитектура Telegram Notify App > Details > Интеграции > Операторские и мониторинговые клиенты", + "content_preview": "- target: ext.operator_and_probes\n- target_type: external_system\n- direction: inbound\n- interaction: calls\n- via: HTTP `/health`, `/actions/{action}`, `/send`\n- purpose: диагностика, lifecycle-управление и ручная отправка сообщений\n- details:\n - transport: FastAPI + UvicornThreadRunner\n - status_mapping: non-ok health -> HTTP 503" + }, + { + "layer": "D0_DOC_CHUNKS", + "path": "docs/architecture/telegram-notify-app-overview.md", + "title": "architecture.telegram_notify_app:Связанные документы", + "document_id": "architecture.telegram_notify_app", + "entity_name": "", + "summary_text": "", + "section_path": "Архитектура Telegram Notify App > Details > Связанные документы", + "content_preview": "- [API /health](../api/health-endpoint.md)\n- [API /actions/{action}](../api/control-actions-endpoint.md)\n- [API /send](../api/send-message-endpoint.md)\n- [Логика цикла отправки уведомлений](../logic/telegram-notification-loop.md)\n- [Доменная модель runtime health](../domains/runtime-health-entity.md)" + }, + { + "layer": "D0_DOC_CHUNKS", + "path": "docs/README.md", + "title": "index.test_echo_app_docs:Навигация", + "document_id": "index.test_echo_app_docs", + "entity_name": "", + "summary_text": "", + "section_path": "Индекс технической документации test_echo_app > Details > Навигация", + "content_preview": "- [Архитектура Telegram Notify App](./architecture/telegram-notify-app-overview.md)\n- [API /health](./api/health-endpoint.md)\n- [API /actions/{action}](./api/control-actions-endpoint.md)\n- [API /send](./api/send-message-endpoint.md)\n- [Логика цикла отправки уведомлений](./logic/telegram-notification-loop.md)\n- [Доменная модель runtime health](./domains/runtime-health-entity.md)\n- [Каталог ошибок](" + }, + { + "layer": "D0_DOC_CHUNKS", + "path": "docs/architecture/telegram-notify-app-overview.md", + "title": "architecture.telegram_notify_app:Summary", + "document_id": "architecture.telegram_notify_app", + "entity_name": "", + "summary_text": "", + "section_path": "Архитектура Telegram Notify App > Summary", + "content_preview": "- Purpose: сервис поднимает HTTP control plane и фоновый worker для отправки уведомлений в Telegram.\n- Entry point: `src/telegram_notify_app/main.py`.\n- Main components: `RuntimeManager`, `TelegramControlChannel`, `TelegramNotifyModule`, `TelegramNotifyWorker`, `TelegramSendService`.\n- Configuration: `config/config.yaml` или путь из `CONFIG_PATH`.\n- Related API: [`/health`](../api/health-endpoint." + }, + { + "layer": "D0_DOC_CHUNKS", + "path": "docs/README.md", + "title": "index.test_echo_app_docs:Summary", + "document_id": "index.test_echo_app_docs", + "entity_name": "", + "summary_text": "", + "section_path": "Индекс технической документации test_echo_app > Summary", + "content_preview": "- Purpose: точка входа в техническую документацию сервиса `test_echo_app`.\n- Scope: архитектура, HTTP API control plane, цикл отправки уведомлений, health-модель и каталог ошибок.\n- Canonical structure: `docs/architecture`, `docs/api`, `docs/logic`, `docs/domains`, `docs/errors`.\n- Primary parent doc: [Архитектура Telegram Notify App](./architecture/telegram-notify-app-overview.md).\n- Navigation: " + }, + { + "layer": "D0_DOC_CHUNKS", + "path": "docs/architecture/telegram-notify-app-overview.md", + "title": "architecture.telegram_notify_app:Интеграционные сценарии", + "document_id": "architecture.telegram_notify_app", + "entity_name": "", + "summary_text": "", + "section_path": "Архитектура Telegram Notify App > Details > Интеграционные сценарии", + "content_preview": "1. При старте `main()` загружает YAML-конфиг, извлекает host, port и интервал отправки, затем собирает runtime.\n2. `RuntimeManager` регистрирует `TelegramControlChannel` для HTTP control plane.\n3. `TelegramNotifyModule` добавляет `TelegramNotifyWorker` и `TelegramSendService` в runtime.\n4. Внешний клиент вызывает endpoint'ы control plane для health-check, lifecycle-операций или ручной отправки.\n5." + } + ] +} +``` + +## process.v2.pipeline +```json +{ + "event": "candidate_generation", + "query": "Как работает метод health?", + "profile": "docs_summary_generic", + "details": { + "target_doc_hints": [], + "candidates_before_ranking": [ + "docs/architecture/telegram-notify-app-overview.md", + "docs/README.md", + "docs/architecture/telegram-notify-app-overview.md", + "docs/architecture/telegram-notify-app-overview.md", + "docs/README.md", + "docs/architecture/telegram-notify-app-overview.md", + "docs/README.md", + "docs/architecture/telegram-notify-app-overview.md" + ] + }, + "resolved_aliases": [], + "target_doc_hints": [], + "candidate_docs_before_ranking": [ + { + "layer": "D1_DOCUMENT_CATALOG", + "path": "docs/architecture/telegram-notify-app-overview.md", + "title": "Архитектура Telegram Notify App", + "document_id": "architecture.telegram_notify_app", + "entity_name": "", + "summary_text": "- Purpose: сервис поднимает HTTP control plane и фоновый worker для отправки уведомлений в Telegram.\n- Entry point: `src/telegram_notify_app/main.py`.\n- Main components: `RuntimeManager`, `TelegramControlChannel`, `TelegramNotifyModule`, `TelegramNotifyWorker`, `TelegramSendService`.\n- Configuration: `config/config.yaml` или путь из `CONFIG_PATH`.\n- Related API: [`/health`](../api/health-endpoint.", + "section_path": "", + "content_preview": "- Purpose: сервис поднимает HTTP control plane и фоновый worker для отправки уведомлений в Telegram.\n- Entry point: `src/telegram_notify_app/main.py`.\n- Main components: `RuntimeManager`, `TelegramControlChannel`, `TelegramNotifyModule`, `TelegramNotifyWorker`, `TelegramSendService`.\n- Configuration: `config/config.yaml` или путь из `CONFIG_PATH`.\n- Related API: [`/health`](../api/health-endpoint." + }, + { + "layer": "D1_DOCUMENT_CATALOG", + "path": "docs/README.md", + "title": "Индекс технической документации test_echo_app", + "document_id": "index.test_echo_app_docs", + "entity_name": "", + "summary_text": "- Purpose: точка входа в техническую документацию сервиса `test_echo_app`.\n- Scope: архитектура, HTTP API control plane, цикл отправки уведомлений, health-модель и каталог ошибок.\n- Canonical structure: `docs/architecture`, `docs/api`, `docs/logic`, `docs/domains`, `docs/errors`.\n- Primary parent doc: [Архитектура Telegram Notify App](./architecture/telegram-notify-app-overview.md).\n- Navigation: ", + "section_path": "", + "content_preview": "- Purpose: точка входа в техническую документацию сервиса `test_echo_app`.\n- Scope: архитектура, HTTP API control plane, цикл отправки уведомлений, health-модель и каталог ошибок.\n- Canonical structure: `docs/architecture`, `docs/api`, `docs/logic`, `docs/domains`, `docs/errors`.\n- Primary parent doc: [Архитектура Telegram Notify App](./architecture/telegram-notify-app-overview.md).\n- Navigation: " + }, + { + "layer": "D0_DOC_CHUNKS", + "path": "docs/architecture/telegram-notify-app-overview.md", + "title": "architecture.telegram_notify_app:Операторские и мониторинговые клиенты", + "document_id": "architecture.telegram_notify_app", + "entity_name": "", + "summary_text": "", + "section_path": "Архитектура Telegram Notify App > Details > Интеграции > Операторские и мониторинговые клиенты", + "content_preview": "- target: ext.operator_and_probes\n- target_type: external_system\n- direction: inbound\n- interaction: calls\n- via: HTTP `/health`, `/actions/{action}`, `/send`\n- purpose: диагностика, lifecycle-управление и ручная отправка сообщений\n- details:\n - transport: FastAPI + UvicornThreadRunner\n - status_mapping: non-ok health -> HTTP 503" + }, + { + "layer": "D0_DOC_CHUNKS", + "path": "docs/architecture/telegram-notify-app-overview.md", + "title": "architecture.telegram_notify_app:Связанные документы", + "document_id": "architecture.telegram_notify_app", + "entity_name": "", + "summary_text": "", + "section_path": "Архитектура Telegram Notify App > Details > Связанные документы", + "content_preview": "- [API /health](../api/health-endpoint.md)\n- [API /actions/{action}](../api/control-actions-endpoint.md)\n- [API /send](../api/send-message-endpoint.md)\n- [Логика цикла отправки уведомлений](../logic/telegram-notification-loop.md)\n- [Доменная модель runtime health](../domains/runtime-health-entity.md)" + }, + { + "layer": "D0_DOC_CHUNKS", + "path": "docs/README.md", + "title": "index.test_echo_app_docs:Навигация", + "document_id": "index.test_echo_app_docs", + "entity_name": "", + "summary_text": "", + "section_path": "Индекс технической документации test_echo_app > Details > Навигация", + "content_preview": "- [Архитектура Telegram Notify App](./architecture/telegram-notify-app-overview.md)\n- [API /health](./api/health-endpoint.md)\n- [API /actions/{action}](./api/control-actions-endpoint.md)\n- [API /send](./api/send-message-endpoint.md)\n- [Логика цикла отправки уведомлений](./logic/telegram-notification-loop.md)\n- [Доменная модель runtime health](./domains/runtime-health-entity.md)\n- [Каталог ошибок](" + }, + { + "layer": "D0_DOC_CHUNKS", + "path": "docs/architecture/telegram-notify-app-overview.md", + "title": "architecture.telegram_notify_app:Summary", + "document_id": "architecture.telegram_notify_app", + "entity_name": "", + "summary_text": "", + "section_path": "Архитектура Telegram Notify App > Summary", + "content_preview": "- Purpose: сервис поднимает HTTP control plane и фоновый worker для отправки уведомлений в Telegram.\n- Entry point: `src/telegram_notify_app/main.py`.\n- Main components: `RuntimeManager`, `TelegramControlChannel`, `TelegramNotifyModule`, `TelegramNotifyWorker`, `TelegramSendService`.\n- Configuration: `config/config.yaml` или путь из `CONFIG_PATH`.\n- Related API: [`/health`](../api/health-endpoint." + }, + { + "layer": "D0_DOC_CHUNKS", + "path": "docs/README.md", + "title": "index.test_echo_app_docs:Summary", + "document_id": "index.test_echo_app_docs", + "entity_name": "", + "summary_text": "", + "section_path": "Индекс технической документации test_echo_app > Summary", + "content_preview": "- Purpose: точка входа в техническую документацию сервиса `test_echo_app`.\n- Scope: архитектура, HTTP API control plane, цикл отправки уведомлений, health-модель и каталог ошибок.\n- Canonical structure: `docs/architecture`, `docs/api`, `docs/logic`, `docs/domains`, `docs/errors`.\n- Primary parent doc: [Архитектура Telegram Notify App](./architecture/telegram-notify-app-overview.md).\n- Navigation: " + }, + { + "layer": "D0_DOC_CHUNKS", + "path": "docs/architecture/telegram-notify-app-overview.md", + "title": "architecture.telegram_notify_app:Интеграционные сценарии", + "document_id": "architecture.telegram_notify_app", + "entity_name": "", + "summary_text": "", + "section_path": "Архитектура Telegram Notify App > Details > Интеграционные сценарии", + "content_preview": "1. При старте `main()` загружает YAML-конфиг, извлекает host, port и интервал отправки, затем собирает runtime.\n2. `RuntimeManager` регистрирует `TelegramControlChannel` для HTTP control plane.\n3. `TelegramNotifyModule` добавляет `TelegramNotifyWorker` и `TelegramSendService` в runtime.\n4. Внешний клиент вызывает endpoint'ы control plane для health-check, lifecycle-операций или ручной отправки.\n5." + } + ], + "sources": { + "seeded": [], + "metadata_lookup": [], + "semantic": [ + { + "layer": "D1_DOCUMENT_CATALOG", + "path": "docs/architecture/telegram-notify-app-overview.md", + "title": "Архитектура Telegram Notify App", + "document_id": "architecture.telegram_notify_app", + "entity_name": "", + "summary_text": "- Purpose: сервис поднимает HTTP control plane и фоновый worker для отправки уведомлений в Telegram.\n- Entry point: `src/telegram_notify_app/main.py`.\n- Main components: `RuntimeManager`, `TelegramControlChannel`, `TelegramNotifyModule`, `TelegramNotifyWorker`, `TelegramSendService`.\n- Configuration: `config/config.yaml` или путь из `CONFIG_PATH`.\n- Related API: [`/health`](../api/health-endpoint.", + "section_path": "", + "content_preview": "- Purpose: сервис поднимает HTTP control plane и фоновый worker для отправки уведомлений в Telegram.\n- Entry point: `src/telegram_notify_app/main.py`.\n- Main components: `RuntimeManager`, `TelegramControlChannel`, `TelegramNotifyModule`, `TelegramNotifyWorker`, `TelegramSendService`.\n- Configuration: `config/config.yaml` или путь из `CONFIG_PATH`.\n- Related API: [`/health`](../api/health-endpoint." + }, + { + "layer": "D1_DOCUMENT_CATALOG", + "path": "docs/README.md", + "title": "Индекс технической документации test_echo_app", + "document_id": "index.test_echo_app_docs", + "entity_name": "", + "summary_text": "- Purpose: точка входа в техническую документацию сервиса `test_echo_app`.\n- Scope: архитектура, HTTP API control plane, цикл отправки уведомлений, health-модель и каталог ошибок.\n- Canonical structure: `docs/architecture`, `docs/api`, `docs/logic`, `docs/domains`, `docs/errors`.\n- Primary parent doc: [Архитектура Telegram Notify App](./architecture/telegram-notify-app-overview.md).\n- Navigation: ", + "section_path": "", + "content_preview": "- Purpose: точка входа в техническую документацию сервиса `test_echo_app`.\n- Scope: архитектура, HTTP API control plane, цикл отправки уведомлений, health-модель и каталог ошибок.\n- Canonical structure: `docs/architecture`, `docs/api`, `docs/logic`, `docs/domains`, `docs/errors`.\n- Primary parent doc: [Архитектура Telegram Notify App](./architecture/telegram-notify-app-overview.md).\n- Navigation: " + }, + { + "layer": "D0_DOC_CHUNKS", + "path": "docs/architecture/telegram-notify-app-overview.md", + "title": "architecture.telegram_notify_app:Операторские и мониторинговые клиенты", + "document_id": "architecture.telegram_notify_app", + "entity_name": "", + "summary_text": "", + "section_path": "Архитектура Telegram Notify App > Details > Интеграции > Операторские и мониторинговые клиенты", + "content_preview": "- target: ext.operator_and_probes\n- target_type: external_system\n- direction: inbound\n- interaction: calls\n- via: HTTP `/health`, `/actions/{action}`, `/send`\n- purpose: диагностика, lifecycle-управление и ручная отправка сообщений\n- details:\n - transport: FastAPI + UvicornThreadRunner\n - status_mapping: non-ok health -> HTTP 503" + }, + { + "layer": "D0_DOC_CHUNKS", + "path": "docs/architecture/telegram-notify-app-overview.md", + "title": "architecture.telegram_notify_app:Связанные документы", + "document_id": "architecture.telegram_notify_app", + "entity_name": "", + "summary_text": "", + "section_path": "Архитектура Telegram Notify App > Details > Связанные документы", + "content_preview": "- [API /health](../api/health-endpoint.md)\n- [API /actions/{action}](../api/control-actions-endpoint.md)\n- [API /send](../api/send-message-endpoint.md)\n- [Логика цикла отправки уведомлений](../logic/telegram-notification-loop.md)\n- [Доменная модель runtime health](../domains/runtime-health-entity.md)" + }, + { + "layer": "D0_DOC_CHUNKS", + "path": "docs/README.md", + "title": "index.test_echo_app_docs:Навигация", + "document_id": "index.test_echo_app_docs", + "entity_name": "", + "summary_text": "", + "section_path": "Индекс технической документации test_echo_app > Details > Навигация", + "content_preview": "- [Архитектура Telegram Notify App](./architecture/telegram-notify-app-overview.md)\n- [API /health](./api/health-endpoint.md)\n- [API /actions/{action}](./api/control-actions-endpoint.md)\n- [API /send](./api/send-message-endpoint.md)\n- [Логика цикла отправки уведомлений](./logic/telegram-notification-loop.md)\n- [Доменная модель runtime health](./domains/runtime-health-entity.md)\n- [Каталог ошибок](" + } + ] + } +} +``` + +## process.v2.pipeline +```json +{ + "event": "retrieval_executed", + "query": "Как работает метод health?", + "profile": "docs_summary_generic", + "row_count": 8, + "target_doc_hints": [], + "top_results": [ + { + "layer": "D1_DOCUMENT_CATALOG", + "path": "docs/architecture/telegram-notify-app-overview.md", + "title": "Архитектура Telegram Notify App", + "document_id": "architecture.telegram_notify_app", + "entity_name": "", + "summary_text": "- Purpose: сервис поднимает HTTP control plane и фоновый worker для отправки уведомлений в Telegram.\n- Entry point: `src/telegram_notify_app/main.py`.\n- Main components: `RuntimeManager`, `TelegramControlChannel`, `TelegramNotifyModule`, `TelegramNotifyWorker`, `TelegramSendService`.\n- Configuration: `config/config.yaml` или путь из `CONFIG_PATH`.\n- Related API: [`/health`](../api/health-endpoint.", + "section_path": "", + "content_preview": "- Purpose: сервис поднимает HTTP control plane и фоновый worker для отправки уведомлений в Telegram.\n- Entry point: `src/telegram_notify_app/main.py`.\n- Main components: `RuntimeManager`, `TelegramControlChannel`, `TelegramNotifyModule`, `TelegramNotifyWorker`, `TelegramSendService`.\n- Configuration: `config/config.yaml` или путь из `CONFIG_PATH`.\n- Related API: [`/health`](../api/health-endpoint." + }, + { + "layer": "D1_DOCUMENT_CATALOG", + "path": "docs/README.md", + "title": "Индекс технической документации test_echo_app", + "document_id": "index.test_echo_app_docs", + "entity_name": "", + "summary_text": "- Purpose: точка входа в техническую документацию сервиса `test_echo_app`.\n- Scope: архитектура, HTTP API control plane, цикл отправки уведомлений, health-модель и каталог ошибок.\n- Canonical structure: `docs/architecture`, `docs/api`, `docs/logic`, `docs/domains`, `docs/errors`.\n- Primary parent doc: [Архитектура Telegram Notify App](./architecture/telegram-notify-app-overview.md).\n- Navigation: ", + "section_path": "", + "content_preview": "- Purpose: точка входа в техническую документацию сервиса `test_echo_app`.\n- Scope: архитектура, HTTP API control plane, цикл отправки уведомлений, health-модель и каталог ошибок.\n- Canonical structure: `docs/architecture`, `docs/api`, `docs/logic`, `docs/domains`, `docs/errors`.\n- Primary parent doc: [Архитектура Telegram Notify App](./architecture/telegram-notify-app-overview.md).\n- Navigation: " + }, + { + "layer": "D0_DOC_CHUNKS", + "path": "docs/architecture/telegram-notify-app-overview.md", + "title": "architecture.telegram_notify_app:Операторские и мониторинговые клиенты", + "document_id": "architecture.telegram_notify_app", + "entity_name": "", + "summary_text": "", + "section_path": "Архитектура Telegram Notify App > Details > Интеграции > Операторские и мониторинговые клиенты", + "content_preview": "- target: ext.operator_and_probes\n- target_type: external_system\n- direction: inbound\n- interaction: calls\n- via: HTTP `/health`, `/actions/{action}`, `/send`\n- purpose: диагностика, lifecycle-управление и ручная отправка сообщений\n- details:\n - transport: FastAPI + UvicornThreadRunner\n - status_mapping: non-ok health -> HTTP 503" + }, + { + "layer": "D0_DOC_CHUNKS", + "path": "docs/architecture/telegram-notify-app-overview.md", + "title": "architecture.telegram_notify_app:Связанные документы", + "document_id": "architecture.telegram_notify_app", + "entity_name": "", + "summary_text": "", + "section_path": "Архитектура Telegram Notify App > Details > Связанные документы", + "content_preview": "- [API /health](../api/health-endpoint.md)\n- [API /actions/{action}](../api/control-actions-endpoint.md)\n- [API /send](../api/send-message-endpoint.md)\n- [Логика цикла отправки уведомлений](../logic/telegram-notification-loop.md)\n- [Доменная модель runtime health](../domains/runtime-health-entity.md)" + }, + { + "layer": "D0_DOC_CHUNKS", + "path": "docs/README.md", + "title": "index.test_echo_app_docs:Навигация", + "document_id": "index.test_echo_app_docs", + "entity_name": "", + "summary_text": "", + "section_path": "Индекс технической документации test_echo_app > Details > Навигация", + "content_preview": "- [Архитектура Telegram Notify App](./architecture/telegram-notify-app-overview.md)\n- [API /health](./api/health-endpoint.md)\n- [API /actions/{action}](./api/control-actions-endpoint.md)\n- [API /send](./api/send-message-endpoint.md)\n- [Логика цикла отправки уведомлений](./logic/telegram-notification-loop.md)\n- [Доменная модель runtime health](./domains/runtime-health-entity.md)\n- [Каталог ошибок](" + } + ] +} +``` + +## process.v2.evidence +```json +{ + "event": "evidence_assembled", + "mode": "summary", + "document_count": 2, + "documents": [ + "docs/README.md", + "docs/architecture/telegram-notify-app-overview.md" + ] +} +``` + +## process.v2.pipeline +```json +{ + "event": "evidence_assembled", + "mode": "summary", + "primary_doc": "docs/README.md", + "document_count": 2 +} +``` + +## process.v2.pipeline +```json +{ + "event": "ranking_explained", + "doc": "docs/README.md", + "score_breakdown": { + "semantic": 20, + "path_match": 0, + "filename_match": 0, + "alias_match": 0, + "anchor_boost": 0, + "target_doc_boost": 0, + "generic_penalty": 0 + }, + "score": 20, + "match_reason": "semantic_match" +} +``` + +## process.v2.pipeline +```json +{ + "event": "ranking_explained", + "doc": "docs/architecture/telegram-notify-app-overview.md", + "score_breakdown": { + "semantic": 20, + "path_match": 0, + "filename_match": 0, + "alias_match": 0, + "anchor_boost": 0, + "target_doc_boost": 0, + "generic_penalty": 0 + }, + "score": 20, + "match_reason": "semantic_match" +} +``` + +## process.v2.pipeline +```json +{ + "event": "ranking_explained", + "top_docs_after_ranking": [ + { + "doc": "docs/README.md", + "score": 20, + "match_reason": "semantic_match" + }, + { + "doc": "docs/architecture/telegram-notify-app-overview.md", + "score": 20, + "match_reason": "semantic_match" + } + ], + "ranking_score_breakdown": [ + { + "doc": "docs/README.md", + "score_breakdown": { + "semantic": 20, + "path_match": 0, + "filename_match": 0, + "alias_match": 0, + "anchor_boost": 0, + "target_doc_boost": 0, + "generic_penalty": 0 + } + }, + { + "doc": "docs/architecture/telegram-notify-app-overview.md", + "score_breakdown": { + "semantic": 20, + "path_match": 0, + "filename_match": 0, + "alias_match": 0, + "anchor_boost": 0, + "target_doc_boost": 0, + "generic_penalty": 0 + } + } + ] +} +``` + +## process.v2.pipeline +```json +{ + "event": "evidence_gate_checked", + "passed": true, + "reason": "target_doc_found", + "answer_mode": "grounded_summary" +} +``` + +## workflow.v2.summary +```json +{ + "event": "workflow_started", + "workflow_id": "v2.docs_explain.summary" +} +``` + +## workflow.v2.summary.llm +```json +{ + "event": "request", + "prompt_name": "v2_docs_explain.summary_answer", + "system_prompt": "Ты объясняешь документацию только на основе найденных SUMMARY-блоков.\nИспользуй только факты из входного контекста.\nЕсли информации мало, прямо скажи об этом и не додумывай детали.\nВ конце перечисли файлы, на которые ты опирался.", + "user_prompt": "Запрос пользователя:\nКак работает метод health?\n\nСигналы запроса:\n{\n \"entity_names\": [],\n \"file_names\": [],\n \"endpoint_paths\": [],\n \"target_doc_hints\": [],\n \"matched_aliases\": [],\n \"process_domain\": null,\n \"process_subdomain\": null,\n \"signal_types\": []\n}\n\nНайденные SUMMARY-блоки:\n\n1. path: docs/README.md\ntitle: Индекс технической документации test_echo_app\nmatch_reason: semantic_match\nsummary: - Purpose: точка входа в техническую документацию сервиса `test_echo_app`.\n- Scope: архитектура, HTTP API control plane, цикл отправки уведомлений, health-модель и каталог ошибок.\n- Canonical structure: `docs/architecture`, `docs/api`, `docs/logic`, `docs/domains`, `docs/errors`.\n- Primary parent doc: [Архитектура Telegram Notify App](./architecture/telegram-notify-app-overview.md).\n- Navigation: документы связаны через `related_docs`, `parent`/`children` и markdown-ссылки без дублирования деталей.\n\n2. path: docs/architecture/telegram-notify-app-overview.md\ntitle: Архитектура Telegram Notify App\nmatch_reason: semantic_match\nsummary: - Purpose: сервис поднимает HTTP control plane и фоновый worker для отправки уведомлений в Telegram.\n- Entry point: `src/telegram_notify_app/main.py`.\n- Main components: `RuntimeManager`, `TelegramControlChannel`, `TelegramNotifyModule`, `TelegramNotifyWorker`, `TelegramSendService`.\n- Configuration: `config/config.yaml` или путь из `CONFIG_PATH`.\n- Related API: [`/health`](../api/health-endpoint.md), [`/actions/{action}`](../api/control-actions-endpoint.md), [`/send`](../api/send-message-endpoint.md).\n- Related logic: [цикл отправки уведомлений](../logic/telegram-notification-loop.md).\n- Related domain: [runtime health](../domains/runtime-health-entity.md).", + "log_context": "agent:req_bab9c8812ac94847bb102cba68516f10" +} +``` + +## workflow.v2.summary.llm +```json +{ + "event": "response", + "text": "На основе представленного контекста невозможно предоставить подробное объяснение работы метода health. \n\nФайлы, на которые я опирался:\n1. docs/README.md\n2. docs/architecture/telegram-notify-app-overview.md" +} +``` + +## workflow.v2.summary +```json +{ + "event": "workflow_trace_flushed", + "workflow_id": "v2.docs_explain.summary", + "steps": [ + { + "step_id": "generate_summary_answer", + "title": "Сборка ответа по summary", + "input": {}, + "output": { + "answer_length": 205 + } + } + ] +} +``` + +## workflow.v2.summary +```json +{ + "event": "workflow_completed", + "workflow_id": "v2.docs_explain.summary" +} +``` + +## process.v2.pipeline +```json +{ + "event": "answer_generated", + "answer_mode": "grounded_summary", + "answer_length": 205 +} +``` + +## result +```json +{ + "status": "done", + "answer": "На основе представленного контекста невозможно предоставить подробное объяснение работы метода health. \n\nФайлы, на которые я опирался:\n1. docs/README.md\n2. docs/architecture/telegram-notify-app-overview.md", + "completed_at": "2026-04-07T18:21:01.793612+00:00" +} +``` diff --git a/runtime_traces/agent_requests/20260407-183339-3284d16c6cb0.md b/runtime_traces/agent_requests/20260407-183339-3284d16c6cb0.md new file mode 100644 index 0000000..460fbe5 --- /dev/null +++ b/runtime_traces/agent_requests/20260407-183339-3284d16c6cb0.md @@ -0,0 +1,1138 @@ +# Runtime Trace: 20260407-183339-3284d16c6cb0 + +- active_rag_session_id: 0fbd48e9-a592-4a64-ac17-3284d16c6cb0 + +## request +```json +{ + "request_id": "req_33518d79abdf4bafa39ab6dfc6064b75", + "session_id": "as_2db6661985714aea88660112c9cfe0ba", + "active_rag_session_id": "0fbd48e9-a592-4a64-ac17-3284d16c6cb0", + "process_version": "v2", + "created_at": "2026-04-07T18:33:39.489400+00:00", + "message": "Как работает метод health?" +} +``` + +## process.v2 +```json +{ + "event": "intent_routed", + "routing_domain": "DOCS", + "intent": "DOC_EXPLAIN", + "subintent": "SUMMARY", + "normalized_query": "Как работает метод health?", + "target_terms": [ + "/health", + "health" + ], + "anchors": { + "entity_names": [], + "file_names": [], + "endpoint_paths": [ + "/health" + ], + "target_doc_hints": [ + "/health", + "health", + "health-endpoint", + "health endpoint", + "docs/api/health-endpoint.md" + ], + "matched_aliases": [], + "process_domain": null, + "process_subdomain": null, + "signal_types": [ + "API_ENDPOINT" + ] + }, + "confidence": 0.9500000000000001, + "routing_mode": "llm_default", + "llm_router_used": true, + "reason_short": "Запрос явно касается объяснения работы конкретного метода (health), что предполагает обзор документации по данному endpoint'у.", + "rag_session_id": "0fbd48e9-a592-4a64-ac17-3284d16c6cb0" +} +``` + +## process.v2.pipeline +```json +{ + "event": "router_resolved", + "domain": "DOCS", + "intent": "DOC_EXPLAIN", + "subintent": "SUMMARY", + "confidence": 0.9500000000000001 +} +``` + +## process.v2.pipeline +```json +{ + "event": "anchors_extracted", + "signal_types": [ + "API_ENDPOINT" + ], + "endpoint_paths": [ + "/health" + ], + "target_doc_hints": [ + "/health", + "health", + "health-endpoint", + "health endpoint", + "docs/api/health-endpoint.md" + ], + "matched_aliases": [], + "target_terms": [ + "/health", + "health" + ] +} +``` + +## process.v2.pipeline +```json +{ + "event": "alias_resolution", + "resolved_aliases": [], + "target_doc_hints": [ + "/health", + "health", + "health-endpoint", + "health endpoint", + "docs/api/health-endpoint.md" + ] +} +``` + +## process.v2.retrieval_policy +```json +{ + "event": "retrieval_plan_resolved", + "profile": "docs_api_method_explain", + "layers": [ + "D1_DOCUMENT_CATALOG", + "D2_FACT_INDEX", + "D0_DOC_CHUNKS" + ], + "limit": 10, + "filters": { + "target_doc_hints": [ + "/health", + "health", + "health-endpoint", + "health endpoint", + "docs/api/health-endpoint.md" + ], + "path_prefixes": [ + "docs/api/", + "docs/endpoints/", + "docs/methods/", + "api/", + "endpoints/", + "methods/" + ], + "prefer_path_prefixes": [ + "docs/api/", + "docs/endpoints/", + "docs/methods/", + "api/", + "endpoints/", + "methods/" + ], + "prefer_like_patterns": [ + "%health%", + "%health-endpoint%", + "%health endpoint%", + "%health-endpoint.md%", + "%/health%", + "%docs/api/health-endpoint.md%" + ] + } +} +``` + +## process.v2.pipeline +```json +{ + "event": "retrieval_profile_selected", + "profile": "docs_api_method_explain", + "layers": [ + "D1_DOCUMENT_CATALOG", + "D2_FACT_INDEX", + "D0_DOC_CHUNKS" + ], + "filters": { + "target_doc_hints": [ + "/health", + "health", + "health-endpoint", + "health endpoint", + "docs/api/health-endpoint.md" + ], + "path_prefixes": [ + "docs/api/", + "docs/endpoints/", + "docs/methods/", + "api/", + "endpoints/", + "methods/" + ], + "prefer_path_prefixes": [ + "docs/api/", + "docs/endpoints/", + "docs/methods/", + "api/", + "endpoints/", + "methods/" + ], + "prefer_like_patterns": [ + "%health%", + "%health-endpoint%", + "%health endpoint%", + "%health-endpoint.md%", + "%/health%", + "%docs/api/health-endpoint.md%" + ] + } +} +``` + +## process.v2.rag_retrieval +```json +{ + "event": "rag_rows_fetched", + "profile": "docs_api_method_explain", + "row_count": 40, + "rows": [ + { + "layer": "D3_ENTITY_CATALOG", + "path": "docs/api/health-endpoint.md", + "title": "TelegramControlChannel", + "document_id": "api.health_endpoint", + "entity_name": "TelegramControlChannel", + "summary_text": "", + "section_path": "", + "content_preview": "TelegramControlChannel" + }, + { + "layer": "D1_DOCUMENT_CATALOG", + "path": "docs/api/health-endpoint.md", + "title": "HTTP API /health", + "document_id": "api.health_endpoint", + "entity_name": "", + "summary_text": "- Purpose: вернуть агрегированный health payload runtime без изменения состояния системы.\n- Actor: monitoring probe или оператор.\n- Trigger: HTTP `GET /health`.\n- Success rule: HTTP `200`, если `payload.status == \"ok\"`.\n- Degraded rule: HTTP `503`, если runtime вернул любой статус кроме `ok`.\n- Related domain: [runtime health](../domains/runtime-health-entity.md).", + "section_path": "", + "content_preview": "- Purpose: вернуть агрегированный health payload runtime без изменения состояния системы.\n- Actor: monitoring probe или оператор.\n- Trigger: HTTP `GET /health`.\n- Success rule: HTTP `200`, если `payload.status == \"ok\"`.\n- Degraded rule: HTTP `503`, если runtime вернул любой статус кроме `ok`.\n- Related domain: [runtime health](../domains/runtime-health-entity.md)." + }, + { + "layer": "D0_DOC_CHUNKS", + "path": "docs/api/health-endpoint.md", + "title": "api.health_endpoint:Summary", + "document_id": "api.health_endpoint", + "entity_name": "", + "summary_text": "", + "section_path": "HTTP API /health > Summary", + "content_preview": "- Purpose: вернуть агрегированный health payload runtime без изменения состояния системы.\n- Actor: monitoring probe или оператор.\n- Trigger: HTTP `GET /health`.\n- Success rule: HTTP `200`, если `payload.status == \"ok\"`.\n- Degraded rule: HTTP `503`, если runtime вернул любой статус кроме `ok`.\n- Related domain: [runtime health](../domains/runtime-health-entity.md)." + }, + { + "layer": "D0_DOC_CHUNKS", + "path": "docs/api/health-endpoint.md", + "title": "api.health_endpoint:Описание", + "document_id": "api.health_endpoint", + "entity_name": "", + "summary_text": "", + "section_path": "HTTP API /health > Details > Описание", + "content_preview": "Endpoint отдает текущее состояние runtime и его компонентов. Метод нужен для readiness/liveness-проверок, мониторинга и диагностики worker'а `telegram_notify`." + }, + { + "layer": "D0_DOC_CHUNKS", + "path": "docs/api/health-endpoint.md", + "title": "api.health_endpoint:Сценарий", + "document_id": "api.health_endpoint", + "entity_name": "", + "summary_text": "", + "section_path": "HTTP API /health > Details > Сценарий", + "content_preview": "**Название:** Получение агрегированного состояния runtime\n\n**Предусловия:**\n- HTTP control plane запущен через `TelegramControlChannel`.\n- Для канала зарегистрирован `health_provider`.\n\n**Триггер:**\n- Внешний клиент отправляет `GET /health`.\n\n**Основной сценарий:**\n1. Endpoint принимает HTTP-запрос без path, query и body параметров.\n2. `TelegramControlChannel` вызывает асинхронный `health_provider" + }, + { + "layer": "D0_DOC_CHUNKS", + "path": "docs/api/health-endpoint.md", + "title": "api.health_endpoint:Функциональные требования", + "document_id": "api.health_endpoint", + "entity_name": "", + "summary_text": "", + "section_path": "HTTP API /health > Details > Функциональные требования", + "content_preview": "- `FR-1`: Метод должен использовать `health_provider` как единственный источник истины для ответа.\n- `FR-2`: Endpoint не должен модифицировать структуру полей, полученных от runtime.\n- `FR-3`: Метод должен возвращать HTTP `200`, если `payload.status == \"ok\"`, и HTTP `503` во всех остальных случаях.\n- `FR-4`: Ответ должен включать компонентный health, в том числе состояние worker'а `telegram_notify" + }, + { + "layer": "D0_DOC_CHUNKS", + "path": "docs/api/health-endpoint.md", + "title": "api.health_endpoint:Нефункциональные требования", + "document_id": "api.health_endpoint", + "entity_name": "", + "summary_text": "", + "section_path": "HTTP API /health > Details > Нефункциональные требования", + "content_preview": "- `NFR-1`: Метод не должен запускать новые бизнес-операции и обязан работать только с текущим состоянием runtime.\n- `NFR-2`: Формат ответа должен быть стабильным для monitoring-систем и health probes.\n- `NFR-3`: Вызов должен укладываться в timeout control channel `5000 ms` при штатной работе runtime." + }, + { + "layer": "D0_DOC_CHUNKS", + "path": "docs/api/health-endpoint.md", + "title": "api.health_endpoint:Метаданные вызова", + "document_id": "api.health_endpoint", + "entity_name": "", + "summary_text": "", + "section_path": "HTTP API /health > Details > Контракт > Метаданные вызова", + "content_preview": "- Method: `GET`\n- Auth: `none`\n- Idempotency: повторный вызов безопасен и не меняет состояние runtime\n- Retry: допустим со стороны клиента, так как endpoint read-only\n- Timeout: `5000 ms`" + }, + { + "layer": "D0_DOC_CHUNKS", + "path": "docs/api/health-endpoint.md", + "title": "api.health_endpoint:Входные параметры", + "document_id": "api.health_endpoint", + "entity_name": "", + "summary_text": "", + "section_path": "HTTP API /health > Details > Контракт > Входные параметры", + "content_preview": "| Параметр | Где передается | Тип | Обязательность | Ограничения | Описание |\n|---|---|---|---|---|---|\n| отсутствуют | - | - | - | - | Endpoint не принимает path, query или body параметры |" + }, + { + "layer": "D0_DOC_CHUNKS", + "path": "docs/api/health-endpoint.md", + "title": "api.health_endpoint:Выходные параметры", + "document_id": "api.health_endpoint", + "entity_name": "", + "summary_text": "", + "section_path": "HTTP API /health > Details > Контракт > Выходные параметры", + "content_preview": "**HTTP 200 / 503**\n\n| Поле | Тип | Обязательность | Ограничения | Описание | Заполнение | Пример |\n|---|---|---|---|---|---|---|\n| `status` | `string` | обязательно | `ok`, `degraded`, `unhealthy`, `unknown` | Общий статус runtime | Берется из health payload | `ok` |\n| `detail` | `string` | опционально | произвольная строка | Диагностическое описание уровня runtime | Передается из health payload, " + }, + { + "layer": "D0_DOC_CHUNKS", + "path": "docs/api/health-endpoint.md", + "title": "api.health_endpoint:Пример ответа", + "document_id": "api.health_endpoint", + "entity_name": "", + "summary_text": "", + "section_path": "HTTP API /health > Details > Контракт > Пример ответа", + "content_preview": "```json\n{\n \"status\": \"ok\",\n \"components\": [\n {\n \"name\": \"telegram_notify\",\n \"status\": \"ok\",\n \"meta\": {\n \"app_started_at\": \"2026-03-20T10:00:00.000000Z\",\n \"notifications_sent\": 3\n }\n }\n ]\n}\n```" + }, + { + "layer": "D0_DOC_CHUNKS", + "path": "docs/api/health-endpoint.md", + "title": "api.health_endpoint:Runtime health provider", + "document_id": "api.health_endpoint", + "entity_name": "", + "summary_text": "", + "section_path": "HTTP API /health > Details > Интеграции > Runtime health provider", + "content_preview": "- 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`" + }, + { + "layer": "D0_DOC_CHUNKS", + "path": "docs/api/health-endpoint.md", + "title": "api.health_endpoint:Monitoring probe", + "document_id": "api.health_endpoint", + "entity_name": "", + "summary_text": "", + "section_path": "HTTP API /health > Details > Интеграции > Monitoring probe", + "content_preview": "- target: ext.health_probe\n- target_type: external_system\n- direction: inbound\n- interaction: calls\n- via: HTTP `GET /health`\n- purpose: readiness/liveness и внешняя диагностика сервиса\n- details:\n - http_ok: 200\n - http_not_ok: 503" + }, + { + "layer": "D0_DOC_CHUNKS", + "path": "docs/api/health-endpoint.md", + "title": "api.health_endpoint:Ошибки", + "document_id": "api.health_endpoint", + "entity_name": "", + "summary_text": "", + "section_path": "HTTP API /health > Details > Ошибки", + "content_preview": "- `health.control_actions_not_configured` - runtime вернул `status: unhealthy`, HTTP `503`; см. [каталог ошибок](../errors/catalog.yaml).\n- `health.status_not_ok` - runtime вернул любой статус, отличный от `ok`, HTTP `503`; см. [каталог ошибок](../errors/catalog.yaml)." + }, + { + "layer": "D0_DOC_CHUNKS", + "path": "docs/api/health-endpoint.md", + "title": "api.health_endpoint:Связанный код", + "document_id": "api.health_endpoint", + "entity_name": "", + "summary_text": "", + "section_path": "HTTP API /health > Details > Связанный код", + "content_preview": "- `src/telegram_notify_app/control_api.py` - endpoint `/health`, вызов `health_provider()` и маппинг `status -> HTTP code`." + }, + { + "layer": "D0_DOC_CHUNKS", + "path": "docs/api/health-endpoint.md", + "title": "api.health_endpoint:Связанные документы", + "document_id": "api.health_endpoint", + "entity_name": "", + "summary_text": "", + "section_path": "HTTP API /health > Details > Связанные документы", + "content_preview": "- [Архитектура Telegram Notify App](../architecture/telegram-notify-app-overview.md)\n- [Доменная модель runtime health](../domains/runtime-health-entity.md)\n- [Логика цикла отправки уведомлений](../logic/telegram-notification-loop.md)" + }, + { + "layer": "D0_DOC_CHUNKS", + "path": "docs/api/health-endpoint.md", + "title": "api.health_endpoint:История изменений", + "document_id": "api.health_endpoint", + "entity_name": "", + "summary_text": "", + "section_path": "HTTP API /health > Details > История изменений", + "content_preview": "- `2026-04-03`: документ приведен к полному шаблону API из `_process`, добавлены структурированный сценарий, интеграции, ошибки и кодовые ссылки." + }, + { + "layer": "D2_FACT_INDEX", + "path": "docs/api/health-endpoint.md", + "title": "api.health_endpoint:mentions_entity", + "document_id": "", + "entity_name": "", + "summary_text": "", + "section_path": "", + "content_preview": "api.health_endpoint mentions_entity TelegramControlChannel" + }, + { + "layer": "D4_WORKFLOW_INDEX", + "path": "docs/api/health-endpoint.md", + "title": "Scenario", + "document_id": "api.health_endpoint", + "entity_name": "", + "summary_text": "", + "section_path": "", + "content_preview": "Scenario\nHTTP control plane запущен через `TelegramControlChannel`.\nДля канала зарегистрирован `health_provider`.\nВнешний клиент отправляет `GET /health`.\nEndpoint принимает HTTP-запрос без path, query и body параметров.\n`TelegramControlChannel` вызывает асинхронный `health_provider`.\nRuntime возвращает health payload с общим статусом и списком компонентов.\nEndpoint сопоставляет HTTP-код с `payloa" + }, + { + "layer": "D5_RELATION_GRAPH", + "path": "docs/api/health-endpoint.md", + "title": "api.health_endpoint:parent", + "document_id": "", + "entity_name": "", + "summary_text": "", + "section_path": "", + "content_preview": "api.health_endpoint parent architecture.telegram_notify_app" + }, + { + "layer": "D6_INTEGRATION_INDEX", + "path": "docs/api/health-endpoint.md", + "title": "Runtime health provider", + "document_id": "", + "entity_name": "", + "summary_text": "", + "section_path": "", + "content_preview": "Runtime health provider | runtime.health_provider | depends_on | async callback `health_provider()` | получить агрегированный health runtime" + }, + { + "layer": "D3_ENTITY_CATALOG", + "path": "docs/domains/runtime-health-entity.md", + "title": "WorkerStatus", + "document_id": "domain.runtime_health", + "entity_name": "WorkerStatus", + "summary_text": "", + "section_path": "", + "content_preview": "WorkerStatus" + }, + { + "layer": "D1_DOCUMENT_CATALOG", + "path": "docs/domains/runtime-health-entity.md", + "title": "Сущность runtime health", + "document_id": "domain.runtime_health", + "entity_name": "", + "summary_text": "- Purpose: описать доменную модель наблюдаемости runtime и компонента `telegram_notify`.\n- Main consumer: [`/health`](../api/health-endpoint.md).\n- Main fields: общий `status`, список `components`, `app_started_at`, `notifications_sent`, `detail`.\n- State model: `ok`, `degraded`, `unhealthy`, `unknown`.\n- Source: `TelegramNotifyWorker.health()` и runtime health provider.", + "section_path": "", + "content_preview": "- Purpose: описать доменную модель наблюдаемости runtime и компонента `telegram_notify`.\n- Main consumer: [`/health`](../api/health-endpoint.md).\n- Main fields: общий `status`, список `components`, `app_started_at`, `notifications_sent`, `detail`.\n- State model: `ok`, `degraded`, `unhealthy`, `unknown`.\n- Source: `TelegramNotifyWorker.health()` и runtime health provider." + }, + { + "layer": "D0_DOC_CHUNKS", + "path": "docs/domains/runtime-health-entity.md", + "title": "domain.runtime_health:Summary", + "document_id": "domain.runtime_health", + "entity_name": "", + "summary_text": "", + "section_path": "Сущность runtime health > Summary", + "content_preview": "- Purpose: описать доменную модель наблюдаемости runtime и компонента `telegram_notify`.\n- Main consumer: [`/health`](../api/health-endpoint.md).\n- Main fields: общий `status`, список `components`, `app_started_at`, `notifications_sent`, `detail`.\n- State model: `ok`, `degraded`, `unhealthy`, `unknown`.\n- Source: `TelegramNotifyWorker.health()` и runtime health provider." + }, + { + "layer": "D0_DOC_CHUNKS", + "path": "docs/domains/runtime-health-entity.md", + "title": "domain.runtime_health:Описание", + "document_id": "domain.runtime_health", + "entity_name": "", + "summary_text": "", + "section_path": "Сущность runtime health > Details > Описание", + "content_preview": "`runtime health` - техническая доменная модель, через которую runtime сообщает внешнему миру собственное состояние и состояние отдельных компонентов. Для `test_echo_app` главным объектом наблюдаемости является worker `telegram_notify`." + }, + { + "layer": "D0_DOC_CHUNKS", + "path": "docs/domains/runtime-health-entity.md", + "title": "domain.runtime_health:Модель данных", + "document_id": "domain.runtime_health", + "entity_name": "", + "summary_text": "", + "section_path": "Сущность runtime health > Details > Модель данных", + "content_preview": "| Поле | Тип | Описание | Источник заполнения |\n|---|---|---|---|\n| `status` | `string` | Общий статус runtime | Health provider runtime |\n| `components` | `array` | Список компонентных health-состояний | Health provider runtime |\n| `components[].name` | `string` | Имя компонента | `TelegramNotifyWorker.name` |\n| `components[].status` | `string` | Статус компонента | `TelegramNotifyWorker." + }, + { + "layer": "D0_DOC_CHUNKS", + "path": "docs/domains/runtime-health-entity.md", + "title": "domain.runtime_health:Состояния и инварианты", + "document_id": "domain.runtime_health", + "entity_name": "", + "summary_text": "", + "section_path": "Сущность runtime health > Details > Состояния и инварианты", + "content_preview": "- Верхнеуровневый `status` отражает агрегированное состояние runtime и может быть `ok`, `degraded`, `unhealthy` или `unknown`.\n- Для компонента `telegram_notify` поле `meta.notifications_sent` всегда неотрицательное целое число.\n- `meta.app_started_at` заполняется после фактического вызова `TelegramNotifyWorker.start()`.\n- Если worker еще не стартовал, `health()` может вернуть `status: ok` c detai" + }, + { + "layer": "D0_DOC_CHUNKS", + "path": "docs/domains/runtime-health-entity.md", + "title": "domain.runtime_health:Технический use case", + "document_id": "domain.runtime_health", + "entity_name": "", + "summary_text": "", + "section_path": "Сущность runtime health > Details > Технический use case", + "content_preview": "1. Worker обновляет внутренние поля `_app_started_at`, `_notifications_sent` и `_last_error`.\n2. При вызове `health()` worker строит `WorkerHealth` с учетом состояния потока, credentials и последней ошибки.\n3. Runtime агрегирует состояние worker'а в общий health payload.\n4. Endpoint [`/health`](../api/health-endpoint.md) публикует payload внешнему клиенту без изменения структуры." + }, + { + "layer": "D0_DOC_CHUNKS", + "path": "docs/domains/runtime-health-entity.md", + "title": "domain.runtime_health:Функциональные требования", + "document_id": "domain.runtime_health", + "entity_name": "", + "summary_text": "", + "section_path": "Сущность runtime health > Details > Функциональные требования", + "content_preview": "- `FR-1`: Доменная модель должна позволять отделить общее состояние runtime от статуса отдельного компонента.\n- `FR-2`: Для компонента `telegram_notify` модель должна содержать минимум `name`, `status`, `detail` и `meta`.\n- `FR-3`: Поле `notifications_sent` должно отражать число успешных отправок с момента старта runtime.\n- `FR-4`: Поле `app_started_at` должно отражать UTC timestamp старта worker'" + }, + { + "layer": "D0_DOC_CHUNKS", + "path": "docs/domains/runtime-health-entity.md", + "title": "domain.runtime_health:Нефункциональные требования", + "document_id": "domain.runtime_health", + "entity_name": "", + "summary_text": "", + "section_path": "Сущность runtime health > Details > Нефункциональные требования", + "content_preview": "- `NFR-1`: Модель должна быть достаточно стабильной для monitoring и troubleshooting.\n- `NFR-2`: Поля должны быть компактными и пригодными для сериализации в JSON.\n- `NFR-3`: Health-модель не должна требовать дополнительного запроса к внешним системам для базовой диагностики." + }, + { + "layer": "D0_DOC_CHUNKS", + "path": "docs/domains/runtime-health-entity.md", + "title": "domain.runtime_health:HTTP health endpoint", + "document_id": "domain.runtime_health", + "entity_name": "", + "summary_text": "", + "section_path": "Сущность runtime health > Details > Интеграции > HTTP health endpoint", + "content_preview": "- target: api.health_endpoint\n- target_type: api\n- direction: outbound\n- interaction: emits\n- via: payload response `/health`\n- purpose: опубликовать health-состояние runtime и worker'а внешнему клиенту\n- details:\n - transport: JSON over HTTP" + }, + { + "layer": "D0_DOC_CHUNKS", + "path": "docs/domains/runtime-health-entity.md", + "title": "domain.runtime_health:Worker lifecycle", + "document_id": "domain.runtime_health", + "entity_name": "", + "summary_text": "", + "section_path": "Сущность runtime health > Details > Интеграции > Worker lifecycle", + "content_preview": "- target: logic.telegram_notification_loop\n- target_type: service\n- direction: inbound\n- interaction: depends_on\n- via: `TelegramNotifyWorker.health()`\n- purpose: формировать наблюдаемое состояние на основе выполнения worker workflow\n- details:\n - primary_fields:\n - app_started_at\n - notifications_sent\n - detail" + }, + { + "layer": "D0_DOC_CHUNKS", + "path": "docs/domains/runtime-health-entity.md", + "title": "domain.runtime_health:Ошибки и деградации", + "document_id": "domain.runtime_health", + "entity_name": "", + "summary_text": "", + "section_path": "Сущность runtime health > Details > Ошибки и деградации", + "content_preview": "- `worker.credentials_missing` - у worker отсутствуют credentials, состояние `degraded`; см. [каталог ошибок](../errors/catalog.yaml).\n- `worker.delivery_failed` - последняя отправка завершилась ошибкой, состояние `degraded`; см. [каталог ошибок](../errors/catalog.yaml).\n- `worker.thread_not_running` - поток worker'а должен работать, но не жив, состояние `unhealthy`; см. [каталог ошибок](../errors" + }, + { + "layer": "D0_DOC_CHUNKS", + "path": "docs/domains/runtime-health-entity.md", + "title": "domain.runtime_health:Связанный код", + "document_id": "domain.runtime_health", + "entity_name": "", + "summary_text": "", + "section_path": "Сущность runtime health > Details > Связанный код", + "content_preview": "- `src/telegram_notify_app/worker.py` - расчет `WorkerHealth`, `WorkerStatus` и метаданных worker'а.\n- `src/telegram_notify_app/control_api.py` - публикация payload наружу через `/health`." + }, + { + "layer": "D0_DOC_CHUNKS", + "path": "docs/domains/runtime-health-entity.md", + "title": "domain.runtime_health:Связанные документы", + "document_id": "domain.runtime_health", + "entity_name": "", + "summary_text": "", + "section_path": "Сущность runtime health > Details > Связанные документы", + "content_preview": "- [Архитектура Telegram Notify App](../architecture/telegram-notify-app-overview.md)\n- [API /health](../api/health-endpoint.md)\n- [Логика цикла отправки уведомлений](../logic/telegram-notification-loop.md)" + }, + { + "layer": "D0_DOC_CHUNKS", + "path": "docs/domains/runtime-health-entity.md", + "title": "domain.runtime_health:История изменений", + "document_id": "domain.runtime_health", + "entity_name": "", + "summary_text": "", + "section_path": "Сущность runtime health > Details > История изменений", + "content_preview": "- `2026-04-03`: frontmatter синхронизирован с `_process`, добавлены состояния, инварианты, интеграции и явные ссылки на каталог ошибок." + }, + { + "layer": "D2_FACT_INDEX", + "path": "docs/domains/runtime-health-entity.md", + "title": "domain.runtime_health:mentions_entity", + "document_id": "", + "entity_name": "", + "summary_text": "", + "section_path": "", + "content_preview": "domain.runtime_health mentions_entity WorkerHealth" + }, + { + "layer": "D4_WORKFLOW_INDEX", + "path": "docs/domains/runtime-health-entity.md", + "title": "Scenario", + "document_id": "domain.runtime_health", + "entity_name": "", + "summary_text": "", + "section_path": "", + "content_preview": "Scenario" + }, + { + "layer": "D5_RELATION_GRAPH", + "path": "docs/domains/runtime-health-entity.md", + "title": "domain.runtime_health:parent", + "document_id": "", + "entity_name": "", + "summary_text": "", + "section_path": "", + "content_preview": "domain.runtime_health parent architecture.telegram_notify_app" + }, + { + "layer": "D6_INTEGRATION_INDEX", + "path": "docs/domains/runtime-health-entity.md", + "title": "HTTP health endpoint", + "document_id": "", + "entity_name": "", + "summary_text": "", + "section_path": "", + "content_preview": "HTTP health endpoint | api.health_endpoint | emits | payload response `/health` | опубликовать health-состояние runtime и worker'а внешнему клиенту" + } + ] +} +``` + +## process.v2.pipeline +```json +{ + "event": "candidate_generation", + "query": "Как работает метод health?", + "profile": "docs_api_method_explain", + "details": { + "target_doc_hints": [ + "/health", + "health", + "health-endpoint", + "health endpoint", + "docs/api/health-endpoint.md" + ], + "candidates_before_ranking": [ + "docs/api/health-endpoint.md", + "docs/api/health-endpoint.md", + "docs/api/health-endpoint.md", + "docs/api/health-endpoint.md", + "docs/api/health-endpoint.md", + "docs/api/health-endpoint.md", + "docs/api/health-endpoint.md", + "docs/api/health-endpoint.md", + "docs/api/health-endpoint.md", + "docs/api/health-endpoint.md", + "docs/api/health-endpoint.md", + "docs/api/health-endpoint.md", + "docs/api/health-endpoint.md", + "docs/api/health-endpoint.md", + "docs/api/health-endpoint.md", + "docs/api/health-endpoint.md", + "docs/api/health-endpoint.md", + "docs/api/health-endpoint.md", + "docs/api/health-endpoint.md", + "docs/api/health-endpoint.md", + "docs/api/health-endpoint.md", + "docs/domains/runtime-health-entity.md", + "docs/domains/runtime-health-entity.md", + "docs/domains/runtime-health-entity.md", + "docs/domains/runtime-health-entity.md", + "docs/domains/runtime-health-entity.md", + "docs/domains/runtime-health-entity.md", + "docs/domains/runtime-health-entity.md", + "docs/domains/runtime-health-entity.md", + "docs/domains/runtime-health-entity.md", + "docs/domains/runtime-health-entity.md", + "docs/domains/runtime-health-entity.md", + "docs/domains/runtime-health-entity.md", + "docs/domains/runtime-health-entity.md", + "docs/domains/runtime-health-entity.md", + "docs/domains/runtime-health-entity.md", + "docs/domains/runtime-health-entity.md", + "docs/domains/runtime-health-entity.md", + "docs/domains/runtime-health-entity.md", + "docs/domains/runtime-health-entity.md" + ] + }, + "resolved_aliases": [], + "target_doc_hints": [ + "/health", + "health", + "health-endpoint", + "health endpoint", + "docs/api/health-endpoint.md" + ], + "candidate_docs_before_ranking": [ + { + "layer": "D3_ENTITY_CATALOG", + "path": "docs/api/health-endpoint.md", + "title": "TelegramControlChannel", + "document_id": "api.health_endpoint", + "entity_name": "TelegramControlChannel", + "summary_text": "", + "section_path": "", + "content_preview": "TelegramControlChannel" + }, + { + "layer": "D1_DOCUMENT_CATALOG", + "path": "docs/api/health-endpoint.md", + "title": "HTTP API /health", + "document_id": "api.health_endpoint", + "entity_name": "", + "summary_text": "- Purpose: вернуть агрегированный health payload runtime без изменения состояния системы.\n- Actor: monitoring probe или оператор.\n- Trigger: HTTP `GET /health`.\n- Success rule: HTTP `200`, если `payload.status == \"ok\"`.\n- Degraded rule: HTTP `503`, если runtime вернул любой статус кроме `ok`.\n- Related domain: [runtime health](../domains/runtime-health-entity.md).", + "section_path": "", + "content_preview": "- Purpose: вернуть агрегированный health payload runtime без изменения состояния системы.\n- Actor: monitoring probe или оператор.\n- Trigger: HTTP `GET /health`.\n- Success rule: HTTP `200`, если `payload.status == \"ok\"`.\n- Degraded rule: HTTP `503`, если runtime вернул любой статус кроме `ok`.\n- Related domain: [runtime health](../domains/runtime-health-entity.md)." + }, + { + "layer": "D0_DOC_CHUNKS", + "path": "docs/api/health-endpoint.md", + "title": "api.health_endpoint:Summary", + "document_id": "api.health_endpoint", + "entity_name": "", + "summary_text": "", + "section_path": "HTTP API /health > Summary", + "content_preview": "- Purpose: вернуть агрегированный health payload runtime без изменения состояния системы.\n- Actor: monitoring probe или оператор.\n- Trigger: HTTP `GET /health`.\n- Success rule: HTTP `200`, если `payload.status == \"ok\"`.\n- Degraded rule: HTTP `503`, если runtime вернул любой статус кроме `ok`.\n- Related domain: [runtime health](../domains/runtime-health-entity.md)." + }, + { + "layer": "D0_DOC_CHUNKS", + "path": "docs/api/health-endpoint.md", + "title": "api.health_endpoint:Описание", + "document_id": "api.health_endpoint", + "entity_name": "", + "summary_text": "", + "section_path": "HTTP API /health > Details > Описание", + "content_preview": "Endpoint отдает текущее состояние runtime и его компонентов. Метод нужен для readiness/liveness-проверок, мониторинга и диагностики worker'а `telegram_notify`." + }, + { + "layer": "D0_DOC_CHUNKS", + "path": "docs/api/health-endpoint.md", + "title": "api.health_endpoint:Сценарий", + "document_id": "api.health_endpoint", + "entity_name": "", + "summary_text": "", + "section_path": "HTTP API /health > Details > Сценарий", + "content_preview": "**Название:** Получение агрегированного состояния runtime\n\n**Предусловия:**\n- HTTP control plane запущен через `TelegramControlChannel`.\n- Для канала зарегистрирован `health_provider`.\n\n**Триггер:**\n- Внешний клиент отправляет `GET /health`.\n\n**Основной сценарий:**\n1. Endpoint принимает HTTP-запрос без path, query и body параметров.\n2. `TelegramControlChannel` вызывает асинхронный `health_provider" + }, + { + "layer": "D0_DOC_CHUNKS", + "path": "docs/api/health-endpoint.md", + "title": "api.health_endpoint:Функциональные требования", + "document_id": "api.health_endpoint", + "entity_name": "", + "summary_text": "", + "section_path": "HTTP API /health > Details > Функциональные требования", + "content_preview": "- `FR-1`: Метод должен использовать `health_provider` как единственный источник истины для ответа.\n- `FR-2`: Endpoint не должен модифицировать структуру полей, полученных от runtime.\n- `FR-3`: Метод должен возвращать HTTP `200`, если `payload.status == \"ok\"`, и HTTP `503` во всех остальных случаях.\n- `FR-4`: Ответ должен включать компонентный health, в том числе состояние worker'а `telegram_notify" + }, + { + "layer": "D0_DOC_CHUNKS", + "path": "docs/api/health-endpoint.md", + "title": "api.health_endpoint:Нефункциональные требования", + "document_id": "api.health_endpoint", + "entity_name": "", + "summary_text": "", + "section_path": "HTTP API /health > Details > Нефункциональные требования", + "content_preview": "- `NFR-1`: Метод не должен запускать новые бизнес-операции и обязан работать только с текущим состоянием runtime.\n- `NFR-2`: Формат ответа должен быть стабильным для monitoring-систем и health probes.\n- `NFR-3`: Вызов должен укладываться в timeout control channel `5000 ms` при штатной работе runtime." + }, + { + "layer": "D0_DOC_CHUNKS", + "path": "docs/api/health-endpoint.md", + "title": "api.health_endpoint:Метаданные вызова", + "document_id": "api.health_endpoint", + "entity_name": "", + "summary_text": "", + "section_path": "HTTP API /health > Details > Контракт > Метаданные вызова", + "content_preview": "- Method: `GET`\n- Auth: `none`\n- Idempotency: повторный вызов безопасен и не меняет состояние runtime\n- Retry: допустим со стороны клиента, так как endpoint read-only\n- Timeout: `5000 ms`" + } + ], + "sources": { + "seeded": [ + { + "layer": "D3_ENTITY_CATALOG", + "path": "docs/api/health-endpoint.md", + "title": "TelegramControlChannel", + "document_id": "api.health_endpoint", + "entity_name": "TelegramControlChannel", + "summary_text": "", + "section_path": "", + "content_preview": "TelegramControlChannel" + }, + { + "layer": "D1_DOCUMENT_CATALOG", + "path": "docs/api/health-endpoint.md", + "title": "HTTP API /health", + "document_id": "api.health_endpoint", + "entity_name": "", + "summary_text": "- Purpose: вернуть агрегированный health payload runtime без изменения состояния системы.\n- Actor: monitoring probe или оператор.\n- Trigger: HTTP `GET /health`.\n- Success rule: HTTP `200`, если `payload.status == \"ok\"`.\n- Degraded rule: HTTP `503`, если runtime вернул любой статус кроме `ok`.\n- Related domain: [runtime health](../domains/runtime-health-entity.md).", + "section_path": "", + "content_preview": "- Purpose: вернуть агрегированный health payload runtime без изменения состояния системы.\n- Actor: monitoring probe или оператор.\n- Trigger: HTTP `GET /health`.\n- Success rule: HTTP `200`, если `payload.status == \"ok\"`.\n- Degraded rule: HTTP `503`, если runtime вернул любой статус кроме `ok`.\n- Related domain: [runtime health](../domains/runtime-health-entity.md)." + }, + { + "layer": "D0_DOC_CHUNKS", + "path": "docs/api/health-endpoint.md", + "title": "api.health_endpoint:Summary", + "document_id": "api.health_endpoint", + "entity_name": "", + "summary_text": "", + "section_path": "HTTP API /health > Summary", + "content_preview": "- Purpose: вернуть агрегированный health payload runtime без изменения состояния системы.\n- Actor: monitoring probe или оператор.\n- Trigger: HTTP `GET /health`.\n- Success rule: HTTP `200`, если `payload.status == \"ok\"`.\n- Degraded rule: HTTP `503`, если runtime вернул любой статус кроме `ok`.\n- Related domain: [runtime health](../domains/runtime-health-entity.md)." + }, + { + "layer": "D0_DOC_CHUNKS", + "path": "docs/api/health-endpoint.md", + "title": "api.health_endpoint:Описание", + "document_id": "api.health_endpoint", + "entity_name": "", + "summary_text": "", + "section_path": "HTTP API /health > Details > Описание", + "content_preview": "Endpoint отдает текущее состояние runtime и его компонентов. Метод нужен для readiness/liveness-проверок, мониторинга и диагностики worker'а `telegram_notify`." + }, + { + "layer": "D0_DOC_CHUNKS", + "path": "docs/api/health-endpoint.md", + "title": "api.health_endpoint:Сценарий", + "document_id": "api.health_endpoint", + "entity_name": "", + "summary_text": "", + "section_path": "HTTP API /health > Details > Сценарий", + "content_preview": "**Название:** Получение агрегированного состояния runtime\n\n**Предусловия:**\n- HTTP control plane запущен через `TelegramControlChannel`.\n- Для канала зарегистрирован `health_provider`.\n\n**Триггер:**\n- Внешний клиент отправляет `GET /health`.\n\n**Основной сценарий:**\n1. Endpoint принимает HTTP-запрос без path, query и body параметров.\n2. `TelegramControlChannel` вызывает асинхронный `health_provider" + } + ], + "metadata_lookup": [ + { + "layer": "D3_ENTITY_CATALOG", + "path": "docs/api/health-endpoint.md", + "title": "TelegramControlChannel", + "document_id": "api.health_endpoint", + "entity_name": "TelegramControlChannel", + "summary_text": "", + "section_path": "", + "content_preview": "TelegramControlChannel" + } + ], + "semantic": [ + { + "layer": "D3_ENTITY_CATALOG", + "path": "docs/api/health-endpoint.md", + "title": "TelegramControlChannel", + "document_id": "api.health_endpoint", + "entity_name": "TelegramControlChannel", + "summary_text": "", + "section_path": "", + "content_preview": "TelegramControlChannel" + }, + { + "layer": "D1_DOCUMENT_CATALOG", + "path": "docs/api/health-endpoint.md", + "title": "HTTP API /health", + "document_id": "api.health_endpoint", + "entity_name": "", + "summary_text": "- Purpose: вернуть агрегированный health payload runtime без изменения состояния системы.\n- Actor: monitoring probe или оператор.\n- Trigger: HTTP `GET /health`.\n- Success rule: HTTP `200`, если `payload.status == \"ok\"`.\n- Degraded rule: HTTP `503`, если runtime вернул любой статус кроме `ok`.\n- Related domain: [runtime health](../domains/runtime-health-entity.md).", + "section_path": "", + "content_preview": "- Purpose: вернуть агрегированный health payload runtime без изменения состояния системы.\n- Actor: monitoring probe или оператор.\n- Trigger: HTTP `GET /health`.\n- Success rule: HTTP `200`, если `payload.status == \"ok\"`.\n- Degraded rule: HTTP `503`, если runtime вернул любой статус кроме `ok`.\n- Related domain: [runtime health](../domains/runtime-health-entity.md)." + }, + { + "layer": "D0_DOC_CHUNKS", + "path": "docs/api/health-endpoint.md", + "title": "api.health_endpoint:Summary", + "document_id": "api.health_endpoint", + "entity_name": "", + "summary_text": "", + "section_path": "HTTP API /health > Summary", + "content_preview": "- Purpose: вернуть агрегированный health payload runtime без изменения состояния системы.\n- Actor: monitoring probe или оператор.\n- Trigger: HTTP `GET /health`.\n- Success rule: HTTP `200`, если `payload.status == \"ok\"`.\n- Degraded rule: HTTP `503`, если runtime вернул любой статус кроме `ok`.\n- Related domain: [runtime health](../domains/runtime-health-entity.md)." + }, + { + "layer": "D0_DOC_CHUNKS", + "path": "docs/api/health-endpoint.md", + "title": "api.health_endpoint:Описание", + "document_id": "api.health_endpoint", + "entity_name": "", + "summary_text": "", + "section_path": "HTTP API /health > Details > Описание", + "content_preview": "Endpoint отдает текущее состояние runtime и его компонентов. Метод нужен для readiness/liveness-проверок, мониторинга и диагностики worker'а `telegram_notify`." + }, + { + "layer": "D0_DOC_CHUNKS", + "path": "docs/api/health-endpoint.md", + "title": "api.health_endpoint:Сценарий", + "document_id": "api.health_endpoint", + "entity_name": "", + "summary_text": "", + "section_path": "HTTP API /health > Details > Сценарий", + "content_preview": "**Название:** Получение агрегированного состояния runtime\n\n**Предусловия:**\n- HTTP control plane запущен через `TelegramControlChannel`.\n- Для канала зарегистрирован `health_provider`.\n\n**Триггер:**\n- Внешний клиент отправляет `GET /health`.\n\n**Основной сценарий:**\n1. Endpoint принимает HTTP-запрос без path, query и body параметров.\n2. `TelegramControlChannel` вызывает асинхронный `health_provider" + } + ] + } +} +``` + +## process.v2.pipeline +```json +{ + "event": "retrieval_executed", + "query": "Как работает метод health?", + "profile": "docs_api_method_explain", + "row_count": 40, + "target_doc_hints": [ + "/health", + "health", + "health-endpoint", + "health endpoint", + "docs/api/health-endpoint.md" + ], + "top_results": [ + { + "layer": "D3_ENTITY_CATALOG", + "path": "docs/api/health-endpoint.md", + "title": "TelegramControlChannel", + "document_id": "api.health_endpoint", + "entity_name": "TelegramControlChannel", + "summary_text": "", + "section_path": "", + "content_preview": "TelegramControlChannel" + }, + { + "layer": "D1_DOCUMENT_CATALOG", + "path": "docs/api/health-endpoint.md", + "title": "HTTP API /health", + "document_id": "api.health_endpoint", + "entity_name": "", + "summary_text": "- Purpose: вернуть агрегированный health payload runtime без изменения состояния системы.\n- Actor: monitoring probe или оператор.\n- Trigger: HTTP `GET /health`.\n- Success rule: HTTP `200`, если `payload.status == \"ok\"`.\n- Degraded rule: HTTP `503`, если runtime вернул любой статус кроме `ok`.\n- Related domain: [runtime health](../domains/runtime-health-entity.md).", + "section_path": "", + "content_preview": "- Purpose: вернуть агрегированный health payload runtime без изменения состояния системы.\n- Actor: monitoring probe или оператор.\n- Trigger: HTTP `GET /health`.\n- Success rule: HTTP `200`, если `payload.status == \"ok\"`.\n- Degraded rule: HTTP `503`, если runtime вернул любой статус кроме `ok`.\n- Related domain: [runtime health](../domains/runtime-health-entity.md)." + }, + { + "layer": "D0_DOC_CHUNKS", + "path": "docs/api/health-endpoint.md", + "title": "api.health_endpoint:Summary", + "document_id": "api.health_endpoint", + "entity_name": "", + "summary_text": "", + "section_path": "HTTP API /health > Summary", + "content_preview": "- Purpose: вернуть агрегированный health payload runtime без изменения состояния системы.\n- Actor: monitoring probe или оператор.\n- Trigger: HTTP `GET /health`.\n- Success rule: HTTP `200`, если `payload.status == \"ok\"`.\n- Degraded rule: HTTP `503`, если runtime вернул любой статус кроме `ok`.\n- Related domain: [runtime health](../domains/runtime-health-entity.md)." + }, + { + "layer": "D0_DOC_CHUNKS", + "path": "docs/api/health-endpoint.md", + "title": "api.health_endpoint:Описание", + "document_id": "api.health_endpoint", + "entity_name": "", + "summary_text": "", + "section_path": "HTTP API /health > Details > Описание", + "content_preview": "Endpoint отдает текущее состояние runtime и его компонентов. Метод нужен для readiness/liveness-проверок, мониторинга и диагностики worker'а `telegram_notify`." + }, + { + "layer": "D0_DOC_CHUNKS", + "path": "docs/api/health-endpoint.md", + "title": "api.health_endpoint:Сценарий", + "document_id": "api.health_endpoint", + "entity_name": "", + "summary_text": "", + "section_path": "HTTP API /health > Details > Сценарий", + "content_preview": "**Название:** Получение агрегированного состояния runtime\n\n**Предусловия:**\n- HTTP control plane запущен через `TelegramControlChannel`.\n- Для канала зарегистрирован `health_provider`.\n\n**Триггер:**\n- Внешний клиент отправляет `GET /health`.\n\n**Основной сценарий:**\n1. Endpoint принимает HTTP-запрос без path, query и body параметров.\n2. `TelegramControlChannel` вызывает асинхронный `health_provider" + } + ] +} +``` + +## process.v2.evidence +```json +{ + "event": "evidence_assembled", + "mode": "summary", + "document_count": 2, + "documents": [ + "docs/api/health-endpoint.md", + "docs/domains/runtime-health-entity.md" + ] +} +``` + +## process.v2.pipeline +```json +{ + "event": "evidence_assembled", + "mode": "summary", + "primary_doc": "docs/api/health-endpoint.md", + "document_count": 2 +} +``` + +## process.v2.pipeline +```json +{ + "event": "ranking_explained", + "doc": "docs/api/health-endpoint.md", + "score_breakdown": { + "semantic": 140, + "path_match": 60, + "filename_match": 600, + "alias_match": 0, + "anchor_boost": 360, + "target_doc_boost": 1540, + "specificity_boost": 460, + "generic_penalty": 0 + }, + "score": 3160, + "match_reason": "exact_path" +} +``` + +## process.v2.pipeline +```json +{ + "event": "ranking_explained", + "doc": "docs/domains/runtime-health-entity.md", + "score_breakdown": { + "semantic": 140, + "path_match": 60, + "filename_match": 600, + "alias_match": 0, + "anchor_boost": 0, + "target_doc_boost": 0, + "specificity_boost": 180, + "generic_penalty": 0 + }, + "score": 980, + "match_reason": "exact_title" +} +``` + +## process.v2.pipeline +```json +{ + "event": "ranking_explained", + "top_docs_after_ranking": [ + { + "doc": "docs/api/health-endpoint.md", + "score": 3160, + "match_reason": "exact_path" + }, + { + "doc": "docs/domains/runtime-health-entity.md", + "score": 980, + "match_reason": "exact_title" + } + ], + "ranking_score_breakdown": [ + { + "doc": "docs/api/health-endpoint.md", + "score_breakdown": { + "semantic": 140, + "path_match": 60, + "filename_match": 600, + "alias_match": 0, + "anchor_boost": 360, + "target_doc_boost": 1540, + "specificity_boost": 460, + "generic_penalty": 0 + } + }, + { + "doc": "docs/domains/runtime-health-entity.md", + "score_breakdown": { + "semantic": 140, + "path_match": 60, + "filename_match": 600, + "alias_match": 0, + "anchor_boost": 0, + "target_doc_boost": 0, + "specificity_boost": 180, + "generic_penalty": 0 + } + } + ] +} +``` + +## process.v2.pipeline +```json +{ + "event": "evidence_gate_checked", + "passed": true, + "reason": "target_doc_found", + "answer_mode": "grounded_summary" +} +``` + +## workflow.v2.summary +```json +{ + "event": "workflow_started", + "workflow_id": "v2.docs_explain.summary" +} +``` + +## workflow.v2.summary.llm +```json +{ + "event": "request", + "prompt_name": "v2_docs_explain.summary_answer", + "system_prompt": "Ты объясняешь документацию только на основе найденных SUMMARY-блоков.\nИспользуй только факты из входного контекста.\nЕсли информации мало, прямо скажи об этом и не додумывай детали.\nВ конце перечисли файлы, на которые ты опирался.", + "user_prompt": "Запрос пользователя:\nКак работает метод health?\n\nСигналы запроса:\n{\n \"entity_names\": [],\n \"file_names\": [],\n \"endpoint_paths\": [\n \"/health\"\n ],\n \"target_doc_hints\": [\n \"/health\",\n \"health\",\n \"health-endpoint\",\n \"health endpoint\",\n \"docs/api/health-endpoint.md\"\n ],\n \"matched_aliases\": [],\n \"process_domain\": null,\n \"process_subdomain\": null,\n \"signal_types\": [\n \"API_ENDPOINT\"\n ]\n}\n\nНайденные SUMMARY-блоки:\n\n1. path: docs/api/health-endpoint.md\ntitle: TelegramControlChannel\nmatch_reason: exact_path\nsummary: TelegramControlChannel\n\n2. path: docs/domains/runtime-health-entity.md\ntitle: WorkerStatus\nmatch_reason: exact_title\nsummary: WorkerStatus", + "log_context": "agent:req_33518d79abdf4bafa39ab6dfc6064b75" +} +``` + +## workflow.v2.summary.llm +```json +{ + "event": "response", + "text": "На основе предоставленных SUMMARY-блоков конкретной информации о том, как именно работает метод health, нет. Документ \"TelegramControlChannel\" в docs/api/health-endpoint.md содержит информацию, связанную с TelegramControlChannel, а \"WorkerStatus\" в docs/domains/runtime-health-entity.md описывает сущность WorkerStatus. Для получения детальной информации о работе метода health необходимо обратиться к полному содержанию указанных документов или предоставить дополнительную информацию.\n\nФайлы для ознакомления:\n- docs/api/health-endpoint.md\n- docs/domains/runtime-health-entity.md" +} +``` + +## workflow.v2.summary +```json +{ + "event": "workflow_trace_flushed", + "workflow_id": "v2.docs_explain.summary", + "steps": [ + { + "step_id": "generate_summary_answer", + "title": "Сборка ответа по summary", + "input": {}, + "output": { + "answer_length": 580 + } + } + ] +} +``` + +## workflow.v2.summary +```json +{ + "event": "workflow_completed", + "workflow_id": "v2.docs_explain.summary" +} +``` + +## process.v2.pipeline +```json +{ + "event": "answer_generated", + "answer_mode": "grounded_summary", + "answer_length": 580 +} +``` + +## result +```json +{ + "status": "done", + "answer": "На основе предоставленных SUMMARY-блоков конкретной информации о том, как именно работает метод health, нет. Документ \"TelegramControlChannel\" в docs/api/health-endpoint.md содержит информацию, связанную с TelegramControlChannel, а \"WorkerStatus\" в docs/domains/runtime-health-entity.md описывает сущность WorkerStatus. Для получения детальной информации о работе метода health необходимо обратиться к полному содержанию указанных документов или предоставить дополнительную информацию.\n\nФайлы для ознакомления:\n- docs/api/health-endpoint.md\n- docs/domains/runtime-health-entity.md", + "completed_at": "2026-04-07T18:33:43.746125+00:00" +} +``` diff --git a/src/app/core/agent/processes/v2/anchor_signals.py b/src/app/core/agent/processes/v2/anchor_signals.py index 9b9f732..ea9b177 100644 --- a/src/app/core/agent/processes/v2/anchor_signals.py +++ b/src/app/core/agent/processes/v2/anchor_signals.py @@ -4,17 +4,17 @@ from app.core.agent.processes.v2.models import V2AnchorType, V2RouteAnchors, V2R def anchor_signal_types(route: V2RouteResult) -> set[str]: - hints = [str(item).strip().lower() for item in route.anchors.target_doc_hints if str(item or "").strip()] + texts = _signal_texts(route) signals: set[str] = set() if route.subintent == V2Subintent.FIND_FILES: signals.add(V2AnchorType.FIND_FILES) - if route.anchors.endpoint_paths or _has_hint(hints, "/api/"): + if route.anchors.endpoint_paths or _has_any(texts, ("/api/", "api", "endpoint")): signals.add(V2AnchorType.API_ENDPOINT) - if _has_hint(hints, "/architecture/"): + if _has_any(texts, ("/architecture/", "architecture", "arch")): signals.add(V2AnchorType.ARCHITECTURE) - if _has_hint(hints, "/logic/"): + if _has_any(texts, ("/logic/", "logic", "workflow", "flow", "process")): signals.add(V2AnchorType.LOGIC_FLOW) - if _has_hint(hints, "/domains/"): + if route.anchors.entity_names or _has_any(texts, ("/domains/", "domain", "entity", "component")): signals.add(V2AnchorType.DOMAIN_ENTITY) return signals @@ -44,5 +44,14 @@ def anchors_have_signal(anchors: V2RouteAnchors, signal: str, *, subintent: str return signal in anchor_signal_types(route) -def _has_hint(hints: list[str], marker: str) -> bool: - return any(marker in hint for hint in hints) +def _signal_texts(route: V2RouteResult) -> list[str]: + items = [ + *route.anchors.target_doc_hints, + *route.anchors.file_names, + *route.anchors.matched_aliases, + ] + return [str(item).strip().lower() for item in items if str(item or "").strip()] + + +def _has_any(items: list[str], markers: tuple[str, ...]) -> bool: + return any(marker in item for item in items for marker in markers) diff --git a/src/app/core/agent/processes/v2/evidence/assembler.py b/src/app/core/agent/processes/v2/evidence/assembler.py index 77eb51e..537a9cc 100644 --- a/src/app/core/agent/processes/v2/evidence/assembler.py +++ b/src/app/core/agent/processes/v2/evidence/assembler.py @@ -11,6 +11,8 @@ from app.core.rag.contracts.enums import RagLayer class DocsEvidenceAssembler: + _API_PATH_PREFIXES = ("docs/api/", "docs/endpoints/", "docs/methods/", "api/", "endpoints/", "methods/") + _GENERIC_DOC_MARKERS = ("readme", "overview", "index", "navigation", "related docs", "catalog") def assemble_summaries(self, rows: list[dict], route: V2RouteResult) -> list[RetrievedSummary]: items = self._rank_rows(rows, route, mode="summary") ranked = [ @@ -71,10 +73,12 @@ class DocsEvidenceAssembler: "score": score, "score_breakdown": breakdown, "match_reason": self._match_reason(breakdown), + "is_generic_doc": self._is_generic_doc(path, self._title(row, path), self._summary(row), row), } ) ranked.sort(key=lambda item: (-item["score"], item["path"])) - return self._ensure_target_docs_in_top_k(ranked, route, k=4 if mode == "find_files" else 3) + ranked = self._ensure_target_docs_in_top_k(ranked, route, k=4 if mode == "find_files" else 3) + return self._promote_specific_primary(ranked, route) def _score_breakdown(self, row: dict, route: V2RouteResult, *, mode: str) -> dict[str, int]: path_raw = self._path(row) @@ -93,6 +97,7 @@ class DocsEvidenceAssembler: "alias_match": 0, "anchor_boost": 0, "target_doc_boost": 0, + "specificity_boost": 0, "generic_penalty": 0, } if route.intent == "GENERAL_QA": @@ -100,6 +105,7 @@ class DocsEvidenceAssembler: hint_norm_lower = {normalize_doc_path(h).lower() for h in route.anchors.target_doc_hints if str(h or "").strip()} if normalize_doc_path(path_raw).lower() in hint_norm_lower: breakdown["target_doc_boost"] += 1000 + hint_texts = [str(hint or "").strip().lower() for hint in route.anchors.target_doc_hints if str(hint or "").strip()] if any(alias.lower() in " ".join([path, title, summary, entity]) for alias in route.anchors.matched_aliases): breakdown["alias_match"] += 500 for token in query_tokens: @@ -111,10 +117,25 @@ class DocsEvidenceAssembler: breakdown["semantic"] += 20 if self._compact(token) in compact_haystack: breakdown["alias_match"] += 250 + for hint in hint_texts: + compact_hint = self._compact(hint) + if compact_hint and compact_hint in compact_haystack: + breakdown["target_doc_boost"] += 180 + elif hint and hint.strip("/") in " ".join([path, title, summary, entity]): + breakdown["semantic"] += 70 + endpoint_text = self._summary(row).lower() + for endpoint in route.anchors.endpoint_paths: + normalized_endpoint = endpoint.strip().lower() + endpoint_slug = normalized_endpoint.strip("/") + if normalized_endpoint and normalized_endpoint in endpoint_text: + breakdown["target_doc_boost"] += 260 + if endpoint_slug and endpoint_slug in filename: + breakdown["filename_match"] += 200 if any(endpoint.strip("/").lower() in filename for endpoint in route.anchors.endpoint_paths): breakdown["filename_match"] += 200 signals = anchor_signal_types(route) breakdown["anchor_boost"] += self._anchor_boost(path, signals) + breakdown["specificity_boost"] += self._specificity_boost(row, path, title, summary, route) breakdown["generic_penalty"] += self._generic_penalty(path, signals) if mode == "find_files": breakdown["path_match"] *= 3 @@ -125,8 +146,8 @@ class DocsEvidenceAssembler: def _anchor_boost(self, path: str, signals: set[str]) -> int: boost = 0 - if V2AnchorType.API_ENDPOINT in signals and path.startswith("docs/api/"): - boost += 300 + if V2AnchorType.API_ENDPOINT in signals and path.startswith(self._API_PATH_PREFIXES): + boost += 360 if V2AnchorType.LOGIC_FLOW in signals and path.startswith("docs/logic/"): boost += 300 if V2AnchorType.DOMAIN_ENTITY in signals and path.startswith("docs/domains/"): @@ -139,8 +160,11 @@ class DocsEvidenceAssembler: def _generic_penalty(self, path: str, signals: set[str]) -> int: penalty = 0 + lowered = path.lower() if path == "docs/README.md" and V2AnchorType.ARCHITECTURE not in signals: - penalty -= 200 + penalty -= 260 + if any(marker in lowered for marker in ("/readme", "readme.md", "/index", "/overview", "/catalog", "/navigation")): + penalty -= 220 if "/architecture/" in path and V2AnchorType.ARCHITECTURE not in signals and signals.intersection( {V2AnchorType.API_ENDPOINT, V2AnchorType.DOMAIN_ENTITY} ): @@ -173,6 +197,17 @@ class DocsEvidenceAssembler: top.sort(key=lambda item: (-item["score"], item["path"])) return top + remaining + def _promote_specific_primary(self, ranked: list[dict], route: V2RouteResult) -> list[dict]: + if len(ranked) < 2: + return ranked + first = ranked[0] + if not first.get("is_generic_doc"): + return ranked + promoted = next((item for item in ranked[1:] if not item.get("is_generic_doc") and self._is_specific_candidate(item, route)), None) + if promoted is None: + return ranked + return [promoted] + [item for item in ranked if item["path"] != promoted["path"]] + def _match_reason(self, breakdown: dict[str, int]) -> str: if breakdown["target_doc_boost"] > 0: return "exact_path" @@ -189,6 +224,53 @@ class DocsEvidenceAssembler: section = str(metadata.get("section_path") or "").lower() return "summary" in section or "свод" in section or "overview" in section + def _specificity_boost(self, row: dict, path: str, title: str, summary: str, route: V2RouteResult) -> int: + boost = 0 + filename = path.split("/")[-1] + lowered_title = title.lower() + lowered_summary = summary.lower() + if not self._is_generic_doc(path, title, summary, row): + boost += 90 + if path.startswith(self._API_PATH_PREFIXES): + boost += 160 + if "endpoint" in filename or "endpoint" in lowered_title or "method" in lowered_title: + boost += 120 + if row.get("layer") == RagLayer.DOCS_DOC_CHUNKS and not self._looks_like_navigation_chunk(row): + boost += 80 + for token in self._query_tokens(route): + if token and token in filename: + boost += 90 + if token and token in lowered_title: + boost += 70 + if token and token in lowered_summary: + boost += 40 + return boost + + def _is_specific_candidate(self, item: dict, route: V2RouteResult) -> bool: + breakdown = dict(item.get("score_breakdown") or {}) + if breakdown.get("target_doc_boost", 0) > 0: + return True + if breakdown.get("specificity_boost", 0) >= 160: + return True + return V2AnchorType.API_ENDPOINT in anchor_signal_types(route) and item["path"].startswith(self._API_PATH_PREFIXES) + + def _is_generic_doc(self, path: str, title: str, summary: str, row: dict) -> bool: + haystack = " ".join([path.lower(), title.lower(), summary.lower()]) + if any(marker in haystack for marker in self._GENERIC_DOC_MARKERS): + return True + return self._looks_like_navigation_chunk(row) + + def _looks_like_navigation_chunk(self, row: dict) -> bool: + text = self._summary(row).lower() + if not text: + return False + lines = [line.strip() for line in text.splitlines() if line.strip()] + bullet_lines = sum(1 for line in lines if line.startswith(("- ", "* ", "1.", "2.", "3."))) + link_lines = sum(1 for line in lines if "](" in line or line.startswith("docs/")) + if "related docs" in text or "navigation" in text: + return True + return bullet_lines >= 3 or link_lines >= 3 + def _query_tokens(self, route: V2RouteResult) -> list[str]: values = list(route.target_terms) + list(route.anchors.matched_aliases) tokens: list[str] = [] diff --git a/src/app/core/agent/processes/v2/intent_router/models.py b/src/app/core/agent/processes/v2/intent_router/models.py index 955ceb6..a054940 100644 --- a/src/app/core/agent/processes/v2/intent_router/models.py +++ b/src/app/core/agent/processes/v2/intent_router/models.py @@ -8,6 +8,7 @@ class QueryFeatures: normalized_query: str target_terms: list[str] endpoint_paths: list[str] + file_names: list[str] matched_aliases: list[str] target_doc_hints: list[str] file_markers: list[str] diff --git a/src/app/core/agent/processes/v2/intent_router/modules/anchors.py b/src/app/core/agent/processes/v2/intent_router/modules/anchors.py index 4730e12..61d83d5 100644 --- a/src/app/core/agent/processes/v2/intent_router/modules/anchors.py +++ b/src/app/core/agent/processes/v2/intent_router/modules/anchors.py @@ -34,10 +34,42 @@ class _MarkerScanner: "где описано", "документ с описанием", ) - _ARCHITECTURE_MARKERS = ("архитектура", "как устроено приложение", "как устроен сервис", "основные части системы", "из чего состоит") - _LOGIC_MARKERS = ("цикл", "loop", "worker", "как работает отправка уведомлений", "логика отправки", "background job", "runtime loop") + _ARCHITECTURE_MARKERS = ( + "архитектура", + "архитектур", + "architecture", + "arch overview", + "как устроено приложение", + "как устроен сервис", + "основные части системы", + "из чего состоит", + ) + _LOGIC_MARKERS = ( + "цикл", + "loop", + "flow", + "workflow", + "process", + "worker", + "как работает отправка уведомлений", + "логика отправки", + "background job", + "runtime loop", + ) _DOMAIN_MARKERS = ("runtime health", "health model", "статусы здоровья", "сущность", "entity", "здоровье runtime") - _ENDPOINT_MARKERS = ("endpoint", "метод api", "ручка", "эндпоинт") + _ENDPOINT_MARKERS = ( + "endpoint", + "api", + "route", + "method", + "метод api", + "метод", + "метода", + "ручка", + "эндпоинт", + "маршрут", + "роут", + ) def scan(self, lowered_query: str) -> dict[str, list[str]]: return { @@ -54,12 +86,13 @@ class _MarkerScanner: class _EntityNameExtractor: _ENTITY_RE = re.compile(r"\b[A-Z][A-Za-z0-9_]+\b") + _IGNORE = {"arch"} def extract(self, query: str) -> list[str]: items: list[str] = [] for match in self._ENTITY_RE.finditer(query): candidate = match.group(0).strip() - if candidate and candidate not in items: + if candidate and candidate.lower() not in self._IGNORE and candidate not in items: items.append(candidate) return items @@ -92,33 +125,61 @@ class _FileNameExtractor: items.append(value) +class _ProcessAnchorExtractor: + _DOMAIN_KEYWORDS = { + "billing": "billing", + "notifications": "notifications", + } + _SUBDOMAIN_KEYWORDS = { + "invoice": ("billing", "invoice"), + "invoices": ("billing", "invoice"), + "delivery_loop": ("notifications", "delivery_loop"), + "delivery": ("notifications", "delivery_loop"), + } + + def extract(self, lowered_query: str) -> tuple[str | None, str | None]: + domain = next((value for token, value in self._DOMAIN_KEYWORDS.items() if token in lowered_query), None) + subdomain: str | None = None + for token, mapping in self._SUBDOMAIN_KEYWORDS.items(): + if token in lowered_query: + domain = domain or mapping[0] + subdomain = mapping[1] + break + return domain, subdomain + + class V2AnchorExtractor: def __init__( self, marker_scanner: _MarkerScanner | None = None, entity_extractor: _EntityNameExtractor | None = None, file_name_extractor: _FileNameExtractor | None = None, + process_anchor_extractor: _ProcessAnchorExtractor | None = None, ) -> None: self._marker_scanner = marker_scanner or _MarkerScanner() self._entity_extractor = entity_extractor or _EntityNameExtractor() self._file_name_extractor = file_name_extractor or _FileNameExtractor() + self._process_anchor_extractor = process_anchor_extractor or _ProcessAnchorExtractor() def extract(self, normalized_query: str, terms: TargetTermsAnalysis) -> AnchorAnalysis: - markers = self._marker_scanner.scan(normalized_query.lower()) + lowered_query = normalized_query.lower() + markers = self._marker_scanner.scan(lowered_query) + process_domain, process_subdomain = self._process_anchor_extractor.extract(lowered_query) anchors = V2RouteAnchors( entity_names=self._entity_extractor.extract(normalized_query), file_names=self._file_name_extractor.extract(normalized_query), endpoint_paths=list(terms.endpoint_paths), target_doc_hints=self._target_doc_hints( endpoint_paths=terms.endpoint_paths, + api_like_terms=terms.api_like_terms, alias_docs=terms.alias_docs, architecture_markers=markers["architecture_markers"], logic_markers=markers["logic_markers"], domain_markers=markers["domain_markers"], ), matched_aliases=list(terms.matched_aliases), - process_domain=None, - process_subdomain=None, + process_domain=process_domain, + process_subdomain=process_subdomain, ) return AnchorAnalysis( anchors=anchors, @@ -133,6 +194,7 @@ class V2AnchorExtractor: self, *, endpoint_paths: list[str], + api_like_terms: list[str], alias_docs: list[str], architecture_markers: list[str], logic_markers: list[str], @@ -145,13 +207,41 @@ class V2AnchorExtractor: "/actions/{action}": "docs/api/control-actions-endpoint.md", } for endpoint in endpoint_paths: + for hint in self._endpoint_hint_variants(endpoint): + self._append_unique(hints, hint) hint = endpoint_map.get(endpoint) - if hint and hint not in hints: - hints.append(hint) - if architecture_markers and "docs/architecture/telegram-notify-app-overview.md" not in hints: - hints.append("docs/architecture/telegram-notify-app-overview.md") - if logic_markers and "docs/logic/telegram-notification-loop.md" not in hints: - hints.append("docs/logic/telegram-notification-loop.md") - if domain_markers and "docs/domains/runtime-health-entity.md" not in hints: - hints.append("docs/domains/runtime-health-entity.md") + self._append_unique(hints, hint) + for term in api_like_terms: + for hint in self._api_like_hint_variants(term): + self._append_unique(hints, hint) + if architecture_markers: + self._append_unique(hints, "docs/architecture/telegram-notify-app-overview.md") + if logic_markers: + self._append_unique(hints, "docs/logic/telegram-notification-loop.md") + if domain_markers: + self._append_unique(hints, "docs/domains/runtime-health-entity.md") return hints + + def _endpoint_hint_variants(self, endpoint: str) -> list[str]: + normalized = str(endpoint or "").strip().lower() + if not normalized: + return [] + slug = normalized.strip("/").replace("/", "-").replace("{", "").replace("}", "") + leaf = next((part for part in reversed(slug.split("-")) if part and part != "id"), "") + hints: list[str] = [normalized] + for value in (slug, leaf): + if not value: + continue + hints.extend([value, f"{value}-endpoint", f"{value} endpoint"]) + return list(dict.fromkeys(hints)) + + def _api_like_hint_variants(self, term: str) -> list[str]: + normalized = str(term or "").strip().lower().lstrip("/") + if not normalized: + return [] + return [normalized, f"/{normalized}", f"{normalized}-endpoint", f"{normalized} endpoint"] + + def _append_unique(self, items: list[str], value: str | None) -> None: + normalized = str(value or "").strip() + if normalized and normalized not in items: + items.append(normalized) diff --git a/src/app/core/agent/processes/v2/intent_router/modules/target_terms.py b/src/app/core/agent/processes/v2/intent_router/modules/target_terms.py index 9b42c91..3af9d55 100644 --- a/src/app/core/agent/processes/v2/intent_router/modules/target_terms.py +++ b/src/app/core/agent/processes/v2/intent_router/modules/target_terms.py @@ -8,6 +8,7 @@ from dataclasses import dataclass class TargetTermsAnalysis: target_terms: list[str] endpoint_paths: list[str] + api_like_terms: list[str] matched_aliases: list[str] alias_docs: list[str] @@ -26,7 +27,7 @@ class _AliasMatcher: _AliasRule(("control actions", "управление runtime"), "/actions/{action}", "docs/api/control-actions-endpoint.md"), _AliasRule(("runtime health", "здоровье runtime", "статусы здоровья"), "runtime_health", "docs/domains/runtime-health-entity.md"), _AliasRule(("цикл отправки уведомлений", "notification loop", "worker loop"), "telegram-notify-loop", "docs/logic/telegram-notification-loop.md"), - _AliasRule(("архитектура приложения", "overview"), "architecture_overview", "docs/architecture/telegram-notify-app-overview.md"), + _AliasRule(("архитектура приложения",), "architecture_overview", "docs/architecture/telegram-notify-app-overview.md"), _AliasRule(("архитектура",), "architecture_overview", "docs/architecture/telegram-notify-app-overview.md"), _AliasRule(("каталог ошибок", "errors catalog"), "errors_catalog", "docs/errors/catalog.yaml"), _AliasRule(("файл-индекс документации", "docs index", "индекс документации"), "docs_index", "docs/README.md"), @@ -51,6 +52,7 @@ class _AliasMatcher: class _EndpointPathExtractor: _PATH_RE = re.compile(r"`([^`]+)`|(/[A-Za-z0-9_./{}-]+)") _VALID_ENDPOINT_RE = re.compile(r"^/[a-z0-9._/-]+(?:/\{[a-z0-9_]+\})?$") + _DOC_EXTENSIONS = (".md", ".yaml", ".yml", ".json") def extract(self, query: str) -> list[str]: values: list[str] = [] @@ -68,28 +70,161 @@ class _EndpointPathExtractor: return trimmed.lower() def _is_endpoint(self, token: str) -> bool: - return bool(token and self._VALID_ENDPOINT_RE.fullmatch(token)) + if not token or not self._VALID_ENDPOINT_RE.fullmatch(token): + return False + return not token.endswith(self._DOC_EXTENSIONS) def _append_unique(self, items: list[str], value: str) -> None: if value and value not in items: items.append(value) +@dataclass(slots=True) +class _ApiLikeAnchorAnalysis: + endpoint_paths: list[str] + candidate_terms: list[str] + + +class _ApiLikeAnchorExtractor: + _TOKEN_RE = re.compile(r"[A-Za-zА-Яа-я0-9_./{}-]+") + _ASCII_ENDPOINT_RE = re.compile(r"^[a-z0-9]+(?:[-_][a-z0-9]+)*$") + _API_MARKERS = { + "api", + "endpoint", + "route", + "method", + "метод", + "метода", + "методу", + "ручка", + "ручки", + "эндпоинт", + "эндпоинта", + "маршрут", + "роут", + } + _EXPLAIN_MARKERS = { + "как", + "что", + "делает", + "работает", + "объясни", + "объяснить", + "расскажи", + "опиши", + "смысл", + } + _NOISE_WORDS = _API_MARKERS | _EXPLAIN_MARKERS | { + "про", + "какой", + "какая", + "какие", + "какого", + "какую", + "кратко", + "нужен", + "нужно", + "у", + } + _SHORT_QUERY_TOKEN_LIMIT = 7 + + def extract(self, query: str, explicit_endpoint_paths: list[str]) -> _ApiLikeAnchorAnalysis: + if explicit_endpoint_paths: + return _ApiLikeAnchorAnalysis(endpoint_paths=list(explicit_endpoint_paths), candidate_terms=[]) + token_entries = self._token_entries(query) + if not token_entries: + return _ApiLikeAnchorAnalysis(endpoint_paths=[], candidate_terms=[]) + candidate_terms = [token for token, _start in token_entries if self._is_api_candidate(token)] + if not candidate_terms: + return _ApiLikeAnchorAnalysis(endpoint_paths=[], candidate_terms=[]) + if self._has_api_marker(token_entries): + primary = self._primary_candidate(token_entries) + endpoint_paths = [self._ensure_endpoint(primary)] if primary else [] + return _ApiLikeAnchorAnalysis( + endpoint_paths=[path for path in endpoint_paths if path], + candidate_terms=[primary] if primary else [], + ) + if self._is_short_explain_query(token_entries) and len(candidate_terms) == 1: + return _ApiLikeAnchorAnalysis(endpoint_paths=[], candidate_terms=list(candidate_terms)) + return _ApiLikeAnchorAnalysis(endpoint_paths=[], candidate_terms=[]) + + def _token_entries(self, query: str) -> list[tuple[str, int]]: + entries: list[tuple[str, int]] = [] + for match in self._TOKEN_RE.finditer(query): + token = str(match.group(0) or "").strip().strip("`'\"()[]!?.,:;").lower() + if token: + entries.append((token, match.start())) + return entries + + def _has_api_marker(self, token_entries: list[tuple[str, int]]) -> bool: + return any(token in self._API_MARKERS for token, _start in token_entries) + + def _is_short_explain_query(self, token_entries: list[tuple[str, int]]) -> bool: + if len(token_entries) > self._SHORT_QUERY_TOKEN_LIMIT: + return False + return any(token in self._EXPLAIN_MARKERS for token, _start in token_entries) + + def _primary_candidate(self, token_entries: list[tuple[str, int]]) -> str | None: + marker_positions = [start for token, start in token_entries if token in self._API_MARKERS] + candidates = [(token, start) for token, start in token_entries if self._is_api_candidate(token)] + if not candidates: + return None + if not marker_positions: + return candidates[-1][0] + primary = min( + candidates, + key=lambda item: min(abs(item[1] - marker_pos) for marker_pos in marker_positions), + ) + return primary[0] + + def _is_api_candidate(self, token: str) -> bool: + if ( + not token + or token in self._NOISE_WORDS + or token.startswith("docs/") + or token.endswith((".md", ".yaml", ".yml", ".json")) + ): + return False + if token.startswith("/"): + return True + return self._ASCII_ENDPOINT_RE.fullmatch(token) is not None and len(token) >= 3 + + def _ensure_endpoint(self, token: str) -> str: + return token if token.startswith("/") else f"/{token}" + + class _TermCollector: _TOKEN_RE = re.compile(r"[A-Za-zА-Яа-я0-9_./{}-]+") _IDENTIFIER_RE = re.compile( r"^(?:[a-z0-9]+(?:[_-][a-z0-9]+)+|[a-z]+[A-Z][A-Za-z0-9]+|(?:[A-Z][a-z0-9]+){2,})$" ) _QUESTION_WORDS = {"что", "как", "где", "какой", "какие", "каком", "когда", "чего"} - _INTENT_WORDS = {"объясни", "покажи", "найди", "расскажи", "дай", "опиши", "нужен"} - _FILLER_WORDS = {"про", "там", "тут", "плз"} + _INTENT_WORDS = {"объясни", "покажи", "найди", "расскажи", "дай", "опиши", "нужен", "show"} + _FILLER_WORDS = {"про", "там", "тут", "плз", "pls", "for"} _MARKER_WORDS = { "файл", "файле", + "file", + "method", + "метод", + "метода", + "методу", + "route", + "ручка", + "ручки", + "эндпоинт", + "эндпоинта", + "overview", + "architecture", + "arch", + "flow", + "process", + "workflow", "док", "дока", "доках", "документ", + "doc", "описан", "док-саммари", "summary", @@ -115,6 +250,7 @@ class _TermCollector: "service", "summary", "endpoint", + "docs", } _MAX_TERMS = 7 @@ -191,19 +327,23 @@ class V2TargetTermsExtractor: self, alias_matcher: _AliasMatcher | None = None, endpoint_extractor: _EndpointPathExtractor | None = None, + api_like_extractor: _ApiLikeAnchorExtractor | None = None, term_collector: _TermCollector | None = None, ) -> None: self._alias_matcher = alias_matcher or _AliasMatcher() self._endpoint_extractor = endpoint_extractor or _EndpointPathExtractor() + self._api_like_extractor = api_like_extractor or _ApiLikeAnchorExtractor() self._term_collector = term_collector or _TermCollector() def extract(self, normalized_query: str) -> TargetTermsAnalysis: lowered = normalized_query.lower() endpoint_paths = self._endpoint_extractor.extract(normalized_query) + api_like = self._api_like_extractor.extract(normalized_query, endpoint_paths) alias_terms, alias_docs, alias_hits = self._alias_matcher.match(lowered) return TargetTermsAnalysis( - target_terms=self._term_collector.collect(normalized_query, alias_terms, endpoint_paths), - endpoint_paths=endpoint_paths, + target_terms=self._term_collector.collect(normalized_query, alias_terms, api_like.endpoint_paths), + endpoint_paths=api_like.endpoint_paths, + api_like_terms=api_like.candidate_terms, matched_aliases=alias_hits, alias_docs=alias_docs, ) diff --git a/src/app/core/agent/processes/v2/intent_router/router.py b/src/app/core/agent/processes/v2/intent_router/router.py index 7226b07..c78596d 100644 --- a/src/app/core/agent/processes/v2/intent_router/router.py +++ b/src/app/core/agent/processes/v2/intent_router/router.py @@ -44,6 +44,7 @@ class V2IntentRouter: normalized_query=normalized_query, target_terms=list(target_terms_analysis.target_terms), endpoint_paths=list(target_terms_analysis.endpoint_paths), + file_names=list(anchor_analysis.anchors.file_names), matched_aliases=list(target_terms_analysis.matched_aliases), target_doc_hints=list(anchor_analysis.anchors.target_doc_hints), file_markers=list(anchor_analysis.file_markers), @@ -58,6 +59,7 @@ class V2IntentRouter: anchors=anchor_analysis.anchors, ) llm_result = self._validator.validate(llm_candidate) + llm_result = self._apply_deterministic_corrections(llm_result, features) if llm_result is not None: confidence = self._confidence_adjuster.adjust(float(llm_result["confidence"]), features) return V2RouteResult( @@ -99,3 +101,18 @@ class V2IntentRouter: ) except Exception: return None + + def _apply_deterministic_corrections(self, candidate: dict | None, features: QueryFeatures) -> dict | None: + if candidate is None: + return None + if candidate.get("routing_domain") == "DOCS" and self._should_force_find_files(features): + corrected = dict(candidate) + corrected["subintent"] = "FIND_FILES" + return corrected + return candidate + + def _should_force_find_files(self, features: QueryFeatures) -> bool: + if features.file_markers or features.file_names: + return True + query = features.normalized_query.lower() + return "show doc" in query or "show file" in query or "doc for" in query diff --git a/src/app/core/agent/processes/v2/intent_router/routers/docs_subintent_resolver.py b/src/app/core/agent/processes/v2/intent_router/routers/docs_subintent_resolver.py index bb3af76..e1265dc 100644 --- a/src/app/core/agent/processes/v2/intent_router/routers/docs_subintent_resolver.py +++ b/src/app/core/agent/processes/v2/intent_router/routers/docs_subintent_resolver.py @@ -6,7 +6,7 @@ from app.core.agent.processes.v2.models import V2Subintent class DocsSubintentResolver: def resolve(self, features: QueryFeatures) -> str | None: - if features.file_markers: + if features.file_markers or self._has_file_like_anchor(features): return V2Subintent.FIND_FILES if any( ( @@ -20,3 +20,9 @@ class DocsSubintentResolver: ): return V2Subintent.SUMMARY return None + + def _has_file_like_anchor(self, features: QueryFeatures) -> bool: + return any( + hint.endswith((".md", ".yaml", ".yml", ".json")) + for hint in features.target_doc_hints + ) or any(token.endswith((".md", ".yaml", ".yml", ".json")) for token in features.file_names) diff --git a/src/app/core/agent/processes/v2/process.py b/src/app/core/agent/processes/v2/process.py index 60b8893..b862ce5 100644 --- a/src/app/core/agent/processes/v2/process.py +++ b/src/app/core/agent/processes/v2/process.py @@ -14,7 +14,6 @@ from app.core.agent.processes.v2.retrieval.target_doc_seeding import ( merge_row_lists, normalize_doc_path, normalized_path_set, - path_variants_for_rag_query, row_path, seed_candidates_from_target_hints, ) @@ -121,11 +120,9 @@ class V2Process(AgentProcess): "retrieval_profile_selected", {"profile": plan.profile, "layers": plan.layers, "filters": plan.filters}, ) - seeded_rows = await self._seed_candidates_from_target_hints(rag_session_id, plan.layers, route) - semantic_rows = await self._rag_adapter.fetch_rows(rag_session_id, route.normalized_query, plan) - metadata_rows = self._metadata_lookup_candidates([*seeded_rows, *semantic_rows], route) - rows = self._merge_candidate_rows(seeded_rows, metadata_rows, semantic_rows) - rows = await self._ensure_target_hints_in_pool(rag_session_id, rows, route) + retrieved_rows = await self._rag_adapter.fetch_rows(rag_session_id, route.normalized_query, plan) + metadata_rows = self._metadata_lookup_candidates(retrieved_rows, route) + rows = self._merge_candidate_rows(retrieved_rows, metadata_rows) rows = seed_candidates_from_target_hints(rows, route.anchors.target_doc_hints, RagRowIndex(rows)) self._print_missing_target_hints(route, rows) context.trace.module("process.v2.rag_retrieval").log( @@ -150,9 +147,9 @@ class V2Process(AgentProcess): "target_doc_hints": route.anchors.target_doc_hints, "candidate_docs_before_ranking": [self._trace_row(row) for row in rows[:8]], "sources": { - "seeded": [self._trace_row(row) for row in seeded_rows[:5]], + "seeded": [self._trace_row(row) for row in retrieved_rows[:5] if row_path(row) in {normalize_doc_path(h) for h in route.anchors.target_doc_hints}], "metadata_lookup": [self._trace_row(row) for row in metadata_rows[:5]], - "semantic": [self._trace_row(row) for row in semantic_rows[:5]], + "semantic": [self._trace_row(row) for row in retrieved_rows[:5]], }, }, ) @@ -262,61 +259,11 @@ class V2Process(AgentProcess): if not str(hint or "").strip(): continue normalized = normalize_doc_path(hint) + if not normalized.startswith("docs/") or "." not in normalized.rsplit("/", 1)[-1]: + continue if normalized not in candidate_paths: print("ERROR: target doc missing from candidates:", normalized) - async def _ensure_target_hints_in_pool(self, rag_session_id: str, rows: list[dict], route) -> list[dict]: - hints_raw = [str(item).strip() for item in route.anchors.target_doc_hints if str(item or "").strip()] - if not hints_raw: - return rows - pool = normalized_path_set(rows) - missing_hints = [h for h in hints_raw if normalize_doc_path(h) not in pool] - if not missing_hints: - return rows - variant_paths: list[str] = [] - for h in missing_hints: - variant_paths.extend(path_variants_for_rag_query(h)) - variant_paths = list(dict.fromkeys(variant_paths)) - extra_exact = await self._rag_adapter.fetch_exact_paths(rag_session_id, paths=variant_paths, layers=None) - pool2 = normalized_path_set(extra_exact) - still_missing = [h for h in missing_hints if normalize_doc_path(h) not in pool2] - fallback_rows: list[dict] = [] - if still_missing: - needles = [normalize_doc_path(h).split("/")[-1] for h in still_missing] - needles = list(dict.fromkeys(n for n in needles if n)) - if needles: - fallback_rows = await self._rag_adapter.fetch_chunks_by_path_substrings( - rag_session_id, - path_needles=needles, - layers=None, - ) - return merge_row_lists(rows, extra_exact, fallback_rows) - - async def _seed_candidates_from_target_hints(self, rag_session_id: str, layers: list[str], route) -> list[dict]: - del layers # seed по пути должен видеть все слои (иначе D0-only чанки теряются при file_lookup). - hints_raw = [str(item).strip() for item in route.anchors.target_doc_hints if str(item or "").strip()] - if not hints_raw: - return [] - variant_paths: list[str] = [] - for h in hints_raw: - variant_paths.extend(path_variants_for_rag_query(h)) - variant_paths = list(dict.fromkeys(variant_paths)) - exact_rows = await self._rag_adapter.fetch_exact_paths(rag_session_id, paths=variant_paths, layers=None) - paths_found = normalized_path_set(exact_rows) - missing = [h for h in hints_raw if normalize_doc_path(h) not in paths_found] - if not missing: - return exact_rows - needles = [normalize_doc_path(h).split("/")[-1] for h in missing] - needles = list(dict.fromkeys(n for n in needles if n)) - if not needles: - return exact_rows - fallback_rows = await self._rag_adapter.fetch_chunks_by_path_substrings( - rag_session_id, - path_needles=needles, - layers=None, - ) - return merge_row_lists(exact_rows, fallback_rows) - def _metadata_lookup_candidates(self, rows: list[dict], route) -> list[dict]: return DocsMetadataLookupIndex(rows).lookup(route) diff --git a/src/app/core/agent/processes/v2/retrieval/policy_resolver.py b/src/app/core/agent/processes/v2/retrieval/policy_resolver.py index bd424d3..3184a73 100644 --- a/src/app/core/agent/processes/v2/retrieval/policy_resolver.py +++ b/src/app/core/agent/processes/v2/retrieval/policy_resolver.py @@ -1,4 +1,4 @@ -"""Intent-aware retrieval policy resolver для процесса v2.""" +"""Intent-aware retrieval policy resolver for process v2.""" from __future__ import annotations @@ -8,91 +8,113 @@ from app.core.rag.contracts.enums import RagLayer from app.core.rag.retrieval.session_retriever import RetrievalPlan -class V2RetrievalPolicyResolver: - _SUMMARY_LAYERS = [ - RagLayer.DOCS_DOCUMENT_CATALOG, - RagLayer.DOCS_ENTITY_CATALOG, - RagLayer.DOCS_DOC_CHUNKS, - ] - _GENERAL_LAYERS = [ - RagLayer.DOCS_DOCUMENT_CATALOG, - RagLayer.DOCS_DOC_CHUNKS, +class _AnchorTermCollector: + def prefer_like_patterns(self, route: V2RouteResult) -> list[str]: + terms = self._hint_basenames(route) + terms.extend(route.anchors.endpoint_paths) + terms.extend(route.target_terms) + terms.extend(route.anchors.file_names) + terms.extend(route.anchors.entity_names) + terms.extend(route.anchors.matched_aliases) + terms.extend(self._process_terms(route)) + return [f"%{term.lower()}%" for term in _unique_terms(terms)] + + def find_files_patterns(self, route: V2RouteResult) -> list[str]: + if route.anchors.target_doc_hints: + return [f"%{name.lower()}%" for name in self._hint_basenames(route)] + return self.prefer_like_patterns(route) + + def api_method_patterns(self, route: V2RouteResult) -> list[str]: + terms = self._hint_basenames(route) + terms.extend(route.anchors.target_doc_hints) + terms.extend(route.anchors.endpoint_paths) + terms.extend(route.target_terms) + patterns: list[str] = [] + for term in _unique_terms(terms): + lowered = term.lower() + stripped = lowered.strip("/") + if stripped: + patterns.append(f"%{stripped}%") + if lowered: + patterns.append(f"%{lowered}%") + return _unique_terms(patterns) + + def _hint_basenames(self, route: V2RouteResult) -> list[str]: + return [hint.rsplit("/", 1)[-1] for hint in route.anchors.target_doc_hints if str(hint).strip()] + + def _process_terms(self, route: V2RouteResult) -> list[str]: + terms: list[str] = [] + if route.anchors.process_domain: + terms.append(route.anchors.process_domain) + if route.anchors.process_subdomain: + terms.append(route.anchors.process_subdomain) + return terms + + +class _RouteFilterBuilder: + _API_DOC_PREFIXES = [ + "docs/api/", + "docs/endpoints/", + "docs/methods/", + "api/", + "endpoints/", + "methods/", ] - def resolve(self, route: V2RouteResult) -> RetrievalPlan: - if route.intent == V2Intent.GENERAL_QA: - return RetrievalPlan( - profile="general_qa_grounded_summary", - layers=list(self._GENERAL_LAYERS), - limit=8, - filters=self._general_filters(route), - ) - if route.subintent == V2Subintent.FIND_FILES: - return RetrievalPlan( - profile="file_lookup", - layers=[RagLayer.DOCS_DOCUMENT_CATALOG, RagLayer.DOCS_ENTITY_CATALOG], - limit=12, - filters=self._find_files_filters(route), - ) - return RetrievalPlan( - profile=self._summary_profile(route), - layers=list(self._SUMMARY_LAYERS), - limit=8, - filters=self._summary_filters(route), - ) + def __init__(self) -> None: + self._terms = _AnchorTermCollector() - def _summary_profile(self, route: V2RouteResult) -> str: - signals = anchor_signal_types(route) - if len(signals - {V2AnchorType.FIND_FILES}) != 1: - return "docs_summary_generic" - mapping = { - V2AnchorType.API_ENDPOINT: "docs_summary_api_endpoint", - V2AnchorType.ARCHITECTURE: "docs_summary_architecture", - V2AnchorType.LOGIC_FLOW: "docs_summary_logic_flow", - V2AnchorType.DOMAIN_ENTITY: "docs_summary_domain_entity", - } - signal = next(iter(signals - {V2AnchorType.FIND_FILES}), None) - return mapping.get(signal, "docs_summary_generic") - - def _general_filters(self, route: V2RouteResult) -> dict[str, object]: + def general_filters(self, route: V2RouteResult) -> dict[str, object]: return { "prefer_path_prefixes": ["docs/architecture/", "docs/"], - "prefer_like_patterns": ["%README.md%", "%overview%"], + "prefer_like_patterns": ["%readme.md%", "%overview%"], "target_doc_hints": list(route.anchors.target_doc_hints), } - def _summary_filters(self, route: V2RouteResult) -> dict[str, object]: - filters: dict[str, object] = { - "prefer_path_prefixes": self._summary_prefixes(route), - "prefer_like_patterns": self._prefer_like_patterns(route), - "target_doc_hints": list(route.anchors.target_doc_hints), - } + def summary_filters(self, route: V2RouteResult) -> dict[str, object]: + if _is_api_method_explain(route): + return self.api_method_filters(route) + filters = self._base_filters(route) + filters["prefer_path_prefixes"] = self._summary_prefixes(route) + filters["prefer_like_patterns"] = self._terms.prefer_like_patterns(route) if V2AnchorType.API_ENDPOINT in anchor_signal_types(route): - filters["path_prefixes"] = ["docs/api/", "docs/architecture/", "docs/"] + filters["path_prefixes"] = ["docs/api/", "docs/"] return filters - def _find_files_filters(self, route: V2RouteResult) -> dict[str, object]: + def api_method_filters(self, route: V2RouteResult) -> dict[str, object]: + filters = self._base_filters(route) + filters["path_prefixes"] = list(self._API_DOC_PREFIXES) + filters["prefer_path_prefixes"] = list(self._API_DOC_PREFIXES) + filters["prefer_like_patterns"] = self._terms.api_method_patterns(route) + return filters + + def find_files_filters(self, route: V2RouteResult) -> dict[str, object]: + filters = self._base_filters(route) + prefixes = self._find_files_prefixes(route) + if prefixes: + filters["path_prefixes"] = prefixes + filters["prefer_path_prefixes"] = self._find_files_prefer_prefixes(route, prefixes) + filters["prefer_like_patterns"] = self._terms.find_files_patterns(route) + return filters + + def _base_filters(self, route: V2RouteResult) -> dict[str, object]: filters: dict[str, object] = { - "prefer_path_prefixes": self._find_files_prefixes(route), - "prefer_like_patterns": self._prefer_like_patterns(route), "target_doc_hints": list(route.anchors.target_doc_hints), } - if route.anchors.target_doc_hints: - filters["prefer_like_patterns"] = [f"%{path.split('/')[-1]}%" for path in route.anchors.target_doc_hints] + if route.anchors.process_domain: + filters["metadata.domain"] = route.anchors.process_domain + if route.anchors.process_subdomain: + filters["metadata.subdomain"] = route.anchors.process_subdomain return filters - def _prefer_like_patterns(self, route: V2RouteResult) -> list[str]: - patterns: list[str] = [] - for path in route.anchors.target_doc_hints: - patterns.append(f"%{path.split('/')[-1]}%") - for endpoint in route.anchors.endpoint_paths: - patterns.append(f"%{endpoint}%") - return patterns - def _find_files_prefixes(self, route: V2RouteResult) -> list[str]: - if route.anchors.target_doc_hints: - prefixes = ["/".join(path.split("/")[:-1]) + "/" for path in route.anchors.target_doc_hints] - return [prefix for prefix in prefixes if prefix] + hint_prefixes = _prefixes_from_paths(route.anchors.target_doc_hints) + if hint_prefixes: + return hint_prefixes + file_prefixes = [name for name in route.anchors.file_names if str(name).strip().startswith("docs/")] + derived = _prefixes_from_paths(file_prefixes) + if derived: + return derived signals = anchor_signal_types(route) if V2AnchorType.API_ENDPOINT in signals: return ["docs/api/", "docs/"] @@ -104,6 +126,12 @@ class V2RetrievalPolicyResolver: return ["docs/domains/", "docs/"] return ["docs/"] + def _find_files_prefer_prefixes(self, route: V2RouteResult, prefixes: list[str]) -> list[str]: + preferred = list(prefixes) + if route.anchors.process_domain or route.anchors.process_subdomain: + preferred.extend(["docs/domains/", "docs/logic/"]) + return _unique_terms(preferred or ["docs/"]) + def _summary_prefixes(self, route: V2RouteResult) -> list[str]: signals = anchor_signal_types(route) prefixes: list[str] = [] @@ -114,5 +142,129 @@ class V2RetrievalPolicyResolver: if V2AnchorType.LOGIC_FLOW in signals: prefixes.extend(["docs/logic/", "docs/architecture/", "docs/"]) if V2AnchorType.DOMAIN_ENTITY in signals: - prefixes.extend(["docs/domains/", "docs/api/", "docs/architecture/"]) - return list(dict.fromkeys(prefixes or ["docs/"])) + prefixes.extend(["docs/domains/", "docs/", "docs/api/"]) + return _unique_terms(prefixes or ["docs/"]) + + +class V2RetrievalPolicyResolver: + _GENERAL_LAYERS = [RagLayer.DOCS_DOCUMENT_CATALOG, RagLayer.DOCS_DOC_CHUNKS] + _FIND_FILES_LAYERS = [RagLayer.DOCS_DOCUMENT_CATALOG, RagLayer.DOCS_ENTITY_CATALOG] + _SUMMARY_LAYERS = { + "docs_api_method_explain": [ + RagLayer.DOCS_DOCUMENT_CATALOG, + RagLayer.DOCS_FACT_INDEX, + RagLayer.DOCS_DOC_CHUNKS, + ], + "docs_summary_api_endpoint": [ + RagLayer.DOCS_DOCUMENT_CATALOG, + RagLayer.DOCS_FACT_INDEX, + RagLayer.DOCS_DOC_CHUNKS, + ], + "docs_summary_logic_flow": [ + RagLayer.DOCS_WORKFLOW_INDEX, + RagLayer.DOCS_DOCUMENT_CATALOG, + RagLayer.DOCS_DOC_CHUNKS, + ], + "docs_summary_domain_entity": [ + RagLayer.DOCS_ENTITY_CATALOG, + RagLayer.DOCS_DOCUMENT_CATALOG, + RagLayer.DOCS_DOC_CHUNKS, + ], + "docs_summary_architecture": [ + RagLayer.DOCS_DOCUMENT_CATALOG, + RagLayer.DOCS_RELATION_GRAPH, + RagLayer.DOCS_DOC_CHUNKS, + ], + "docs_summary_generic": [ + RagLayer.DOCS_DOCUMENT_CATALOG, + RagLayer.DOCS_DOC_CHUNKS, + ], + } + + def __init__(self) -> None: + self._filters = _RouteFilterBuilder() + + def resolve(self, route: V2RouteResult) -> RetrievalPlan: + if route.intent == V2Intent.GENERAL_QA: + return RetrievalPlan( + profile="general_qa_grounded_summary", + layers=list(self._GENERAL_LAYERS), + limit=8, + filters=self._filters.general_filters(route), + ) + if route.subintent == V2Subintent.FIND_FILES: + return RetrievalPlan( + profile="file_lookup", + layers=list(self._FIND_FILES_LAYERS), + limit=12, + filters=self._filters.find_files_filters(route), + ) + profile = self._summary_profile(route) + return RetrievalPlan( + profile=profile, + layers=list(self._SUMMARY_LAYERS[profile]), + limit=10 if profile == "docs_api_method_explain" else 8, + filters=self._filters.summary_filters(route), + ) + + def _summary_profile(self, route: V2RouteResult) -> str: + if _is_api_method_explain(route): + return "docs_api_method_explain" + meaningful = anchor_signal_types(route) - {V2AnchorType.FIND_FILES} + if len(meaningful) != 1: + return "docs_summary_generic" + mapping = { + V2AnchorType.API_ENDPOINT: "docs_summary_api_endpoint", + V2AnchorType.ARCHITECTURE: "docs_summary_architecture", + V2AnchorType.LOGIC_FLOW: "docs_summary_logic_flow", + V2AnchorType.DOMAIN_ENTITY: "docs_summary_domain_entity", + } + return mapping.get(next(iter(meaningful)), "docs_summary_generic") + + +def _prefixes_from_paths(paths: list[str]) -> list[str]: + prefixes = [] + for path in paths: + value = str(path).strip().strip("/") + if "/" not in value: + continue + prefix = value.rsplit("/", 1)[0] + "/" + if prefix: + prefixes.append(prefix) + return _unique_terms(prefixes) + + +def _unique_terms(items: list[str]) -> list[str]: + seen: set[str] = set() + unique: list[str] = [] + for raw in items: + value = str(raw or "").strip() + if not value or value in seen: + continue + seen.add(value) + unique.append(value) + return unique + + +def _is_api_method_explain(route: V2RouteResult) -> bool: + if route.subintent != V2Subintent.SUMMARY: + return False + if route.anchors.endpoint_paths: + return True + if _has_api_like_hints(route.anchors.target_doc_hints): + return True + return V2AnchorType.API_ENDPOINT in anchor_signal_types(route) + + +def _has_api_like_hints(hints: list[str]) -> bool: + for hint in hints: + value = str(hint or "").strip().lower() + if not value: + continue + if value.startswith("/"): + return True + if value.startswith(("docs/api/", "docs/endpoints/", "docs/methods/")): + return True + if "endpoint" in value or "method" in value: + return True + return False diff --git a/src/app/core/agent/processes/v2/retrieval/v2_rag_adapter.py b/src/app/core/agent/processes/v2/retrieval/v2_rag_adapter.py index b5ca6d0..7246c83 100644 --- a/src/app/core/agent/processes/v2/retrieval/v2_rag_adapter.py +++ b/src/app/core/agent/processes/v2/retrieval/v2_rag_adapter.py @@ -1,18 +1,23 @@ -"""Адаптер v2 к :class:`RagSessionRetriever` для подстановки в тестах.""" +"""Адаптер v2 к :class:`RagSessionRetriever` с plan-driven execution strategy.""" from __future__ import annotations +from app.core.agent.processes.v2.retrieval.target_doc_seeding import ( + merge_row_lists, + normalize_doc_path, + path_variants_for_rag_query, +) from app.core.rag.retrieval.session_retriever import RagSessionRetriever, RetrievalPlan -class V2RagRetrievalAdapter: - """Обёртка над :class:`RagSessionRetriever` для подмены в тестах.""" - +class _PlanDrivenRetrieval: def __init__(self, retriever: RagSessionRetriever) -> None: self._retriever = retriever async def fetch_rows(self, rag_session_id: str, query_text: str, plan: RetrievalPlan) -> list[dict]: - return await self._retriever.retrieve(rag_session_id, query_text, plan) + seeded_rows = await self._seed_from_target_hints(rag_session_id, plan) + semantic_rows = await self._retriever.retrieve(rag_session_id, query_text, plan) + return merge_row_lists(seeded_rows, semantic_rows) async def fetch_exact_paths(self, rag_session_id: str, *, paths: list[str], layers: list[str] | None = None) -> list[dict]: return await self._retriever.retrieve_exact_files(rag_session_id, paths=paths, layers=layers) @@ -31,3 +36,73 @@ class V2RagRetrievalAdapter: layers=layers, limit=limit, ) + + async def _seed_from_target_hints(self, rag_session_id: str, plan: RetrievalPlan) -> list[dict]: + hints = self._target_doc_hints(plan) + if not hints: + return [] + exact_rows = await self._fetch_exact_rows(rag_session_id, hints) + missing = self._missing_hints(hints, exact_rows) + if not missing: + return exact_rows + fallback_rows = await self._fetch_substring_rows(rag_session_id, missing) + return merge_row_lists(exact_rows, fallback_rows) + + async def _fetch_exact_rows(self, rag_session_id: str, hints: list[str]) -> list[dict]: + variant_paths: list[str] = [] + for hint in hints: + variant_paths.extend(path_variants_for_rag_query(hint)) + unique_paths = list(dict.fromkeys(path for path in variant_paths if path)) + if not unique_paths: + return [] + return await self._retriever.retrieve_exact_files(rag_session_id, paths=unique_paths, layers=None) + + async def _fetch_substring_rows(self, rag_session_id: str, hints: list[str]) -> list[dict]: + needles = [normalize_doc_path(hint).split("/")[-1] for hint in hints] + unique_needles = list(dict.fromkeys(needle for needle in needles if needle)) + if not unique_needles: + return [] + return await self._retriever.retrieve_chunks_by_path_substrings( + rag_session_id, + path_needles=unique_needles, + layers=None, + limit=200, + ) + + def _target_doc_hints(self, plan: RetrievalPlan) -> list[str]: + raw = plan.filters.get("target_doc_hints") + if not isinstance(raw, list): + return [] + return [str(item).strip() for item in raw if str(item or "").strip()] + + def _missing_hints(self, hints: list[str], rows: list[dict]) -> list[str]: + pool = {normalize_doc_path(str(row.get("path") or "")) for row in rows} + return [hint for hint in hints if normalize_doc_path(hint) not in pool] + + +class V2RagRetrievalAdapter: + """Обёртка над :class:`RagSessionRetriever` для plan-driven retrieval и подмены в тестах.""" + + def __init__(self, retriever: RagSessionRetriever) -> None: + self._retriever = _PlanDrivenRetrieval(retriever) + + async def fetch_rows(self, rag_session_id: str, query_text: str, plan: RetrievalPlan) -> list[dict]: + return await self._retriever.fetch_rows(rag_session_id, query_text, plan) + + async def fetch_exact_paths(self, rag_session_id: str, *, paths: list[str], layers: list[str] | None = None) -> list[dict]: + return await self._retriever.fetch_exact_paths(rag_session_id, paths=paths, layers=layers) + + async def fetch_chunks_by_path_substrings( + self, + rag_session_id: str, + *, + path_needles: list[str], + layers: list[str] | None = None, + limit: int = 200, + ) -> list[dict]: + return await self._retriever.fetch_chunks_by_path_substrings( + rag_session_id, + path_needles=path_needles, + layers=layers, + limit=limit, + ) diff --git a/src/app/core/rag/indexing/docs/integration_extractor.py b/src/app/core/rag/indexing/docs/integration_extractor.py index 296fe51..bea9cd1 100644 --- a/src/app/core/rag/indexing/docs/integration_extractor.py +++ b/src/app/core/rag/indexing/docs/integration_extractor.py @@ -1,20 +1,24 @@ from __future__ import annotations +import logging + import yaml from app.core.rag.indexing.docs.chunkers.markdown_chunker import SectionChunk from app.core.rag.indexing.docs.models import IntegrationRecord +LOGGER = logging.getLogger(__name__) + class DocsIntegrationExtractor: _SECTION_TITLES = {"integrations", "интеграции"} - def extract(self, sections: list[SectionChunk]) -> list[IntegrationRecord]: + def extract(self, sections: list[SectionChunk], *, path: str = "") -> list[IntegrationRecord]: records: list[IntegrationRecord] = [] for section in sections: if not self._is_integration_section(section.section_path): continue - payload = self._payload(section.content) + payload = self._payload(section.content, path=path, section_path=section.section_path) target = str(payload.get("target") or "").strip() if not target: continue @@ -40,7 +44,7 @@ class DocsIntegrationExtractor: parts = [item.strip().lower() for item in section_path.split(" > ") if item.strip()] return any(part in self._SECTION_TITLES for part in parts[:-1]) or (parts and parts[-1] in self._SECTION_TITLES) - def _payload(self, text: str) -> dict: + def _payload(self, text: str, *, path: str, section_path: str) -> dict: payload: dict = {} details_lines: list[str] = [] collecting_details = False @@ -61,15 +65,27 @@ class DocsIntegrationExtractor: collecting_details = True details_lines = [] if value: - payload[key] = self._yaml_value(value) + payload[key] = self._yaml_value( + value, + path=path, + section_path=section_path, + field_name=key, + fallback="", + ) continue collecting_details = False - payload[key] = self._yaml_value(value) + payload[key] = self._yaml_value( + value, + path=path, + section_path=section_path, + field_name=key, + fallback=value, + ) if details_lines: - payload["details"] = self._details_payload(details_lines) + payload["details"] = self._details_payload(details_lines, path=path, section_path=section_path) return payload - def _details_payload(self, lines: list[str]) -> dict: + def _details_payload(self, lines: list[str], *, path: str, section_path: str) -> dict: normalized: list[str] = [] for raw_line in lines: line = raw_line[2:] if raw_line.startswith(" ") else raw_line @@ -78,7 +94,13 @@ class DocsIntegrationExtractor: if indent == 0 and stripped.startswith("- "): stripped = stripped[2:] normalized.append((" " * indent) + stripped) - payload = yaml.safe_load("\n".join(normalized)) or {} + payload = self._yaml_value( + "\n".join(normalized), + path=path, + section_path=section_path, + field_name="details", + fallback={}, + ) or {} return payload if isinstance(payload, dict) else {} def _split_key_value(self, text: str) -> tuple[str, str]: @@ -87,7 +109,17 @@ class DocsIntegrationExtractor: key, value = text.split(":", 1) return key.strip(), value.strip() - def _yaml_value(self, value: str): + def _yaml_value(self, value: str, *, path: str, section_path: str, field_name: str, fallback): if not value: return "" - return yaml.safe_load(value) + try: + return yaml.safe_load(value) + except yaml.YAMLError as exc: + LOGGER.warning( + "docs integration parse warning: path=%s section=%s field=%s reason=%s", + path or "", + section_path, + field_name, + exc.__class__.__name__, + ) + return fallback diff --git a/src/app/core/rag/indexing/docs/pipeline.py b/src/app/core/rag/indexing/docs/pipeline.py index 291326e..0236b57 100644 --- a/src/app/core/rag/indexing/docs/pipeline.py +++ b/src/app/core/rag/indexing/docs/pipeline.py @@ -1,5 +1,8 @@ from __future__ import annotations +import logging +from collections.abc import Callable + from app.core.rag.contracts import RagDocument, RagSource from app.core.rag.indexing.docs.chunkers.markdown_chunker import MarkdownDocChunker from app.core.rag.indexing.docs.classifier import DocsClassifier @@ -15,6 +18,8 @@ from app.core.rag.indexing.docs.relation_extractor import DocsRelationExtractor from app.core.rag.indexing.docs.support_layer_builder import DocsSupportLayerBuilder from app.core.rag.indexing.docs.workflow_extractor import DocsWorkflowExtractor +LOGGER = logging.getLogger(__name__) + class DocsIndexingPipeline: def __init__(self) -> None: @@ -59,7 +64,11 @@ class DocsIndexingPipeline: for section in sections: docs.append(self._builder.build_doc_chunk(source, section, parsed.frontmatter, doc_kind)) document_id = frontmatter_view.document_id or source.path - for fact in self._facts.extract(parsed.frontmatter, sections): + for fact in self._safe_extract( + extractor_name="fact_extractor", + path=path, + run=lambda: self._facts.extract(parsed.frontmatter, sections), + ): docs.append( self._support_builder.build_fact( source, @@ -72,13 +81,29 @@ class DocsIndexingPipeline: subdomain=frontmatter_view.subdomain, ) ) - for entity in self._entities.extract(parsed.frontmatter): + for entity in self._safe_extract( + extractor_name="entity_extractor", + path=path, + run=lambda: self._entities.extract(parsed.frontmatter), + ): docs.append(self._builder.build_entity_record(source, parsed.frontmatter, entity)) - for workflow in self._workflows.extract(parsed.detail_sections): + for workflow in self._safe_extract( + extractor_name="workflow_extractor", + path=path, + run=lambda: self._workflows.extract(parsed.detail_sections), + ): docs.append(self._support_builder.build_workflow_record(source, parsed.frontmatter, workflow)) - for edge in self._relations.extract(parsed.frontmatter, source_id=document_id): + for edge in self._safe_extract( + extractor_name="relation_extractor", + path=path, + run=lambda: self._relations.extract(parsed.frontmatter, source_id=document_id), + ): docs.append(self._support_builder.build_relation_record(source, parsed.frontmatter, edge)) - for integration in self._integrations.extract(sections): + for integration in self._safe_extract( + extractor_name="integration_extractor", + path=path, + run=lambda: self._integrations.extract(sections, path=path), + ): docs.append(self._support_builder.build_integration_record(source, parsed.frontmatter, integration)) return docs @@ -86,3 +111,15 @@ class DocsIndexingPipeline: tail = path.rsplit("/", 1)[-1] stem = tail.rsplit(".", 1)[0] return stem.replace("-", " ").replace("_", " ").strip().title() + + def _safe_extract(self, *, extractor_name: str, path: str, run: Callable[[], list]) -> list: + try: + return run() + except Exception as exc: + LOGGER.warning( + "docs pipeline extractor warning: path=%s extractor=%s reason=%s", + path, + extractor_name, + exc.__class__.__name__, + ) + return [] diff --git a/src/app/core/rag/persistence/query_repository.py b/src/app/core/rag/persistence/query_repository.py index 593f7ed..a4732ad 100644 --- a/src/app/core/rag/persistence/query_repository.py +++ b/src/app/core/rag/persistence/query_repository.py @@ -25,6 +25,8 @@ class RagQueryRepository: exclude_like_patterns: list[str] | None = None, prefer_path_prefixes: list[str] | None = None, prefer_like_patterns: list[str] | None = None, + metadata_domain: str | None = None, + metadata_subdomain: str | None = None, prefer_non_tests: bool = False, ) -> list[dict]: sql, params = self._builder.build_retrieve( @@ -38,6 +40,8 @@ class RagQueryRepository: exclude_like_patterns=exclude_like_patterns, prefer_path_prefixes=prefer_path_prefixes, prefer_like_patterns=prefer_like_patterns, + metadata_domain=metadata_domain, + metadata_subdomain=metadata_subdomain, prefer_non_tests=prefer_non_tests, ) with get_engine().connect() as conn: @@ -234,6 +238,54 @@ class RagQueryRepository: rows = conn.execute(stmt, params).mappings().fetchall() return [self._row_to_dict(row) for row in rows] + def retrieve_chunks_by_path_substrings( + self, + rag_session_id: str, + *, + path_needles: list[str], + layers: list[str] | None = None, + limit: int = 200, + ) -> list[dict]: + normalized_needles = [str(item).strip().lower() for item in path_needles if str(item).strip()] + if not normalized_needles: + return [] + params: dict = { + "sid": rag_session_id, + "lim": max(1, int(limit)), + } + filters = ["rag_session_id = :sid"] + like_parts: list[str] = [] + for idx, needle in enumerate(normalized_needles): + key = f"needle_{idx}" + params[key] = f"%{needle}%" + like_parts.append(f"lower(path) LIKE :{key}") + filters.append("(" + " OR ".join(like_parts) + ")") + if layers: + normalized_layers = [str(item).strip() for item in layers if str(item).strip()] + if normalized_layers: + params["layers"] = normalized_layers + filters.append("layer IN :layers") + stmt = text( + f""" + SELECT path, content, layer, title, metadata_json, span_start, span_end, + 0 AS lexical_rank, + 0 AS prefer_bonus, + 0 AS test_penalty, + 0 AS structural_rank, + 0 AS layer_rank, + 0 AS distance + FROM rag_chunks + WHERE {' AND '.join(filters)} + ORDER BY path ASC, COALESCE(span_start, 0) ASC, COALESCE(chunk_index, 0) ASC + LIMIT :lim + """ + ) + if "layers" in params: + stmt = stmt.bindparams(bindparam("layers", expanding=True)) + with get_engine().connect() as conn: + rows = conn.execute(stmt, params).mappings().fetchall() + return [self._row_to_dict(row) for row in rows] + def _row_to_dict(self, row) -> dict: data = dict(row) raw_metadata = data.pop("metadata_json") diff --git a/src/app/core/rag/persistence/repository.py b/src/app/core/rag/persistence/repository.py index cb99adb..ca5ba07 100644 --- a/src/app/core/rag/persistence/repository.py +++ b/src/app/core/rag/persistence/repository.py @@ -69,6 +69,8 @@ class RagRepository: exclude_like_patterns: list[str] | None = None, prefer_path_prefixes: list[str] | None = None, prefer_like_patterns: list[str] | None = None, + metadata_domain: str | None = None, + metadata_subdomain: str | None = None, prefer_non_tests: bool = False, ) -> list[dict]: return self._query.retrieve( @@ -82,6 +84,8 @@ class RagRepository: exclude_like_patterns=exclude_like_patterns, prefer_path_prefixes=prefer_path_prefixes, prefer_like_patterns=prefer_like_patterns, + metadata_domain=metadata_domain, + metadata_subdomain=metadata_subdomain, prefer_non_tests=prefer_non_tests, ) @@ -141,3 +145,18 @@ class RagRepository: layers=layers, limit=limit, ) + + def retrieve_chunks_by_path_substrings( + self, + rag_session_id: str, + *, + path_needles: list[str], + layers: list[str] | None = None, + limit: int = 200, + ) -> list[dict]: + return self._query.retrieve_chunks_by_path_substrings( + rag_session_id, + path_needles=path_needles, + layers=layers, + limit=limit, + ) diff --git a/src/app/core/rag/persistence/retrieval_statement_builder.py b/src/app/core/rag/persistence/retrieval_statement_builder.py index 3fb34c3..bdedb92 100644 --- a/src/app/core/rag/persistence/retrieval_statement_builder.py +++ b/src/app/core/rag/persistence/retrieval_statement_builder.py @@ -19,6 +19,8 @@ class RetrievalStatementBuilder: exclude_like_patterns: list[str] | None = None, prefer_path_prefixes: list[str] | None = None, prefer_like_patterns: list[str] | None = None, + metadata_domain: str | None = None, + metadata_subdomain: str | None = None, prefer_non_tests: bool = False, ) -> tuple[str, dict]: emb = "[" + ",".join(str(x) for x in query_embedding) + "]" @@ -29,6 +31,8 @@ class RetrievalStatementBuilder: self._append_prefix_group(filters, params, "path", path_prefixes) self._append_prefix_group(filters, params, "exclude_prefix", exclude_path_prefixes, negate=True) self._append_like_group(filters, params, "exclude_like", exclude_like_patterns, negate=True) + self._append_metadata_equals(filters, params, "metadata_domain", "domain", metadata_domain) + self._append_metadata_equals(filters, params, "metadata_subdomain", "subdomain", metadata_subdomain) if layers: filters.append("layer = ANY(:layers)") params["layers"] = layers @@ -202,6 +206,20 @@ class RetrievalStatementBuilder: joined = " OR ".join(parts) filters.append(f"NOT ({joined})" if negate else f"({joined})") + def _append_metadata_equals( + self, + filters: list[str], + params: dict, + param_key: str, + metadata_key: str, + value: str | None, + ) -> None: + normalized = str(value or "").strip().lower() + if not normalized: + return + params[param_key] = normalized + filters.append(f"lower(COALESCE({self._metadata_text(metadata_key)}, '')) = :{param_key}") + def _test_penalty_sql( self, enabled: bool, diff --git a/src/app/core/rag/retrieval/session_retriever.py b/src/app/core/rag/retrieval/session_retriever.py index ea9004a..d19e661 100644 --- a/src/app/core/rag/retrieval/session_retriever.py +++ b/src/app/core/rag/retrieval/session_retriever.py @@ -94,4 +94,8 @@ class RagSessionRetriever: for key in keys: if key in filters: out[key] = filters[key] + if "metadata.domain" in filters: + out["metadata_domain"] = filters["metadata.domain"] + if "metadata.subdomain" in filters: + out["metadata_subdomain"] = filters["metadata.subdomain"] return out diff --git a/tests/pipeline_setup_v4/README.md b/tests/pipeline_setup_v4/README.md index 057c1c4..a11178e 100644 --- a/tests/pipeline_setup_v4/README.md +++ b/tests/pipeline_setup_v4/README.md @@ -6,7 +6,10 @@ Differences from `v3`: - each YAML case targets a single isolated component; - results are written next to the suite in `cases/.../test_runs/...`; -- the first supported component is `process_v2_intent_router`. +- supported components are `process_v2_intent_router` and `process_v2_retrieval_policy_resolver`. + Also available: `process_v2_router_plus_retrieval_policy` for the linked route -> plan chain, + `process_v2_router_plus_retrieval_policy_rag` for the linked route -> plan -> rag chain, + and `process_v2_full_chain` for the full route -> plan -> rag -> evidence -> workflow LLM chain. ## Run @@ -23,3 +26,48 @@ PYTHONPATH=. python -m tests.pipeline_setup_v4.run \ --cases-dir tests/pipeline_setup_v4/cases/suite_02/process_v2_intent_router/router_llm_first_v3.yaml \ --run-name llm_first_v3 ``` + +Retrieval policy resolver suite: + +```bash +PYTHONPATH=. python -m tests.pipeline_setup_v4.run \ + --cases-dir tests/pipeline_setup_v4/cases/suite_03/process_v2_retrieval_policy_resolver/cases.yaml \ + --run-name retrieval_policy_v1 +``` + +Linked router + retrieval policy suite: + +```bash +PYTHONPATH=. python3 -m tests.pipeline_setup_v4.run \ + --cases-dir tests/pipeline_setup_v4/cases/suite_04/process_v2_router_plus_retrieval_policy \ + --run-name router_plus_policy_v1 +``` + +Inside `suite_04`, cases are split into: + +- `strict_regression_cases.yaml` for contract-level invariants +- `soft_observational_cases.yaml` for LLM-sensitive boundary scenarios + +Quality-gate mini-pack: + +```bash +PYTHONPATH=. python3 -m tests.pipeline_setup_v4.run \ + --cases-dir tests/pipeline_setup_v4/cases/suite_05/process_v2_router_plus_retrieval_policy_quality_gate/cases.yaml \ + --run-name router_plus_policy_qg_v1 +``` + +Linked router + retrieval policy + rag suite: + +```bash +PYTHONPATH=src:. DATABASE_URL='postgresql+psycopg://agent:agent@127.0.0.1:5432/agent' python3 -m tests.pipeline_setup_v4.run \ + --cases-dir tests/pipeline_setup_v4/cases/suite_06/process_v2_router_plus_retrieval_policy_rag/cases.yaml \ + --run-name router_plus_policy_rag_v1 +``` + +Full process v2 chain with workflow LLM: + +```bash +PYTHONPATH=src:. DATABASE_URL='postgresql+psycopg://agent:agent@127.0.0.1:5432/agent' python3 -m tests.pipeline_setup_v4.run \ + --cases-dir tests/pipeline_setup_v4/cases/suite_07/process_v2_full_chain/cases.yaml \ + --run-name process_v2_full_chain_v1 +``` diff --git a/tests/pipeline_setup_v4/cases/suite_03/process_v2_retrieval_policy_resolver/cases.yaml b/tests/pipeline_setup_v4/cases/suite_03/process_v2_retrieval_policy_resolver/cases.yaml new file mode 100644 index 0000000..cb7a37a --- /dev/null +++ b/tests/pipeline_setup_v4/cases/suite_03/process_v2_retrieval_policy_resolver/cases.yaml @@ -0,0 +1,540 @@ +defaults: + component: process_v2_retrieval_policy_resolver + +cases: + - id: general-overview-grounded + route: + routing_domain: GENERAL + intent: GENERAL_QA + subintent: SUMMARY + user_query: "Что это за сервис?" + normalized_query: "что это за сервис" + anchors: + target_doc_hints: [] + endpoint_paths: [] + expected: + plan: + profile: general_qa_grounded_summary + layers: [D1_DOCUMENT_CATALOG, D0_DOC_CHUNKS] + limit: 8 + filters: + prefer_path_prefixes: [docs/architecture/, docs/] + + - id: general-does-not-become-docs-summary + route: + routing_domain: GENERAL + intent: GENERAL_QA + subintent: SUMMARY + user_query: "Дай общий обзор, включая /health" + normalized_query: "дай общий обзор включая /health" + anchors: + endpoint_paths: ["/health"] + target_doc_hints: ["docs/api/health-endpoint.md"] + matched_aliases: ["api"] + expected: + plan: + profile: general_qa_grounded_summary + layers: [D1_DOCUMENT_CATALOG, D0_DOC_CHUNKS] + limit: 8 + + - id: find-files-with-target-hint + route: + routing_domain: DOCS + intent: DOC_EXPLAIN + subintent: FIND_FILES + user_query: "Покажи файл про health endpoint" + normalized_query: "покажи файл про health endpoint" + anchors: + endpoint_paths: ["/health"] + target_doc_hints: ["docs/api/health-endpoint.md"] + expected: + plan: + profile: file_lookup + layers: [D1_DOCUMENT_CATALOG, D3_ENTITY_CATALOG] + limit: 12 + filters: + target_doc_hints: ["docs/api/health-endpoint.md"] + path_prefixes: [docs/api/] + prefer_like_patterns: ["%health-endpoint.md%"] + + - id: find-files-endpoint-only + route: + routing_domain: DOCS + intent: DOC_EXPLAIN + subintent: FIND_FILES + user_query: "Где описан /send?" + normalized_query: "где описан /send" + anchors: + endpoint_paths: ["/send"] + target_doc_hints: [] + expected: + plan: + profile: file_lookup + layers: [D1_DOCUMENT_CATALOG, D3_ENTITY_CATALOG] + limit: 12 + filters: + path_prefixes: [docs/api/, docs/] + prefer_like_patterns: ["%/send%"] + + - id: find-files-entities-and-domain + route: + routing_domain: DOCS + intent: DOC_EXPLAIN + subintent: FIND_FILES + user_query: "В каком документе описан ManualSendWorker?" + normalized_query: "в каком документе описан manualsendworker" + anchors: + entity_names: ["ManualSendWorker"] + matched_aliases: ["manual send"] + process_domain: "messaging" + process_subdomain: "manual_send" + target_doc_hints: [] + expected: + plan: + profile: file_lookup + layers: [D1_DOCUMENT_CATALOG, D3_ENTITY_CATALOG] + limit: 12 + filters: + metadata.domain: messaging + metadata.subdomain: manual_send + prefer_path_prefixes: [docs/domains/, docs/, docs/logic/] + prefer_like_patterns: ["%manualsendworker%", "%manual send%", "%messaging%", "%manual_send%"] + + - id: docs-summary-api-endpoint-health + route: + routing_domain: DOCS + intent: DOC_EXPLAIN + subintent: SUMMARY + user_query: "Объясни /health" + normalized_query: "объясни /health" + target_terms: ["health", "/health"] + anchors: + endpoint_paths: ["/health"] + target_doc_hints: ["docs/api/health-endpoint.md"] + expected: + plan: + 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"] + path_prefixes: [docs/api/, docs/] + prefer_path_prefixes: [docs/api/, docs/] + + - id: docs-summary-architecture + route: + routing_domain: DOCS + intent: DOC_EXPLAIN + subintent: SUMMARY + user_query: "Как устроена архитектура сервиса?" + normalized_query: "как устроена архитектура сервиса" + anchors: + file_names: ["docs/architecture/runtime-manager.md"] + target_doc_hints: ["docs/architecture/runtime-manager.md"] + matched_aliases: ["architecture"] + expected: + plan: + profile: docs_summary_architecture + layers: [D1_DOCUMENT_CATALOG, D5_RELATION_GRAPH, D0_DOC_CHUNKS] + limit: 8 + filters: + target_doc_hints: ["docs/architecture/runtime-manager.md"] + prefer_path_prefixes: [docs/architecture/, docs/] + + - id: docs-summary-logic-flow + route: + routing_domain: DOCS + intent: DOC_EXPLAIN + subintent: SUMMARY + user_query: "Опиши workflow отправки уведомлений" + normalized_query: "опиши workflow отправки уведомлений" + anchors: + matched_aliases: ["workflow"] + process_domain: "notifications" + process_subdomain: "delivery_loop" + target_doc_hints: [] + expected: + plan: + profile: docs_summary_logic_flow + layers: [D4_WORKFLOW_INDEX, D1_DOCUMENT_CATALOG, D0_DOC_CHUNKS] + limit: 8 + filters: + metadata.domain: notifications + metadata.subdomain: delivery_loop + prefer_path_prefixes: [docs/logic/, docs/architecture/, docs/] + + - id: docs-summary-domain-entity + route: + routing_domain: DOCS + intent: DOC_EXPLAIN + subintent: SUMMARY + user_query: "Что такое RuntimeManager?" + normalized_query: "что такое runtimemanager" + anchors: + entity_names: ["RuntimeManager"] + process_domain: "runtime" + expected: + plan: + profile: docs_summary_domain_entity + layers: [D3_ENTITY_CATALOG, D1_DOCUMENT_CATALOG, D0_DOC_CHUNKS] + limit: 8 + filters: + metadata.domain: runtime + prefer_path_prefixes: [docs/domains/, docs/, docs/api/] + prefer_like_patterns: ["%runtimemanager%", "%runtime%"] + + - id: docs-summary-generic-weak-signals + route: + routing_domain: DOCS + intent: DOC_EXPLAIN + subintent: SUMMARY + user_query: "Дай краткое summary документации" + normalized_query: "дай краткое summary документации" + anchors: + target_doc_hints: [] + endpoint_paths: [] + entity_names: [] + matched_aliases: [] + expected: + plan: + profile: docs_summary_generic + layers: [D1_DOCUMENT_CATALOG, D0_DOC_CHUNKS] + limit: 8 + filters: + prefer_path_prefixes: [docs/] + + - id: docs-summary-generic-conflicting-signals + route: + routing_domain: DOCS + intent: DOC_EXPLAIN + subintent: SUMMARY + user_query: "Как связан /health и RuntimeManager?" + normalized_query: "как связан /health и runtimemanager" + anchors: + endpoint_paths: ["/health"] + entity_names: ["RuntimeManager"] + expected: + plan: + profile: docs_summary_generic + layers: [D1_DOCUMENT_CATALOG, D0_DOC_CHUNKS] + limit: 8 + + - id: find-files-stays-file-lookup-on-mixed-signals + route: + routing_domain: DOCS + intent: DOC_EXPLAIN + subintent: FIND_FILES + user_query: "Найди документ по architecture runtime manager" + normalized_query: "найди документ по architecture runtime manager" + anchors: + entity_names: ["RuntimeManager"] + matched_aliases: ["architecture"] + file_names: ["docs/architecture/runtime-manager.md"] + expected: + plan: + profile: file_lookup + layers: [D1_DOCUMENT_CATALOG, D3_ENTITY_CATALOG] + limit: 12 + filters: + path_prefixes: [docs/architecture/] + + - id: resolver-survives-partial-empty-anchors + route: + routing_domain: DOCS + intent: DOC_EXPLAIN + subintent: SUMMARY + user_query: "Что там по docs?" + normalized_query: "что там по docs" + anchors: + entity_names: [] + file_names: [""] + endpoint_paths: [] + target_doc_hints: [] + matched_aliases: [] + process_domain: + process_subdomain: + expected: + plan: + profile: docs_summary_generic + layers: [D1_DOCUMENT_CATALOG, D0_DOC_CHUNKS] + limit: 8 + + - id: find-files-file-name-priority + route: + routing_domain: DOCS + intent: DOC_EXPLAIN + subintent: FIND_FILES + user_query: "Покажи документ manual-send" + normalized_query: "покажи документ manual-send" + anchors: + file_names: ["docs/workflows/manual-send.md"] + matched_aliases: ["manual send"] + target_doc_hints: [] + expected: + plan: + profile: file_lookup + layers: [D1_DOCUMENT_CATALOG, D3_ENTITY_CATALOG] + limit: 12 + filters: + path_prefixes: [docs/workflows/] + prefer_like_patterns: ["%docs/workflows/manual-send.md%", "%manual send%"] + + - id: conflict-api-hint-vs-workflow-metadata + route: + routing_domain: DOCS + intent: DOC_EXPLAIN + subintent: SUMMARY + user_query: "Опиши flow для /health в notification loop" + normalized_query: "опиши flow для /health в notification loop" + anchors: + endpoint_paths: ["/health"] + target_doc_hints: ["docs/api/health-endpoint.md"] + matched_aliases: ["workflow"] + process_domain: "notifications" + process_subdomain: "delivery_loop" + expected: + plan: + profile: docs_summary_generic + layers: [D1_DOCUMENT_CATALOG, D0_DOC_CHUNKS] + limit: 8 + filters: + target_doc_hints: ["docs/api/health-endpoint.md"] + metadata.domain: notifications + metadata.subdomain: delivery_loop + path_prefixes: [docs/api/, docs/] + + - id: conflict-file-name-vs-architecture-alias + route: + routing_domain: DOCS + intent: DOC_EXPLAIN + subintent: SUMMARY + user_query: "Объясни architecture для notification loop" + normalized_query: "объясни architecture для notification loop" + anchors: + file_names: ["docs/logic/notification-loop.md"] + matched_aliases: ["architecture"] + expected: + plan: + profile: docs_summary_generic + layers: [D1_DOCUMENT_CATALOG, D0_DOC_CHUNKS] + limit: 8 + filters: + prefer_path_prefixes: [docs/architecture/, docs/, docs/logic/] + prefer_like_patterns: ["%docs/logic/notification-loop.md%", "%architecture%"] + + - id: conflict-hint-vs-entity-soft-signals + route: + routing_domain: DOCS + intent: DOC_EXPLAIN + subintent: SUMMARY + user_query: "Что делает /send и ManualSendWorker?" + normalized_query: "что делает /send и manualsendworker" + anchors: + endpoint_paths: ["/send"] + target_doc_hints: ["docs/api/send-endpoint.md"] + entity_names: ["ManualSendWorker"] + matched_aliases: ["manual send"] + expected: + plan: + profile: docs_summary_generic + layers: [D1_DOCUMENT_CATALOG, D0_DOC_CHUNKS] + limit: 8 + filters: + target_doc_hints: ["docs/api/send-endpoint.md"] + path_prefixes: [docs/api/, docs/] + prefer_like_patterns: ["%send-endpoint.md%", "%/send%", "%manualsendworker%", "%manual send%"] + + - id: metadata-only-find-files + route: + routing_domain: DOCS + intent: DOC_EXPLAIN + subintent: FIND_FILES + user_query: "Найди документы по notifications delivery loop" + normalized_query: "найди документы по notifications delivery loop" + anchors: + process_domain: "notifications" + process_subdomain: "delivery_loop" + expected: + plan: + profile: file_lookup + layers: [D1_DOCUMENT_CATALOG, D3_ENTITY_CATALOG] + limit: 12 + filters: + path_prefixes: [docs/] + metadata.domain: notifications + metadata.subdomain: delivery_loop + prefer_path_prefixes: [docs/, docs/domains/, docs/logic/] + + - id: metadata-only-generic-summary + route: + routing_domain: DOCS + intent: DOC_EXPLAIN + subintent: SUMMARY + user_query: "Дай summary по notifications delivery loop" + normalized_query: "дай summary по notifications delivery loop" + anchors: + process_domain: "notifications" + process_subdomain: "delivery_loop" + expected: + plan: + profile: docs_summary_generic + layers: [D1_DOCUMENT_CATALOG, D0_DOC_CHUNKS] + limit: 8 + filters: + metadata.domain: notifications + metadata.subdomain: delivery_loop + prefer_path_prefixes: [docs/] + prefer_like_patterns: ["%notifications%", "%delivery_loop%"] + + - id: metadata-domain-entity-with-alias + route: + routing_domain: DOCS + intent: DOC_EXPLAIN + subintent: SUMMARY + user_query: "Объясни компонент billing" + normalized_query: "объясни компонент billing" + anchors: + matched_aliases: ["component"] + process_domain: "billing" + expected: + plan: + profile: docs_summary_domain_entity + layers: [D3_ENTITY_CATALOG, D1_DOCUMENT_CATALOG, D0_DOC_CHUNKS] + limit: 8 + filters: + metadata.domain: billing + prefer_path_prefixes: [docs/domains/, docs/, docs/api/] + prefer_like_patterns: ["%component%", "%billing%"] + + - id: alias-only-api + route: + routing_domain: DOCS + intent: DOC_EXPLAIN + subintent: SUMMARY + user_query: "Объясни api health" + normalized_query: "объясни api health" + anchors: + matched_aliases: ["api endpoint"] + expected: + plan: + profile: docs_summary_api_endpoint + layers: [D1_DOCUMENT_CATALOG, D2_FACT_INDEX, D0_DOC_CHUNKS] + limit: 8 + filters: + path_prefixes: [docs/api/, docs/] + prefer_like_patterns: ["%api endpoint%"] + + - id: alias-only-architecture + route: + routing_domain: DOCS + intent: DOC_EXPLAIN + subintent: SUMMARY + user_query: "Расскажи про architecture" + normalized_query: "расскажи про architecture" + anchors: + matched_aliases: ["architecture"] + expected: + plan: + profile: docs_summary_architecture + layers: [D1_DOCUMENT_CATALOG, D5_RELATION_GRAPH, D0_DOC_CHUNKS] + limit: 8 + filters: + prefer_path_prefixes: [docs/architecture/, docs/] + prefer_like_patterns: ["%architecture%"] + + - id: partial-only-endpoint-path + route: + routing_domain: DOCS + intent: DOC_EXPLAIN + subintent: SUMMARY + user_query: "Что делает /status?" + normalized_query: "что делает /status" + anchors: + endpoint_paths: ["/status"] + expected: + plan: + profile: docs_summary_api_endpoint + layers: [D1_DOCUMENT_CATALOG, D2_FACT_INDEX, D0_DOC_CHUNKS] + limit: 8 + filters: + path_prefixes: [docs/api/, docs/] + prefer_like_patterns: ["%/status%"] + + - id: partial-only-target-doc-hint + route: + routing_domain: DOCS + intent: DOC_EXPLAIN + subintent: SUMMARY + user_query: "Объясни notification loop" + normalized_query: "объясни notification loop" + anchors: + target_doc_hints: ["docs/logic/notification-loop.md"] + expected: + plan: + profile: docs_summary_logic_flow + layers: [D4_WORKFLOW_INDEX, D1_DOCUMENT_CATALOG, D0_DOC_CHUNKS] + limit: 8 + filters: + target_doc_hints: ["docs/logic/notification-loop.md"] + prefer_path_prefixes: [docs/logic/, docs/architecture/, docs/] + + - id: generic-neutral-with-nonsemantic-hint + route: + routing_domain: DOCS + intent: DOC_EXPLAIN + subintent: SUMMARY + user_query: "Дай общий summary intro docs" + normalized_query: "дай общий summary intro docs" + anchors: + target_doc_hints: ["docs/intro/overview.md"] + expected: + plan: + profile: docs_summary_generic + layers: [D1_DOCUMENT_CATALOG, D0_DOC_CHUNKS] + limit: 8 + filters: + target_doc_hints: ["docs/intro/overview.md"] + prefer_path_prefixes: [docs/] + + - id: generic-neutral-weak-mixed-aliases + route: + routing_domain: DOCS + intent: DOC_EXPLAIN + subintent: SUMMARY + user_query: "Нужен общий summary про architecture component" + normalized_query: "нужен общий summary про architecture component" + anchors: + matched_aliases: ["architecture", "component"] + expected: + plan: + profile: docs_summary_generic + layers: [D1_DOCUMENT_CATALOG, D0_DOC_CHUNKS] + limit: 8 + filters: + prefer_path_prefixes: [docs/architecture/, docs/, docs/domains/, docs/api/] + + - id: find-files-hard-priority-with-multiple-hints + route: + routing_domain: DOCS + intent: DOC_EXPLAIN + subintent: FIND_FILES + user_query: "Найди документы по /health и runtime manager" + normalized_query: "найди документы по /health и runtime manager" + anchors: + endpoint_paths: ["/health"] + entity_names: ["RuntimeManager"] + matched_aliases: ["architecture"] + target_doc_hints: + - "docs/api/health-endpoint.md" + - "docs/architecture/runtime-manager.md" + expected: + plan: + profile: file_lookup + layers: [D1_DOCUMENT_CATALOG, D3_ENTITY_CATALOG] + limit: 12 + filters: + target_doc_hints: + - "docs/api/health-endpoint.md" + - "docs/architecture/runtime-manager.md" + path_prefixes: [docs/api/, docs/architecture/] + prefer_like_patterns: ["%health-endpoint.md%", "%runtime-manager.md%"] diff --git a/tests/pipeline_setup_v4/cases/suite_03/process_v2_retrieval_policy_resolver/test_runs/retrieval_policy_v1/20260407_144425.zip b/tests/pipeline_setup_v4/cases/suite_03/process_v2_retrieval_policy_resolver/test_runs/retrieval_policy_v1/20260407_144425.zip new file mode 100644 index 0000000000000000000000000000000000000000..27ffcb668005167eca6ab4e495226f1aa9b9b8f5 GIT binary patch literal 39589 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 B!#n^0 literal 0 HcmV?d00001 diff --git a/tests/pipeline_setup_v4/cases/suite_03/process_v2_retrieval_policy_resolver/test_runs/retrieval_policy_v2/20260407_145436.zip b/tests/pipeline_setup_v4/cases/suite_03/process_v2_retrieval_policy_resolver/test_runs/retrieval_policy_v2/20260407_145436.zip new file mode 100644 index 0000000000000000000000000000000000000000..ff45fe4645ed9b43af67be8622639394e1fbf418 GIT binary patch literal 75279 zcmd43WmKLC(yfiV1$TFMcXzkoZo%E%-Q7a4;2zu^f(3Vn;O>F%N%wi@%*=PXCo}W@ z=&VH&esC{t)~btLyLOeL3@8{X(9b_Ju3&FZ z;NlJo1Ppcy3ltIW8Jys3eSQ8+`~5ppYa5j>Eh3?!5V3Mr>L z;Eg{4KK}T-=lg&BEQ}4EOr7-2O>Ipb4Xx=+>`a~LZ0(%sj7*K~Y)k>48#~cCx!Bkk zI(pFCnEcTThWwu&T&3EWEeaFr+N6f&=QvVJK2e^5t5PmDJJ3I3i`} zP0l)-EOKa;Z-f>o*|XAt&Ughek=vfuK z)Rix-ppSH#2-NG>Bi6eu?VXT74|NCQ9IUh@$Yemp#>UE5B=&d+N`flvL0dRWelp3- zW#(q4PGRbAAZ?z$`T(9qEYW;J7Hhw%UDB}@Bjo3F;BV`U*t*iD{V1%jbxqcKX;O1- z-4);z1`eXoEaE@(2!+!Ia?!q61P|Av_;oe?$;}ty+Ew#DGi+ScxDd5#Kv46p+$zS~ zd9kbq`W1$k{4SBLxfmFoq9(KdE0?^=M+6jv>MR$=p>ZhO0e0u{B5gL? zRi5PEbK{?=0pQ60S`q#gD8u%Ll(9E-bhZRg#?IE-gU;E|(cIMeCt>I;ENz{CjV|&y zztctN1PYWcin=X)3SCGTTvSnQxH&hgf7ZCIiGoaww)M2wZ_4kmD(eoXm*CbDpA?kH zWLge&wcv`4%^uR=^dVQN{iyE?!qYdOI4U zSt2$acwT;wB^N{#mb`f?C>|6v9r8V%CI$=if&=6r>IWU*`;U_2U{V#BLB!+fWuCaW zA}1JL_Ui-UnDN5EE}JYOLp)682s$UsTLHEm(Ha?L!%NvUs@wjdT#!;x*~@SR7B(>_ zG)a#ZPfkzAXU}VKZFb)TzHNwFGC%~fFf^biH#L~KPe3Elu*i$RV zBhmKbOz%ATtVv-dOv3I=SGz~dZR5@hk^;U#7qv)?npfR7CC?sP-?^2lF0w;8J2tdq z!i;cW*n!`Q*Kg0P`N}Ml^{P3luA`O_NCy6z1p77WgL|1KmxHNEYMi|>R9;9}X9!>6 z1K7uR>!Nd>6%5!;9TfpxU#4vq6*{XB+{p{e&&Q6&`{1(eQLVxc3uQ}d#6%woL2$Q( znN!tU-CoaXIyTC)>7O~T^KRPK?k=B4(}*s=AHbcEa^{g}q!cyCWuWR4WsLo3)3R+b z1*826Tn=O!8Fzz=Hx*0*EATP0h><|c9uj>?h)jIP^mLvUG&^8>#O0l@P?h1EA1N9ADUvcqLsDyY})0N{to*YSH?Y zi&;zJikdgT`XFSvOR0fG5|zR%4x%ZOq=cGFv-1r}Ral-XOY!4wjUxhS=izN=#@taUrf zc;>#JXjuy{(DeUwd3=c8b(yRT8*FM3&XTfha~zUy)|o>kl(7o&Jw0nkd(V`tA<+VM zp;3uSQ^t%T9M~LloRUJzA$4Nf04*=zGj8P-IXJT+Zr}&zIsXZ2o%WqB~cdmY$-zY>;;oMW@w+qe$RdXlQW7*s;Y~n43(a8 zZnj2r5UdGzN}Is28JkgP)};+ z($uZ1ug7M7u-7z}E|zUA7~po*Oo`@hRk=aZV!?k#n%Z4}AU6mvww3PXXbW)FSLzX0 z7Ojyw)dY@%Q>`bZx7o49p6C=F4)Gb5&-_})DnwQlxnvDlRr8hG{$@u6()sB@XR zI(WGl6~# z4#&t7pF1h|4)$WizRH&E2AdLotHJZ)Y13WGhK|MG({-ZCEmB3~!=I!XTli803 zQa*{~`C;Wa##^axM^*w<3di7O3I)^&)Fx%9