Compare commits
11 Commits
5d77ab1a88
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 2b807623f1 | |||
| 77851e99a7 | |||
| 7f22a00696 | |||
| acac19da71 | |||
| 4e3435ad92 | |||
| 2352f91cd3 | |||
| f62fb678b8 | |||
| 7387e5cc51 | |||
| 8b7b72967e | |||
| 0a25e42ea1 | |||
| 51378c5d66 |
+10
-1
@@ -1,3 +1,12 @@
|
||||
.env
|
||||
.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)
|
||||
tests/**/test_runs/**/*.md
|
||||
tests/**/test_runs/**/*.json
|
||||
tests/**/test_results/**/*.md
|
||||
tests/**/test_results/**/*.json
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"files.exclude": {
|
||||
"**/__pycache__": true
|
||||
}
|
||||
}
|
||||
@@ -915,15 +915,15 @@ flowchart TD
|
||||
|
||||
### 4.1.3. Канонический MVP runtime (CODE-first)
|
||||
|
||||
Единая точка входа исполнения — пакет `app.modules.agent.runtime`:
|
||||
Единая точка входа исполнения — пакет `app.core.agent.runtime`:
|
||||
|
||||
- **Роутер:** `app.modules.agent.intent_router_v2`; он отвечает и за routing, и за retrieval planning.
|
||||
- **LLM-слой:** `app.modules.agent.llm`; здесь живут `AgentLlmService`, `PromptLoader` и системные prompt assets.
|
||||
- **Runtime:** `app.modules.agent.runtime`; внутри него stages разложены по подпакетам `retrieval`, `context`, `gates`, `answer_policy`, `generation`, `finalization`.
|
||||
- **Роутер:** `app.core.agent.intent_router`; он отвечает и за routing, и за retrieval planning.
|
||||
- **LLM-слой:** `app.core.agent.llm`; здесь живут `AgentLlmService`, `PromptLoader` и системные prompt assets.
|
||||
- **Runtime:** `app.core.agent.runtime`; внутри него stages разложены по подпакетам `retrieval`, `context`, `gates`, `answer_policy`, `generation`, `finalization`.
|
||||
- **Цепочка:** запрос → `IntentRouterV2` → retrieval planning → runtime retrieval adapter → нормализованный context/evidence → evidence gate 1 → answer policy → LLM generation → evidence gate 2 → finalization → diagnostics.
|
||||
- **Evidence gates:** pre/post проверки достаточности evidence и качества ответа по сценарию.
|
||||
- **Диагностика:** runtime возвращает machine-readable diagnostics и trace по стадиям.
|
||||
- **RAG:** `app.modules.rag` больше не содержит agent use-case слоев; он остается инфраструктурой indexing/retrieval/storage.
|
||||
- **RAG:** `app.core.rag` больше не содержит agent use-case слоев; он остается инфраструктурой indexing/retrieval/storage.
|
||||
|
||||
Тесты: `pipeline_setup_v3` и связанные suite-ы проверяют канонический runtime и его stage-based execution.
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
# Запросы
|
||||
1. Какие методы апи есть в проекте
|
||||
2. Какие методы апи есть для healthcheck
|
||||
3. Где документация на healthcheck
|
||||
Binary file not shown.
@@ -0,0 +1,59 @@
|
||||
# Intents
|
||||
|
||||
## Domains
|
||||
|
||||
- `DOCS`
|
||||
- `GENERAL`
|
||||
- `CODE` - временно отключен
|
||||
|
||||
## GENERAL
|
||||
|
||||
### Intent `GENERAL_QA`
|
||||
|
||||
Общий интент для вопросов без точного маршрута.
|
||||
В дальнейшем может использоваться как fallback.
|
||||
|
||||
Subintents:
|
||||
- `SUMMARY` - ответы на общие вопросы по SUMMARY
|
||||
|
||||
|
||||
|
||||
## DOCS
|
||||
|
||||
### Intent `ARCHITECTURE`
|
||||
|
||||
Обработка вопросов по архитектуре.
|
||||
Subintents пока отсутствуют.
|
||||
Интент запланирован, без реализации.
|
||||
|
||||
### Intent `DOC_EXPLAIN`
|
||||
|
||||
Объяснение по документации.
|
||||
|
||||
Subintents:
|
||||
- `SUMMARY` - краткое объяснение темы по SUMMARY-блокам документации
|
||||
- `FIND_FILES` - поиск файлов с релевантной информацией
|
||||
- `EXPLAIN_API` - объяснение работы метода
|
||||
- `COMPONENT_INTEGRATIONS` - перечень интеграций компонента, API, UI, сущности, внешних систем
|
||||
- `ENTITY_INTEGRATIONS` - перечень интеграций сущности
|
||||
|
||||
В текущем узком MVP реально реализованы только:
|
||||
|
||||
- `SUMMARY`
|
||||
- `FIND_FILES`
|
||||
|
||||
Для запросов по интеграциям целевым retrieval-слоем является `D6_INTEGRATION_INDEX`.
|
||||
|
||||
### Intent `OPENAPI_GENERATION`
|
||||
|
||||
Генерация OpenAPI-спеки.
|
||||
|
||||
Subintents:
|
||||
- `FULL_SPEC` - создание полной спецификации
|
||||
|
||||
### Intent `DOC_GENERATION`
|
||||
|
||||
Редактирование документации.
|
||||
|
||||
Subintents:
|
||||
- `FROM_FEATURE` - создание документации из системной аналитики на фичу
|
||||
|
||||
@@ -0,0 +1,345 @@
|
||||
# RAG
|
||||
|
||||
## Состояние as is
|
||||
|
||||
RAG сейчас используется как общее ядро индексации и retrieval по коду и документации.
|
||||
Основной storage - `rag_session` и многослойный индекс в БД.
|
||||
|
||||
## Основные части
|
||||
|
||||
- `RagService` - фасад индексации и retrieval
|
||||
- `DocsIndexingPipeline` - индексация документации
|
||||
- `CodeIndexingPipeline` - индексация кода
|
||||
- `RagRepository` - persistence и retrieval
|
||||
- `IntentRouterV2` - планирование retrieval: слои, фильтры, ограничения
|
||||
- `RuntimeRetrievalAdapter` - выполнение retrieval в runtime
|
||||
|
||||
## Индексация
|
||||
|
||||
Индексация идет по двум направлениям:
|
||||
|
||||
- `DOCS`
|
||||
- `CODE`
|
||||
|
||||
На вход подается snapshot или changes.
|
||||
Для каждого файла выбирается подходящий pipeline.
|
||||
На выходе формируются документы по слоям и сохраняются в RAG-хранилище.
|
||||
|
||||
## Структура БД
|
||||
|
||||
Все слои сохраняются в общую таблицу `rag_chunks`.
|
||||
|
||||
### Общие поля по слоям
|
||||
|
||||
| Поле БД | Назначение |
|
||||
|---|---|
|
||||
| `rag_session_id` | идентификатор сессии индексации |
|
||||
| `path` | путь исходного файла |
|
||||
| `content` | основной текст записи для retrieval |
|
||||
| `layer` | идентификатор слоя |
|
||||
| `title` | короткий заголовок записи |
|
||||
| `lang` | язык исходного содержимого, в основном для code-слоев |
|
||||
| `repo_id` | идентификатор репозитория или проекта |
|
||||
| `commit_sha` | версия кода или документов на момент индексации |
|
||||
| `span_start`, `span_end` | диапазон строк в исходном файле, если он есть |
|
||||
| `embedding` | векторное представление записи |
|
||||
| `metadata_json` | структурированные атрибуты конкретного слоя |
|
||||
|
||||
### Поля со смыслом слоя
|
||||
|
||||
Смысл конкретного слоя хранится в `metadata_json`.
|
||||
Именно эти атрибуты определяют, какой объект был извлечен и как его интерпретировать в retrieval.
|
||||
Домены и поддомены должны храниться в `metadata_json` явно.
|
||||
|
||||
## Слои DOCS
|
||||
|
||||
### `D0_DOC_CHUNKS`
|
||||
|
||||
Задача:
|
||||
Хранит текстовые фрагменты документации для retrieval по содержимому разделов.
|
||||
|
||||
Формирование:
|
||||
Документ сначала разбирается на frontmatter и body, затем body режется на секции через markdown chunker.
|
||||
Для каждой секции создается отдельная запись слоя.
|
||||
Нарезка идет по разделам документа.
|
||||
Только в fallback-сценарии, когда markdown-структура не найдена, используется нарезка по фиксированным текстовым чанкам.
|
||||
|
||||
Фиксация в БД:
|
||||
| Атрибут в `metadata_json` | Описание | Источник |
|
||||
|---|---|---|
|
||||
| `document_id` | идентификатор документа-источника | `frontmatter.id`, иначе путь файла |
|
||||
| `type` | тип документа из frontmatter | `frontmatter.type` |
|
||||
| `module` | модуль документа | `frontmatter.module` |
|
||||
| `domain` | домен документа | `frontmatter.domain` |
|
||||
| `subdomain` | поддомен документа | `frontmatter.subdomain` |
|
||||
| `tags` | теги документа | `frontmatter.tags` |
|
||||
| `section_path` | полный путь секции в иерархии заголовков | результат `MarkdownDocChunker` |
|
||||
| `section_title` | заголовок текущей секции | результат `MarkdownDocChunker` |
|
||||
| `order` | порядок секции внутри документа | результат `MarkdownDocChunker` |
|
||||
| `doc_kind` | классификация документа, например `readme`, `spec`, `runbook` | `DocsClassifier.classify(path)` |
|
||||
| `source_path` | исходный путь документа | путь файла |
|
||||
| `artifact_type` | тип артефакта, здесь `DOCS` | константа builder |
|
||||
|
||||
Связанные классы:
|
||||
`DocsIndexingPipeline`, `DocsContentParser`, `MarkdownDocChunker`, `DocsDocumentBuilder`
|
||||
|
||||
### `D1_DOCUMENT_CATALOG`
|
||||
|
||||
Задача:
|
||||
Хранит карточку документа как точку входа в документ и его краткое описание.
|
||||
|
||||
Формирование:
|
||||
Источник данных - frontmatter `as is`, 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 документа, а не склейка всех частей документа в сплошной текст.
|
||||
|
||||
Фиксация в БД:
|
||||
| Атрибут в `metadata_json` | Описание | Источник |
|
||||
|---|---|---|
|
||||
| `*` frontmatter fields | все поля frontmatter в исходном виде | frontmatter документа |
|
||||
| `document_id` | идентификатор документа, добавляется только если в frontmatter есть `id` | `frontmatter.id` |
|
||||
| `source_path` | исходный путь документа | путь файла |
|
||||
| `summary_text` | краткое содержание документа | секция `# Summary` |
|
||||
| `doc_kind` | классификация документа, например `readme`, `spec`, `runbook` | `DocsClassifier.classify(path)` |
|
||||
|
||||
Связанные классы:
|
||||
`DocsIndexingPipeline`, `DocsFrontmatterParser`, `DocsClassifier`, `DocsContentParser`, `DocsDocumentBuilder`
|
||||
|
||||
### `D2_FACT_INDEX`
|
||||
|
||||
Задача:
|
||||
Хранит атомарные факты в форме `subject-predicate-object` для точного retrieval по утверждениям.
|
||||
|
||||
Формирование:
|
||||
Факты извлекаются из frontmatter и секций документа, после чего каждая найденная тройка превращается в отдельную запись.
|
||||
|
||||
Фиксация в БД:
|
||||
| Атрибут в `metadata_json` | Описание | Источник |
|
||||
|---|---|---|
|
||||
| `fact_id` | идентификатор факта | вычисляется builder из содержимого факта и пути |
|
||||
| `subject_id` | субъект факта | `DocsFactExtractor` |
|
||||
| `predicate` | предикат или тип связи | `DocsFactExtractor` |
|
||||
| `object` | значение или объект факта | `DocsFactExtractor` |
|
||||
| `object_ref` | ссылка на объект, если она выделена отдельно | `DocsFactExtractor` |
|
||||
| `anchor` | место в документе, откуда взят факт | `DocsFactExtractor` |
|
||||
| `tags` | теги факта | `DocsFactExtractor` |
|
||||
| `source_path` | исходный путь документа | путь файла |
|
||||
|
||||
Связанные классы:
|
||||
`DocsIndexingPipeline`, `DocsFactExtractor`, `DocsDocumentBuilder`
|
||||
|
||||
### `D3_ENTITY_CATALOG`
|
||||
|
||||
Задача:
|
||||
Хранит сущности, найденные в документации, чтобы искать документы и связи вокруг конкретной сущности.
|
||||
|
||||
Формирование:
|
||||
Сущности извлекаются из frontmatter документа, после чего каждая сущность сохраняется отдельной записью.
|
||||
|
||||
Фиксация в БД:
|
||||
| Атрибут в `metadata_json` | Описание | Источник |
|
||||
|---|---|---|
|
||||
| `entity_name` | имя сущности | `DocsEntityExtractor` |
|
||||
| `document_id` | идентификатор документа, где найдена сущность | `frontmatter.id`, иначе путь файла |
|
||||
| `document_type` | тип документа-источника | `frontmatter.type` |
|
||||
| `module` | модуль документа | `frontmatter.module` |
|
||||
| `domain` | домен документа | `frontmatter.domain` |
|
||||
| `subdomain` | поддомен документа | `frontmatter.subdomain` |
|
||||
| `tags` | теги документа или сущности | `frontmatter.tags` |
|
||||
| `source_path` | исходный путь документа | путь файла |
|
||||
|
||||
Связанные классы:
|
||||
`DocsIndexingPipeline`, `DocsEntityExtractor`, `DocsDocumentBuilder`
|
||||
|
||||
### `D4_WORKFLOW_INDEX`
|
||||
|
||||
Задача:
|
||||
Хранит workflow и сценарии из документации для ответов про flow, шаги и жизненный цикл процесса.
|
||||
|
||||
Формирование:
|
||||
Workflow извлекаются из detail sections документа и сохраняются как отдельные сценарии.
|
||||
Извлечение идет из структуры `Details -> ## Сценарий`.
|
||||
|
||||
Фиксация в БД:
|
||||
| Атрибут в `metadata_json` | Описание | Источник |
|
||||
|---|---|---|
|
||||
| `workflow_id` | идентификатор сценария | вычисляется builder из названия, anchor и документа |
|
||||
| `document_id` | идентификатор документа-источника | `frontmatter.id`, иначе путь файла |
|
||||
| `workflow_name` | название сценария | блок `Details -> ## Сценарий -> **Название**` |
|
||||
| `preconditions` | предусловия сценария | блок `Details -> ## Сценарий -> **Предусловия**` |
|
||||
| `trigger` | триггер или событие запуска | блок `Details -> ## Сценарий -> **Триггер**` |
|
||||
| `main_flow` | основной сценарий | блок `Details -> ## Сценарий -> **Основной сценарий**` |
|
||||
| `alternative_flow` | альтернативные ветки | блок `Details -> ## Сценарий -> **Альтернативный сценарий**` |
|
||||
| `error_handling` | обработка ошибок | блок `Details -> ## Сценарий -> **Обработка ошибок**` |
|
||||
| `postconditions` | постусловия | блок `Details -> ## Сценарий -> **Постусловие**` |
|
||||
| `source_path` | исходный путь документа | путь файла |
|
||||
|
||||
Связанные классы:
|
||||
`DocsIndexingPipeline`, `DocsWorkflowExtractor`, `DocsDocumentBuilder`
|
||||
|
||||
### `D5_RELATION_GRAPH`
|
||||
|
||||
Задача:
|
||||
Хранит связи между документами и сущностями документации для navigation и related docs retrieval.
|
||||
|
||||
Формирование:
|
||||
Связи извлекаются из frontmatter и сохраняются как отдельные relation edges.
|
||||
|
||||
Фиксация в БД:
|
||||
| Атрибут в `metadata_json` | Описание | Источник |
|
||||
|---|---|---|
|
||||
| `relation_id` | идентификатор связи | вычисляется builder из source, target, relation type и anchor |
|
||||
| `source_id` | источник связи | `frontmatter.id` или source документа в extractor |
|
||||
| `relation_type` | тип связи | `DocsRelationExtractor` |
|
||||
| `target_id` | целевой объект связи | `DocsRelationExtractor` |
|
||||
| `anchor` | место в документе, где обнаружена связь | `DocsRelationExtractor` |
|
||||
| `source_path` | исходный путь документа | путь файла |
|
||||
|
||||
Связанные классы:
|
||||
`DocsIndexingPipeline`, `DocsRelationExtractor`, `DocsDocumentBuilder`
|
||||
|
||||
### `D6_INTEGRATION_INDEX`
|
||||
|
||||
Задача:
|
||||
Хранит прикладные интеграции компонента, API, UI, сущности или внешней системы.
|
||||
Используется для ответов на вопросы вида "какие интеграции есть у компонента".
|
||||
|
||||
Формирование:
|
||||
Интеграции извлекаются из блока `## Integrations` документа.
|
||||
Одна интеграция должна превращаться в отдельную запись слоя.
|
||||
Описание интеграции может быть развернутым, а структурированные атрибуты должны выделяться в словарь.
|
||||
|
||||
Фиксация в БД:
|
||||
| Атрибут в `metadata_json` | Описание | Источник |
|
||||
|---|---|---|
|
||||
| `integration_id` | идентификатор интеграции | вычисляется builder из source, target и anchor |
|
||||
| `source_id` | идентификатор объекта, для которого описана интеграция | `frontmatter.id` документа-источника |
|
||||
| `source_type` | тип исходного объекта | `frontmatter.doc_type` |
|
||||
| `target` | целевой объект интеграции | блок `## Integrations` |
|
||||
| `target_type` | тип целевого объекта, например `api`, `ui`, `entity`, `service`, `external_system` | блок `## Integrations` |
|
||||
| `direction` | направление интеграции | блок `## Integrations` |
|
||||
| `interaction` | тип взаимодействия | блок `## Integrations` |
|
||||
| `via` | технический канал интеграции | блок `## Integrations` |
|
||||
| `purpose` | назначение интеграции | блок `## Integrations` |
|
||||
| `details` | дополнительные атрибуты интеграции в виде словаря | блок `## Integrations` |
|
||||
| `domain` | домен документа | `frontmatter.domain` |
|
||||
| `subdomain` | поддомен документа | `frontmatter.subdomain` |
|
||||
| `source_path` | исходный путь документа | путь файла |
|
||||
| `anchor` | место в документе, где описана интеграция | блок `## Integrations` |
|
||||
|
||||
Связанные классы:
|
||||
`DocsIndexingPipeline`, `DocsIntegrationExtractor`, `DocsDocumentBuilder`
|
||||
|
||||
## Слои CODE
|
||||
|
||||
### `C0_SOURCE_CHUNKS`
|
||||
|
||||
Задача:
|
||||
Хранит фрагменты исходного кода как базовый слой для цитирования, explain и точечной догрузки кода.
|
||||
|
||||
Формирование:
|
||||
Исходный файл режется на кодовые чанки, и для каждого чанка создается отдельная запись.
|
||||
|
||||
Фиксация в БД:
|
||||
| Атрибут в `metadata_json` | Описание | Источник |
|
||||
|---|---|---|
|
||||
| `chunk_index` | порядковый номер чанка в файле | индекс чанка при нарезке |
|
||||
| `chunk_type` | тип чанка, например функция, класс или текстовый блок | `CodeTextChunker` |
|
||||
| `module_or_unit` | модуль, к которому относится chunk | вычисляется из пути файла |
|
||||
| `is_test` | признак тестового файла | `is_test_path(path)` |
|
||||
|
||||
Связанные классы:
|
||||
`CodeIndexingPipeline`, `CodeTextChunker`, `CodeTextDocumentBuilder`
|
||||
|
||||
### `C1_SYMBOL_CATALOG`
|
||||
|
||||
Задача:
|
||||
Хранит символы кода: классы, функции и методы. Используется для поиска по именам и структуре кода.
|
||||
|
||||
Формирование:
|
||||
Символы извлекаются `SymbolExtractor`, и каждый символ сохраняется как отдельная запись.
|
||||
|
||||
Фиксация в БД:
|
||||
| Атрибут в `metadata_json` | Описание | Источник |
|
||||
|---|---|---|
|
||||
| `symbol_id` | идентификатор символа | `SymbolExtractor` |
|
||||
| `qname` | полное квалифицированное имя | `SymbolExtractor` |
|
||||
| `kind` | тип символа: класс, функция, метод | `SymbolExtractor` |
|
||||
| `signature` | сигнатура символа | `SymbolExtractor` |
|
||||
| `parent_symbol_id` | родительский символ | `SymbolExtractor` |
|
||||
| `package_or_module` | модуль или пакет символа | вычисляется из пути файла |
|
||||
| `is_test` | признак тестового файла | `is_test_path(path)` |
|
||||
|
||||
Связанные классы:
|
||||
`CodeIndexingPipeline`, `PythonAstParser`, `SymbolExtractor`, `SymbolDocumentBuilder`
|
||||
|
||||
### `C2_DEPENDENCY_GRAPH`
|
||||
|
||||
Задача:
|
||||
Хранит связи между символами кода: вызовы, импорты, наследование. Используется для анализа зависимостей и flow.
|
||||
|
||||
Формирование:
|
||||
Связи строятся `EdgeExtractor` по AST и списку символов, после чего каждая связь сохраняется отдельной записью.
|
||||
|
||||
Фиксация в БД:
|
||||
| Атрибут в `metadata_json` | Описание | Источник |
|
||||
|---|---|---|
|
||||
| `edge_id` | идентификатор связи | `EdgeExtractor` |
|
||||
| `edge_type` | тип связи: вызов, импорт, наследование | `EdgeExtractor` |
|
||||
| `src_symbol_id` | исходный символ | `EdgeExtractor` |
|
||||
| `src_qname` | полное имя исходного символа | `EdgeExtractor` |
|
||||
| `dst_symbol_id` | целевой символ, если он разрешен | `EdgeExtractor` |
|
||||
| `dst_ref` | текстовая ссылка на целевой символ | `EdgeExtractor` |
|
||||
| `resolution` | статус разрешения связи | `EdgeExtractor` |
|
||||
| `is_test` | признак тестового файла | `is_test_path(path)` |
|
||||
|
||||
Связанные классы:
|
||||
`CodeIndexingPipeline`, `EdgeExtractor`, `EdgeDocumentBuilder`
|
||||
|
||||
### `C3_ENTRYPOINTS`
|
||||
|
||||
Задача:
|
||||
Хранит точки входа приложения: HTTP routes, CLI commands и другие entrypoints.
|
||||
|
||||
Формирование:
|
||||
Детекторы ищут HTTP и CLI точки входа по символам файла, после чего каждый найденный entrypoint сохраняется отдельной записью.
|
||||
|
||||
Фиксация в БД:
|
||||
| Атрибут в `metadata_json` | Описание | Источник |
|
||||
|---|---|---|
|
||||
| `entry_id` | идентификатор точки входа | detector entrypoint model |
|
||||
| `entry_type` | тип точки входа | detector entrypoint model |
|
||||
| `framework` | framework, в котором найдена точка входа | detector entrypoint model |
|
||||
| `route_or_command` | route или команда | detector entrypoint model |
|
||||
| `handler_symbol_id` | идентификатор обработчика | detector entrypoint model |
|
||||
| `handler_symbol` | имя обработчика | detector entrypoint model |
|
||||
| `declaring_symbol` | символ, в котором объявлен entrypoint | detector entrypoint model |
|
||||
| `entrypoint_kind` | вид точки входа | detector entrypoint model |
|
||||
| `http_method` | HTTP-метод | detector entrypoint model |
|
||||
| `route_path` | путь маршрута | detector entrypoint model |
|
||||
| `decorator_text` | текст декоратора или объявления | detector entrypoint model |
|
||||
| `summary_text` | краткое описание точки входа | detector entrypoint model |
|
||||
| `is_test` | признак тестового файла | `is_test_path(path)` |
|
||||
| `lang_payload` | дополнительные данные детектора | detector metadata |
|
||||
| `artifact_type` | тип артефакта, здесь `CODE` | константа builder |
|
||||
|
||||
Связанные классы:
|
||||
`CodeIndexingPipeline`, `EntrypointDetectorRegistry`, `FastApiEntrypointDetector`, `FlaskEntrypointDetector`, `TyperClickEntrypointDetector`, `EntrypointDocumentBuilder`
|
||||
|
||||
### `C4_SEMANTIC_ROLES`
|
||||
|
||||
Задача:
|
||||
Слой объявлен в enum и retrieval-планах как слой семантических ролей кода.
|
||||
|
||||
Формирование:
|
||||
Слой формируется на основе символов, связей, dataflow slices и execution traces.
|
||||
В текущем runtime этот слой не используется как активный маршрут, так как домен `CODE` отключен.
|
||||
|
||||
Фиксация в БД:
|
||||
Смысловые атрибуты слоя сохраняются в `rag_chunks.metadata_json`.
|
||||
Точное краткое описание состава этих атрибутов в текущем документе пока не зафиксировано.
|
||||
|
||||
Связанные классы:
|
||||
`CodeIndexingPipeline`, `SemanticRoleBuilder`, `SemanticRoleDocumentBuilder`
|
||||
@@ -0,0 +1,289 @@
|
||||
## 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`.
|
||||
- Не допускать хаотичной вложенности заголовков.
|
||||
- Вместо дублирования использовать ссылки на связанные документы.
|
||||
- Сценарии, правила, ограничения и кодовые привязки держать раздельно.
|
||||
@@ -0,0 +1,212 @@
|
||||
# Системная аналитика
|
||||
|
||||
## Общее описание
|
||||
|
||||
Документ описывает изменения в автоматизированной системе. Пишется системными аналитиками для разработчиков и тестировщиков и проходит согласование с экспертами по архитектуре, безопасности и сопровождению.
|
||||
|
||||
Документ может описывать как новый процесс, так и инкремент доработки существующей функциональности.
|
||||
|
||||
## Требования к заголовкам
|
||||
|
||||
- Заголовок должен отражать суть раздела.
|
||||
- Заголовок не должен содержать лишнюю информацию, которая относится к метаданным (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`.
|
||||
|
||||
## Термины
|
||||
|
||||
- Аудит: события, которые фиксируют действия пользователя и позволяют ответить на вопрос «кто, что, когда сделал».
|
||||
- Мониторинг: технические события/метрики для контроля стабильности и поиска сбоев.
|
||||
@@ -1,790 +0,0 @@
|
||||
## 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;
|
||||
- ошибки;
|
||||
- НФТ;
|
||||
- связи;
|
||||
- кодовые привязки.
|
||||
|
||||
## 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
|
||||
## Errors
|
||||
## Нефункциональные требования
|
||||
## Связанные блоки логики
|
||||
## Связанный код
|
||||
## Связанные документы
|
||||
## История изменений
|
||||
```
|
||||
|
||||
#### Требования к разделу `Contract`
|
||||
Контракт может:
|
||||
- быть кратко описан прямо в документе;
|
||||
- ссылаться на OpenAPI;
|
||||
- ссылаться на отдельный контрактный файл.
|
||||
|
||||
Для REST API целевым источником истины должен становиться `OpenAPI`.
|
||||
|
||||
### Reusable Logic Block
|
||||
|
||||
```md
|
||||
# <Название блока логики>
|
||||
|
||||
## Summary
|
||||
## Назначение
|
||||
## Контекст
|
||||
## Технический use case
|
||||
## Функциональные требования
|
||||
## Ограничения и условия вызова
|
||||
## Нефункциональные требования
|
||||
## Связанные 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 документации;
|
||||
- глубокая автоматизация подготовки системной аналитики.
|
||||
@@ -0,0 +1,100 @@
|
||||
# MVP: процесс v1
|
||||
|
||||
## 1. Общее описание
|
||||
|
||||
Запрос пользователя обрабатывается цепочкой API → рантайм агента → зарегистрированный процесс версии `v1` → один workflow из трёх последовательных шагов. Процесс **не** обращается к RAG и **не** маршрутизирует интенты: текст сообщения передаётся в LLM по фиксированному промпту. Ответ агента — результат генерации с лёгкой постобработкой (trim).
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph api [API]
|
||||
RS[RequestService]
|
||||
end
|
||||
subgraph runtime [Agent runtime]
|
||||
AR[AgentRuntime]
|
||||
PR[ProcessRunner]
|
||||
end
|
||||
subgraph v1 [Процесс v1]
|
||||
P1[V1Process]
|
||||
WG[V1FlowMainGraph]
|
||||
end
|
||||
subgraph wf [Workflow v1.flow_main]
|
||||
S1[PrepareUserMessageStep]
|
||||
S2[GenerateAnswerStep]
|
||||
S3[FinalizeAnswerStep]
|
||||
end
|
||||
LLM[AgentLlmService]
|
||||
RS --> AR
|
||||
AR --> PR
|
||||
PR --> P1
|
||||
P1 --> WG
|
||||
WG --> S1 --> S2 --> S3
|
||||
S2 --> LLM
|
||||
```
|
||||
|
||||
Клиент создаёт запрос с `process_version: v1`. `AgentRuntime` поднимает `RuntimeExecutionContext` (запрос, сессия, publisher, trace), выбирает `V1Process` из реестра и вызывает `run`. `V1Process` собирает `V1FlowContext` и прогоняет линейный граф: подготовка текста, один вызов LLM, финализация строки ответа. Итог попадает в `ProcessResult.answer` и дальше в ответ пользователю.
|
||||
|
||||
---
|
||||
|
||||
## 2. Шаги и контракты
|
||||
|
||||
### 2.1. Вход в процесс: `V1Process.run`
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| **Название** | Запуск процесса v1 |
|
||||
| **Задача** | Собрать контекст workflow и выполнить граф до готового ответа. |
|
||||
| **Вход** | `RuntimeExecutionContext`: `request` (в т.ч. `message`), `session`, `publisher`, `trace`. |
|
||||
| **Выход** | `ProcessResult` с полем `answer: str`. |
|
||||
| **Как работает** | Создаётся `V1FlowContext` с `prompt_name` по умолчанию `v1_flow_main.answer`. Вызывается `V1FlowMainGraph.run`. Возвращается ответ из контекста workflow. |
|
||||
|
||||
Код: `src/app/core/agent/processes/v1/process.py`.
|
||||
|
||||
---
|
||||
|
||||
### 2.2. Шаг workflow: `PrepareUserMessageStep`
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| **Название** | Подготовка сообщения пользователя |
|
||||
| **Задача** | Сформировать строку, которая уйдёт в LLM как пользовательский ввод. |
|
||||
| **Вход** | `V1FlowContext` с заполненным `runtime` и `prompt_name`. |
|
||||
| **Выход** | Тот же контекст с `prepared_message: str`. |
|
||||
| **Как работает** | Берётся `context.runtime.request.message` и обрезаются пробелы по краям (`strip`). Результат пишется в `prepared_message`. Других преобразований нет. |
|
||||
|
||||
Код: `src/app/core/agent/processes/v1/workflow/flow_main/steps/prepare_user_message_step.py`.
|
||||
|
||||
---
|
||||
|
||||
### 2.3. Шаг workflow: `GenerateAnswerStep`
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| **Название** | Вызов LLM |
|
||||
| **Задача** | Сгенерировать ответ по выбранному промпту и подготовленному сообщению. |
|
||||
| **Вход** | `V1FlowContext` с `prepared_message`, `prompt_name`, `runtime.trace` для модуля LLM. |
|
||||
| **Выход** | Контекст с `answer: str` (сырой ответ модели). |
|
||||
| **Как работает** | Асинхронно в пуле потоков вызывается `AgentLlmService.generate(prompt_name, prepared_message, ...)`. В trace подключается модуль `workflow.v1.llm`. Идентификатор запроса передаётся в `log_context` для логов. |
|
||||
|
||||
Код: `src/app/core/agent/processes/v1/workflow/flow_main/steps/generate_answer_step.py`.
|
||||
|
||||
---
|
||||
|
||||
### 2.4. Шаг workflow: `FinalizeAnswerStep`
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| **Название** | Финализация ответа |
|
||||
| **Задача** | Нормализовать строку ответа перед выдачей пользователю. |
|
||||
| **Вход** | `V1FlowContext` с заполненным `answer` после LLM. |
|
||||
| **Выход** | Контекст с обновлённым `answer`. |
|
||||
| **Как работает** | К ответу применяется `strip()` по краям. Другой логики нет. |
|
||||
|
||||
Код: `src/app/core/agent/processes/v1/workflow/flow_main/steps/finalize_answer_step.py`.
|
||||
|
||||
---
|
||||
|
||||
### 2.5. Транспорт: `WorkflowGraph` (v1)
|
||||
|
||||
Граф для v1 использует стандартный `WorkflowGraph`: на каждом шаге пишутся события `workflow_started`, `step_started`, `step_completed`, `workflow_completed` в `runtime_traces` через `context.runtime.trace`.
|
||||
|
||||
Код: `src/app/core/agent/utils/workflow/graph.py`, обёртка `V1FlowMainGraph` в `src/app/core/agent/processes/v1/workflow/flow_main/graph.py`.
|
||||
@@ -0,0 +1,33 @@
|
||||
Нужно реализовать 2 вещи
|
||||
|
||||
Создать процесс внесения изменений в файл документации
|
||||
Создать контекст этого процесса
|
||||
|
||||
Контекст наполнять атрибутами
|
||||
что-то явно задано, фоллбэк через ллм
|
||||
|
||||
|
||||
|
||||
Написать тестовую аналитику - круд над сущностью
|
||||
фронт, ефс, ппрб
|
||||
Все в своей БД
|
||||
Атрибуты сущности задать в требованиях
|
||||
|
||||
|
||||
|
||||
|
||||
Аналитика имеет структуру
|
||||
Внутри модули - один модуль на правку одного файла.
|
||||
|
||||
|
||||
Модуль извлекается из аналитики парсером и из него формируется задача на редактирование файла
|
||||
если парсер не сработал - фоллбэк ан ллм
|
||||
|
||||
|
||||
|
||||
Процесс редактирования работает стандартно
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
# MVP: процесс v2
|
||||
|
||||
## 1. Общее описание
|
||||
|
||||
Процесс v2 в текущем MVP ориентирован в первую очередь на **документацию проекта**, но роутер также поддерживает `GENERAL / GENERAL_QA / SUMMARY` для общих обзорных вопросов. Для документных веток нужна активная RAG-сессия с проиндексированными документами.
|
||||
|
||||
Это **узкий MVP**, а не полная target architecture. Поддерживаются три маршрута:
|
||||
|
||||
- `GENERAL`
|
||||
- `GENERAL_QA`
|
||||
- `SUMMARY`
|
||||
- `DOCS`
|
||||
- `DOC_EXPLAIN`
|
||||
- `SUMMARY`
|
||||
- `FIND_FILES`
|
||||
|
||||
Запрос проходит следующие смысловые этапы:
|
||||
|
||||
1. проверка готовности сессии;
|
||||
2. intent routing;
|
||||
3. формирование retrieval-параметров;
|
||||
4. retrieval из `DOCS RAG`;
|
||||
5. минимальная сборка evidence;
|
||||
6. запуск task-focused workflow нужной ветки;
|
||||
7. формирование ответа.
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph api [API]
|
||||
RS[RequestService]
|
||||
end
|
||||
subgraph runtime [Agent runtime]
|
||||
AR[AgentRuntime]
|
||||
PR[ProcessRunner]
|
||||
end
|
||||
subgraph v2 [Процесс v2]
|
||||
P2[V2Process]
|
||||
IR[V2IntentRouter]
|
||||
POL[V2RetrievalPolicyResolver]
|
||||
AD[V2RagRetrievalAdapter]
|
||||
RSR[RagSessionRetriever]
|
||||
ASM[DocsEvidenceAssembler]
|
||||
end
|
||||
subgraph rag [Пакет rag]
|
||||
RR[RagRepository]
|
||||
end
|
||||
subgraph wf [Workflow]
|
||||
SUM[DocsExplainSummaryGraph]
|
||||
FF[DocsExplainFindFilesGraph]
|
||||
end
|
||||
LLM[AgentLlmService]
|
||||
RS --> AR --> PR --> P2
|
||||
P2 --> IR --> POL --> AD --> RSR --> RR
|
||||
AD --> ASM
|
||||
ASM --> SUM
|
||||
ASM --> FF
|
||||
SUM --> LLM
|
||||
```
|
||||
|
||||
Клиент указывает `process_version: v2`. Без `active_rag_session_id` в сессии процесс возвращает сообщение об ошибке. Иначе выполняется цепочка:
|
||||
|
||||
маршрутизация → `RetrievalPlan` → retrieval строк из `DOCS RAG` → минимальная сборка evidence → ветвление по `subintent` → запуск workflow.
|
||||
|
||||
### Реализованные домены, интенты и сабинтенты
|
||||
|
||||
В коде заданы константы `V2Domain`, `V2Intent`, `V2Subintent`. Сейчас процесс intentionally ограничен одной рабочей областью.
|
||||
|
||||
| Уровень | Значение (строка) | Реализация |
|
||||
|--------|-------------------|------------|
|
||||
| **Домен (routing_domain)** | `DOCS` | Единственный поддерживаемый домен: документация проекта. |
|
||||
| **Интент** | `DOC_EXPLAIN` | Единственный интент: объяснение по документации. |
|
||||
| **Сабинтент** | `SUMMARY` | Объяснение темы по SUMMARY-блокам документации. |
|
||||
| **Сабинтент** | `FIND_FILES` | Поиск путей к документам, где описана нужная сущность или тема. |
|
||||
|
||||
Итого в текущем MVP реализована **одна** рабочая тройка домен×интент: `DOCS` + `DOC_EXPLAIN`, с **двумя** ветками по сабинтенту.
|
||||
|
||||
---
|
||||
|
||||
## 2. Этапы вне workflow (внутри `V2Process.run`)
|
||||
|
||||
### 2.1. `V2IntentRouter.route`
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| **Название** | Маршрутизация запроса (v2) |
|
||||
| **Задача** | Определить домен, интент, subintent и извлечь якоря из текста. |
|
||||
| **Вход** | `user_query: str` (текст сообщения пользователя). |
|
||||
| **Выход** | `V2RouteResult`: `routing_domain`, `intent`, `subintent`, `user_query`, `normalized_query`, `target_terms`, `anchors` (`V2RouteAnchors`), `confidence`. |
|
||||
| **Как работает** | Router реализован по схеме **LLM-first**: `normalization` → `target_terms`/`anchors extraction` → `LLM router` → `deterministic validator` → `fallback`. LLM является **основным селектором маршрута**. Deterministic-слой больше не выбирает маршрут по умолчанию: он отвечает только за extraction, валидацию enum/комбинаций и fallback при сломанном или невалидном ответе LLM. В trace пишется событие `intent_routed`. |
|
||||
|
||||
Код: `src/app/core/agent/processes/v2/intent_router/router.py`, `modules/normalizer.py`, `modules/target_terms.py`, `modules/anchors.py`, `routers/llm.py`, `routers/validator.py`, `routers/fallback.py`.
|
||||
|
||||
---
|
||||
|
||||
### 2.2. `V2RetrievalPolicyResolver.resolve`
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| **Название** | Политика retrieval для v2 |
|
||||
| **Задача** | По результату роутинга выбрать профиль, список слоёв RAG и лимит строк выдачи. |
|
||||
| **Вход** | `V2RouteResult`. |
|
||||
| **Выход** | `RetrievalPlan`: `profile`, `layers`, `limit`, опционально `filters`. |
|
||||
| **Как работает** | Это отдельный смысловой шаг между routing и retrieval. Он не ходит в БД и не извлекает данные, а только подготавливает параметры поиска. Для `FIND_FILES` выбирается один профиль слоёв и лимит, для `SUMMARY` — другой. Лог: `retrieval_plan_resolved`. |
|
||||
|
||||
Код: `src/app/core/agent/processes/v2/retrieval/policy_resolver.py`.
|
||||
|
||||
---
|
||||
|
||||
### 2.3. `V2RagRetrievalAdapter` → `RagSessionRetriever.retrieve`
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| **Название** | Загрузка сырых строк из RAG по плану |
|
||||
| **Задача** | Делегировать поиск в единственную реализацию retrieval в пакете `rag`. |
|
||||
| **Вход** | `rag_session_id`, `query_text` (нормализованный запрос), `RetrievalPlan`. |
|
||||
| **Выход** | `list[dict]` — строки чанков в формате `RagRepository.retrieve` (поля `path`, `layer`, `metadata`, и т.д.). |
|
||||
| **Как работает** | Выполняется retrieval по уже сформированному плану: профиль, список слоёв и лимит. На этом шаге происходит только извлечение сырых строк из `DOCS RAG`. Лог: `rag_rows_fetched`. |
|
||||
|
||||
Код адаптера: `src/app/core/agent/processes/v2/retrieval/v2_rag_adapter.py`.
|
||||
Код API: `src/app/core/rag/retrieval/session_retriever.py`.
|
||||
|
||||
---
|
||||
|
||||
### 2.4. `DocsEvidenceAssembler`
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| **Название** | Сборка evidence для задачи |
|
||||
| **Задача** | Превратить сырые строки retrieval в списки summary или кандидатов файлов с дедупом и скорингом. |
|
||||
| **Вход** | Список строк `rows`, `V2RouteResult` (для `target_terms`). |
|
||||
| **Выход** | `list[RetrievedSummary]` или `list[RetrievedFile]`. |
|
||||
| **Как работает** | Это **минимальная evidence-проверка**, достаточная для MVP. Для `SUMMARY` отбрасываются записи без summary-текста и summary-like секции, затем применяется дедуп и простой скоринг по терминам. Для `FIND_FILES` остаются только релевантные пути документов, также с дедупом и простым скорингом. Здесь нет сложной многоступенчатой валидации: задача шага — отфильтровать очевидный шум и передать в workflow компактное evidence. Лог: `evidence_assembled`. |
|
||||
|
||||
Код: `src/app/core/agent/processes/v2/evidence/assembler.py`.
|
||||
|
||||
---
|
||||
|
||||
## 3. Шаги workflow
|
||||
|
||||
Текущие workflow являются **task-focused**: каждая ветка решает одну узкую прикладную задачу и не содержит общей универсальной логики для всех типов вопросов.
|
||||
|
||||
### 3.1. Ветка `SUMMARY`: `GenerateSummaryAnswerStep`
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| **Название** | Сборка ответа по summary |
|
||||
| **Задача** | Сформировать ответ пользователю по найденным SUMMARY-блокам или сообщить об отсутствии. |
|
||||
| **Вход** | `DocsExplainSummaryContext`: `runtime`, `route`, `rag_session_id`, `prompt_name`, `documents` (список `RetrievedSummary`). |
|
||||
| **Выход** | Контекст с `answer: str`, `prompt_input` при успешном вызове LLM. |
|
||||
| **Как работает** | Workflow получает уже отобранные summary-документы. Если документов нет — возвращает честный fallback-ответ. Иначе собирает prompt input из запроса пользователя и найденных summary-блоков и вызывает LLM. Workflow не занимается retrieval и не строит retrieval-план: он решает только задачу генерации ответа по уже подготовленному evidence. |
|
||||
|
||||
Код: `src/app/core/agent/processes/v2/workflows/docs_explain_summary/steps/generate_summary_answer_step.py`.
|
||||
Граф: `DocsExplainSummaryGraph` (`V2WorkflowGraph`).
|
||||
|
||||
---
|
||||
|
||||
### 3.2. Ветка `FIND_FILES`: `FinalizeFindFilesAnswerStep`
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| **Название** | Сборка списка файлов |
|
||||
| **Задача** | Вывести пользователю markdown-список путей к файлам документации. |
|
||||
| **Вход** | `DocsExplainFindFilesContext`: `runtime`, `route`, `rag_session_id`, `files` (`RetrievedFile`). |
|
||||
| **Выход** | Контекст с `answer: str`. |
|
||||
| **Как работает** | Workflow получает уже собранный список файлов и формирует финальный ответ. Если файлов нет — возвращает fallback. Если файлы есть — отдает детерминированный список путей. Эта ветка intentionally не использует LLM, потому что задача сводится к выдаче путей, а не к генерации объяснения. |
|
||||
|
||||
Код: `src/app/core/agent/processes/v2/workflows/docs_explain_find_files/steps/finalize_find_files_answer_step.py`.
|
||||
Граф: `DocsExplainFindFilesGraph` (`V2WorkflowGraph`).
|
||||
|
||||
---
|
||||
|
||||
### 3.3. Транспорт: `V2WorkflowGraph`
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| **Название** | Workflow v2 с буфером trace |
|
||||
| **Задача** | Выполнить шаги без пошаговых `step_started`/`step_completed` в trace; один раз сбросить сводку. |
|
||||
| **Вход** | Контекст workflow (`DocsExplainSummaryContext` или `DocsExplainFindFilesContext`). |
|
||||
| **Выход** | Обновлённый контекст. |
|
||||
| **Как работает** | Для каждого шага: `trace_input` до `run`, затем `run`, затем `trace_output`; записи копятся в список. В trace уходят `workflow_started`, затем `workflow_trace_flushed` с массивом шагов, затем `workflow_completed`. Статусы пользователю публикуются через `publisher` как и раньше. |
|
||||
|
||||
Код: `src/app/core/agent/processes/v2/workflows/v2_workflow_graph.py`.
|
||||
|
||||
---
|
||||
|
||||
## 4. Сборка в приложении
|
||||
|
||||
В `ModularApplication` создаются `RagSessionRetriever`, `V2RagRetrievalAdapter`, `V2RetrievalPolicyResolver`, `DocsEvidenceAssembler` и передаются в `V2Process` (см. `src/app/core/application.py`).
|
||||
|
||||
---
|
||||
|
||||
## 5. Итоговая концептуальная схема текущего MVP
|
||||
|
||||
В концептуальном виде текущий `v2` работает так:
|
||||
|
||||
1. **Session check**
|
||||
Проверка, что есть активная RAG-сессия проекта.
|
||||
|
||||
2. **LLM-first intent routing**
|
||||
Нормализация, extraction (`target_terms`, `anchors`), затем основной выбор маршрута через LLM.
|
||||
|
||||
3. **Deterministic validation + fallback**
|
||||
Проверка enum/комбинации маршрута и fallback только если LLM не ответил или вернул невалидный маршрут.
|
||||
|
||||
4. **Retrieval parameter planning**
|
||||
Формирование профиля поиска, слоёв и лимитов.
|
||||
|
||||
5. **RAG retrieval**
|
||||
Загрузка сырых строк из `DOCS RAG`.
|
||||
|
||||
6. **Minimal evidence assembly**
|
||||
Дедуп, базовый скоринг, отбор полезных summary или файлов.
|
||||
|
||||
7. **Task-focused workflow**
|
||||
Узкая ветка `SUMMARY` или `FIND_FILES`.
|
||||
|
||||
8. **Final response**
|
||||
Либо explanation через LLM, либо детерминированный список файлов.
|
||||
|
||||
Это и есть актуальная архитектура **узкого MVP**, синхронизированная с текущей реализацией.
|
||||
@@ -0,0 +1,346 @@
|
||||
# V2IntentRouter Architecture
|
||||
|
||||
## 1. Архитектура
|
||||
|
||||
Текущий `V2IntentRouter` реализован как **LLM-first router**.
|
||||
Deterministic-слой не выбирает маршрут по умолчанию и используется только для:
|
||||
|
||||
- preprocessing
|
||||
- validation ответа LLM
|
||||
- fallback, если LLM не ответил или вернул невалидный маршрут
|
||||
|
||||
Актуальные компоненты:
|
||||
|
||||
- `router.py`
|
||||
Главная точка входа и оркестратор пайплайна.
|
||||
|
||||
- `modules/normalizer.py`
|
||||
Нормализация текста запроса в `normalized_query`.
|
||||
|
||||
- `modules/target_terms.py`
|
||||
Извлечение retrieval-oriented `target_terms`, `endpoint_paths`, `matched_aliases`, `alias_docs`.
|
||||
|
||||
- `modules/anchors.py`
|
||||
Извлечение `anchors` и marker-сигналов для fallback и downstream retrieval.
|
||||
|
||||
- `routers/route_catalog.py`
|
||||
Каталог допустимых маршрутов (`allowed_routes`).
|
||||
|
||||
- `routers/llm.py`
|
||||
Основной LLM-router. Получает нормализованный запрос, `target_terms`, `anchors` и список допустимых маршрутов.
|
||||
|
||||
- `routers/validator.py`
|
||||
Deterministic validator для enum-значений, комбинации маршрута и базовой нормализации `confidence`.
|
||||
|
||||
- `routers/confidence.py`
|
||||
Пост-обработка confidence после ответа LLM.
|
||||
|
||||
- `routers/fallback.py`
|
||||
Fallback-маршрутизация, если LLM не ответил или ответ не прошёл validator.
|
||||
|
||||
- `routers/prompts.yml`
|
||||
Prompt-контракт для LLM-router.
|
||||
|
||||
## 2. Контракт
|
||||
|
||||
### Вход
|
||||
|
||||
- `user_query: str`
|
||||
|
||||
### Выход
|
||||
|
||||
`V2RouteResult`:
|
||||
|
||||
- `routing_domain: str`
|
||||
- `intent: str`
|
||||
- `subintent: str`
|
||||
- `user_query: str`
|
||||
- `normalized_query: str`
|
||||
- `target_terms: list[str]`
|
||||
- `anchors: V2RouteAnchors`
|
||||
- `confidence: float`
|
||||
- `routing_mode: str`
|
||||
- `llm_router_used: bool`
|
||||
- `reason_short: str`
|
||||
|
||||
`V2RouteAnchors`:
|
||||
|
||||
- `entity_names: list[str]`
|
||||
- `file_names: list[str]`
|
||||
- `endpoint_paths: list[str]`
|
||||
- `target_doc_hints: list[str]`
|
||||
- `matched_aliases: list[str]`
|
||||
- `process_domain: str | None`
|
||||
- `process_subdomain: str | None`
|
||||
|
||||
## 3. Поддерживаемые домены, интенты и сабинтенты
|
||||
|
||||
### Домены
|
||||
|
||||
- `DOCS`
|
||||
- `GENERAL`
|
||||
|
||||
### Интенты
|
||||
|
||||
- `DOC_EXPLAIN`
|
||||
- `GENERAL_QA`
|
||||
|
||||
### Сабинтенты
|
||||
|
||||
- `SUMMARY`
|
||||
- `FIND_FILES`
|
||||
|
||||
### Допустимые маршруты
|
||||
|
||||
- `GENERAL / GENERAL_QA / SUMMARY`
|
||||
- `DOCS / DOC_EXPLAIN / SUMMARY`
|
||||
- `DOCS / DOC_EXPLAIN / FIND_FILES`
|
||||
|
||||
Эти маршруты централизованно заданы в `routers/route_catalog.py`.
|
||||
|
||||
## 4. Актуальный флоу
|
||||
|
||||
Пайплайн обработки запроса:
|
||||
|
||||
1. `router.py` принимает `user_query`.
|
||||
2. `modules/normalizer.py` строит `normalized_query`.
|
||||
3. `modules/target_terms.py` извлекает:
|
||||
- `target_terms`
|
||||
- `endpoint_paths`
|
||||
- `matched_aliases`
|
||||
- `alias_docs`
|
||||
4. `modules/anchors.py` строит:
|
||||
- `anchors`
|
||||
- `file_markers`
|
||||
- `architecture_markers`
|
||||
- `logic_markers`
|
||||
- `domain_markers`
|
||||
- `endpoint_markers`
|
||||
5. `router.py` собирает `QueryFeatures`.
|
||||
6. `routers/llm.py` вызывается как **основной селектор маршрута**.
|
||||
7. `routers/validator.py` проверяет:
|
||||
- что значения входят в допустимые enum
|
||||
- что комбинация маршрута разрешена
|
||||
- что `confidence` можно привести к `float`
|
||||
8. `routers/confidence.py` корректирует confidence на основе силы сигналов.
|
||||
9. Если ответ LLM валиден, возвращается `V2RouteResult` с `routing_mode="llm_default"`.
|
||||
10. Если LLM не ответил, вернул сломанный JSON или невалидный маршрут, `routers/fallback.py` строит fallback route:
|
||||
- `FIND_FILES`, если есть `file_markers`
|
||||
- `DOCS / DOC_EXPLAIN / SUMMARY`, если есть docs-oriented anchors
|
||||
- иначе `GENERAL / GENERAL_QA / SUMMARY`
|
||||
|
||||
## 5. Компоненты по флоу
|
||||
|
||||
### `router.py`
|
||||
|
||||
- Задача
|
||||
Оркестрировать полный routing pipeline.
|
||||
|
||||
- Как решает
|
||||
Последовательно вызывает:
|
||||
- normalizer
|
||||
- target terms extractor
|
||||
- anchor extractor
|
||||
- LLM router
|
||||
- validator
|
||||
- confidence adjuster
|
||||
- fallback router
|
||||
|
||||
- Вход
|
||||
`user_query: str`
|
||||
|
||||
- Выход
|
||||
`V2RouteResult`
|
||||
|
||||
### `modules/normalizer.py`
|
||||
|
||||
- Задача
|
||||
Привести запрос к стабильной форме для анализа.
|
||||
|
||||
- Как решает
|
||||
Схлопывает лишние пробелы через `" ".join(...split())`.
|
||||
|
||||
- Вход
|
||||
`user_query: str`
|
||||
|
||||
- Выход
|
||||
`normalized_query: str`
|
||||
|
||||
### `modules/target_terms.py`
|
||||
|
||||
- Задача
|
||||
Построить **чистое retrieval-поле** `target_terms`.
|
||||
|
||||
- Как решает
|
||||
Использует позитивную модель отбора и включает в `target_terms` только:
|
||||
- endpoint paths
|
||||
- identifier-like tokens
|
||||
- alias canonical terms
|
||||
- domain terms
|
||||
|
||||
Исключаются:
|
||||
- question words
|
||||
- intent words
|
||||
- filler/noisy words
|
||||
- marker words
|
||||
- короткие токены `< 3`, если это не endpoint или alias
|
||||
- битые path-like токены
|
||||
|
||||
Дополнительно:
|
||||
- lowercase
|
||||
- trim punctuation по краям
|
||||
- dedupe
|
||||
- ограничение до `7` элементов
|
||||
- приоритет: endpoints → identifiers → aliases → domain terms
|
||||
|
||||
- Вход
|
||||
`normalized_query: str`
|
||||
|
||||
- Выход
|
||||
`TargetTermsAnalysis`:
|
||||
- `target_terms`
|
||||
- `endpoint_paths`
|
||||
- `matched_aliases`
|
||||
- `alias_docs`
|
||||
|
||||
### `modules/anchors.py`
|
||||
|
||||
- Задача
|
||||
Построить `anchors` и marker-сигналы, не смешивая их с `target_terms`.
|
||||
|
||||
- Как решает
|
||||
Извлекает:
|
||||
- `entity_names` из PascalCase-like токенов
|
||||
- `file_names` только по жёстким правилам:
|
||||
- `*.md`, `*.yaml`, `*.yml`, `*.json`
|
||||
- `docs/...`, `doc/...`, `documentation/...`
|
||||
- `endpoint_paths` из `TargetTermsAnalysis`
|
||||
- `target_doc_hints` из alias docs, endpoint map и marker-сигналов
|
||||
|
||||
Marker-сигналы живут отдельно:
|
||||
- `file_markers`
|
||||
- `architecture_markers`
|
||||
- `logic_markers`
|
||||
- `domain_markers`
|
||||
- `endpoint_markers`
|
||||
|
||||
- Вход
|
||||
- `normalized_query: str`
|
||||
- `TargetTermsAnalysis`
|
||||
|
||||
- Выход
|
||||
`AnchorAnalysis`
|
||||
|
||||
### `routers/route_catalog.py`
|
||||
|
||||
- Задача
|
||||
Держать один источник истины для допустимых маршрутов.
|
||||
|
||||
- Как решает
|
||||
Возвращает:
|
||||
- список `allowed_routes` для payload LLM
|
||||
- проверку допустимости комбинации `routing_domain + intent + subintent`
|
||||
|
||||
### `routers/llm.py`
|
||||
|
||||
- Задача
|
||||
Выбрать маршрут через LLM как основной селектор.
|
||||
|
||||
- Как решает
|
||||
Формирует JSON payload из:
|
||||
- `normalized_query`
|
||||
- `target_terms`
|
||||
- `anchors`
|
||||
- `allowed_routes`
|
||||
|
||||
Затем:
|
||||
- вызывает LLM
|
||||
- парсит JSON
|
||||
- возвращает сырой candidate route без deterministic business-routing
|
||||
|
||||
- Вход
|
||||
- `normalized_query: str`
|
||||
- `target_terms: list[str]`
|
||||
- `anchors: dict`
|
||||
|
||||
- Выход
|
||||
`dict | None`
|
||||
|
||||
### `routers/validator.py`
|
||||
|
||||
- Задача
|
||||
Deterministic validation ответа LLM.
|
||||
|
||||
- Как решает
|
||||
Проверяет:
|
||||
- что `routing_domain`, `intent`, `subintent` заполнены
|
||||
- что комбинация маршрута входит в `route_catalog`
|
||||
- что `confidence` можно привести к числу
|
||||
|
||||
- Вход
|
||||
`dict | None`
|
||||
|
||||
- Выход
|
||||
Валидированный `dict | None`
|
||||
|
||||
### `routers/confidence.py`
|
||||
|
||||
- Задача
|
||||
Сделать confidence осмысленным после ответа LLM.
|
||||
|
||||
- Как решает
|
||||
Корректирует confidence:
|
||||
- `-0.1`, если нет strong anchors
|
||||
- `-0.1`, если запрос короткий или vague
|
||||
- `+0.05`, если есть явный signal (`file_markers`, `endpoint_paths`, `endpoint_markers`)
|
||||
- затем clamp в диапазон `0.0..1.0`
|
||||
|
||||
- Вход
|
||||
- `confidence: float`
|
||||
- `QueryFeatures`
|
||||
|
||||
- Выход
|
||||
`confidence: float`
|
||||
|
||||
### `routers/fallback.py`
|
||||
|
||||
- Задача
|
||||
Построить deterministic fallback, если LLM невалиден.
|
||||
|
||||
- Как решает
|
||||
Правила:
|
||||
- есть `file_markers` → `DOCS / DOC_EXPLAIN / FIND_FILES`
|
||||
- есть docs-signals (`endpoint_paths`, `target_doc_hints`, `matched_aliases`, marker groups) → `DOCS / DOC_EXPLAIN / SUMMARY`
|
||||
- иначе → `GENERAL / GENERAL_QA / SUMMARY`
|
||||
|
||||
- Вход
|
||||
- `user_query: str`
|
||||
- `QueryFeatures`
|
||||
- `anchors: V2RouteAnchors`
|
||||
- `llm_attempted: bool`
|
||||
|
||||
- Выход
|
||||
`V2RouteResult`
|
||||
|
||||
### `routers/prompts.yml`
|
||||
|
||||
- Задача
|
||||
Задать LLM-router контракт ответа и guidance по confidence.
|
||||
|
||||
- Как решает
|
||||
Ограничивает модель только `allowed_routes` и требует JSON с полями:
|
||||
- `routing_domain`
|
||||
- `intent`
|
||||
- `subintent`
|
||||
- `confidence`
|
||||
- `reason_short`
|
||||
|
||||
## 6. Ключевые инварианты
|
||||
|
||||
- LLM является default router.
|
||||
- Deterministic-слой не принимает основной routing decision.
|
||||
- `target_terms` содержат только retrieval-useful terms.
|
||||
- `anchors` не содержат `terms`.
|
||||
- `/health` и другие endpoint paths не должны попадать в `file_names`, если это не файл с расширением.
|
||||
- `file_names` содержат только реальные file/doc paths.
|
||||
- Fallback используется только если LLM недоступен или вернул невалидный маршрут.
|
||||
@@ -0,0 +1,316 @@
|
||||
# V2RetrievalPolicyResolver Architecture
|
||||
|
||||
## 1. Роль компонента
|
||||
|
||||
`V2RetrievalPolicyResolver` это deterministic bridge между `V2IntentRouter` и docs-RAG retrieval.
|
||||
|
||||
Компонент работает поверх уже готового `V2RouteResult` и не делает повторную интерпретацию пользовательского текста:
|
||||
|
||||
- не вызывает LLM;
|
||||
- не меняет `intent` и `subintent`;
|
||||
- не ранжирует документы;
|
||||
- не собирает evidence.
|
||||
|
||||
Его задача: собрать один `RetrievalPlan` с полями:
|
||||
|
||||
- `profile`
|
||||
- `layers`
|
||||
- `limit`
|
||||
- `filters`
|
||||
|
||||
## 2. Зависимости
|
||||
|
||||
Актуальная реализация опирается на:
|
||||
|
||||
- `src/app/core/agent/processes/v2/retrieval/policy_resolver.py`
|
||||
- `src/app/core/agent/processes/v2/anchor_signals.py`
|
||||
- `src/app/core/agent/processes/v2/models.py`
|
||||
- `src/app/core/rag/contracts/enums.py`
|
||||
- `src/app/core/agent/processes/v2/retrieval/v2_rag_adapter.py`
|
||||
- `src/app/core/rag/retrieval/session_retriever.py`
|
||||
- `src/app/core/rag/persistence/repository.py`
|
||||
- `src/app/core/rag/persistence/query_repository.py`
|
||||
- `src/app/core/rag/persistence/retrieval_statement_builder.py`
|
||||
|
||||
## 3. Входной контракт
|
||||
|
||||
Resolver использует:
|
||||
|
||||
- `route.intent`
|
||||
- `route.subintent`
|
||||
- `route.anchors.entity_names`
|
||||
- `route.anchors.file_names`
|
||||
- `route.anchors.endpoint_paths`
|
||||
- `route.anchors.target_doc_hints`
|
||||
- `route.anchors.matched_aliases`
|
||||
- `route.anchors.process_domain`
|
||||
- `route.anchors.process_subdomain`
|
||||
|
||||
`route.target_terms` в текущей реализации profile/filter branching не влияет.
|
||||
|
||||
## 4. Верхнеуровневый branching
|
||||
|
||||
`resolve(route)` имеет три ветки:
|
||||
|
||||
1. `GENERAL_QA` -> `general_qa_grounded_summary`
|
||||
2. `FIND_FILES` -> `file_lookup`
|
||||
3. иначе -> docs summary branch
|
||||
|
||||
Инварианты:
|
||||
|
||||
- `GENERAL_QA` всегда остаётся general profile;
|
||||
- `FIND_FILES` всегда остаётся `file_lookup`;
|
||||
- resolver всегда возвращает один валидный `RetrievalPlan`.
|
||||
|
||||
## 5. Внутренняя декомпозиция
|
||||
|
||||
Текущая реализация разбита на два helper-класса.
|
||||
|
||||
### `_AnchorTermCollector`
|
||||
|
||||
Собирает термы для `prefer_like_patterns`.
|
||||
|
||||
Источники:
|
||||
|
||||
- basename из `target_doc_hints`
|
||||
- `endpoint_paths`
|
||||
- `file_names`
|
||||
- `entity_names`
|
||||
- `matched_aliases`
|
||||
- `process_domain`
|
||||
- `process_subdomain`
|
||||
|
||||
Все значения нормализуются в lower-case и превращаются в SQL-like patterns вида `"%term%"`.
|
||||
|
||||
Для `FIND_FILES` действует отдельное правило:
|
||||
|
||||
- если есть `target_doc_hints`, `prefer_like_patterns` строится только по basename hints;
|
||||
- иначе используется общий набор collected terms.
|
||||
|
||||
### `_RouteFilterBuilder`
|
||||
|
||||
Собирает `filters` для трёх веток:
|
||||
|
||||
- `general_filters(route)`
|
||||
- `summary_filters(route)`
|
||||
- `find_files_filters(route)`
|
||||
|
||||
Дополнительно содержит path selection:
|
||||
|
||||
- `_summary_prefixes(route)`
|
||||
- `_find_files_prefixes(route)`
|
||||
- `_find_files_prefer_prefixes(route)`
|
||||
|
||||
## 6. Signal detection
|
||||
|
||||
Summary profile и часть path preferences зависят от `anchor_signal_types(route)`.
|
||||
|
||||
Сигналы вычисляются так:
|
||||
|
||||
- `FIND_FILES`
|
||||
- если `route.subintent == FIND_FILES`
|
||||
- `API_ENDPOINT`
|
||||
- если есть `endpoint_paths`
|
||||
- или в `target_doc_hints` / `file_names` / `matched_aliases` встречаются маркеры `"/api/"`, `"api"`, `"endpoint"`
|
||||
- `ARCHITECTURE`
|
||||
- если в `target_doc_hints` / `file_names` / `matched_aliases` встречаются `"/architecture/"`, `"architecture"`, `"arch"`
|
||||
- `LOGIC_FLOW`
|
||||
- если в `target_doc_hints` / `file_names` / `matched_aliases` встречаются `"/logic/"`, `"logic"`, `"workflow"`, `"flow"`, `"process"`
|
||||
- `DOMAIN_ENTITY`
|
||||
- если есть `entity_names`
|
||||
- или в `target_doc_hints` / `file_names` / `matched_aliases` встречаются `"/domains/"`, `"domain"`, `"entity"`, `"component"`
|
||||
|
||||
Важно:
|
||||
|
||||
- `process_domain` и `process_subdomain` сейчас **не участвуют** в signal detection;
|
||||
- они влияют только на filters и `prefer_like_patterns`.
|
||||
|
||||
## 7. Summary profile selection
|
||||
|
||||
Метод `_summary_profile(route)` использует:
|
||||
|
||||
- `meaningful = anchor_signal_types(route) - {FIND_FILES}`
|
||||
|
||||
Правило:
|
||||
|
||||
- если meaningful signal не ровно один -> `docs_summary_generic`
|
||||
- если ровно один:
|
||||
- `API_ENDPOINT` -> `docs_summary_api_endpoint`
|
||||
- `ARCHITECTURE` -> `docs_summary_architecture`
|
||||
- `LOGIC_FLOW` -> `docs_summary_logic_flow`
|
||||
- `DOMAIN_ENTITY` -> `docs_summary_domain_entity`
|
||||
|
||||
Следствие:
|
||||
|
||||
- конфликт API + architecture -> generic;
|
||||
- API + entity -> generic;
|
||||
- weak/no signals -> generic.
|
||||
|
||||
## 8. Profiles, layers, limits
|
||||
|
||||
### `general_qa_grounded_summary`
|
||||
|
||||
- condition: `route.intent == GENERAL_QA`
|
||||
- layers: `[D1_DOCUMENT_CATALOG, D0_DOC_CHUNKS]`
|
||||
- limit: `8`
|
||||
|
||||
### `file_lookup`
|
||||
|
||||
- condition: `route.subintent == FIND_FILES`
|
||||
- layers: `[D1_DOCUMENT_CATALOG, D3_ENTITY_CATALOG]`
|
||||
- limit: `12`
|
||||
|
||||
### `docs_summary_api_endpoint`
|
||||
|
||||
- layers: `[D1_DOCUMENT_CATALOG, D2_FACT_INDEX, D0_DOC_CHUNKS]`
|
||||
- limit: `8`
|
||||
|
||||
### `docs_summary_logic_flow`
|
||||
|
||||
- layers: `[D4_WORKFLOW_INDEX, D1_DOCUMENT_CATALOG, D0_DOC_CHUNKS]`
|
||||
- limit: `8`
|
||||
|
||||
### `docs_summary_domain_entity`
|
||||
|
||||
- layers: `[D3_ENTITY_CATALOG, D1_DOCUMENT_CATALOG, D0_DOC_CHUNKS]`
|
||||
- limit: `8`
|
||||
|
||||
### `docs_summary_architecture`
|
||||
|
||||
- layers: `[D1_DOCUMENT_CATALOG, D5_RELATION_GRAPH, D0_DOC_CHUNKS]`
|
||||
- limit: `8`
|
||||
|
||||
### `docs_summary_generic`
|
||||
|
||||
- layers: `[D1_DOCUMENT_CATALOG, D0_DOC_CHUNKS]`
|
||||
- limit: `8`
|
||||
|
||||
## 9. Filters by branch
|
||||
|
||||
### General branch
|
||||
|
||||
`general_filters(route)` возвращает:
|
||||
|
||||
- `prefer_path_prefixes = ["docs/architecture/", "docs/"]`
|
||||
- `prefer_like_patterns = ["%readme.md%", "%overview%"]`
|
||||
- `target_doc_hints = list(route.anchors.target_doc_hints)`
|
||||
|
||||
Это обзорный, но не узкий plan: hard `path_prefixes` здесь нет.
|
||||
|
||||
### Summary branch
|
||||
|
||||
`summary_filters(route)` всегда включает:
|
||||
|
||||
- `target_doc_hints`
|
||||
- `metadata.domain`, если есть `process_domain`
|
||||
- `metadata.subdomain`, если есть `process_subdomain`
|
||||
- `prefer_path_prefixes`
|
||||
- `prefer_like_patterns`
|
||||
|
||||
Дополнительно:
|
||||
|
||||
- если есть `API_ENDPOINT` signal, добавляется hard `path_prefixes = ["docs/api/", "docs/"]`
|
||||
|
||||
`prefer_path_prefixes` для summary:
|
||||
|
||||
- API -> `["docs/api/", "docs/"]`
|
||||
- ARCHITECTURE -> `["docs/architecture/", "docs/"]`
|
||||
- LOGIC_FLOW -> `["docs/logic/", "docs/architecture/", "docs/"]`
|
||||
- DOMAIN_ENTITY -> `["docs/domains/", "docs/", "docs/api/"]`
|
||||
- empty signals -> `["docs/"]`
|
||||
|
||||
Если сигналов несколько, prefixes объединяются и dedupe-ятся с сохранением порядка.
|
||||
|
||||
### FIND_FILES branch
|
||||
|
||||
`find_files_filters(route)` всегда включает:
|
||||
|
||||
- `target_doc_hints`
|
||||
- `metadata.domain`, если есть `process_domain`
|
||||
- `metadata.subdomain`, если есть `process_subdomain`
|
||||
- `path_prefixes`
|
||||
- `prefer_path_prefixes`
|
||||
- `prefer_like_patterns`
|
||||
|
||||
`path_prefixes` для `FIND_FILES` выбираются по приоритету:
|
||||
|
||||
1. директории из `target_doc_hints`
|
||||
2. директории из `file_names`, если путь начинается с `docs/`
|
||||
3. signal-based fallback:
|
||||
- API -> `["docs/api/", "docs/"]`
|
||||
- ARCHITECTURE -> `["docs/architecture/", "docs/"]`
|
||||
- LOGIC_FLOW -> `["docs/logic/", "docs/"]`
|
||||
- DOMAIN_ENTITY -> `["docs/domains/", "docs/"]`
|
||||
4. default -> `["docs/"]`
|
||||
|
||||
`prefer_path_prefixes` для `FIND_FILES`:
|
||||
|
||||
- начинается с `path_prefixes`
|
||||
- если есть `process_domain` или `process_subdomain`, дополнительно добавляет:
|
||||
- `"docs/domains/"`
|
||||
- `"docs/logic/"`
|
||||
|
||||
## 10. Hard и soft сигналы в текущей реализации
|
||||
|
||||
В терминах текущего кода:
|
||||
|
||||
Hard-ish / narrowing filters:
|
||||
|
||||
- `path_prefixes`
|
||||
- `metadata.domain`
|
||||
- `metadata.subdomain`
|
||||
|
||||
Soft preferences:
|
||||
|
||||
- `prefer_path_prefixes`
|
||||
- `prefer_like_patterns`
|
||||
|
||||
Отдельно:
|
||||
|
||||
- `target_doc_hints` всегда сохраняются в `RetrievalPlan.filters`, но **не маппятся напрямую** в `RagRepository.retrieve(...)` как SQL hard filter.
|
||||
|
||||
То есть сейчас `target_doc_hints` это не прямой DB filter, а downstream anchor для других шагов пайплайна и для deterministic exact-doc seeding logic.
|
||||
|
||||
## 11. Интеграция с retrieval stack
|
||||
|
||||
Следующий слой после resolver теперь исполняет plan не напрямую в `V2Process`, а через `V2RagRetrievalAdapter`.
|
||||
|
||||
`V2RagRetrievalAdapter.fetch_rows(...)` использует `RetrievalPlan` так:
|
||||
|
||||
- читает `filters["target_doc_hints"]` из самого плана;
|
||||
- делает exact-path seed через `retrieve_exact_files(...)`;
|
||||
- для missing hints делает substring fallback через `retrieve_chunks_by_path_substrings(...)`;
|
||||
- затем делает обычный semantic retrieve через `RagSessionRetriever.retrieve(...)`;
|
||||
- объединяет exact / substring / semantic rows через dedupe merge.
|
||||
|
||||
Это важный сдвиг: execution strategy теперь зависит от **контракта `RetrievalPlan`**, а не от скрытой route-specific логики внутри `V2Process`.
|
||||
|
||||
`RagSessionRetriever._map_filters()` прокидывает в `RagRepository.retrieve(...)`:
|
||||
|
||||
- `path_prefixes`
|
||||
- `exclude_path_prefixes`
|
||||
- `exclude_like_patterns`
|
||||
- `prefer_path_prefixes`
|
||||
- `prefer_like_patterns`
|
||||
- `prefer_non_tests`
|
||||
- `metadata_domain` из `filters["metadata.domain"]`
|
||||
- `metadata_subdomain` из `filters["metadata.subdomain"]`
|
||||
|
||||
`RetrievalStatementBuilder.build_retrieve(...)` добавляет SQL predicates:
|
||||
|
||||
- `lower(metadata_json->>'domain') = :metadata_domain`
|
||||
- `lower(metadata_json->>'subdomain') = :metadata_subdomain`
|
||||
|
||||
Таким образом:
|
||||
|
||||
- `process_domain/process_subdomain` реально участвуют в retrieval query;
|
||||
- `target_doc_hints` реально участвуют в retrieval execution strategy на уровне adapter;
|
||||
- `V2RetrievalPolicyResolver` определяет plan contract, а следующий шаг исполняет этот contract более буквально.
|
||||
|
||||
## 12. Актуальные ограничения
|
||||
|
||||
- Логика полностью deterministic.
|
||||
- `target_terms` сейчас не участвуют в branching resolver.
|
||||
- `process_domain/process_subdomain` не влияют на summary profile selection.
|
||||
- API signal добавляет `path_prefixes` даже в generic summary, если среди конфликтующих сигналов присутствует API.
|
||||
- `target_doc_hints` не являются прямым SQL filter внутри обычного `retrieve`, но используются adapter-уровнем для exact-path / substring seeding до semantic retrieval.
|
||||
@@ -0,0 +1,37 @@
|
||||
# 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`
|
||||
@@ -0,0 +1,28 @@
|
||||
# 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/логика источника данных)
|
||||
@@ -0,0 +1,17 @@
|
||||
# DB Columns Rules
|
||||
|
||||
## Формат
|
||||
Структура таблицы оформляется таблицей.
|
||||
|
||||
## Обязательные колонки
|
||||
- `Поле`
|
||||
- `Тип`
|
||||
- `Nullable`
|
||||
- `Описание`
|
||||
- `Источник заполнения`
|
||||
- `Использование`
|
||||
|
||||
## Правила
|
||||
- перечислять все ключевые поля таблицы;
|
||||
- для служебных полей (`id`, `created_at`, `updated_at`, `deleted_at`) явно описывать назначение;
|
||||
- если тип или nullable не заданы в аналитике, допускается инженерное предположение с рабочим вариантом.
|
||||
@@ -0,0 +1,16 @@
|
||||
# DB Constraints Rules
|
||||
|
||||
## Что включать
|
||||
- primary key;
|
||||
- unique constraints;
|
||||
- foreign keys;
|
||||
- важные индексы;
|
||||
- бизнес-ограничения на уровне БД.
|
||||
|
||||
## Формат
|
||||
- списком или таблицей;
|
||||
- для каждого индекса и ограничения писать, зачем оно нужно.
|
||||
|
||||
## Правила
|
||||
- если индекс нужен для сценария чтения/пагинации, это должно быть явно сказано;
|
||||
- если точные названия индексов неизвестны, можно использовать осмысленные проектные названия.
|
||||
@@ -0,0 +1,12 @@
|
||||
# DB Table Purpose Rules
|
||||
|
||||
## Что описывать
|
||||
- назначение таблицы;
|
||||
- в каком сценарии она используется;
|
||||
- кто является владельцем данных;
|
||||
- является ли таблица источником истины или производным хранилищем.
|
||||
|
||||
## Формат
|
||||
- 1-3 абзаца без воды;
|
||||
- явно указывать доменную сущность, которую хранит таблица;
|
||||
- если сделаны допущения по БД, фиксировать их отдельной фразой.
|
||||
@@ -0,0 +1,11 @@
|
||||
# DB Usage Rules
|
||||
|
||||
## Что описывать
|
||||
- какие API / logic block / batch job используют таблицу;
|
||||
- какие операции выполняются: read / insert / update / delete;
|
||||
- как таблица участвует в пользовательском сценарии.
|
||||
|
||||
## Правила
|
||||
- ссылки на связанные документы давать по `doc_id` или path;
|
||||
- не дублировать полный use case, а показывать роль таблицы в сценарии;
|
||||
- если таблица используется для пагинации, фильтрации или сортировки, это нужно отметить явно.
|
||||
@@ -0,0 +1,10 @@
|
||||
# Details Rules
|
||||
|
||||
## Назначение
|
||||
Этот файл задает общие правила для секции `## Details`.
|
||||
|
||||
## Правила
|
||||
- `Details` оформляется как `## Details`.
|
||||
- Внутри `Details` используются заголовки уровня `###` и ниже.
|
||||
- Структура `Details` определяется template типа документа.
|
||||
- В `Details` не нужно дублировать навигацию и связи, если они уже есть во frontmatter.
|
||||
@@ -0,0 +1,34 @@
|
||||
# Functional Requirements Rules
|
||||
|
||||
Этот rule описывает только тело секции `### Функциональные требования`.
|
||||
|
||||
## Формат
|
||||
- `FR.<номер>. <Название>`
|
||||
- Нумерация инкрементальная внутри документа.
|
||||
|
||||
## Правила
|
||||
- FR расширяют шаги сценария.
|
||||
- FR не копируют шаги сценария без добавления новой информации.
|
||||
- Для интеграционных шагов FR обязательны.
|
||||
- Если в сценарии есть вызов внешнего API / сервиса / БД, нужен отдельный FR на интеграцию.
|
||||
- Запрещено повторять заголовок `### Функциональные требования` внутри тела секции.
|
||||
|
||||
## FR для интеграционных шагов
|
||||
Для интеграционного FR обязательно раскрывать:
|
||||
- как формируется запрос;
|
||||
- откуда берется каждый значимый атрибут запроса;
|
||||
- какой downstream вызывается;
|
||||
- какой ответ считается успешным;
|
||||
- какие ответы и ситуации считаются бизнес-ошибкой;
|
||||
- какие ситуации считаются технической ошибкой;
|
||||
- как downstream-ответ маппится в контракт текущего слоя.
|
||||
|
||||
## FR для шагов доступа к БД
|
||||
Если шаг читает или пишет БД, FR должен по возможности включать:
|
||||
- таблицу или набор таблиц;
|
||||
- логику фильтрации;
|
||||
- логику сортировки;
|
||||
- логику пагинации;
|
||||
- пример SQL или близкий к рабочему псевдо-SQL.
|
||||
|
||||
Если СУБД и диалект не заданы, допускается сделать рабочее предположение и явно зафиксировать его.
|
||||
@@ -0,0 +1,20 @@
|
||||
# Non-Functional Requirements Rules
|
||||
|
||||
## Для api_method
|
||||
- Подразделы:
|
||||
- `#### Аудит` (если применимо)
|
||||
- `#### Мониторинг`
|
||||
|
||||
## Мониторинг
|
||||
Оформлять таблицей:
|
||||
- `Метрика`
|
||||
- `Описание`
|
||||
- `Условие срабатывания`
|
||||
|
||||
Запрещено:
|
||||
- использовать «точка измерения = метод» вместо условий срабатывания.
|
||||
|
||||
Базовые суффиксы метрик:
|
||||
- `_SUCCESS`
|
||||
- `_FAIL`
|
||||
- `_BUSINESS_ERROR`
|
||||
@@ -0,0 +1,15 @@
|
||||
# SQL Example Rules
|
||||
|
||||
## Назначение
|
||||
Секция показывает пример рабочего SQL для основного сценария использования таблицы.
|
||||
|
||||
## Правила
|
||||
- SQL должен быть близок к рабочему, а не абстрактным псевдокодом;
|
||||
- если диалект БД не указан, допускается выбрать наиболее вероятный вариант и явно зафиксировать допущение;
|
||||
- пример должен отражать реальный сценарий документа: чтение, вставка, обновление или агрегация;
|
||||
- для read-сценариев по возможности показывать фильтрацию, сортировку и пагинацию;
|
||||
- если есть join, нужно кратко пояснить, зачем он нужен.
|
||||
|
||||
## Формат
|
||||
- fenced code block с указанием `sql`;
|
||||
- под кодом 1-3 поясняющих bullets о ключевых условиях, индексах и параметрах.
|
||||
@@ -0,0 +1,10 @@
|
||||
# Summary Rules
|
||||
|
||||
## Назначение
|
||||
Этот файл задает правила для секции `## Summary`.
|
||||
|
||||
## Правила
|
||||
- `Summary` должен быть коротким слоем быстрого контекста.
|
||||
- `Summary` должен объяснять суть документа без длинных деталей.
|
||||
- Предпочтительный формат: краткий список ключевых фактов.
|
||||
- `Summary` не должен дублировать `Details`.
|
||||
@@ -0,0 +1,19 @@
|
||||
# Tech Use Case Rules
|
||||
|
||||
Этот rule описывает только тело секции `### Технический use case`.
|
||||
|
||||
## Обязательные части
|
||||
- название
|
||||
- предусловия
|
||||
- триггер
|
||||
- основной сценарий
|
||||
- альтернативный сценарий
|
||||
- обработка ошибок
|
||||
- постусловие
|
||||
|
||||
## Правила шага
|
||||
- Один шаг = одно предложение до 15-20 слов.
|
||||
- Формат шага: смысловое действие + техническая реализация (endpoint/топик/операция).
|
||||
- Длинные технические детали выносить в FR и ссылаться на FR из шага.
|
||||
- Для интеграционных шагов описание обработки ошибок обязательно.
|
||||
- Запрещено повторять заголовок `### Технический use case` внутри тела секции.
|
||||
@@ -0,0 +1,22 @@
|
||||
# UI Requirements Rules
|
||||
|
||||
## Структура блока
|
||||
- `### Требования к UI`
|
||||
- Внутри обязательно отдельные формы:
|
||||
- табличное представление
|
||||
- пустой список (empty state)
|
||||
- ошибка (error state)
|
||||
|
||||
## Обязательные правила
|
||||
- Если есть интеграция, обязательно описывать показ ошибки.
|
||||
- Если есть список, обязательно описывать показ отсутствия данных.
|
||||
|
||||
## Описание UI-элементов
|
||||
UI-элементы описываются строго в таблице.
|
||||
|
||||
Обязательные колонки (где применимо):
|
||||
- `Код элемента`
|
||||
- `Название и описание`
|
||||
- `Данные`
|
||||
- `Поведение`
|
||||
- `Валидация`
|
||||
@@ -0,0 +1,7 @@
|
||||
# User Analytics Rules
|
||||
|
||||
События пользовательской аналитики оформлять таблицей:
|
||||
- `Название события`
|
||||
- `Описание`
|
||||
- `Точка вызова`
|
||||
- `Payload`
|
||||
@@ -0,0 +1,56 @@
|
||||
# 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`
|
||||
@@ -0,0 +1,10 @@
|
||||
# Analytics to Documentation Mapping
|
||||
|
||||
## Принцип
|
||||
- Системная аналитика задает «что».
|
||||
- Документация детализирует «как».
|
||||
|
||||
## Маппинг
|
||||
- Из раздела архитектуры аналитики переносить контейнеры, интеграции и цепочки вызовов.
|
||||
- Из раздела изменений аналитики строить отдельные технические страницы (`ui_page`, `api_method`, `logic_block`).
|
||||
- Если в аналитике упрощенный use case, в документации раскрывать полный технический сценарий по правилам `tech-use-case.md`.
|
||||
@@ -0,0 +1,67 @@
|
||||
# Правила определения путей файлов
|
||||
|
||||
Текущая 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`.
|
||||
@@ -0,0 +1,37 @@
|
||||
# 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`
|
||||
@@ -0,0 +1,10 @@
|
||||
# Header Rules
|
||||
|
||||
## Правила
|
||||
- Заголовок описывает только смысл раздела.
|
||||
- Не включать в заголовок: `id`, `doc_type`, `application`, `platform`, `domain`, `sub_domain`.
|
||||
- Метаданные указываются отдельными строками ниже заголовка или во frontmatter.
|
||||
|
||||
## Пример
|
||||
- Правильно: `## 6.2 Метод UFS получения списка заказов`
|
||||
- Неправильно: `## 6.2 Блок api_method (id=..., platform=ufs)`
|
||||
@@ -0,0 +1,10 @@
|
||||
# Layer Responsibility
|
||||
|
||||
- `ui`: отображение, UX, запуск пользовательских сценариев.
|
||||
- `ufs`: авторизация/аутентификация, агрегация, маппинг, оркестрация вызовов.
|
||||
- `pprb`: API, БД, доменная логика backend.
|
||||
|
||||
## Правила согласованности
|
||||
- Проверка ролевой модели пользователя обычно фиксируется на уровне `ufs`.
|
||||
- Если проверка роли вынесена в `ufs`, в `pprb`-сценарии не дублировать этот шаг.
|
||||
- Аудит для `pprb` может отсутствовать, если это явно принято для домена/фичи.
|
||||
@@ -0,0 +1,34 @@
|
||||
---
|
||||
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`
|
||||
@@ -0,0 +1,38 @@
|
||||
---
|
||||
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`
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
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`
|
||||
@@ -0,0 +1,33 @@
|
||||
---
|
||||
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`
|
||||
@@ -37,11 +37,11 @@ tags:
|
||||
|
||||
- Scope: модуль индексации проектных файлов, хранения RAG-слоёв и выдачи retrieval-контекста.
|
||||
- Purpose: построить индекс по документации и Python-коду и дать runtime доступ к релевантным фрагментам.
|
||||
- Main modules: `RagModule`, `RagService`, `IndexingOrchestrator`, `RagRepository`, `RepoWebhookService`.
|
||||
- Main modules: `RagModule`, `RagService`, `IndexingOrchestrator`, `RagRepository`.
|
||||
- Main domains: RAG-сессии, задачи индексации, документы индекса, blob-cache, retrieval.
|
||||
- Main integrations: PostgreSQL/pgvector, GigaChat embeddings, FastAPI, EventBus, story context.
|
||||
- Key entrypoints: `/api/rag/sessions`, `/api/rag/sessions/{rag_session_id}/changes`, `/api/rag/sessions/{rag_session_id}/jobs/{index_job_id}`, `/internal/rag-repo/webhook`.
|
||||
- Key data flows: snapshot indexing, incremental reindex, retrieval из `rag_chunks`, webhook-нормализация коммитов.
|
||||
- Key entrypoints: `/api/rag/sessions`, `/api/rag/sessions/{rag_session_id}/changes`, `/api/rag/sessions/{rag_session_id}/jobs/{index_job_id}`, `/api/rag/sessions/{rag_session_id}/jobs/{index_job_id}/events`.
|
||||
- Key data flows: snapshot indexing, incremental reindex, retrieval из `rag_chunks`.
|
||||
- Source of truth: код `src/app/modules/rag/*`.
|
||||
|
||||
## Назначение
|
||||
@@ -50,7 +50,7 @@ tags:
|
||||
|
||||
## Контекст
|
||||
|
||||
Модуль используется как инфраструктурный слой для agent/runtime и смежных интеграций. На вход он принимает либо список файлов проекта, либо webhook репозитория. На выходе формирует устойчивый индекс, ассоциированный с `rag_session_id`, и статус задач индексации, пригодный для опроса и SSE-подписки.
|
||||
Модуль используется как инфраструктурный слой для agent/runtime. На вход он принимает snapshot и изменения файлов проекта. На выходе формирует устойчивый индекс, ассоциированный с `rag_session_id`, и статус задач индексации, пригодный для опроса и SSE-подписки.
|
||||
|
||||
## Границы системы
|
||||
|
||||
@@ -74,7 +74,7 @@ tags:
|
||||
|
||||
## Архитектурная схема
|
||||
|
||||
`RagModule` собирает зависимости модуля и публикует HTTP endpoints. Для индексации он использует `RagSessionStore`, `IndexJobStore`, `IndexingOrchestrator` и `RagService`. `RagService` выбирает docs/code pipeline, обогащает документы метаданными файла, запрашивает embeddings и записывает результат через `RagRepository`. `RagRepository` агрегирует schema/session/job/document/cache/query репозитории. Отдельно `RagRepoModule` принимает repository webhooks и прокидывает нормализованный commit context в story storage и cache writer.
|
||||
`RagModule` собирает зависимости модуля и публикует HTTP endpoints. Для индексации он использует `RagSessionStore`, `IndexJobStore`, `IndexingOrchestrator` и `RagService`. `RagService` выбирает docs/code pipeline, обогащает документы метаданными файла, запрашивает embeddings и записывает результат через `RagRepository`. `RagRepository` агрегирует schema/session/job/document/cache/query репозитории.
|
||||
|
||||
## Основные модули
|
||||
|
||||
@@ -87,7 +87,6 @@ tags:
|
||||
| `DocsIndexingPipeline` | построение слоёв документации `D1-D4` | classifier, chunker, document builder | `src/app/modules/rag/indexing/docs/pipeline.py` |
|
||||
| `CodeIndexingPipeline` | построение слоёв кода `C0-C4` | AST parser, symbol/edge/entrypoint/role builders | `src/app/modules/rag/indexing/code/pipeline.py` |
|
||||
| `RagRepository` | единая точка persistence и retrieval | schema/session/job/document/cache/query repositories | `src/app/modules/rag/persistence/repository.py` |
|
||||
| `RepoWebhookService` | нормализация webhook payload и выделение story id | story writer, cache writer | `src/app/modules/rag/webhook_service.py` |
|
||||
|
||||
|
||||
## Основные доменные области
|
||||
@@ -104,9 +103,8 @@ tags:
|
||||
| ------------------------ | --------- | --------------------------------------------------- | ---------------------------------- | -------------------------------------------------------------------------- |
|
||||
| PostgreSQL + pgvector | outbound | хранение документов, jobs, sessions и vector search | SQLAlchemy / SQL / pgvector | `logic-rag-retrieval` |
|
||||
| GigaChat embeddings | outbound | получение embedding для batch документов | HTTP client через `GigaChatClient` | `logic-rag-indexing` |
|
||||
| FastAPI | inbound | публичный и internal API модуля | HTTP | `api-rag-session-create`, `api-rag-session-changes`, `api-rag-session-job` |
|
||||
| FastAPI | inbound | публичный HTTP API модуля | HTTP | `api-rag-session-create`, `api-rag-session-changes`, `api-rag-session-job` |
|
||||
| EventBus | outbound | публикация прогресса индексации и terminal events | in-process async events / SSE | `api-rag-session-job` |
|
||||
| Story context repository | outbound | запись webhook-коммитов для story | Python interface | `logic-rag-indexing` |
|
||||
|
||||
|
||||
## Основные потоки
|
||||
@@ -138,7 +136,7 @@ tags:
|
||||
|
||||
- Code indexing поддерживает только Python-файлы.
|
||||
- Docs indexing ориентирован на markdown и frontmatter YAML.
|
||||
- Deprecated endpoint `/internal/rag/retrieve` не используется для рабочего retrieval.
|
||||
- HTTP retrieval endpoint в модуле не публикуется.
|
||||
- Реальное retrieval API доступно через repository/runtime adapters, а не через публичный HTTP endpoint модуля.
|
||||
|
||||
### Risks
|
||||
@@ -152,7 +150,6 @@ tags:
|
||||
### Security
|
||||
|
||||
- Публичные endpoints не содержат собственной бизнес-авторизации внутри модуля и полагаются на внешний слой приложения.
|
||||
- Webhook provider определяется по headers/payload без явной проверки подписи в самом `RepoWebhookService`.
|
||||
|
||||
### Reliability
|
||||
|
||||
@@ -164,7 +161,7 @@ tags:
|
||||
- Logs: `RagService` пишет предупреждения по cache hit/miss и skipped files.
|
||||
- Metrics: явные метрики не выделены.
|
||||
- Traces: явная трассировка не реализована.
|
||||
- Audit: job status и webhook commit binding сохраняются в БД.
|
||||
- Audit: job status сохраняется в БД.
|
||||
|
||||
### Performance
|
||||
|
||||
@@ -191,16 +188,13 @@ tags:
|
||||
- `src/app/modules/rag/indexing_service.py`
|
||||
- `src/app/modules/rag/persistence/repository.py`
|
||||
- `src/app/modules/rag/persistence/schema_repository.py`
|
||||
- `src/app/modules/rag/webhook_service.py`
|
||||
|
||||
### Symbols
|
||||
|
||||
- `RagModule`
|
||||
- `RagRepoModule`
|
||||
- `RagService`
|
||||
- `IndexingOrchestrator`
|
||||
- `RagRepository`
|
||||
- `RepoWebhookService`
|
||||
|
||||
## Связанные документы
|
||||
|
||||
@@ -218,5 +212,3 @@ tags:
|
||||
| Date | Source | Changes |
|
||||
| ---------- | ------ | ------------------------------------------------------------------- |
|
||||
| 2026-03-13 | code | Создан обзор архитектуры пакета `rag` на основе текущей реализации. |
|
||||
|
||||
|
||||
|
||||
@@ -125,8 +125,6 @@ tags:
|
||||
|
||||
- `POST /api/rag/sessions`
|
||||
- `POST /api/rag/sessions/{rag_session_id}/changes`
|
||||
- `POST /internal/rag/index/snapshot`
|
||||
- `POST /internal/rag/index/changes`
|
||||
|
||||
## Связанные сущности
|
||||
|
||||
|
||||
@@ -90,7 +90,7 @@ tags:
|
||||
|
||||
- Retrieval работает только внутри одной `rag_session_id` и не агрегирует несколько сессий.
|
||||
- Layer ranking зашит в код SQL-builder и требует явного обновления при появлении новых слоёв.
|
||||
- Полноценный HTTP retrieval endpoint в модуле отсутствует: `/internal/rag/retrieve` возвращает `410 deprecated`.
|
||||
- Полноценный HTTP retrieval endpoint в модуле не публикуется.
|
||||
|
||||
## Нефункциональные требования
|
||||
|
||||
@@ -115,7 +115,7 @@ tags:
|
||||
|
||||
- Runtime retrieval adapters в `src/app/modules/agent/runtime/steps/retrieval/adapter.py`
|
||||
- Explain retrieval gateway в `src/app/modules/agent/runtime/steps/explain/layered_gateway.py`
|
||||
- Deprecated endpoint `POST /internal/rag/retrieve`
|
||||
- HTTP retrieval endpoint отсутствует
|
||||
|
||||
## Связанные сущности
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
# DOCS Intent Router MVP
|
||||
|
||||
## Supported Intents
|
||||
|
||||
- `DOCS_QA.API_METHOD_EXPLAIN`
|
||||
- `DOCS_DISCOVERY.LIST_API_METHODS`
|
||||
- `DOCS_DISCOVERY.FIND_DOCUMENTS_BY_DOMAIN`
|
||||
- `DOCS_GENERATION.GENERATE_OPENAPI`
|
||||
- `DOCS_FALLBACK.GENERAL_DOCS_QA`
|
||||
|
||||
## Routing Flow
|
||||
|
||||
1. `Stage A`: deterministic pre-routing нормализует запрос, извлекает anchors и scope, считает rule-based confidence.
|
||||
2. `Stage B`: confidence gating пропускает high-confidence кейсы напрямую и эскалирует ambiguous/weak запросы в LLM.
|
||||
3. `Stage C`: LLM classifier выбирает только один из 5 MVP саб-интентов и возвращает строгий JSON.
|
||||
4. После выбора саб-интента router всегда прикрепляет декларативный `retrieval_plan`.
|
||||
|
||||
## Confidence And Escalation
|
||||
|
||||
- `>= 0.8` и без конфликтующих сигналов: `routing_mode=deterministic`.
|
||||
- Ниже порога, при пересечении интентов, слабых anchors или коротком неоднозначном запросе: `routing_mode=llm_assisted`.
|
||||
- Если LLM недоступен или вернул невалидный класс: `routing_mode=llm_fallback` c fallback в `GENERAL_DOCS_QA`.
|
||||
|
||||
## Retrieval Plan Mapping
|
||||
|
||||
- `API_METHOD_EXPLAIN` -> `docs_api_method_explain_v1`
|
||||
- `LIST_API_METHODS` -> `docs_list_api_methods_v1`
|
||||
- `FIND_DOCUMENTS_BY_DOMAIN` -> `docs_find_documents_by_domain_v1`
|
||||
- `GENERATE_OPENAPI` -> `docs_generate_openapi_v1`
|
||||
- `GENERAL_DOCS_QA` -> `docs_general_docs_qa_v1`
|
||||
|
||||
`retrieval_plan` хранится декларативно в `src/app/modules/agent/intent_router_v2/docs_mvp/retrieval_plans.py`, а legacy `retrieval_spec.filters` обогащается теми же anchors и scope для совместимости с текущим runtime.
|
||||
@@ -0,0 +1,105 @@
|
||||
`pipeline_setup_v3` это YAML-driven test harness для проверки agent pipeline на уровне сценариев, а не unit-тестов.
|
||||
|
||||
Как он работает:
|
||||
- Берёт один YAML-файл или директорию с YAML-кейсами.
|
||||
- Каждый кейс описывает:
|
||||
- `id`
|
||||
- `query`
|
||||
- `runner`
|
||||
- `mode`
|
||||
- `input`
|
||||
- `expected`
|
||||
- Если в `input` нет готового `rag_session_id`, harness сам получает его:
|
||||
- либо берёт из `input.rag_session_id`
|
||||
- либо индексирует `input.repo_path` в RAG и кеширует полученную сессию для одинакового `(repo_path, project_id)`
|
||||
|
||||
Какие режимы кейсов есть:
|
||||
- `router_only`
|
||||
Проверяется только роутинг, без retrieval и без LLM.
|
||||
- `router_rag`
|
||||
Проверяется роутинг плюс retrieval, но без полной генерации ответа.
|
||||
- `full_chain`
|
||||
Проверяется полный pipeline: router → retrieval → downstream pipeline/LLM → final answer.
|
||||
|
||||
Как устроен execution flow:
|
||||
1. Loader читает YAML и превращает каждый кейс в `V3Case`.
|
||||
2. Runner для каждого кейса резолвит `rag_session_id`.
|
||||
3. `AgentRuntimeAdapter` исполняет кейс в зависимости от `mode`.
|
||||
4. Возвращаются два объекта:
|
||||
- `actual`
|
||||
- `details`
|
||||
5. Validator сравнивает `actual/details` с `expected`.
|
||||
6. Writer сохраняет:
|
||||
- JSON с машинными результатами
|
||||
- Markdown с человекочитаемой диагностикой
|
||||
- итоговый `summary.md` по всему прогону
|
||||
|
||||
Что обычно лежит в `actual`:
|
||||
- `intent`
|
||||
- `sub_intent`
|
||||
- `graph_id`
|
||||
- `conversation_mode`
|
||||
- `rag_count`
|
||||
- `answer_mode`
|
||||
- `llm_answer`
|
||||
- `path_scope`
|
||||
- `doc_scope`
|
||||
- `entity_candidates`
|
||||
- `symbol_candidates`
|
||||
- `layers`
|
||||
- `filters`
|
||||
|
||||
Что лежит в `details`:
|
||||
- `router_result`
|
||||
- `retrieval_request`
|
||||
- `retrieval_result`
|
||||
- `rag_rows`
|
||||
- `diagnostics`
|
||||
- `llm_request`
|
||||
- `pipeline_steps`
|
||||
- иногда `validation`, `token_usage`, `runtime_trace`
|
||||
|
||||
Что умеют expectations:
|
||||
- `expected.router`
|
||||
Проверяет `intent`, `sub_intent`, `graph_id`, `conversation_mode`
|
||||
- `expected.retrieval`
|
||||
Проверяет:
|
||||
- пустой/непустой retrieval
|
||||
- минимум строк
|
||||
- наличие нужных слоёв
|
||||
- path/doc scope
|
||||
- symbol/entity candidates
|
||||
- фильтры
|
||||
- `expected.llm`
|
||||
Проверяет:
|
||||
- есть ли ответ
|
||||
- содержит ли ответ обязательные фразы
|
||||
- не содержит ли запрещённые фразы
|
||||
- `answer_mode`
|
||||
- `expected.pipeline`
|
||||
Проверяет в основном итоговый `answer_mode`
|
||||
|
||||
Что важно при формулировке нового test case для ChatGPT:
|
||||
- кейс должен описывать не “как реализовать код”, а “какой пользовательский сценарий проверяем”
|
||||
- у кейса должны быть:
|
||||
- понятный `query`
|
||||
- корректный `mode`
|
||||
- вход: `rag_session_id` или `repo_path`
|
||||
- минимально достаточные `expected`
|
||||
- не надо переописывать весь output, лучше проверять только ключевые инварианты
|
||||
|
||||
Хороший шаблон задания для ChatGPT:
|
||||
1. Укажи, для какого suite нужен кейс.
|
||||
2. Укажи `mode`: `router_only`, `router_rag` или `full_chain`.
|
||||
3. Дай пользовательский `query`.
|
||||
4. Опиши, что именно должно проверяться:
|
||||
- роутинг
|
||||
- retrieval layers/scope
|
||||
- answer mode
|
||||
- ключевые фразы в ответе
|
||||
5. Попроси вернуть YAML-фрагмент в формате `pipeline_setup_v3`.
|
||||
|
||||
Пример формулировки для ChatGPT:
|
||||
“Сформируй YAML test case для `pipeline_setup_v3` в режиме `full_chain`. Нужно проверить, что запрос `Объясни по документации как работает /health` маршрутизируется в docs-intent, retrieval использует docs layers, retrieval непустой, а ответ содержит `/health` и не содержит фраз про отсутствие данных.”
|
||||
|
||||
Если хочешь, я могу сразу подготовить тебе готовый prompt для ChatGPT, который будет генерировать новые кейсы в нужном формате.
|
||||
@@ -11,6 +11,7 @@ requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
"fastapi>=0.116",
|
||||
"uvicorn>=0.35",
|
||||
"python-dotenv>=1.0",
|
||||
"pydantic>=2.11",
|
||||
"langgraph>=0.6",
|
||||
"langgraph-checkpoint-postgres>=2.0",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
fastapi==0.116.1
|
||||
uvicorn==0.35.0
|
||||
python-dotenv==1.0.1
|
||||
pydantic==2.11.7
|
||||
langgraph==0.6.7
|
||||
langgraph-checkpoint-postgres==2.0.23
|
||||
|
||||
@@ -1,171 +0,0 @@
|
||||
# Request Trace: req_33758fd1ed834100a23fe95871b34181
|
||||
|
||||
- session_id: as_0bb449183cc242efaec50afd8193dcaf
|
||||
- active_rag_session_id: 292cad80-45ef-4edb-a23c-82f01732d295
|
||||
- process_version: v1
|
||||
- created_at: 2026-04-01T09:27:07.987130+00:00
|
||||
|
||||
## User Message
|
||||
Ты здесь?
|
||||
|
||||
## orchestrator
|
||||
```json
|
||||
{
|
||||
"event": "bootstrap",
|
||||
"status": "started",
|
||||
"process_version": "v1"
|
||||
}
|
||||
```
|
||||
|
||||
## client_event
|
||||
```json
|
||||
{
|
||||
"event": "status",
|
||||
"source": "orchestrator",
|
||||
"text": "Запрос принят и поставлен в обработку.",
|
||||
"payload": {},
|
||||
"created_at": "2026-04-01T09:27:07.987920+00:00"
|
||||
}
|
||||
```
|
||||
|
||||
## client_event
|
||||
```json
|
||||
{
|
||||
"event": "status",
|
||||
"source": "orchestrator",
|
||||
"text": "Запускаю процесс обработки v1.",
|
||||
"payload": {
|
||||
"process_version": "v1"
|
||||
},
|
||||
"created_at": "2026-04-01T09:27:07.988004+00:00"
|
||||
}
|
||||
```
|
||||
|
||||
## orchestrator
|
||||
```json
|
||||
{
|
||||
"event": "bootstrap",
|
||||
"status": "completed"
|
||||
}
|
||||
```
|
||||
|
||||
## client_event
|
||||
```json
|
||||
{
|
||||
"event": "status",
|
||||
"source": "task_workflow",
|
||||
"text": "Запускаю workflow simple_llm.",
|
||||
"payload": {},
|
||||
"created_at": "2026-04-01T09:27:07.988104+00:00"
|
||||
}
|
||||
```
|
||||
|
||||
## client_event
|
||||
```json
|
||||
{
|
||||
"event": "status",
|
||||
"source": "prompt_builder",
|
||||
"text": "Формирую prompt payload для LLM.",
|
||||
"payload": {},
|
||||
"created_at": "2026-04-01T09:27:07.988150+00:00"
|
||||
}
|
||||
```
|
||||
|
||||
## task_workflow
|
||||
```json
|
||||
{
|
||||
"event": "started",
|
||||
"workflow_id": "simple_llm"
|
||||
}
|
||||
```
|
||||
|
||||
## llm
|
||||
```json
|
||||
{
|
||||
"event": "request",
|
||||
"prompt_name": "simple_llm_answer",
|
||||
"system_prompt": "Ты полезный AI-ассистент проекта.\n\nНа вход приходит JSON с полем:\n- question\n\nПравила:\n- Отвечай как персонаж мемов из дагестана\n- Если вопрос неясный, аккуратно укажи, чего не хватает\n- Не выдумывай несуществующие факты о проекте\n- Формулируй ответ как обычное сообщение пользователю",
|
||||
"user_prompt": "{\n \"question\": \"Ты здесь?\"\n}",
|
||||
"log_context": "agent:req_33758fd1ed834100a23fe95871b34181"
|
||||
}
|
||||
```
|
||||
|
||||
## llm
|
||||
```json
|
||||
{
|
||||
"event": "response",
|
||||
"text": "Да тут я, на месте! А то в горах связи иногда нет, но ты лови ответ от меня, как пастух ловит сигнал телефона в ауле!"
|
||||
}
|
||||
```
|
||||
|
||||
## task_workflow
|
||||
```json
|
||||
{
|
||||
"event": "completed",
|
||||
"workflow_id": "simple_llm",
|
||||
"prompt_name": "simple_llm_answer",
|
||||
"answer_length": 117
|
||||
}
|
||||
```
|
||||
|
||||
## client_event
|
||||
```json
|
||||
{
|
||||
"event": "status",
|
||||
"source": "llm_process",
|
||||
"text": "Ответ от LLM получен.",
|
||||
"payload": {
|
||||
"workflow_id": "simple_llm",
|
||||
"prompt_name": "simple_llm_answer",
|
||||
"answer_length": 117
|
||||
},
|
||||
"created_at": "2026-04-01T09:27:08.991752+00:00"
|
||||
}
|
||||
```
|
||||
|
||||
## orchestrator
|
||||
```json
|
||||
{
|
||||
"event": "finalize",
|
||||
"status": "started"
|
||||
}
|
||||
```
|
||||
|
||||
## client_event
|
||||
```json
|
||||
{
|
||||
"event": "user",
|
||||
"source": "agent",
|
||||
"text": "Да тут я, на месте! А то в горах связи иногда нет, но ты лови ответ от меня, как пастух ловит сигнал телефона в ауле!",
|
||||
"payload": {},
|
||||
"created_at": "2026-04-01T09:27:08.992387+00:00"
|
||||
}
|
||||
```
|
||||
|
||||
## client_event
|
||||
```json
|
||||
{
|
||||
"event": "status",
|
||||
"source": "orchestrator",
|
||||
"text": "Обработка запроса завершена.",
|
||||
"payload": {},
|
||||
"created_at": "2026-04-01T09:27:08.992694+00:00"
|
||||
}
|
||||
```
|
||||
|
||||
## orchestrator
|
||||
```json
|
||||
{
|
||||
"event": "finalize",
|
||||
"status": "completed"
|
||||
}
|
||||
```
|
||||
|
||||
## result
|
||||
```json
|
||||
{
|
||||
"status": "done",
|
||||
"answer": "Да тут я, на месте! А то в горах связи иногда нет, но ты лови ответ от меня, как пастух ловит сигнал телефона в ауле!",
|
||||
"completed_at": "2026-04-01T09:27:08.994005+00:00"
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,9 @@
|
||||
__all__ = ["AgentRuntime"]
|
||||
|
||||
|
||||
def __getattr__(name: str):
|
||||
if name == "AgentRuntime":
|
||||
from app.core.agent.runtime import AgentRuntime
|
||||
|
||||
return AgentRuntime
|
||||
raise AttributeError(name)
|
||||
@@ -0,0 +1,22 @@
|
||||
__all__ = [
|
||||
"AgentProcess",
|
||||
"ProcessResult",
|
||||
"V1Process",
|
||||
"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)
|
||||
@@ -0,0 +1,26 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import field
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from app.schemas.changeset import ChangeItem
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from app.core.agent.runtime.execution_context import RuntimeExecutionContext
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class ProcessResult:
|
||||
answer: str = ""
|
||||
changeset: list[ChangeItem] = field(default_factory=list)
|
||||
apply_changeset: bool = False
|
||||
|
||||
|
||||
class AgentProcess(ABC):
|
||||
version = ""
|
||||
|
||||
@abstractmethod
|
||||
async def run(self, context: "RuntimeExecutionContext") -> ProcessResult:
|
||||
raise NotImplementedError
|
||||
@@ -0,0 +1,3 @@
|
||||
from app.core.agent.processes.v1.process import V1Process
|
||||
|
||||
__all__ = ["V1Process"]
|
||||
@@ -0,0 +1,22 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from app.core.agent.processes.base import AgentProcess, ProcessResult
|
||||
from app.core.agent.processes.v1.workflow import V1FlowMainGraph
|
||||
from app.core.agent.processes.v1.workflow.flow_main import V1FlowContext
|
||||
from app.core.agent.utils.llm import AgentLlmService
|
||||
|
||||
|
||||
class V1Process(AgentProcess):
|
||||
version = "v1"
|
||||
|
||||
def __init__(self, llm: AgentLlmService, prompt_name: str = "v1_flow_main.answer") -> None:
|
||||
self._prompt_name = prompt_name
|
||||
self._workflow = V1FlowMainGraph(llm)
|
||||
|
||||
async def run(self, context) -> ProcessResult:
|
||||
flow_context = V1FlowContext(
|
||||
runtime=context,
|
||||
prompt_name=self._prompt_name,
|
||||
)
|
||||
flow_context = await self._workflow.run(flow_context)
|
||||
return ProcessResult(answer=flow_context.answer)
|
||||
@@ -0,0 +1,3 @@
|
||||
from app.core.agent.processes.v1.workflow.flow_main.graph import V1FlowMainGraph
|
||||
|
||||
__all__ = ["V1FlowMainGraph"]
|
||||
@@ -0,0 +1,7 @@
|
||||
from app.core.agent.processes.v1.workflow.flow_main.context import V1FlowContext
|
||||
from app.core.agent.processes.v1.workflow.flow_main.graph import V1FlowMainGraph
|
||||
|
||||
__all__ = [
|
||||
"V1FlowContext",
|
||||
"V1FlowMainGraph",
|
||||
]
|
||||
@@ -0,0 +1,13 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from app.core.agent.runtime.execution_context import RuntimeExecutionContext
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class V1FlowContext:
|
||||
runtime: RuntimeExecutionContext
|
||||
prompt_name: str
|
||||
prepared_message: str = ""
|
||||
answer: str = ""
|
||||
@@ -0,0 +1,24 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from app.core.agent.processes.v1.workflow.flow_main.context import V1FlowContext
|
||||
from app.core.agent.processes.v1.workflow.flow_main.steps.finalize_answer_step import FinalizeAnswerStep
|
||||
from app.core.agent.processes.v1.workflow.flow_main.steps.generate_answer_step import GenerateAnswerStep
|
||||
from app.core.agent.processes.v1.workflow.flow_main.steps.prepare_user_message_step import PrepareUserMessageStep
|
||||
from app.core.agent.utils.llm import AgentLlmService
|
||||
from app.core.agent.utils.workflow import WorkflowGraph
|
||||
|
||||
|
||||
class V1FlowMainGraph:
|
||||
def __init__(self, llm: AgentLlmService) -> None:
|
||||
self._graph = WorkflowGraph(
|
||||
workflow_id="v1.flow_main",
|
||||
source="workflow.v1",
|
||||
steps=(
|
||||
PrepareUserMessageStep(),
|
||||
GenerateAnswerStep(llm),
|
||||
FinalizeAnswerStep(),
|
||||
),
|
||||
)
|
||||
|
||||
async def run(self, context: V1FlowContext) -> V1FlowContext:
|
||||
return await self._graph.run(context)
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace: v1_flow_main
|
||||
|
||||
prompts:
|
||||
answer: |
|
||||
Ты полезный ассистент.
|
||||
Ответь на сообщение пользователя по существу.
|
||||
Не придумывай факты, если данных недостаточно.
|
||||
Если пользователь пишет по-русски, отвечай по-русски.
|
||||
@@ -0,0 +1,9 @@
|
||||
from app.core.agent.processes.v1.workflow.flow_main.steps.finalize_answer_step import FinalizeAnswerStep
|
||||
from app.core.agent.processes.v1.workflow.flow_main.steps.generate_answer_step import GenerateAnswerStep
|
||||
from app.core.agent.processes.v1.workflow.flow_main.steps.prepare_user_message_step import PrepareUserMessageStep
|
||||
|
||||
__all__ = [
|
||||
"FinalizeAnswerStep",
|
||||
"GenerateAnswerStep",
|
||||
"PrepareUserMessageStep",
|
||||
]
|
||||
@@ -0,0 +1,19 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from app.core.agent.processes.v1.workflow.flow_main.context import V1FlowContext
|
||||
from app.core.agent.utils.workflow import WorkflowStep
|
||||
|
||||
|
||||
class FinalizeAnswerStep(WorkflowStep[V1FlowContext]):
|
||||
step_id = "finalize_answer"
|
||||
title = "Финализация ответа"
|
||||
|
||||
async def run(self, context: V1FlowContext) -> V1FlowContext:
|
||||
context.answer = context.answer.strip()
|
||||
return context
|
||||
|
||||
def trace_input(self, context: V1FlowContext) -> dict[str, object]:
|
||||
return {"answer_length_before_strip": len(context.answer)}
|
||||
|
||||
def trace_output(self, context: V1FlowContext) -> dict[str, object]:
|
||||
return {"answer_length": len(context.answer)}
|
||||
@@ -0,0 +1,32 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
from app.core.agent.processes.v1.workflow.flow_main.context import V1FlowContext
|
||||
from app.core.agent.utils.llm import AgentLlmService
|
||||
from app.core.agent.utils.workflow import WorkflowStep
|
||||
|
||||
|
||||
class GenerateAnswerStep(WorkflowStep[V1FlowContext]):
|
||||
step_id = "generate_answer"
|
||||
title = "Вызов LLM"
|
||||
|
||||
def __init__(self, llm: AgentLlmService) -> None:
|
||||
self._llm = llm
|
||||
|
||||
async def run(self, context: V1FlowContext) -> V1FlowContext:
|
||||
request_id = context.runtime.request.request_id
|
||||
context.answer = await asyncio.to_thread(
|
||||
self._llm.generate,
|
||||
context.prompt_name,
|
||||
context.prepared_message,
|
||||
log_context=f"agent:{request_id}",
|
||||
trace=context.runtime.trace.module("workflow.v1.llm"),
|
||||
)
|
||||
return context
|
||||
|
||||
def trace_input(self, context: V1FlowContext) -> dict[str, object]:
|
||||
return {"prompt_name": context.prompt_name, "prepared_message_length": len(context.prepared_message)}
|
||||
|
||||
def trace_output(self, context: V1FlowContext) -> dict[str, object]:
|
||||
return {"answer_length": len(context.answer)}
|
||||
@@ -0,0 +1,16 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from app.core.agent.processes.v1.workflow.flow_main.context import V1FlowContext
|
||||
from app.core.agent.utils.workflow import WorkflowStep
|
||||
|
||||
|
||||
class PrepareUserMessageStep(WorkflowStep[V1FlowContext]):
|
||||
step_id = "prepare_user_message"
|
||||
title = "Подготовка сообщения"
|
||||
|
||||
async def run(self, context: V1FlowContext) -> V1FlowContext:
|
||||
context.prepared_message = context.runtime.request.message.strip()
|
||||
return context
|
||||
|
||||
def trace_output(self, context: V1FlowContext) -> dict[str, object]:
|
||||
return {"prepared_message_length": len(context.prepared_message)}
|
||||
@@ -0,0 +1,13 @@
|
||||
__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)
|
||||
@@ -0,0 +1,54 @@
|
||||
# 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`
|
||||
@@ -0,0 +1,40 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,31 @@
|
||||
# Architecture Overview Rules
|
||||
|
||||
## Назначение
|
||||
|
||||
Этот файл задает правила для документов типа `architecture_overview`.
|
||||
|
||||
## Когда использовать
|
||||
|
||||
Использовать как входной документ для понимания системы, модуля или сервиса.
|
||||
|
||||
## Обязательная структура
|
||||
|
||||
Документ должен содержать:
|
||||
- YAML frontmatter
|
||||
- `# <title>`
|
||||
- `## Summary`
|
||||
- `## Details`
|
||||
|
||||
## Что описывать в Details
|
||||
|
||||
- границы системы
|
||||
- основные компоненты
|
||||
- ключевые взаимодействия
|
||||
- интеграционные сценарии
|
||||
- главные ограничения
|
||||
- ссылки на дочерние документы по API, logic, domain и другим артефактам
|
||||
|
||||
## Ошибки оформления
|
||||
|
||||
- Нельзя дублировать в архитектурном обзоре полные API-контракты.
|
||||
- Нельзя делать архитектурный обзор единственным документом на всю систему без декомпозиции.
|
||||
@@ -0,0 +1,30 @@
|
||||
# Domain Entity Rules
|
||||
|
||||
## Назначение
|
||||
|
||||
Этот файл задает правила для документов типа `domain_entity`.
|
||||
|
||||
## Когда использовать
|
||||
|
||||
Использовать для описания одной доменной сущности, ее смысла, состояния и роли в системе.
|
||||
|
||||
## Обязательная структура
|
||||
|
||||
Документ должен содержать:
|
||||
- YAML frontmatter
|
||||
- `# <title>`
|
||||
- `## Summary`
|
||||
- `## Details`
|
||||
|
||||
## Что описывать в Details
|
||||
|
||||
- смысл сущности
|
||||
- ключевые атрибуты
|
||||
- состояния или инварианты
|
||||
- использование сущности в системе
|
||||
- интеграции с API, workflow или внешними потребителями, если они важны для понимания модели
|
||||
|
||||
## Ошибки оформления
|
||||
|
||||
- Нельзя смешивать несколько независимых сущностей в одном документе.
|
||||
- Нельзя подменять доменную сущность описанием endpoint или workflow.
|
||||
@@ -0,0 +1,25 @@
|
||||
# Integration Doc Rules
|
||||
|
||||
## Назначение
|
||||
|
||||
Этот файл задает правила для документов типа `integration_doc`.
|
||||
|
||||
## Когда использовать
|
||||
|
||||
Использовать для описания интеграции между системами, сервисами или внешними провайдерами.
|
||||
|
||||
## Обязательная структура
|
||||
|
||||
Документ должен содержать:
|
||||
- YAML frontmatter
|
||||
- `# <title>`
|
||||
- `## Summary`
|
||||
- `## Details`
|
||||
|
||||
## Что описывать в Details
|
||||
|
||||
- цель интеграции
|
||||
- участвующие стороны
|
||||
- направление обмена
|
||||
- ключевой сценарий взаимодействия
|
||||
- ограничения и риски
|
||||
@@ -0,0 +1,31 @@
|
||||
# Logic Block Rules
|
||||
|
||||
## Назначение
|
||||
|
||||
Этот файл задает правила для документов типа `logic_block`.
|
||||
|
||||
## Когда использовать
|
||||
|
||||
Использовать для описания одного законченного блока логики, workflow или процесса.
|
||||
|
||||
## Обязательная структура
|
||||
|
||||
Документ должен содержать:
|
||||
- YAML frontmatter
|
||||
- `# <title>`
|
||||
- `## Summary`
|
||||
- `## Details`
|
||||
|
||||
## Что описывать в Details
|
||||
|
||||
- назначение логического блока
|
||||
- входы и выходы
|
||||
- последовательность выполнения
|
||||
- интеграции
|
||||
- ключевые ограничения
|
||||
- состояние и ошибки, если они важны для понимания блока
|
||||
|
||||
## Ошибки оформления
|
||||
|
||||
- Нельзя описывать весь модуль целиком, если логика распадается на несколько независимых блоков.
|
||||
- Нельзя превращать документ в пересказ исходного кода построчно.
|
||||
@@ -0,0 +1,24 @@
|
||||
# UI Page Rules
|
||||
|
||||
## Назначение
|
||||
|
||||
Этот файл задает правила для документов типа `ui_page`.
|
||||
|
||||
## Когда использовать
|
||||
|
||||
Использовать для описания одной пользовательской страницы, экрана или отдельного UI-сценария.
|
||||
|
||||
## Обязательная структура
|
||||
|
||||
Документ должен содержать:
|
||||
- YAML frontmatter
|
||||
- `# <title>`
|
||||
- `## Summary`
|
||||
- `## Details`
|
||||
|
||||
## Что описывать в Details
|
||||
|
||||
- назначение страницы
|
||||
- пользовательский сценарий
|
||||
- основные блоки интерфейса
|
||||
- связанные API и сущности
|
||||
@@ -0,0 +1,71 @@
|
||||
# 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-коды
|
||||
@@ -0,0 +1,38 @@
|
||||
# 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`
|
||||
@@ -0,0 +1,68 @@
|
||||
# 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
|
||||
```
|
||||
@@ -0,0 +1,33 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,24 @@
|
||||
# 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` допускаются пробелы и естественный язык.
|
||||
@@ -0,0 +1,19 @@
|
||||
# Writing Style
|
||||
|
||||
## Назначение
|
||||
|
||||
Этот файл задает правила стиля для текстового наполнения документации.
|
||||
|
||||
## Правила стиля
|
||||
|
||||
- Текст должен быть лаконичным.
|
||||
- Формулировки должны быть точными и техническими.
|
||||
- Summary должен быть кратким explain-слоем.
|
||||
- Details должен раскрывать суть без лишней воды.
|
||||
- Нежелательно смешивать несколько тем в одном документе.
|
||||
- Если детали относятся к другому артефакту, их нужно выносить в отдельный документ.
|
||||
|
||||
## Язык
|
||||
|
||||
- Основной язык документации — русский.
|
||||
- Технические термины, названия классов, API, RAG, OpenAPI, runtime и другие устоявшиеся identifiers можно оставлять на английском.
|
||||
@@ -0,0 +1,24 @@
|
||||
# API Contract Rules
|
||||
|
||||
## Назначение
|
||||
|
||||
Этот файл описывает, как оформлять подраздел `## Контракт` в API-документах.
|
||||
|
||||
## Что должно быть описано
|
||||
|
||||
- входные параметры
|
||||
- выходные параметры
|
||||
- JSON-структуры запросов и ответов
|
||||
- обязательность полей
|
||||
- типы полей
|
||||
- ограничения
|
||||
- описание назначения полей
|
||||
- примеры данных
|
||||
- auth
|
||||
- idempotency
|
||||
- timeout
|
||||
- ошибки и их HTTP-коды
|
||||
|
||||
## Правило качества
|
||||
|
||||
Контракт должен быть достаточно формальным, чтобы по нему можно было собрать OpenAPI-спецификацию.
|
||||
@@ -0,0 +1,13 @@
|
||||
# Details Section Rules
|
||||
|
||||
## Назначение
|
||||
|
||||
Этот файл задает общие правила для секции `## Details`.
|
||||
|
||||
## Правила
|
||||
|
||||
- `Details` оформляется как `## Details`.
|
||||
- Внутри `Details` используются заголовки уровня `###` и ниже.
|
||||
- Структура Details зависит от типа документа.
|
||||
- В Details не нужно повторно дублировать навигацию и связи, если они уже есть во frontmatter.
|
||||
- Интеграции, ошибки и кодовые привязки должны быть выделены в отдельные подразделы, если они существенны для понимания документа.
|
||||
@@ -0,0 +1,37 @@
|
||||
# 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 критерии успешного ответа >
|
||||
@@ -0,0 +1,16 @@
|
||||
# Requirements Format Rules
|
||||
|
||||
## Назначение
|
||||
|
||||
Этот файл задает формат для функциональных и нефункциональных требований.
|
||||
|
||||
## Функциональные требования
|
||||
|
||||
- Использовать коды `FR-1`, `FR-2`, `FR-3` и так далее.
|
||||
- Каждое требование должно описывать отдельный обязательный аспект поведения.
|
||||
- Идентификаторы локальны в пределах одного документа.
|
||||
|
||||
## Нефункциональные требования
|
||||
|
||||
- Использовать коды `NFR-1`, `NFR-2`, `NFR-3` и так далее.
|
||||
- Требования должны описывать характеристики качества, ограничения и эксплуатационные свойства.
|
||||
@@ -0,0 +1,13 @@
|
||||
# Summary Section Rules
|
||||
|
||||
## Назначение
|
||||
|
||||
Этот файл задает правила для секции `## Summary`.
|
||||
|
||||
## Правила
|
||||
|
||||
- Summary должен быть коротким explain-слоем быстрого контекста.
|
||||
- Summary должен объяснять суть документа без лишних деталей.
|
||||
- Summary должен быть пригоден для explain и быстрого чтения.
|
||||
- Предпочтительный формат: список ключевых фактов `Purpose`, `Actor`, `Trigger`, `Errors`, `Related ...` и т.д.
|
||||
- Для крупных документов допустим более длинный summary, если он остается структурированным.
|
||||
@@ -0,0 +1,66 @@
|
||||
# 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
|
||||
- Получение данных клиента из АС ЕПК
|
||||
- Проверка уровня доступа
|
||||
- Сценарий построения списка связанных предложений
|
||||
@@ -0,0 +1,85 @@
|
||||
---
|
||||
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.**
|
||||
|
||||
## Контракт
|
||||
|
||||
### Входные параметры
|
||||
|
||||
| Параметр | Где передается | Тип | Обязательность | Ограничения | Описание | Пример |
|
||||
|---|---|---|---|---|---|---|
|
||||
| | | | | | | |
|
||||
|
||||
### Выходные параметры
|
||||
|
||||
| Поле | Тип | Обязательность | Ограничения | Описание | Заполнение | Пример |
|
||||
|---|---|---|---|---|---|---|
|
||||
| | | | | | | |
|
||||
|
||||
### Интеграции
|
||||
|
||||
### Ошибки
|
||||
|
||||
### Связанный код
|
||||
|
||||
### История изменений
|
||||
@@ -0,0 +1,48 @@
|
||||
---
|
||||
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
|
||||
|
||||
### Описание
|
||||
|
||||
### Контекст
|
||||
|
||||
### Границы системы
|
||||
|
||||
### Компоненты
|
||||
|
||||
### Интеграционные сценарии
|
||||
|
||||
### Интеграции
|
||||
|
||||
### Ограничения
|
||||
|
||||
### Связанный код
|
||||
|
||||
### Связанные документы
|
||||
|
||||
### История изменений
|
||||
@@ -0,0 +1,48 @@
|
||||
---
|
||||
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
|
||||
|
||||
### Функциональные требования
|
||||
|
||||
### Нефункциональные требования
|
||||
|
||||
### Интеграции
|
||||
|
||||
### Связанный код
|
||||
|
||||
### Связанные документы
|
||||
|
||||
### История изменений
|
||||
@@ -0,0 +1,50 @@
|
||||
---
|
||||
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
|
||||
|
||||
### Связанный код
|
||||
|
||||
### История изменений
|
||||
@@ -0,0 +1,50 @@
|
||||
---
|
||||
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 и сущности
|
||||
|
||||
### Функциональные требования
|
||||
|
||||
### Нефункциональные требования
|
||||
|
||||
### Ограничения и граничные случаи
|
||||
|
||||
### Ошибки и валидации
|
||||
|
||||
### Связанный код
|
||||
|
||||
### Связанные документы
|
||||
|
||||
### История изменений
|
||||
@@ -0,0 +1,3 @@
|
||||
from app.core.agent.processes.v2.intent_router.router import V2IntentRouter
|
||||
|
||||
__all__ = ["V2IntentRouter"]
|
||||
@@ -0,0 +1,19 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class QueryFeatures:
|
||||
normalized_query: str
|
||||
target_terms: list[str]
|
||||
endpoint_paths: list[str]
|
||||
file_names: list[str]
|
||||
matched_aliases: list[str]
|
||||
target_doc_hints: list[str]
|
||||
file_markers: list[str]
|
||||
architecture_markers: list[str]
|
||||
logic_markers: list[str]
|
||||
domain_markers: list[str]
|
||||
endpoint_markers: list[str]
|
||||
scope_type: str = "unknown"
|
||||
@@ -0,0 +1,11 @@
|
||||
from app.core.agent.processes.v2.intent_router.modules.anchors import AnchorAnalysis, V2AnchorExtractor
|
||||
from app.core.agent.processes.v2.intent_router.modules.normalizer import V2QueryNormalizer
|
||||
from app.core.agent.processes.v2.intent_router.modules.target_terms import TargetTermsAnalysis, V2TargetTermsExtractor
|
||||
|
||||
__all__ = [
|
||||
"AnchorAnalysis",
|
||||
"TargetTermsAnalysis",
|
||||
"V2AnchorExtractor",
|
||||
"V2QueryNormalizer",
|
||||
"V2TargetTermsExtractor",
|
||||
]
|
||||
@@ -0,0 +1,247 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
|
||||
from app.core.agent.processes.v2.intent_router.modules.target_terms import TargetTermsAnalysis
|
||||
from app.core.agent.utils.process_v2.models import V2RouteAnchors
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class AnchorAnalysis:
|
||||
anchors: V2RouteAnchors
|
||||
file_markers: list[str]
|
||||
architecture_markers: list[str]
|
||||
logic_markers: list[str]
|
||||
domain_markers: list[str]
|
||||
endpoint_markers: list[str]
|
||||
|
||||
|
||||
class _MarkerScanner:
|
||||
_FILE_MARKERS = (
|
||||
"в каком файле",
|
||||
"в каком документе",
|
||||
"в каких файлах",
|
||||
"где находится",
|
||||
"где описан",
|
||||
"где описана",
|
||||
"где описаны",
|
||||
"покажи файл",
|
||||
"какие файлы",
|
||||
"найди файл",
|
||||
"найди файлы",
|
||||
"покажи документ",
|
||||
"где описано",
|
||||
"документ с описанием",
|
||||
)
|
||||
_ARCHITECTURE_MARKERS = (
|
||||
"архитектура",
|
||||
"архитектур",
|
||||
"architecture",
|
||||
"arch overview",
|
||||
"как устроено приложение",
|
||||
"как устроен сервис",
|
||||
"основные части системы",
|
||||
"из чего состоит",
|
||||
)
|
||||
_LOGIC_MARKERS = (
|
||||
"цикл",
|
||||
"loop",
|
||||
"flow",
|
||||
"workflow",
|
||||
"process",
|
||||
"worker",
|
||||
"как работает отправка уведомлений",
|
||||
"логика отправки",
|
||||
"background job",
|
||||
"runtime loop",
|
||||
)
|
||||
_DOMAIN_MARKERS = ("runtime health", "health model", "статусы здоровья", "сущность", "entity", "здоровье runtime")
|
||||
_ENDPOINT_MARKERS = (
|
||||
"endpoint",
|
||||
"api",
|
||||
"route",
|
||||
"method",
|
||||
"метод api",
|
||||
"метод",
|
||||
"метода",
|
||||
"ручка",
|
||||
"эндпоинт",
|
||||
"маршрут",
|
||||
"роут",
|
||||
)
|
||||
|
||||
def scan(self, lowered_query: str) -> dict[str, list[str]]:
|
||||
return {
|
||||
"file_markers": self._matching(lowered_query, self._FILE_MARKERS),
|
||||
"architecture_markers": self._matching(lowered_query, self._ARCHITECTURE_MARKERS),
|
||||
"logic_markers": self._matching(lowered_query, self._LOGIC_MARKERS),
|
||||
"domain_markers": self._matching(lowered_query, self._DOMAIN_MARKERS),
|
||||
"endpoint_markers": self._matching(lowered_query, self._ENDPOINT_MARKERS),
|
||||
}
|
||||
|
||||
def _matching(self, query: str, markers: tuple[str, ...]) -> list[str]:
|
||||
return [marker for marker in markers if marker in query]
|
||||
|
||||
|
||||
class _EntityNameExtractor:
|
||||
_ENTITY_RE = re.compile(r"\b[A-Z][A-Za-z0-9_]+\b")
|
||||
_IGNORE = {"arch"}
|
||||
|
||||
def extract(self, query: str) -> list[str]:
|
||||
items: list[str] = []
|
||||
for match in self._ENTITY_RE.finditer(query):
|
||||
candidate = match.group(0).strip()
|
||||
if candidate and candidate.lower() not in self._IGNORE and candidate not in items:
|
||||
items.append(candidate)
|
||||
return items
|
||||
|
||||
|
||||
class _FileNameExtractor:
|
||||
_TOKEN_RE = re.compile(r"`([^`]+)`|([A-Za-z0-9_./-]+)")
|
||||
_WITH_EXTENSION_RE = re.compile(r".+\.(md|yaml|yml|json)$", re.IGNORECASE)
|
||||
_DOC_PATH_RE = re.compile(r"^(docs|doc|documentation)/.+")
|
||||
|
||||
def extract(self, query: str) -> list[str]:
|
||||
items: list[str] = []
|
||||
for match in self._TOKEN_RE.finditer(query):
|
||||
candidate = next((item for item in match.groups() if item), "")
|
||||
normalized = str(candidate or "").strip().strip("`'\"")
|
||||
if self._is_file_name(normalized):
|
||||
self._append_unique(items, normalized.lower())
|
||||
return items
|
||||
|
||||
def _is_file_name(self, token: str) -> bool:
|
||||
if not token:
|
||||
return False
|
||||
if token.startswith("/") and "." not in token:
|
||||
return False
|
||||
if self._WITH_EXTENSION_RE.fullmatch(token):
|
||||
return True
|
||||
return self._DOC_PATH_RE.fullmatch(token) is not None
|
||||
|
||||
def _append_unique(self, items: list[str], value: str) -> None:
|
||||
if value and value not in items:
|
||||
items.append(value)
|
||||
|
||||
|
||||
class _ProcessAnchorExtractor:
|
||||
_DOMAIN_KEYWORDS = {
|
||||
"billing": "billing",
|
||||
"notifications": "notifications",
|
||||
}
|
||||
_SUBDOMAIN_KEYWORDS = {
|
||||
"invoice": ("billing", "invoice"),
|
||||
"invoices": ("billing", "invoice"),
|
||||
"delivery_loop": ("notifications", "delivery_loop"),
|
||||
"delivery": ("notifications", "delivery_loop"),
|
||||
}
|
||||
|
||||
def extract(self, lowered_query: str) -> tuple[str | None, str | None]:
|
||||
domain = next((value for token, value in self._DOMAIN_KEYWORDS.items() if token in lowered_query), None)
|
||||
subdomain: str | None = None
|
||||
for token, mapping in self._SUBDOMAIN_KEYWORDS.items():
|
||||
if token in lowered_query:
|
||||
domain = domain or mapping[0]
|
||||
subdomain = mapping[1]
|
||||
break
|
||||
return domain, subdomain
|
||||
|
||||
|
||||
class V2AnchorExtractor:
|
||||
def __init__(
|
||||
self,
|
||||
marker_scanner: _MarkerScanner | None = None,
|
||||
entity_extractor: _EntityNameExtractor | None = None,
|
||||
file_name_extractor: _FileNameExtractor | None = None,
|
||||
process_anchor_extractor: _ProcessAnchorExtractor | None = None,
|
||||
) -> None:
|
||||
self._marker_scanner = marker_scanner or _MarkerScanner()
|
||||
self._entity_extractor = entity_extractor or _EntityNameExtractor()
|
||||
self._file_name_extractor = file_name_extractor or _FileNameExtractor()
|
||||
self._process_anchor_extractor = process_anchor_extractor or _ProcessAnchorExtractor()
|
||||
|
||||
def extract(self, normalized_query: str, terms: TargetTermsAnalysis) -> AnchorAnalysis:
|
||||
lowered_query = normalized_query.lower()
|
||||
markers = self._marker_scanner.scan(lowered_query)
|
||||
process_domain, process_subdomain = self._process_anchor_extractor.extract(lowered_query)
|
||||
anchors = V2RouteAnchors(
|
||||
entity_names=self._entity_extractor.extract(normalized_query),
|
||||
file_names=self._file_name_extractor.extract(normalized_query),
|
||||
endpoint_paths=list(terms.endpoint_paths),
|
||||
target_doc_hints=self._target_doc_hints(
|
||||
endpoint_paths=terms.endpoint_paths,
|
||||
api_like_terms=terms.api_like_terms,
|
||||
alias_docs=terms.alias_docs,
|
||||
architecture_markers=markers["architecture_markers"],
|
||||
logic_markers=markers["logic_markers"],
|
||||
domain_markers=markers["domain_markers"],
|
||||
),
|
||||
matched_aliases=list(terms.matched_aliases),
|
||||
process_domain=process_domain,
|
||||
process_subdomain=process_subdomain,
|
||||
)
|
||||
return AnchorAnalysis(
|
||||
anchors=anchors,
|
||||
file_markers=markers["file_markers"],
|
||||
architecture_markers=markers["architecture_markers"],
|
||||
logic_markers=markers["logic_markers"],
|
||||
domain_markers=markers["domain_markers"],
|
||||
endpoint_markers=markers["endpoint_markers"],
|
||||
)
|
||||
|
||||
def _target_doc_hints(
|
||||
self,
|
||||
*,
|
||||
endpoint_paths: list[str],
|
||||
api_like_terms: list[str],
|
||||
alias_docs: list[str],
|
||||
architecture_markers: list[str],
|
||||
logic_markers: list[str],
|
||||
domain_markers: list[str],
|
||||
) -> list[str]:
|
||||
hints = list(alias_docs)
|
||||
endpoint_map = {
|
||||
"/health": "docs/api/health-endpoint.md",
|
||||
"/send": "docs/api/send-message-endpoint.md",
|
||||
"/actions/{action}": "docs/api/control-actions-endpoint.md",
|
||||
}
|
||||
for endpoint in endpoint_paths:
|
||||
for hint in self._endpoint_hint_variants(endpoint):
|
||||
self._append_unique(hints, hint)
|
||||
hint = endpoint_map.get(endpoint)
|
||||
self._append_unique(hints, hint)
|
||||
for term in api_like_terms:
|
||||
for hint in self._api_like_hint_variants(term):
|
||||
self._append_unique(hints, hint)
|
||||
if architecture_markers:
|
||||
self._append_unique(hints, "docs/architecture/telegram-notify-app-overview.md")
|
||||
if logic_markers:
|
||||
self._append_unique(hints, "docs/logic/telegram-notification-loop.md")
|
||||
if domain_markers:
|
||||
self._append_unique(hints, "docs/domains/runtime-health-entity.md")
|
||||
return hints
|
||||
|
||||
def _endpoint_hint_variants(self, endpoint: str) -> list[str]:
|
||||
normalized = str(endpoint or "").strip().lower()
|
||||
if not normalized:
|
||||
return []
|
||||
slug = normalized.strip("/").replace("/", "-").replace("{", "").replace("}", "")
|
||||
leaf = next((part for part in reversed(slug.split("-")) if part and part != "id"), "")
|
||||
hints: list[str] = [normalized]
|
||||
for value in (slug, leaf):
|
||||
if not value:
|
||||
continue
|
||||
hints.extend([value, f"{value}-endpoint", f"{value} endpoint"])
|
||||
return list(dict.fromkeys(hints))
|
||||
|
||||
def _api_like_hint_variants(self, term: str) -> list[str]:
|
||||
normalized = str(term or "").strip().lower().lstrip("/")
|
||||
if not normalized:
|
||||
return []
|
||||
return [normalized, f"/{normalized}", f"{normalized}-endpoint", f"{normalized} endpoint"]
|
||||
|
||||
def _append_unique(self, items: list[str], value: str | None) -> None:
|
||||
normalized = str(value or "").strip()
|
||||
if normalized and normalized not in items:
|
||||
items.append(normalized)
|
||||
@@ -0,0 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
class V2QueryNormalizer:
|
||||
def normalize(self, user_query: str) -> str:
|
||||
return " ".join(str(user_query or "").strip().split())
|
||||
@@ -0,0 +1,176 @@
|
||||
"""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 ""
|
||||
@@ -0,0 +1,443 @@
|
||||
"""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
|
||||
@@ -0,0 +1,363 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class TargetTermsAnalysis:
|
||||
target_terms: list[str]
|
||||
endpoint_paths: list[str]
|
||||
api_like_terms: list[str]
|
||||
matched_aliases: list[str]
|
||||
alias_docs: list[str]
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class _AliasRule:
|
||||
phrases: tuple[str, ...]
|
||||
canonical_term: str
|
||||
target_doc_hint: str
|
||||
|
||||
|
||||
class _AliasMatcher:
|
||||
_RULES = (
|
||||
_AliasRule(("ручная отправка сообщения", "отправка сообщения вручную"), "/send", "docs/api/send-message-endpoint.md"),
|
||||
_AliasRule(("статус сервиса", "проверка здоровья"), "/health", "docs/api/health-endpoint.md"),
|
||||
_AliasRule(("control actions", "управление runtime"), "/actions/{action}", "docs/api/control-actions-endpoint.md"),
|
||||
_AliasRule(("runtime health", "здоровье runtime", "статусы здоровья"), "runtime_health", "docs/domains/runtime-health-entity.md"),
|
||||
_AliasRule(("цикл отправки уведомлений", "notification loop", "worker loop"), "telegram-notify-loop", "docs/logic/telegram-notification-loop.md"),
|
||||
_AliasRule(("архитектура приложения",), "architecture_overview", "docs/architecture/telegram-notify-app-overview.md"),
|
||||
_AliasRule(("архитектура",), "architecture_overview", "docs/architecture/telegram-notify-app-overview.md"),
|
||||
_AliasRule(("каталог ошибок", "errors catalog"), "errors_catalog", "docs/errors/catalog.yaml"),
|
||||
_AliasRule(("файл-индекс документации", "docs index", "индекс документации"), "docs_index", "docs/README.md"),
|
||||
)
|
||||
|
||||
def match(self, lowered_query: str) -> tuple[list[str], list[str], list[str]]:
|
||||
terms: list[str] = []
|
||||
docs: list[str] = []
|
||||
aliases: list[str] = []
|
||||
for rule in self._RULES:
|
||||
if any(phrase in lowered_query for phrase in rule.phrases):
|
||||
self._append_unique(terms, rule.canonical_term.lower())
|
||||
self._append_unique(docs, rule.target_doc_hint)
|
||||
self._append_unique(aliases, rule.canonical_term.lower())
|
||||
return terms, docs, aliases
|
||||
|
||||
def _append_unique(self, items: list[str], value: str) -> None:
|
||||
if value and value not in items:
|
||||
items.append(value)
|
||||
|
||||
|
||||
class _EndpointPathExtractor:
|
||||
_PATH_RE = re.compile(r"`([^`]+)`|(/[A-Za-z0-9_./{}-]+)")
|
||||
_VALID_ENDPOINT_RE = re.compile(r"^/[a-z0-9._/-]+(?:/\{[a-z0-9_]+\})?$")
|
||||
_DOC_EXTENSIONS = (".md", ".yaml", ".yml", ".json")
|
||||
_FILESYSTEM_PREFIXES = (
|
||||
"/users/",
|
||||
"/home/",
|
||||
"/tmp/",
|
||||
"/var/",
|
||||
"/opt/",
|
||||
"/etc/",
|
||||
"/private/",
|
||||
"/mnt/",
|
||||
"/workspace/",
|
||||
"/workspaces/",
|
||||
)
|
||||
|
||||
def extract(self, query: str) -> list[str]:
|
||||
values: list[str] = []
|
||||
for match in self._PATH_RE.finditer(query):
|
||||
candidate = next((item for item in match.groups() if item and item.startswith("/")), "")
|
||||
normalized = self._normalize(candidate)
|
||||
if self._is_endpoint(normalized):
|
||||
self._append_unique(values, normalized)
|
||||
return values
|
||||
|
||||
def _normalize(self, token: str) -> str:
|
||||
trimmed = str(token or "").strip().strip("`'\"()[]!?.,:;")
|
||||
if "{" in trimmed and "}" not in trimmed:
|
||||
return ""
|
||||
return trimmed.lower()
|
||||
|
||||
def _is_endpoint(self, token: str) -> bool:
|
||||
if not token or not self._VALID_ENDPOINT_RE.fullmatch(token):
|
||||
return False
|
||||
if token.startswith(self._FILESYSTEM_PREFIXES):
|
||||
return False
|
||||
return not token.endswith(self._DOC_EXTENSIONS)
|
||||
|
||||
def _append_unique(self, items: list[str], value: str) -> None:
|
||||
if value and value not in items:
|
||||
items.append(value)
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class _ApiLikeAnchorAnalysis:
|
||||
endpoint_paths: list[str]
|
||||
candidate_terms: list[str]
|
||||
|
||||
|
||||
class _ApiLikeAnchorExtractor:
|
||||
_TOKEN_RE = re.compile(r"[A-Za-zА-Яа-я0-9_./{}-]+")
|
||||
_ASCII_ENDPOINT_RE = re.compile(r"^[a-z0-9]+(?:[-_][a-z0-9]+)*$")
|
||||
_API_MARKERS = {
|
||||
"api",
|
||||
"endpoint",
|
||||
"route",
|
||||
"method",
|
||||
"метод",
|
||||
"метода",
|
||||
"методу",
|
||||
"ручка",
|
||||
"ручки",
|
||||
"эндпоинт",
|
||||
"эндпоинта",
|
||||
"маршрут",
|
||||
"роут",
|
||||
}
|
||||
_EXPLAIN_MARKERS = {
|
||||
"как",
|
||||
"что",
|
||||
"делает",
|
||||
"работает",
|
||||
"объясни",
|
||||
"объяснить",
|
||||
"расскажи",
|
||||
"опиши",
|
||||
"смысл",
|
||||
}
|
||||
_NOISE_WORDS = _API_MARKERS | _EXPLAIN_MARKERS | {
|
||||
"про",
|
||||
"какой",
|
||||
"какая",
|
||||
"какие",
|
||||
"какого",
|
||||
"какую",
|
||||
"кратко",
|
||||
"нужен",
|
||||
"нужно",
|
||||
"у",
|
||||
}
|
||||
_SHORT_QUERY_TOKEN_LIMIT = 7
|
||||
|
||||
def extract(self, query: str, explicit_endpoint_paths: list[str]) -> _ApiLikeAnchorAnalysis:
|
||||
if explicit_endpoint_paths:
|
||||
return _ApiLikeAnchorAnalysis(endpoint_paths=list(explicit_endpoint_paths), candidate_terms=[])
|
||||
token_entries = self._token_entries(query)
|
||||
if not token_entries:
|
||||
return _ApiLikeAnchorAnalysis(endpoint_paths=[], candidate_terms=[])
|
||||
candidate_terms = [token for token, _start in token_entries if self._is_api_candidate(token)]
|
||||
if not candidate_terms:
|
||||
return _ApiLikeAnchorAnalysis(endpoint_paths=[], candidate_terms=[])
|
||||
if self._has_api_marker(token_entries):
|
||||
primary = self._primary_candidate(token_entries)
|
||||
endpoint_paths = [self._ensure_endpoint(primary)] if primary else []
|
||||
return _ApiLikeAnchorAnalysis(
|
||||
endpoint_paths=[path for path in endpoint_paths if path],
|
||||
candidate_terms=[primary] if primary else [],
|
||||
)
|
||||
if self._is_short_explain_query(token_entries) and len(candidate_terms) == 1:
|
||||
return _ApiLikeAnchorAnalysis(endpoint_paths=[], candidate_terms=list(candidate_terms))
|
||||
return _ApiLikeAnchorAnalysis(endpoint_paths=[], candidate_terms=[])
|
||||
|
||||
def _token_entries(self, query: str) -> list[tuple[str, int]]:
|
||||
entries: list[tuple[str, int]] = []
|
||||
for match in self._TOKEN_RE.finditer(query):
|
||||
token = str(match.group(0) or "").strip().strip("`'\"()[]!?.,:;").lower()
|
||||
if token:
|
||||
entries.append((token, match.start()))
|
||||
return entries
|
||||
|
||||
def _has_api_marker(self, token_entries: list[tuple[str, int]]) -> bool:
|
||||
return any(token in self._API_MARKERS for token, _start in token_entries)
|
||||
|
||||
def _is_short_explain_query(self, token_entries: list[tuple[str, int]]) -> bool:
|
||||
if len(token_entries) > self._SHORT_QUERY_TOKEN_LIMIT:
|
||||
return False
|
||||
return any(token in self._EXPLAIN_MARKERS for token, _start in token_entries)
|
||||
|
||||
def _primary_candidate(self, token_entries: list[tuple[str, int]]) -> str | None:
|
||||
marker_positions = [start for token, start in token_entries if token in self._API_MARKERS]
|
||||
candidates = [(token, start) for token, start in token_entries if self._is_api_candidate(token)]
|
||||
if not candidates:
|
||||
return None
|
||||
if not marker_positions:
|
||||
return candidates[-1][0]
|
||||
primary = min(
|
||||
candidates,
|
||||
key=lambda item: min(abs(item[1] - marker_pos) for marker_pos in marker_positions),
|
||||
)
|
||||
return primary[0]
|
||||
|
||||
def _is_api_candidate(self, token: str) -> bool:
|
||||
if (
|
||||
not token
|
||||
or token in self._NOISE_WORDS
|
||||
or token.startswith("docs/")
|
||||
or token.endswith((".md", ".yaml", ".yml", ".json"))
|
||||
):
|
||||
return False
|
||||
if token.startswith("/"):
|
||||
return True
|
||||
return self._ASCII_ENDPOINT_RE.fullmatch(token) is not None and len(token) >= 3
|
||||
|
||||
def _ensure_endpoint(self, token: str) -> str:
|
||||
return token if token.startswith("/") else f"/{token}"
|
||||
|
||||
|
||||
class _TermCollector:
|
||||
_TOKEN_RE = re.compile(r"[A-Za-zА-Яа-я0-9_./{}-]+")
|
||||
_IDENTIFIER_RE = re.compile(
|
||||
r"^(?:[a-z0-9]+(?:[_-][a-z0-9]+)+|[a-z]+[A-Z][A-Za-z0-9]+|(?:[A-Z][a-z0-9]+){2,})$"
|
||||
)
|
||||
_QUESTION_WORDS = {"что", "как", "где", "какой", "какие", "каком", "когда", "чего"}
|
||||
_INTENT_WORDS = {"объясни", "покажи", "найди", "расскажи", "дай", "опиши", "нужен", "show"}
|
||||
_FILLER_WORDS = {"про", "там", "тут", "плз", "pls", "for"}
|
||||
_MARKER_WORDS = {
|
||||
"файл",
|
||||
"файле",
|
||||
"file",
|
||||
"method",
|
||||
"метод",
|
||||
"метода",
|
||||
"методу",
|
||||
"route",
|
||||
"ручка",
|
||||
"ручки",
|
||||
"эндпоинт",
|
||||
"эндпоинта",
|
||||
"overview",
|
||||
"architecture",
|
||||
"arch",
|
||||
"flow",
|
||||
"process",
|
||||
"workflow",
|
||||
"док",
|
||||
"дока",
|
||||
"доках",
|
||||
"документ",
|
||||
"doc",
|
||||
"описан",
|
||||
"док-саммари",
|
||||
"summary",
|
||||
"саммари",
|
||||
}
|
||||
_SERVICE_WORDS = {
|
||||
"кратко",
|
||||
"краткий",
|
||||
"для",
|
||||
"есть",
|
||||
"делает",
|
||||
"работает",
|
||||
"это",
|
||||
"этой",
|
||||
"этого",
|
||||
"этот",
|
||||
"документы",
|
||||
"документация",
|
||||
"документации",
|
||||
"файлы",
|
||||
"путь",
|
||||
"пути",
|
||||
"service",
|
||||
"summary",
|
||||
"endpoint",
|
||||
"docs",
|
||||
}
|
||||
_MAX_TERMS = 7
|
||||
|
||||
def collect(self, query: str, alias_terms: list[str], endpoint_paths: list[str]) -> list[str]:
|
||||
explicit_terms: list[str] = []
|
||||
for value in endpoint_paths:
|
||||
self._append_unique(explicit_terms, value)
|
||||
for token in self._TOKEN_RE.findall(query):
|
||||
normalized = self._normalize(token)
|
||||
if not normalized:
|
||||
continue
|
||||
if self._is_endpoint(normalized) or self._is_identifier(normalized) or self._is_valid_term(normalized):
|
||||
self._append_unique(explicit_terms, normalized)
|
||||
alias_bucket = self._collect_alias_terms(alias_terms, explicit_terms)
|
||||
prioritized = self._prioritize(explicit_terms, alias_bucket)
|
||||
return prioritized[: self._MAX_TERMS]
|
||||
|
||||
def _normalize(self, token: str) -> str:
|
||||
trimmed = str(token or "").strip().strip("`'\"()[]!?.,:;")
|
||||
if "{" in trimmed and "}" not in trimmed:
|
||||
return ""
|
||||
return trimmed.lower()
|
||||
|
||||
def _is_endpoint(self, token: str) -> bool:
|
||||
return token.startswith("/") and len(token) > 1 and "{" not in token.replace("{", "", 1)
|
||||
|
||||
def _is_identifier(self, token: str) -> bool:
|
||||
return bool(self._IDENTIFIER_RE.fullmatch(token))
|
||||
|
||||
def _is_valid_term(self, token: str) -> bool:
|
||||
if len(token) < 3 or "/" in token or "." in token:
|
||||
return False
|
||||
if (
|
||||
token in self._QUESTION_WORDS
|
||||
or token in self._INTENT_WORDS
|
||||
or token in self._FILLER_WORDS
|
||||
or token in self._MARKER_WORDS
|
||||
or token in self._SERVICE_WORDS
|
||||
):
|
||||
return False
|
||||
return True
|
||||
|
||||
def _collect_alias_terms(self, alias_terms: list[str], explicit_terms: list[str]) -> list[str]:
|
||||
collected: list[str] = []
|
||||
explicit_set = set(explicit_terms)
|
||||
for term in alias_terms:
|
||||
normalized = self._normalize(term)
|
||||
if not normalized:
|
||||
continue
|
||||
if normalized in explicit_set:
|
||||
continue
|
||||
if self._is_identifier(normalized):
|
||||
parts = [part for part in re.split(r"[_-]", normalized) if part]
|
||||
if parts and all(part in explicit_set for part in parts):
|
||||
continue
|
||||
self._append_unique(collected, normalized)
|
||||
return collected
|
||||
|
||||
def _prioritize(self, explicit_terms: list[str], alias_terms: list[str]) -> list[str]:
|
||||
terms = explicit_terms + [term for term in alias_terms if term not in explicit_terms]
|
||||
endpoints = [term for term in terms if self._is_endpoint(term)]
|
||||
identifiers = [term for term in terms if term not in endpoints and self._is_identifier(term)]
|
||||
aliases = [term for term in alias_terms if term not in endpoints and term not in identifiers]
|
||||
other_terms = [term for term in terms if term not in endpoints and term not in identifiers and term not in aliases]
|
||||
return endpoints + identifiers + aliases + other_terms
|
||||
|
||||
def _append_unique(self, items: list[str], value: str) -> None:
|
||||
if value and value not in items:
|
||||
items.append(value)
|
||||
|
||||
|
||||
class V2TargetTermsExtractor:
|
||||
def __init__(
|
||||
self,
|
||||
alias_matcher: _AliasMatcher | None = None,
|
||||
endpoint_extractor: _EndpointPathExtractor | None = None,
|
||||
api_like_extractor: _ApiLikeAnchorExtractor | None = None,
|
||||
term_collector: _TermCollector | None = None,
|
||||
) -> None:
|
||||
self._alias_matcher = alias_matcher or _AliasMatcher()
|
||||
self._endpoint_extractor = endpoint_extractor or _EndpointPathExtractor()
|
||||
self._api_like_extractor = api_like_extractor or _ApiLikeAnchorExtractor()
|
||||
self._term_collector = term_collector or _TermCollector()
|
||||
|
||||
def extract(self, normalized_query: str) -> TargetTermsAnalysis:
|
||||
lowered = normalized_query.lower()
|
||||
endpoint_paths = self._endpoint_extractor.extract(normalized_query)
|
||||
api_like = self._api_like_extractor.extract(normalized_query, endpoint_paths)
|
||||
alias_terms, alias_docs, alias_hits = self._alias_matcher.match(lowered)
|
||||
return TargetTermsAnalysis(
|
||||
target_terms=self._term_collector.collect(normalized_query, alias_terms, api_like.endpoint_paths),
|
||||
endpoint_paths=api_like.endpoint_paths,
|
||||
api_like_terms=api_like.candidate_terms,
|
||||
matched_aliases=alias_hits,
|
||||
alias_docs=alias_docs,
|
||||
)
|
||||
@@ -0,0 +1,307 @@
|
||||
"""Маршрутизация запроса в домен/интент/subintent и якоря для v2."""
|
||||
|
||||
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.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.models import QueryFeatures
|
||||
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.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.validator import V2RouteValidator
|
||||
from app.core.agent.utils.process_v2.models import V2RouteResult, V2ScopeType, V2Subintent
|
||||
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:
|
||||
def __init__(
|
||||
self,
|
||||
normalizer: V2QueryNormalizer | None = None,
|
||||
target_terms_extractor: V2TargetTermsExtractor | None = None,
|
||||
anchor_extractor: V2AnchorExtractor | None = None,
|
||||
llm: AgentLlmService | None = None,
|
||||
enable_llm_disambiguation: bool = True,
|
||||
route_catalog: V2RouteCatalog | None = None,
|
||||
confidence_adjuster: V2ConfidenceAdjuster | None = None,
|
||||
scope_rows_provider: Callable[[str], list[dict]] | None = None,
|
||||
) -> None:
|
||||
self._normalizer = normalizer or V2QueryNormalizer()
|
||||
self._target_terms_extractor = target_terms_extractor or V2TargetTermsExtractor()
|
||||
self._anchor_extractor = anchor_extractor or V2AnchorExtractor()
|
||||
self._catalog = route_catalog or V2RouteCatalog()
|
||||
self._validator = V2RouteValidator(self._catalog)
|
||||
self._fallback_router = V2FallbackRouter()
|
||||
self._docs_subintent_resolver = DocsSubintentResolver()
|
||||
self._confidence_adjuster = confidence_adjuster or V2ConfidenceAdjuster()
|
||||
self._enable_llm_disambiguation = enable_llm_disambiguation
|
||||
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:
|
||||
normalized_query = self._normalizer.normalize(user_query)
|
||||
target_terms_analysis = self._target_terms_extractor.extract(normalized_query)
|
||||
sanitized_eps = plausible_doc_endpoint_paths(list(target_terms_analysis.endpoint_paths))
|
||||
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(
|
||||
normalized_query=normalized_query,
|
||||
target_terms=list(refined_terms.target_terms),
|
||||
endpoint_paths=list(refined_terms.endpoint_paths),
|
||||
file_names=list(anchor_analysis.anchors.file_names),
|
||||
matched_aliases=list(refined_terms.matched_aliases),
|
||||
target_doc_hints=list(anchor_analysis.anchors.target_doc_hints),
|
||||
file_markers=list(anchor_analysis.file_markers),
|
||||
architecture_markers=list(anchor_analysis.architecture_markers),
|
||||
logic_markers=list(anchor_analysis.logic_markers),
|
||||
domain_markers=list(anchor_analysis.domain_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_candidate = self._route_with_llm(
|
||||
features=features,
|
||||
anchors=anchor_analysis.anchors,
|
||||
)
|
||||
llm_result = self._validator.validate(llm_candidate)
|
||||
if llm_result is not None:
|
||||
confidence = self._confidence_adjuster.adjust(float(llm_result["confidence"]), features)
|
||||
return V2RouteResult(
|
||||
routing_domain=llm_result["routing_domain"],
|
||||
intent=llm_result["intent"],
|
||||
subintent=llm_result["subintent"],
|
||||
user_query=user_query,
|
||||
normalized_query=features.normalized_query,
|
||||
target_terms=features.target_terms,
|
||||
anchors=anchor_analysis.anchors,
|
||||
confidence=confidence,
|
||||
routing_mode="llm_default",
|
||||
llm_router_used=True,
|
||||
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(
|
||||
user_query=user_query,
|
||||
features=features,
|
||||
anchors=anchor_analysis.anchors,
|
||||
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:
|
||||
if not self._enable_llm_disambiguation or self._llm_router is None:
|
||||
return None
|
||||
try:
|
||||
return self._llm_router.classify(
|
||||
normalized_query=features.normalized_query,
|
||||
target_terms=features.target_terms,
|
||||
scope_type=features.scope_type,
|
||||
anchors={
|
||||
"entity_names": anchors.entity_names,
|
||||
"file_names": anchors.file_names,
|
||||
"endpoint_paths": anchors.endpoint_paths,
|
||||
"target_doc_hints": anchors.target_doc_hints,
|
||||
"matched_aliases": anchors.matched_aliases,
|
||||
"process_domain": anchors.process_domain,
|
||||
"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:
|
||||
return None
|
||||
@@ -0,0 +1,5 @@
|
||||
from app.core.agent.processes.v2.intent_router.routers.docs_subintent_resolver import DocsSubintentResolver
|
||||
from app.core.agent.processes.v2.intent_router.routers.deterministic import V2DeterministicRouter
|
||||
from app.core.agent.processes.v2.intent_router.routers.llm import V2LlmRouter
|
||||
|
||||
__all__ = ["DocsSubintentResolver", "V2DeterministicRouter", "V2LlmRouter"]
|
||||
@@ -0,0 +1,25 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from app.core.agent.processes.v2.intent_router.models import QueryFeatures
|
||||
|
||||
|
||||
class V2ConfidenceAdjuster:
|
||||
def adjust(self, confidence: float, features: QueryFeatures) -> float:
|
||||
adjusted = confidence
|
||||
if not self._has_strong_anchor(features):
|
||||
adjusted -= 0.1
|
||||
if self._is_short_or_vague(features):
|
||||
adjusted -= 0.1
|
||||
if self._has_explicit_signal(features):
|
||||
adjusted += 0.05
|
||||
return min(max(adjusted, 0.0), 1.0)
|
||||
|
||||
def _has_strong_anchor(self, features: QueryFeatures) -> bool:
|
||||
return any((features.file_markers, features.endpoint_paths, features.target_doc_hints, features.matched_aliases))
|
||||
|
||||
def _is_short_or_vague(self, features: QueryFeatures) -> bool:
|
||||
token_count = len([token for token in features.normalized_query.split() if token.strip()])
|
||||
return token_count <= 3 or len(features.target_terms) <= 1
|
||||
|
||||
def _has_explicit_signal(self, features: QueryFeatures) -> bool:
|
||||
return bool(features.file_markers or features.endpoint_paths or features.endpoint_markers)
|
||||
@@ -0,0 +1,73 @@
|
||||
from __future__ import annotations
|
||||
|
||||
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.intent_router.routers.docs_subintent_resolver import DocsSubintentResolver
|
||||
|
||||
|
||||
class V2DeterministicRouter:
|
||||
_GENERAL_MARKERS = (
|
||||
"что это за сервис",
|
||||
"для чего нужен",
|
||||
"какую задачу решает",
|
||||
"что входит в документацию",
|
||||
"какие документы стоит читать сначала",
|
||||
"дай короткое summary",
|
||||
"с чего начать",
|
||||
"что тут есть кроме api",
|
||||
"как в целом устроено приложение",
|
||||
"какие основные части есть",
|
||||
"из чего состоит telegram notify app",
|
||||
)
|
||||
|
||||
def __init__(self, subintent_resolver: DocsSubintentResolver | None = None) -> None:
|
||||
self._subintent_resolver = subintent_resolver or DocsSubintentResolver()
|
||||
|
||||
def route(self, user_query: str, features: QueryFeatures, anchors) -> V2RouteResult | None:
|
||||
subintent = self._subintent_resolver.resolve(features)
|
||||
if subintent == V2Subintent.FIND_FILES:
|
||||
return self._build_docs_route(user_query, features, anchors, subintent, "deterministic file anchor")
|
||||
if subintent is not None and not self._has_conflicting_doc_anchors(features):
|
||||
return self._build_docs_route(user_query, features, anchors, subintent, "deterministic signal")
|
||||
if self._is_general_summary(features.normalized_query):
|
||||
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=1.0,
|
||||
routing_mode="deterministic",
|
||||
llm_router_used=False,
|
||||
reason_short="general fallback signal",
|
||||
)
|
||||
return None
|
||||
|
||||
def _build_docs_route(self, user_query: str, features: QueryFeatures, anchors, subintent: str, reason: str) -> V2RouteResult:
|
||||
return V2RouteResult(
|
||||
routing_domain=V2Domain.DOCS,
|
||||
intent=V2Intent.DOC_EXPLAIN,
|
||||
subintent=subintent,
|
||||
user_query=user_query,
|
||||
normalized_query=features.normalized_query,
|
||||
target_terms=features.target_terms,
|
||||
anchors=anchors,
|
||||
confidence=1.0,
|
||||
routing_mode="deterministic",
|
||||
llm_router_used=False,
|
||||
reason_short=reason,
|
||||
)
|
||||
|
||||
def _is_general_summary(self, normalized_query: str) -> bool:
|
||||
query = normalized_query.lower()
|
||||
return any(marker in query for marker in self._GENERAL_MARKERS)
|
||||
|
||||
def _has_conflicting_doc_anchors(self, features: QueryFeatures) -> bool:
|
||||
signals = 0
|
||||
signals += 1 if features.endpoint_paths or features.endpoint_markers else 0
|
||||
signals += 1 if features.architecture_markers else 0
|
||||
signals += 1 if features.logic_markers else 0
|
||||
signals += 1 if features.domain_markers else 0
|
||||
return signals > 1
|
||||
@@ -0,0 +1,81 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from app.core.agent.processes.v2.intent_router.models import QueryFeatures
|
||||
from app.core.agent.utils.process_v2.models import V2Subintent
|
||||
|
||||
|
||||
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:
|
||||
if features.file_markers:
|
||||
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(
|
||||
(
|
||||
features.endpoint_paths,
|
||||
features.endpoint_markers,
|
||||
features.architecture_markers,
|
||||
features.logic_markers,
|
||||
features.domain_markers,
|
||||
features.target_doc_hints,
|
||||
)
|
||||
):
|
||||
return V2Subintent.SUMMARY
|
||||
return None
|
||||
|
||||
def _has_file_like_anchor(self, features: QueryFeatures) -> bool:
|
||||
return any(
|
||||
hint.endswith((".md", ".yaml", ".yml", ".json"))
|
||||
for hint in features.target_doc_hints
|
||||
) or any(token.endswith((".md", ".yaml", ".yml", ".json")) for token in features.file_names)
|
||||
|
||||
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
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user