Compare commits
5 Commits
main
..
8fb76bb331
| Author | SHA1 | Date | |
|---|---|---|---|
| 8fb76bb331 | |||
| bc29d51a29 | |||
| 6b74d410cd | |||
| 5d77ab1a88 | |||
| 0bff171936 |
@@ -2,9 +2,6 @@
|
|||||||
.venv
|
.venv
|
||||||
__pycache__
|
__pycache__
|
||||||
|
|
||||||
# Runtime agent traces (local only; written by RequestTraceLogger)
|
|
||||||
runtime_traces/
|
|
||||||
|
|
||||||
# Pipeline harness: per-run artifacts (md/json from tests.pipeline_setup_v3/v4)
|
# Pipeline harness: per-run artifacts (md/json from tests.pipeline_setup_v3/v4)
|
||||||
tests/**/test_runs/**/*.md
|
tests/**/test_runs/**/*.md
|
||||||
tests/**/test_runs/**/*.json
|
tests/**/test_runs/**/*.json
|
||||||
|
|||||||
Vendored
-5
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"files.exclude": {
|
|
||||||
"**/__pycache__": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Binary file not shown.
+17
-6
@@ -89,17 +89,28 @@ RAG сейчас используется как общее ядро индек
|
|||||||
Хранит карточку документа как точку входа в документ и его краткое описание.
|
Хранит карточку документа как точку входа в документ и его краткое описание.
|
||||||
|
|
||||||
Формирование:
|
Формирование:
|
||||||
Источник данных - frontmatter `as is`, summary и doc kind, вычисленный классификатором документации.
|
Источник данных - frontmatter, fallback title, summary и doc kind, вычисленный классификатором документации.
|
||||||
В `metadata_json` копируются все `key-value` из frontmatter без нормализации и без fallback для frontmatter-атрибутов.
|
Данные извлекаются структурированно по атрибутам.
|
||||||
Дополнительно в `metadata_json` добавляются служебные поля `source_path`, `summary_text`, `doc_kind`.
|
|
||||||
Атрибут `document_id` добавляется только при наличии `frontmatter.id` (fallback до пути файла не применяется).
|
|
||||||
В `content` попадает summary документа, а не склейка всех частей документа в сплошной текст.
|
В `content` попадает summary документа, а не склейка всех частей документа в сплошной текст.
|
||||||
|
|
||||||
Фиксация в БД:
|
Фиксация в БД:
|
||||||
| Атрибут в `metadata_json` | Описание | Источник |
|
| Атрибут в `metadata_json` | Описание | Источник |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `*` frontmatter fields | все поля frontmatter в исходном виде | frontmatter документа |
|
| `document_id` | идентификатор документа | `frontmatter.id`, иначе путь файла |
|
||||||
| `document_id` | идентификатор документа, добавляется только если в frontmatter есть `id` | `frontmatter.id` |
|
| `type` | тип документа из frontmatter | `frontmatter.type` |
|
||||||
|
| `name` | системное имя документа | `frontmatter.name` |
|
||||||
|
| `title` | человекочитаемый заголовок документа | `frontmatter.title`, иначе fallback title |
|
||||||
|
| `module` | модуль документа | `frontmatter.module` |
|
||||||
|
| `domain` | домен документа | `frontmatter.domain` |
|
||||||
|
| `subdomain` | поддомен документа | `frontmatter.subdomain` |
|
||||||
|
| `layer` | логический слой, указанный в frontmatter документа | `frontmatter.layer` |
|
||||||
|
| `status` | статус документа | `frontmatter.status` |
|
||||||
|
| `updated_at` | дата или отметка последнего обновления | `frontmatter.updated_at` |
|
||||||
|
| `tags` | теги документа | `frontmatter.tags` |
|
||||||
|
| `entities` | сущности, связанные с документом | `frontmatter.entities` |
|
||||||
|
| `parent` | родительский документ | `frontmatter.parent` |
|
||||||
|
| `children` | дочерние документы | `frontmatter.children` |
|
||||||
|
| `links` | ссылки на связанные материалы | `frontmatter.links` |
|
||||||
| `source_path` | исходный путь документа | путь файла |
|
| `source_path` | исходный путь документа | путь файла |
|
||||||
| `summary_text` | краткое содержание документа | секция `# Summary` |
|
| `summary_text` | краткое содержание документа | секция `# Summary` |
|
||||||
| `doc_kind` | классификация документа, например `readme`, `spec`, `runbook` | `DocsClassifier.classify(path)` |
|
| `doc_kind` | классификация документа, например `readme`, `spec`, `runbook` | `DocsClassifier.classify(path)` |
|
||||||
|
|||||||
@@ -1,289 +0,0 @@
|
|||||||
## 1. Формат ведения технической документации агентом
|
|
||||||
|
|
||||||
## 1.1. Общие принципы
|
|
||||||
|
|
||||||
Техническая документация, формируемая агентом, должна строиться как система атомарных, не пересекающихся по смыслу документов, связанных между собой явными ссылками.
|
|
||||||
|
|
||||||
Ключевые принципы:
|
|
||||||
- один документ описывает одну сущность или один устойчивый технический аспект;
|
|
||||||
- документ не должен дублировать соседние документы;
|
|
||||||
- общая система знаний должна собираться через ссылки, а не через копипасту;
|
|
||||||
- структура документации должна быть пригодна как для чтения человеком, так и для индексирования в RAG.
|
|
||||||
|
|
||||||
## 1.2. Требования к заголовкам
|
|
||||||
|
|
||||||
- Заголовок должен отражать только суть раздела.
|
|
||||||
- Заголовок не должен содержать метаданные (`id`, `doc_type`, `application`, `platform`, `domain`, `sub_domain`).
|
|
||||||
- Метаданные указываются отдельными строками в теле раздела или в YAML frontmatter.
|
|
||||||
|
|
||||||
Пример:
|
|
||||||
- правильно: `## 6.2 Метод UFS получения списка заказов`
|
|
||||||
- неправильно: `## 6.2 Блок api_method (id=..., platform=ufs)`
|
|
||||||
|
|
||||||
## 1.3. Базовые типы документных единиц
|
|
||||||
|
|
||||||
Базовые типы:
|
|
||||||
- `ui_page`
|
|
||||||
- `api_method`
|
|
||||||
- `logic_block`
|
|
||||||
|
|
||||||
Дополнительно могут использоваться:
|
|
||||||
- `architecture_overview`
|
|
||||||
- `integration_doc`
|
|
||||||
- `domain_entity`
|
|
||||||
- `glossary_item`
|
|
||||||
- `index_page`
|
|
||||||
|
|
||||||
## 1.4. Принцип декомпозиции страниц / файлов
|
|
||||||
|
|
||||||
### Один устойчивый объект - один документ
|
|
||||||
Если объект можно переиспользовать или на него могут ссылаться другие документы, его нужно выносить в отдельный файл.
|
|
||||||
|
|
||||||
### Документы не должны пересекаться по смыслу
|
|
||||||
Если описание повторяется в нескольких местах, нужно выделять общий документ и ссылаться на него.
|
|
||||||
|
|
||||||
### Use case и детали живут раздельно
|
|
||||||
Сценарий описывает поток работы, а детали выносятся в функциональные требования, отдельные блоки логики или контрактные описания.
|
|
||||||
|
|
||||||
## 1.5. Иерархическая организация документации
|
|
||||||
|
|
||||||
Документация должна быть организована как иерархическое дерево каталогов и файлов.
|
|
||||||
|
|
||||||
Пример:
|
|
||||||
|
|
||||||
```text
|
|
||||||
docs/
|
|
||||||
ui/
|
|
||||||
api/
|
|
||||||
logic/
|
|
||||||
domains/
|
|
||||||
integrations/
|
|
||||||
architecture/
|
|
||||||
glossary/
|
|
||||||
errors/
|
|
||||||
```
|
|
||||||
|
|
||||||
## 1.6. Учет связей между документами
|
|
||||||
|
|
||||||
Связи должны быть явными.
|
|
||||||
|
|
||||||
Примеры:
|
|
||||||
- UI-страница ссылается на вызываемые API;
|
|
||||||
- API-документ ссылается на используемые блоки логики;
|
|
||||||
- логический блок ссылается на интеграции;
|
|
||||||
- документ по коду ссылается на системную аналитику, инициировавшую изменения.
|
|
||||||
|
|
||||||
## 1.7. Формат markdown-документов
|
|
||||||
|
|
||||||
Каждый документ состоит из:
|
|
||||||
1. YAML frontmatter;
|
|
||||||
2. Markdown body.
|
|
||||||
|
|
||||||
## 1.8. YAML frontmatter
|
|
||||||
|
|
||||||
### Обязательные поля
|
|
||||||
- `id`
|
|
||||||
- `title`
|
|
||||||
- `doc_type`
|
|
||||||
- `status`
|
|
||||||
- `domain`
|
|
||||||
- `sub_domain`
|
|
||||||
- `related_docs`
|
|
||||||
|
|
||||||
### Рекомендуемые поля
|
|
||||||
- `owner`
|
|
||||||
- `entities`
|
|
||||||
- `tags`
|
|
||||||
- `feature`
|
|
||||||
- `system_analytics_refs`
|
|
||||||
- `source_of_truth`
|
|
||||||
- `related_code`
|
|
||||||
|
|
||||||
### Допустимые значения `doc_type`
|
|
||||||
- `ui_page`
|
|
||||||
- `api_method`
|
|
||||||
- `logic_block`
|
|
||||||
- `architecture_overview`
|
|
||||||
- `integration_doc`
|
|
||||||
- `domain_entity`
|
|
||||||
- `glossary_item`
|
|
||||||
- `index_page`
|
|
||||||
|
|
||||||
### Допустимые значения `status`
|
|
||||||
- `draft`
|
|
||||||
- `in_review`
|
|
||||||
- `approved`
|
|
||||||
- `outdated`
|
|
||||||
- `generated`
|
|
||||||
- `active`
|
|
||||||
|
|
||||||
## 1.9. Синхронизация с системной аналитикой
|
|
||||||
|
|
||||||
Техническая документация строится на основе системной аналитики (features).
|
|
||||||
|
|
||||||
Обязательно учитывать:
|
|
||||||
- концептуальный уровень аналитики;
|
|
||||||
- детализацию технической документации;
|
|
||||||
- согласованность терминов, ролей и интеграционных цепочек.
|
|
||||||
|
|
||||||
Если атрибуты или детали отсутствуют в аналитике:
|
|
||||||
- определить их из текста аналитики;
|
|
||||||
- дополнить данными из репозитория (код, контракты, существующие документы);
|
|
||||||
- зафиксировать итог в документации как явные метаданные и требования.
|
|
||||||
|
|
||||||
## 1.10. Формат body-разделов для блока изменений
|
|
||||||
|
|
||||||
Для секции изменений (по аналогии с разделом `6` в аналитике) использовать единый формат.
|
|
||||||
|
|
||||||
Под корнем секции изменений указывать общие атрибуты:
|
|
||||||
- `domain`
|
|
||||||
- `sub_domain`
|
|
||||||
|
|
||||||
Для каждого подраздела `X.Y` указывать метаданные строками сразу после заголовка:
|
|
||||||
- `id`
|
|
||||||
- `doc_type`
|
|
||||||
- `application`
|
|
||||||
- `platform`
|
|
||||||
|
|
||||||
## 1.11. Различие аналитики и документации
|
|
||||||
|
|
||||||
- Аналитика - концептуальный уровень, упрощенный use case.
|
|
||||||
- Документация - детальный инженерный уровень.
|
|
||||||
|
|
||||||
Для документации:
|
|
||||||
- технический use case должен быть детализированным;
|
|
||||||
- функциональные требования расширяют use case и описывают детали интеграций, логики и поведения;
|
|
||||||
- функциональные требования не должны копировать шаги сценария без добавления новой информации.
|
|
||||||
|
|
||||||
Источник правил:
|
|
||||||
- `src/app/core/agent/processes/v2/doc_rules_v2/common-elements/tech-use-case.md`
|
|
||||||
- `src/app/core/agent/processes/v2/doc_rules_v2/common-elements/fr.md`
|
|
||||||
|
|
||||||
## 1.12. Требования к `ui_page`
|
|
||||||
|
|
||||||
Обязательная структура:
|
|
||||||
- `### Технический use case`
|
|
||||||
- `### Требования к UI`
|
|
||||||
- `### Функциональные требования`
|
|
||||||
- `### Нефункциональные требования`
|
|
||||||
|
|
||||||
### Требования к UI
|
|
||||||
Внутри обязательно отдельно описывать каждую форму UI:
|
|
||||||
- табличное представление;
|
|
||||||
- пустой список (empty state);
|
|
||||||
- ошибка (error state).
|
|
||||||
|
|
||||||
Обязательные правила:
|
|
||||||
- если есть интеграция, обязательно описать показ ошибки;
|
|
||||||
- если показывается список, обязательно описать показ отсутствия данных.
|
|
||||||
|
|
||||||
### UI-элементы
|
|
||||||
UI-поля и элементы в документации описываются строго в таблицах.
|
|
||||||
|
|
||||||
Обязательные колонки (заполнять там, где применимо):
|
|
||||||
- `Код элемента`
|
|
||||||
- `Название и описание`
|
|
||||||
- `Данные`
|
|
||||||
- `Поведение`
|
|
||||||
- `Валидация`
|
|
||||||
|
|
||||||
## 1.13. Пользовательская аналитика для `ui_page`
|
|
||||||
|
|
||||||
События пользовательской аналитики оформляются таблицей:
|
|
||||||
- `Название события`
|
|
||||||
- `Описание`
|
|
||||||
- `Точка вызова`
|
|
||||||
- `Payload`
|
|
||||||
|
|
||||||
## 1.14. Требования к `api_method`
|
|
||||||
|
|
||||||
Обязательная структура:
|
|
||||||
- `### Технический use case`
|
|
||||||
- `### Функциональные требования`
|
|
||||||
- `### Нефункциональные требования`
|
|
||||||
- `### Контракт`
|
|
||||||
|
|
||||||
### Технический use case
|
|
||||||
Оформляется детально по правилам `tech-use-case.md`.
|
|
||||||
|
|
||||||
Обязательные части:
|
|
||||||
- название
|
|
||||||
- предусловия
|
|
||||||
- триггер
|
|
||||||
- основной сценарий
|
|
||||||
- альтернативный сценарий
|
|
||||||
- обработка ошибок
|
|
||||||
- постусловие
|
|
||||||
|
|
||||||
### Функциональные требования
|
|
||||||
Оформляются по правилам `fr.md`:
|
|
||||||
- формат `FR.<номер>. <Название>`;
|
|
||||||
- FR расширяют use case;
|
|
||||||
- FR не дублируют шаги сценария без дополнительной ценности;
|
|
||||||
- для интеграционных шагов FR обязательны.
|
|
||||||
|
|
||||||
## 1.15. Нефункциональные требования для `api_method`
|
|
||||||
|
|
||||||
Разделять на подразделы:
|
|
||||||
- `#### Аудит` (если применимо)
|
|
||||||
- `#### Мониторинг`
|
|
||||||
|
|
||||||
### Мониторинг
|
|
||||||
Оформлять таблицей:
|
|
||||||
- `Метрика`
|
|
||||||
- `Описание`
|
|
||||||
- `Условие срабатывания`
|
|
||||||
|
|
||||||
Правила:
|
|
||||||
- в условиях указывать, при каких состояниях фиксируется событие;
|
|
||||||
- не использовать формулировку вида «точка измерения = метод»;
|
|
||||||
- базово закладывать метрики:
|
|
||||||
- `<METRIC_NAME>_SUCCESS`
|
|
||||||
- `<METRIC_NAME>_FAIL`
|
|
||||||
- `<METRIC_NAME>_BUSINESS_ERROR`
|
|
||||||
|
|
||||||
## 1.16. Распределение ответственности по слоям
|
|
||||||
|
|
||||||
- Проверка ролевой модели пользователя обычно выполняется в `ufs`.
|
|
||||||
- Для `pprb` аудит может не фиксироваться, если это согласовано правилами домена.
|
|
||||||
- Если проверка ролей вынесена в `ufs`, не дублировать этот шаг в use case `pprb`.
|
|
||||||
|
|
||||||
## 1.17. Контракты API
|
|
||||||
|
|
||||||
Контракт может быть:
|
|
||||||
- в markdown-таблицах;
|
|
||||||
- в OpenAPI;
|
|
||||||
- в отдельном контрактном файле.
|
|
||||||
|
|
||||||
Для markdown-контракта минимум:
|
|
||||||
- endpoint/method;
|
|
||||||
- request fields;
|
|
||||||
- required/optional;
|
|
||||||
- constraints;
|
|
||||||
- response;
|
|
||||||
- errors;
|
|
||||||
- auth;
|
|
||||||
- retry;
|
|
||||||
- timeout;
|
|
||||||
- idempotency.
|
|
||||||
|
|
||||||
## 1.18. Integrations-блок
|
|
||||||
|
|
||||||
Если у документа есть интеграции, выделять отдельный `## Integrations`.
|
|
||||||
|
|
||||||
Рекомендуемые атрибуты интеграции:
|
|
||||||
- `target`
|
|
||||||
- `target_type`
|
|
||||||
- `direction`
|
|
||||||
- `interaction`
|
|
||||||
- `via`
|
|
||||||
- `purpose`
|
|
||||||
- `details`
|
|
||||||
|
|
||||||
## 1.19. Общие требования к markdown body
|
|
||||||
|
|
||||||
- В документе должен быть один `H1`, совпадающий с `title`.
|
|
||||||
- Основные разделы - `H2`, подразделы - `H3`.
|
|
||||||
- Не допускать хаотичной вложенности заголовков.
|
|
||||||
- Вместо дублирования использовать ссылки на связанные документы.
|
|
||||||
- Сценарии, правила, ограничения и кодовые привязки держать раздельно.
|
|
||||||
@@ -1,212 +0,0 @@
|
|||||||
# Системная аналитика
|
|
||||||
|
|
||||||
## Общее описание
|
|
||||||
|
|
||||||
Документ описывает изменения в автоматизированной системе. Пишется системными аналитиками для разработчиков и тестировщиков и проходит согласование с экспертами по архитектуре, безопасности и сопровождению.
|
|
||||||
|
|
||||||
Документ может описывать как новый процесс, так и инкремент доработки существующей функциональности.
|
|
||||||
|
|
||||||
## Требования к заголовкам
|
|
||||||
|
|
||||||
- Заголовок должен отражать суть раздела.
|
|
||||||
- Заголовок не должен содержать лишнюю информацию, которая относится к метаданным (id, doc_type, platform, application и т.д.).
|
|
||||||
- Метаданные указываются отдельными строками в теле раздела.
|
|
||||||
|
|
||||||
## Состав документа
|
|
||||||
|
|
||||||
Каждый раздел верхнего уровня оформляется заголовком уровня `#`.
|
|
||||||
|
|
||||||
### 1. Цели
|
|
||||||
|
|
||||||
- Коротко описать, какую проблему и для кого решаем.
|
|
||||||
- 1-2 предложения.
|
|
||||||
- Не дублировать критерии приемки.
|
|
||||||
|
|
||||||
### 2. Процесс AS IS и TO BE
|
|
||||||
|
|
||||||
- Фокус на пользовательских и бизнес-изменениях.
|
|
||||||
- Не указывать технические детали (платформы, API, внутренние интеграции).
|
|
||||||
|
|
||||||
### 3. Ограничения
|
|
||||||
|
|
||||||
- Ограничения и допущения в техническом и бизнесовом плане.
|
|
||||||
|
|
||||||
### 4. Критерии приемки
|
|
||||||
|
|
||||||
- Описывать с точки зрения пользователя.
|
|
||||||
- Не добавлять технические детали (платформы, API, внутренние компоненты).
|
|
||||||
|
|
||||||
### 5. Архитектура
|
|
||||||
|
|
||||||
Нужно указать:
|
|
||||||
|
|
||||||
- схему контейнеров,
|
|
||||||
- таблицу интеграций,
|
|
||||||
- сквозные интеграционные сценарии.
|
|
||||||
|
|
||||||
Слои:
|
|
||||||
|
|
||||||
- `ui` - web-приложение, клиент.
|
|
||||||
- `ufs` - BFF: аутентификация/авторизация, агрегация и маппинг данных.
|
|
||||||
- `pprb` - backend: API, БД, логика жизненного цикла сущностей.
|
|
||||||
|
|
||||||
#### Диаграмма
|
|
||||||
|
|
||||||
Mermaid-диаграмма должна содержать:
|
|
||||||
|
|
||||||
- основные контейнеры,
|
|
||||||
- названия приложений и платформ,
|
|
||||||
- интеграции между приложениями,
|
|
||||||
- названия вызываемых endpoint или топиков.
|
|
||||||
|
|
||||||
#### Таблица интеграций
|
|
||||||
|
|
||||||
Обязательные колонки:
|
|
||||||
|
|
||||||
- Код
|
|
||||||
- Название endpoint/топика
|
|
||||||
- Источник данных
|
|
||||||
- Потребитель данных
|
|
||||||
- Инициатор вызова
|
|
||||||
- Передаваемые данные
|
|
||||||
|
|
||||||
#### Сквозной интеграционный сценарий
|
|
||||||
|
|
||||||
- Нумерованный список вызовов вида: «Компонент 1 вызывает endpoint в Компонент 2».
|
|
||||||
- Только интеграционная цепочка, без детального разбора логики.
|
|
||||||
|
|
||||||
### 6. Описание изменений
|
|
||||||
|
|
||||||
Раздел состоит из подразделов уровня `##` (например, `6.1`, `6.2`, `6.3`).
|
|
||||||
|
|
||||||
Под корнем раздела `# 6` указываются общие метаданные:
|
|
||||||
|
|
||||||
- `domain`
|
|
||||||
- `sub_domain`
|
|
||||||
|
|
||||||
Для каждого раздела `6.x` обязательно указывать метаданные строками сразу после заголовка:
|
|
||||||
|
|
||||||
- `id`
|
|
||||||
- `doc_type`
|
|
||||||
- `application`
|
|
||||||
- `platform`
|
|
||||||
|
|
||||||
Дополнительные метаданные для случаев изменения существующей документации:
|
|
||||||
|
|
||||||
- `action`
|
|
||||||
- `target_doc_id`
|
|
||||||
- `target_path`
|
|
||||||
|
|
||||||
#### 6.x для `ui_page`
|
|
||||||
|
|
||||||
Обязательная структура:
|
|
||||||
|
|
||||||
- `### Технический use case (тезисно)`
|
|
||||||
- `### Требования к UI`
|
|
||||||
- `### Функциональные требования`
|
|
||||||
- `### Нефункциональные требования`
|
|
||||||
|
|
||||||
Требования к разделу `### Требования к UI`:
|
|
||||||
|
|
||||||
- Внутри нужно отдельно описывать каждую UI-форму.
|
|
||||||
- Если есть интеграция, обязательно описать, как показывается ошибка.
|
|
||||||
- Если показываем список, обязательно описать, как показывается отсутствие данных.
|
|
||||||
|
|
||||||
Рекомендуемая детализация UI-форм:
|
|
||||||
|
|
||||||
- табличное представление,
|
|
||||||
- пустой список (empty state),
|
|
||||||
- ошибка (error state).
|
|
||||||
|
|
||||||
Правила описания UI-полей:
|
|
||||||
|
|
||||||
- Поля описывать списком (не таблицей).
|
|
||||||
- Общие правила (например, read-only, поведение при пустом значении) выносить в общий блок, не дублировать для каждого поля.
|
|
||||||
|
|
||||||
Отдельно нужно различать два сценария описания:
|
|
||||||
|
|
||||||
1. Если описывается новая UI-страница или новая самостоятельная UI-форма, раздел оформляется полноценно по шаблону `ui_page`.
|
|
||||||
- Нужно дать достаточный контекст для разработки и тестирования.
|
|
||||||
- Нужно подробно описывать структуру формы, состояния отображения, поведение полей, ошибки, empty state и пользовательские действия.
|
|
||||||
|
|
||||||
2. Если описывается доработка уже существующей страницы или существующей UI-формы, не нужно повторно копировать полное описание из действующей документации.
|
|
||||||
- Нужно учитывать уже существующее описание страницы в документации и аналитике.
|
|
||||||
- В аналитике нужно явно указать, что именно меняется в существующем сценарии: что добавляется, редактируется или удаляется.
|
|
||||||
- Нужно указывать точку изменения: в какой существующей странице, форме, блоке или сценарии вносится изменение.
|
|
||||||
- Нужно ссылаться на существующий документ или раздел, где базовое поведение уже описано.
|
|
||||||
- Нужно описывать только delta изменений, достаточную для реализации доработки и актуализации документации.
|
|
||||||
- Полное описание существующей страницы в таком разделе не дублируется.
|
|
||||||
- Для такой доработки в metadata нужно явно указывать `action: update`.
|
|
||||||
- Если изменение должно попасть в уже существующий markdown-документ, нужно явно указывать `target_doc_id` и/или `target_path`.
|
|
||||||
- `target_doc_id` должен совпадать с `id` существующего документа, который требуется обновить.
|
|
||||||
- Если `target_doc_id`/`target_path` не указаны, агент может ошибочно интерпретировать раздел как создание нового документа.
|
|
||||||
|
|
||||||
Нефункциональные требования для `ui_page`:
|
|
||||||
|
|
||||||
- пользовательская аналитика оформляется таблицей с колонками:
|
|
||||||
- `Название события`
|
|
||||||
- `Описание`
|
|
||||||
- `Точка вызова`
|
|
||||||
- `Payload`
|
|
||||||
|
|
||||||
#### 6.x для `api_method`
|
|
||||||
|
|
||||||
Обязательная структура:
|
|
||||||
|
|
||||||
- `### Технический use case (тезисно)`
|
|
||||||
- `### Функциональные требования`
|
|
||||||
- `### Нефункциональные требования`
|
|
||||||
- `### Контракт метода`
|
|
||||||
|
|
||||||
Правило для функциональных требований:
|
|
||||||
|
|
||||||
- Если дополнительных требований нет (дублируют сценарий), писать: `Не выявлены`.
|
|
||||||
|
|
||||||
Нефункциональные требования:
|
|
||||||
|
|
||||||
- Разделять на подразделы:
|
|
||||||
- `#### Аудит` (если применимо)
|
|
||||||
- `#### Мониторинг`
|
|
||||||
|
|
||||||
Для `Мониторинг` использовать таблицу с колонками:
|
|
||||||
|
|
||||||
- `Метрика`
|
|
||||||
- `Описание`
|
|
||||||
- `Условие срабатывания`
|
|
||||||
|
|
||||||
Важно:
|
|
||||||
|
|
||||||
- В мониторинге описывать условия срабатывания, а не «точку измерения = метод».
|
|
||||||
- Базово закладывать 3 метрики:
|
|
||||||
- `<METRIC_NAME>_SUCCESS`
|
|
||||||
- `<METRIC_NAME>_FAIL`
|
|
||||||
- `<METRIC_NAME>_BUSINESS_ERROR`
|
|
||||||
|
|
||||||
Контракт метода:
|
|
||||||
|
|
||||||
- Для запроса: таблица параметров (`header/query/path`) с колонками: название, тип параметра, тип данных, обязательность, описание, пример.
|
|
||||||
- Для тела JSON (если есть): структура отдельной таблицей.
|
|
||||||
- Для ответа JSON: таблица с колонками: название, тип данных, обязательность, описание, заполнение, пример.
|
|
||||||
|
|
||||||
#### 6.x для `logic_block`
|
|
||||||
|
|
||||||
Обязательная структура:
|
|
||||||
|
|
||||||
- `### Технический use case (тезисно)`
|
|
||||||
- `### Функциональные требования`
|
|
||||||
- `### Нефункциональные требования`
|
|
||||||
|
|
||||||
`logic_block` удобно использовать для фиксации точечных изменений существующего сценария, если раздел не описывает новую самостоятельную страницу или новую самостоятельную форму, а только уточняет delta к уже существующей документации.
|
|
||||||
|
|
||||||
Если точечное изменение должно изменить существующий документ другого типа, `logic_block` для этого использовать нельзя. В этом случае metadata раздела должна указывать тип и идентификатор целевого существующего документа, который требуется обновить.
|
|
||||||
|
|
||||||
## Дополнительные правила по слоям
|
|
||||||
|
|
||||||
- Проверка ролевой модели пользователя обычно выполняется на уровне `ufs`.
|
|
||||||
- Для `pprb` аудит может не фиксироваться, если это правило принято для конкретной фичи/домена.
|
|
||||||
- Если проверка ролей вынесена в `ufs`, не дублировать этот шаг в сценарии `pprb`.
|
|
||||||
|
|
||||||
## Термины
|
|
||||||
|
|
||||||
- Аудит: события, которые фиксируют действия пользователя и позволяют ответить на вопрос «кто, что, когда сделал».
|
|
||||||
- Мониторинг: технические события/метрики для контроля стабильности и поиска сбоев.
|
|
||||||
@@ -0,0 +1,852 @@
|
|||||||
|
## 1. Формат ведения технической документации агентом
|
||||||
|
|
||||||
|
## 1.1. Общие принципы
|
||||||
|
|
||||||
|
Техническая документация, формируемая агентом, должна строиться как **система атомарных, не пересекающихся по смыслу документов**, связанных между собой явными ссылками.
|
||||||
|
|
||||||
|
Ключевые принципы:
|
||||||
|
- один документ описывает одну сущность или один устойчивый технический аспект;
|
||||||
|
- документ не должен дублировать соседние документы;
|
||||||
|
- общая система знаний должна собираться через ссылки, а не через копипасту;
|
||||||
|
- структура документации должна быть пригодна как для чтения человеком, так и для индексирования в RAG.
|
||||||
|
|
||||||
|
## 1.2. Базовые типы документных единиц
|
||||||
|
|
||||||
|
На первом этапе логично сохранить текущую семантику типов документов, но перенести ее в файловую модель.
|
||||||
|
|
||||||
|
Базовые типы:
|
||||||
|
- `ui_page`
|
||||||
|
- `api_method`
|
||||||
|
- `logic_block`
|
||||||
|
|
||||||
|
Позже могут добавиться:
|
||||||
|
- `architecture_overview`
|
||||||
|
- `integration_doc`
|
||||||
|
- `domain_entity`
|
||||||
|
- `glossary_item`
|
||||||
|
- `index_page`
|
||||||
|
|
||||||
|
## 1.3. Принцип декомпозиции страниц / файлов
|
||||||
|
|
||||||
|
### Один устойчивый объект — один документ
|
||||||
|
Если объект можно переиспользовать или на него могут ссылаться другие документы, его надо выносить в отдельный файл.
|
||||||
|
|
||||||
|
Примеры:
|
||||||
|
- отдельная UI-страница;
|
||||||
|
- отдельный API endpoint;
|
||||||
|
- отдельный блок логики;
|
||||||
|
- отдельный интеграционный сценарий.
|
||||||
|
|
||||||
|
### Документы не должны пересекаться по смыслу
|
||||||
|
Если описание повторяется в нескольких местах, нужно выделять общий документ и ссылаться на него.
|
||||||
|
|
||||||
|
Примеры:
|
||||||
|
- фронтальная страница не должна заново описывать логику API;
|
||||||
|
- документ по API не должен заново раскрывать общую логику переиспользуемого блока;
|
||||||
|
- вместо дублирования должен быть переход по ссылке.
|
||||||
|
|
||||||
|
### Use case и детальные правила живут раздельно
|
||||||
|
Сценарий описывает поток работы, а детали выносятся в функциональные требования, отдельные блоки логики или контрактные описания.
|
||||||
|
|
||||||
|
Это важно и для RAG-индексации:
|
||||||
|
- сценарии индексируются как workflows;
|
||||||
|
- отдельные правила — как facts;
|
||||||
|
- сущности и блоки — как entities.
|
||||||
|
|
||||||
|
## 1.4. Иерархическая организация документации
|
||||||
|
|
||||||
|
Документация должна быть организована как иерархическое дерево каталогов и файлов, а не как набор неструктурированных страниц.
|
||||||
|
|
||||||
|
Пример верхнего уровня:
|
||||||
|
|
||||||
|
```text
|
||||||
|
docs/
|
||||||
|
ui/
|
||||||
|
api/
|
||||||
|
logic/
|
||||||
|
domains/
|
||||||
|
integrations/
|
||||||
|
architecture/
|
||||||
|
glossary/
|
||||||
|
errors/
|
||||||
|
```
|
||||||
|
|
||||||
|
Пример организации:
|
||||||
|
|
||||||
|
```text
|
||||||
|
docs/
|
||||||
|
ui/
|
||||||
|
order-create-page.md
|
||||||
|
order-edit-page.md
|
||||||
|
api/
|
||||||
|
orders-create.md
|
||||||
|
orders-get.md
|
||||||
|
logic/
|
||||||
|
order-validation.md
|
||||||
|
order-enrichment.md
|
||||||
|
architecture/
|
||||||
|
system-overview.md
|
||||||
|
integration-landscape.md
|
||||||
|
errors/
|
||||||
|
catalog.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
## 1.5. Учет связей между документами
|
||||||
|
|
||||||
|
Связи должны быть **явными и поддерживаемыми агентом**.
|
||||||
|
|
||||||
|
Примеры:
|
||||||
|
- UI-страница ссылается на API, который она вызывает;
|
||||||
|
- API-документ ссылается на переиспользуемые логические блоки;
|
||||||
|
- логический блок ссылается на связанные интеграции;
|
||||||
|
- архитектурный обзор ссылается на набор конкретных модулей и документов;
|
||||||
|
- документ по коду может ссылаться на системную аналитику, которая инициировала изменение.
|
||||||
|
|
||||||
|
Именно эта сеть ссылок затем индексируется в слоях:
|
||||||
|
- `D1_DOCUMENT_CATALOG`
|
||||||
|
- `D3_ENTITY_CATALOG`
|
||||||
|
- `D4_WORKFLOW_INDEX`
|
||||||
|
- `D5_REFERENCE_GRAPH`
|
||||||
|
- `D6_DOC_CODE_LINKS`
|
||||||
|
|
||||||
|
## 1.6. Формат markdown-документов
|
||||||
|
|
||||||
|
Основной формат технической документации — `Markdown`.
|
||||||
|
|
||||||
|
Каждый документ состоит из двух частей:
|
||||||
|
1. **YAML frontmatter** — структурные метаданные;
|
||||||
|
2. **Markdown body** — основное содержимое по шаблону.
|
||||||
|
|
||||||
|
## 3.7. YAML frontmatter
|
||||||
|
|
||||||
|
Frontmatter нужен для:
|
||||||
|
- определения типа документа;
|
||||||
|
- идентификации документа;
|
||||||
|
- определения его места в иерархии;
|
||||||
|
- фиксации связей с кодом и другими документами;
|
||||||
|
- выделения сущностей и тегов;
|
||||||
|
- упрощения построения слоев `D1`, `D3`, `D5`, `D6`.
|
||||||
|
|
||||||
|
### Базовый frontmatter
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
id: ui-order-create-page
|
||||||
|
title: Страница создания заказа
|
||||||
|
doc_type: ui_page
|
||||||
|
domain: orders
|
||||||
|
status: draft
|
||||||
|
related_docs:
|
||||||
|
- api-orders-create
|
||||||
|
- logic-order-validation
|
||||||
|
entities:
|
||||||
|
- Order
|
||||||
|
- CreateOrder
|
||||||
|
tags:
|
||||||
|
- ui
|
||||||
|
- orders
|
||||||
|
- creation
|
||||||
|
#owner: system-analyst
|
||||||
|
#source_of_truth: code
|
||||||
|
#related_code:
|
||||||
|
# - src/orders/ui/create_page.tsx
|
||||||
|
# - src/orders/api/orders_controller.py
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
### Обязательные поля
|
||||||
|
|
||||||
|
- `id` — стабильный уникальный идентификатор документа;
|
||||||
|
- `title` — человекочитаемый заголовок;
|
||||||
|
- `doc_type` — тип документа;
|
||||||
|
- `related_docs` — ссылки на связанные документы;
|
||||||
|
- `status` — статус документа;
|
||||||
|
- `domain` - домен фичи (Карточка клиента, Задачи, Сделки, Предложения)
|
||||||
|
- `sub_domain` - поддомен внутри основной сущности (Счета, ЗДА, ECM)
|
||||||
|
|
||||||
|
### Рекомендуемые поля
|
||||||
|
- `owner`
|
||||||
|
- `entities`
|
||||||
|
- `tags`
|
||||||
|
- `parent`
|
||||||
|
- `children`
|
||||||
|
- `feature`
|
||||||
|
- `system_analytics_refs`
|
||||||
|
- `business_refs`
|
||||||
|
- `updated_from`
|
||||||
|
- `reviewers`
|
||||||
|
- `source_of_truth`
|
||||||
|
- `related_code`
|
||||||
|
|
||||||
|
### Допустимые значения `doc_type`
|
||||||
|
- `ui_page`
|
||||||
|
- `api_method`
|
||||||
|
- `logic_block`
|
||||||
|
- `architecture_overview`
|
||||||
|
- `integration_doc`
|
||||||
|
- `domain_entity`
|
||||||
|
- `glossary_item`
|
||||||
|
- `index_page`
|
||||||
|
|
||||||
|
### Допустимые значения `status`
|
||||||
|
- `draft`
|
||||||
|
- `in_review`
|
||||||
|
- `approved`
|
||||||
|
- `outdated`
|
||||||
|
- `generated`
|
||||||
|
- `active`
|
||||||
|
|
||||||
|
### Допустимые значения `source_of_truth`
|
||||||
|
- `code`
|
||||||
|
- `doc`
|
||||||
|
- `system_analysis`
|
||||||
|
- `business_requirements`
|
||||||
|
- `mixed`
|
||||||
|
|
||||||
|
## 1.8. Typed frontmatter для разных типов документов
|
||||||
|
|
||||||
|
У каждого типа документа есть:
|
||||||
|
- **общие поля**;
|
||||||
|
- **тип-специфичные поля**.
|
||||||
|
|
||||||
|
### Пример для `api_method`
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
id: api.create_invoice
|
||||||
|
doc_type: api_method
|
||||||
|
domain: billing
|
||||||
|
title: Создание инвойса
|
||||||
|
|
||||||
|
endpoint: POST /api/v1/invoices
|
||||||
|
auth: USER
|
||||||
|
idempotent: false
|
||||||
|
timeout_ms: 3000
|
||||||
|
|
||||||
|
links:
|
||||||
|
called_by:
|
||||||
|
- ui.invoice_form
|
||||||
|
uses_logic:
|
||||||
|
- logic.invoice_validation
|
||||||
|
writes_db:
|
||||||
|
- db.invoices
|
||||||
|
- db.invoice_items
|
||||||
|
integrates_with:
|
||||||
|
- int.crm_sync
|
||||||
|
|
||||||
|
related_docs:
|
||||||
|
- ui.invoice_form
|
||||||
|
- logic.invoice_validation
|
||||||
|
related_code:
|
||||||
|
- services/billing/api/create_invoice.py
|
||||||
|
entities:
|
||||||
|
- Invoice
|
||||||
|
- CreateInvoice
|
||||||
|
|
||||||
|
tags:
|
||||||
|
- invoice
|
||||||
|
- create
|
||||||
|
- billing
|
||||||
|
status: active
|
||||||
|
version: 1.3
|
||||||
|
source_of_truth: code
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
### Для `api_method` полезно поддерживать
|
||||||
|
|
||||||
|
- `endpoint`
|
||||||
|
- `sup_parameters`
|
||||||
|
- `role_model_actions`
|
||||||
|
- `monitoring_actions`
|
||||||
|
- `audit_actions`
|
||||||
|
- `idempotent`
|
||||||
|
- `timeout_ms`
|
||||||
|
- `links.called_by`
|
||||||
|
- `links.uses_logic`
|
||||||
|
- `links.writes_db`
|
||||||
|
- `links.integrates_with`
|
||||||
|
|
||||||
|
### Для `ui_page` позже полезно поддерживать
|
||||||
|
- `calls_api`
|
||||||
|
- `user_analitycs_actions`
|
||||||
|
- `sup_parameters`
|
||||||
|
- `role_model_actions`
|
||||||
|
- `entry_points`
|
||||||
|
- `uses_logic`
|
||||||
|
|
||||||
|
### Для `logic_block` полезно поддерживать
|
||||||
|
|
||||||
|
- `called_from`
|
||||||
|
- `uses_logic`
|
||||||
|
- `reads_db`
|
||||||
|
- `writes_db`
|
||||||
|
- `integrates_with`
|
||||||
|
|
||||||
|
## 1.9. Двухслойная структура документа: `Summary` + `Details`
|
||||||
|
|
||||||
|
LLM не должна каждый раз тонуть в полном документе. Поэтому каждый документ должен содержать два уровня представления.
|
||||||
|
|
||||||
|
### `Summary`
|
||||||
|
Короткая, строго структурированная спецификация. Это слой **быстрого контекста**.
|
||||||
|
|
||||||
|
Рекомендуемый объем:
|
||||||
|
- примерно 30–60 строк;
|
||||||
|
- без длинных пояснений;
|
||||||
|
- только ключевые факты.
|
||||||
|
|
||||||
|
Пример:
|
||||||
|
|
||||||
|
```md
|
||||||
|
## Summary
|
||||||
|
- Purpose: создание инвойса из формы
|
||||||
|
- Actor: пользователь
|
||||||
|
- Trigger: Submit
|
||||||
|
- Main API: POST /api/v1/invoices (api.create_invoice)
|
||||||
|
- Validation: required fields, amount > 0, date <= today
|
||||||
|
- Errors: 400(field errors), 409(duplicate), 503(retryable)
|
||||||
|
- Analytics: event invoice_submit, invoice_error
|
||||||
|
```
|
||||||
|
|
||||||
|
### `Details`
|
||||||
|
Полное раскрытие объекта:
|
||||||
|
- use case;
|
||||||
|
- функциональные требования;
|
||||||
|
- UI;
|
||||||
|
- API;
|
||||||
|
- integrations;
|
||||||
|
- ошибки;
|
||||||
|
- НФТ;
|
||||||
|
- связи;
|
||||||
|
- кодовые привязки.
|
||||||
|
|
||||||
|
### Блок `## Integrations`
|
||||||
|
|
||||||
|
Если у объекта есть интеграции, они должны быть выделены в отдельный блок `## Integrations`.
|
||||||
|
Интеграции не нужно дублировать во frontmatter.
|
||||||
|
Основное описание хранится в body документа.
|
||||||
|
|
||||||
|
Ожидаемый принцип:
|
||||||
|
- одна интеграция = одна отдельная запись внутри блока;
|
||||||
|
- у интеграции есть краткое имя;
|
||||||
|
- у интеграции есть структурированные атрибуты;
|
||||||
|
- дополнительные детали допускаются в свободной форме через вложенный словарь.
|
||||||
|
|
||||||
|
Рекомендуемые атрибуты интеграции:
|
||||||
|
- `target`
|
||||||
|
- `target_type`
|
||||||
|
- `direction`
|
||||||
|
- `interaction`
|
||||||
|
- `via`
|
||||||
|
- `purpose`
|
||||||
|
- `details`
|
||||||
|
|
||||||
|
Где:
|
||||||
|
- `target` - идентификатор или имя целевого объекта;
|
||||||
|
- `target_type` - тип цели: `api`, `ui`, `entity`, `service`, `queue`, `db`, `external_system`;
|
||||||
|
- `direction` - направление: `inbound`, `outbound`, `bidirectional`;
|
||||||
|
- `interaction` - тип взаимодействия: `calls`, `reads`, `writes`, `emits`, `consumes`, `depends_on`;
|
||||||
|
- `via` - технический канал интеграции;
|
||||||
|
- `purpose` - зачем нужна интеграция;
|
||||||
|
- `details` - словарь с гибкой структурой под дополнительные параметры.
|
||||||
|
|
||||||
|
Пример:
|
||||||
|
|
||||||
|
```md
|
||||||
|
## Integrations
|
||||||
|
|
||||||
|
### Orders API
|
||||||
|
- target: api.orders.create
|
||||||
|
- target_type: api
|
||||||
|
- direction: outbound
|
||||||
|
- interaction: calls
|
||||||
|
- via: POST /api/orders
|
||||||
|
- purpose: создание заказа
|
||||||
|
- details:
|
||||||
|
- auth: service-token
|
||||||
|
- retry: true
|
||||||
|
|
||||||
|
### Order Entity
|
||||||
|
- target: domain.order
|
||||||
|
- target_type: entity
|
||||||
|
- direction: outbound
|
||||||
|
- interaction: writes
|
||||||
|
- via: repository
|
||||||
|
- purpose: сохранение состояния заказа
|
||||||
|
- details:
|
||||||
|
- transaction: required
|
||||||
|
```
|
||||||
|
|
||||||
|
Этот блок должен быть пригоден и для чтения человеком, и для последующего извлечения в отдельный RAG-слой интеграций.
|
||||||
|
|
||||||
|
## 1.10. Общие требования к markdown body
|
||||||
|
|
||||||
|
1. В документе должен быть один `H1`, совпадающий с `title`.
|
||||||
|
2. Основные разделы используют `H2`.
|
||||||
|
3. Подразделы внутри разделов используют `H3`.
|
||||||
|
4. Не должно быть хаотической вложенности заголовков.
|
||||||
|
5. Один раздел должен описывать одну смысловую часть.
|
||||||
|
6. Текст не должен дублировать соседние документы.
|
||||||
|
7. Вместо дублирования должны использоваться явные ссылки на связанные документы.
|
||||||
|
8. Сценарии, правила, ограничения и ссылки на код должны быть отделены друг от друга.
|
||||||
|
|
||||||
|
## 1.11. Базовый каркас markdown-документа
|
||||||
|
|
||||||
|
```md
|
||||||
|
---
|
||||||
|
id: api-orders-create
|
||||||
|
title: Метод создания заказа
|
||||||
|
doc_type: api_method
|
||||||
|
domain: orders
|
||||||
|
status: draft
|
||||||
|
source_of_truth: code
|
||||||
|
related_docs:
|
||||||
|
- logic-order-validation
|
||||||
|
- ui-order-create-page
|
||||||
|
related_code:
|
||||||
|
- src/orders/api/create_order.py
|
||||||
|
entities:
|
||||||
|
- Order
|
||||||
|
- CreateOrder
|
||||||
|
tags:
|
||||||
|
- api
|
||||||
|
- orders
|
||||||
|
---
|
||||||
|
|
||||||
|
# Метод создания заказа
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
- Purpose: создание заказа
|
||||||
|
- Actor: пользователь
|
||||||
|
- Trigger: submit формы
|
||||||
|
- Main API: POST /orders
|
||||||
|
|
||||||
|
## Details
|
||||||
|
### Описание
|
||||||
|
### Технический use case
|
||||||
|
### Функциональные требования
|
||||||
|
### Нефункциональные требования
|
||||||
|
### Контракт
|
||||||
|
|
||||||
|
|
||||||
|
## 3.13. Специализированные шаблоны документов
|
||||||
|
|
||||||
|
### UI Page
|
||||||
|
|
||||||
|
```md
|
||||||
|
# <Название страницы>
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
## Назначение
|
||||||
|
## Контекст
|
||||||
|
## Технический use case
|
||||||
|
## Описание UI
|
||||||
|
## UI Elements
|
||||||
|
## Функциональные требования
|
||||||
|
## Нефункциональные требования
|
||||||
|
## Связанные API
|
||||||
|
## Связанные блоки логики
|
||||||
|
## Связанный код
|
||||||
|
## Связанные документы
|
||||||
|
## История изменений
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Требования к разделу `Описание UI`
|
||||||
|
Для каждого элемента желательно описывать:
|
||||||
|
- тип элемента;
|
||||||
|
- назначение;
|
||||||
|
- источник данных;
|
||||||
|
- default / placeholder;
|
||||||
|
- правила активации;
|
||||||
|
- поведение при взаимодействии;
|
||||||
|
- валидацию.
|
||||||
|
|
||||||
|
#### Требования к разделу `UI Elements`
|
||||||
|
UI-элементы должны храниться в **табличном** или **полуструктурированном** виде.
|
||||||
|
|
||||||
|
Пример:
|
||||||
|
|
||||||
|
```md
|
||||||
|
## UI Elements
|
||||||
|
|
||||||
|
| id | type | label | data_source | validation | behavior |
|
||||||
|
|--------|--------|---------|------------|------------|----------|
|
||||||
|
| amount | input | Amount | local | >0 | enables submit |
|
||||||
|
| submit | button | Create | - | - | calls api.create_invoice |
|
||||||
|
```
|
||||||
|
|
||||||
|
Если модель UI сложная, допустим sidecar-файл `ui_elements.yaml` или `ui_elements.json` рядом с основным документом.
|
||||||
|
|
||||||
|
### API Method
|
||||||
|
|
||||||
|
```md
|
||||||
|
# <Название API метода>
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
## Назначение
|
||||||
|
## Контекст
|
||||||
|
## Технический use case
|
||||||
|
## Функциональные требования
|
||||||
|
## Contract
|
||||||
|
## Integrations
|
||||||
|
## Errors
|
||||||
|
## Нефункциональные требования
|
||||||
|
## Связанные блоки логики
|
||||||
|
## Связанный код
|
||||||
|
## Связанные документы
|
||||||
|
## История изменений
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Требования к разделу `Contract`
|
||||||
|
Контракт может:
|
||||||
|
- быть кратко описан прямо в документе;
|
||||||
|
- ссылаться на OpenAPI;
|
||||||
|
- ссылаться на отдельный контрактный файл.
|
||||||
|
|
||||||
|
Для REST API целевым источником истины должен становиться `OpenAPI`.
|
||||||
|
|
||||||
|
### Reusable Logic Block
|
||||||
|
|
||||||
|
```md
|
||||||
|
# <Название блока логики>
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
## Назначение
|
||||||
|
## Контекст
|
||||||
|
## Технический use case
|
||||||
|
## Функциональные требования
|
||||||
|
## Integrations
|
||||||
|
## Ограничения и условия вызова
|
||||||
|
## Нефункциональные требования
|
||||||
|
## Связанные API / UI / integration points
|
||||||
|
## Связанный код
|
||||||
|
## Связанные документы
|
||||||
|
## История изменений
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3.14. Машинно-читаемые API-контракты
|
||||||
|
|
||||||
|
Для API контрактов **источником истины** должен становиться:
|
||||||
|
- `OpenAPI` — предпочтительно;
|
||||||
|
- либо временно строгий markdown/yaml-контракт, если OpenAPI еще нет.
|
||||||
|
|
||||||
|
Минимальный набор для API-контракта:
|
||||||
|
- `endpoint`
|
||||||
|
- `method`
|
||||||
|
- `request fields`
|
||||||
|
- `required / optional`
|
||||||
|
- `constraints`
|
||||||
|
- `response`
|
||||||
|
- `errors`
|
||||||
|
- `idempotency`
|
||||||
|
- `retry`
|
||||||
|
- `timeout`
|
||||||
|
- `auth`
|
||||||
|
|
||||||
|
## 3.15. Каталог ошибок
|
||||||
|
|
||||||
|
Ошибки, HTTP-коды, retry-правила и клиентское поведение не должны размазываться по разным документам.
|
||||||
|
|
||||||
|
Нужен единый каталог ошибок, например `docs/errors/catalog.yaml`.
|
||||||
|
|
||||||
|
Пример:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
errors:
|
||||||
|
- error_id: invoice_validation_failed
|
||||||
|
http_code: 400
|
||||||
|
internal_code: BILLING_400_01
|
||||||
|
when: invalid request fields
|
||||||
|
client_behavior: show field errors
|
||||||
|
retry: false
|
||||||
|
owner: billing
|
||||||
|
|
||||||
|
- error_id: invoice_duplicate
|
||||||
|
http_code: 409
|
||||||
|
internal_code: BILLING_409_01
|
||||||
|
when: duplicate invoice detected
|
||||||
|
client_behavior: show duplicate warning
|
||||||
|
retry: false
|
||||||
|
owner: billing
|
||||||
|
|
||||||
|
- error_id: crm_sync_unavailable
|
||||||
|
http_code: 503
|
||||||
|
internal_code: BILLING_503_02
|
||||||
|
when: downstream CRM unavailable
|
||||||
|
client_behavior: retry later
|
||||||
|
retry: true
|
||||||
|
owner: billing
|
||||||
|
```
|
||||||
|
|
||||||
|
В API- и logic-документах лучше ссылаться на `error_id`, а не заново подробно описывать каждую ошибку.
|
||||||
|
|
||||||
|
## 3.16. Требования к качеству документа для RAG
|
||||||
|
|
||||||
|
1. **Явные заголовки** — не использовать безымянные блоки текста.
|
||||||
|
2. **Атомарные утверждения** — не смешивать несколько правил в одном пункте, если их можно разделить.
|
||||||
|
3. **Явные сущности** — использовать стабильные названия компонентов, API, модулей, страниц.
|
||||||
|
4. **Явные ссылки** — не писать «этот метод», если можно указать конкретную ссылку или идентификатор.
|
||||||
|
5. **Минимум дублирования** — повторяющийся контент должен заменяться ссылками.
|
||||||
|
6. **Привязка к коду** — если документ описывает кодовый объект, это должно быть явно указано.
|
||||||
|
7. **Разделение сценариев и правил** — workflow и fact-like требования должны быть отделены.
|
||||||
|
|
||||||
|
## 3.17. Как структура markdown помогает RAG
|
||||||
|
|
||||||
|
- `frontmatter` + заголовки → `D1_DOCUMENT_CATALOG`
|
||||||
|
- `entities`, `tags`, устойчивые термины → `D3_ENTITY_CATALOG`
|
||||||
|
- атомарные функциональные и нефункциональные требования → `D2_FACT_INDEX`
|
||||||
|
- `Технический use case` → `D4_WORKFLOW_INDEX`
|
||||||
|
- `related_docs`, явные ссылки → `D5_REFERENCE_GRAPH`
|
||||||
|
- `related_code`, упоминания symbols и файлов → `D6_DOC_CODE_LINKS`
|
||||||
|
- `Summary` → быстрый retrieval и short-form context для LLM
|
||||||
|
|
||||||
|
## 3.18. Принципы генерации документации агентом
|
||||||
|
|
||||||
|
Когда документ пишет агент, он должен:
|
||||||
|
- сначала извлечь evidence из кода, системной аналитики и существующих документов;
|
||||||
|
- определить тип документа;
|
||||||
|
- заполнить frontmatter;
|
||||||
|
- построить markdown body по шаблону;
|
||||||
|
- явно указать связи с кодом и другими документами;
|
||||||
|
- не дублировать уже существующее описание, если можно сослаться на него.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
## 4.4. Layered RAG
|
||||||
|
|
||||||
|
RAG строится как система специализированных слоев для двух основных доменов:
|
||||||
|
- `CODE RAG`
|
||||||
|
- `DOCS RAG`
|
||||||
|
|
||||||
|
Каждый graph извлекает контекст не из одного общего индекса, а из нужного набора слоев в зависимости от intent.
|
||||||
|
|
||||||
|
## 4.5. Evidence gate
|
||||||
|
|
||||||
|
Перед синтезом ответа или документа агент должен проверять, хватает ли опоры.
|
||||||
|
|
||||||
|
Примеры:
|
||||||
|
- найден ли symbol;
|
||||||
|
- найдено ли достаточное количество code chunks;
|
||||||
|
- есть ли supporting relations;
|
||||||
|
- есть ли document evidence;
|
||||||
|
- есть ли docs ↔ code mapping.
|
||||||
|
|
||||||
|
Если опоры недостаточно, агент должен:
|
||||||
|
- деградировать в упрощенный режим;
|
||||||
|
- честно фиксировать неполноту ответа;
|
||||||
|
- при необходимости уходить в fallback.
|
||||||
|
|
||||||
|
## 4.6. Synthesis layer
|
||||||
|
|
||||||
|
На этом этапе LLM:
|
||||||
|
- агрегирует найденные артефакты;
|
||||||
|
- формирует объяснение;
|
||||||
|
- пишет документ;
|
||||||
|
- структурирует результат под нужный шаблон.
|
||||||
|
|
||||||
|
LLM не должна быть основным источником фактов. Фактическая основа должна приходить из RAG и диагностируемого pipeline.
|
||||||
|
|
||||||
|
## 4.7. Diagnostics
|
||||||
|
|
||||||
|
Система должна сохранять диагностический след:
|
||||||
|
- какой graph был выбран;
|
||||||
|
- какие слои использовались;
|
||||||
|
- что было найдено;
|
||||||
|
- где retrieval был слабым;
|
||||||
|
- почему был выбран fallback;
|
||||||
|
- какие evidence стали основой ответа.
|
||||||
|
|
||||||
|
## 4.8. Сценарии: Target Architecture vs MVP-now
|
||||||
|
|
||||||
|
### 4.8.1. Target Architecture
|
||||||
|
|
||||||
|
#### CODE
|
||||||
|
- `OPEN_FILE` — открыть конкретный файл;
|
||||||
|
- `OPEN_SYMBOL` — открыть класс / функцию / метод;
|
||||||
|
- `EXPLAIN` — объяснить, как работает сущность или фрагмент;
|
||||||
|
- `FIND_TESTS` — найти релевантные тесты;
|
||||||
|
- `FIND_ENTRYPOINTS` — найти основные точки входа;
|
||||||
|
- `RELATED_CODE` — найти связанные сущности и ближайший контекст.
|
||||||
|
|
||||||
|
#### DOCS
|
||||||
|
- `DOC_SEARCH` — найти релевантный фрагмент документации;
|
||||||
|
- `DOC_EXPLAIN` — кратко объяснить, что сказано в документации по теме;
|
||||||
|
- `DOC_ENTITY_LOOKUP` — найти разделы, связанные с сущностью или компонентом;
|
||||||
|
- `GENERATE_DOCS_FROM_CODE` — сформировать документацию по коду с нуля для модуля, класса, функции, компонента или сценария.
|
||||||
|
|
||||||
|
#### CROSS-DOMAIN
|
||||||
|
- `FIND_IMPLEMENTATION_BY_DOC` — найти реализацию по описанию;
|
||||||
|
- `FIND_DOC_BY_CODE` — найти документацию по коду;
|
||||||
|
- `COMPARE_DOCS_AND_CODE` — базовое сопоставление документации и реализации.
|
||||||
|
|
||||||
|
#### GENERAL / FALLBACK
|
||||||
|
- `GENERAL_QA` — общий сценарий ответа на вопрос, если домен или интент не удалось определить уверенно.
|
||||||
|
|
||||||
|
### 4.8.2. MVP-now
|
||||||
|
|
||||||
|
В текущем цикле фокус на сценариях:
|
||||||
|
|
||||||
|
- `OPEN_FILE`
|
||||||
|
- `EXPLAIN`
|
||||||
|
- `FIND_TESTS`
|
||||||
|
- `FIND_ENTRYPOINTS`
|
||||||
|
- `GENERAL_QA`
|
||||||
|
|
||||||
|
DOCS и CROSS_DOMAIN остаются частью target architecture; в текущем цикле они не являются обязательной частью test-first MVP.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Структура слоев RAG
|
||||||
|
|
||||||
|
## 5.1. CODE RAG
|
||||||
|
|
||||||
|
### C0 — Source Chunks
|
||||||
|
**Назначение:** базовые фрагменты исходного кода.
|
||||||
|
**Единица:** chunk кода.
|
||||||
|
**Как формируется:** исходные файлы обходятся и режутся на chunk’и с учетом структурных границ.
|
||||||
|
**Статус в MVP:** да.
|
||||||
|
|
||||||
|
### C1 — Symbol Catalog
|
||||||
|
**Назначение:** каталог модулей, классов, функций, методов и других значимых сущностей.
|
||||||
|
**Единица:** symbol.
|
||||||
|
**Как формируется:** из AST и синтаксического разбора кода.
|
||||||
|
**Статус в MVP:** да.
|
||||||
|
|
||||||
|
### C2 — Symbol Relations
|
||||||
|
**Назначение:** связи между symbols.
|
||||||
|
**Единица:** relation.
|
||||||
|
**Как формируется:** вторым проходом по AST и структурным зависимостям.
|
||||||
|
**Статус в MVP:** да, в ограниченном виде.
|
||||||
|
|
||||||
|
### C3 — Entrypoints
|
||||||
|
**Назначение:** каталог точек входа системы.
|
||||||
|
**Единица:** entrypoint.
|
||||||
|
**Как формируется:** специализированными детекторами entrypoint-паттернов.
|
||||||
|
**Статус в MVP:** да, минимально.
|
||||||
|
|
||||||
|
### C4 — Execution Paths
|
||||||
|
**Назначение:** типовые пути исполнения.
|
||||||
|
**Единица:** path.
|
||||||
|
**Как формируется:** поверх `C2` и `C3` через производную трассировку.
|
||||||
|
**Статус в MVP:** нет.
|
||||||
|
|
||||||
|
### C5 — Test Mappings
|
||||||
|
**Назначение:** связи production code ↔ tests.
|
||||||
|
**Единица:** mapping.
|
||||||
|
**Как формируется:** по путям, именам, импортам и конвенциям проекта.
|
||||||
|
**Статус в MVP:** да, минимально.
|
||||||
|
|
||||||
|
### C6 — Code Facts
|
||||||
|
**Назначение:** нормализованные факты из кода.
|
||||||
|
**Единица:** fact.
|
||||||
|
**Как формируется:** поверх `C1–C3` как производный слой.
|
||||||
|
**Статус в MVP:** нет.
|
||||||
|
|
||||||
|
## 5.2. DOCS RAG
|
||||||
|
|
||||||
|
### D0 — Document Chunks
|
||||||
|
**Назначение:** базовые фрагменты документации.
|
||||||
|
**Единица:** document chunk.
|
||||||
|
**Как формируется:** документы нормализуются и режутся на chunk’и с сохранением `section path`.
|
||||||
|
**Статус в MVP:** да.
|
||||||
|
|
||||||
|
### D1 — Document Catalog
|
||||||
|
**Назначение:** каталог документов и разделов.
|
||||||
|
**Единица:** `document node / section node`.
|
||||||
|
**Как формируется:** из структуры документов и их заголовков.
|
||||||
|
**Статус в MVP:** да.
|
||||||
|
|
||||||
|
### D2 — Fact Index
|
||||||
|
**Назначение:** атомарные факты из документации.
|
||||||
|
**Единица:** fact.
|
||||||
|
**Как формируется:** из `D0/D1` через правила, шаблоны и при необходимости LLM extraction с валидацией.
|
||||||
|
**Статус в MVP:** частично.
|
||||||
|
|
||||||
|
### D3 — Entity Catalog
|
||||||
|
**Назначение:** каталог сущностей и понятий документации.
|
||||||
|
**Единица:** entity / concept.
|
||||||
|
**Как формируется:** из устойчивых терминов, заголовков, словарей и нормализации повторяющихся сущностей.
|
||||||
|
**Статус в MVP:** да, минимально.
|
||||||
|
|
||||||
|
### D4 — Workflow Index
|
||||||
|
**Назначение:** процедуры, сценарии, последовательности шагов.
|
||||||
|
**Единица:** workflow.
|
||||||
|
**Как формируется:** из use case, процессных разделов и последовательных описаний шагов.
|
||||||
|
**Статус в MVP:** нет.
|
||||||
|
|
||||||
|
### D5 — Reference Graph
|
||||||
|
**Назначение:** граф ссылок между документами, секциями, сущностями и фактами.
|
||||||
|
**Единица:** reference link.
|
||||||
|
**Как формируется:** из явных и неявных cross-links между документами.
|
||||||
|
**Статус в MVP:** нет.
|
||||||
|
|
||||||
|
### D6 — Doc-Code Links
|
||||||
|
**Назначение:** мост между документацией и кодом.
|
||||||
|
**Единица:** `doc artifact ↔ code artifact link`.
|
||||||
|
**Как формируется:** из имен, aliases, путей, устойчивых терминов и других надежных соответствий.
|
||||||
|
**Статус в MVP:** да, минимально.
|
||||||
|
|
||||||
|
## 5.3. Layer scope: Target Architecture vs MVP-now
|
||||||
|
|
||||||
|
### 5.3.1. Target Architecture
|
||||||
|
|
||||||
|
Полная карта слоёв:
|
||||||
|
|
||||||
|
- **CODE:** C0–C6 (Source Chunks, Symbol Catalog, Symbol Relations, Entrypoints, Execution Paths, Test Mappings, Code Facts)
|
||||||
|
- **DOCS:** D0–D6 (Document Chunks, Document Catalog, Fact Index, Entity Catalog, Workflow Index, Reference Graph, Doc-Code Links)
|
||||||
|
|
||||||
|
### 5.3.2. MVP-now
|
||||||
|
|
||||||
|
**Обязательные сейчас:**
|
||||||
|
|
||||||
|
- `C0_SOURCE_CHUNKS`
|
||||||
|
- `C1_SYMBOL_CATALOG`
|
||||||
|
- `C2_SYMBOL_RELATIONS`
|
||||||
|
- `C3_ENTRYPOINTS`
|
||||||
|
|
||||||
|
**В облегчённом виде:**
|
||||||
|
|
||||||
|
- `C5_TEST_MAPPINGS` или `C5-lite`
|
||||||
|
|
||||||
|
**Не блокируют текущий этап:**
|
||||||
|
|
||||||
|
- `C4_EXECUTION_PATHS`
|
||||||
|
- `C6_CODE_FACTS`
|
||||||
|
- весь docs runtime (слои D0–D6 в исполнении runtime)
|
||||||
|
|
||||||
|
Слои документации остаются частью target architecture; docs retrieval пока не обязателен для текущего code-first milestone.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Итоговая рамка MVP-now
|
||||||
|
|
||||||
|
Сейчас система должна стабильно работать в **test-first** режиме.
|
||||||
|
|
||||||
|
**Фокус:**
|
||||||
|
|
||||||
|
- CODE_QA;
|
||||||
|
- через тесты настраиваются:
|
||||||
|
- intent routing (IntentRouterV2);
|
||||||
|
- layered retrieval;
|
||||||
|
- evidence sufficiency;
|
||||||
|
- answer quality;
|
||||||
|
- diagnostics.
|
||||||
|
|
||||||
|
**Не входят в текущий milestone:**
|
||||||
|
|
||||||
|
- UI-интеграция;
|
||||||
|
- docs runtime;
|
||||||
|
- полная интеграция orchestration переносится на следующий этап после стабилизации test pipeline.
|
||||||
|
|
||||||
|
В целевой архитектуре по-прежнему заложены:
|
||||||
|
- уверенная работа с кодом, symbols, entrypoints, тестами;
|
||||||
|
- ответ по документации и мост docs ↔ code;
|
||||||
|
- генерация документации по коду;
|
||||||
|
- fallback при неуверенном роутинге.
|
||||||
|
|
||||||
|
В MVP-now сознательно **не включаются** самые дорогие части:
|
||||||
|
- полноценные execution paths для всей системы;
|
||||||
|
- богатые fact-индексы по всем доменам;
|
||||||
|
- полный reference graph документации;
|
||||||
|
- глубокая автоматизация подготовки системной аналитики.
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
Нужно реализовать 2 вещи
|
|
||||||
|
|
||||||
Создать процесс внесения изменений в файл документации
|
|
||||||
Создать контекст этого процесса
|
|
||||||
|
|
||||||
Контекст наполнять атрибутами
|
|
||||||
что-то явно задано, фоллбэк через ллм
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Написать тестовую аналитику - круд над сущностью
|
|
||||||
фронт, ефс, ппрб
|
|
||||||
Все в своей БД
|
|
||||||
Атрибуты сущности задать в требованиях
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Аналитика имеет структуру
|
|
||||||
Внутри модули - один модуль на правку одного файла.
|
|
||||||
|
|
||||||
|
|
||||||
Модуль извлекается из аналитики парсером и из него формируется задача на редактирование файла
|
|
||||||
если парсер не сработал - фоллбэк ан ллм
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Процесс редактирования работает стандартно
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
# Documentation Rules V3
|
|
||||||
|
|
||||||
Этот каталог содержит правила генерации технической документации из системной аналитики.
|
|
||||||
|
|
||||||
## Цель
|
|
||||||
- синхронизировать требования к документации с требованиями к аналитике (`04. Analitycs artefacts - features.md`);
|
|
||||||
- сохранить детальность техдокументации по сравнению с аналитикой;
|
|
||||||
- убрать дублирование структуры и manifest-слоя между разными файлами;
|
|
||||||
- собирать итоговый промпт из модулей: глобальные правила + template с manifest + блоки.
|
|
||||||
|
|
||||||
## Структура
|
|
||||||
- `documentation-rules.md` — верхнеуровневый регламент и порядок сборки.
|
|
||||||
- `global/` — общие правила (заголовки, frontmatter, слой ответственности, мост аналитика->документация).
|
|
||||||
- `common-elements/` — правила для общих блоков (`summary`, `details`, `use case`, `FR`, `NFR`, `UI`, `Contract`).
|
|
||||||
- `templates/` — единственный источник истины для структуры итоговой страницы и manifest-метаданных типа документа.
|
|
||||||
|
|
||||||
## Принцип сборки
|
|
||||||
Для конкретного документа агент собирает единый набор правил из:
|
|
||||||
1. `documentation-rules.md`
|
|
||||||
2. `global/*.md`
|
|
||||||
3. `templates/<doc_type>.template.md`
|
|
||||||
4. `common-elements/*.md`, указанных в frontmatter template
|
|
||||||
|
|
||||||
## Правило без дублирования
|
|
||||||
- `templates/` отвечают за структуру документа, порядок разделов и manifest-метаданные типа.
|
|
||||||
- `common-elements/` отвечают только за правила написания конкретного раздела.
|
|
||||||
- отдельный слой `types/` не нужен, если для типа документа используется один основной template.
|
|
||||||
|
|
||||||
## Формат template-manifest
|
|
||||||
Manifest оформляется в YAML frontmatter самого template.
|
|
||||||
|
|
||||||
Обязательные поля manifest:
|
|
||||||
- `doc_type`
|
|
||||||
- `required_common_elements`
|
|
||||||
|
|
||||||
Рекомендуемые поля:
|
|
||||||
- `special_rules`
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
# API Contract Rules
|
|
||||||
|
|
||||||
Этот rule описывает только тело секции `### Контракт`.
|
|
||||||
|
|
||||||
## Обязательные части
|
|
||||||
- request parameters (`header/query/path`)
|
|
||||||
- request body (если применимо)
|
|
||||||
- response body
|
|
||||||
- errors
|
|
||||||
- auth
|
|
||||||
- timeout
|
|
||||||
- retry/idempotency (если применимо)
|
|
||||||
|
|
||||||
## Правила заголовков внутри тела секции
|
|
||||||
- Не повторять заголовок `Контракт`.
|
|
||||||
- Запрещено выводить `## Контракт` и `### Контракт` внутри тела секции.
|
|
||||||
- Если нужны подзаголовки, использовать только уровень ниже родительской секции: `#### Запрос`, `#### Ответ`, `#### Ошибки`, `#### Auth`, `#### Timeout`, `#### Retry/Idempotency`.
|
|
||||||
|
|
||||||
## Табличный формат
|
|
||||||
Для request/response таблицы должны содержать:
|
|
||||||
- название
|
|
||||||
- тип данных
|
|
||||||
- обязательность
|
|
||||||
- описание
|
|
||||||
- пример
|
|
||||||
|
|
||||||
Для response дополнительно:
|
|
||||||
- заполнение (mapping/логика источника данных)
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
# DB Columns Rules
|
|
||||||
|
|
||||||
## Формат
|
|
||||||
Структура таблицы оформляется таблицей.
|
|
||||||
|
|
||||||
## Обязательные колонки
|
|
||||||
- `Поле`
|
|
||||||
- `Тип`
|
|
||||||
- `Nullable`
|
|
||||||
- `Описание`
|
|
||||||
- `Источник заполнения`
|
|
||||||
- `Использование`
|
|
||||||
|
|
||||||
## Правила
|
|
||||||
- перечислять все ключевые поля таблицы;
|
|
||||||
- для служебных полей (`id`, `created_at`, `updated_at`, `deleted_at`) явно описывать назначение;
|
|
||||||
- если тип или nullable не заданы в аналитике, допускается инженерное предположение с рабочим вариантом.
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
# DB Constraints Rules
|
|
||||||
|
|
||||||
## Что включать
|
|
||||||
- primary key;
|
|
||||||
- unique constraints;
|
|
||||||
- foreign keys;
|
|
||||||
- важные индексы;
|
|
||||||
- бизнес-ограничения на уровне БД.
|
|
||||||
|
|
||||||
## Формат
|
|
||||||
- списком или таблицей;
|
|
||||||
- для каждого индекса и ограничения писать, зачем оно нужно.
|
|
||||||
|
|
||||||
## Правила
|
|
||||||
- если индекс нужен для сценария чтения/пагинации, это должно быть явно сказано;
|
|
||||||
- если точные названия индексов неизвестны, можно использовать осмысленные проектные названия.
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
# DB Table Purpose Rules
|
|
||||||
|
|
||||||
## Что описывать
|
|
||||||
- назначение таблицы;
|
|
||||||
- в каком сценарии она используется;
|
|
||||||
- кто является владельцем данных;
|
|
||||||
- является ли таблица источником истины или производным хранилищем.
|
|
||||||
|
|
||||||
## Формат
|
|
||||||
- 1-3 абзаца без воды;
|
|
||||||
- явно указывать доменную сущность, которую хранит таблица;
|
|
||||||
- если сделаны допущения по БД, фиксировать их отдельной фразой.
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
# DB Usage Rules
|
|
||||||
|
|
||||||
## Что описывать
|
|
||||||
- какие API / logic block / batch job используют таблицу;
|
|
||||||
- какие операции выполняются: read / insert / update / delete;
|
|
||||||
- как таблица участвует в пользовательском сценарии.
|
|
||||||
|
|
||||||
## Правила
|
|
||||||
- ссылки на связанные документы давать по `doc_id` или path;
|
|
||||||
- не дублировать полный use case, а показывать роль таблицы в сценарии;
|
|
||||||
- если таблица используется для пагинации, фильтрации или сортировки, это нужно отметить явно.
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
# Details Rules
|
|
||||||
|
|
||||||
## Назначение
|
|
||||||
Этот файл задает общие правила для секции `## Details`.
|
|
||||||
|
|
||||||
## Правила
|
|
||||||
- `Details` оформляется как `## Details`.
|
|
||||||
- Внутри `Details` используются заголовки уровня `###` и ниже.
|
|
||||||
- Структура `Details` определяется template типа документа.
|
|
||||||
- В `Details` не нужно дублировать навигацию и связи, если они уже есть во frontmatter.
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
# Functional Requirements Rules
|
|
||||||
|
|
||||||
Этот rule описывает только тело секции `### Функциональные требования`.
|
|
||||||
|
|
||||||
## Формат
|
|
||||||
- `FR.<номер>. <Название>`
|
|
||||||
- Нумерация инкрементальная внутри документа.
|
|
||||||
|
|
||||||
## Правила
|
|
||||||
- FR расширяют шаги сценария.
|
|
||||||
- FR не копируют шаги сценария без добавления новой информации.
|
|
||||||
- Для интеграционных шагов FR обязательны.
|
|
||||||
- Если в сценарии есть вызов внешнего API / сервиса / БД, нужен отдельный FR на интеграцию.
|
|
||||||
- Запрещено повторять заголовок `### Функциональные требования` внутри тела секции.
|
|
||||||
|
|
||||||
## FR для интеграционных шагов
|
|
||||||
Для интеграционного FR обязательно раскрывать:
|
|
||||||
- как формируется запрос;
|
|
||||||
- откуда берется каждый значимый атрибут запроса;
|
|
||||||
- какой downstream вызывается;
|
|
||||||
- какой ответ считается успешным;
|
|
||||||
- какие ответы и ситуации считаются бизнес-ошибкой;
|
|
||||||
- какие ситуации считаются технической ошибкой;
|
|
||||||
- как downstream-ответ маппится в контракт текущего слоя.
|
|
||||||
|
|
||||||
## FR для шагов доступа к БД
|
|
||||||
Если шаг читает или пишет БД, FR должен по возможности включать:
|
|
||||||
- таблицу или набор таблиц;
|
|
||||||
- логику фильтрации;
|
|
||||||
- логику сортировки;
|
|
||||||
- логику пагинации;
|
|
||||||
- пример SQL или близкий к рабочему псевдо-SQL.
|
|
||||||
|
|
||||||
Если СУБД и диалект не заданы, допускается сделать рабочее предположение и явно зафиксировать его.
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
# Non-Functional Requirements Rules
|
|
||||||
|
|
||||||
## Для api_method
|
|
||||||
- Подразделы:
|
|
||||||
- `#### Аудит` (если применимо)
|
|
||||||
- `#### Мониторинг`
|
|
||||||
|
|
||||||
## Мониторинг
|
|
||||||
Оформлять таблицей:
|
|
||||||
- `Метрика`
|
|
||||||
- `Описание`
|
|
||||||
- `Условие срабатывания`
|
|
||||||
|
|
||||||
Запрещено:
|
|
||||||
- использовать «точка измерения = метод» вместо условий срабатывания.
|
|
||||||
|
|
||||||
Базовые суффиксы метрик:
|
|
||||||
- `_SUCCESS`
|
|
||||||
- `_FAIL`
|
|
||||||
- `_BUSINESS_ERROR`
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
# SQL Example Rules
|
|
||||||
|
|
||||||
## Назначение
|
|
||||||
Секция показывает пример рабочего SQL для основного сценария использования таблицы.
|
|
||||||
|
|
||||||
## Правила
|
|
||||||
- SQL должен быть близок к рабочему, а не абстрактным псевдокодом;
|
|
||||||
- если диалект БД не указан, допускается выбрать наиболее вероятный вариант и явно зафиксировать допущение;
|
|
||||||
- пример должен отражать реальный сценарий документа: чтение, вставка, обновление или агрегация;
|
|
||||||
- для read-сценариев по возможности показывать фильтрацию, сортировку и пагинацию;
|
|
||||||
- если есть join, нужно кратко пояснить, зачем он нужен.
|
|
||||||
|
|
||||||
## Формат
|
|
||||||
- fenced code block с указанием `sql`;
|
|
||||||
- под кодом 1-3 поясняющих bullets о ключевых условиях, индексах и параметрах.
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
# Summary Rules
|
|
||||||
|
|
||||||
## Назначение
|
|
||||||
Этот файл задает правила для секции `## Summary`.
|
|
||||||
|
|
||||||
## Правила
|
|
||||||
- `Summary` должен быть коротким слоем быстрого контекста.
|
|
||||||
- `Summary` должен объяснять суть документа без длинных деталей.
|
|
||||||
- Предпочтительный формат: краткий список ключевых фактов.
|
|
||||||
- `Summary` не должен дублировать `Details`.
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
# Tech Use Case Rules
|
|
||||||
|
|
||||||
Этот rule описывает только тело секции `### Технический use case`.
|
|
||||||
|
|
||||||
## Обязательные части
|
|
||||||
- название
|
|
||||||
- предусловия
|
|
||||||
- триггер
|
|
||||||
- основной сценарий
|
|
||||||
- альтернативный сценарий
|
|
||||||
- обработка ошибок
|
|
||||||
- постусловие
|
|
||||||
|
|
||||||
## Правила шага
|
|
||||||
- Один шаг = одно предложение до 15-20 слов.
|
|
||||||
- Формат шага: смысловое действие + техническая реализация (endpoint/топик/операция).
|
|
||||||
- Длинные технические детали выносить в FR и ссылаться на FR из шага.
|
|
||||||
- Для интеграционных шагов описание обработки ошибок обязательно.
|
|
||||||
- Запрещено повторять заголовок `### Технический use case` внутри тела секции.
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
# UI Requirements Rules
|
|
||||||
|
|
||||||
## Структура блока
|
|
||||||
- `### Требования к UI`
|
|
||||||
- Внутри обязательно отдельные формы:
|
|
||||||
- табличное представление
|
|
||||||
- пустой список (empty state)
|
|
||||||
- ошибка (error state)
|
|
||||||
|
|
||||||
## Обязательные правила
|
|
||||||
- Если есть интеграция, обязательно описывать показ ошибки.
|
|
||||||
- Если есть список, обязательно описывать показ отсутствия данных.
|
|
||||||
|
|
||||||
## Описание UI-элементов
|
|
||||||
UI-элементы описываются строго в таблице.
|
|
||||||
|
|
||||||
Обязательные колонки (где применимо):
|
|
||||||
- `Код элемента`
|
|
||||||
- `Название и описание`
|
|
||||||
- `Данные`
|
|
||||||
- `Поведение`
|
|
||||||
- `Валидация`
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
# User Analytics Rules
|
|
||||||
|
|
||||||
События пользовательской аналитики оформлять таблицей:
|
|
||||||
- `Название события`
|
|
||||||
- `Описание`
|
|
||||||
- `Точка вызова`
|
|
||||||
- `Payload`
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
# Documentation Rules V3
|
|
||||||
|
|
||||||
## 1. Общий контракт
|
|
||||||
- Документация строится на основе системной аналитики, но на более детальном уровне.
|
|
||||||
- Заголовки отражают только суть раздела; метаданные в заголовках запрещены.
|
|
||||||
- Метаданные указываются во frontmatter и/или отдельными строками в body.
|
|
||||||
- Структура документа определяется только template соответствующего типа.
|
|
||||||
- Правила написания конкретного раздела определяются только соответствующим `common-elements` файлом.
|
|
||||||
- Manifest типа документа хранится во frontmatter соответствующего template.
|
|
||||||
- Генератор секции всегда пишет только тело секции, а не сам заголовок секции.
|
|
||||||
- Дублирование заголовков запрещено: нельзя повторно выводить заголовок текущей секции внутри ее тела.
|
|
||||||
- Если template уже содержит `### <Заголовок секции>`, то внутри тела допустимы только подзаголовки более глубокого уровня (`####` и ниже).
|
|
||||||
- Нельзя повышать уровень заголовка внутри тела секции до `##` или повторять `###` с тем же названием секции.
|
|
||||||
|
|
||||||
## 2. Источники требований
|
|
||||||
При генерации документа учитывать:
|
|
||||||
- `/Users/alex/Dev_projects_v2/ai driven app process/v2/agent/_process/04. Analitycs artefacts - documentation.md`
|
|
||||||
- `/Users/alex/Dev_projects_v2/ai driven app process/v2/agent/_process/04. Analitycs artefacts - features.md`
|
|
||||||
- правила v2 из `src/app/core/agent/processes/v2/doc_rules_v2`
|
|
||||||
|
|
||||||
## 3. Разрыв аналитика vs документация
|
|
||||||
- Аналитика: концептуальная, укрупненная.
|
|
||||||
- Документация: технически детальная.
|
|
||||||
- Технический use case в документации не копирует аналитический 1-в-1, а детализирует его.
|
|
||||||
- Функциональные требования расширяют сценарий и не дублируют шаги без новой информации.
|
|
||||||
|
|
||||||
## 4. Заполнение пробелов
|
|
||||||
Если атрибуты/детали отсутствуют в аналитике:
|
|
||||||
1. восстановить из формулировок аналитики;
|
|
||||||
2. уточнить по репозиторию (код, контракты, существующие документы);
|
|
||||||
3. зафиксировать в документации явно.
|
|
||||||
|
|
||||||
## 5. Сборка итогового промпта
|
|
||||||
1. Загрузить global-правила.
|
|
||||||
2. Загрузить template типа документа.
|
|
||||||
3. Прочитать YAML frontmatter template как manifest.
|
|
||||||
4. Загрузить общие блоки, указанные в manifest.
|
|
||||||
5. Применить body template как единственный источник структуры.
|
|
||||||
5. Проверить чек-лист совместимости с аналитикой (domain/sub_domain, роли слоев, интеграции, ошибки).
|
|
||||||
|
|
||||||
## 6. Специальные инварианты для `api_method`
|
|
||||||
- Во frontmatter обязательно должно присутствовать поле `endpoint`.
|
|
||||||
- Внутри `## Details` секция `### Контракт` должна присутствовать ровно один раз.
|
|
||||||
- Внутри тела секции `### Контракт` запрещено повторять заголовки `## Контракт` и `### Контракт`.
|
|
||||||
- Внутри `### Технический use case` запрещено повторять заголовок `### Технический use case`.
|
|
||||||
- Внутри `### Функциональные требования` запрещено повторять заголовок `### Функциональные требования`.
|
|
||||||
|
|
||||||
## 7. Формат manifest типа документа
|
|
||||||
Manifest типа документа хранится во frontmatter `templates/<doc_type>.template.md`.
|
|
||||||
|
|
||||||
Минимальная схема:
|
|
||||||
- `doc_type`
|
|
||||||
- `required_common_elements`
|
|
||||||
|
|
||||||
Дополнительно можно указывать:
|
|
||||||
- `special_rules`
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
# Analytics to Documentation Mapping
|
|
||||||
|
|
||||||
## Принцип
|
|
||||||
- Системная аналитика задает «что».
|
|
||||||
- Документация детализирует «как».
|
|
||||||
|
|
||||||
## Маппинг
|
|
||||||
- Из раздела архитектуры аналитики переносить контейнеры, интеграции и цепочки вызовов.
|
|
||||||
- Из раздела изменений аналитики строить отдельные технические страницы (`ui_page`, `api_method`, `logic_block`).
|
|
||||||
- Если в аналитике упрощенный use case, в документации раскрывать полный технический сценарий по правилам `tech-use-case.md`.
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
# Правила определения путей файлов
|
|
||||||
|
|
||||||
Текущая happy-path реализация строит путь документа по фиксированному шаблону:
|
|
||||||
|
|
||||||
`docs/<domain>/<platform>/<doc_type>/<doc_id>.md`
|
|
||||||
|
|
||||||
Пример:
|
|
||||||
|
|
||||||
`docs/orders/pprb/ui_page/orders.ui.list.md`
|
|
||||||
|
|
||||||
## Источники атрибутов
|
|
||||||
|
|
||||||
Для построения пути используются четыре основных атрибута:
|
|
||||||
|
|
||||||
- `domain`
|
|
||||||
- `application`
|
|
||||||
- `platform`
|
|
||||||
- `doc_type`
|
|
||||||
- `id` как `doc_id`
|
|
||||||
|
|
||||||
Если атрибуты явно указаны в подразделе `6.x`, нужно использовать их.
|
|
||||||
Если атрибут не указан, он может быть взят из общих метаданных аналитики или определен fallback-логикой.
|
|
||||||
|
|
||||||
## Нормализация сегментов
|
|
||||||
|
|
||||||
Каждый сегмент пути нормализуется одинаково:
|
|
||||||
|
|
||||||
- значение переводится в lowercase;
|
|
||||||
- все символы, кроме `a-z`, `0-9`, `.`, `_`, `-`, заменяются на `-`;
|
|
||||||
- ведущие и хвостовые `.` и `-` удаляются.
|
|
||||||
|
|
||||||
Примеры нормализации:
|
|
||||||
|
|
||||||
- `Payment Status` -> `payment-status`
|
|
||||||
- `UFS Orders` -> `ufs-orders`
|
|
||||||
- `crm.mobile` -> `crm.mobile`
|
|
||||||
|
|
||||||
## Значения по умолчанию
|
|
||||||
|
|
||||||
Если после нормализации сегмент пустой, используются fallback-значения:
|
|
||||||
|
|
||||||
- корневая папка: `domain`, иначе `application`, иначе `common`
|
|
||||||
- `platform` -> `web`
|
|
||||||
- `doc_type` -> `misc`
|
|
||||||
- `doc_id` -> `untitled`
|
|
||||||
|
|
||||||
## Что важно в текущей версии
|
|
||||||
|
|
||||||
- для корневой папки сначала используется `domain`;
|
|
||||||
- если `domain` не задан, используется `application`;
|
|
||||||
- `sub_domain` сейчас не участвует в построении пути;
|
|
||||||
- операции `create`, `update`, `delete` работают с одним и тем же правилом вычисления пути;
|
|
||||||
- специальных исключений для разных типов документов пока нет;
|
|
||||||
- отдельные каталоги для `pprb`, `ufs`, `web` задаются только через значение `platform`.
|
|
||||||
|
|
||||||
## Практическое правило для агента
|
|
||||||
|
|
||||||
Если нужно предложить или определить путь новой страницы, агент должен:
|
|
||||||
|
|
||||||
1. определить `application`;
|
|
||||||
2. определить `domain`;
|
|
||||||
3. определить `platform`;
|
|
||||||
4. определить `doc_type`;
|
|
||||||
5. определить стабильный `doc_id`;
|
|
||||||
6. взять корневую папку как `domain`, а если он пустой, то `application`;
|
|
||||||
7. нормализовать все сегменты;
|
|
||||||
8. собрать путь по шаблону `docs/<root>/<platform>/<doc_type>/<doc_id>.md`.
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
# Frontmatter Rules
|
|
||||||
|
|
||||||
## Обязательные поля
|
|
||||||
```yaml
|
|
||||||
id: string
|
|
||||||
title: string
|
|
||||||
doc_type: string
|
|
||||||
domain: string
|
|
||||||
sub_domain: string
|
|
||||||
related_docs: []
|
|
||||||
status: string
|
|
||||||
```
|
|
||||||
|
|
||||||
## Рекомендуемые поля
|
|
||||||
```yaml
|
|
||||||
tags: []
|
|
||||||
entities: []
|
|
||||||
source_of_truth: string
|
|
||||||
related_code: []
|
|
||||||
system_analytics_refs: []
|
|
||||||
```
|
|
||||||
|
|
||||||
## Дополнительные обязательные поля по типам документов
|
|
||||||
- Для `doc_type: api_method` поле `endpoint` обязательно.
|
|
||||||
- Значение `endpoint` должно содержать HTTP-метод и путь, например: `GET /orders/{orderId}`.
|
|
||||||
- Если в аналитике endpoint указан в заголовке раздела, use case, контракте или интеграционной схеме, его нужно перенести во frontmatter и не опускать.
|
|
||||||
|
|
||||||
## Body-метаданные для секции изменений
|
|
||||||
Под корнем секции изменений указывать:
|
|
||||||
- `domain`
|
|
||||||
- `sub_domain`
|
|
||||||
|
|
||||||
Для каждого подраздела `X.Y` указывать строками:
|
|
||||||
- `id`
|
|
||||||
- `doc_type`
|
|
||||||
- `application`
|
|
||||||
- `platform`
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
# Header Rules
|
|
||||||
|
|
||||||
## Правила
|
|
||||||
- Заголовок описывает только смысл раздела.
|
|
||||||
- Не включать в заголовок: `id`, `doc_type`, `application`, `platform`, `domain`, `sub_domain`.
|
|
||||||
- Метаданные указываются отдельными строками ниже заголовка или во frontmatter.
|
|
||||||
|
|
||||||
## Пример
|
|
||||||
- Правильно: `## 6.2 Метод UFS получения списка заказов`
|
|
||||||
- Неправильно: `## 6.2 Блок api_method (id=..., platform=ufs)`
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
# Layer Responsibility
|
|
||||||
|
|
||||||
- `ui`: отображение, UX, запуск пользовательских сценариев.
|
|
||||||
- `ufs`: авторизация/аутентификация, агрегация, маппинг, оркестрация вызовов.
|
|
||||||
- `pprb`: API, БД, доменная логика backend.
|
|
||||||
|
|
||||||
## Правила согласованности
|
|
||||||
- Проверка ролевой модели пользователя обычно фиксируется на уровне `ufs`.
|
|
||||||
- Если проверка роли вынесена в `ufs`, в `pprb`-сценарии не дублировать этот шаг.
|
|
||||||
- Аудит для `pprb` может отсутствовать, если это явно принято для домена/фичи.
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
---
|
|
||||||
doc_type: api_method
|
|
||||||
required_common_elements:
|
|
||||||
- common-elements/summary.md
|
|
||||||
- common-elements/details.md
|
|
||||||
- common-elements/tech-use-case.md
|
|
||||||
- common-elements/fr.md
|
|
||||||
- common-elements/nfr.md
|
|
||||||
- common-elements/api-contract.md
|
|
||||||
special_rules:
|
|
||||||
- Технический use case детализируется по `common-elements/tech-use-case.md`.
|
|
||||||
- FR расширяют use case и не дублируют шаги сценария без новой информации.
|
|
||||||
- Для интеграционных шагов FR обязательны.
|
|
||||||
---
|
|
||||||
|
|
||||||
# <title>
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
Правила оформления: `../common-elements/summary.md`
|
|
||||||
|
|
||||||
## Details
|
|
||||||
Правила оформления: `../common-elements/details.md`
|
|
||||||
|
|
||||||
### Технический use case
|
|
||||||
Правила оформления: `../common-elements/tech-use-case.md`
|
|
||||||
|
|
||||||
### Функциональные требования
|
|
||||||
Правила оформления: `../common-elements/fr.md`
|
|
||||||
|
|
||||||
### Нефункциональные требования
|
|
||||||
Правила оформления: `../common-elements/nfr.md`
|
|
||||||
|
|
||||||
### Контракт
|
|
||||||
Правила оформления: `../common-elements/api-contract.md`
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
---
|
|
||||||
doc_type: db_table
|
|
||||||
required_common_elements:
|
|
||||||
- common-elements/summary.md
|
|
||||||
- common-elements/details.md
|
|
||||||
- common-elements/db-purpose.md
|
|
||||||
- common-elements/db-columns.md
|
|
||||||
- common-elements/db-constraints.md
|
|
||||||
- common-elements/db-usage.md
|
|
||||||
- common-elements/sql-example.md
|
|
||||||
special_rules:
|
|
||||||
- Документ описывает одну физическую таблицу БД или materialized view.
|
|
||||||
- Нужно фиксировать назначение таблицы, поля, ограничения, индексы, связи и сценарии использования.
|
|
||||||
- Если точные детали БД не заданы, допустимо сделать рабочие инженерные допущения и явно записать их в документ.
|
|
||||||
---
|
|
||||||
|
|
||||||
# <title>
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
Правила оформления: `../common-elements/summary.md`
|
|
||||||
|
|
||||||
## Details
|
|
||||||
Правила оформления: `../common-elements/details.md`
|
|
||||||
|
|
||||||
### Назначение таблицы
|
|
||||||
Правила оформления: `../common-elements/db-purpose.md`
|
|
||||||
|
|
||||||
### Структура таблицы
|
|
||||||
Правила оформления: `../common-elements/db-columns.md`
|
|
||||||
|
|
||||||
### Ограничения и индексы
|
|
||||||
Правила оформления: `../common-elements/db-constraints.md`
|
|
||||||
|
|
||||||
### Использование в сценариях
|
|
||||||
Правила оформления: `../common-elements/db-usage.md`
|
|
||||||
|
|
||||||
### Пример SQL
|
|
||||||
Правила оформления: `../common-elements/sql-example.md`
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
---
|
|
||||||
doc_type: logic_block
|
|
||||||
required_common_elements:
|
|
||||||
- common-elements/summary.md
|
|
||||||
- common-elements/details.md
|
|
||||||
- common-elements/tech-use-case.md
|
|
||||||
- common-elements/fr.md
|
|
||||||
- common-elements/nfr.md
|
|
||||||
special_rules:
|
|
||||||
- Logic block описывает переиспользуемую логику без дублирования UI/API деталей.
|
|
||||||
---
|
|
||||||
|
|
||||||
# <title>
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
Правила оформления: `../common-elements/summary.md`
|
|
||||||
|
|
||||||
## Details
|
|
||||||
Правила оформления: `../common-elements/details.md`
|
|
||||||
|
|
||||||
### Технический use case
|
|
||||||
Правила оформления: `../common-elements/tech-use-case.md`
|
|
||||||
|
|
||||||
### Функциональные требования
|
|
||||||
Правила оформления: `../common-elements/fr.md`
|
|
||||||
|
|
||||||
### Нефункциональные требования
|
|
||||||
Правила оформления: `../common-elements/nfr.md`
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
---
|
|
||||||
doc_type: ui_page
|
|
||||||
required_common_elements:
|
|
||||||
- common-elements/summary.md
|
|
||||||
- common-elements/details.md
|
|
||||||
- common-elements/tech-use-case.md
|
|
||||||
- common-elements/ui-requirements.md
|
|
||||||
- common-elements/fr.md
|
|
||||||
- common-elements/user-analytics.md
|
|
||||||
special_rules:
|
|
||||||
- Для списочных страниц обязательно описывать табличное представление, empty state и error state.
|
|
||||||
- UI-элементы описываются в таблицах по правилам `common-elements/ui-requirements.md`.
|
|
||||||
---
|
|
||||||
|
|
||||||
# <title>
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
Правила оформления: `../common-elements/summary.md`
|
|
||||||
|
|
||||||
## Details
|
|
||||||
Правила оформления: `../common-elements/details.md`
|
|
||||||
|
|
||||||
### Технический use case
|
|
||||||
Правила оформления: `../common-elements/tech-use-case.md`
|
|
||||||
|
|
||||||
### Требования к UI
|
|
||||||
Правила оформления: `../common-elements/ui-requirements.md`
|
|
||||||
|
|
||||||
### Функциональные требования
|
|
||||||
Правила оформления: `../common-elements/fr.md`
|
|
||||||
|
|
||||||
### Нефункциональные требования
|
|
||||||
Правила оформления: `../common-elements/user-analytics.md`
|
|
||||||
@@ -11,7 +11,6 @@ requires-python = ">=3.11"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"fastapi>=0.116",
|
"fastapi>=0.116",
|
||||||
"uvicorn>=0.35",
|
"uvicorn>=0.35",
|
||||||
"python-dotenv>=1.0",
|
|
||||||
"pydantic>=2.11",
|
"pydantic>=2.11",
|
||||||
"langgraph>=0.6",
|
"langgraph>=0.6",
|
||||||
"langgraph-checkpoint-postgres>=2.0",
|
"langgraph-checkpoint-postgres>=2.0",
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
fastapi==0.116.1
|
fastapi==0.116.1
|
||||||
uvicorn==0.35.0
|
uvicorn==0.35.0
|
||||||
python-dotenv==1.0.1
|
|
||||||
pydantic==2.11.7
|
pydantic==2.11.7
|
||||||
langgraph==0.6.7
|
langgraph==0.6.7
|
||||||
langgraph-checkpoint-postgres==2.0.23
|
langgraph-checkpoint-postgres==2.0.23
|
||||||
|
|||||||
@@ -0,0 +1,265 @@
|
|||||||
|
# Runtime Trace: 20260406-153629-250147960243
|
||||||
|
|
||||||
|
- active_rag_session_id: fdf3ff03-81f0-4772-b68e-250147960243
|
||||||
|
|
||||||
|
## request
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"request_id": "req_64906a91cdb6487ca2737a091cdaddab",
|
||||||
|
"session_id": "as_d60e71ff542642649c81221db325cbcc",
|
||||||
|
"active_rag_session_id": "fdf3ff03-81f0-4772-b68e-250147960243",
|
||||||
|
"process_version": "v2",
|
||||||
|
"created_at": "2026-04-06T15:36:29.264730+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": {
|
||||||
|
"terms": [
|
||||||
|
"/health",
|
||||||
|
"как",
|
||||||
|
"работает"
|
||||||
|
],
|
||||||
|
"entity_names": [],
|
||||||
|
"file_names": [
|
||||||
|
"/health"
|
||||||
|
],
|
||||||
|
"process_domain": null,
|
||||||
|
"process_subdomain": null
|
||||||
|
},
|
||||||
|
"confidence": 1.0,
|
||||||
|
"routing_mode": "deterministic",
|
||||||
|
"llm_router_used": false,
|
||||||
|
"reason_short": "deterministic signal",
|
||||||
|
"rag_session_id": "fdf3ff03-81f0-4772-b68e-250147960243"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## process.v2.retrieval_policy
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"event": "retrieval_plan_resolved",
|
||||||
|
"profile": "docs_explain_summary",
|
||||||
|
"layers": [
|
||||||
|
"D1_DOCUMENT_CATALOG",
|
||||||
|
"D3_ENTITY_CATALOG",
|
||||||
|
"D0_DOC_CHUNKS"
|
||||||
|
],
|
||||||
|
"limit": 12
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## process.v2.rag_retrieval
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"event": "rag_rows_fetched",
|
||||||
|
"profile": "docs_explain_summary",
|
||||||
|
"row_count": 12,
|
||||||
|
"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": "TelegramNotifyWorker",
|
||||||
|
"document_id": "architecture.telegram_notify_app",
|
||||||
|
"entity_name": "TelegramNotifyWorker",
|
||||||
|
"summary_text": "",
|
||||||
|
"section_path": "",
|
||||||
|
"content_preview": "TelegramNotifyWorker"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"layer": "D3_ENTITY_CATALOG",
|
||||||
|
"path": "docs/architecture/telegram-notify-app-overview.md",
|
||||||
|
"title": "TelegramSendService",
|
||||||
|
"document_id": "architecture.telegram_notify_app",
|
||||||
|
"entity_name": "TelegramSendService",
|
||||||
|
"summary_text": "",
|
||||||
|
"section_path": "",
|
||||||
|
"content_preview": "TelegramSendService"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"layer": "D3_ENTITY_CATALOG",
|
||||||
|
"path": "docs/architecture/telegram-notify-app-overview.md",
|
||||||
|
"title": "TelegramControlChannel",
|
||||||
|
"document_id": "architecture.telegram_notify_app",
|
||||||
|
"entity_name": "TelegramControlChannel",
|
||||||
|
"summary_text": "",
|
||||||
|
"section_path": "",
|
||||||
|
"content_preview": "TelegramControlChannel"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"layer": "D3_ENTITY_CATALOG",
|
||||||
|
"path": "docs/architecture/telegram-notify-app-overview.md",
|
||||||
|
"title": "RuntimeManager",
|
||||||
|
"document_id": "architecture.telegram_notify_app",
|
||||||
|
"entity_name": "RuntimeManager",
|
||||||
|
"summary_text": "",
|
||||||
|
"section_path": "",
|
||||||
|
"content_preview": "RuntimeManager"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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:Операторские и мониторинговые клиенты",
|
||||||
|
"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/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": "Архитектурный документ описывает состав runtime и связи между контейнероподобными компонентами приложения. Детали контрактов HTTP API вынесены в документы endpoint'ов, а сценарий фоновой отправки и health-модель описаны на отдельных страницах."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## process.v2.evidence
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"event": "evidence_assembled",
|
||||||
|
"mode": "summary",
|
||||||
|
"document_count": 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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Найденные SUMMARY-блоки:\n\n1. path: docs/architecture/telegram-notify-app-overview.md\ntitle: Архитектура Telegram Notify App\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_64906a91cdb6487ca2737a091cdaddab"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## workflow.v2.summary.llm
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"event": "response",
|
||||||
|
"text": "На основе представленной информации, можно сказать следующее:\n\nСервис \"Telegram Notify App\" предоставляет HTTP-интерфейс, включая endpoint `/health`. Эта информация содержится в блоке, где упомянуты связанные API (Related API). Однако подробностей о том, как именно работает данный endpoint, его функциональность или формат ответа, в предоставленных данных нет.\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": 444
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## workflow.v2.summary
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"event": "workflow_completed",
|
||||||
|
"workflow_id": "v2.docs_explain.summary"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## result
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "done",
|
||||||
|
"answer": "На основе представленной информации, можно сказать следующее:\n\nСервис \"Telegram Notify App\" предоставляет HTTP-интерфейс, включая endpoint `/health`. Эта информация содержится в блоке, где упомянуты связанные API (Related API). Однако подробностей о том, как именно работает данный endpoint, его функциональность или формат ответа, в предоставленных данных нет.\n\nФайлы, на которые я опирался:\n- docs/architecture/telegram-notify-app-overview.md",
|
||||||
|
"completed_at": "2026-04-06T15:36:31.411613+00:00"
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
```
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||||
|
}
|
||||||
|
```
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,3 @@
|
|||||||
|
from app.core.agent.runtime import AgentRuntime
|
||||||
|
|
||||||
__all__ = ["AgentRuntime"]
|
__all__ = ["AgentRuntime"]
|
||||||
|
|
||||||
|
|
||||||
def __getattr__(name: str):
|
|
||||||
if name == "AgentRuntime":
|
|
||||||
from app.core.agent.runtime import AgentRuntime
|
|
||||||
|
|
||||||
return AgentRuntime
|
|
||||||
raise AttributeError(name)
|
|
||||||
|
|||||||
@@ -1,22 +1,10 @@
|
|||||||
|
from app.core.agent.processes.base import AgentProcess, ProcessResult
|
||||||
|
from app.core.agent.processes.v1.process import V1Process
|
||||||
|
from app.core.agent.processes.v2.process import V2Process
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"AgentProcess",
|
"AgentProcess",
|
||||||
"ProcessResult",
|
"ProcessResult",
|
||||||
"V1Process",
|
"V1Process",
|
||||||
"V2Process",
|
"V2Process",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def __getattr__(name: str):
|
|
||||||
if name in {"AgentProcess", "ProcessResult"}:
|
|
||||||
from app.core.agent.processes.base import AgentProcess, ProcessResult
|
|
||||||
|
|
||||||
return {"AgentProcess": AgentProcess, "ProcessResult": ProcessResult}[name]
|
|
||||||
if name == "V1Process":
|
|
||||||
from app.core.agent.processes.v1.process import V1Process
|
|
||||||
|
|
||||||
return V1Process
|
|
||||||
if name == "V2Process":
|
|
||||||
from app.core.agent.processes.v2.v2_process import V2Process
|
|
||||||
|
|
||||||
return V2Process
|
|
||||||
raise AttributeError(name)
|
|
||||||
|
|||||||
@@ -2,11 +2,8 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from dataclasses import field
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from app.schemas.changeset import ChangeItem
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from app.core.agent.runtime.execution_context import RuntimeExecutionContext
|
from app.core.agent.runtime.execution_context import RuntimeExecutionContext
|
||||||
|
|
||||||
@@ -14,8 +11,6 @@ if TYPE_CHECKING:
|
|||||||
@dataclass(slots=True)
|
@dataclass(slots=True)
|
||||||
class ProcessResult:
|
class ProcessResult:
|
||||||
answer: str = ""
|
answer: str = ""
|
||||||
changeset: list[ChangeItem] = field(default_factory=list)
|
|
||||||
apply_changeset: bool = False
|
|
||||||
|
|
||||||
|
|
||||||
class AgentProcess(ABC):
|
class AgentProcess(ABC):
|
||||||
|
|||||||
@@ -1,13 +1,4 @@
|
|||||||
|
from app.core.agent.processes.v2.process import V2Process
|
||||||
|
from app.core.agent.processes.v2.intent_router.router import V2IntentRouter
|
||||||
|
|
||||||
__all__ = ["V2IntentRouter", "V2Process"]
|
__all__ = ["V2IntentRouter", "V2Process"]
|
||||||
|
|
||||||
|
|
||||||
def __getattr__(name: str):
|
|
||||||
if name == "V2IntentRouter":
|
|
||||||
from app.core.agent.processes.v2.intent_router.router import V2IntentRouter
|
|
||||||
|
|
||||||
return V2IntentRouter
|
|
||||||
if name == "V2Process":
|
|
||||||
from app.core.agent.processes.v2.v2_process import V2Process
|
|
||||||
|
|
||||||
return V2Process
|
|
||||||
raise AttributeError(name)
|
|
||||||
|
|||||||
+1
-6
@@ -1,6 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from app.core.agent.utils.process_v2.models import V2AnchorType, V2RouteAnchors, V2RouteResult, V2Subintent
|
from app.core.agent.processes.v2.models import V2AnchorType, V2RouteAnchors, V2RouteResult, V2Subintent
|
||||||
|
|
||||||
|
|
||||||
def anchor_signal_types(route: V2RouteResult) -> set[str]:
|
def anchor_signal_types(route: V2RouteResult) -> set[str]:
|
||||||
@@ -28,11 +28,6 @@ def route_anchor_summary(route: V2RouteResult) -> dict[str, object]:
|
|||||||
"matched_aliases": list(route.anchors.matched_aliases),
|
"matched_aliases": list(route.anchors.matched_aliases),
|
||||||
"process_domain": route.anchors.process_domain,
|
"process_domain": route.anchors.process_domain,
|
||||||
"process_subdomain": route.anchors.process_subdomain,
|
"process_subdomain": route.anchors.process_subdomain,
|
||||||
"scope_type": route.scope_type,
|
|
||||||
"candidate_domains": [c.value for c in route.anchors.candidate_domains],
|
|
||||||
"candidate_subdomains": [c.value for c in route.anchors.candidate_subdomains],
|
|
||||||
"candidate_entities": [c.value for c in route.anchors.candidate_entities],
|
|
||||||
"candidate_apis": [c.value for c in route.anchors.candidate_apis],
|
|
||||||
"signal_types": sorted(anchor_signal_types(route)),
|
"signal_types": sorted(anchor_signal_types(route)),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
# Documentation Rules Index
|
|
||||||
|
|
||||||
Этот каталог содержит локализованную проекцию правил построения документации проекта.
|
|
||||||
Источником истины для структуры и качества документов являются process-документы:
|
|
||||||
- `/Users/alex/Dev_projects_v2/ai driven app process/v2/agent/_process/01. Process.md`
|
|
||||||
- `/Users/alex/Dev_projects_v2/ai driven app process/v2/agent/_process/04. Analitycs artefacts.md`
|
|
||||||
|
|
||||||
Файлы ниже не должны противоречить этим документам, а лишь конкретизируют их для `test_echo_app`.
|
|
||||||
|
|
||||||
## Порядок использования
|
|
||||||
|
|
||||||
1. Сначала прочитать `global/documentation-system.md`.
|
|
||||||
2. Затем прочитать `global/frontmatter.md` и `global/linking.md`.
|
|
||||||
3. Затем выбрать правило из `artifact-types/` по `doc_type`.
|
|
||||||
4. Затем использовать шаблон из `templates/`.
|
|
||||||
5. Для уточнения отдельных частей документа использовать правила из `sections/`.
|
|
||||||
|
|
||||||
## Структура каталога
|
|
||||||
|
|
||||||
- `global/` — общие правила системы документации.
|
|
||||||
- `artifact-types/` — правила по типам артефактов.
|
|
||||||
- `sections/` — правила для отдельных секций документов.
|
|
||||||
- `templates/` — шаблоны документов.
|
|
||||||
|
|
||||||
## Содержимое
|
|
||||||
|
|
||||||
### Global
|
|
||||||
- `global/documentation-system.md`
|
|
||||||
- `global/frontmatter.md`
|
|
||||||
- `global/writing-style.md`
|
|
||||||
- `global/linking.md`
|
|
||||||
- `global/naming.md`
|
|
||||||
|
|
||||||
### Artifact types
|
|
||||||
- `artifact-types/api_method.md`
|
|
||||||
- `artifact-types/logic_block.md`
|
|
||||||
- `artifact-types/architecture_overview.md`
|
|
||||||
- `artifact-types/domain_entity.md`
|
|
||||||
- `artifact-types/ui_page.md`
|
|
||||||
- `artifact-types/integration_doc.md`
|
|
||||||
|
|
||||||
### Sections
|
|
||||||
- `sections/summary.md`
|
|
||||||
- `sections/details.md`
|
|
||||||
- `sections/tech-use-case.md`
|
|
||||||
- `sections/fr.md`
|
|
||||||
- `sections/api-contract.md`
|
|
||||||
- `sections/requirements-format.md`
|
|
||||||
|
|
||||||
### Templates
|
|
||||||
- `templates/api_method.template.md`
|
|
||||||
- `templates/logic_block.template.md`
|
|
||||||
- `templates/architecture_overview.template.md`
|
|
||||||
- `templates/domain_entity.template.md`
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
# API Method Rules
|
|
||||||
|
|
||||||
## Назначение
|
|
||||||
|
|
||||||
Этот файл задает правила для документов типа `api_method`.
|
|
||||||
|
|
||||||
## Когда использовать
|
|
||||||
|
|
||||||
Использовать для описания одного HTTP endpoint или одного отдельного API метода.
|
|
||||||
|
|
||||||
## Обязательная структура
|
|
||||||
|
|
||||||
Документ должен содержать:
|
|
||||||
- YAML frontmatter
|
|
||||||
- `# <title>`
|
|
||||||
- `## Summary`
|
|
||||||
- `## Details`
|
|
||||||
|
|
||||||
Внутри `## Details` обязательны:
|
|
||||||
- `### Описание`
|
|
||||||
- `### Сценарий`
|
|
||||||
- `### Функциональные требования`
|
|
||||||
- `### Нефункциональные требования`
|
|
||||||
- `### Контракт`
|
|
||||||
|
|
||||||
## Особые правила
|
|
||||||
|
|
||||||
- Во frontmatter обязательно указывать `endpoint` (например: `POST /api/v1/clients/contacts-dgr`).
|
|
||||||
- Сценарий оформляется как технический use case.
|
|
||||||
- Функциональные требования маркируются `FR-*`.
|
|
||||||
- Нефункциональные требования маркируются `NFR-*`.
|
|
||||||
- Контракт должен быть пригоден для последующей сборки OpenAPI.
|
|
||||||
- Если у метода есть интеграции, они выносятся в `### Интеграции`.
|
|
||||||
- Ошибки и HTTP-коды либо описываются в `### Ошибки`, либо ссылаются на централизованный каталог ошибок.
|
|
||||||
|
|
||||||
## Ошибки оформления
|
|
||||||
|
|
||||||
- Нельзя заменять контракт общим текстовым описанием.
|
|
||||||
- Нельзя смешивать несколько endpoint в одном документе.
|
|
||||||
- Нельзя хранить связи и навигацию вне frontmatter.
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
# Architecture Overview Rules
|
|
||||||
|
|
||||||
## Назначение
|
|
||||||
|
|
||||||
Этот файл задает правила для документов типа `architecture_overview`.
|
|
||||||
|
|
||||||
## Когда использовать
|
|
||||||
|
|
||||||
Использовать как входной документ для понимания системы, модуля или сервиса.
|
|
||||||
|
|
||||||
## Обязательная структура
|
|
||||||
|
|
||||||
Документ должен содержать:
|
|
||||||
- YAML frontmatter
|
|
||||||
- `# <title>`
|
|
||||||
- `## Summary`
|
|
||||||
- `## Details`
|
|
||||||
|
|
||||||
## Что описывать в Details
|
|
||||||
|
|
||||||
- границы системы
|
|
||||||
- основные компоненты
|
|
||||||
- ключевые взаимодействия
|
|
||||||
- интеграционные сценарии
|
|
||||||
- главные ограничения
|
|
||||||
- ссылки на дочерние документы по API, logic, domain и другим артефактам
|
|
||||||
|
|
||||||
## Ошибки оформления
|
|
||||||
|
|
||||||
- Нельзя дублировать в архитектурном обзоре полные API-контракты.
|
|
||||||
- Нельзя делать архитектурный обзор единственным документом на всю систему без декомпозиции.
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
# Domain Entity Rules
|
|
||||||
|
|
||||||
## Назначение
|
|
||||||
|
|
||||||
Этот файл задает правила для документов типа `domain_entity`.
|
|
||||||
|
|
||||||
## Когда использовать
|
|
||||||
|
|
||||||
Использовать для описания одной доменной сущности, ее смысла, состояния и роли в системе.
|
|
||||||
|
|
||||||
## Обязательная структура
|
|
||||||
|
|
||||||
Документ должен содержать:
|
|
||||||
- YAML frontmatter
|
|
||||||
- `# <title>`
|
|
||||||
- `## Summary`
|
|
||||||
- `## Details`
|
|
||||||
|
|
||||||
## Что описывать в Details
|
|
||||||
|
|
||||||
- смысл сущности
|
|
||||||
- ключевые атрибуты
|
|
||||||
- состояния или инварианты
|
|
||||||
- использование сущности в системе
|
|
||||||
- интеграции с API, workflow или внешними потребителями, если они важны для понимания модели
|
|
||||||
|
|
||||||
## Ошибки оформления
|
|
||||||
|
|
||||||
- Нельзя смешивать несколько независимых сущностей в одном документе.
|
|
||||||
- Нельзя подменять доменную сущность описанием endpoint или workflow.
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
# Integration Doc Rules
|
|
||||||
|
|
||||||
## Назначение
|
|
||||||
|
|
||||||
Этот файл задает правила для документов типа `integration_doc`.
|
|
||||||
|
|
||||||
## Когда использовать
|
|
||||||
|
|
||||||
Использовать для описания интеграции между системами, сервисами или внешними провайдерами.
|
|
||||||
|
|
||||||
## Обязательная структура
|
|
||||||
|
|
||||||
Документ должен содержать:
|
|
||||||
- YAML frontmatter
|
|
||||||
- `# <title>`
|
|
||||||
- `## Summary`
|
|
||||||
- `## Details`
|
|
||||||
|
|
||||||
## Что описывать в Details
|
|
||||||
|
|
||||||
- цель интеграции
|
|
||||||
- участвующие стороны
|
|
||||||
- направление обмена
|
|
||||||
- ключевой сценарий взаимодействия
|
|
||||||
- ограничения и риски
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
# Logic Block Rules
|
|
||||||
|
|
||||||
## Назначение
|
|
||||||
|
|
||||||
Этот файл задает правила для документов типа `logic_block`.
|
|
||||||
|
|
||||||
## Когда использовать
|
|
||||||
|
|
||||||
Использовать для описания одного законченного блока логики, workflow или процесса.
|
|
||||||
|
|
||||||
## Обязательная структура
|
|
||||||
|
|
||||||
Документ должен содержать:
|
|
||||||
- YAML frontmatter
|
|
||||||
- `# <title>`
|
|
||||||
- `## Summary`
|
|
||||||
- `## Details`
|
|
||||||
|
|
||||||
## Что описывать в Details
|
|
||||||
|
|
||||||
- назначение логического блока
|
|
||||||
- входы и выходы
|
|
||||||
- последовательность выполнения
|
|
||||||
- интеграции
|
|
||||||
- ключевые ограничения
|
|
||||||
- состояние и ошибки, если они важны для понимания блока
|
|
||||||
|
|
||||||
## Ошибки оформления
|
|
||||||
|
|
||||||
- Нельзя описывать весь модуль целиком, если логика распадается на несколько независимых блоков.
|
|
||||||
- Нельзя превращать документ в пересказ исходного кода построчно.
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
# UI Page Rules
|
|
||||||
|
|
||||||
## Назначение
|
|
||||||
|
|
||||||
Этот файл задает правила для документов типа `ui_page`.
|
|
||||||
|
|
||||||
## Когда использовать
|
|
||||||
|
|
||||||
Использовать для описания одной пользовательской страницы, экрана или отдельного UI-сценария.
|
|
||||||
|
|
||||||
## Обязательная структура
|
|
||||||
|
|
||||||
Документ должен содержать:
|
|
||||||
- YAML frontmatter
|
|
||||||
- `# <title>`
|
|
||||||
- `## Summary`
|
|
||||||
- `## Details`
|
|
||||||
|
|
||||||
## Что описывать в Details
|
|
||||||
|
|
||||||
- назначение страницы
|
|
||||||
- пользовательский сценарий
|
|
||||||
- основные блоки интерфейса
|
|
||||||
- связанные API и сущности
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
# Documentation Rules
|
|
||||||
|
|
||||||
Этот каталог оформляет MVP документации проекта в атомарном формате.
|
|
||||||
|
|
||||||
## Базовая структура
|
|
||||||
|
|
||||||
- Каждый документ содержит YAML frontmatter.
|
|
||||||
- В документе должен быть один `H1`, совпадающий с `title`.
|
|
||||||
- Основные разделы оформляются как `## Summary` и `## Details`.
|
|
||||||
- Внутри `Details` используются заголовки уровня `###` и ниже.
|
|
||||||
- Связи, сущности и навигация описываются во frontmatter через `related_docs`, `links`, `entities`, `parent`, `children`.
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
|
|
||||||
- Краткий explain-слой быстрого контекста.
|
|
||||||
- Должен позволять быстро понять назначение документа без чтения `Details`.
|
|
||||||
- Предпочтительный формат: компактный список ключевых фактов без длинных абзацев.
|
|
||||||
|
|
||||||
## Details
|
|
||||||
|
|
||||||
- Раскрывает полное описание объекта.
|
|
||||||
- Структура `Details` зависит от типа документа.
|
|
||||||
- Сценарии, ограничения, интеграции, ошибки и кодовые привязки должны быть разнесены по отдельным подразделам.
|
|
||||||
|
|
||||||
## API documents
|
|
||||||
|
|
||||||
Для `api_method` внутри `## Details` обязательны разделы:
|
|
||||||
- `### Описание`
|
|
||||||
- `### Сценарий`
|
|
||||||
- `### Функциональные требования`
|
|
||||||
- `### Нефункциональные требования`
|
|
||||||
- `### Контракт`
|
|
||||||
|
|
||||||
Если у метода есть интеграции и ошибки, также обязательны:
|
|
||||||
- `### Интеграции`
|
|
||||||
- `### Ошибки`
|
|
||||||
- `### Связанный код`
|
|
||||||
- `### История изменений`
|
|
||||||
|
|
||||||
### Сценарий
|
|
||||||
|
|
||||||
Сценарий оформляется как технический use case и содержит:
|
|
||||||
- название
|
|
||||||
- предусловия
|
|
||||||
- триггер
|
|
||||||
- основной сценарий
|
|
||||||
- альтернативный сценарий
|
|
||||||
- обработку ошибок
|
|
||||||
- постусловие
|
|
||||||
|
|
||||||
### Требования
|
|
||||||
|
|
||||||
- Функциональные требования маркируются как `FR-1`, `FR-2`, ...
|
|
||||||
- Нефункциональные требования маркируются как `NFR-1`, `NFR-2`, ...
|
|
||||||
- Идентификаторы требований локальны в рамках одного документа.
|
|
||||||
|
|
||||||
### Контракт
|
|
||||||
|
|
||||||
Контракт должен быть пригоден для последующей сборки OpenAPI-спецификации и включать:
|
|
||||||
- входные параметры
|
|
||||||
- выходные параметры
|
|
||||||
- структуру JSON-сообщений
|
|
||||||
- обязательность полей
|
|
||||||
- типы и ограничения
|
|
||||||
- описание полей
|
|
||||||
- правила заполнения
|
|
||||||
- примеры данных
|
|
||||||
- auth
|
|
||||||
- idempotency
|
|
||||||
- timeout
|
|
||||||
- ошибки и их HTTP-коды
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
# Documentation System
|
|
||||||
|
|
||||||
## Назначение
|
|
||||||
|
|
||||||
Этот файл задает общую модель документации проекта.
|
|
||||||
|
|
||||||
## Базовая модель
|
|
||||||
|
|
||||||
Каждый документ должен состоять из двух слоев:
|
|
||||||
- YAML frontmatter
|
|
||||||
- контент
|
|
||||||
|
|
||||||
Контент всегда состоит из двух обязательных разделов:
|
|
||||||
- `## Summary`
|
|
||||||
- `## Details`
|
|
||||||
|
|
||||||
Над ними должен быть один заголовок `# <title>`, совпадающий со значением `title` во frontmatter.
|
|
||||||
|
|
||||||
## Принципы
|
|
||||||
|
|
||||||
- Документы должны быть атомарными.
|
|
||||||
- Один документ описывает одну тему.
|
|
||||||
- Вместо дублирования между документами используются явные ссылки.
|
|
||||||
- Связи и навигация должны быть формализованы.
|
|
||||||
- Документы должны быть пригодны для чтения человеком и для RAG.
|
|
||||||
- Документы должны быть пригодны для частичного обновления без деградации структуры.
|
|
||||||
|
|
||||||
## Типы документов
|
|
||||||
|
|
||||||
На уровне проекта поддерживаются типы:
|
|
||||||
- `api_method`
|
|
||||||
- `logic_block`
|
|
||||||
- `architecture_overview`
|
|
||||||
- `domain_entity`
|
|
||||||
- `ui_page`
|
|
||||||
- `integration_doc`
|
|
||||||
- `index_page`
|
|
||||||
- `glossary_item`
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
# Frontmatter Rules
|
|
||||||
|
|
||||||
## Назначение
|
|
||||||
|
|
||||||
Этот файл описывает единый контракт YAML frontmatter для всех документов.
|
|
||||||
|
|
||||||
## Обязательные поля
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
id: string
|
|
||||||
title: string
|
|
||||||
doc_type: string
|
|
||||||
domain: string
|
|
||||||
sub_domain: string
|
|
||||||
related_docs: []
|
|
||||||
status: string
|
|
||||||
```
|
|
||||||
|
|
||||||
## Поля совместимости и рекомендуемые поля
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
type: string
|
|
||||||
name: string
|
|
||||||
module: string
|
|
||||||
layer: string
|
|
||||||
updated_at: YYYY-MM-DD
|
|
||||||
tags: []
|
|
||||||
entities: []
|
|
||||||
parent: string | null
|
|
||||||
children: []
|
|
||||||
links: {}
|
|
||||||
source_of_truth: string
|
|
||||||
related_code: []
|
|
||||||
system_analytics_refs: []
|
|
||||||
```
|
|
||||||
|
|
||||||
## Правила
|
|
||||||
|
|
||||||
- `id` должен быть стабильным и уникальным в пределах документации проекта.
|
|
||||||
- `title` — человекочитаемый заголовок.
|
|
||||||
- `doc_type` — канонический тип документа.
|
|
||||||
- `domain` и `sub_domain` определяют бизнес-контекст документа.
|
|
||||||
- `related_docs` хранит явные связи с другими markdown-документами.
|
|
||||||
- `status` хранит жизненный цикл документа: например `draft`, `approved`, `active`.
|
|
||||||
- `type` допустимо дублировать как alias для tooling-совместимости с индексаторами.
|
|
||||||
- `name` — короткое системное имя документа.
|
|
||||||
- `module` — модуль или подсистема.
|
|
||||||
- `layer` — слой системы.
|
|
||||||
- `updated_at` хранится в формате `YYYY-MM-DD`.
|
|
||||||
- Для документов с `doc_type: api_method` поле `endpoint` является обязательным.
|
|
||||||
|
|
||||||
## Связи и навигация
|
|
||||||
|
|
||||||
- `entities` описывает сущности, связанные с документом.
|
|
||||||
- `parent` и `children` описывают иерархию.
|
|
||||||
- `links` описывает typed graph связей между документами, кодом и интеграциями.
|
|
||||||
|
|
||||||
## Формат links
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
links:
|
|
||||||
called_by:
|
|
||||||
- ext.health_probe
|
|
||||||
uses_logic:
|
|
||||||
- logic.some_flow
|
|
||||||
integrates_with:
|
|
||||||
- ext.some_system
|
|
||||||
```
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
# Linking Rules
|
|
||||||
|
|
||||||
## Назначение
|
|
||||||
|
|
||||||
Этот файл описывает, как связывать документы между собой.
|
|
||||||
|
|
||||||
## Иерархия
|
|
||||||
|
|
||||||
- `parent` используется для родительского документа.
|
|
||||||
- `children` используется для прямых дочерних документов.
|
|
||||||
- Иерархия должна быть осмысленной и стабильной.
|
|
||||||
- Для общей точки входа допустим `index_page`.
|
|
||||||
|
|
||||||
## Графовые связи
|
|
||||||
|
|
||||||
Для `related_docs` используются ссылки на соседние документы.
|
|
||||||
|
|
||||||
Для `links` рекомендуется использовать typed-ключи:
|
|
||||||
- `called_by`
|
|
||||||
- `uses_logic`
|
|
||||||
- `reads_db`
|
|
||||||
- `writes_db`
|
|
||||||
- `integrates_with`
|
|
||||||
- `used_by`
|
|
||||||
- `exposes_api`
|
|
||||||
- `uses_entities`
|
|
||||||
|
|
||||||
## Правила использования
|
|
||||||
|
|
||||||
- Если документ логически входит в другой, использовать `parent`/`children`.
|
|
||||||
- Если связь нужна для навигации между равноправными документами, дублировать ее в `related_docs`.
|
|
||||||
- Если связь отражает поведение, интеграции или переиспользование, фиксировать ее в `links`.
|
|
||||||
- Детальное описание интеграций хранить в body документа, а не только во frontmatter.
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
# Naming Rules
|
|
||||||
|
|
||||||
## Назначение
|
|
||||||
|
|
||||||
Этот файл описывает правила именования документов, файлов и идентификаторов.
|
|
||||||
|
|
||||||
## Правила для файлов
|
|
||||||
|
|
||||||
- Имена файлов должны быть в kebab-case.
|
|
||||||
- Имя файла должно отражать одну тему.
|
|
||||||
- Для шаблонов использовать суффикс `.template.md`.
|
|
||||||
|
|
||||||
## Правила для id
|
|
||||||
|
|
||||||
- `id` строится в формате `<type-group>.<name>`.
|
|
||||||
- Примеры:
|
|
||||||
- `api.send_message_endpoint`
|
|
||||||
- `logic.telegram_notification_loop`
|
|
||||||
- `architecture.telegram_notify_app`
|
|
||||||
|
|
||||||
## Правила для title
|
|
||||||
|
|
||||||
- `title` должен быть кратким и человекочитаемым.
|
|
||||||
- В `title` допускаются пробелы и естественный язык.
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
# Writing Style
|
|
||||||
|
|
||||||
## Назначение
|
|
||||||
|
|
||||||
Этот файл задает правила стиля для текстового наполнения документации.
|
|
||||||
|
|
||||||
## Правила стиля
|
|
||||||
|
|
||||||
- Текст должен быть лаконичным.
|
|
||||||
- Формулировки должны быть точными и техническими.
|
|
||||||
- Summary должен быть кратким explain-слоем.
|
|
||||||
- Details должен раскрывать суть без лишней воды.
|
|
||||||
- Нежелательно смешивать несколько тем в одном документе.
|
|
||||||
- Если детали относятся к другому артефакту, их нужно выносить в отдельный документ.
|
|
||||||
|
|
||||||
## Язык
|
|
||||||
|
|
||||||
- Основной язык документации — русский.
|
|
||||||
- Технические термины, названия классов, API, RAG, OpenAPI, runtime и другие устоявшиеся identifiers можно оставлять на английском.
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
# API Contract Rules
|
|
||||||
|
|
||||||
## Назначение
|
|
||||||
|
|
||||||
Этот файл описывает, как оформлять подраздел `## Контракт` в API-документах.
|
|
||||||
|
|
||||||
## Что должно быть описано
|
|
||||||
|
|
||||||
- входные параметры
|
|
||||||
- выходные параметры
|
|
||||||
- JSON-структуры запросов и ответов
|
|
||||||
- обязательность полей
|
|
||||||
- типы полей
|
|
||||||
- ограничения
|
|
||||||
- описание назначения полей
|
|
||||||
- примеры данных
|
|
||||||
- auth
|
|
||||||
- idempotency
|
|
||||||
- timeout
|
|
||||||
- ошибки и их HTTP-коды
|
|
||||||
|
|
||||||
## Правило качества
|
|
||||||
|
|
||||||
Контракт должен быть достаточно формальным, чтобы по нему можно было собрать OpenAPI-спецификацию.
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
# Details Section Rules
|
|
||||||
|
|
||||||
## Назначение
|
|
||||||
|
|
||||||
Этот файл задает общие правила для секции `## Details`.
|
|
||||||
|
|
||||||
## Правила
|
|
||||||
|
|
||||||
- `Details` оформляется как `## Details`.
|
|
||||||
- Внутри `Details` используются заголовки уровня `###` и ниже.
|
|
||||||
- Структура Details зависит от типа документа.
|
|
||||||
- В Details не нужно повторно дублировать навигацию и связи, если они уже есть во frontmatter.
|
|
||||||
- Интеграции, ошибки и кодовые привязки должны быть выделены в отдельные подразделы, если они существенны для понимания документа.
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
# Functional requrements rules
|
|
||||||
|
|
||||||
## Назначение
|
|
||||||
|
|
||||||
Этот файл описывает, как оформлять функциональные требования в подраздел `### Функциональные требования` в документах.
|
|
||||||
|
|
||||||
## Правила
|
|
||||||
- Функциональное требование (FR) расширяет и дополняет шаги, описанные в сценарии.
|
|
||||||
- Функциональное требование (FR) не должно копировать шаг сценария не неся дополнительной информации.
|
|
||||||
- Название функционального требования формируется следующим образом - "FR.<номер>. <Название>", где
|
|
||||||
- <номер> идет инкрементально внутри конкретного документа, начинается с 1.
|
|
||||||
- <Название> - кратко описывает что делает требование, суть действий (от 3 до 7 слов)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Пример целевого описания сценария
|
|
||||||
|
|
||||||
### Примеры названия FR
|
|
||||||
- Получение данных клиента из АС ЕПК
|
|
||||||
- Проверка уровня доступа
|
|
||||||
- Сценарий построения списка связанных предложений
|
|
||||||
|
|
||||||
|
|
||||||
### Примеры описания FR
|
|
||||||
FR.1. Получение данных клиента из АС ЕПК
|
|
||||||
1. Сформировать запрос к эндпоинту POST /api/v1/path/to/resourse в АС ЕПК
|
|
||||||
- Заголовки
|
|
||||||
- <тут идет описание заголовков и того как они формируются>
|
|
||||||
- Параметры запроса
|
|
||||||
- <тут идет описание параметров и того как они формируются>
|
|
||||||
- Тело запроса
|
|
||||||
- <тут идет описание структуры объекта JSON или payload в другмо формате так как это задано требованиями>
|
|
||||||
|
|
||||||
2. Обработать ответ от АС ЕПК
|
|
||||||
Успешный ответ - <взять из описания вызываеого api критерии успешного ответа >
|
|
||||||
Ничего не найдено - <взять из описания вызываеого api критерии успешного овтета, опционально (если применимо)>
|
|
||||||
Ошибка - <взять из описания вызываеого api критерии успешного ответа >
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
# Requirements Format Rules
|
|
||||||
|
|
||||||
## Назначение
|
|
||||||
|
|
||||||
Этот файл задает формат для функциональных и нефункциональных требований.
|
|
||||||
|
|
||||||
## Функциональные требования
|
|
||||||
|
|
||||||
- Использовать коды `FR-1`, `FR-2`, `FR-3` и так далее.
|
|
||||||
- Каждое требование должно описывать отдельный обязательный аспект поведения.
|
|
||||||
- Идентификаторы локальны в пределах одного документа.
|
|
||||||
|
|
||||||
## Нефункциональные требования
|
|
||||||
|
|
||||||
- Использовать коды `NFR-1`, `NFR-2`, `NFR-3` и так далее.
|
|
||||||
- Требования должны описывать характеристики качества, ограничения и эксплуатационные свойства.
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
# Summary Section Rules
|
|
||||||
|
|
||||||
## Назначение
|
|
||||||
|
|
||||||
Этот файл задает правила для секции `## Summary`.
|
|
||||||
|
|
||||||
## Правила
|
|
||||||
|
|
||||||
- Summary должен быть коротким explain-слоем быстрого контекста.
|
|
||||||
- Summary должен объяснять суть документа без лишних деталей.
|
|
||||||
- Summary должен быть пригоден для explain и быстрого чтения.
|
|
||||||
- Предпочтительный формат: список ключевых фактов `Purpose`, `Actor`, `Trigger`, `Errors`, `Related ...` и т.д.
|
|
||||||
- Для крупных документов допустим более длинный summary, если он остается структурированным.
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
# Scenario Rules
|
|
||||||
|
|
||||||
## Назначение
|
|
||||||
|
|
||||||
Этот файл описывает, как оформлять технический USE CASE в подраздел `### Сценарий` в документах.
|
|
||||||
|
|
||||||
## Обязательные части
|
|
||||||
|
|
||||||
- название
|
|
||||||
- предусловия
|
|
||||||
- триггер
|
|
||||||
- основной сценарий
|
|
||||||
- альтернативный сценарий
|
|
||||||
- обработка ошибок
|
|
||||||
- постусловие
|
|
||||||
|
|
||||||
## Правила
|
|
||||||
- Основной и альтернативные сценарии состоят из шагов.
|
|
||||||
|
|
||||||
- Каждый шаг описывается одним предложением не более 15-20 слов, и состоит из двух частей. Первая часть описывает что мы делаем по смыслу, чтобы это было понятно человеку без низкоуровневых технических деталей. Например: авторизует запрос, получает данные клиента, запрашивает справочники. Вторая часть описывает как это реализовано технически - вызывает эндпоинт /path/to/resource в системе <название системы>.
|
|
||||||
|
|
||||||
- В описании шага не должно быть длинных технических деталей. Если техничсекую реализацию нельхзя описатьодним предложенеим (в лимите длины описания шага), то необхлодимо это вынести в отдельное функциональное требование FR.<номер>. <Название> и описать в нем технические детали. А в шаге сослаться на это требование через "Описание приведено в FR.<номер>. <Название>"
|
|
||||||
|
|
||||||
- Для шагов авторизации обязателен доп шаг с описанием обработки ошибки.
|
|
||||||
- Для шагов с интеграцией обязателен доп шаг с описанием обработки ошибки.
|
|
||||||
- Для шагов с проверкой условий обязательны доп шаги с описанием переходов по сценарию.
|
|
||||||
|
|
||||||
- Название "FR.<номер>. <Название>" формируется следующим образом:
|
|
||||||
- <номер> идет инкрементально внутри конкретного документа, начинается с 1.
|
|
||||||
- <Название> - кратко описывает что делает требование, суть действий.
|
|
||||||
|
|
||||||
- Для каждого шага при необходимости нужно прописать логику действий в случае ошибки или если логика шага определяет несколько сценариев разивития при выполнении заданных условий.
|
|
||||||
|
|
||||||
- Для шагов, которые описывают интеграцию с другой системой необходимо указать название точки интеграции (название эндпоинта, название топика и так далее) и сделать ссылку на FR.<номер>. <Название> с описанием шагов интеграции - как сформировать запрос/сообщение, как обработать ответ, политику ретраев.
|
|
||||||
|
|
||||||
- Сценарий собирается из тезисов, приведенных системной аналимтике в свободной формулировке
|
|
||||||
|
|
||||||
- Функциональные требования "FR.<номер>. <Название>" не должны дублировать шагов сценария в use case. Они содержат детали, которые вынесены из юзкейса чтобы не делать его тяжелым. Если шаг юзкейса описывается одним предложением в лимите длины, то FR делать не нужно.
|
|
||||||
|
|
||||||
- FR обязательно описывается для шага с интеграцией
|
|
||||||
- FR Не описывается для шага авторизации.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Пример целевого описания сценария
|
|
||||||
|
|
||||||
### Примеры шагов сценария
|
|
||||||
|
|
||||||
Пример 1
|
|
||||||
- Авторизует запрос пользователя по наличию у него экшена ролевой модели CI02792632.ContactsDGR.Detail
|
|
||||||
- В случае ошибки - завершить сценарий с кодом UNAUTHORIZED
|
|
||||||
|
|
||||||
Пример 2
|
|
||||||
- Запрашивает данные клиента - вызывает /api/v1/clients/{client-id}/info
|
|
||||||
- В случае ошибки - завершить сценарий с кодом CLIENT_INFO_REQUEST_FAIL
|
|
||||||
|
|
||||||
Пример 3
|
|
||||||
- Возвращает ответ в формате <название DTO>
|
|
||||||
|
|
||||||
### Примеры названия FR
|
|
||||||
- Получение данных клиента из АС ЕПК
|
|
||||||
- Проверка уровня доступа
|
|
||||||
- Сценарий построения списка связанных предложений
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
---
|
|
||||||
id: api.example_method
|
|
||||||
type: api_method
|
|
||||||
doc_type: api_method
|
|
||||||
name: example_method
|
|
||||||
title: HTTP API /example
|
|
||||||
module: example_module
|
|
||||||
layer: application
|
|
||||||
domain: example_domain
|
|
||||||
sub_domain: example_subdomain
|
|
||||||
endpoint: POST /api/v1/example
|
|
||||||
related_docs: []
|
|
||||||
status: draft
|
|
||||||
updated_at: 2026-03-20
|
|
||||||
source_of_truth: code
|
|
||||||
parent: null
|
|
||||||
children: []
|
|
||||||
tags: []
|
|
||||||
entities: []
|
|
||||||
links: {}
|
|
||||||
---
|
|
||||||
|
|
||||||
# HTTP API /example
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
|
|
||||||
Краткое описание метода.
|
|
||||||
|
|
||||||
## Details
|
|
||||||
|
|
||||||
## Описание
|
|
||||||
|
|
||||||
Короткое описание сути метода.
|
|
||||||
|
|
||||||
## Сценарий
|
|
||||||
|
|
||||||
**Название:**
|
|
||||||
|
|
||||||
**Предусловия:**
|
|
||||||
-
|
|
||||||
|
|
||||||
**Триггер:**
|
|
||||||
-
|
|
||||||
|
|
||||||
**Основной сценарий:**
|
|
||||||
1.
|
|
||||||
|
|
||||||
**Альтернативный сценарий:**
|
|
||||||
1.
|
|
||||||
|
|
||||||
**Обработка ошибок:**
|
|
||||||
1.
|
|
||||||
|
|
||||||
**Постусловие:**
|
|
||||||
-
|
|
||||||
|
|
||||||
## Функциональные требования
|
|
||||||
|
|
||||||
**FR-1.**
|
|
||||||
|
|
||||||
## Нефункциональные требования
|
|
||||||
|
|
||||||
**NFR-1.**
|
|
||||||
|
|
||||||
## Контракт
|
|
||||||
|
|
||||||
### Входные параметры
|
|
||||||
|
|
||||||
| Параметр | Где передается | Тип | Обязательность | Ограничения | Описание | Пример |
|
|
||||||
|---|---|---|---|---|---|---|
|
|
||||||
| | | | | | | |
|
|
||||||
|
|
||||||
### Выходные параметры
|
|
||||||
|
|
||||||
| Поле | Тип | Обязательность | Ограничения | Описание | Заполнение | Пример |
|
|
||||||
|---|---|---|---|---|---|---|
|
|
||||||
| | | | | | | |
|
|
||||||
|
|
||||||
### Интеграции
|
|
||||||
|
|
||||||
### Ошибки
|
|
||||||
|
|
||||||
### Связанный код
|
|
||||||
|
|
||||||
### История изменений
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
---
|
|
||||||
id: architecture.example_system
|
|
||||||
type: architecture_overview
|
|
||||||
doc_type: architecture_overview
|
|
||||||
name: example_system
|
|
||||||
title: Обзор архитектуры Example System
|
|
||||||
module: example_module
|
|
||||||
layer: system
|
|
||||||
domain: example_domain
|
|
||||||
sub_domain: example_subdomain
|
|
||||||
related_docs: []
|
|
||||||
status: draft
|
|
||||||
updated_at: 2026-03-20
|
|
||||||
source_of_truth: mixed
|
|
||||||
parent: null
|
|
||||||
children: []
|
|
||||||
tags: []
|
|
||||||
entities: []
|
|
||||||
links: {}
|
|
||||||
---
|
|
||||||
|
|
||||||
# Обзор архитектуры Example System
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
|
|
||||||
Краткое описание архитектуры.
|
|
||||||
|
|
||||||
## Details
|
|
||||||
|
|
||||||
### Описание
|
|
||||||
|
|
||||||
### Контекст
|
|
||||||
|
|
||||||
### Границы системы
|
|
||||||
|
|
||||||
### Компоненты
|
|
||||||
|
|
||||||
### Интеграционные сценарии
|
|
||||||
|
|
||||||
### Интеграции
|
|
||||||
|
|
||||||
### Ограничения
|
|
||||||
|
|
||||||
### Связанный код
|
|
||||||
|
|
||||||
### Связанные документы
|
|
||||||
|
|
||||||
### История изменений
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
---
|
|
||||||
id: domain.example_entity
|
|
||||||
type: domain_entity
|
|
||||||
doc_type: domain_entity
|
|
||||||
name: example_entity
|
|
||||||
title: Пример доменной сущности
|
|
||||||
module: example_module
|
|
||||||
layer: domain
|
|
||||||
domain: example_domain
|
|
||||||
sub_domain: example_subdomain
|
|
||||||
related_docs: []
|
|
||||||
status: draft
|
|
||||||
updated_at: 2026-03-20
|
|
||||||
source_of_truth: code
|
|
||||||
parent: null
|
|
||||||
children: []
|
|
||||||
tags: []
|
|
||||||
entities: []
|
|
||||||
links: {}
|
|
||||||
---
|
|
||||||
|
|
||||||
# Пример доменной сущности
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
|
|
||||||
Краткое описание сущности.
|
|
||||||
|
|
||||||
## Details
|
|
||||||
|
|
||||||
### Описание
|
|
||||||
|
|
||||||
### Модель данных
|
|
||||||
|
|
||||||
### Состояния и инварианты
|
|
||||||
|
|
||||||
### Технический use case
|
|
||||||
|
|
||||||
### Функциональные требования
|
|
||||||
|
|
||||||
### Нефункциональные требования
|
|
||||||
|
|
||||||
### Интеграции
|
|
||||||
|
|
||||||
### Связанный код
|
|
||||||
|
|
||||||
### Связанные документы
|
|
||||||
|
|
||||||
### История изменений
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
---
|
|
||||||
id: logic.example_block
|
|
||||||
type: logic_block
|
|
||||||
doc_type: logic_block
|
|
||||||
name: example_block
|
|
||||||
title: Пример блока логики
|
|
||||||
module: example_module
|
|
||||||
layer: application
|
|
||||||
domain: example_domain
|
|
||||||
sub_domain: example_subdomain
|
|
||||||
related_docs: []
|
|
||||||
status: draft
|
|
||||||
updated_at: 2026-03-20
|
|
||||||
source_of_truth: code
|
|
||||||
parent: null
|
|
||||||
children: []
|
|
||||||
tags: []
|
|
||||||
entities: []
|
|
||||||
links: {}
|
|
||||||
---
|
|
||||||
|
|
||||||
# Пример блока логики
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
|
|
||||||
Краткое описание блока логики.
|
|
||||||
|
|
||||||
## Details
|
|
||||||
|
|
||||||
### Описание
|
|
||||||
|
|
||||||
### Контекст
|
|
||||||
|
|
||||||
### Технический use case
|
|
||||||
|
|
||||||
### Функциональные требования
|
|
||||||
|
|
||||||
### Нефункциональные требования
|
|
||||||
|
|
||||||
### Интеграции
|
|
||||||
|
|
||||||
### Ограничения и условия вызова
|
|
||||||
|
|
||||||
### Ошибки и деградации
|
|
||||||
|
|
||||||
### Связанные API
|
|
||||||
|
|
||||||
### Связанный код
|
|
||||||
|
|
||||||
### История изменений
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
---
|
|
||||||
id: ui.example_page
|
|
||||||
type: ui_page
|
|
||||||
doc_type: ui_page
|
|
||||||
name: example_page
|
|
||||||
title: Пример UI-страницы
|
|
||||||
module: example_module
|
|
||||||
layer: presentation
|
|
||||||
domain: example_domain
|
|
||||||
sub_domain: example_subdomain
|
|
||||||
related_docs: []
|
|
||||||
status: draft
|
|
||||||
updated_at: 2026-03-20
|
|
||||||
source_of_truth: mixed
|
|
||||||
parent: null
|
|
||||||
children: []
|
|
||||||
tags: []
|
|
||||||
entities: []
|
|
||||||
links: {}
|
|
||||||
---
|
|
||||||
|
|
||||||
# Пример UI-страницы
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
|
|
||||||
Краткое описание страницы и её назначения.
|
|
||||||
|
|
||||||
## Details
|
|
||||||
|
|
||||||
### Назначение страницы
|
|
||||||
|
|
||||||
### Пользовательский сценарий
|
|
||||||
|
|
||||||
### Основные блоки интерфейса
|
|
||||||
|
|
||||||
### Связанные API и сущности
|
|
||||||
|
|
||||||
### Функциональные требования
|
|
||||||
|
|
||||||
### Нефункциональные требования
|
|
||||||
|
|
||||||
### Ограничения и граничные случаи
|
|
||||||
|
|
||||||
### Ошибки и валидации
|
|
||||||
|
|
||||||
### Связанный код
|
|
||||||
|
|
||||||
### Связанные документы
|
|
||||||
|
|
||||||
### История изменений
|
|
||||||
+9
-9
@@ -4,16 +4,15 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from app.core.agent.utils.process_v2.anchor_signals import anchor_signal_types
|
from app.core.agent.processes.v2.anchor_signals import anchor_signal_types
|
||||||
from app.core.agent.utils.process_v2.models import RetrievedFile, RetrievedSummary, V2AnchorType, V2RouteResult
|
from app.core.agent.processes.v2.models import RetrievedFile, RetrievedSummary, V2AnchorType, V2RouteResult
|
||||||
from app.core.agent.utils.process_v2.rag_retrieval.target_doc_seeding import normalize_doc_path
|
from app.core.agent.processes.v2.retrieval.target_doc_seeding import normalize_doc_path
|
||||||
from app.core.rag.contracts.enums import RagLayer
|
from app.core.rag.contracts.enums import RagLayer
|
||||||
|
|
||||||
|
|
||||||
class DocsEvidenceAssembler:
|
class DocsEvidenceAssembler:
|
||||||
_API_PATH_PREFIXES = ("docs/api/", "docs/endpoints/", "docs/methods/", "api/", "endpoints/", "methods/")
|
_API_PATH_PREFIXES = ("docs/api/", "docs/endpoints/", "docs/methods/", "api/", "endpoints/", "methods/")
|
||||||
_GENERIC_DOC_MARKERS = ("readme", "overview", "index", "navigation", "related docs", "catalog")
|
_GENERIC_DOC_MARKERS = ("readme", "overview", "index", "navigation", "related docs", "catalog")
|
||||||
|
|
||||||
def assemble_summaries(self, rows: list[dict], route: V2RouteResult) -> list[RetrievedSummary]:
|
def assemble_summaries(self, rows: list[dict], route: V2RouteResult) -> list[RetrievedSummary]:
|
||||||
items = self._rank_rows(rows, route, mode="summary")
|
items = self._rank_rows(rows, route, mode="summary")
|
||||||
ranked = [
|
ranked = [
|
||||||
@@ -141,6 +140,7 @@ class DocsEvidenceAssembler:
|
|||||||
if mode == "find_files":
|
if mode == "find_files":
|
||||||
breakdown["path_match"] *= 3
|
breakdown["path_match"] *= 3
|
||||||
breakdown["filename_match"] *= 2
|
breakdown["filename_match"] *= 2
|
||||||
|
breakdown["alias_match"] *= 1
|
||||||
breakdown["semantic"] = max(0, breakdown["semantic"] // 2)
|
breakdown["semantic"] = max(0, breakdown["semantic"] // 2)
|
||||||
return breakdown
|
return breakdown
|
||||||
|
|
||||||
@@ -181,7 +181,10 @@ class DocsEvidenceAssembler:
|
|||||||
hn = normalize_doc_path(hint).lower()
|
hn = normalize_doc_path(hint).lower()
|
||||||
if hn in top_norm:
|
if hn in top_norm:
|
||||||
continue
|
continue
|
||||||
candidate = next((item for item in ranked if normalize_doc_path(item["path"]).lower() == hn), None)
|
candidate = next(
|
||||||
|
(item for item in ranked if normalize_doc_path(item["path"]).lower() == hn),
|
||||||
|
None,
|
||||||
|
)
|
||||||
if candidate is None:
|
if candidate is None:
|
||||||
continue
|
continue
|
||||||
if len(top) < k:
|
if len(top) < k:
|
||||||
@@ -200,10 +203,7 @@ class DocsEvidenceAssembler:
|
|||||||
first = ranked[0]
|
first = ranked[0]
|
||||||
if not first.get("is_generic_doc"):
|
if not first.get("is_generic_doc"):
|
||||||
return ranked
|
return ranked
|
||||||
promoted = next(
|
promoted = next((item for item in ranked[1:] if not item.get("is_generic_doc") and self._is_specific_candidate(item, route)), None)
|
||||||
(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:
|
if promoted is None:
|
||||||
return ranked
|
return ranked
|
||||||
return [promoted] + [item for item in ranked if item["path"] != promoted["path"]]
|
return [promoted] + [item for item in ranked if item["path"] != promoted["path"]]
|
||||||
+2
-2
@@ -2,8 +2,8 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
from app.core.agent.utils.process_v2.anchor_signals import anchor_signal_types
|
from app.core.agent.processes.v2.anchor_signals import anchor_signal_types
|
||||||
from app.core.agent.utils.process_v2.models import RetrievedFile, RetrievedSummary, V2AnchorType, V2Intent, V2RouteResult
|
from app.core.agent.processes.v2.models import RetrievedFile, RetrievedSummary, V2AnchorType, V2Intent, V2RouteResult
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True)
|
@dataclass(slots=True)
|
||||||
@@ -16,4 +16,3 @@ class QueryFeatures:
|
|||||||
logic_markers: list[str]
|
logic_markers: list[str]
|
||||||
domain_markers: list[str]
|
domain_markers: list[str]
|
||||||
endpoint_markers: list[str]
|
endpoint_markers: list[str]
|
||||||
scope_type: str = "unknown"
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import re
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
from app.core.agent.processes.v2.intent_router.modules.target_terms import TargetTermsAnalysis
|
from app.core.agent.processes.v2.intent_router.modules.target_terms import TargetTermsAnalysis
|
||||||
from app.core.agent.utils.process_v2.models import V2RouteAnchors
|
from app.core.agent.processes.v2.models import V2RouteAnchors
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True)
|
@dataclass(slots=True)
|
||||||
|
|||||||
@@ -1,176 +0,0 @@
|
|||||||
"""Build an in-memory DOCS scope index from D1/D3 catalog rows (no chunk retrieval).
|
|
||||||
|
|
||||||
Parses metadata from ``D1_DOCUMENT_CATALOG`` and ``D3_ENTITY_CATALOG`` rows produced by the
|
|
||||||
existing RAG indexer—no additional layers or chunk scans.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import re
|
|
||||||
from dataclasses import dataclass, field
|
|
||||||
|
|
||||||
|
|
||||||
def _norm_text(value: object) -> str:
|
|
||||||
return re.sub(r"\s+", " ", str(value or "").strip().lower())
|
|
||||||
|
|
||||||
|
|
||||||
def _split_multi(value: object) -> list[str]:
|
|
||||||
if value is None:
|
|
||||||
return []
|
|
||||||
if isinstance(value, list):
|
|
||||||
raw = value
|
|
||||||
else:
|
|
||||||
raw = re.split(r"[;,|]", str(value))
|
|
||||||
out: list[str] = []
|
|
||||||
for item in raw:
|
|
||||||
s = str(item).strip()
|
|
||||||
if s:
|
|
||||||
out.append(s)
|
|
||||||
return out
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True)
|
|
||||||
class DocsScopeCatalog:
|
|
||||||
"""Flattened terms from D1_DOCUMENT_CATALOG and D3_ENTITY_CATALOG for lexical grounding."""
|
|
||||||
|
|
||||||
domain_values: set[str] = field(default_factory=set)
|
|
||||||
subdomain_pairs: list[tuple[str, str]] = field(default_factory=list) # (domain, subdomain)
|
|
||||||
entity_records: list[dict[str, object]] = field(default_factory=list)
|
|
||||||
api_records: list[dict[str, object]] = field(default_factory=list)
|
|
||||||
|
|
||||||
|
|
||||||
def build_docs_scope_catalog(rows: list[dict]) -> DocsScopeCatalog:
|
|
||||||
"""Derive searchable terms from catalog layers only (existing RAG index rows)."""
|
|
||||||
catalog = DocsScopeCatalog()
|
|
||||||
for row in rows:
|
|
||||||
layer = str(row.get("layer") or "")
|
|
||||||
meta = row.get("metadata")
|
|
||||||
if not isinstance(meta, dict):
|
|
||||||
meta = {}
|
|
||||||
path = str(row.get("path") or "")
|
|
||||||
title = str(row.get("title") or "")
|
|
||||||
content = str(row.get("content") or "")
|
|
||||||
|
|
||||||
if layer == "D1_DOCUMENT_CATALOG":
|
|
||||||
_ingest_d1_row(catalog, path=path, title=title, content=content, metadata=meta)
|
|
||||||
elif layer == "D3_ENTITY_CATALOG":
|
|
||||||
_ingest_d3_row(catalog, path=path, title=title, metadata=meta)
|
|
||||||
|
|
||||||
return catalog
|
|
||||||
|
|
||||||
|
|
||||||
def _ingest_d1_row(
|
|
||||||
catalog: DocsScopeCatalog,
|
|
||||||
*,
|
|
||||||
path: str,
|
|
||||||
title: str,
|
|
||||||
content: str,
|
|
||||||
metadata: dict,
|
|
||||||
) -> None:
|
|
||||||
doc_type = _norm_text(metadata.get("type") or metadata.get("doc_type"))
|
|
||||||
domain = _norm_text(metadata.get("domain"))
|
|
||||||
subdomain = _norm_text(metadata.get("subdomain"))
|
|
||||||
name = _norm_text(metadata.get("name"))
|
|
||||||
summary = _norm_text(metadata.get("summary_text"))
|
|
||||||
endpoint = _norm_text(metadata.get("endpoint"))
|
|
||||||
|
|
||||||
entities = [_norm_text(e) for e in _split_multi(metadata.get("entities"))]
|
|
||||||
tags = [_norm_text(t) for t in _split_multi(metadata.get("tags"))]
|
|
||||||
|
|
||||||
if domain:
|
|
||||||
catalog.domain_values.add(domain)
|
|
||||||
if domain and subdomain:
|
|
||||||
catalog.subdomain_pairs.append((domain, subdomain))
|
|
||||||
|
|
||||||
blob = " ".join(x for x in (name, title, summary, content) if x)
|
|
||||||
for ent in entities:
|
|
||||||
if ent:
|
|
||||||
catalog.entity_records.append(
|
|
||||||
{
|
|
||||||
"name": ent,
|
|
||||||
"domain": domain or None,
|
|
||||||
"subdomain": subdomain or None,
|
|
||||||
"source_layer": "D1_DOCUMENT_CATALOG",
|
|
||||||
"path": path,
|
|
||||||
"blob": blob,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
for tag in tags:
|
|
||||||
if tag and len(tag) >= 3:
|
|
||||||
catalog.entity_records.append(
|
|
||||||
{
|
|
||||||
"name": tag,
|
|
||||||
"domain": domain or None,
|
|
||||||
"subdomain": subdomain or None,
|
|
||||||
"source_layer": "D1_DOCUMENT_CATALOG",
|
|
||||||
"path": path,
|
|
||||||
"blob": blob,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
is_api_method = doc_type == "api_method" or "api_method" in path.lower()
|
|
||||||
if is_api_method or endpoint:
|
|
||||||
ep = endpoint or _endpoint_from_title(title)
|
|
||||||
if ep:
|
|
||||||
catalog.api_records.append(
|
|
||||||
{
|
|
||||||
"endpoint": ep,
|
|
||||||
"domain": domain or None,
|
|
||||||
"source_layer": "D1_DOCUMENT_CATALOG",
|
|
||||||
"path": path,
|
|
||||||
"title": title,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _ingest_d3_row(
|
|
||||||
catalog: DocsScopeCatalog,
|
|
||||||
*,
|
|
||||||
path: str,
|
|
||||||
title: str,
|
|
||||||
metadata: dict,
|
|
||||||
) -> None:
|
|
||||||
entity_name = str(metadata.get("entity_name") or "").strip()
|
|
||||||
domain = _norm_text(metadata.get("domain"))
|
|
||||||
subdomain = _norm_text(metadata.get("subdomain"))
|
|
||||||
module = _norm_text(metadata.get("module"))
|
|
||||||
source_path = str(metadata.get("source_path") or "").strip()
|
|
||||||
tags = [_norm_text(t) for t in _split_multi(metadata.get("tags"))]
|
|
||||||
|
|
||||||
if domain:
|
|
||||||
catalog.domain_values.add(domain)
|
|
||||||
if domain and subdomain:
|
|
||||||
catalog.subdomain_pairs.append((domain, subdomain))
|
|
||||||
|
|
||||||
blob = " ".join(
|
|
||||||
_norm_text(x)
|
|
||||||
for x in (entity_name, title, module, source_path, " ".join(tags))
|
|
||||||
if x
|
|
||||||
)
|
|
||||||
if entity_name:
|
|
||||||
catalog.entity_records.append(
|
|
||||||
{
|
|
||||||
"name": _norm_text(entity_name),
|
|
||||||
"domain": domain or None,
|
|
||||||
"subdomain": subdomain or None,
|
|
||||||
"module": module or None,
|
|
||||||
"source_layer": "D3_ENTITY_CATALOG",
|
|
||||||
"path": path or source_path,
|
|
||||||
"blob": blob,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _endpoint_from_title(title: str) -> str:
|
|
||||||
t = str(title or "").strip()
|
|
||||||
if not t:
|
|
||||||
return ""
|
|
||||||
upper = t.upper()
|
|
||||||
for method in ("GET ", "POST ", "PUT ", "PATCH ", "DELETE "):
|
|
||||||
if method in upper:
|
|
||||||
idx = upper.index(method)
|
|
||||||
rest = t[idx:].split()
|
|
||||||
if len(rest) >= 2 and rest[1].startswith("/"):
|
|
||||||
return _norm_text(rest[1])
|
|
||||||
m = re.search(r"(\/[a-z0-9_./{}-]+)", t, re.IGNORECASE)
|
|
||||||
return _norm_text(m.group(1)) if m else ""
|
|
||||||
@@ -1,443 +0,0 @@
|
|||||||
"""Deterministic scope resolution from query + derived DOCS catalog (pre-LLM).
|
|
||||||
|
|
||||||
Matches the user query against catalog terms (exact / normalized). Optional embedding-based
|
|
||||||
retrieval can extend candidates later; final ``scope_type`` never relies on embeddings alone.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import re
|
|
||||||
from dataclasses import dataclass, field
|
|
||||||
|
|
||||||
from app.core.agent.processes.v2.intent_router.modules.scope_catalog import DocsScopeCatalog
|
|
||||||
from app.core.agent.processes.v2.intent_router.modules.target_terms import TargetTermsAnalysis
|
|
||||||
from app.core.agent.utils.process_v2.models import ScopeCandidate, V2ScopeType
|
|
||||||
|
|
||||||
|
|
||||||
_SCORE_EXACT = 1.0
|
|
||||||
_SCORE_NORMALIZED = 0.88
|
|
||||||
_SCORE_SOFT = 0.72
|
|
||||||
_STRONG_THRESHOLD = 0.85
|
|
||||||
|
|
||||||
_ENUM_MARKERS_RU = (
|
|
||||||
"какие ",
|
|
||||||
"какие\n",
|
|
||||||
"какой ",
|
|
||||||
"какого ",
|
|
||||||
"список",
|
|
||||||
"перечисли",
|
|
||||||
"перечислить",
|
|
||||||
"все api",
|
|
||||||
"все методы",
|
|
||||||
"какие api",
|
|
||||||
"какие методы",
|
|
||||||
"каких ",
|
|
||||||
)
|
|
||||||
_SINGLE_SEGMENT_ENDPOINT_ALLOWLIST = frozenset(
|
|
||||||
{
|
|
||||||
"/health",
|
|
||||||
"/send",
|
|
||||||
"/healthz",
|
|
||||||
"/ready",
|
|
||||||
"/live",
|
|
||||||
"/metrics",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
_PROJECT_WIDE_MARKERS = (
|
|
||||||
"в проекте",
|
|
||||||
"в системе",
|
|
||||||
"в приложении",
|
|
||||||
"по проекту",
|
|
||||||
"во всем проекте",
|
|
||||||
"overall",
|
|
||||||
"in the project",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True)
|
|
||||||
class ScopeResolution:
|
|
||||||
scope_type: str = V2ScopeType.UNKNOWN
|
|
||||||
candidate_domains: list[ScopeCandidate] = field(default_factory=list)
|
|
||||||
candidate_subdomains: list[ScopeCandidate] = field(default_factory=list)
|
|
||||||
candidate_entities: list[ScopeCandidate] = field(default_factory=list)
|
|
||||||
candidate_apis: list[ScopeCandidate] = field(default_factory=list)
|
|
||||||
strong_domain: str | None = None
|
|
||||||
strong_subdomain: str | None = None
|
|
||||||
strong_entity_names: list[str] = field(default_factory=list)
|
|
||||||
strong_endpoint_paths: list[str] = field(default_factory=list)
|
|
||||||
catalog_loaded: bool = False
|
|
||||||
|
|
||||||
|
|
||||||
def _catalog_has_index_terms(catalog: DocsScopeCatalog) -> bool:
|
|
||||||
return bool(
|
|
||||||
catalog.domain_values
|
|
||||||
or catalog.subdomain_pairs
|
|
||||||
or catalog.entity_records
|
|
||||||
or catalog.api_records
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def plausible_doc_endpoint_paths(paths: list[str]) -> list[str]:
|
|
||||||
"""Drop spurious ``/token`` paths from api-like heuristics (e.g. ``/billing`` after ``api``)."""
|
|
||||||
out: list[str] = []
|
|
||||||
for raw in paths:
|
|
||||||
p = str(raw or "").strip().lower()
|
|
||||||
if not p.startswith("/"):
|
|
||||||
continue
|
|
||||||
segments = [s for s in p.split("/") if s]
|
|
||||||
if len(segments) >= 2:
|
|
||||||
out.append(p)
|
|
||||||
continue
|
|
||||||
if len(segments) == 1 and p in _SINGLE_SEGMENT_ENDPOINT_ALLOWLIST:
|
|
||||||
out.append(p)
|
|
||||||
continue
|
|
||||||
return out
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_docs_scope(
|
|
||||||
normalized_query: str,
|
|
||||||
terms: TargetTermsAnalysis,
|
|
||||||
catalog: DocsScopeCatalog | None,
|
|
||||||
) -> ScopeResolution:
|
|
||||||
"""Lexical scope resolution; embeddings never set final scope alone (not used here)."""
|
|
||||||
resolution = ScopeResolution()
|
|
||||||
if catalog is None:
|
|
||||||
return resolution
|
|
||||||
if not _catalog_has_index_terms(catalog):
|
|
||||||
return resolution
|
|
||||||
|
|
||||||
resolution.catalog_loaded = True
|
|
||||||
query_l = _norm_query(normalized_query)
|
|
||||||
if not query_l:
|
|
||||||
resolution.scope_type = V2ScopeType.UNKNOWN
|
|
||||||
return resolution
|
|
||||||
|
|
||||||
_collect_domain_candidates(query_l, catalog, resolution)
|
|
||||||
_collect_subdomain_candidates(query_l, catalog, resolution)
|
|
||||||
_collect_entity_candidates(query_l, catalog, resolution)
|
|
||||||
_collect_api_candidates(query_l, catalog, resolution)
|
|
||||||
|
|
||||||
_dedupe_candidates(resolution)
|
|
||||||
|
|
||||||
endpoint_paths = plausible_doc_endpoint_paths(list(terms.endpoint_paths))
|
|
||||||
strong_api = _pick_strong(resolution.candidate_apis)
|
|
||||||
strong_entity = _pick_strong(resolution.candidate_entities)
|
|
||||||
strong_sub = _pick_strong(resolution.candidate_subdomains)
|
|
||||||
strong_dom = _pick_strong(resolution.candidate_domains)
|
|
||||||
|
|
||||||
resolution.strong_endpoint_paths = list(dict.fromkeys(endpoint_paths))
|
|
||||||
|
|
||||||
if endpoint_paths:
|
|
||||||
resolution.scope_type = V2ScopeType.ENTITY
|
|
||||||
resolution.strong_entity_names = _merge_unique(resolution.strong_entity_names, _entities_for_endpoints(endpoint_paths, catalog))
|
|
||||||
return resolution
|
|
||||||
|
|
||||||
if strong_api and strong_api.score >= _STRONG_THRESHOLD:
|
|
||||||
resolution.scope_type = V2ScopeType.ENTITY
|
|
||||||
resolution.strong_endpoint_paths = _merge_unique(resolution.strong_endpoint_paths, [strong_api.value])
|
|
||||||
return resolution
|
|
||||||
|
|
||||||
strong_sub_pre = _pick_strong(resolution.candidate_subdomains)
|
|
||||||
if (
|
|
||||||
strong_sub_pre
|
|
||||||
and strong_sub_pre.score >= _STRONG_THRESHOLD
|
|
||||||
and _subdomain_aligned_with_query(query_l, strong_sub_pre.value)
|
|
||||||
):
|
|
||||||
resolution.scope_type = V2ScopeType.SUBDOMAIN
|
|
||||||
parts = _split_subdomain_value(strong_sub_pre.value)
|
|
||||||
if parts:
|
|
||||||
resolution.strong_domain = parts[0]
|
|
||||||
resolution.strong_subdomain = parts[1]
|
|
||||||
return resolution
|
|
||||||
|
|
||||||
if strong_entity and strong_entity.score >= _STRONG_THRESHOLD:
|
|
||||||
resolution.scope_type = V2ScopeType.ENTITY
|
|
||||||
resolution.strong_entity_names = _merge_unique(
|
|
||||||
resolution.strong_entity_names,
|
|
||||||
[strong_entity.value],
|
|
||||||
)
|
|
||||||
return resolution
|
|
||||||
|
|
||||||
if strong_sub and strong_sub.score >= _STRONG_THRESHOLD:
|
|
||||||
resolution.scope_type = V2ScopeType.SUBDOMAIN
|
|
||||||
parts = _split_subdomain_value(strong_sub.value)
|
|
||||||
if parts:
|
|
||||||
resolution.strong_domain = parts[0]
|
|
||||||
resolution.strong_subdomain = parts[1]
|
|
||||||
return resolution
|
|
||||||
|
|
||||||
if strong_dom and strong_dom.score >= _STRONG_THRESHOLD:
|
|
||||||
resolution.scope_type = V2ScopeType.DOMAIN
|
|
||||||
resolution.strong_domain = strong_dom.value
|
|
||||||
return resolution
|
|
||||||
|
|
||||||
if _is_global_enumeration(query_l, has_strong_any=bool(_any_strong(resolution))):
|
|
||||||
resolution.scope_type = V2ScopeType.GLOBAL
|
|
||||||
return resolution
|
|
||||||
|
|
||||||
resolution.scope_type = V2ScopeType.UNKNOWN
|
|
||||||
return resolution
|
|
||||||
|
|
||||||
|
|
||||||
def promote_target_terms(
|
|
||||||
raw_terms: list[str],
|
|
||||||
terms: TargetTermsAnalysis,
|
|
||||||
resolution: ScopeResolution,
|
|
||||||
) -> list[str]:
|
|
||||||
"""Keep only high-confidence terms in ``target_terms``; weak matches stay in candidate_* only."""
|
|
||||||
if not resolution.catalog_loaded:
|
|
||||||
return list(raw_terms)
|
|
||||||
out: list[str] = []
|
|
||||||
strong_values = {c.value for c in _all_candidates(resolution) if c.score >= _STRONG_THRESHOLD}
|
|
||||||
strong_values |= {c.value for c in _all_candidates(resolution) if c.match_type == "exact"}
|
|
||||||
strong_entity = set(resolution.strong_entity_names)
|
|
||||||
endpoints = set(terms.endpoint_paths)
|
|
||||||
aliases = set(terms.matched_aliases)
|
|
||||||
|
|
||||||
for term in raw_terms:
|
|
||||||
t = str(term or "").strip()
|
|
||||||
if not t:
|
|
||||||
continue
|
|
||||||
tl = t.lower()
|
|
||||||
if t in endpoints or tl in {e.lower() for e in endpoints}:
|
|
||||||
_append_unique(out, tl if tl.startswith("/") else t)
|
|
||||||
continue
|
|
||||||
if t in aliases or tl in {a.lower() for a in aliases}:
|
|
||||||
_append_unique(out, tl)
|
|
||||||
continue
|
|
||||||
if tl in strong_values or t in strong_entity:
|
|
||||||
_append_unique(out, tl)
|
|
||||||
continue
|
|
||||||
if _is_explicit_identifier(t) and tl in strong_entity:
|
|
||||||
_append_unique(out, tl)
|
|
||||||
continue
|
|
||||||
# Drop weak/ungrounded terms (remain only in candidates on anchors)
|
|
||||||
return out
|
|
||||||
|
|
||||||
|
|
||||||
def _all_candidates(resolution: ScopeResolution) -> list[ScopeCandidate]:
|
|
||||||
return [
|
|
||||||
*resolution.candidate_domains,
|
|
||||||
*resolution.candidate_subdomains,
|
|
||||||
*resolution.candidate_entities,
|
|
||||||
*resolution.candidate_apis,
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def _any_strong(resolution: ScopeResolution) -> bool:
|
|
||||||
return any(c.score >= _STRONG_THRESHOLD for c in _all_candidates(resolution))
|
|
||||||
|
|
||||||
|
|
||||||
def _pick_strong(candidates: list[ScopeCandidate]) -> ScopeCandidate | None:
|
|
||||||
if not candidates:
|
|
||||||
return None
|
|
||||||
return max(candidates, key=lambda c: (c.score, len(c.value)))
|
|
||||||
|
|
||||||
|
|
||||||
def _norm_query(q: str) -> str:
|
|
||||||
return re.sub(r"\s+", " ", str(q or "").strip().lower())
|
|
||||||
|
|
||||||
|
|
||||||
def _append_unique(items: list[str], value: str) -> None:
|
|
||||||
if value and value not in items:
|
|
||||||
items.append(value)
|
|
||||||
|
|
||||||
|
|
||||||
def _merge_unique(a: list[str], b: list[str]) -> list[str]:
|
|
||||||
return list(dict.fromkeys([*a, *b]))
|
|
||||||
|
|
||||||
|
|
||||||
def _is_explicit_identifier(token: str) -> bool:
|
|
||||||
return bool(re.fullmatch(r"[A-Za-z][A-Za-z0-9_]+", token))
|
|
||||||
|
|
||||||
|
|
||||||
def _split_subdomain_value(value: str) -> tuple[str, str] | None:
|
|
||||||
parts = str(value or "").split("::", 1)
|
|
||||||
if len(parts) == 2 and parts[0] and parts[1]:
|
|
||||||
return parts[0].strip().lower(), parts[1].strip().lower()
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def _subdomain_aligned_with_query(query_l: str, composite: str) -> bool:
|
|
||||||
"""True when both domain and subdomain tokens match the query (substring / token match)."""
|
|
||||||
parts = str(composite or "").split("::", 1)
|
|
||||||
if len(parts) != 2:
|
|
||||||
return False
|
|
||||||
dom, sub = parts[0].strip().lower(), parts[1].strip().lower()
|
|
||||||
s_dom, _ = _match_score(query_l, dom)
|
|
||||||
s_sub, _ = _match_score(query_l, sub)
|
|
||||||
return s_dom > 0 and s_sub > 0
|
|
||||||
|
|
||||||
|
|
||||||
def _entities_for_endpoints(endpoint_paths: list[str], catalog: DocsScopeCatalog) -> list[str]:
|
|
||||||
found: list[str] = []
|
|
||||||
eps = {e.lower() for e in endpoint_paths if e}
|
|
||||||
for rec in catalog.entity_records:
|
|
||||||
blob = str(rec.get("blob") or "").lower()
|
|
||||||
name = str(rec.get("name") or "").strip().lower()
|
|
||||||
if not name:
|
|
||||||
continue
|
|
||||||
if any(ep and ep in blob for ep in eps):
|
|
||||||
_append_unique(found, name)
|
|
||||||
return found
|
|
||||||
|
|
||||||
|
|
||||||
def _collect_domain_candidates(query_l: str, catalog: DocsScopeCatalog, resolution: ScopeResolution) -> None:
|
|
||||||
for dom in catalog.domain_values:
|
|
||||||
if not dom:
|
|
||||||
continue
|
|
||||||
score, mtype = _match_score(query_l, dom)
|
|
||||||
if score <= 0:
|
|
||||||
continue
|
|
||||||
resolution.candidate_domains.append(
|
|
||||||
ScopeCandidate(
|
|
||||||
value=dom,
|
|
||||||
score=score,
|
|
||||||
source_layer="D1_DOCUMENT_CATALOG",
|
|
||||||
match_type=mtype,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _collect_subdomain_candidates(query_l: str, catalog: DocsScopeCatalog, resolution: ScopeResolution) -> None:
|
|
||||||
seen: set[str] = set()
|
|
||||||
for dom, sub in catalog.subdomain_pairs:
|
|
||||||
if not dom or not sub:
|
|
||||||
continue
|
|
||||||
composite = f"{dom}::{sub}"
|
|
||||||
if composite in seen:
|
|
||||||
continue
|
|
||||||
seen.add(composite)
|
|
||||||
score_dom, _ = _match_score(query_l, dom)
|
|
||||||
score_sub, mt_sub = _match_score(query_l, sub)
|
|
||||||
phrase = _phrase_score(query_l, dom, sub)
|
|
||||||
if phrase > 0:
|
|
||||||
score = phrase
|
|
||||||
mt = "normalized"
|
|
||||||
elif score_dom > 0 and score_sub > 0:
|
|
||||||
score = min(score_dom, score_sub)
|
|
||||||
mt = mt_sub
|
|
||||||
else:
|
|
||||||
# Avoid promoting a (domain, subdomain) pair when only the domain token matches.
|
|
||||||
score = 0.0
|
|
||||||
mt = mt_sub
|
|
||||||
if score <= 0:
|
|
||||||
continue
|
|
||||||
resolution.candidate_subdomains.append(
|
|
||||||
ScopeCandidate(
|
|
||||||
value=composite,
|
|
||||||
score=score,
|
|
||||||
source_layer="D1_DOCUMENT_CATALOG",
|
|
||||||
match_type=mt,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _collect_entity_candidates(query_l: str, catalog: DocsScopeCatalog, resolution: ScopeResolution) -> None:
|
|
||||||
for rec in catalog.entity_records:
|
|
||||||
name = str(rec.get("name") or "").strip().lower()
|
|
||||||
if not name or len(name) < 2:
|
|
||||||
continue
|
|
||||||
blob = str(rec.get("blob") or "").lower()
|
|
||||||
layer = str(rec.get("source_layer") or "")
|
|
||||||
score, mtype = _match_entity(query_l, name, blob)
|
|
||||||
if score <= 0:
|
|
||||||
continue
|
|
||||||
resolution.candidate_entities.append(
|
|
||||||
ScopeCandidate(value=name, score=score, source_layer=layer, match_type=mtype)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _collect_api_candidates(query_l: str, catalog: DocsScopeCatalog, resolution: ScopeResolution) -> None:
|
|
||||||
for rec in catalog.api_records:
|
|
||||||
ep = str(rec.get("endpoint") or "").strip().lower()
|
|
||||||
if not ep:
|
|
||||||
continue
|
|
||||||
layer = str(rec.get("source_layer") or "")
|
|
||||||
score, mtype = _match_score(query_l, ep.replace(" ", ""))
|
|
||||||
if score <= 0:
|
|
||||||
continue
|
|
||||||
resolution.candidate_apis.append(
|
|
||||||
ScopeCandidate(value=ep, score=score, source_layer=layer, match_type=mtype)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _phrase_score(query_l: str, dom: str, sub: str) -> float:
|
|
||||||
if _contains_token(query_l, dom) and _contains_token(query_l, sub):
|
|
||||||
return max(_SCORE_NORMALIZED, 0.9)
|
|
||||||
joined = re.sub(r"\s+", " ", f"{dom} {sub}".strip())
|
|
||||||
if joined in query_l or query_l in joined:
|
|
||||||
return _SCORE_NORMALIZED
|
|
||||||
return 0.0
|
|
||||||
|
|
||||||
|
|
||||||
def _match_entity(query_l: str, name: str, blob: str) -> tuple[float, str]:
|
|
||||||
score, mt = _match_score(query_l, name)
|
|
||||||
if score > 0:
|
|
||||||
return score, mt
|
|
||||||
if name in blob and len(name) >= 4:
|
|
||||||
# cross-language hints: name appears in catalog blob; small boost if query token overlaps blob
|
|
||||||
q_tokens = set(query_l.split())
|
|
||||||
b_tokens = set(blob.split())
|
|
||||||
overlap = q_tokens & b_tokens
|
|
||||||
if overlap and (q_tokens & {name} or name[:4] in query_l):
|
|
||||||
return _SCORE_SOFT, "normalized"
|
|
||||||
return 0.0, "normalized"
|
|
||||||
|
|
||||||
|
|
||||||
def _match_score(query_l: str, value: str) -> tuple[float, str]:
|
|
||||||
v = str(value or "").strip().lower()
|
|
||||||
if not v:
|
|
||||||
return 0.0, "normalized"
|
|
||||||
v_compact = v.replace(" ", "")
|
|
||||||
q_compact = query_l.replace(" ", "")
|
|
||||||
if v == query_l or v_compact == q_compact:
|
|
||||||
return _SCORE_EXACT, "exact"
|
|
||||||
if _contains_token(query_l, v) or _contains_token(query_l, v.replace("/", " ")):
|
|
||||||
return _SCORE_EXACT, "exact"
|
|
||||||
if v in q_compact or v_compact in q_compact:
|
|
||||||
return _SCORE_NORMALIZED, "normalized"
|
|
||||||
if v in query_l:
|
|
||||||
return _SCORE_NORMALIZED, "normalized"
|
|
||||||
# prefix / slug
|
|
||||||
for token in query_l.split():
|
|
||||||
if token.startswith(v[: min(4, len(v))]) and len(v) >= 4:
|
|
||||||
return _SCORE_SOFT, "normalized"
|
|
||||||
return 0.0, "normalized"
|
|
||||||
|
|
||||||
|
|
||||||
def _contains_token(hay: str, needle: str) -> bool:
|
|
||||||
if not needle:
|
|
||||||
return False
|
|
||||||
return f" {needle} " in f" {hay} "
|
|
||||||
|
|
||||||
|
|
||||||
def _dedupe_candidates(resolution: ScopeResolution) -> None:
|
|
||||||
resolution.candidate_domains = _dedupe_list(resolution.candidate_domains)
|
|
||||||
resolution.candidate_subdomains = _dedupe_list(resolution.candidate_subdomains)
|
|
||||||
resolution.candidate_entities = _dedupe_list(resolution.candidate_entities)
|
|
||||||
resolution.candidate_apis = _dedupe_list(resolution.candidate_apis)
|
|
||||||
|
|
||||||
|
|
||||||
def _dedupe_list(items: list[ScopeCandidate]) -> list[ScopeCandidate]:
|
|
||||||
best: dict[str, ScopeCandidate] = {}
|
|
||||||
for c in items:
|
|
||||||
key = f"{c.value}|{c.source_layer}"
|
|
||||||
prev = best.get(key)
|
|
||||||
if prev is None or c.score > prev.score:
|
|
||||||
best[key] = c
|
|
||||||
return sorted(best.values(), key=lambda c: (-c.score, c.value))
|
|
||||||
|
|
||||||
|
|
||||||
def _is_global_enumeration(query_l: str, *, has_strong_any: bool) -> bool:
|
|
||||||
if has_strong_any:
|
|
||||||
return False
|
|
||||||
if any(m in query_l for m in _PROJECT_WIDE_MARKERS) and any(
|
|
||||||
m in query_l for m in ("какие", "какой", "список", "перечисли", "метод", "api")
|
|
||||||
):
|
|
||||||
return True
|
|
||||||
if any(query_l.strip().startswith(m.strip()) for m in _ENUM_MARKERS_RU if len(m.strip()) > 2):
|
|
||||||
if any(k in query_l for k in ("метод", "api", "ручк", "эндпоинт")):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
@@ -53,18 +53,6 @@ class _EndpointPathExtractor:
|
|||||||
_PATH_RE = re.compile(r"`([^`]+)`|(/[A-Za-z0-9_./{}-]+)")
|
_PATH_RE = re.compile(r"`([^`]+)`|(/[A-Za-z0-9_./{}-]+)")
|
||||||
_VALID_ENDPOINT_RE = re.compile(r"^/[a-z0-9._/-]+(?:/\{[a-z0-9_]+\})?$")
|
_VALID_ENDPOINT_RE = re.compile(r"^/[a-z0-9._/-]+(?:/\{[a-z0-9_]+\})?$")
|
||||||
_DOC_EXTENSIONS = (".md", ".yaml", ".yml", ".json")
|
_DOC_EXTENSIONS = (".md", ".yaml", ".yml", ".json")
|
||||||
_FILESYSTEM_PREFIXES = (
|
|
||||||
"/users/",
|
|
||||||
"/home/",
|
|
||||||
"/tmp/",
|
|
||||||
"/var/",
|
|
||||||
"/opt/",
|
|
||||||
"/etc/",
|
|
||||||
"/private/",
|
|
||||||
"/mnt/",
|
|
||||||
"/workspace/",
|
|
||||||
"/workspaces/",
|
|
||||||
)
|
|
||||||
|
|
||||||
def extract(self, query: str) -> list[str]:
|
def extract(self, query: str) -> list[str]:
|
||||||
values: list[str] = []
|
values: list[str] = []
|
||||||
@@ -84,8 +72,6 @@ class _EndpointPathExtractor:
|
|||||||
def _is_endpoint(self, token: str) -> bool:
|
def _is_endpoint(self, token: str) -> bool:
|
||||||
if not token or not self._VALID_ENDPOINT_RE.fullmatch(token):
|
if not token or not self._VALID_ENDPOINT_RE.fullmatch(token):
|
||||||
return False
|
return False
|
||||||
if token.startswith(self._FILESYSTEM_PREFIXES):
|
|
||||||
return False
|
|
||||||
return not token.endswith(self._DOC_EXTENSIONS)
|
return not token.endswith(self._DOC_EXTENSIONS)
|
||||||
|
|
||||||
def _append_unique(self, items: list[str], value: str) -> None:
|
def _append_unique(self, items: list[str], value: str) -> None:
|
||||||
|
|||||||
@@ -2,99 +2,18 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import re
|
|
||||||
from collections.abc import Callable
|
|
||||||
from dataclasses import replace
|
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
|
|
||||||
from app.core.agent.processes.v2.intent_router.modules.anchors import V2AnchorExtractor
|
from app.core.agent.processes.v2.intent_router.modules.anchors import V2AnchorExtractor
|
||||||
from app.core.agent.processes.v2.intent_router.modules.normalizer import V2QueryNormalizer
|
from app.core.agent.processes.v2.intent_router.modules.normalizer import V2QueryNormalizer
|
||||||
from app.core.agent.processes.v2.intent_router.modules.scope_catalog import DocsScopeCatalog, build_docs_scope_catalog
|
|
||||||
from app.core.agent.processes.v2.intent_router.modules.scope_resolver import (
|
|
||||||
plausible_doc_endpoint_paths,
|
|
||||||
promote_target_terms,
|
|
||||||
resolve_docs_scope,
|
|
||||||
)
|
|
||||||
from app.core.agent.processes.v2.intent_router.modules.target_terms import V2TargetTermsExtractor
|
from app.core.agent.processes.v2.intent_router.modules.target_terms import V2TargetTermsExtractor
|
||||||
from app.core.agent.processes.v2.intent_router.models import QueryFeatures
|
from app.core.agent.processes.v2.intent_router.models import QueryFeatures
|
||||||
from app.core.agent.processes.v2.intent_router.routers.confidence import V2ConfidenceAdjuster
|
from app.core.agent.processes.v2.intent_router.routers.confidence import V2ConfidenceAdjuster
|
||||||
from app.core.agent.processes.v2.intent_router.routers.docs_subintent_resolver import DocsSubintentResolver
|
|
||||||
from app.core.agent.processes.v2.intent_router.routers.fallback import V2FallbackRouter
|
from app.core.agent.processes.v2.intent_router.routers.fallback import V2FallbackRouter
|
||||||
from app.core.agent.processes.v2.intent_router.routers.llm import V2LlmRouter
|
from app.core.agent.processes.v2.intent_router.routers.llm import V2LlmRouter
|
||||||
from app.core.agent.processes.v2.intent_router.routers.route_catalog import V2RouteCatalog
|
from app.core.agent.processes.v2.intent_router.routers.route_catalog import V2RouteCatalog
|
||||||
from app.core.agent.processes.v2.intent_router.routers.validator import V2RouteValidator
|
from app.core.agent.processes.v2.intent_router.routers.validator import V2RouteValidator
|
||||||
from app.core.agent.utils.process_v2.models import V2RouteResult, V2ScopeType, V2Subintent
|
from app.core.agent.processes.v2.models import V2RouteResult
|
||||||
from app.core.agent.utils.llm import AgentLlmService
|
from app.core.agent.utils.llm import AgentLlmService
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from app.core.rag.persistence.query_repository import RagQueryRepository
|
|
||||||
|
|
||||||
|
|
||||||
class _ExplicitDocsUpdateResolver:
|
|
||||||
_UPDATE_MARKERS = (
|
|
||||||
"собери документац",
|
|
||||||
"сгенерир",
|
|
||||||
"построй документац",
|
|
||||||
"обнови документац",
|
|
||||||
"обновить документац",
|
|
||||||
"generate documentation",
|
|
||||||
"build documentation",
|
|
||||||
"update documentation",
|
|
||||||
)
|
|
||||||
_FEATURE_MARKERS = (
|
|
||||||
"/features/",
|
|
||||||
"\\features\\",
|
|
||||||
"feature",
|
|
||||||
"системной аналитик",
|
|
||||||
"confluence",
|
|
||||||
)
|
|
||||||
_PATH_PATTERN = re.compile(r"(/[^\n`]+?\.md)")
|
|
||||||
_URL_PATTERN = re.compile(r"https?://[^\s)]*confluence[^\s)]*")
|
|
||||||
|
|
||||||
def matches(self, user_query: str) -> bool:
|
|
||||||
query = str(user_query or "")
|
|
||||||
lowered = query.lower()
|
|
||||||
if not any(marker in lowered for marker in self._UPDATE_MARKERS):
|
|
||||||
return False
|
|
||||||
path = self._extract_path(query)
|
|
||||||
if path and self._is_feature_source(path):
|
|
||||||
return True
|
|
||||||
url = self._extract_confluence_url(query)
|
|
||||||
if url:
|
|
||||||
return True
|
|
||||||
return any(marker in lowered for marker in self._FEATURE_MARKERS)
|
|
||||||
|
|
||||||
def _extract_path(self, query: str) -> str:
|
|
||||||
if "`" in query:
|
|
||||||
for chunk in query.split("`"):
|
|
||||||
value = chunk.strip().strip('"').strip("'")
|
|
||||||
if value.endswith(".md") and value.startswith("/"):
|
|
||||||
return value
|
|
||||||
match = self._PATH_PATTERN.search(query)
|
|
||||||
return match.group(1).strip().strip('"').strip("'") if match else ""
|
|
||||||
|
|
||||||
def _extract_confluence_url(self, query: str) -> str:
|
|
||||||
match = self._URL_PATTERN.search(query)
|
|
||||||
return match.group(0).strip() if match else ""
|
|
||||||
|
|
||||||
def _is_feature_source(self, path: str) -> bool:
|
|
||||||
lowered = str(path or "").lower()
|
|
||||||
return "/feature" in lowered
|
|
||||||
|
|
||||||
|
|
||||||
class _ExplicitFileLookupResolver:
|
|
||||||
def matches(self, anchor_analysis) -> bool:
|
|
||||||
return bool(getattr(anchor_analysis.anchors, "file_names", []))
|
|
||||||
|
|
||||||
|
|
||||||
def _scope_candidate_dict(candidate) -> dict[str, object]:
|
|
||||||
return {
|
|
||||||
"value": candidate.value,
|
|
||||||
"score": candidate.score,
|
|
||||||
"source_layer": candidate.source_layer,
|
|
||||||
"match_type": candidate.match_type,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class V2IntentRouter:
|
class V2IntentRouter:
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -106,7 +25,6 @@ class V2IntentRouter:
|
|||||||
enable_llm_disambiguation: bool = True,
|
enable_llm_disambiguation: bool = True,
|
||||||
route_catalog: V2RouteCatalog | None = None,
|
route_catalog: V2RouteCatalog | None = None,
|
||||||
confidence_adjuster: V2ConfidenceAdjuster | None = None,
|
confidence_adjuster: V2ConfidenceAdjuster | None = None,
|
||||||
scope_rows_provider: Callable[[str], list[dict]] | None = None,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
self._normalizer = normalizer or V2QueryNormalizer()
|
self._normalizer = normalizer or V2QueryNormalizer()
|
||||||
self._target_terms_extractor = target_terms_extractor or V2TargetTermsExtractor()
|
self._target_terms_extractor = target_terms_extractor or V2TargetTermsExtractor()
|
||||||
@@ -114,99 +32,26 @@ class V2IntentRouter:
|
|||||||
self._catalog = route_catalog or V2RouteCatalog()
|
self._catalog = route_catalog or V2RouteCatalog()
|
||||||
self._validator = V2RouteValidator(self._catalog)
|
self._validator = V2RouteValidator(self._catalog)
|
||||||
self._fallback_router = V2FallbackRouter()
|
self._fallback_router = V2FallbackRouter()
|
||||||
self._docs_subintent_resolver = DocsSubintentResolver()
|
|
||||||
self._confidence_adjuster = confidence_adjuster or V2ConfidenceAdjuster()
|
self._confidence_adjuster = confidence_adjuster or V2ConfidenceAdjuster()
|
||||||
self._enable_llm_disambiguation = enable_llm_disambiguation
|
self._enable_llm_disambiguation = enable_llm_disambiguation
|
||||||
self._llm_router = V2LlmRouter(llm, catalog=self._catalog) if llm is not None else None
|
self._llm_router = V2LlmRouter(llm, catalog=self._catalog) if llm is not None else None
|
||||||
self._scope_rows_provider = scope_rows_provider
|
|
||||||
self._explicit_docs_update_resolver = _ExplicitDocsUpdateResolver()
|
|
||||||
self._explicit_file_lookup_resolver = _ExplicitFileLookupResolver()
|
|
||||||
|
|
||||||
def route(self, user_query: str, *, rag_session_id: str | None = None) -> V2RouteResult:
|
def route(self, user_query: str) -> V2RouteResult:
|
||||||
normalized_query = self._normalizer.normalize(user_query)
|
normalized_query = self._normalizer.normalize(user_query)
|
||||||
target_terms_analysis = self._target_terms_extractor.extract(normalized_query)
|
target_terms_analysis = self._target_terms_extractor.extract(normalized_query)
|
||||||
sanitized_eps = plausible_doc_endpoint_paths(list(target_terms_analysis.endpoint_paths))
|
anchor_analysis = self._anchor_extractor.extract(normalized_query, target_terms_analysis)
|
||||||
if sanitized_eps != list(target_terms_analysis.endpoint_paths):
|
|
||||||
target_terms_analysis = replace(target_terms_analysis, endpoint_paths=sanitized_eps)
|
|
||||||
allowed_paths = set(sanitized_eps)
|
|
||||||
target_terms_analysis = replace(
|
|
||||||
target_terms_analysis,
|
|
||||||
target_terms=[
|
|
||||||
t
|
|
||||||
for t in target_terms_analysis.target_terms
|
|
||||||
if not str(t).startswith("/") or str(t).lower() in allowed_paths
|
|
||||||
],
|
|
||||||
)
|
|
||||||
raw_target_terms = list(target_terms_analysis.target_terms)
|
|
||||||
scope_rows = self._load_scope_rows(rag_session_id)
|
|
||||||
scope_catalog: DocsScopeCatalog | None
|
|
||||||
if not scope_rows:
|
|
||||||
scope_catalog = None
|
|
||||||
else:
|
|
||||||
scope_catalog = build_docs_scope_catalog(scope_rows)
|
|
||||||
resolution = resolve_docs_scope(normalized_query, target_terms_analysis, scope_catalog)
|
|
||||||
promoted_terms = promote_target_terms(raw_target_terms, target_terms_analysis, resolution)
|
|
||||||
refined_terms = replace(target_terms_analysis, target_terms=promoted_terms)
|
|
||||||
anchor_analysis = self._anchor_extractor.extract(normalized_query, refined_terms)
|
|
||||||
self._apply_scope_to_anchors(anchor_analysis.anchors, resolution)
|
|
||||||
features = QueryFeatures(
|
features = QueryFeatures(
|
||||||
normalized_query=normalized_query,
|
normalized_query=normalized_query,
|
||||||
target_terms=list(refined_terms.target_terms),
|
target_terms=list(target_terms_analysis.target_terms),
|
||||||
endpoint_paths=list(refined_terms.endpoint_paths),
|
endpoint_paths=list(target_terms_analysis.endpoint_paths),
|
||||||
file_names=list(anchor_analysis.anchors.file_names),
|
file_names=list(anchor_analysis.anchors.file_names),
|
||||||
matched_aliases=list(refined_terms.matched_aliases),
|
matched_aliases=list(target_terms_analysis.matched_aliases),
|
||||||
target_doc_hints=list(anchor_analysis.anchors.target_doc_hints),
|
target_doc_hints=list(anchor_analysis.anchors.target_doc_hints),
|
||||||
file_markers=list(anchor_analysis.file_markers),
|
file_markers=list(anchor_analysis.file_markers),
|
||||||
architecture_markers=list(anchor_analysis.architecture_markers),
|
architecture_markers=list(anchor_analysis.architecture_markers),
|
||||||
logic_markers=list(anchor_analysis.logic_markers),
|
logic_markers=list(anchor_analysis.logic_markers),
|
||||||
domain_markers=list(anchor_analysis.domain_markers),
|
domain_markers=list(anchor_analysis.domain_markers),
|
||||||
endpoint_markers=list(anchor_analysis.endpoint_markers),
|
endpoint_markers=list(anchor_analysis.endpoint_markers),
|
||||||
scope_type=resolution.scope_type,
|
|
||||||
)
|
|
||||||
if self._explicit_docs_update_resolver.matches(user_query):
|
|
||||||
return V2RouteResult(
|
|
||||||
routing_domain="DOCS",
|
|
||||||
intent="DOC_UPDATE",
|
|
||||||
subintent="FROM_FEATURE",
|
|
||||||
user_query=user_query,
|
|
||||||
normalized_query=features.normalized_query,
|
|
||||||
target_terms=features.target_terms,
|
|
||||||
anchors=anchor_analysis.anchors,
|
|
||||||
confidence=1.0,
|
|
||||||
routing_mode="deterministic",
|
|
||||||
llm_router_used=False,
|
|
||||||
reason_short="explicit docs update from feature source",
|
|
||||||
scope_type=resolution.scope_type,
|
|
||||||
)
|
|
||||||
if self._explicit_file_lookup_resolver.matches(anchor_analysis):
|
|
||||||
return V2RouteResult(
|
|
||||||
routing_domain="DOCS",
|
|
||||||
intent="DOC_EXPLAIN",
|
|
||||||
subintent="FIND_FILES",
|
|
||||||
user_query=user_query,
|
|
||||||
normalized_query=features.normalized_query,
|
|
||||||
target_terms=features.target_terms,
|
|
||||||
anchors=anchor_analysis.anchors,
|
|
||||||
confidence=1.0,
|
|
||||||
routing_mode="deterministic",
|
|
||||||
llm_router_used=False,
|
|
||||||
reason_short="explicit file reference",
|
|
||||||
scope_type=resolution.scope_type,
|
|
||||||
)
|
|
||||||
if self._docs_subintent_resolver.resolve(features) == V2Subintent.OPENAPI_GENERATE:
|
|
||||||
return V2RouteResult(
|
|
||||||
routing_domain="DOCS",
|
|
||||||
intent="DOC_EXPLAIN",
|
|
||||||
subintent=V2Subintent.OPENAPI_GENERATE,
|
|
||||||
user_query=user_query,
|
|
||||||
normalized_query=features.normalized_query,
|
|
||||||
target_terms=features.target_terms,
|
|
||||||
anchors=anchor_analysis.anchors,
|
|
||||||
confidence=1.0,
|
|
||||||
routing_mode="deterministic",
|
|
||||||
llm_router_used=False,
|
|
||||||
reason_short="explicit openapi generation request",
|
|
||||||
scope_type=resolution.scope_type,
|
|
||||||
)
|
)
|
||||||
llm_attempted = self._enable_llm_disambiguation and self._llm_router is not None
|
llm_attempted = self._enable_llm_disambiguation and self._llm_router is not None
|
||||||
llm_candidate = self._route_with_llm(
|
llm_candidate = self._route_with_llm(
|
||||||
@@ -214,6 +59,7 @@ class V2IntentRouter:
|
|||||||
anchors=anchor_analysis.anchors,
|
anchors=anchor_analysis.anchors,
|
||||||
)
|
)
|
||||||
llm_result = self._validator.validate(llm_candidate)
|
llm_result = self._validator.validate(llm_candidate)
|
||||||
|
llm_result = self._apply_deterministic_corrections(llm_result, features)
|
||||||
if llm_result is not None:
|
if llm_result is not None:
|
||||||
confidence = self._confidence_adjuster.adjust(float(llm_result["confidence"]), features)
|
confidence = self._confidence_adjuster.adjust(float(llm_result["confidence"]), features)
|
||||||
return V2RouteResult(
|
return V2RouteResult(
|
||||||
@@ -228,59 +74,14 @@ class V2IntentRouter:
|
|||||||
routing_mode="llm_default",
|
routing_mode="llm_default",
|
||||||
llm_router_used=True,
|
llm_router_used=True,
|
||||||
reason_short=str(llm_result["reason_short"]),
|
reason_short=str(llm_result["reason_short"]),
|
||||||
scope_type=resolution.scope_type,
|
|
||||||
)
|
|
||||||
if llm_attempted:
|
|
||||||
return self._fallback_router.route(
|
|
||||||
user_query=user_query,
|
|
||||||
features=features,
|
|
||||||
anchors=anchor_analysis.anchors,
|
|
||||||
scope_type=resolution.scope_type,
|
|
||||||
llm_attempted=True,
|
|
||||||
)
|
)
|
||||||
return self._fallback_router.route(
|
return self._fallback_router.route(
|
||||||
user_query=user_query,
|
user_query=user_query,
|
||||||
features=features,
|
features=features,
|
||||||
anchors=anchor_analysis.anchors,
|
anchors=anchor_analysis.anchors,
|
||||||
llm_attempted=llm_attempted,
|
llm_attempted=llm_attempted,
|
||||||
scope_type=resolution.scope_type,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def _load_scope_rows(self, rag_session_id: str | None) -> list[dict]:
|
|
||||||
sid = str(rag_session_id or "").strip()
|
|
||||||
if not sid:
|
|
||||||
return []
|
|
||||||
if self._scope_rows_provider is not None:
|
|
||||||
return self._scope_rows_provider(sid)
|
|
||||||
try:
|
|
||||||
return self._build_query_repository().list_docs_scope_index_rows(sid)
|
|
||||||
except Exception:
|
|
||||||
return []
|
|
||||||
|
|
||||||
def _build_query_repository(self) -> "RagQueryRepository":
|
|
||||||
from app.core.rag.persistence.query_repository import RagQueryRepository
|
|
||||||
|
|
||||||
return RagQueryRepository()
|
|
||||||
|
|
||||||
def _apply_scope_to_anchors(self, anchors, resolution) -> None:
|
|
||||||
anchors.candidate_domains = list(resolution.candidate_domains)
|
|
||||||
anchors.candidate_subdomains = list(resolution.candidate_subdomains)
|
|
||||||
anchors.candidate_entities = list(resolution.candidate_entities)
|
|
||||||
anchors.candidate_apis = list(resolution.candidate_apis)
|
|
||||||
if not resolution.catalog_loaded:
|
|
||||||
return
|
|
||||||
merged_endpoints = list(dict.fromkeys([*resolution.strong_endpoint_paths, *anchors.endpoint_paths]))
|
|
||||||
anchors.endpoint_paths = merged_endpoints
|
|
||||||
merged_entities = list(dict.fromkeys([*resolution.strong_entity_names, *anchors.entity_names]))
|
|
||||||
anchors.entity_names = merged_entities
|
|
||||||
if resolution.strong_domain:
|
|
||||||
anchors.process_domain = resolution.strong_domain
|
|
||||||
if resolution.strong_subdomain:
|
|
||||||
anchors.process_subdomain = resolution.strong_subdomain
|
|
||||||
if resolution.scope_type == V2ScopeType.SUBDOMAIN and resolution.strong_domain and resolution.strong_subdomain:
|
|
||||||
anchors.process_domain = resolution.strong_domain
|
|
||||||
anchors.process_subdomain = resolution.strong_subdomain
|
|
||||||
|
|
||||||
def _route_with_llm(self, *, features: QueryFeatures, anchors) -> dict | None:
|
def _route_with_llm(self, *, features: QueryFeatures, anchors) -> dict | None:
|
||||||
if not self._enable_llm_disambiguation or self._llm_router is None:
|
if not self._enable_llm_disambiguation or self._llm_router is None:
|
||||||
return None
|
return None
|
||||||
@@ -288,7 +89,6 @@ class V2IntentRouter:
|
|||||||
return self._llm_router.classify(
|
return self._llm_router.classify(
|
||||||
normalized_query=features.normalized_query,
|
normalized_query=features.normalized_query,
|
||||||
target_terms=features.target_terms,
|
target_terms=features.target_terms,
|
||||||
scope_type=features.scope_type,
|
|
||||||
anchors={
|
anchors={
|
||||||
"entity_names": anchors.entity_names,
|
"entity_names": anchors.entity_names,
|
||||||
"file_names": anchors.file_names,
|
"file_names": anchors.file_names,
|
||||||
@@ -297,11 +97,22 @@ class V2IntentRouter:
|
|||||||
"matched_aliases": anchors.matched_aliases,
|
"matched_aliases": anchors.matched_aliases,
|
||||||
"process_domain": anchors.process_domain,
|
"process_domain": anchors.process_domain,
|
||||||
"process_subdomain": anchors.process_subdomain,
|
"process_subdomain": anchors.process_subdomain,
|
||||||
"candidate_domains": [_scope_candidate_dict(c) for c in anchors.candidate_domains],
|
|
||||||
"candidate_subdomains": [_scope_candidate_dict(c) for c in anchors.candidate_subdomains],
|
|
||||||
"candidate_entities": [_scope_candidate_dict(c) for c in anchors.candidate_entities],
|
|
||||||
"candidate_apis": [_scope_candidate_dict(c) for c in anchors.candidate_apis],
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
return None
|
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
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from app.core.agent.processes.v2.intent_router.models import QueryFeatures
|
from app.core.agent.processes.v2.intent_router.models import QueryFeatures
|
||||||
from app.core.agent.utils.process_v2.models import V2Domain, V2Intent, V2RouteResult, V2Subintent
|
from app.core.agent.processes.v2.models import V2Domain, V2Intent, V2RouteResult, V2Subintent
|
||||||
from app.core.agent.processes.v2.intent_router.routers.docs_subintent_resolver import DocsSubintentResolver
|
from app.core.agent.processes.v2.intent_router.routers.docs_subintent_resolver import DocsSubintentResolver
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,47 +1,13 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from app.core.agent.processes.v2.intent_router.models import QueryFeatures
|
from app.core.agent.processes.v2.intent_router.models import QueryFeatures
|
||||||
from app.core.agent.utils.process_v2.models import V2Subintent
|
from app.core.agent.processes.v2.models import V2Subintent
|
||||||
|
|
||||||
|
|
||||||
class DocsSubintentResolver:
|
class DocsSubintentResolver:
|
||||||
_OPENAPI_MARKERS = (
|
|
||||||
"openapi",
|
|
||||||
"swagger",
|
|
||||||
"спецификац",
|
|
||||||
"спека",
|
|
||||||
"contract yaml",
|
|
||||||
"api yaml",
|
|
||||||
)
|
|
||||||
_GENERATE_MARKERS = ("сгенерируй", "построй", "собери", "generate", "build", "show")
|
|
||||||
_FORMAT_MARKERS = ("yaml", "json", "xml")
|
|
||||||
_API_ENUM_MARKERS = (
|
|
||||||
"какие api",
|
|
||||||
"какие эндпоинты",
|
|
||||||
"какие endpoint",
|
|
||||||
"список api",
|
|
||||||
"список эндпоинтов",
|
|
||||||
"список endpoint",
|
|
||||||
"все api",
|
|
||||||
"все эндпоинты",
|
|
||||||
"перечисли api",
|
|
||||||
"перечисли эндпоинты",
|
|
||||||
"доступные api",
|
|
||||||
"available endpoints",
|
|
||||||
"exposed api",
|
|
||||||
)
|
|
||||||
_API_WORD_MARKERS = ("api", "эндпоинт", "endpoint", "роут", "route", "метод")
|
|
||||||
_LIST_WORD_MARKERS = ("какие", "список", "перечисли", "все", "доступные", "list", "available", "exposed")
|
|
||||||
|
|
||||||
def resolve(self, features: QueryFeatures) -> str | None:
|
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
|
return V2Subintent.FIND_FILES
|
||||||
if self._is_openapi_request(features):
|
|
||||||
return V2Subintent.OPENAPI_GENERATE
|
|
||||||
if self._has_file_like_anchor(features):
|
|
||||||
return V2Subintent.FIND_FILES
|
|
||||||
if self._is_api_exposed_request(features):
|
|
||||||
return V2Subintent.API_EXPOSED
|
|
||||||
if any(
|
if any(
|
||||||
(
|
(
|
||||||
features.endpoint_paths,
|
features.endpoint_paths,
|
||||||
@@ -60,22 +26,3 @@ class DocsSubintentResolver:
|
|||||||
hint.endswith((".md", ".yaml", ".yml", ".json"))
|
hint.endswith((".md", ".yaml", ".yml", ".json"))
|
||||||
for hint in features.target_doc_hints
|
for hint in features.target_doc_hints
|
||||||
) or any(token.endswith((".md", ".yaml", ".yml", ".json")) for token in features.file_names)
|
) or any(token.endswith((".md", ".yaml", ".yml", ".json")) for token in features.file_names)
|
||||||
|
|
||||||
def _is_openapi_request(self, features: QueryFeatures) -> bool:
|
|
||||||
query = features.normalized_query.lower()
|
|
||||||
if any(marker in query for marker in self._OPENAPI_MARKERS):
|
|
||||||
return True
|
|
||||||
has_api_words = any(marker in query for marker in self._API_WORD_MARKERS)
|
|
||||||
has_generate_words = any(marker in query for marker in self._GENERATE_MARKERS)
|
|
||||||
has_format_words = any(marker in query for marker in self._FORMAT_MARKERS)
|
|
||||||
return has_api_words and has_generate_words and has_format_words
|
|
||||||
|
|
||||||
def _is_api_exposed_request(self, features: QueryFeatures) -> bool:
|
|
||||||
query = features.normalized_query.lower()
|
|
||||||
if features.endpoint_paths:
|
|
||||||
return False
|
|
||||||
if any(marker in query for marker in self._API_ENUM_MARKERS):
|
|
||||||
return True
|
|
||||||
has_api_words = any(marker in query for marker in self._API_WORD_MARKERS)
|
|
||||||
has_list_words = any(marker in query for marker in self._LIST_WORD_MARKERS)
|
|
||||||
return has_api_words and has_list_words
|
|
||||||
|
|||||||
@@ -1,33 +1,10 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from app.core.agent.processes.v2.intent_router.models import QueryFeatures
|
from app.core.agent.processes.v2.intent_router.models import QueryFeatures
|
||||||
from app.core.agent.utils.process_v2.models import V2Domain, V2Intent, V2RouteResult, V2ScopeType, V2Subintent
|
from app.core.agent.processes.v2.models import V2Domain, V2Intent, V2RouteResult, V2Subintent
|
||||||
|
|
||||||
|
|
||||||
class V2FallbackRouter:
|
class V2FallbackRouter:
|
||||||
def route_without_deterministic_signals(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
user_query: str,
|
|
||||||
features: QueryFeatures,
|
|
||||||
anchors,
|
|
||||||
scope_type: str = V2ScopeType.UNKNOWN,
|
|
||||||
) -> V2RouteResult:
|
|
||||||
return V2RouteResult(
|
|
||||||
routing_domain=V2Domain.GENERAL,
|
|
||||||
intent=V2Intent.GENERAL_QA,
|
|
||||||
subintent=V2Subintent.SUMMARY,
|
|
||||||
user_query=user_query,
|
|
||||||
normalized_query=features.normalized_query,
|
|
||||||
target_terms=features.target_terms,
|
|
||||||
anchors=anchors,
|
|
||||||
confidence=0.0,
|
|
||||||
routing_mode="llm_fallback",
|
|
||||||
llm_router_used=True,
|
|
||||||
reason_short="llm route unresolved",
|
|
||||||
scope_type=scope_type,
|
|
||||||
)
|
|
||||||
|
|
||||||
def route(
|
def route(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
@@ -35,7 +12,6 @@ class V2FallbackRouter:
|
|||||||
features: QueryFeatures,
|
features: QueryFeatures,
|
||||||
anchors,
|
anchors,
|
||||||
llm_attempted: bool,
|
llm_attempted: bool,
|
||||||
scope_type: str = V2ScopeType.UNKNOWN,
|
|
||||||
) -> V2RouteResult:
|
) -> V2RouteResult:
|
||||||
if features.file_markers:
|
if features.file_markers:
|
||||||
return self._build_docs_result(
|
return self._build_docs_result(
|
||||||
@@ -45,42 +21,6 @@ class V2FallbackRouter:
|
|||||||
subintent=V2Subintent.FIND_FILES,
|
subintent=V2Subintent.FIND_FILES,
|
||||||
llm_attempted=llm_attempted,
|
llm_attempted=llm_attempted,
|
||||||
reason="fallback file markers",
|
reason="fallback file markers",
|
||||||
scope_type=scope_type,
|
|
||||||
)
|
|
||||||
if self._has_docs_update_signal(features):
|
|
||||||
return V2RouteResult(
|
|
||||||
routing_domain=V2Domain.DOCS,
|
|
||||||
intent=V2Intent.DOC_UPDATE,
|
|
||||||
subintent=V2Subintent.FROM_FEATURE,
|
|
||||||
user_query=user_query,
|
|
||||||
normalized_query=features.normalized_query,
|
|
||||||
target_terms=features.target_terms,
|
|
||||||
anchors=anchors,
|
|
||||||
confidence=0.0,
|
|
||||||
routing_mode=self._routing_mode(llm_attempted),
|
|
||||||
llm_router_used=llm_attempted,
|
|
||||||
reason_short="fallback docs update from feature",
|
|
||||||
scope_type=scope_type,
|
|
||||||
)
|
|
||||||
if self._has_openapi_signal(features):
|
|
||||||
return self._build_docs_result(
|
|
||||||
user_query=user_query,
|
|
||||||
features=features,
|
|
||||||
anchors=anchors,
|
|
||||||
subintent=V2Subintent.OPENAPI_GENERATE,
|
|
||||||
llm_attempted=llm_attempted,
|
|
||||||
reason="fallback docs openapi",
|
|
||||||
scope_type=scope_type,
|
|
||||||
)
|
|
||||||
if self._has_api_exposed_signal(features):
|
|
||||||
return self._build_docs_result(
|
|
||||||
user_query=user_query,
|
|
||||||
features=features,
|
|
||||||
anchors=anchors,
|
|
||||||
subintent=V2Subintent.API_EXPOSED,
|
|
||||||
llm_attempted=llm_attempted,
|
|
||||||
reason="fallback docs api exposed",
|
|
||||||
scope_type=scope_type,
|
|
||||||
)
|
)
|
||||||
if self._has_docs_signal(features):
|
if self._has_docs_signal(features):
|
||||||
return self._build_docs_result(
|
return self._build_docs_result(
|
||||||
@@ -90,7 +30,6 @@ class V2FallbackRouter:
|
|||||||
subintent=V2Subintent.SUMMARY,
|
subintent=V2Subintent.SUMMARY,
|
||||||
llm_attempted=llm_attempted,
|
llm_attempted=llm_attempted,
|
||||||
reason="fallback docs summary",
|
reason="fallback docs summary",
|
||||||
scope_type=scope_type,
|
|
||||||
)
|
)
|
||||||
return V2RouteResult(
|
return V2RouteResult(
|
||||||
routing_domain=V2Domain.GENERAL,
|
routing_domain=V2Domain.GENERAL,
|
||||||
@@ -104,7 +43,6 @@ class V2FallbackRouter:
|
|||||||
routing_mode=self._routing_mode(llm_attempted),
|
routing_mode=self._routing_mode(llm_attempted),
|
||||||
llm_router_used=llm_attempted,
|
llm_router_used=llm_attempted,
|
||||||
reason_short="fallback general summary",
|
reason_short="fallback general summary",
|
||||||
scope_type=scope_type,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def _build_docs_result(
|
def _build_docs_result(
|
||||||
@@ -116,7 +54,6 @@ class V2FallbackRouter:
|
|||||||
subintent: str,
|
subintent: str,
|
||||||
llm_attempted: bool,
|
llm_attempted: bool,
|
||||||
reason: str,
|
reason: str,
|
||||||
scope_type: str = V2ScopeType.UNKNOWN,
|
|
||||||
) -> V2RouteResult:
|
) -> V2RouteResult:
|
||||||
return V2RouteResult(
|
return V2RouteResult(
|
||||||
routing_domain=V2Domain.DOCS,
|
routing_domain=V2Domain.DOCS,
|
||||||
@@ -130,7 +67,6 @@ class V2FallbackRouter:
|
|||||||
routing_mode=self._routing_mode(llm_attempted),
|
routing_mode=self._routing_mode(llm_attempted),
|
||||||
llm_router_used=llm_attempted,
|
llm_router_used=llm_attempted,
|
||||||
reason_short=reason,
|
reason_short=reason,
|
||||||
scope_type=scope_type,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def _has_docs_signal(self, features: QueryFeatures) -> bool:
|
def _has_docs_signal(self, features: QueryFeatures) -> bool:
|
||||||
@@ -146,38 +82,5 @@ class V2FallbackRouter:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
def _has_openapi_signal(self, features: QueryFeatures) -> bool:
|
|
||||||
query = features.normalized_query.lower()
|
|
||||||
has_spec = any(marker in query for marker in ("openapi", "swagger", "спецификац", "спека"))
|
|
||||||
has_format = any(marker in query for marker in ("yaml", "json", "xml"))
|
|
||||||
has_generate = any(marker in query for marker in ("сгенерируй", "построй", "собери", "generate", "build"))
|
|
||||||
has_api = any(marker in query for marker in ("api", "эндпоинт", "endpoint", "роут", "route", "метод"))
|
|
||||||
return has_spec or (has_api and has_generate and has_format)
|
|
||||||
|
|
||||||
def _has_api_exposed_signal(self, features: QueryFeatures) -> bool:
|
|
||||||
query = features.normalized_query.lower()
|
|
||||||
has_api = any(marker in query for marker in ("api", "эндпоинт", "endpoint", "роут", "route", "метод"))
|
|
||||||
has_listing = any(marker in query for marker in ("какие", "список", "перечисли", "все", "available", "list"))
|
|
||||||
return has_api and has_listing and not features.endpoint_paths and not features.file_markers
|
|
||||||
|
|
||||||
def _has_docs_update_signal(self, features: QueryFeatures) -> bool:
|
|
||||||
query = features.normalized_query.lower()
|
|
||||||
has_update = any(
|
|
||||||
marker in query
|
|
||||||
for marker in (
|
|
||||||
"обнов",
|
|
||||||
"измен",
|
|
||||||
"внести правк",
|
|
||||||
"docs update",
|
|
||||||
"update documentation",
|
|
||||||
"документац",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
has_feature = any(
|
|
||||||
marker in query
|
|
||||||
for marker in ("системной аналитик", "feature", ".md", "confluence", "from feature")
|
|
||||||
)
|
|
||||||
return has_update and has_feature
|
|
||||||
|
|
||||||
def _routing_mode(self, llm_attempted: bool) -> str:
|
def _routing_mode(self, llm_attempted: bool) -> str:
|
||||||
return "llm_fallback" if llm_attempted else "deterministic_fallback"
|
return "llm_fallback" if llm_attempted else "deterministic_fallback"
|
||||||
|
|||||||
@@ -17,18 +17,10 @@ class V2LlmRouter:
|
|||||||
self._prompt_name = prompt_name
|
self._prompt_name = prompt_name
|
||||||
self._catalog = catalog or V2RouteCatalog()
|
self._catalog = catalog or V2RouteCatalog()
|
||||||
|
|
||||||
def classify(
|
def classify(self, *, normalized_query: str, target_terms: list[str], anchors: dict) -> dict | None:
|
||||||
self,
|
|
||||||
*,
|
|
||||||
normalized_query: str,
|
|
||||||
target_terms: list[str],
|
|
||||||
anchors: dict,
|
|
||||||
scope_type: str = "unknown",
|
|
||||||
) -> dict | None:
|
|
||||||
payload = {
|
payload = {
|
||||||
"normalized_query": normalized_query,
|
"normalized_query": normalized_query,
|
||||||
"target_terms": target_terms,
|
"target_terms": target_terms,
|
||||||
"scope_type": scope_type,
|
|
||||||
"anchors": anchors,
|
"anchors": anchors,
|
||||||
"allowed_routes": self._catalog.allowed_routes(),
|
"allowed_routes": self._catalog.allowed_routes(),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,13 +3,9 @@ namespace: v2_intent_router
|
|||||||
prompts:
|
prompts:
|
||||||
route: |
|
route: |
|
||||||
Ты выбираешь маршрут для узкого процесса v2.
|
Ты выбираешь маршрут для узкого процесса v2.
|
||||||
Поле `scope_type` и блок `anchors` с `candidate_*` — это предварительная привязка к каталогу документации текущей RAG-сессии (детерминированно извлечённые кандидаты). Не выдумывай домены, сущности и API, которых нет в этих полях; используй их для снятия неоднозначности.
|
|
||||||
Основной принцип:
|
Основной принцип:
|
||||||
- DOCS / DOC_EXPLAIN / FIND_FILES: запрос просит найти файл, документ или путь.
|
- DOCS / DOC_EXPLAIN / FIND_FILES: запрос просит найти файл, документ или путь.
|
||||||
- DOCS / DOC_EXPLAIN / API_EXPOSED: запрос просит перечислить доступные API-методы/эндпоинты.
|
|
||||||
- DOCS / DOC_EXPLAIN / OPENAPI_GENERATE: запрос просит собрать OpenAPI/Swagger спецификацию по API-методам.
|
|
||||||
- DOCS / DOC_EXPLAIN / SUMMARY: запрос просит объяснить документацию, endpoint, архитектуру, процесс или сущность.
|
- DOCS / DOC_EXPLAIN / SUMMARY: запрос просит объяснить документацию, endpoint, архитектуру, процесс или сущность.
|
||||||
- DOCS / DOC_UPDATE / FROM_FEATURE: запрос просит обновить документацию по системной аналитике (feature markdown/confluence).
|
|
||||||
- GENERAL / GENERAL_QA / SUMMARY: общий обзорный вопрос без явного запроса к документации.
|
- GENERAL / GENERAL_QA / SUMMARY: общий обзорный вопрос без явного запроса к документации.
|
||||||
|
|
||||||
Используй только маршруты из поля `allowed_routes`.
|
Используй только маршруты из поля `allowed_routes`.
|
||||||
@@ -21,8 +17,8 @@ prompts:
|
|||||||
Ответь только JSON-объектом вида:
|
Ответь только JSON-объектом вида:
|
||||||
{
|
{
|
||||||
"routing_domain": "GENERAL" | "DOCS",
|
"routing_domain": "GENERAL" | "DOCS",
|
||||||
"intent": "GENERAL_QA" | "DOC_EXPLAIN" | "DOC_UPDATE",
|
"intent": "GENERAL_QA" | "DOC_EXPLAIN",
|
||||||
"subintent": "SUMMARY" | "FIND_FILES" | "API_EXPOSED" | "OPENAPI_GENERATE" | "FROM_FEATURE",
|
"subintent": "SUMMARY" | "FIND_FILES",
|
||||||
"confidence": 0.0-1.0,
|
"confidence": 0.0-1.0,
|
||||||
"reason_short": "короткая причина"
|
"reason_short": "короткая причина"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from app.core.agent.utils.process_v2.models import V2Domain, V2Intent, V2Subintent
|
from app.core.agent.processes.v2.models import V2Domain, V2Intent, V2Subintent
|
||||||
|
|
||||||
|
|
||||||
class V2RouteCatalog:
|
class V2RouteCatalog:
|
||||||
_ALLOWED_ROUTES = (
|
_ALLOWED_ROUTES = (
|
||||||
(V2Domain.DOCS, V2Intent.DOC_EXPLAIN, V2Subintent.FIND_FILES),
|
(V2Domain.DOCS, V2Intent.DOC_EXPLAIN, V2Subintent.FIND_FILES),
|
||||||
(V2Domain.DOCS, V2Intent.DOC_EXPLAIN, V2Subintent.API_EXPOSED),
|
|
||||||
(V2Domain.DOCS, V2Intent.DOC_EXPLAIN, V2Subintent.OPENAPI_GENERATE),
|
|
||||||
(V2Domain.DOCS, V2Intent.DOC_EXPLAIN, V2Subintent.SUMMARY),
|
(V2Domain.DOCS, V2Intent.DOC_EXPLAIN, V2Subintent.SUMMARY),
|
||||||
(V2Domain.DOCS, V2Intent.DOC_UPDATE, V2Subintent.FROM_FEATURE),
|
|
||||||
(V2Domain.GENERAL, V2Intent.GENERAL_QA, V2Subintent.SUMMARY),
|
(V2Domain.GENERAL, V2Intent.GENERAL_QA, V2Subintent.SUMMARY),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
+1
-30
@@ -1,4 +1,4 @@
|
|||||||
"""Route and retrieval models for process v2."""
|
"""Типы маршрута и выдачи retrieval для процесса v2."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -12,16 +12,12 @@ class V2Domain:
|
|||||||
|
|
||||||
class V2Intent:
|
class V2Intent:
|
||||||
DOC_EXPLAIN = "DOC_EXPLAIN"
|
DOC_EXPLAIN = "DOC_EXPLAIN"
|
||||||
DOC_UPDATE = "DOC_UPDATE"
|
|
||||||
GENERAL_QA = "GENERAL_QA"
|
GENERAL_QA = "GENERAL_QA"
|
||||||
|
|
||||||
|
|
||||||
class V2Subintent:
|
class V2Subintent:
|
||||||
SUMMARY = "SUMMARY"
|
SUMMARY = "SUMMARY"
|
||||||
FIND_FILES = "FIND_FILES"
|
FIND_FILES = "FIND_FILES"
|
||||||
API_EXPOSED = "API_EXPOSED"
|
|
||||||
OPENAPI_GENERATE = "OPENAPI_GENERATE"
|
|
||||||
FROM_FEATURE = "FROM_FEATURE"
|
|
||||||
|
|
||||||
|
|
||||||
class V2AnchorType:
|
class V2AnchorType:
|
||||||
@@ -33,26 +29,6 @@ class V2AnchorType:
|
|||||||
FIND_FILES = "FIND_FILES"
|
FIND_FILES = "FIND_FILES"
|
||||||
|
|
||||||
|
|
||||||
class V2ScopeType:
|
|
||||||
"""Grounded documentation scope (pre-LLM, catalog-backed)."""
|
|
||||||
|
|
||||||
GLOBAL = "global"
|
|
||||||
DOMAIN = "domain"
|
|
||||||
SUBDOMAIN = "subdomain"
|
|
||||||
ENTITY = "entity"
|
|
||||||
UNKNOWN = "unknown"
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True)
|
|
||||||
class ScopeCandidate:
|
|
||||||
"""A single catalog-backed match candidate for intent-router scope grounding."""
|
|
||||||
|
|
||||||
value: str
|
|
||||||
score: float
|
|
||||||
source_layer: str
|
|
||||||
match_type: str
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True)
|
@dataclass(slots=True)
|
||||||
class V2RouteAnchors:
|
class V2RouteAnchors:
|
||||||
"""Якоря из запроса для retrieval и downstream."""
|
"""Якоря из запроса для retrieval и downstream."""
|
||||||
@@ -64,10 +40,6 @@ class V2RouteAnchors:
|
|||||||
matched_aliases: list[str] = field(default_factory=list)
|
matched_aliases: list[str] = field(default_factory=list)
|
||||||
process_domain: str | None = None
|
process_domain: str | None = None
|
||||||
process_subdomain: str | None = None
|
process_subdomain: str | None = None
|
||||||
candidate_domains: list[ScopeCandidate] = field(default_factory=list)
|
|
||||||
candidate_subdomains: list[ScopeCandidate] = field(default_factory=list)
|
|
||||||
candidate_entities: list[ScopeCandidate] = field(default_factory=list)
|
|
||||||
candidate_apis: list[ScopeCandidate] = field(default_factory=list)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True)
|
@dataclass(slots=True)
|
||||||
@@ -83,7 +55,6 @@ class V2RouteResult:
|
|||||||
routing_mode: str = "deterministic"
|
routing_mode: str = "deterministic"
|
||||||
llm_router_used: bool = False
|
llm_router_used: bool = False
|
||||||
reason_short: str = ""
|
reason_short: str = ""
|
||||||
scope_type: str = V2ScopeType.UNKNOWN
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def domain(self) -> str:
|
def domain(self) -> str:
|
||||||
@@ -0,0 +1,304 @@
|
|||||||
|
"""Процесс v2: роутинг, план retrieval, вызов rag API, сборка evidence и workflow."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from app.core.agent.processes.v2.anchor_signals import route_anchor_summary
|
||||||
|
from app.core.agent.processes.v2.evidence.assembler import DocsEvidenceAssembler
|
||||||
|
from app.core.agent.processes.v2.evidence.gate import DocsEvidenceGate
|
||||||
|
from app.core.agent.processes.v2.intent_router import V2IntentRouter
|
||||||
|
from app.core.agent.processes.v2.models import V2Intent, V2Subintent
|
||||||
|
from app.core.agent.processes.v2.retrieval import DocsMetadataLookupIndex
|
||||||
|
from app.core.agent.processes.v2.retrieval.policy_resolver import V2RetrievalPolicyResolver
|
||||||
|
from app.core.agent.processes.v2.retrieval.target_doc_seeding import (
|
||||||
|
RagRowIndex,
|
||||||
|
merge_row_lists,
|
||||||
|
normalize_doc_path,
|
||||||
|
normalized_path_set,
|
||||||
|
row_path,
|
||||||
|
seed_candidates_from_target_hints,
|
||||||
|
)
|
||||||
|
from app.core.agent.processes.v2.retrieval.v2_rag_adapter import V2RagRetrievalAdapter
|
||||||
|
from app.core.agent.processes.v2.workflows.docs_explain_find_files.context import DocsExplainFindFilesContext
|
||||||
|
from app.core.agent.processes.v2.workflows.docs_explain_find_files.graph import DocsExplainFindFilesGraph
|
||||||
|
from app.core.agent.processes.v2.workflows.docs_explain_summary.context import DocsExplainSummaryContext
|
||||||
|
from app.core.agent.processes.v2.workflows.docs_explain_summary.graph import DocsExplainSummaryGraph
|
||||||
|
from app.core.agent.processes.v2.workflows.general_summary.context import GeneralSummaryContext
|
||||||
|
from app.core.agent.processes.v2.workflows.general_summary.graph import GeneralSummaryGraph
|
||||||
|
from app.core.agent.processes.base import AgentProcess, ProcessResult
|
||||||
|
from app.core.agent.utils.llm import AgentLlmService
|
||||||
|
|
||||||
|
|
||||||
|
class V2Process(AgentProcess):
|
||||||
|
version = "v2"
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
llm: AgentLlmService,
|
||||||
|
policy_resolver: V2RetrievalPolicyResolver,
|
||||||
|
rag_adapter: V2RagRetrievalAdapter,
|
||||||
|
evidence_assembler: DocsEvidenceAssembler,
|
||||||
|
evidence_gate: DocsEvidenceGate | None = None,
|
||||||
|
router: V2IntentRouter | None = None,
|
||||||
|
docs_summary_prompt_name: str = "v2_docs_explain.summary_answer",
|
||||||
|
general_summary_prompt_name: str = "v2_general.summary_answer",
|
||||||
|
workflow_llm_enabled: bool = True,
|
||||||
|
) -> None:
|
||||||
|
self._router = router or V2IntentRouter()
|
||||||
|
self._policy_resolver = policy_resolver
|
||||||
|
self._rag_adapter = rag_adapter
|
||||||
|
self._evidence_assembler = evidence_assembler
|
||||||
|
self._evidence_gate = evidence_gate or DocsEvidenceGate()
|
||||||
|
self._docs_summary_prompt_name = docs_summary_prompt_name
|
||||||
|
self._general_summary_prompt_name = general_summary_prompt_name
|
||||||
|
self._workflow_llm_enabled = workflow_llm_enabled
|
||||||
|
self._summary_graph = DocsExplainSummaryGraph(llm)
|
||||||
|
self._find_files_graph = DocsExplainFindFilesGraph()
|
||||||
|
self._general_summary_graph = GeneralSummaryGraph(llm)
|
||||||
|
|
||||||
|
async def run(self, context) -> ProcessResult:
|
||||||
|
route = self._router.route(context.request.message)
|
||||||
|
rag_session_id = context.session.active_rag_session_id
|
||||||
|
context.trace.module("process.v2").log(
|
||||||
|
"intent_routed",
|
||||||
|
{
|
||||||
|
"routing_domain": route.routing_domain,
|
||||||
|
"intent": route.intent,
|
||||||
|
"subintent": route.subintent,
|
||||||
|
"normalized_query": route.normalized_query,
|
||||||
|
"target_terms": route.target_terms,
|
||||||
|
"anchors": route_anchor_summary(route),
|
||||||
|
"confidence": route.confidence,
|
||||||
|
"routing_mode": route.routing_mode,
|
||||||
|
"llm_router_used": route.llm_router_used,
|
||||||
|
"reason_short": route.reason_short,
|
||||||
|
"rag_session_id": rag_session_id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self._log_step(
|
||||||
|
context,
|
||||||
|
"router_resolved",
|
||||||
|
{
|
||||||
|
"domain": route.routing_domain,
|
||||||
|
"intent": route.intent,
|
||||||
|
"subintent": route.subintent,
|
||||||
|
"confidence": route.confidence,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self._log_step(
|
||||||
|
context,
|
||||||
|
"anchors_extracted",
|
||||||
|
{
|
||||||
|
"signal_types": route_anchor_summary(route)["signal_types"],
|
||||||
|
"endpoint_paths": route.anchors.endpoint_paths,
|
||||||
|
"target_doc_hints": route.anchors.target_doc_hints,
|
||||||
|
"matched_aliases": route.anchors.matched_aliases,
|
||||||
|
"target_terms": route.target_terms,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self._log_step(
|
||||||
|
context,
|
||||||
|
"alias_resolution",
|
||||||
|
{
|
||||||
|
"resolved_aliases": route.anchors.matched_aliases,
|
||||||
|
"target_doc_hints": route.anchors.target_doc_hints,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if not rag_session_id:
|
||||||
|
if route.intent == V2Intent.GENERAL_QA:
|
||||||
|
answer = "Не могу собрать grounded summary без активной RAG-сессии с проиндексированной документацией."
|
||||||
|
self._log_step(context, "evidence_gate_checked", {"passed": False, "reason": "missing_rag_session"})
|
||||||
|
self._log_step(context, "answer_generated", {"answer_mode": "insufficient_evidence"})
|
||||||
|
return ProcessResult(answer=answer)
|
||||||
|
return ProcessResult(answer="Для процесса v2 нужна активная RAG-сессия проекта с проиндексированной документацией.")
|
||||||
|
plan = self._policy_resolver.resolve(route)
|
||||||
|
context.trace.module("process.v2.retrieval_policy").log(
|
||||||
|
"retrieval_plan_resolved",
|
||||||
|
{"profile": plan.profile, "layers": plan.layers, "limit": plan.limit, "filters": plan.filters},
|
||||||
|
)
|
||||||
|
self._log_step(
|
||||||
|
context,
|
||||||
|
"retrieval_profile_selected",
|
||||||
|
{"profile": plan.profile, "layers": plan.layers, "filters": plan.filters},
|
||||||
|
)
|
||||||
|
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(
|
||||||
|
"rag_rows_fetched",
|
||||||
|
{
|
||||||
|
"profile": plan.profile,
|
||||||
|
"row_count": len(rows),
|
||||||
|
"rows": [self._trace_row(row) for row in rows],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self._log_step(
|
||||||
|
context,
|
||||||
|
"candidate_generation",
|
||||||
|
{
|
||||||
|
"query": route.user_query,
|
||||||
|
"profile": plan.profile,
|
||||||
|
"details": {
|
||||||
|
"target_doc_hints": list(route.anchors.target_doc_hints),
|
||||||
|
"candidates_before_ranking": [row_path(row) for row in rows if row_path(row)],
|
||||||
|
},
|
||||||
|
"resolved_aliases": route.anchors.matched_aliases,
|
||||||
|
"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 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 retrieved_rows[:5]],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self._log_step(
|
||||||
|
context,
|
||||||
|
"retrieval_executed",
|
||||||
|
{
|
||||||
|
"query": route.user_query,
|
||||||
|
"profile": plan.profile,
|
||||||
|
"row_count": len(rows),
|
||||||
|
"target_doc_hints": route.anchors.target_doc_hints,
|
||||||
|
"top_results": [self._trace_row(row) for row in rows[:5]],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if route.subintent == V2Subintent.FIND_FILES:
|
||||||
|
files = self._evidence_assembler.assemble_files(rows, route)
|
||||||
|
gate = self._evidence_gate.check_files(route, files)
|
||||||
|
context.trace.module("process.v2.evidence").log(
|
||||||
|
"evidence_assembled",
|
||||||
|
{"mode": "find_files", "file_count": len(files), "files": [file.path for file in files]},
|
||||||
|
)
|
||||||
|
self._log_step(
|
||||||
|
context,
|
||||||
|
"evidence_assembled",
|
||||||
|
{"mode": "find_files", "primary_file": files[0].path if files else None, "file_count": len(files)},
|
||||||
|
)
|
||||||
|
self._log_ranking(context, files)
|
||||||
|
self._log_step(
|
||||||
|
context,
|
||||||
|
"evidence_gate_checked",
|
||||||
|
{"passed": gate.passed, "reason": gate.reason, "answer_mode": gate.answer_mode},
|
||||||
|
)
|
||||||
|
flow_context = DocsExplainFindFilesContext(
|
||||||
|
runtime=context,
|
||||||
|
route=route,
|
||||||
|
rag_session_id=rag_session_id,
|
||||||
|
files=files,
|
||||||
|
gate_decision=gate,
|
||||||
|
)
|
||||||
|
flow_context = await self._find_files_graph.run(flow_context)
|
||||||
|
self._log_step(context, "answer_generated", {"answer_mode": gate.answer_mode, "answer_length": len(flow_context.answer)})
|
||||||
|
return ProcessResult(answer=flow_context.answer)
|
||||||
|
documents = self._evidence_assembler.assemble_summaries(rows, route)
|
||||||
|
gate = self._evidence_gate.check_summaries(route, documents)
|
||||||
|
context.trace.module("process.v2.evidence").log(
|
||||||
|
"evidence_assembled",
|
||||||
|
{"mode": "summary", "document_count": len(documents), "documents": [item.path for item in documents]},
|
||||||
|
)
|
||||||
|
self._log_step(
|
||||||
|
context,
|
||||||
|
"evidence_assembled",
|
||||||
|
{"mode": "summary", "primary_doc": documents[0].path if documents else None, "document_count": len(documents)},
|
||||||
|
)
|
||||||
|
self._log_ranking(context, documents)
|
||||||
|
self._log_step(
|
||||||
|
context,
|
||||||
|
"evidence_gate_checked",
|
||||||
|
{"passed": gate.passed, "reason": gate.reason, "answer_mode": gate.answer_mode},
|
||||||
|
)
|
||||||
|
if route.intent == V2Intent.GENERAL_QA:
|
||||||
|
flow_context = GeneralSummaryContext(
|
||||||
|
runtime=context,
|
||||||
|
route=route,
|
||||||
|
prompt_name=self._general_summary_prompt_name,
|
||||||
|
workflow_llm_enabled=self._workflow_llm_enabled,
|
||||||
|
documents=documents,
|
||||||
|
gate_decision=gate,
|
||||||
|
)
|
||||||
|
flow_context = await self._general_summary_graph.run(flow_context)
|
||||||
|
self._log_step(context, "answer_generated", {"answer_mode": gate.answer_mode, "answer_length": len(flow_context.answer)})
|
||||||
|
return ProcessResult(answer=flow_context.answer)
|
||||||
|
flow_context = DocsExplainSummaryContext(
|
||||||
|
runtime=context,
|
||||||
|
route=route,
|
||||||
|
rag_session_id=rag_session_id,
|
||||||
|
prompt_name=self._docs_summary_prompt_name,
|
||||||
|
workflow_llm_enabled=self._workflow_llm_enabled,
|
||||||
|
documents=documents,
|
||||||
|
gate_decision=gate,
|
||||||
|
)
|
||||||
|
flow_context = await self._summary_graph.run(flow_context)
|
||||||
|
self._log_step(context, "answer_generated", {"answer_mode": gate.answer_mode, "answer_length": len(flow_context.answer)})
|
||||||
|
return ProcessResult(answer=flow_context.answer)
|
||||||
|
|
||||||
|
def _trace_row(self, row: dict) -> dict[str, object]:
|
||||||
|
metadata = row.get("metadata") or {}
|
||||||
|
content = str(row.get("content") or "").strip()
|
||||||
|
return {
|
||||||
|
"layer": str(row.get("layer") or ""),
|
||||||
|
"path": str(row.get("path") or ""),
|
||||||
|
"title": str(row.get("title") or ""),
|
||||||
|
"document_id": str(metadata.get("document_id") or metadata.get("doc_id") or ""),
|
||||||
|
"entity_name": str(metadata.get("entity_name") or ""),
|
||||||
|
"summary_text": str(metadata.get("summary_text") or "")[:400],
|
||||||
|
"section_path": str(metadata.get("section_path") or ""),
|
||||||
|
"content_preview": content[:400],
|
||||||
|
}
|
||||||
|
|
||||||
|
def _log_step(self, context, step: str, payload: dict[str, object]) -> None:
|
||||||
|
context.trace.module("process.v2.pipeline").log(step, payload)
|
||||||
|
|
||||||
|
def _print_missing_target_hints(self, route, rows: list[dict]) -> None:
|
||||||
|
if not route.anchors.target_doc_hints:
|
||||||
|
return
|
||||||
|
candidate_paths = normalized_path_set(rows)
|
||||||
|
for hint in route.anchors.target_doc_hints:
|
||||||
|
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)
|
||||||
|
|
||||||
|
def _metadata_lookup_candidates(self, rows: list[dict], route) -> list[dict]:
|
||||||
|
return DocsMetadataLookupIndex(rows).lookup(route)
|
||||||
|
|
||||||
|
def _merge_candidate_rows(self, *groups: list[dict]) -> list[dict]:
|
||||||
|
return merge_row_lists(*groups)
|
||||||
|
|
||||||
|
def _log_ranking(self, context, items: list) -> None:
|
||||||
|
top_docs: list[dict[str, object]] = []
|
||||||
|
for item in items[:4]:
|
||||||
|
top_docs.append(
|
||||||
|
{
|
||||||
|
"doc": getattr(item, "path", ""),
|
||||||
|
"score": getattr(item, "score", 0),
|
||||||
|
"match_reason": getattr(item, "match_reason", ""),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
context.trace.module("process.v2.pipeline").log(
|
||||||
|
"ranking_explained",
|
||||||
|
{
|
||||||
|
"doc": getattr(item, "path", ""),
|
||||||
|
"score_breakdown": getattr(item, "score_breakdown", {}),
|
||||||
|
"score": getattr(item, "score", 0),
|
||||||
|
"match_reason": getattr(item, "match_reason", ""),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
context.trace.module("process.v2.pipeline").log(
|
||||||
|
"ranking_explained",
|
||||||
|
{
|
||||||
|
"top_docs_after_ranking": top_docs,
|
||||||
|
"ranking_score_breakdown": [
|
||||||
|
{
|
||||||
|
"doc": getattr(item, "path", ""),
|
||||||
|
"score_breakdown": getattr(item, "score_breakdown", {}),
|
||||||
|
}
|
||||||
|
for item in items[:4]
|
||||||
|
],
|
||||||
|
},
|
||||||
|
)
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
from app.core.agent.processes.v2.retrieval.metadata_lookup import DocsMetadataLookupIndex
|
||||||
|
from app.core.agent.processes.v2.retrieval.policy_resolver import V2RetrievalPolicyResolver
|
||||||
|
from app.core.agent.processes.v2.retrieval.target_doc_seeding import (
|
||||||
|
RagRowIndex,
|
||||||
|
normalize_doc_path,
|
||||||
|
seed_candidates_from_target_hints,
|
||||||
|
)
|
||||||
|
from app.core.agent.processes.v2.retrieval.v2_rag_adapter import V2RagRetrievalAdapter
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"V2RetrievalPolicyResolver",
|
||||||
|
"V2RagRetrievalAdapter",
|
||||||
|
"DocsMetadataLookupIndex",
|
||||||
|
"normalize_doc_path",
|
||||||
|
"RagRowIndex",
|
||||||
|
"seed_candidates_from_target_hints",
|
||||||
|
]
|
||||||
+1
-3
@@ -1,11 +1,9 @@
|
|||||||
"""Индекс метаданных RAG-строк для подбора кандидатов по маршруту v2."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import re
|
import re
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
from app.core.agent.utils.process_v2.models import V2RouteResult
|
from app.core.agent.processes.v2.models import V2RouteResult
|
||||||
|
|
||||||
|
|
||||||
class DocsMetadataLookupIndex:
|
class DocsMetadataLookupIndex:
|
||||||
@@ -0,0 +1,270 @@
|
|||||||
|
"""Intent-aware retrieval policy resolver for process v2."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from app.core.agent.processes.v2.anchor_signals import anchor_signal_types
|
||||||
|
from app.core.agent.processes.v2.models import V2AnchorType, V2Intent, V2RouteResult, V2Subintent
|
||||||
|
from app.core.rag.contracts.enums import RagLayer
|
||||||
|
from app.core.rag.retrieval.session_retriever import RetrievalPlan
|
||||||
|
|
||||||
|
|
||||||
|
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 __init__(self) -> None:
|
||||||
|
self._terms = _AnchorTermCollector()
|
||||||
|
|
||||||
|
def general_filters(self, route: V2RouteResult) -> dict[str, object]:
|
||||||
|
return {
|
||||||
|
"prefer_path_prefixes": ["docs/architecture/", "docs/"],
|
||||||
|
"prefer_like_patterns": ["%readme.md%", "%overview%"],
|
||||||
|
"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/"]
|
||||||
|
return filters
|
||||||
|
|
||||||
|
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] = {
|
||||||
|
"target_doc_hints": list(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 _find_files_prefixes(self, route: V2RouteResult) -> list[str]:
|
||||||
|
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/"]
|
||||||
|
if V2AnchorType.ARCHITECTURE in signals:
|
||||||
|
return ["docs/architecture/", "docs/"]
|
||||||
|
if V2AnchorType.LOGIC_FLOW in signals:
|
||||||
|
return ["docs/logic/", "docs/"]
|
||||||
|
if V2AnchorType.DOMAIN_ENTITY in signals:
|
||||||
|
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] = []
|
||||||
|
if V2AnchorType.API_ENDPOINT in signals:
|
||||||
|
prefixes.extend(["docs/api/", "docs/"])
|
||||||
|
if V2AnchorType.ARCHITECTURE in signals:
|
||||||
|
prefixes.extend(["docs/architecture/", "docs/"])
|
||||||
|
if V2AnchorType.LOGIC_FLOW in signals:
|
||||||
|
prefixes.extend(["docs/logic/", "docs/architecture/", "docs/"])
|
||||||
|
if V2AnchorType.DOMAIN_ENTITY in signals:
|
||||||
|
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
|
||||||
-2
@@ -1,5 +1,3 @@
|
|||||||
"""Нормализация путей документов, склейка RAG-строк и сидирование по target hints."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|
||||||
+2
-92
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from app.core.agent.utils.process_v2.rag_retrieval.target_doc_seeding import (
|
from app.core.agent.processes.v2.retrieval.target_doc_seeding import (
|
||||||
merge_row_lists,
|
merge_row_lists,
|
||||||
normalize_doc_path,
|
normalize_doc_path,
|
||||||
path_variants_for_rag_query,
|
path_variants_for_rag_query,
|
||||||
@@ -17,11 +17,7 @@ class _PlanDrivenRetrieval:
|
|||||||
async def fetch_rows(self, rag_session_id: str, query_text: str, plan: RetrievalPlan) -> list[dict]:
|
async def fetch_rows(self, rag_session_id: str, query_text: str, plan: RetrievalPlan) -> list[dict]:
|
||||||
seeded_rows = await self._seed_from_target_hints(rag_session_id, 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)
|
semantic_rows = await self._retriever.retrieve(rag_session_id, query_text, plan)
|
||||||
merged = merge_row_lists(seeded_rows, semantic_rows)
|
return merge_row_lists(seeded_rows, semantic_rows)
|
||||||
if not _needs_docs_catalog_fallback(plan):
|
|
||||||
return _apply_query_signal_filter(merged, plan)
|
|
||||||
fallback_rows = await self._fetch_docs_catalog_rows(rag_session_id, plan)
|
|
||||||
return _apply_query_signal_filter(merge_row_lists(merged, fallback_rows), plan)
|
|
||||||
|
|
||||||
async def fetch_exact_paths(self, rag_session_id: str, *, paths: list[str], layers: list[str] | None = None) -> list[dict]:
|
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)
|
return await self._retriever.retrieve_exact_files(rag_session_id, paths=paths, layers=layers)
|
||||||
@@ -73,10 +69,6 @@ class _PlanDrivenRetrieval:
|
|||||||
limit=200,
|
limit=200,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _fetch_docs_catalog_rows(self, rag_session_id: str, plan: RetrievalPlan) -> list[dict]:
|
|
||||||
rows = await self._retriever.list_docs_scope_rows(rag_session_id, limit=max(1000, plan.limit * 10))
|
|
||||||
return _filter_docs_rows(rows, plan)[: plan.limit]
|
|
||||||
|
|
||||||
def _target_doc_hints(self, plan: RetrievalPlan) -> list[str]:
|
def _target_doc_hints(self, plan: RetrievalPlan) -> list[str]:
|
||||||
raw = plan.filters.get("target_doc_hints")
|
raw = plan.filters.get("target_doc_hints")
|
||||||
if not isinstance(raw, list):
|
if not isinstance(raw, list):
|
||||||
@@ -114,85 +106,3 @@ class V2RagRetrievalAdapter:
|
|||||||
layers=layers,
|
layers=layers,
|
||||||
limit=limit,
|
limit=limit,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _needs_docs_catalog_fallback(plan: RetrievalPlan) -> bool:
|
|
||||||
layers = {str(item).strip() for item in plan.layers}
|
|
||||||
if "D1_DOCUMENT_CATALOG" not in layers:
|
|
||||||
return False
|
|
||||||
return "metadata.type" in plan.filters or "metadata.doc_type" in plan.filters
|
|
||||||
|
|
||||||
|
|
||||||
def _filter_docs_rows(rows: list[dict], plan: RetrievalPlan) -> list[dict]:
|
|
||||||
allowed_layers = {str(item).strip() for item in plan.layers if str(item).strip()}
|
|
||||||
metadata_type = _norm(plan.filters.get("metadata.type")) or _norm(plan.filters.get("metadata.doc_type"))
|
|
||||||
metadata_domain = _norm(plan.filters.get("metadata.domain"))
|
|
||||||
metadata_subdomain = _norm(plan.filters.get("metadata.subdomain"))
|
|
||||||
out: list[dict] = []
|
|
||||||
for row in rows:
|
|
||||||
layer = str(row.get("layer") or "").strip()
|
|
||||||
if allowed_layers and layer not in allowed_layers:
|
|
||||||
continue
|
|
||||||
metadata = row.get("metadata") if isinstance(row.get("metadata"), dict) else {}
|
|
||||||
row_type = _norm(metadata.get("type")) or _norm(metadata.get("doc_type"))
|
|
||||||
if metadata_type and row_type != metadata_type:
|
|
||||||
continue
|
|
||||||
if metadata_domain and _norm(metadata.get("domain")) != metadata_domain:
|
|
||||||
continue
|
|
||||||
if metadata_subdomain and _norm(metadata.get("subdomain")) != metadata_subdomain:
|
|
||||||
continue
|
|
||||||
out.append(row)
|
|
||||||
return sorted(out, key=lambda item: str(item.get("path") or ""))
|
|
||||||
|
|
||||||
|
|
||||||
def _norm(value: object) -> str:
|
|
||||||
return str(value or "").strip().lower()
|
|
||||||
|
|
||||||
|
|
||||||
def _apply_query_signal_filter(rows: list[dict], plan: RetrievalPlan) -> list[dict]:
|
|
||||||
signals = _query_signals(plan)
|
|
||||||
if not signals:
|
|
||||||
return rows
|
|
||||||
strict = [row for row in rows if _matches_any_signal(row, signals, strict=True)]
|
|
||||||
if strict:
|
|
||||||
return strict
|
|
||||||
broad = [row for row in rows if _matches_any_signal(row, signals, strict=False)]
|
|
||||||
return broad or rows
|
|
||||||
|
|
||||||
|
|
||||||
def _query_signals(plan: RetrievalPlan) -> list[str]:
|
|
||||||
raw = plan.filters.get("query_signals")
|
|
||||||
if not isinstance(raw, list):
|
|
||||||
return []
|
|
||||||
return [item for item in (_norm(value) for value in raw) if item]
|
|
||||||
|
|
||||||
|
|
||||||
def _matches_any_signal(row: dict, signals: list[str], *, strict: bool) -> bool:
|
|
||||||
haystack = _strict_haystack(row) if strict else _broad_haystack(row)
|
|
||||||
return any(signal in haystack for signal in signals)
|
|
||||||
|
|
||||||
|
|
||||||
def _strict_haystack(row: dict) -> str:
|
|
||||||
metadata = row.get("metadata") if isinstance(row.get("metadata"), dict) else {}
|
|
||||||
parts = [
|
|
||||||
row.get("path"),
|
|
||||||
row.get("title"),
|
|
||||||
metadata.get("endpoint"),
|
|
||||||
metadata.get("name"),
|
|
||||||
]
|
|
||||||
return " ".join(_norm(part) for part in parts if _norm(part))
|
|
||||||
|
|
||||||
|
|
||||||
def _broad_haystack(row: dict) -> str:
|
|
||||||
metadata = row.get("metadata") if isinstance(row.get("metadata"), dict) else {}
|
|
||||||
parts = [
|
|
||||||
row.get("path"),
|
|
||||||
row.get("title"),
|
|
||||||
row.get("content"),
|
|
||||||
metadata.get("endpoint"),
|
|
||||||
metadata.get("name"),
|
|
||||||
metadata.get("summary_text"),
|
|
||||||
metadata.get("entities"),
|
|
||||||
metadata.get("tags"),
|
|
||||||
]
|
|
||||||
return " ".join(_norm(part) for part in parts if _norm(part))
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from dataclasses import dataclass
|
|
||||||
|
|
||||||
from app.core.agent.utils.process_v2.models import V2Intent, V2RouteResult, V2Subintent
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, slots=True)
|
|
||||||
class RouterStatusEvent:
|
|
||||||
text: str
|
|
||||||
payload: dict[str, object]
|
|
||||||
|
|
||||||
|
|
||||||
class V2RouterStatusBuilder:
|
|
||||||
def build(self, route: V2RouteResult) -> RouterStatusEvent:
|
|
||||||
subintent_label = self._subintent_label(route.intent, route.subintent)
|
|
||||||
subintent_comment = self._subintent_comment(route.intent, route.subintent)
|
|
||||||
return RouterStatusEvent(
|
|
||||||
text=f"Выбран subintent {route.subintent} - {subintent_comment}",
|
|
||||||
payload={
|
|
||||||
"routing_domain": route.routing_domain,
|
|
||||||
"intent": route.intent,
|
|
||||||
"subintent": route.subintent,
|
|
||||||
"subintent_label": subintent_label,
|
|
||||||
"subintent_comment": subintent_comment,
|
|
||||||
"confidence": route.confidence,
|
|
||||||
"routing_mode": route.routing_mode,
|
|
||||||
"llm_router_used": route.llm_router_used,
|
|
||||||
"reason_short": route.reason_short,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
def _subintent_label(self, intent: str, subintent: str) -> str:
|
|
||||||
labels = {
|
|
||||||
(V2Intent.DOC_EXPLAIN, V2Subintent.SUMMARY): "объяснение документации",
|
|
||||||
(V2Intent.DOC_EXPLAIN, V2Subintent.FIND_FILES): "поиск файлов документации",
|
|
||||||
(V2Intent.DOC_EXPLAIN, V2Subintent.API_EXPOSED): "поиск API-методов",
|
|
||||||
(V2Intent.DOC_EXPLAIN, V2Subintent.OPENAPI_GENERATE): "генерация OpenAPI-спецификации",
|
|
||||||
(V2Intent.DOC_UPDATE, V2Subintent.FROM_FEATURE): "обновление документации по аналитике",
|
|
||||||
(V2Intent.GENERAL_QA, V2Subintent.SUMMARY): "общий ответ по проекту",
|
|
||||||
}
|
|
||||||
return labels.get((intent, subintent), str(subintent).lower())
|
|
||||||
|
|
||||||
def _subintent_comment(self, intent: str, subintent: str) -> str:
|
|
||||||
comments = {
|
|
||||||
(V2Intent.DOC_EXPLAIN, V2Subintent.SUMMARY): (
|
|
||||||
"отвечаю на вопрос по существующей документации с опорой на найденные документы"
|
|
||||||
),
|
|
||||||
(V2Intent.DOC_EXPLAIN, V2Subintent.FIND_FILES): (
|
|
||||||
"ищу конкретные файлы документации, которые соответствуют запросу"
|
|
||||||
),
|
|
||||||
(V2Intent.DOC_EXPLAIN, V2Subintent.API_EXPOSED): (
|
|
||||||
"проверяю, опубликован ли API наружу и через какие контракты или endpoint'ы он доступен"
|
|
||||||
),
|
|
||||||
(V2Intent.DOC_EXPLAIN, V2Subintent.OPENAPI_GENERATE): (
|
|
||||||
"собираю OpenAPI-спецификацию по существующей документации API"
|
|
||||||
),
|
|
||||||
(V2Intent.DOC_UPDATE, V2Subintent.FROM_FEATURE): (
|
|
||||||
"обновляю или создаю документацию по системной аналитике"
|
|
||||||
),
|
|
||||||
(V2Intent.GENERAL_QA, V2Subintent.SUMMARY): (
|
|
||||||
"даю общий ответ по проекту, когда запрос не сводится к конкретному документу"
|
|
||||||
),
|
|
||||||
}
|
|
||||||
return comments.get((intent, subintent), "выполняю сценарий, который лучше всего соответствует запросу")
|
|
||||||
@@ -1,250 +0,0 @@
|
|||||||
"""Процесс v2: роутинг запроса и dispatch в workflow."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from app.core.agent.processes.base import AgentProcess, ProcessResult
|
|
||||||
from app.core.agent.processes.v2.intent_router import V2IntentRouter
|
|
||||||
from app.core.agent.processes.v2.router_status import V2RouterStatusBuilder
|
|
||||||
from app.core.agent.processes.v2.workflows.doc_explain_api_exposed.workflow_runtime.context import (
|
|
||||||
DocExplainApiExposedContext,
|
|
||||||
)
|
|
||||||
from app.core.agent.processes.v2.workflows.doc_explain_api_exposed.graph import DocExplainApiExposedGraph
|
|
||||||
from app.core.agent.processes.v2.workflows.doc_explain_find_files.workflow_runtime.context import DocExplainFindFilesContext
|
|
||||||
from app.core.agent.processes.v2.workflows.doc_explain_find_files.graph import DocExplainFindFilesGraph
|
|
||||||
from app.core.agent.processes.v2.workflows.doc_generate_openapi.workflow_runtime.context import (
|
|
||||||
DocGenerateOpenApiContext,
|
|
||||||
)
|
|
||||||
from app.core.agent.processes.v2.workflows.doc_generate_openapi.graph import DocGenerateOpenApiGraph
|
|
||||||
from app.core.agent.processes.v2.workflows.doc_explain_summary.workflow_runtime.context import DocExplainSummaryContext
|
|
||||||
from app.core.agent.processes.v2.workflows.doc_explain_summary.graph import DocExplainSummaryGraph
|
|
||||||
from app.core.agent.processes.v2.workflows.doc_update_from_feature.graph import DocUpdateFromFeatureGraph
|
|
||||||
from app.core.agent.processes.v2.workflows.doc_update_from_feature.workflow_runtime.context import (
|
|
||||||
DocUpdateFromFeatureContext,
|
|
||||||
)
|
|
||||||
from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.graph import DocUpdateFromFeatureV2Graph
|
|
||||||
from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.workflow_runtime.context import (
|
|
||||||
DocUpdateFromFeatureV2Context,
|
|
||||||
)
|
|
||||||
from app.core.agent.processes.v2.workflows.general_qa_summary.workflow_runtime.context import GeneralQaSummaryContext
|
|
||||||
from app.core.agent.processes.v2.workflows.general_qa_summary.graph import GeneralQaSummaryGraph
|
|
||||||
from app.core.agent.utils.llm import AgentLlmService
|
|
||||||
from app.core.agent.utils.process_v2.anchor_signals import route_anchor_summary
|
|
||||||
from app.core.agent.utils.process_v2.evidence.assembler import DocsEvidenceAssembler
|
|
||||||
from app.core.agent.utils.process_v2.evidence.gate import DocsEvidenceGate
|
|
||||||
from app.core.agent.utils.process_v2.models import V2Domain, V2Intent, V2Subintent
|
|
||||||
from app.core.agent.utils.process_v2.plan_resolver import RetrievalPlanResolver
|
|
||||||
from app.core.agent.utils.process_v2.rag_retrieval import V2RagRetrievalAdapter
|
|
||||||
|
|
||||||
|
|
||||||
class V2Process(AgentProcess):
|
|
||||||
version = "v2"
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
llm: AgentLlmService,
|
|
||||||
policy_resolver: RetrievalPlanResolver,
|
|
||||||
rag_adapter: V2RagRetrievalAdapter,
|
|
||||||
evidence_assembler: DocsEvidenceAssembler,
|
|
||||||
evidence_gate: DocsEvidenceGate | None = None,
|
|
||||||
router: V2IntentRouter | None = None,
|
|
||||||
docs_summary_prompt_name: str = "v2_docs_explain.summary_answer",
|
|
||||||
general_summary_prompt_name: str = "v2_general.summary_answer",
|
|
||||||
workflow_llm_enabled: bool = True,
|
|
||||||
doc_rules_enabled: bool = True,
|
|
||||||
doc_update_workflow_version: str = "v2",
|
|
||||||
) -> None:
|
|
||||||
self._router = router or V2IntentRouter()
|
|
||||||
self._router_status_builder = V2RouterStatusBuilder()
|
|
||||||
gate = evidence_gate or DocsEvidenceGate()
|
|
||||||
self._docs_summary_prompt_name = docs_summary_prompt_name
|
|
||||||
self._general_summary_prompt_name = general_summary_prompt_name
|
|
||||||
self._workflow_llm_enabled = workflow_llm_enabled
|
|
||||||
self._doc_rules_enabled = doc_rules_enabled
|
|
||||||
self._doc_update_workflow_version = doc_update_workflow_version.strip().lower() or "v2"
|
|
||||||
doc_update_graph = self._build_doc_update_workflow(llm, doc_rules_enabled)
|
|
||||||
self._workflows: dict[tuple[str, str, str], Any] = {
|
|
||||||
(V2Domain.DOCS, V2Intent.DOC_EXPLAIN, V2Subintent.SUMMARY): DocExplainSummaryGraph(
|
|
||||||
llm,
|
|
||||||
policy_resolver=policy_resolver,
|
|
||||||
rag_adapter=rag_adapter,
|
|
||||||
evidence_assembler=evidence_assembler,
|
|
||||||
evidence_gate=gate,
|
|
||||||
),
|
|
||||||
(V2Domain.DOCS, V2Intent.DOC_EXPLAIN, V2Subintent.FIND_FILES): DocExplainFindFilesGraph(
|
|
||||||
policy_resolver=policy_resolver,
|
|
||||||
rag_adapter=rag_adapter,
|
|
||||||
evidence_assembler=evidence_assembler,
|
|
||||||
evidence_gate=gate,
|
|
||||||
),
|
|
||||||
(V2Domain.DOCS, V2Intent.DOC_EXPLAIN, V2Subintent.API_EXPOSED): DocExplainApiExposedGraph(
|
|
||||||
policy_resolver=policy_resolver,
|
|
||||||
rag_adapter=rag_adapter,
|
|
||||||
),
|
|
||||||
(V2Domain.DOCS, V2Intent.DOC_EXPLAIN, V2Subintent.OPENAPI_GENERATE): DocGenerateOpenApiGraph(
|
|
||||||
llm,
|
|
||||||
policy_resolver=policy_resolver,
|
|
||||||
rag_adapter=rag_adapter,
|
|
||||||
),
|
|
||||||
(V2Domain.DOCS, V2Intent.DOC_UPDATE, V2Subintent.FROM_FEATURE): doc_update_graph,
|
|
||||||
(V2Domain.GENERAL, V2Intent.GENERAL_QA, V2Subintent.SUMMARY): GeneralQaSummaryGraph(
|
|
||||||
llm,
|
|
||||||
policy_resolver=policy_resolver,
|
|
||||||
rag_adapter=rag_adapter,
|
|
||||||
evidence_assembler=evidence_assembler,
|
|
||||||
evidence_gate=gate,
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
async def run(self, context) -> ProcessResult:
|
|
||||||
rag_session_id = context.session.active_rag_session_id or ""
|
|
||||||
route = await asyncio.to_thread(
|
|
||||||
self._router.route,
|
|
||||||
context.request.message,
|
|
||||||
rag_session_id=rag_session_id or None,
|
|
||||||
)
|
|
||||||
router_status = self._router_status_builder.build(route)
|
|
||||||
context.request.set_route(
|
|
||||||
routing_domain=route.routing_domain,
|
|
||||||
intent=route.intent,
|
|
||||||
subintent=route.subintent,
|
|
||||||
subintent_label=str(router_status.payload.get("subintent_label") or route.subintent),
|
|
||||||
subintent_comment=str(router_status.payload.get("subintent_comment") or ""),
|
|
||||||
)
|
|
||||||
await context.publisher.publish_status(
|
|
||||||
context.request.request_id,
|
|
||||||
"process.v2",
|
|
||||||
router_status.text,
|
|
||||||
dict(router_status.payload),
|
|
||||||
)
|
|
||||||
context.trace.module("process.v2").log(
|
|
||||||
"intent_routed",
|
|
||||||
{
|
|
||||||
"routing_domain": route.routing_domain,
|
|
||||||
"intent": route.intent,
|
|
||||||
"subintent": route.subintent,
|
|
||||||
"normalized_query": route.normalized_query,
|
|
||||||
"target_terms": route.target_terms,
|
|
||||||
"anchors": route_anchor_summary(route),
|
|
||||||
"confidence": route.confidence,
|
|
||||||
"routing_mode": route.routing_mode,
|
|
||||||
"llm_router_used": route.llm_router_used,
|
|
||||||
"reason_short": route.reason_short,
|
|
||||||
"rag_session_id": rag_session_id,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
self._log_step(
|
|
||||||
context,
|
|
||||||
"router_resolved",
|
|
||||||
{
|
|
||||||
"domain": route.routing_domain,
|
|
||||||
"intent": route.intent,
|
|
||||||
"subintent": route.subintent,
|
|
||||||
"confidence": route.confidence,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
self._log_step(
|
|
||||||
context,
|
|
||||||
"anchors_extracted",
|
|
||||||
{
|
|
||||||
"signal_types": route_anchor_summary(route)["signal_types"],
|
|
||||||
"endpoint_paths": route.anchors.endpoint_paths,
|
|
||||||
"target_doc_hints": route.anchors.target_doc_hints,
|
|
||||||
"matched_aliases": route.anchors.matched_aliases,
|
|
||||||
"target_terms": route.target_terms,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
self._log_step(
|
|
||||||
context,
|
|
||||||
"alias_resolution",
|
|
||||||
{
|
|
||||||
"resolved_aliases": route.anchors.matched_aliases,
|
|
||||||
"target_doc_hints": route.anchors.target_doc_hints,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
flow_context = await self._run_workflow(context, route, rag_session_id)
|
|
||||||
if flow_context.answer_generated_payload is not None:
|
|
||||||
self._log_step(context, "answer_generated", dict(flow_context.answer_generated_payload))
|
|
||||||
changeset = list(getattr(flow_context, "changeset", []) or [])
|
|
||||||
apply_changeset = bool(getattr(flow_context, "apply_changeset", False))
|
|
||||||
return ProcessResult(
|
|
||||||
answer=flow_context.answer,
|
|
||||||
changeset=changeset,
|
|
||||||
apply_changeset=apply_changeset,
|
|
||||||
)
|
|
||||||
|
|
||||||
def _log_step(self, context, step: str, payload: dict[str, object]) -> None:
|
|
||||||
context.trace.module("process.v2.pipeline").log(step, payload)
|
|
||||||
|
|
||||||
async def _run_workflow(self, runtime_context, route, rag_session_id: str):
|
|
||||||
workflow = self._workflows.get((route.routing_domain, route.intent, route.subintent))
|
|
||||||
if workflow is None:
|
|
||||||
raise ValueError(f"Unsupported v2 workflow route: {(route.routing_domain, route.intent, route.subintent)!r}")
|
|
||||||
if route.intent == V2Intent.GENERAL_QA:
|
|
||||||
return await workflow.run(
|
|
||||||
GeneralQaSummaryContext(
|
|
||||||
runtime=runtime_context,
|
|
||||||
route=route,
|
|
||||||
rag_session_id=rag_session_id,
|
|
||||||
prompt_name=self._general_summary_prompt_name,
|
|
||||||
workflow_llm_enabled=self._workflow_llm_enabled,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if route.subintent == V2Subintent.FIND_FILES:
|
|
||||||
return await workflow.run(
|
|
||||||
DocExplainFindFilesContext(
|
|
||||||
runtime=runtime_context,
|
|
||||||
route=route,
|
|
||||||
rag_session_id=rag_session_id,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if route.subintent == V2Subintent.API_EXPOSED:
|
|
||||||
return await workflow.run(
|
|
||||||
DocExplainApiExposedContext(
|
|
||||||
runtime=runtime_context,
|
|
||||||
route=route,
|
|
||||||
rag_session_id=rag_session_id,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if route.subintent == V2Subintent.OPENAPI_GENERATE:
|
|
||||||
return await workflow.run(
|
|
||||||
DocGenerateOpenApiContext(
|
|
||||||
runtime=runtime_context,
|
|
||||||
route=route,
|
|
||||||
rag_session_id=rag_session_id,
|
|
||||||
workflow_llm_enabled=self._workflow_llm_enabled,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if route.intent == V2Intent.DOC_UPDATE and route.subintent == V2Subintent.FROM_FEATURE:
|
|
||||||
if self._doc_update_workflow_version == "legacy":
|
|
||||||
return await workflow.run(
|
|
||||||
DocUpdateFromFeatureContext(
|
|
||||||
runtime=runtime_context,
|
|
||||||
route=route,
|
|
||||||
rag_session_id=rag_session_id,
|
|
||||||
doc_rules_enabled=self._doc_rules_enabled,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return await workflow.run(
|
|
||||||
DocUpdateFromFeatureV2Context(
|
|
||||||
runtime=runtime_context,
|
|
||||||
route=route,
|
|
||||||
rag_session_id=rag_session_id,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return await workflow.run(
|
|
||||||
DocExplainSummaryContext(
|
|
||||||
runtime=runtime_context,
|
|
||||||
route=route,
|
|
||||||
rag_session_id=rag_session_id,
|
|
||||||
prompt_name=self._docs_summary_prompt_name,
|
|
||||||
workflow_llm_enabled=self._workflow_llm_enabled,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
def _build_doc_update_workflow(self, llm: AgentLlmService, doc_rules_enabled: bool):
|
|
||||||
if self._doc_update_workflow_version == "legacy":
|
|
||||||
return DocUpdateFromFeatureGraph(llm=llm, doc_rules_enabled=doc_rules_enabled)
|
|
||||||
return DocUpdateFromFeatureV2Graph(llm=llm)
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
# DOC_EXPLAIN / API_EXPOSED Workflow
|
|
||||||
|
|
||||||
## Контракт сабинтента
|
|
||||||
|
|
||||||
| Поле | Значение |
|
|
||||||
|---|---|
|
|
||||||
| `domain` | `DOCS` |
|
|
||||||
| `intent` | `DOC_EXPLAIN` |
|
|
||||||
| `subintent` | `API_EXPOSED` |
|
|
||||||
| `workflow_id` | `v2.docs_explain.api_exposed` |
|
|
||||||
| `source` | `workflow.v2.api_exposed` |
|
|
||||||
|
|
||||||
## Выходной формат
|
|
||||||
|
|
||||||
Ответ формируется детерминированно как список endpoint-путей (`/path`) по одному на строку.
|
|
||||||
Scope учитывается через retrieval-policy фильтры `metadata.domain`/`metadata.subdomain` и path-префиксы API-документации.
|
|
||||||
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
from app.core.agent.processes.v2.workflows.doc_explain_api_exposed.graph import DocExplainApiExposedGraph
|
|
||||||
|
|
||||||
__all__ = ["DocExplainApiExposedGraph"]
|
|
||||||
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from app.core.agent.processes.v2.workflows.doc_explain_api_exposed.steps.build_api_exposed_evidence_step import (
|
|
||||||
BuildApiExposedEvidenceStep,
|
|
||||||
)
|
|
||||||
from app.core.agent.processes.v2.workflows.doc_explain_api_exposed.steps.fetch_rag_rows_step import FetchRagRowsStep
|
|
||||||
from app.core.agent.processes.v2.workflows.doc_explain_api_exposed.steps.finalize_api_exposed_answer_step import (
|
|
||||||
FinalizeApiExposedAnswerStep,
|
|
||||||
)
|
|
||||||
from app.core.agent.processes.v2.workflows.doc_explain_api_exposed.steps.require_rag_session_step import (
|
|
||||||
RequireRagSessionStep,
|
|
||||||
)
|
|
||||||
from app.core.agent.processes.v2.workflows.doc_explain_api_exposed.steps.resolve_retrieval_plan_step import (
|
|
||||||
ResolveRetrievalPlanStep,
|
|
||||||
)
|
|
||||||
from app.core.agent.processes.v2.workflows.doc_explain_api_exposed.steps.retrieval.api_endpoint_collector import (
|
|
||||||
ApiEndpointCollector,
|
|
||||||
)
|
|
||||||
from app.core.agent.processes.v2.workflows.doc_explain_api_exposed.workflow_runtime.buffered_graph import (
|
|
||||||
DocExplainApiExposedWorkflowGraph,
|
|
||||||
)
|
|
||||||
from app.core.agent.processes.v2.workflows.doc_explain_api_exposed.workflow_runtime.context import (
|
|
||||||
DocExplainApiExposedContext,
|
|
||||||
)
|
|
||||||
from app.core.agent.utils.process_v2.plan_resolver import RetrievalPlanResolver
|
|
||||||
from app.core.agent.utils.process_v2.rag_retrieval import V2RagRetrievalAdapter
|
|
||||||
|
|
||||||
|
|
||||||
class DocExplainApiExposedGraph(DocExplainApiExposedWorkflowGraph[DocExplainApiExposedContext]):
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
policy_resolver: RetrievalPlanResolver,
|
|
||||||
rag_adapter: V2RagRetrievalAdapter,
|
|
||||||
) -> None:
|
|
||||||
super().__init__(
|
|
||||||
workflow_id="v2.docs_explain.api_exposed",
|
|
||||||
source="workflow.v2.api_exposed",
|
|
||||||
steps=[
|
|
||||||
RequireRagSessionStep(
|
|
||||||
missing_message="Для процесса v2 нужна активная RAG-сессия проекта с проиндексированной документацией."
|
|
||||||
),
|
|
||||||
ResolveRetrievalPlanStep(policy_resolver),
|
|
||||||
FetchRagRowsStep(rag_adapter),
|
|
||||||
BuildApiExposedEvidenceStep(ApiEndpointCollector()),
|
|
||||||
FinalizeApiExposedAnswerStep(),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
"""Steps for DOC_EXPLAIN/API_EXPOSED workflow."""
|
|
||||||
|
|
||||||
-39
@@ -1,39 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from typing import Generic, TypeVar
|
|
||||||
|
|
||||||
from app.core.agent.processes.v2.workflows.doc_explain_api_exposed.steps.retrieval.api_endpoint_collector import (
|
|
||||||
ApiEndpointCollector,
|
|
||||||
)
|
|
||||||
from app.core.agent.processes.v2.workflows.doc_explain_api_exposed.workflow_runtime.context_protocols import ApiWorkflowContext
|
|
||||||
from app.core.agent.processes.v2.workflows.doc_explain_api_exposed.workflow_runtime.pipeline_logging import log_pipeline_step
|
|
||||||
from app.core.agent.utils.workflow import WorkflowStep
|
|
||||||
|
|
||||||
TContext = TypeVar("TContext", bound=ApiWorkflowContext)
|
|
||||||
|
|
||||||
|
|
||||||
class BuildApiExposedEvidenceStep(WorkflowStep[TContext], Generic[TContext]):
|
|
||||||
step_id = "build_api_exposed_evidence"
|
|
||||||
title = "Сборка списка API"
|
|
||||||
|
|
||||||
def __init__(self, collector: ApiEndpointCollector) -> None:
|
|
||||||
self._collector = collector
|
|
||||||
|
|
||||||
async def run(self, context: TContext) -> TContext:
|
|
||||||
if context.answer:
|
|
||||||
return context
|
|
||||||
context.endpoints = self._collector.collect(context.retrieved_rows)
|
|
||||||
context.runtime.trace.module("process.v2.evidence").log(
|
|
||||||
"evidence_assembled",
|
|
||||||
{"mode": "api_exposed", "endpoint_count": len(context.endpoints), "endpoints": context.endpoints},
|
|
||||||
)
|
|
||||||
log_pipeline_step(
|
|
||||||
context.runtime,
|
|
||||||
"evidence_assembled",
|
|
||||||
{"mode": "api_exposed", "endpoint_count": len(context.endpoints)},
|
|
||||||
)
|
|
||||||
return context
|
|
||||||
|
|
||||||
def trace_output(self, context: TContext) -> dict[str, object]:
|
|
||||||
return {"endpoint_count": len(context.endpoints)}
|
|
||||||
|
|
||||||
-31
@@ -1,31 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from typing import Generic, TypeVar
|
|
||||||
|
|
||||||
from app.core.agent.processes.v2.workflows.doc_explain_api_exposed.workflow_runtime.context_protocols import RetrievalWorkflowContext
|
|
||||||
from app.core.agent.utils.process_v2.rag_retrieval import V2RagRetrievalAdapter
|
|
||||||
from app.core.agent.utils.workflow import WorkflowStep
|
|
||||||
|
|
||||||
TContext = TypeVar("TContext", bound=RetrievalWorkflowContext)
|
|
||||||
|
|
||||||
|
|
||||||
class FetchRagRowsStep(WorkflowStep[TContext], Generic[TContext]):
|
|
||||||
step_id = "fetch_rag_rows"
|
|
||||||
title = "Получение строк из RAG"
|
|
||||||
|
|
||||||
def __init__(self, rag_adapter: V2RagRetrievalAdapter) -> None:
|
|
||||||
self._rag_adapter = rag_adapter
|
|
||||||
|
|
||||||
async def run(self, context: TContext) -> TContext:
|
|
||||||
if context.answer or context.retrieval_plan is None:
|
|
||||||
return context
|
|
||||||
context.retrieved_rows = await self._rag_adapter.fetch_rows(
|
|
||||||
context.rag_session_id,
|
|
||||||
context.route.normalized_query,
|
|
||||||
context.retrieval_plan,
|
|
||||||
)
|
|
||||||
return context
|
|
||||||
|
|
||||||
def trace_output(self, context: TContext) -> dict[str, object]:
|
|
||||||
return {"retrieved_row_count": len(context.retrieved_rows)}
|
|
||||||
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user