Compare commits

5 Commits

1270 changed files with 24847 additions and 552727 deletions
+14
View File
@@ -0,0 +1,14 @@
# Analysis Assets
Этот каталог содержит служебные артефакты для аналитической и генеративной работы агента.
## Структура
- `rules/` — правила построения документации, frontmatter и шаблоны документов.
## Назначение
Каталог `.analysis/` отделен от `docs/`, чтобы:
- хранить служебные policy- и template-материалы вне пользовательской документации;
- передавать правила в LLM как отдельный policy-context;
- не смешивать документацию проекта и внутренние артефакты анализа.
+32
View File
@@ -0,0 +1,32 @@
# Documentation Rules
## Назначение
Этот файл фиксирует общие правила формирования, обновления и поддержки технической документации проекта.
Документация проекта должна создаваться как система атомарных, связанных между собой документов, пригодных:
- для чтения человеком;
- для сопровождения командой;
- для индексирования в RAG;
- для автоматического обновления агентом на основе кода и существующих артефактов.
Этот файл задает:
- общие принципы документационной архитектуры;
- правила декомпозиции документации;
- правила размещения файлов;
- требования к связям между документами;
- требования к качеству markdown-документов;
- правила генерации и обновления документации агентом.
Детальные шаблоны документов и правила frontmatter описываются отдельно:
- `.analysis/rules/frontmatter-rules.md`
- `.analysis/rules/templates/*.md`
---
## Область действия
Правила из этого файла применяются ко всей проектной документации, размещаемой в:
```text
docs/documentation/
+60
View File
@@ -0,0 +1,60 @@
# Frontmatter Rules
## Назначение
Этот файл фиксирует правила YAML frontmatter для документов в `docs/documentation/`.
Frontmatter обязателен для каждого markdown-документа и нужен для:
- идентификации документа;
- определения типа документа;
- фиксации связей с кодом и другими документами;
- выделения сущностей, тегов и домена;
- поддержки индексирования в RAG.
Общие правила построения документации описаны в:
- `.analysis/rules/documentation-rules.md`
Шаблоны markdown body описаны в:
- `.analysis/rules/templates/*.md`
---
## Общие правила
1. Frontmatter размещается в начале файла.
2. Формат — YAML между двумя строками `---`.
3. Все документы в `docs/documentation/` должны содержать frontmatter.
4. Поля должны быть стабильными и заполняться единообразно.
5. Не использовать произвольные поля без необходимости.
6. Если значение неизвестно и его нельзя уверенно вывести из evidence, поле лучше не заполнять, кроме обязательных полей.
7. Списковые поля должны оформляться как YAML-массивы.
8. Идентификаторы и ссылки должны быть стабильными и пригодными для машинной обработки.
---
## Базовый frontmatter
Каждый документ должен начинаться с frontmatter вида:
```yaml
---
id: api-orders-create
title: Метод создания заказа
doc_type: api_method
domain: orders
status: draft
owner: system-analyst
source_of_truth: code
related_docs:
- ui-order-create-page
- logic-order-validation
related_code:
- src/orders/api/create_order.py
entities:
- Order
- CreateOrder
tags:
- api
- orders
- create
---
@@ -0,0 +1,29 @@
# Rule: Use Document Templates From Fixed Paths
Агент должен создавать и обновлять техническую документацию только с опорой на шаблоны документов, расположенные в `.analysis/rules`.
Если агент формирует новый документ, он обязан:
- определить тип документа;
- выбрать соответствующий шаблон по фиксированному пути;
- сохранить структуру секций и базовых метаданных из шаблона;
- заполнять только те секции, которые подтверждены кодом и артефактами;
- не придумывать новые произвольные форматы, если для типа уже существует шаблон.
Пути к базовым шаблонам:
- `.analysis/rules/legacy/template_ui_page.md`
- `.analysis/rules/legacy/template_api_method.md`
- `.analysis/rules/legacy/template_logic_block.md`
Правило выбора шаблона:
- для документа типа `ui_page` использовать `.analysis/rules/legacy/template_ui_page.md`
- для документа типа `api_method` использовать `.analysis/rules/legacy/template_api_method.md`
- для документа типа `logic_block` использовать `.analysis/rules/legacy/template_logic_block.md`
Если для нужного типа шаблон отсутствует, агент должен:
1. использовать ближайший подходящий существующий шаблон как временную основу;
2. явно сохранить тип документа в `YAML frontmatter`;
3. не смешивать в одном документе несколько независимых сущностей.
@@ -0,0 +1,89 @@
# Template: api_method
```md
---
id: api-<stable-id>
title: <Human-readable title>
doc_type: api_method
status: draft
source_of_truth: code
domain: <domain-name>
owner: system-analyst
endpoint: <METHOD /path>
auth: <auth-mode-or-unknown>
idempotent: <true-or-false>
related_docs:
- <doc-id>
related_code:
- <path/to/file>
entities:
- <EntityName>
tags:
- api
---
# <API Method Title>
## Purpose
Кратко опиши, какую системную задачу решает метод.
## Endpoint Summary
- Endpoint: `<METHOD /path>`
- Auth: `<auth-mode>`
- Idempotent: `<true/false>`
- Triggered by: `<ui/system/integration if known>`
## Technical Use Case
Опиши пошагово обработку запроса:
- вход в endpoint;
- ключевые проверки;
- вызовы логики;
- обращения к БД и внешним системам;
- формирование ответа.
## Functional Requirements
Вынеси сюда подтвержденные правила, которые дополняют основной сценарий:
- валидации;
- branching logic;
- побочные эффекты;
- ограничения по данным;
- условия ошибок.
## Request and Response Contract
Опиши контракт в кратком виде или дай ссылку на OpenAPI / контрактный файл.
## Related Logic Blocks
- [<Logic block title>](<path-or-doc-link>)
## Data Access and Integrations
- Reads DB: `<if known>`
- Writes DB: `<if known>`
- Integrates with: `<if known>`
## Non-Functional Requirements
Укажи только подтвержденные НФТ:
- timeout;
- audit;
- monitoring;
- security;
- idempotency rules.
## Related Code
- `<path/to/file>`
## Related Documents
- [<Related document>](<path-or-doc-link>)
```
@@ -0,0 +1,71 @@
# Template: logic_block
```md
---
id: logic-<stable-id>
title: <Human-readable title>
doc_type: logic_block
status: draft
source_of_truth: code
domain: <domain-name>
owner: system-analyst
related_docs:
- <doc-id>
related_code:
- <path/to/file>
entities:
- <EntityName>
tags:
- logic
---
# <Logic Block Title>
## Purpose
Кратко опиши, какую переиспользуемую или устойчивую логику реализует блок.
## Where Used
- Called from: `<ui/api/jobs/services if known>`
- Used by: `<list of known callers>`
## Technical Use Case
Опиши пошагово, как работает логический блок:
- входные данные;
- ключевые проверки;
- преобразования;
- обращения к данным;
- результат работы.
## Functional Requirements
Вынеси сюда устойчивые правила и ограничения:
- бизнес-правила;
- проверки;
- ветвления;
- ограничения на вход и выход;
- условия отказа.
## Dependencies
- Uses logic: `<other logic blocks if known>`
- Reads DB: `<if known>`
- Writes DB: `<if known>`
- Integrates with: `<if known>`
## Error Cases
Опиши значимые ошибки и условия их возникновения, если они подтверждены кодом.
## Related Code
- `<path/to/file>`
## Related Documents
- [<Related document>](<path-or-doc-link>)
```
@@ -0,0 +1,82 @@
# Template: ui_page
```md
---
id: ui-<stable-id>
title: <Human-readable title>
doc_type: ui_page
status: draft
source_of_truth: code
domain: <domain-name>
owner: system-analyst
related_docs:
- <doc-id>
related_code:
- <path/to/file>
entities:
- <EntityName>
tags:
- ui
---
# <Page Title>
## Purpose
Кратко опиши, какую пользовательскую задачу решает страница.
## Route and Entry Points
- Route: `<route-if-known>`
- Entry points: `<where user comes from>`
## Technical Use Case
Опиши пошаговый сценарий работы страницы как поток действий и системных реакций.
## UI Structure
Перечисли основные UI-элементы и для каждого укажи:
- назначение;
- источник данных;
- значение по умолчанию или placeholder;
- условия доступности или активации;
- поведение при взаимодействии;
- правила валидации.
## Functional Requirements
Вынеси сюда детальные правила, которые не стоит перегружать в use case:
- вызовы API;
- обработку ответов;
- локальные правила отображения;
- условия переходов;
- feature toggles.
## Related APIs
- [<API document title>](<path-or-doc-link>)
## Related Logic Blocks
- [<Logic block title>](<path-or-doc-link>)
## Non-Functional Requirements
Укажи НФТ, если они подтверждены:
- analytics events;
- observability;
- feature toggles;
- security constraints.
## Related Code
- `<path/to/file>`
## Related Documents
- [<Related document>](<path-or-doc-link>)
```
+115
View File
@@ -0,0 +1,115 @@
# {{title}}
## Summary
- Purpose:
- Actor:
- Trigger:
- Endpoint:
- Main entities:
- Main logic:
- Main errors:
- Source of truth:
## Назначение
## Контекст
## Технический use case
### Основной сценарий
1.
2.
3.
### Альтернативные ветки
-
-
## Функциональные требования
### Request validation
-
### Processing rules
-
### State changes
-
### Side effects
-
## Contract
### Endpoint
- Method:
- Path:
- Auth:
- Idempotent:
- Timeout:
- Retry:
### Request
| Field | Type | Required | Constraints | Description |
|------|------|----------|-------------|-------------|
| | | | | |
### Response
| Field | Type | Description |
|------|------|-------------|
| | | |
### External contract refs
- OpenAPI:
- Schema:
- DTO / serializer:
- Additional refs:
## Errors
| error_id | http_code | when | client_behavior | retry |
|----------|-----------|------|-----------------|-------|
| | | | | |
## Нефункциональные требования
### Security
-
### Observability
- Logs:
- Metrics:
- Traces:
- Audit:
### Reliability
-
-
### Performance
-
## Связанные блоки логики
-
## Связанные сущности
-
## Связанный код
### Files
-
### Symbols
-
## Связанные документы
-
## История изменений
| Date | Source | Changes |
|------|--------|---------|
| | | |
@@ -0,0 +1,105 @@
# {{title}}
## Summary
- Scope:
- Purpose:
- Main modules:
- Main domains:
- Main integrations:
- Key entrypoints:
- Key data flows:
- Source of truth:
## Назначение
## Контекст
## Границы системы
### In scope
-
### Out of scope
-
## Архитектурная схема
## Основные модули
| module | responsibility | depends_on | key_code_refs |
|--------|----------------|------------|---------------|
| | | | |
## Основные доменные области
-
-
## Основные интеграции
| integration | direction | purpose | protocol / transport | related_docs |
|-------------|-----------|---------|----------------------|--------------|
| | | | | |
## Основные потоки
### Flow 1
1.
2.
3.
### Flow 2
1.
2.
3.
## Архитектурные решения и ограничения
### Key decisions
-
### Constraints
-
### Risks
-
## Нефункциональные аспекты
### Security
-
### Reliability
-
### Observability
- Logs:
- Metrics:
- Traces:
- Audit:
### Performance
-
### Scalability
-
## Связанные сущности
-
## Связанный код
### Files
-
### Symbols
-
## Связанные документы
-
## История изменений
| Date | Source | Changes |
|------|--------|---------|
| | | |
@@ -0,0 +1,92 @@
# {{title}}
## Summary
- Domain:
- Purpose:
- Entity role:
- Main attributes:
- Lifecycle:
- Invariants:
- Related APIs:
- Related logic:
- Source of truth:
## Назначение
## Контекст
## Роль в доменной модели
## Атрибуты
| attribute | type | required | description | constraints |
|-----------|------|----------|-------------|-------------|
| | | | | |
## Состояния и жизненный цикл
### Основные состояния
-
### Переходы состояний
1.
2.
3.
## Инварианты и ограничения
-
-
## Связи с другими сущностями
| entity | relation | description |
|--------|----------|-------------|
| | | |
## Использование в системе
### Related API
-
### Related UI
-
### Related logic
-
### Related integrations
-
## Функциональные требования
-
-
## Нефункциональные требования
### Audit / history
-
### Security
-
### Observability
-
## Связанный код
### Files
-
### Symbols
-
## Связанные документы
-
## История изменений
| Date | Source | Changes |
|------|--------|---------|
| | | |
+93
View File
@@ -0,0 +1,93 @@
```md
# {{title}}
## Summary
- Purpose:
- Trigger:
- Inputs:
- Outputs:
- Main entities:
- Main dependencies:
- Side effects:
- Source of truth:
## Назначение
## Контекст
## Технический use case
### Основной сценарий
1.
2.
3.
### Альтернативные ветки
-
-
## Функциональные требования
### Preconditions
-
### Processing rules
-
### Validation rules
-
### Output / result rules
-
### Side effects
-
## Ограничения и условия вызова
-
-
## Нефункциональные требования
### Security
-
### Observability
- Logs:
- Metrics:
- Traces:
- Audit:
### Reliability
-
-
### Performance
-
## Связанные API / UI / integration points
-
## Связанные сущности
-
## Связанный код
### Files
-
### Symbols
-
## Связанные документы
-
## История изменений
| Date | Source | Changes |
|------|--------|---------|
| | | |
```
+97
View File
@@ -0,0 +1,97 @@
# {{title}}
## Summary
- Purpose:
- Actor:
- Trigger:
- Route:
- Main API:
- Main entities:
- Main logic:
- Main states:
- Source of truth:
## Назначение
## Контекст
## Технический use case
### Основной сценарий
1.
2.
3.
### Альтернативные ветки
-
-
## Описание UI
## UI Elements
| id | type | label | data_source | default / placeholder | validation | behavior |
|----|------|-------|-------------|------------------------|------------|----------|
| | | | | | | |
## Функциональные требования
### Input rules
-
### State rules
-
### Navigation rules
-
### Client-side validation
-
## Нефункциональные требования
### Security
-
### Observability
- Logs:
- Metrics:
- Traces:
- Analytics:
### Accessibility
-
### Performance
-
### Feature toggles
-
## Связанные API
-
## Связанные блоки логики
-
## Связанные сущности
-
## Связанный код
### Files
-
### Symbols
-
## Связанные документы
-
## История изменений
| Date | Source | Changes |
|------|--------|---------|
| | | |
+15
View File
@@ -0,0 +1,15 @@
---
alwaysApply: true
---
При задачах на создание или обновление документации всегда:
1. Читай .analysis/rules/documentation-rules.md, .analysis/rules/frontmatter-rules.md и нужный шаблон из .analysis/rules/templates/.
2. Создавай и обновляй документы только в docs/documentation/.
3. Не создавай дублей: сначала ищи существующий документ, потом обновляй его.
4. Соблюдай принцип: один документ = одна сущность / один устойчивый аспект.
5. Каждый документ должен иметь YAML frontmatter, обязательные разделы Summary и Details и структуру по шаблону.
6. Все связи фиксируй явно: related_docs, related_code, entities, tags и typed-поля.
7. Используй только подтвержденный evidence из кода, контрактов, конфигов и существующей документации.
8. Не дублируй содержание между документами — используй ссылки.
9. Явно указывай связанный код и связанные документы.
10. Не выдумывай факты, если evidence недостаточно.
+7 -1
View File
@@ -1,3 +1,9 @@
.env .env
.venv .venv
__pycache__ __pycache__
# Pipeline harness: per-run artifacts (md/json from tests.pipeline_setup_v3/v4)
tests/**/test_runs/**/*.md
tests/**/test_runs/**/*.json
tests/**/test_results/**/*.md
tests/**/test_results/**/*.json
+25
View File
@@ -0,0 +1,25 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Agent Backend: Uvicorn (Debug)",
"type": "python",
"request": "launch",
"module": "uvicorn",
"args": [
"app.main:app",
"--host",
"0.0.0.0",
"--port",
"15000"
],
"cwd": "${workspaceFolder}",
"envFile": "${workspaceFolder}/.env",
"env": {
"PYTHONPATH": "${workspaceFolder}/src"
},
"console": "integratedTerminal",
"justMyCode": false
}
]
}
+3 -2
View File
@@ -3,12 +3,13 @@ FROM python:3.12-slim
WORKDIR /app WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE=1 \ ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 PYTHONUNBUFFERED=1 \
PYTHONPATH=/app/src
COPY requirements.txt ./ COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt RUN pip install --no-cache-dir -r requirements.txt
COPY app ./app COPY src ./src
EXPOSE 15000 EXPOSE 15000
+5 -5
View File
@@ -915,15 +915,15 @@ flowchart TD
### 4.1.3. Канонический MVP runtime (CODE-first) ### 4.1.3. Канонический MVP runtime (CODE-first)
Единая точка входа исполнения — пакет `app.modules.agent.runtime`: Единая точка входа исполнения — пакет `app.core.agent.runtime`:
- **Роутер:** `app.modules.agent.intent_router_v2`; он отвечает и за routing, и за retrieval planning. - **Роутер:** `app.core.agent.intent_router`; он отвечает и за routing, и за retrieval planning.
- **LLM-слой:** `app.modules.agent.llm`; здесь живут `AgentLlmService`, `PromptLoader` и системные prompt assets. - **LLM-слой:** `app.core.agent.llm`; здесь живут `AgentLlmService`, `PromptLoader` и системные prompt assets.
- **Runtime:** `app.modules.agent.runtime`; внутри него stages разложены по подпакетам `retrieval`, `context`, `gates`, `answer_policy`, `generation`, `finalization`. - **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. - **Цепочка:** запрос → `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 и качества ответа по сценарию. - **Evidence gates:** pre/post проверки достаточности evidence и качества ответа по сценарию.
- **Диагностика:** runtime возвращает machine-readable diagnostics и trace по стадиям. - **Диагностика:** 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. Тесты: `pipeline_setup_v3` и связанные suite-ы проверяют канонический runtime и его stage-based execution.
+4
View File
@@ -0,0 +1,4 @@
# Запросы
1. Какие методы апи есть в проекте
2. Какие методы апи есть для healthcheck
3. Где документация на healthcheck
BIN
View File
Binary file not shown.
+126
View File
@@ -0,0 +1,126 @@
# Процессы работы с документацией (AS IS / TO BE)
## Основные артефакты системной аналитики
Системные аналитики работают с 3 артефактами:
- бизнес-требованиями
- системной аналитикой
- технической документацией
### Бизнес требования
Описывает бизнес и пользовательские требования, пользователькие use case, макеты экранов.
Сейчас не всегда оформляется как отдельный документ, часто этот шаг пропускается и требования фиксируются сразу в документе системной аналитики.
### Системная аналитика
Документ описыватет изменения в автоматизированной системе. Пишется системными аналитиками для разработчиков и тестировщиков. Так же этот документ проходит согласование с экспертами по архитектуре, безопасности, сопровождению.
Может описывать как целиком процесс (в случае реализации с нуля), так и инкремент, который вносит небольшие изменения в существующие процессы.
В данном документе содкржится вся информация по сути вносимых изменений, но отсутствует контекст о текущей реализации системы.
Состоит из разделов:
- Цели - короткое описание какую проблему и для кого решаем.
- Процесс AS IS и TO BE - фокус на изменения с точки зрения бизнес функций, без технической детализации.
- Ограничения - ограничения и допущения в реализации.
- Архитектура - описывает схему уровня контейнеров, основной фокус на интеграции между контейнерами и интеграционные сценарии.
- Функциональные требования - описывают изменения в системе.
- Нефункциональные требования - требования к аудиту, мониторингу, фичетоглам, пользовтелькой аналитике.
### Техническая документация
Техническая документация описывает реализацию системы. Эта информация используется командой разработки при проектировании и реализации новых фичей, понимании как работает система. Артефакт живет чуть впереди кода
Представялет из себя иерархическую модель документов, сейчас реализованную в конфлюенсе.
Есть несколько типов страниц, каждая из которы описывает определенный тип функциональности
- UI страницы
- API методы
- БД
- Логические блоки
#### UI страницы
Описывают экран на UI.
**Декомпозиция**
Как правило на страницу с описанием выносится целый макет/страница фронтального приложения, с одной основной интеграцией и опционально вспомогательными интеграциями.
Например - форма создания сущности. Есть вспомогательгные методы для полученяи правочников, использующихся при заполнении полей на форме, и вызов оснвного метода создания сущности.
Таким образом приложение декомпозируется на отдельные экраны, коотры свызываются между собой последовательно, но сами по себе являются независимыми
**Состав описания**
Все разделы обязательны.
Страница с описанием содержит:
- Краткое описание
- Технический use case
- Описание макета с декомпозицией на компоненты + их поведение
- Функциональные требования - описание интеграций и логики, специфичной для этой формы UI
- Нефункциональные требования - фичетоглы и события пользовательской аналитики
#### API методы
**Декомпозиция**
На каждый метод API заводится отдельная страница.
**Состав описания**
Все разделы обязательны.
Страница с описанием содержит:
- Краткое описание
- Технический use case
- Функциональные требования - описание интеграций и логики, специфичной для этой формы UI
- Нефункциональные требования - фичетоглы и события пользовательской аналитики
- Контракт метода - описание запроса и ответа. Для ответа так же приводится описание как заполнять поля.
#### БД
**Декомпозиция**
Сейсас это только странциа с описанием таблица. На каждую таблицу заводится отдельная страница.
**Состав описания**
Все разделы обязательны.
Страница с описанием содержит:
- Краткое описание
- Таблица с офисанием физической модели данных
#### Логические блоки
**Декомпозиция**
На отдельную страницу может быть вынесен общий переиспользуемый блок логики. Это позволяет не дублировать его на страницах документации. Как правило соответствует реализации общего компонента в коде.
**Состав описания**
Часть разделов в описании может отсутствовать.
- Краткое описание
- Технический use case
- Функциональные требования - описание интеграций и логики, специфичной для этой формы UI
- Нефункциональные требования - фичетоглы и события пользовательской аналитики
#### Прочие особенности процесса
##### Описание технических use cases
Сценарий описывает основные шаги процесса в разрезе участников, все технические детали, если их нельзя описать одним предложением, выносятся в разделы функциональных требований, нефункциональных требований, или даются ссылки на другие страницы (как правило это страницы с логическими блоками).
В технических use cases приводятся ссылки на страницы с описнаием вызываемых методов API. Особенно это актуально для страниц фронта, т.к. он использует наши методы API, которые есть в документации. Для интеграций с другими АС как правило приводистя ссылка на описание конфлюенса.
## AS IS
Сейчас все артефакты ведутся в конфлюенс. Одна страница содержит описанием одного аретфакта (бизнес требования, системная аналитика, страница документации), страницы организованы иерархически, используюстя ссылки для обозначения связей.
Проблемы:
- документация со временем теряет актуальность
- отсутствие автоматизации
- ручное ведение
---
## TO BE
Целевое состояние:
- аналитик продолжает писать артефакты бизнес-требований и системной аналитики
- агент генерирует и обновляет документацию по странице системной аналитики
- документация становится инженерным артефактом, который ведется в GIT
### Форматы
- Markdown
- OpenAPI
- Mermaid / PlantUML
### Роль агента
- использование документации как базы знаний - как для ответов на вопросы, так и для проектирования изменений в системе.
- внесение изменений в документацию по артефактам системной аналитики
- генерация из документации спецификаций OPENAPI и JSON-schema
+235
View File
@@ -0,0 +1,235 @@
Ниже обновленная версия с учетом гибридной модели интент роутера.
---
## 1. Концепция агента
Агент проектируется как intent-driven система для работы с кодом и документацией, где пользовательский запрос сначала нормализуется и интерпретируется, затем по нему извлекается релевантный контекст из многослойного RAG, после чего специализированный task workflow выполняет целевую задачу. Агент не является единым “умным чатом”: логика разделена на маршрутизацию, retrieval и специализированные execution workflows. Проверка evidence, вызовы LLM и правила сборки ответа находятся внутри task workflows и зависят от типа задачи.
---
## 2. Компонентная модель
```mermaid
flowchart LR
IDE[IDE Plugin / Client] --> API[API Layer]
API --> IR[IntentRouter V3]
IR --> RAG[Retrieval RAG]
RAG --> TW1[Task Workflow: Documentation Explain]
RAG --> TW2[Task Workflow: OpenAPI Generation]
RAG --> TW3[Task Workflow: Documentation Generation]
RAG --> TWN[Other Specialized Task Workflows]
TW1 --> OUT[Response / Artifact]
TW2 --> OUT
TW3 --> OUT
TWN --> OUT
```
---
## 3. Основной flow процесса
### Основной процесс
1. Пользователь отправляет запрос через IDE plugin или другой клиент.
2. `API Layer` принимает запрос и передает его в агент.
3. `IntentRouter V3`:
* нормализует запрос;
* детерминированно извлекает ключевые артефакты;
* с помощью LLM определяет тип задачи и параметры обработки;
* формирует параметры retrieval.
4. Выполняется извлечение данных из `Retrieval RAG`.
5. Извлеченный контекст передается в соответствующий `Task Workflow`.
6. Внутри workflow выполняется:
* подготовка контекста;
* evidence-проверки;
* вызовы LLM;
* формирование результата.
7. Результат возвращается пользователю.
### Sequence diagram
```mermaid
sequenceDiagram
participant User as User / IDE Plugin
participant API as API Layer
participant Router as IntentRouter V3
participant RAG as Retrieval RAG
participant WF as Task Workflow
User->>API: request
API->>Router: agent call
Router->>Router: normalize + extract artifacts
Router->>Router: LLM routing (task / intent)
Router->>RAG: retrieval request
RAG-->>Router: retrieved context
Router->>WF: route result + context
WF->>WF: evidence logic + LLM calls
WF-->>API: final result
API-->>User: response
```
---
## 4. Описание компонентов
### 4.1. IDE Plugin / Client
**Задача**
Точка входа пользователя в агент.
**Как устроен**
Любой внешний клиент (IDE plugin, web UI и др.), который отправляет запрос и получает результат.
**Почему так**
Агент изначально проектируется как backend-система, независимая от интерфейса.
---
### 4.2. API Layer
**Задача**
Обеспечивает внешний интерфейс взаимодействия с агентом.
**Как устроен**
Принимает запрос, валидирует его и передает во внутренний pipeline, затем возвращает результат.
**Почему так**
Позволяет изолировать транспортный слой от логики агента.
---
### 4.3. IntentRouter V3
**Задача**
Определяет, как должен обрабатываться пользовательский запрос и какой сценарий выполнения применить.
**Как устроен**
Гибридная модель из двух частей:
#### 1. Детерминированный слой
Выполняет:
* нормализацию запроса;
* извлечение ключевых артефактов:
* домены;
* типы сущностей (API, entity, component и т.д.);
* явные ссылки (endpoint, путь, имя);
* выделение базовых сигналов (например: explain / list / generate).
Этот слой задает **жесткие рамки интерпретации запроса**.
#### 2. LLM-роутинг
Использует:
* нормализованный запрос;
* извлеченные артефакты;
* описание доступных типов задач;
и определяет:
* тип задачи;
* общий сценарий обработки;
* параметры retrieval;
* ожидаемую форму ответа.
#### Итог
Router формирует:
* параметры retrieval;
* тип task workflow;
* контекст для дальнейшего выполнения.
**Почему решение такое**
Ранее использовался более детерминированный подход с фиксированными сценариями, который хорошо работал в узком наборе задач, но плохо масштабируется. Полностью LLM-based роутинг, наоборот, дает гибкость, но теряет предсказуемость и управляемость.
Поэтому выбран гибридный подход:
* детерминированный слой фиксирует ключевые артефакты и ограничения;
* LLM выполняет гибкую интерпретацию задачи.
Это позволяет:
* сохранить управляемость и стабильность;
* избежать взрывного роста количества сценариев;
* поддерживать сложные и нетиповые запросы.
---
### 4.4. Retrieval RAG
**Задача**
Извлечь релевантный контекст для выполнения задачи.
**Как устроен**
Многослойная система хранения знаний (код, документация, факты, связи), из которой извлекается структурированный контекст в зависимости от параметров, заданных роутером.
**Почему так**
Разные задачи требуют разных типов данных, поэтому используется слойная модель вместо плоского поиска.
---
### 4.5. Task Workflows
**Задача**
Реализуют прикладную логику выполнения конкретного типа задачи.
**Как устроены**
Набор специализированных workflows, например:
* объяснение по документации;
* генерация OpenAPI;
* генерация документации;
* другие сценарии.
Внутри workflow находятся:
* обработка контекста;
* evidence-проверки;
* вызовы LLM;
* сборка результата.
**Почему так**
Логика проверки данных и генерации сильно зависит от задачи, поэтому она инкапсулируется в отдельных workflows, а не в одном универсальном слое.
---
### 4.6. Output / Artifact
**Задача**
Вернуть результат пользователю.
**Как устроен**
Может быть:
* текстовый ответ;
* структурированный список;
* OpenAPI спецификация;
* документация;
* иной артефакт.
**Почему так**
Агент должен поддерживать не только ответы, но и генерацию инженерных артефактов.
---
## Итог
Обновленная архитектура строится на следующем принципе:
* **детерминированное извлечение ключевых артефактов** задает рамки;
* **LLM выполняет гибкий роутинг внутри этих рамок**;
* **retrieval обеспечивает данные**;
* **task workflows реализуют прикладную логику и контроль качества**.
Это позволяет одновременно сохранить управляемость системы и обеспечить масштабируемость под новые типы задач.
+59
View File
@@ -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` - создание документации из системной аналитики на фичу
+356
View File
@@ -0,0 +1,356 @@
# 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, fallback title, summary и doc kind, вычисленный классификатором документации.
Данные извлекаются структурированно по атрибутам.
В `content` попадает summary документа, а не склейка всех частей документа в сплошной текст.
Фиксация в БД:
| Атрибут в `metadata_json` | Описание | Источник |
|---|---|---|
| `document_id` | идентификатор документа | `frontmatter.id`, иначе путь файла |
| `type` | тип документа из frontmatter | `frontmatter.type` |
| `name` | системное имя документа | `frontmatter.name` |
| `title` | человекочитаемый заголовок документа | `frontmatter.title`, иначе fallback title |
| `module` | модуль документа | `frontmatter.module` |
| `domain` | домен документа | `frontmatter.domain` |
| `subdomain` | поддомен документа | `frontmatter.subdomain` |
| `layer` | логический слой, указанный в frontmatter документа | `frontmatter.layer` |
| `status` | статус документа | `frontmatter.status` |
| `updated_at` | дата или отметка последнего обновления | `frontmatter.updated_at` |
| `tags` | теги документа | `frontmatter.tags` |
| `entities` | сущности, связанные с документом | `frontmatter.entities` |
| `parent` | родительский документ | `frontmatter.parent` |
| `children` | дочерние документы | `frontmatter.children` |
| `links` | ссылки на связанные материалы | `frontmatter.links` |
| `source_path` | исходный путь документа | путь файла |
| `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`
+852
View File
@@ -0,0 +1,852 @@
## 1. Формат ведения технической документации агентом
## 1.1. Общие принципы
Техническая документация, формируемая агентом, должна строиться как **система атомарных, не пересекающихся по смыслу документов**, связанных между собой явными ссылками.
Ключевые принципы:
- один документ описывает одну сущность или один устойчивый технический аспект;
- документ не должен дублировать соседние документы;
- общая система знаний должна собираться через ссылки, а не через копипасту;
- структура документации должна быть пригодна как для чтения человеком, так и для индексирования в RAG.
## 1.2. Базовые типы документных единиц
На первом этапе логично сохранить текущую семантику типов документов, но перенести ее в файловую модель.
Базовые типы:
- `ui_page`
- `api_method`
- `logic_block`
Позже могут добавиться:
- `architecture_overview`
- `integration_doc`
- `domain_entity`
- `glossary_item`
- `index_page`
## 1.3. Принцип декомпозиции страниц / файлов
### Один устойчивый объект — один документ
Если объект можно переиспользовать или на него могут ссылаться другие документы, его надо выносить в отдельный файл.
Примеры:
- отдельная UI-страница;
- отдельный API endpoint;
- отдельный блок логики;
- отдельный интеграционный сценарий.
### Документы не должны пересекаться по смыслу
Если описание повторяется в нескольких местах, нужно выделять общий документ и ссылаться на него.
Примеры:
- фронтальная страница не должна заново описывать логику API;
- документ по API не должен заново раскрывать общую логику переиспользуемого блока;
- вместо дублирования должен быть переход по ссылке.
### Use case и детальные правила живут раздельно
Сценарий описывает поток работы, а детали выносятся в функциональные требования, отдельные блоки логики или контрактные описания.
Это важно и для RAG-индексации:
- сценарии индексируются как workflows;
- отдельные правила — как facts;
- сущности и блоки — как entities.
## 1.4. Иерархическая организация документации
Документация должна быть организована как иерархическое дерево каталогов и файлов, а не как набор неструктурированных страниц.
Пример верхнего уровня:
```text
docs/
ui/
api/
logic/
domains/
integrations/
architecture/
glossary/
errors/
```
Пример организации:
```text
docs/
ui/
order-create-page.md
order-edit-page.md
api/
orders-create.md
orders-get.md
logic/
order-validation.md
order-enrichment.md
architecture/
system-overview.md
integration-landscape.md
errors/
catalog.yaml
```
## 1.5. Учет связей между документами
Связи должны быть **явными и поддерживаемыми агентом**.
Примеры:
- UI-страница ссылается на API, который она вызывает;
- API-документ ссылается на переиспользуемые логические блоки;
- логический блок ссылается на связанные интеграции;
- архитектурный обзор ссылается на набор конкретных модулей и документов;
- документ по коду может ссылаться на системную аналитику, которая инициировала изменение.
Именно эта сеть ссылок затем индексируется в слоях:
- `D1_DOCUMENT_CATALOG`
- `D3_ENTITY_CATALOG`
- `D4_WORKFLOW_INDEX`
- `D5_REFERENCE_GRAPH`
- `D6_DOC_CODE_LINKS`
## 1.6. Формат markdown-документов
Основной формат технической документации — `Markdown`.
Каждый документ состоит из двух частей:
1. **YAML frontmatter** — структурные метаданные;
2. **Markdown body** — основное содержимое по шаблону.
## 3.7. YAML frontmatter
Frontmatter нужен для:
- определения типа документа;
- идентификации документа;
- определения его места в иерархии;
- фиксации связей с кодом и другими документами;
- выделения сущностей и тегов;
- упрощения построения слоев `D1`, `D3`, `D5`, `D6`.
### Базовый frontmatter
```yaml
---
id: ui-order-create-page
title: Страница создания заказа
doc_type: ui_page
domain: orders
status: draft
related_docs:
- api-orders-create
- logic-order-validation
entities:
- Order
- CreateOrder
tags:
- ui
- orders
- creation
#owner: system-analyst
#source_of_truth: code
#related_code:
# - src/orders/ui/create_page.tsx
# - src/orders/api/orders_controller.py
---
```
### Обязательные поля
- `id` — стабильный уникальный идентификатор документа;
- `title` — человекочитаемый заголовок;
- `doc_type` — тип документа;
- `related_docs` — ссылки на связанные документы;
- `status` — статус документа;
- `domain` - домен фичи (Карточка клиента, Задачи, Сделки, Предложения)
- `sub_domain` - поддомен внутри основной сущности (Счета, ЗДА, ECM)
### Рекомендуемые поля
- `owner`
- `entities`
- `tags`
- `parent`
- `children`
- `feature`
- `system_analytics_refs`
- `business_refs`
- `updated_from`
- `reviewers`
- `source_of_truth`
- `related_code`
### Допустимые значения `doc_type`
- `ui_page`
- `api_method`
- `logic_block`
- `architecture_overview`
- `integration_doc`
- `domain_entity`
- `glossary_item`
- `index_page`
### Допустимые значения `status`
- `draft`
- `in_review`
- `approved`
- `outdated`
- `generated`
- `active`
### Допустимые значения `source_of_truth`
- `code`
- `doc`
- `system_analysis`
- `business_requirements`
- `mixed`
## 1.8. Typed frontmatter для разных типов документов
У каждого типа документа есть:
- **общие поля**;
- **тип-специфичные поля**.
### Пример для `api_method`
```yaml
---
id: api.create_invoice
doc_type: api_method
domain: billing
title: Создание инвойса
endpoint: POST /api/v1/invoices
auth: USER
idempotent: false
timeout_ms: 3000
links:
called_by:
- ui.invoice_form
uses_logic:
- logic.invoice_validation
writes_db:
- db.invoices
- db.invoice_items
integrates_with:
- int.crm_sync
related_docs:
- ui.invoice_form
- logic.invoice_validation
related_code:
- services/billing/api/create_invoice.py
entities:
- Invoice
- CreateInvoice
tags:
- invoice
- create
- billing
status: active
version: 1.3
source_of_truth: code
---
```
### Для `api_method` полезно поддерживать
- `endpoint`
- `sup_parameters`
- `role_model_actions`
- `monitoring_actions`
- `audit_actions`
- `idempotent`
- `timeout_ms`
- `links.called_by`
- `links.uses_logic`
- `links.writes_db`
- `links.integrates_with`
### Для `ui_page` позже полезно поддерживать
- `calls_api`
- `user_analitycs_actions`
- `sup_parameters`
- `role_model_actions`
- `entry_points`
- `uses_logic`
### Для `logic_block` полезно поддерживать
- `called_from`
- `uses_logic`
- `reads_db`
- `writes_db`
- `integrates_with`
## 1.9. Двухслойная структура документа: `Summary` + `Details`
LLM не должна каждый раз тонуть в полном документе. Поэтому каждый документ должен содержать два уровня представления.
### `Summary`
Короткая, строго структурированная спецификация. Это слой **быстрого контекста**.
Рекомендуемый объем:
- примерно 30–60 строк;
- без длинных пояснений;
- только ключевые факты.
Пример:
```md
## Summary
- Purpose: создание инвойса из формы
- Actor: пользователь
- Trigger: Submit
- Main API: POST /api/v1/invoices (api.create_invoice)
- Validation: required fields, amount > 0, date <= today
- Errors: 400(field errors), 409(duplicate), 503(retryable)
- Analytics: event invoice_submit, invoice_error
```
### `Details`
Полное раскрытие объекта:
- use case;
- функциональные требования;
- UI;
- API;
- integrations;
- ошибки;
- НФТ;
- связи;
- кодовые привязки.
### Блок `## Integrations`
Если у объекта есть интеграции, они должны быть выделены в отдельный блок `## Integrations`.
Интеграции не нужно дублировать во frontmatter.
Основное описание хранится в body документа.
Ожидаемый принцип:
- одна интеграция = одна отдельная запись внутри блока;
- у интеграции есть краткое имя;
- у интеграции есть структурированные атрибуты;
- дополнительные детали допускаются в свободной форме через вложенный словарь.
Рекомендуемые атрибуты интеграции:
- `target`
- `target_type`
- `direction`
- `interaction`
- `via`
- `purpose`
- `details`
Где:
- `target` - идентификатор или имя целевого объекта;
- `target_type` - тип цели: `api`, `ui`, `entity`, `service`, `queue`, `db`, `external_system`;
- `direction` - направление: `inbound`, `outbound`, `bidirectional`;
- `interaction` - тип взаимодействия: `calls`, `reads`, `writes`, `emits`, `consumes`, `depends_on`;
- `via` - технический канал интеграции;
- `purpose` - зачем нужна интеграция;
- `details` - словарь с гибкой структурой под дополнительные параметры.
Пример:
```md
## Integrations
### Orders API
- target: api.orders.create
- target_type: api
- direction: outbound
- interaction: calls
- via: POST /api/orders
- purpose: создание заказа
- details:
- auth: service-token
- retry: true
### Order Entity
- target: domain.order
- target_type: entity
- direction: outbound
- interaction: writes
- via: repository
- purpose: сохранение состояния заказа
- details:
- transaction: required
```
Этот блок должен быть пригоден и для чтения человеком, и для последующего извлечения в отдельный RAG-слой интеграций.
## 1.10. Общие требования к markdown body
1. В документе должен быть один `H1`, совпадающий с `title`.
2. Основные разделы используют `H2`.
3. Подразделы внутри разделов используют `H3`.
4. Не должно быть хаотической вложенности заголовков.
5. Один раздел должен описывать одну смысловую часть.
6. Текст не должен дублировать соседние документы.
7. Вместо дублирования должны использоваться явные ссылки на связанные документы.
8. Сценарии, правила, ограничения и ссылки на код должны быть отделены друг от друга.
## 1.11. Базовый каркас markdown-документа
```md
---
id: api-orders-create
title: Метод создания заказа
doc_type: api_method
domain: orders
status: draft
source_of_truth: code
related_docs:
- logic-order-validation
- ui-order-create-page
related_code:
- src/orders/api/create_order.py
entities:
- Order
- CreateOrder
tags:
- api
- orders
---
# Метод создания заказа
## Summary
- Purpose: создание заказа
- Actor: пользователь
- Trigger: submit формы
- Main API: POST /orders
## Details
### Описание
### Технический use case
### Функциональные требования
### Нефункциональные требования
### Контракт
## 3.13. Специализированные шаблоны документов
### UI Page
```md
# <Название страницы>
## Summary
## Назначение
## Контекст
## Технический use case
## Описание UI
## UI Elements
## Функциональные требования
## Нефункциональные требования
## Связанные API
## Связанные блоки логики
## Связанный код
## Связанные документы
## История изменений
```
#### Требования к разделу `Описание UI`
Для каждого элемента желательно описывать:
- тип элемента;
- назначение;
- источник данных;
- default / placeholder;
- правила активации;
- поведение при взаимодействии;
- валидацию.
#### Требования к разделу `UI Elements`
UI-элементы должны храниться в **табличном** или **полуструктурированном** виде.
Пример:
```md
## UI Elements
| id | type | label | data_source | validation | behavior |
|--------|--------|---------|------------|------------|----------|
| amount | input | Amount | local | >0 | enables submit |
| submit | button | Create | - | - | calls api.create_invoice |
```
Если модель UI сложная, допустим sidecar-файл `ui_elements.yaml` или `ui_elements.json` рядом с основным документом.
### API Method
```md
# <Название API метода>
## Summary
## Назначение
## Контекст
## Технический use case
## Функциональные требования
## Contract
## Integrations
## Errors
## Нефункциональные требования
## Связанные блоки логики
## Связанный код
## Связанные документы
## История изменений
```
#### Требования к разделу `Contract`
Контракт может:
- быть кратко описан прямо в документе;
- ссылаться на OpenAPI;
- ссылаться на отдельный контрактный файл.
Для REST API целевым источником истины должен становиться `OpenAPI`.
### Reusable Logic Block
```md
# <Название блока логики>
## Summary
## Назначение
## Контекст
## Технический use case
## Функциональные требования
## Integrations
## Ограничения и условия вызова
## Нефункциональные требования
## Связанные API / UI / integration points
## Связанный код
## Связанные документы
## История изменений
```
## 3.14. Машинно-читаемые API-контракты
Для API контрактов **источником истины** должен становиться:
- `OpenAPI` — предпочтительно;
- либо временно строгий markdown/yaml-контракт, если OpenAPI еще нет.
Минимальный набор для API-контракта:
- `endpoint`
- `method`
- `request fields`
- `required / optional`
- `constraints`
- `response`
- `errors`
- `idempotency`
- `retry`
- `timeout`
- `auth`
## 3.15. Каталог ошибок
Ошибки, HTTP-коды, retry-правила и клиентское поведение не должны размазываться по разным документам.
Нужен единый каталог ошибок, например `docs/errors/catalog.yaml`.
Пример:
```yaml
errors:
- error_id: invoice_validation_failed
http_code: 400
internal_code: BILLING_400_01
when: invalid request fields
client_behavior: show field errors
retry: false
owner: billing
- error_id: invoice_duplicate
http_code: 409
internal_code: BILLING_409_01
when: duplicate invoice detected
client_behavior: show duplicate warning
retry: false
owner: billing
- error_id: crm_sync_unavailable
http_code: 503
internal_code: BILLING_503_02
when: downstream CRM unavailable
client_behavior: retry later
retry: true
owner: billing
```
В API- и logic-документах лучше ссылаться на `error_id`, а не заново подробно описывать каждую ошибку.
## 3.16. Требования к качеству документа для RAG
1. **Явные заголовки** — не использовать безымянные блоки текста.
2. **Атомарные утверждения** — не смешивать несколько правил в одном пункте, если их можно разделить.
3. **Явные сущности** — использовать стабильные названия компонентов, API, модулей, страниц.
4. **Явные ссылки** — не писать «этот метод», если можно указать конкретную ссылку или идентификатор.
5. **Минимум дублирования** — повторяющийся контент должен заменяться ссылками.
6. **Привязка к коду** — если документ описывает кодовый объект, это должно быть явно указано.
7. **Разделение сценариев и правил** — workflow и fact-like требования должны быть отделены.
## 3.17. Как структура markdown помогает RAG
- `frontmatter` + заголовки → `D1_DOCUMENT_CATALOG`
- `entities`, `tags`, устойчивые термины → `D3_ENTITY_CATALOG`
- атомарные функциональные и нефункциональные требования → `D2_FACT_INDEX`
- `Технический use case``D4_WORKFLOW_INDEX`
- `related_docs`, явные ссылки → `D5_REFERENCE_GRAPH`
- `related_code`, упоминания symbols и файлов → `D6_DOC_CODE_LINKS`
- `Summary` → быстрый retrieval и short-form context для LLM
## 3.18. Принципы генерации документации агентом
Когда документ пишет агент, он должен:
- сначала извлечь evidence из кода, системной аналитики и существующих документов;
- определить тип документа;
- заполнить frontmatter;
- построить markdown body по шаблону;
- явно указать связи с кодом и другими документами;
- не дублировать уже существующее описание, если можно сослаться на него.
---
## 4.4. Layered RAG
RAG строится как система специализированных слоев для двух основных доменов:
- `CODE RAG`
- `DOCS RAG`
Каждый graph извлекает контекст не из одного общего индекса, а из нужного набора слоев в зависимости от intent.
## 4.5. Evidence gate
Перед синтезом ответа или документа агент должен проверять, хватает ли опоры.
Примеры:
- найден ли symbol;
- найдено ли достаточное количество code chunks;
- есть ли supporting relations;
- есть ли document evidence;
- есть ли docs ↔ code mapping.
Если опоры недостаточно, агент должен:
- деградировать в упрощенный режим;
- честно фиксировать неполноту ответа;
- при необходимости уходить в fallback.
## 4.6. Synthesis layer
На этом этапе LLM:
- агрегирует найденные артефакты;
- формирует объяснение;
- пишет документ;
- структурирует результат под нужный шаблон.
LLM не должна быть основным источником фактов. Фактическая основа должна приходить из RAG и диагностируемого pipeline.
## 4.7. Diagnostics
Система должна сохранять диагностический след:
- какой graph был выбран;
- какие слои использовались;
- что было найдено;
- где retrieval был слабым;
- почему был выбран fallback;
- какие evidence стали основой ответа.
## 4.8. Сценарии: Target Architecture vs MVP-now
### 4.8.1. Target Architecture
#### CODE
- `OPEN_FILE` — открыть конкретный файл;
- `OPEN_SYMBOL` — открыть класс / функцию / метод;
- `EXPLAIN` — объяснить, как работает сущность или фрагмент;
- `FIND_TESTS` — найти релевантные тесты;
- `FIND_ENTRYPOINTS` — найти основные точки входа;
- `RELATED_CODE` — найти связанные сущности и ближайший контекст.
#### DOCS
- `DOC_SEARCH` — найти релевантный фрагмент документации;
- `DOC_EXPLAIN` — кратко объяснить, что сказано в документации по теме;
- `DOC_ENTITY_LOOKUP` — найти разделы, связанные с сущностью или компонентом;
- `GENERATE_DOCS_FROM_CODE` — сформировать документацию по коду с нуля для модуля, класса, функции, компонента или сценария.
#### CROSS-DOMAIN
- `FIND_IMPLEMENTATION_BY_DOC` — найти реализацию по описанию;
- `FIND_DOC_BY_CODE` — найти документацию по коду;
- `COMPARE_DOCS_AND_CODE` — базовое сопоставление документации и реализации.
#### GENERAL / FALLBACK
- `GENERAL_QA` — общий сценарий ответа на вопрос, если домен или интент не удалось определить уверенно.
### 4.8.2. MVP-now
В текущем цикле фокус на сценариях:
- `OPEN_FILE`
- `EXPLAIN`
- `FIND_TESTS`
- `FIND_ENTRYPOINTS`
- `GENERAL_QA`
DOCS и CROSS_DOMAIN остаются частью target architecture; в текущем цикле они не являются обязательной частью test-first MVP.
---
## 5. Структура слоев RAG
## 5.1. CODE RAG
### C0 — Source Chunks
**Назначение:** базовые фрагменты исходного кода.
**Единица:** chunk кода.
**Как формируется:** исходные файлы обходятся и режутся на chunk’и с учетом структурных границ.
**Статус в MVP:** да.
### C1 — Symbol Catalog
**Назначение:** каталог модулей, классов, функций, методов и других значимых сущностей.
**Единица:** symbol.
**Как формируется:** из AST и синтаксического разбора кода.
**Статус в MVP:** да.
### C2 — Symbol Relations
**Назначение:** связи между symbols.
**Единица:** relation.
**Как формируется:** вторым проходом по AST и структурным зависимостям.
**Статус в MVP:** да, в ограниченном виде.
### C3 — Entrypoints
**Назначение:** каталог точек входа системы.
**Единица:** entrypoint.
**Как формируется:** специализированными детекторами entrypoint-паттернов.
**Статус в MVP:** да, минимально.
### C4 — Execution Paths
**Назначение:** типовые пути исполнения.
**Единица:** path.
**Как формируется:** поверх `C2` и `C3` через производную трассировку.
**Статус в MVP:** нет.
### C5 — Test Mappings
**Назначение:** связи production code ↔ tests.
**Единица:** mapping.
**Как формируется:** по путям, именам, импортам и конвенциям проекта.
**Статус в MVP:** да, минимально.
### C6 — Code Facts
**Назначение:** нормализованные факты из кода.
**Единица:** fact.
**Как формируется:** поверх `C1C3` как производный слой.
**Статус в 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:** C0C6 (Source Chunks, Symbol Catalog, Symbol Relations, Entrypoints, Execution Paths, Test Mappings, Code Facts)
- **DOCS:** D0D6 (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 (слои D0D6 в исполнении 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 документации;
- глубокая автоматизация подготовки системной аналитики.
+100
View File
@@ -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`.
+220
View File
@@ -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.
+1 -1
View File
@@ -27,7 +27,7 @@ services:
env_file: env_file:
- .env - .env
environment: environment:
DATABASE_URL: ${DATABASE_URL} DATABASE_URL: postgresql+psycopg://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
GIGACHAT_AUTH_URL: ${GIGACHAT_AUTH_URL} GIGACHAT_AUTH_URL: ${GIGACHAT_AUTH_URL}
GIGACHAT_API_URL: ${GIGACHAT_API_URL} GIGACHAT_API_URL: ${GIGACHAT_API_URL}
GIGACHAT_SCOPE: ${GIGACHAT_SCOPE} GIGACHAT_SCOPE: ${GIGACHAT_SCOPE}
-380
View File
@@ -1,380 +0,0 @@
{
"layers": {
"C0_SOURCE_CHUNKS": {
"retriever": {
"class": "RagService",
"file": "app/modules/rag/services/rag_service.py",
"method": "retrieve"
},
"indexer": {
"class": "CodeTextDocumentBuilder",
"file": "app/modules/rag/indexing/code/code_text/document_builder.py",
"method": "build"
},
"input": {
"type": "observed shape",
"fields": {
"rag_session_id": {
"type": "string",
"required": true
},
"query": {
"type": "string",
"required": true
},
"layers": {
"type": "implicit list[string]",
"required": false,
"source": "RagQueryRouter.layers_for_mode('code')"
}
}
},
"output": {
"type": "list[dict]",
"fields": {
"source": "string",
"content": "string",
"layer": "\"C0_SOURCE_CHUNKS\"",
"title": "string",
"metadata": {
"chunk_index": "int",
"chunk_type": "\"symbol_block\" | \"window\"",
"module_or_unit": "string",
"artifact_type": "\"CODE\""
},
"score": "float | null"
}
},
"examples": {
"input": {
"rag_session_id": "rag-123",
"query": "where is implemented get_user"
},
"output": {
"source": "app/api/users.py",
"content": "async def get_user(user_id: str):\n service = UserService()\n return service.get_user(user_id)",
"layer": "C0_SOURCE_CHUNKS",
"title": "app/api/users.py:get_user",
"metadata": {
"chunk_index": 0,
"chunk_type": "symbol_block",
"module_or_unit": "app.api.users",
"artifact_type": "CODE"
},
"score": 0.07
}
},
"defaults": {
"retrieve_limit": 8,
"embed_batch_size_env": "RAG_EMBED_BATCH_SIZE",
"embed_batch_size_default": 16,
"window_chunk_size_lines": 80,
"window_overlap_lines": 15
},
"limitations": [
"Line spans are stored but not returned in the public retrieval item shape.",
"No direct path or namespace filter is exposed through the retrieval endpoint."
]
},
"C1_SYMBOL_CATALOG": {
"retriever": {
"class": "RagService",
"file": "app/modules/rag/services/rag_service.py",
"method": "retrieve"
},
"indexer": {
"class": "SymbolDocumentBuilder",
"file": "app/modules/rag/indexing/code/symbols/document_builder.py",
"method": "build"
},
"input": {
"type": "observed shape",
"fields": {
"rag_session_id": {
"type": "string",
"required": true
},
"query": {
"type": "string",
"required": true
},
"query_term_expansion": {
"type": "list[string]",
"required": false,
"source": "extract_query_terms(query_text)",
"max_items": 6
}
}
},
"output": {
"type": "list[dict]",
"fields": {
"source": "string",
"content": "string",
"layer": "\"C1_SYMBOL_CATALOG\"",
"title": "string",
"metadata": {
"symbol_id": "string",
"qname": "string",
"kind": "\"class\" | \"function\" | \"method\" | \"const\"",
"signature": "string",
"decorators_or_annotations": "list[string]",
"docstring_or_javadoc": "string | null",
"parent_symbol_id": "string | null",
"package_or_module": "string",
"is_entry_candidate": "bool",
"lang_payload": "object",
"artifact_type": "\"CODE\""
},
"score": "float | null"
}
},
"examples": {
"input": {
"rag_session_id": "rag-123",
"query": "where is implemented get_user"
},
"output": {
"source": "app/api/users.py",
"content": "function get_user\nget_user(user_id)",
"layer": "C1_SYMBOL_CATALOG",
"title": "get_user",
"metadata": {
"symbol_id": "sha256(...)",
"qname": "get_user",
"kind": "function",
"signature": "get_user(user_id)",
"decorators_or_annotations": [
"router.get"
],
"docstring_or_javadoc": null,
"parent_symbol_id": null,
"package_or_module": "app.api.users",
"is_entry_candidate": true,
"lang_payload": {
"async": true
},
"artifact_type": "CODE"
},
"score": 0.07
}
},
"defaults": {
"retrieve_limit": 8,
"layer_rank": 1
},
"limitations": [
"Only Python AST symbols are indexed.",
"Cross-file resolution is not implemented.",
"parent_symbol_id is an observed qname-like value, not guaranteed to be a symbol hash."
]
},
"C2_DEPENDENCY_GRAPH": {
"retriever": {
"class": "RagService",
"file": "app/modules/rag/services/rag_service.py",
"method": "retrieve"
},
"indexer": {
"class": "EdgeDocumentBuilder",
"file": "app/modules/rag/indexing/code/edges/document_builder.py",
"method": "build"
},
"input": {
"type": "observed shape",
"fields": {
"rag_session_id": {
"type": "string",
"required": true
},
"query": {
"type": "string",
"required": true
}
}
},
"output": {
"type": "list[dict]",
"fields": {
"source": "string",
"content": "string",
"layer": "\"C2_DEPENDENCY_GRAPH\"",
"title": "string",
"metadata": {
"edge_id": "string",
"edge_type": "\"calls\" | \"imports\" | \"inherits\"",
"src_symbol_id": "string",
"src_qname": "string",
"dst_symbol_id": "string | null",
"dst_ref": "string | null",
"resolution": "\"resolved\" | \"partial\"",
"lang_payload": "object",
"artifact_type": "\"CODE\""
},
"score": "float | null"
}
},
"examples": {
"input": {
"rag_session_id": "rag-123",
"query": "how get_user calls service"
},
"output": {
"source": "app/api/users.py",
"content": "get_user calls UserService",
"layer": "C2_DEPENDENCY_GRAPH",
"title": "get_user:calls",
"metadata": {
"edge_id": "sha256(...)",
"edge_type": "calls",
"src_symbol_id": "sha256(...)",
"src_qname": "get_user",
"dst_symbol_id": null,
"dst_ref": "UserService",
"resolution": "partial",
"lang_payload": {
"callsite_kind": "function_call"
},
"artifact_type": "CODE"
},
"score": 0.11
}
},
"defaults": {
"retrieve_limit": 8,
"layer_rank": 2,
"graph_build_mode": "static_python_ast"
},
"limitations": [
"No traversal API exists.",
"Edges are stored as retrievable rows, not as a graph-native store.",
"Destination resolution is local to one indexed file."
]
},
"C3_ENTRYPOINTS": {
"retriever": {
"class": "RagService",
"file": "app/modules/rag/services/rag_service.py",
"method": "retrieve"
},
"indexer": {
"class": "EntrypointDocumentBuilder",
"file": "app/modules/rag/indexing/code/entrypoints/document_builder.py",
"method": "build"
},
"input": {
"type": "observed shape",
"fields": {
"rag_session_id": {
"type": "string",
"required": true
},
"query": {
"type": "string",
"required": true
}
}
},
"output": {
"type": "list[dict]",
"fields": {
"source": "string",
"content": "string",
"layer": "\"C3_ENTRYPOINTS\"",
"title": "string",
"metadata": {
"entry_id": "string",
"entry_type": "\"http\" | \"cli\"",
"framework": "\"fastapi\" | \"flask\" | \"typer\" | \"click\"",
"route_or_command": "string",
"handler_symbol_id": "string",
"lang_payload": "object",
"artifact_type": "\"CODE\""
},
"score": "float | null"
}
},
"examples": {
"input": {
"rag_session_id": "rag-123",
"query": "which endpoint handles get user"
},
"output": {
"source": "app/api/users.py",
"content": "fastapi http \"/users/{user_id}\"",
"layer": "C3_ENTRYPOINTS",
"title": "\"/users/{user_id}\"",
"metadata": {
"entry_id": "sha256(...)",
"entry_type": "http",
"framework": "fastapi",
"route_or_command": "\"/users/{user_id}\"",
"handler_symbol_id": "sha256(...)",
"lang_payload": {
"methods": [
"GET"
]
},
"artifact_type": "CODE"
},
"score": 0.05
}
},
"defaults": {
"retrieve_limit": 8,
"layer_rank": 0
},
"limitations": [
"Detection is decorator-string based.",
"No Django, Celery, RQ, or cron entrypoints were found.",
"Returned payload does not expose line spans."
]
}
},
"retrieval_endpoint": {
"entrypoint": {
"file": "app/modules/rag_session/module.py",
"method": "internal_router.retrieve"
},
"request": {
"type": "dict",
"fields": {
"rag_session_id": "string | optional if project_id provided",
"project_id": "string | optional fallback for rag_session_id",
"query": "string"
}
},
"response": {
"type": "dict",
"fields": {
"items": "list[retrieval item]"
}
},
"defaults": {
"mode": "docs unless RagQueryRouter detects code hints",
"limit": 8,
"embedding_provider": "GigaChat embeddings",
"fallback_after_embedding_error": true,
"fallback_to_docs_when_code_empty": true
}
},
"ranking": {
"storage": "PostgreSQL rag_chunks + pgvector",
"query_repository": {
"class": "RagQueryRepository",
"file": "app/modules/rag/persistence/query_repository.py",
"method": "retrieve"
},
"order_by": [
"lexical_rank ASC",
"test_penalty ASC",
"layer_rank ASC",
"embedding <=> query_embedding ASC"
],
"notes": [
"lexical_rank is derived from qname/symbol_id/title/path/content matching extracted query terms",
"test_penalty is applied only when prefer_non_tests=true",
"layer priority is C3 > C1 > C2 > C0 for code retrieval"
]
}
}
-270
View File
@@ -1,270 +0,0 @@
# LLM Inventory
## Provider and SDK
- Provider in code: GigaChat / Sber
- Local SDK style: custom thin HTTP client over `requests`
- Core files:
- `app/modules/shared/gigachat/client.py`
- `app/modules/shared/gigachat/settings.py`
- `app/modules/shared/gigachat/token_provider.py`
- `app/modules/agent/llm/service.py`
There is no OpenAI SDK, Azure SDK, or local model runtime in the current implementation.
## Configuration
Model and endpoint configuration are read from environment in `GigaChatSettings.from_env()`:
- `GIGACHAT_AUTH_URL`
- default: `https://ngw.devices.sberbank.ru:9443/api/v2/oauth`
- `GIGACHAT_API_URL`
- default: `https://gigachat.devices.sberbank.ru/api/v1`
- `GIGACHAT_SCOPE`
- default: `GIGACHAT_API_PERS`
- `GIGACHAT_TOKEN`
- required for auth
- `GIGACHAT_SSL_VERIFY`
- default: `true`
- `GIGACHAT_MODEL`
- default: `GigaChat`
- `GIGACHAT_EMBEDDING_MODEL`
- default: `Embeddings`
- `AGENT_PROMPTS_DIR`
- optional prompt directory override
PostgreSQL config for retrieval storage is separate:
- `DATABASE_URL`
- default: `postgresql+psycopg://agent:agent@db:5432/agent`
## Default models
- Chat/completions model default: `GigaChat`
- Embedding model default: `Embeddings`
## Completion payload
Observed payload sent by `GigaChatClient.complete(...)`:
```json
{
"model": "GigaChat",
"messages": [
{"role": "system", "content": "<prompt template text>"},
{"role": "user", "content": "<runtime user input>"}
]
}
```
Endpoint:
- `POST {GIGACHAT_API_URL}/chat/completions`
Observed response handling:
- reads `choices[0].message.content`
- if no choices: returns empty string
## Embeddings payload
Observed payload sent by `GigaChatClient.embed(...)`:
```json
{
"model": "Embeddings",
"input": [
"<text1>",
"<text2>"
]
}
```
Endpoint:
- `POST {GIGACHAT_API_URL}/embeddings`
Observed response handling:
- expects `data` list
- maps each `item.embedding` to `list[float]`
## Parameters
### Explicitly implemented
- `model`
- `messages`
- `input`
- HTTP timeout:
- completions: `90s`
- embeddings: `90s`
- auth: `30s`
- TLS verification flag:
- `verify=settings.ssl_verify`
### Not implemented in payload
- `temperature`
- `top_p`
- `max_tokens`
- `response_format`
- tools/function calling
- streaming
- seed
- stop sequences
`ASSUMPTION:` the service uses provider defaults for sampling and output length because these fields are not sent in the request payload.
## Context and budget limits
There is no centralized token budget manager in the current code.
Observed practical limits instead:
- prompt file text is loaded as-is from disk
- user input is passed as-is
- RAG context shaping happens outside the LLM client
- docs indexing summary truncation:
- docs module catalog summary: `4000` chars
- docs policy text: `4000` chars
- project QA source bundle caps:
- top `12` rag items
- top `10` file candidates
- logging truncation only:
- LLM input/output logs capped at `1500` chars for logs
`ASSUMPTION:` there is no explicit max-context enforcement before chat completion requests. The current system relies on upstream graph logic to keep inputs small enough.
## Retry, backoff, timeout
### Timeouts
- auth: `30s`
- chat completion: `90s`
- embeddings: `90s`
### Retry
- Generic async retry wrapper exists in `app/modules/shared/retry_executor.py`
- It retries only:
- `TimeoutError`
- `ConnectionError`
- `OSError`
- Retry constants:
- `MAX_RETRIES = 5`
- backoff: `0.1 * attempt` seconds
### Important current limitation
- `GigaChatClient` raises `GigaChatError` on HTTP and request failures.
- `RetryExecutor` does not catch `GigaChatError`.
- Result: LLM and embeddings calls are effectively not retried by this generic retry helper unless errors are converted upstream.
## Prompt formation
Prompt loading is handled by `PromptLoader`:
- base dir: `app/modules/agent/prompts`
- override: `AGENT_PROMPTS_DIR`
- file naming convention: `<prompt_name>.txt`
Prompt composition model today:
- system prompt:
- full contents of selected prompt file
- user prompt:
- raw runtime input string passed by the caller
- no separate developer prompt layer in the application payload
If a prompt file is missing:
- fallback system prompt: `You are a helpful assistant.`
## Prompt templates present
- `router_intent`
- `general_answer`
- `project_answer`
- `docs_detect`
- `docs_strategy`
- `docs_plan_sections`
- `docs_generation`
- `docs_self_check`
- `docs_execution_summary`
- `project_edits_plan`
- `project_edits_hunks`
- `project_edits_self_check`
## Key LLM call entrypoints
### Composition roots
- `app/modules/agent/module.py`
- builds `GigaChatSettings`
- builds `GigaChatTokenProvider`
- builds `GigaChatClient`
- builds `PromptLoader`
- builds `AgentLlmService`
- `app/modules/rag_session/module.py`
- builds the same provider stack for embeddings used by RAG
### Main abstraction
- `AgentLlmService.generate(prompt_name, user_input, log_context=None)`
### Current generate callsites
- `app/modules/agent/engine/router/intent_classifier.py`
- `router_intent`
- `app/modules/agent/engine/graphs/base_graph.py`
- `general_answer`
- `app/modules/agent/engine/graphs/project_qa_graph.py`
- `project_answer`
- `app/modules/agent/engine/graphs/docs_graph_logic.py`
- `docs_detect`
- `docs_strategy`
- `docs_plan_sections`
- `docs_generation`
- `docs_self_check`
- `docs_execution_summary`-like usage via summary step
- `app/modules/agent/engine/graphs/project_edits_logic.py`
- `project_edits_plan`
- `project_edits_self_check`
- `project_edits_hunks`
## Logging and observability
`AgentLlmService` logs:
- input:
- `graph llm input: context=... prompt=... user_input=...`
- output:
- `graph llm output: context=... prompt=... output=...`
Log truncation:
- 1500 chars
RAG retrieval logs separately in `RagService`, but without embedding vectors.
## Integration with retrieval
There are two distinct GigaChat usages:
1. Chat/completion path for agent reasoning and generation
2. Embedding path for RAG indexing and retrieval
The embedding adapter is `GigaChatEmbedder`, used by:
- `app/modules/rag/services/rag_service.py`
## Notable limitations
- Single provider coupling: chat and embeddings both depend on GigaChat-specific endpoints.
- No model routing by scenario.
- No tool/function calling.
- No centralized prompt token budgeting.
- No explicit retry for `GigaChatError`.
- No streaming completions.
- No structured response mode beyond prompt conventions and downstream parsing.
@@ -1,13 +0,0 @@
| column | used_by | safe_to_drop | notes |
| --- | --- | --- | --- |
| `layer` | `USED_BY_CODE_V2`, `USED_BY_DOCS_INDEXING` | no | Core selector for C0-C3 and D1-D4 queries. |
| `title` | `USED_BY_CODE_V2`, `USED_BY_DOCS_INDEXING` | no | Used in lexical ranking and prompt evidence labels. |
| `metadata_json` | `USED_BY_CODE_V2`, `USED_BY_DOCS_INDEXING` | no | C2/C0 graph lookups and docs metadata depend on it. |
| `span_start`, `span_end` | `USED_BY_CODE_V2` | no | Needed for symbol-to-chunk resolution and locations. |
| `symbol_id`, `qname`, `kind`, `lang` | `USED_BY_CODE_V2` | no | Used by code indexing, ranking, trace building, and diagnostics. |
| `repo_id`, `commit_sha` | `USED_BY_CODE_V2`, `USED_BY_DOCS_INDEXING` | no | Used by indexing/cache and retained for provenance. |
| `entrypoint_type`, `framework` | `USED_BY_CODE_V2` | no | Used by C3 filtering and entrypoint diagnostics. |
| `doc_kind`, `module_id`, `section_path` | `USED_BY_DOCS_INDEXING` | no | Still written by docs indexing and covered by docs tests. |
| `artifact_type`, `section`, `doc_version`, `owner`, `system_component`, `last_modified`, `staleness_score` | `USED_BY_DOCS_INDEXING` | no | File metadata still flows through indexing/cache; left intact for now. |
| `rag_doc_id` | `UNUSED` | yes | Written into `rag_chunks` only; no reads in runtime/indexing code. |
| `links_json` | `UNUSED` | yes | Stored in `rag_chunks` only; reads exist for `rag_chunk_cache`, not `rag_chunks`. |
-31
View File
@@ -1,31 +0,0 @@
flowchart TD
A["HTTP: POST /internal/rag/retrieve"] --> B["RagModule.internal_router.retrieve(payload)"]
B --> C["RagService.retrieve(rag_session_id, query)"]
C --> D["RagQueryRouter.resolve_mode(query)"]
D --> E["RagQueryRouter.layers_for_mode(mode)"]
C --> F["GigaChatEmbedder.embed([query])"]
F --> G["GigaChatClient.embed(payload)"]
G --> H["POST /embeddings"]
C --> I["RagRepository.retrieve(...)"]
I --> J["RagQueryRepository.retrieve(...)"]
J --> K["PostgreSQL rag_chunks + pgvector"]
K --> L["ORDER BY lexical_rank, test_penalty, layer_rank, vector distance"]
L --> M["rows: path/content/layer/title/metadata/span/distance"]
M --> N["normalize to {source, content, layer, title, metadata, score}"]
N --> O["response: {items: [...]}"]
C --> P["embedding error?"]
P -->|yes| Q["RagRepository.fallback_chunks(...)"]
Q --> R["latest rows by id DESC"]
R --> N
C --> S["no rows and mode != docs?"]
S -->|yes| T["fallback to docs layers"]
T --> I
U["GraphAgentRuntime for project/qa"] --> V["ProjectQaRetrievalGraphFactory._retrieve_context"]
V --> C
V --> W["ProjectQaSupport.build_source_bundle(...)"]
W --> X["source_bundle"]
X --> Y["context_analysis"]
Y --> Z["answer_composition"]
-457
View File
@@ -1,457 +0,0 @@
# Retrieval Inventory
## Scope and method
This document describes the retrieval and indexing pipeline as implemented in code today. The inventory is based primarily on:
- `app/modules/rag/services/rag_service.py`
- `app/modules/rag/persistence/*.py`
- `app/modules/rag/indexing/code/**/*.py`
- `app/modules/rag/indexing/docs/**/*.py`
- `app/modules/rag_session/module.py`
- `app/modules/agent/engine/graphs/project_qa_step_graphs.py`
- `app/modules/agent/engine/orchestrator/*.py`
`ASSUMPTION:` the intended layer semantics are the ones implied by code and tests, not by future architecture plans. This matters because only `C0` through `C3` are materially implemented today; `C4+` exist only as enum constants.
## Current retrieval pipeline
1. Retrieval entrypoint is `POST /internal/rag/retrieve` in `app/modules/rag_session/module.py`.
2. The endpoint calls `RagService.retrieve(rag_session_id, query)`.
3. `RagQueryRouter` chooses `docs` or `code` mode from the raw query text.
4. `RagService` computes a single embedding for the full query via `GigaChatEmbedder`.
5. `RagQueryRepository.retrieve(...)` runs one SQL query against `rag_chunks` in PostgreSQL with `pgvector`.
6. Ranking order is:
- lexical rank
- test-file penalty
- layer rank
- vector distance `embedding <=> query_embedding`
7. Response items are normalized to `{source, content, layer, title, metadata, score}`.
8. If embeddings fail, retrieval falls back to latest chunks from the same layers.
9. If code retrieval returns nothing, service falls back to docs layers.
## Storage and indices
- Primary store: PostgreSQL from `DATABASE_URL`, configured in `app/modules/shared/db.py`.
- Vector extension: `CREATE EXTENSION IF NOT EXISTS vector` in `app/modules/rag/persistence/schema_repository.py`.
- Primary table: `rag_chunks`.
- Cache tables:
- `rag_blob_cache`
- `rag_chunk_cache`
- `rag_session_chunk_map`
- SQL indexes currently created:
- `(rag_session_id)`
- `(rag_session_id, layer)`
- `(rag_session_id, layer, path)`
- `(qname)`
- `(symbol_id)`
- `(module_id)`
- `(doc_kind)`
- `(entrypoint_type, framework)`
`ASSUMPTION:` there is no explicit ANN index for the vector column in schema code. The code creates general SQL indexes, but no `ivfflat`/`hnsw` index is defined here.
## Layer: C0_SOURCE_CHUNKS
### Implementation
- Produced by `CodeIndexingPipeline.index_file(...)` in `app/modules/rag/indexing/code/pipeline.py`.
- Chunking logic: `CodeTextChunker.chunk(...)` in `app/modules/rag/indexing/code/code_text/chunker.py`.
- Document builder: `CodeTextDocumentBuilder.build(...)` in `app/modules/rag/indexing/code/code_text/document_builder.py`.
- Persisted via `RagDocumentRepository.insert_documents(...)` into `rag_chunks`.
### Input contract
This is an indexing layer, not a direct public retriever. The observed upstream indexing input is a file dict with at least:
- required:
- `path: str`
- `content: str`
- optional:
- `commit_sha: str | None`
- `content_hash: str`
- metadata fields copied through by `RagService._document_metadata(...)`
For retrieval, the layer is queried only indirectly through:
- `rag_session_id: str`
- `query: str`
- inferred mode/layers from `RagQueryRouter`
- fixed `limit=8`
### Output contract
Stored document shape:
- top-level:
- `layer = "C0_SOURCE_CHUNKS"`
- `lang = "python"`
- `source.repo_id`
- `source.commit_sha`
- `source.path`
- `title`
- `text`
- `span.start_line`
- `span.end_line`
- `embedding`
- metadata:
- `chunk_index`
- `chunk_type`: `symbol_block` or `window`
- `module_or_unit`
- `artifact_type = "CODE"`
- plus file-level metadata injected by `RagService`
Returned retrieval item shape:
- `source`
- `content`
- `layer`
- `title`
- `metadata`
- `score`
No `line_start` / `line_end` are returned to the caller directly; they remain in DB columns `span_start` / `span_end` and are only used in logs.
### Defaults & limits
- AST chunking prefers one chunk per top-level class/function/async function.
- Fallback window chunking:
- `size = 80` lines
- `overlap = 15` lines
- Global retrieval limit from `RagService.retrieve(...)`: `8`
- Embedding batch size from env:
- `RAG_EMBED_BATCH_SIZE`
- default `16`
### Known issues
- Nested methods/functions are not emitted as C0 chunks unless represented inside a selected top-level block.
- Returned API payload omits line spans even though storage has them.
- No direct filter by path, namespace, symbol, or `top_k` is exposed through the current endpoint.
## Layer: C1_SYMBOL_CATALOG
### Implementation
- Symbol extraction: `SymbolExtractor.extract(...)` in `app/modules/rag/indexing/code/symbols/extractor.py`.
- AST parsing: `PythonAstParser.parse_module(...)`.
- Document builder: `SymbolDocumentBuilder.build(...)`.
- Retrieval reads rows from `rag_chunks`; there is no dedicated symbol table.
### Input contract
Indexing input is the same per-file payload as C0.
Observed symbol extraction source:
- Python AST only
- supported symbol kinds:
- `class`
- `function`
- `method`
- `const` for top-level imports/import aliases
Retrieval input is still the generic text query endpoint. Query terms are enriched by `extract_query_terms(...)`:
- extracts identifier-like tokens from query text
- normalizes camelCase/PascalCase to snake_case
- adds special intent terms for management/control-related queries
- max observed query terms: `6`
### Output contract
Stored document shape:
- top-level:
- `layer = "C1_SYMBOL_CATALOG"`
- `title = qname`
- `text = "<kind> <qname>\n<signature>\n<docstring?>"`
- `span.start_line`
- `span.end_line`
- metadata:
- `symbol_id`
- `qname`
- `kind`
- `signature`
- `decorators_or_annotations`
- `docstring_or_javadoc`
- `parent_symbol_id`
- `package_or_module`
- `is_entry_candidate`
- `lang_payload`
- `artifact_type = "CODE"`
Observed `lang_payload` variants:
- class:
- `bases`
- function/method:
- `async`
- import alias:
- `imported_from`
- `import_alias`
### Defaults & limits
- Only Python source files are indexed into C-layers.
- Import and import-from declarations are materialized as `const` symbols only at module top level.
- Retrieval ranking gives C1 priority rank `1`, after C3 and before C2/C0.
### Known issues
- No explicit visibility/public-private model.
- `parent_symbol_id` currently stores the parent qname string from the stack, not the parent symbol hash. This is an observed implementation detail.
- Cross-file symbol resolution is not implemented; `dst_symbol_id` in edges resolves only against symbols extracted from the same file.
## Layer: C2_DEPENDENCY_GRAPH
### Implementation
- Edge extraction: `EdgeExtractor.extract(...)` in `app/modules/rag/indexing/code/edges/extractor.py`.
- Document builder: `EdgeDocumentBuilder.build(...)`.
- Built during `CodeIndexingPipeline.index_file(...)`.
### Input contract
Indexing input is the same per-file source payload as C0/C1.
Graph construction method:
- static analysis only
- Python AST walk only
- no runtime tracing
- no tree-sitter
Observed edge types:
- `calls`
- `imports`
- `inherits`
### Output contract
Stored document shape:
- top-level:
- `layer = "C2_DEPENDENCY_GRAPH"`
- `title = "<src_qname>:<edge_type>"`
- `text = "<src_qname> <edge_type> <dst>"`
- `span.start_line`
- `span.end_line`
- `links` contains one evidence link of type `EDGE`
- metadata:
- `edge_id`
- `edge_type`
- `src_symbol_id`
- `src_qname`
- `dst_symbol_id`
- `dst_ref`
- `resolution`: `resolved` or `partial`
- `lang_payload`
- `artifact_type = "CODE"`
Observed `lang_payload` usage:
- for calls: may include `callsite_kind = "function_call"`
### Defaults & limits
- Edge extraction is per-file only.
- `imports` edges are emitted only while visiting a class/function scope; top-level imports do not become C2 edges.
- Layer rank in retrieval SQL: `2`
### Known issues
- There is no traversal API, graph repository, or query language over C2. Retrieval only treats edges as text/vector rows in `rag_chunks`.
- Destination resolution is local to the file-level qname map.
- Top-level module import relationships are incompletely represented because `visit_Import` / `visit_ImportFrom` skip when there is no current scope.
## Layer: C3_ENTRYPOINTS
### Implementation
- Detection registry: `EntrypointDetectorRegistry.detect_all(...)`.
- Detectors:
- `FastApiEntrypointDetector`
- `FlaskEntrypointDetector`
- `TyperClickEntrypointDetector`
- Document builder: `EntrypointDocumentBuilder.build(...)`.
### Input contract
Indexing input is the same per-file source payload as other C-layers.
Detected entrypoint families today:
- HTTP:
- FastAPI decorators such as `.get`, `.post`, `.put`, `.patch`, `.delete`, `.route`
- Flask `.route`
- CLI:
- Typer/Click `.command`
- Typer/Click `.callback`
Not detected:
- Django routes
- Celery tasks
- RQ jobs
- cron jobs / scheduler entries
### Output contract
Stored document shape:
- top-level:
- `layer = "C3_ENTRYPOINTS"`
- `title = route_or_command`
- `text = "<framework> <entry_type> <route_or_command>"`
- `span.start_line`
- `span.end_line`
- `links` contains one evidence link of type `CODE_SPAN`
- metadata:
- `entry_id`
- `entry_type`: observed `http` or `cli`
- `framework`: observed `fastapi`, `flask`, `typer`, `click`
- `route_or_command`
- `handler_symbol_id`
- `lang_payload`
- `artifact_type = "CODE"`
FastAPI-specific observed payload:
- `lang_payload.methods = [HTTP_METHOD]` for `.get/.post/...`
### Defaults & limits
- Retrieval layer rank: `0` highest among code layers.
- Entrypoint mapping is handler-symbol centric:
- decorator match -> symbol -> `handler_symbol_id`
- physical location comes from symbol span
### Known issues
- Route parsing is string-based from decorator text, not semantic AST argument parsing.
- No dedicated entrypoint tags beyond `entry_type`, `framework`, and raw decorator-derived payload.
- Background jobs and non-decorator entrypoints are not indexed.
## Dependency graph / trace current state
### Exists or stub?
- C2 exists and is populated.
- It is not a stub.
- It is also not a full-project dependency graph service; it is a set of per-edge documents stored in `rag_chunks`.
### How the graph is built
- static Python AST analysis
- no runtime instrumentation
- no import graph resolver across modules
- no tree-sitter
### Edge types in data
- `calls`
- `imports`
- `inherits`
### Traversal API
- No traversal API was found in `app/modules/rag/*` or `app/modules/agent/*`.
- No method accepts graph traversal parameters such as depth, start node, edge filters, or BFS/DFS strategy.
- Current access path is only retrieval over indexed edge documents.
## Entrypoints current state
### Implemented extraction
- HTTP routes:
- FastAPI
- Flask
- CLI:
- Typer
- Click
### Mapping model
- `entrypoint -> handler_symbol_id -> symbol span/path`
- The entrypoint record itself stores:
- framework
- entry type
- raw route/command string
- handler symbol id
### Tags/types
- `entry_type` is the main normalized tag.
- Observed values: `http`, `cli`.
- `framework` is the second discriminator.
- There are no richer endpoint taxonomies such as `job`, `worker`, `webhook`, `scheduler`.
## Defaults and operational limits
- Query mode default: `docs`
- Code mode is enabled by keyword heuristics in `RagQueryRouter`
- Retrieval hard limit: `8`
- Fallback limit: `8`
- Query term extraction limit: `6`
- Ranked source bundle for project QA:
- top `12` RAG items
- top `10` file candidates
- No exposed `namespace`, `path_prefixes`, `top_k`, `max_chars`, `max_chunks`, `max_depth` in the public/internal retrieval endpoint
`ASSUMPTION:` the absence of these controls in endpoint and service signatures means they are not part of the current supported contract, even though `RagQueryRepository.retrieve(...)` has an internal `path_prefixes` parameter.
## Known cross-cutting issues
- Retrieval contract is effectively text-only at API level; structured retrieval exists only as internal SQL parameters.
- Response payload drops explicit line spans even though spans are stored.
- Vector retrieval is coupled to a single provider-specific embedder.
- Docs mode is the default, so code retrieval depends on heuristic query phrasing unless the project/qa graph prepends `по коду`.
- There is no separate retrieval contract per layer exposed over API; all layer selection is implicit.
## Where to plug ExplainPack pipeline
### Option 1: replace or extend `project_qa/context_analysis`
- Code location:
- `app/modules/agent/engine/graphs/project_qa_step_graphs.py`
- Why:
- retrieval is already complete at this step
- input bundle already contains ranked `rag_items` and `file_candidates`
- output is already a structured `analysis_brief`
- Risk:
- low
- minimal invasion if ExplainPack consumes `source_bundle` and emits the same `analysis_brief` shape
### Option 2: insert a new orchestrator step between `context_retrieval` and `context_analysis`
- Code location:
- `app/modules/agent/engine/orchestrator/template_registry.py`
- `app/modules/agent/engine/orchestrator/step_registry.py`
- Why:
- preserves current retrieval behavior
- makes ExplainPack an explicit pipeline stage with its own artifact
- cleanest for observability and future A/B migration
- Risk:
- low to medium
- requires one new artifact contract and one extra orchestration step, but no change to retrieval storage
### Option 3: introduce ExplainPack inside `ExplainActions.extract_logic`
- Code location:
- `app/modules/agent/engine/orchestrator/actions/explain_actions.py`
- Why:
- useful if ExplainPack is meant only for explain-style scenarios
- keeps general project QA untouched
- Risk:
- medium
- narrower integration point; may create duplicate reasoning logic separate from project QA analysis path
## Bottom line
- C0-C3 are implemented and persisted in one physical store: `rag_chunks`.
- Retrieval is a hybrid SQL ranking over lexical heuristics plus pgvector distance.
- C2 exists, but only as retrievable edge documents, not as a traversable graph subsystem.
- C3 covers FastAPI/Flask/Typer/Click only.
- The least invasive ExplainPack integration point is after retrieval and before answer composition, preferably as a new explicit orchestrator artifact or as a replacement for `context_analysis`.
@@ -0,0 +1,168 @@
---
id: api-rag-session-changes
title: Применение изменений к RAG-сессии
doc_type: api_method
domain: rag
status: draft
owner: system-analyst
source_of_truth: code
related_docs:
- arch-rag-package
- logic-rag-indexing
- entity-rag-session
- entity-rag-index-job
related_code:
- src/app/modules/rag/module.py
- src/app/schemas/rag_sessions.py
- src/app/schemas/indexing.py
entities:
- RagSession
- IndexJob
tags:
- rag
- api
- changes
- incremental-indexing
---
# Применение изменений к RAG-сессии
## Summary
- Purpose: поставить incremental indexing для уже существующей `RagSession`.
- Actor: внешний клиент модуля RAG.
- Trigger: частичное обновление индекса после изменения файлов.
- Endpoint: `POST /api/rag/sessions/{rag_session_id}/changes`
- Main entities: `RagSession`, `IndexJob`.
- Main logic: проверка существования сессии, создание change job, асинхронная обработка `upsert`/`delete`.
- Main errors: `not_found` для отсутствующей сессии, `422` для некорректного payload.
- Source of truth: `src/app/modules/rag/module.py`, `src/app/schemas/rag_sessions.py`.
## Назначение
Метод позволяет обновить индекс без полной переиндексации проекта. Он принимает только изменённые файлы и операции удаления.
## Контекст
Endpoint относится к новому API с явной работой через `rag_session_id`. В отличие от legacy `/api/index/changes`, он не создаёт сессию молча и требует, чтобы она уже существовала.
## Технический use case
### Основной сценарий
1. Клиент передаёт `rag_session_id` в path и список `changed_files` в body.
2. Endpoint проверяет наличие сессии через `RagSessionStore.get`.
3. При успехе `IndexingOrchestrator.enqueue_changes` создаёт новую job и запускает фоновое применение изменений.
4. API возвращает `index_job_id` и стартовый статус.
### Альтернативные ветки
- Если `rag_session_id` не найдена, endpoint бросает `AppError("not_found", ...)`.
- Для `op=delete` в последующей логике происходит удаление документов по пути без повторной генерации embeddings.
## Функциональные требования
### Request validation
- Path parameter `rag_session_id` обязателен.
- `changed_files` обязателен и состоит из элементов `ChangedFile`.
- Для каждого элемента обязательны `op` и `path`.
- `op` допускает только `upsert` или `delete`.
### Processing rules
- Сессия должна существовать до постановки change job.
- Каждый вызов создаёт новый `IndexJob`.
- Фактическое применение изменений выполняется асинхронно.
### State changes
- В `rag_index_jobs` появляется новая задача.
- Сам индекс меняется позже, внутри `RagService.index_changes`.
### Side effects
- Публикация job events.
- Удаление документов по `delete_paths` и upsert новых документов в фоне.
## Contract
### Endpoint
- Method: `POST`
- Path: `/api/rag/sessions/{rag_session_id}/changes`
- Auth: определяется внешним слоем приложения.
- Idempotent: нет, повторный вызов создаёт новую job.
- Timeout: короткий, endpoint не дожидается завершения индексации.
- Retry: только если клиент готов к созданию дополнительной job.
### Request
| Field | Type | Required | Constraints | Description |
|------|------|----------|-------------|-------------|
| `rag_session_id` | `string` | yes | path param, non-empty | идентификатор существующей RAG-сессии |
| `changed_files` | `array<ChangedFile>` | yes | схема каждого элемента обязательна | изменения файлов |
| `changed_files[].op` | `enum` | yes | `upsert` or `delete` | тип операции |
| `changed_files[].path` | `string` | yes | `min_length=1` | путь файла |
| `changed_files[].content` | `string \| null` | no | нужен для `upsert` | содержимое файла |
| `changed_files[].content_hash` | `string \| null` | no | повышает cache reuse | hash содержимого |
### Response
| Field | Type | Description |
|------|------|-------------|
| `index_job_id` | `string` | идентификатор фоновой задачи |
| `status` | `string` | стартовый статус задачи |
### External contract refs
- OpenAPI: формируется FastAPI по `response_model=IndexJobQueuedResponse`.
- Schema: `RagSessionChangesRequest`, `ChangedFile`, `IndexJobQueuedResponse`.
- DTO / serializer: `src/app/schemas/rag_sessions.py`, `src/app/schemas/indexing.py`.
- Additional refs: `logic-rag-indexing`.
## Errors
| error_id | http_code | when | client_behavior | retry |
|----------|-----------|------|-----------------|-------|
| `not_found` | `404` | `rag_session_id` отсутствует | создать новую сессию или исправить id | no |
| `validation_error` | `422` | нарушена схема request | исправить payload | no |
## Нефункциональные требования
### Security
- Метод доверяет внешнему слою авторизации.
### Observability
- Logs: прямое логирование endpoint отсутствует.
- Metrics: нет отдельной метрики на уровне метода.
- Traces: отсутствуют.
- Audit: каждая операция материализуется в `IndexJob`.
### Reliability
- Проверка существования сессии защищает от случайной записи в неинициализированный scope.
- Ошибки индексации доступны через job status и SSE events.
### Performance
- Быстрый ответ за счёт фонового выполнения.
## Связанные блоки логики
- `logic-rag-indexing`
## Связанные сущности
- `RagSession`
- `IndexJob`
## Связанный код
### Files
- `src/app/modules/rag/module.py`
- `src/app/schemas/rag_sessions.py`
- `src/app/schemas/indexing.py`
### Symbols
- `RagModule.public_router.rag_session_changes`
- `RagSessionStore.get`
- `IndexingOrchestrator.enqueue_changes`
## Связанные документы
- `arch-rag-package`
- `logic-rag-indexing`
- `entity-rag-session`
- `entity-rag-index-job`
## История изменений
| Date | Source | Changes |
|------|--------|---------|
| 2026-03-13 | code | Задокументирован публичный endpoint incremental indexing для существующей сессии. |
@@ -0,0 +1,166 @@
---
id: api-rag-session-create
title: Создание RAG-сессии и запуск snapshot-индексации
doc_type: api_method
domain: rag
status: draft
owner: system-analyst
source_of_truth: code
related_docs:
- arch-rag-package
- logic-rag-indexing
- entity-rag-session
- entity-rag-index-job
related_code:
- src/app/modules/rag/module.py
- src/app/schemas/rag_sessions.py
- src/app/schemas/indexing.py
entities:
- RagSession
- IndexJob
tags:
- rag
- api
- session
- snapshot
---
# Создание RAG-сессии и запуск snapshot-индексации
## Summary
- Purpose: создать новую `RagSession` и асинхронно поставить полную индексацию snapshot-файлов.
- Actor: внешний клиент модуля RAG.
- Trigger: первичная загрузка файлов проекта в индекс.
- Endpoint: `POST /api/rag/sessions`
- Main entities: `RagSession`, `IndexJob`.
- Main logic: создание UUID-сессии, постановка snapshot job, возврат идентификаторов сессии и job.
- Main errors: в коде endpoint нет собственной бизнес-валидации сверх pydantic; ошибки индексации проявляются позже в job status.
- Source of truth: `src/app/modules/rag/module.py`, `src/app/schemas/rag_sessions.py`.
## Назначение
Метод открывает новую RAG-сессию и запускает первичную индексацию файлов. Он используется как основной публичный вход для нового API пакета `rag`.
## Контекст
В отличие от legacy `/api/index/snapshot`, этот endpoint всегда создаёт новый `rag_session_id`, что позволяет независимо хранить несколько снимков одного проекта.
## Технический use case
### Основной сценарий
1. Клиент передаёт `project_id` и массив `files`.
2. `RagSessionStore.create` создаёт новую запись в `rag_sessions`.
3. `IndexingOrchestrator.enqueue_snapshot` создаёт `IndexJob` и запускает фоновую обработку.
4. API сразу возвращает `rag_session_id`, `index_job_id` и стартовый статус.
### Альтернативные ветки
- Если часть файлов не подлежит индексации, они будут отфильтрованы уже внутри indexing pipeline, а не на этапе ответа API.
- Ошибки индексации не меняют синхронный ответ create endpoint, а отражаются в последующем статусе job.
## Функциональные требования
### Request validation
- `project_id` обязателен и не может быть пустым.
- `files` передаются списком объектов `FileSnapshot`.
- Для каждого файла обязательны `path`, `content`, `content_hash`.
### Processing rules
- На каждый вызов создаётся новая `RagSession`.
- Snapshot job создаётся сразу после сохранения сессии.
- Ответ не ждёт завершения индексации.
### State changes
- В `rag_sessions` появляется новая запись.
- В `rag_index_jobs` появляется новая запись в статусе `queued`.
### Side effects
- Запуск фоновой `asyncio` task.
- Последующая публикация progress events в EventBus.
## Contract
### Endpoint
- Method: `POST`
- Path: `/api/rag/sessions`
- Auth: определяется внешним слоем приложения, внутри endpoint не задана.
- Idempotent: нет.
- Timeout: короткий, так как endpoint не ждёт индексацию.
- Retry: допустим только на стороне клиента с пониманием, что будет создана новая сессия.
### Request
| Field | Type | Required | Constraints | Description |
|------|------|----------|-------------|-------------|
| `project_id` | `string` | yes | `min_length=1` | идентификатор проекта |
| `files` | `array<FileSnapshot>` | yes | может быть пустым, но схема обязана соблюдаться | snapshot файлов для первичной индексации |
| `files[].path` | `string` | yes | `min_length=1` | путь файла |
| `files[].content` | `string` | yes | без дополнительных ограничений | содержимое файла |
| `files[].content_hash` | `string` | yes | `min_length=1` | hash содержимого для cache reuse |
### Response
| Field | Type | Description |
|------|------|-------------|
| `rag_session_id` | `string` | идентификатор созданной сессии |
| `index_job_id` | `string` | идентификатор фоновой задачи индексации |
| `status` | `IndexJobStatus` | стартовый статус задачи, обычно `queued` |
### External contract refs
- OpenAPI: формируется FastAPI по `response_model=RagSessionCreateResponse`.
- Schema: `RagSessionCreateRequest`, `RagSessionCreateResponse`.
- DTO / serializer: `src/app/schemas/rag_sessions.py`, `src/app/schemas/indexing.py`.
- Additional refs: `logic-rag-indexing`.
## Errors
| error_id | http_code | when | client_behavior | retry |
|----------|-----------|------|-----------------|-------|
| `validation_error` | `422` | нарушена pydantic-схема request | исправить payload | no |
## Нефункциональные требования
### Security
- Авторизация и аутентификация находятся вне этого метода.
### Observability
- Logs: прямое логирование в endpoint отсутствует.
- Metrics: отдельные API-метрики не выделены.
- Traces: отсутствуют.
- Audit: факт вызова материализуется через `RagSession` и `IndexJob`.
### Reliability
- Даже при дальнейшей ошибке индексации клиент может получить статус через job endpoint.
- Фоновая задача создаётся немедленно после ответа.
### Performance
- Время ответа не зависит от размера snapshot, кроме времени сериализации request.
## Связанные блоки логики
- `logic-rag-indexing`
## Связанные сущности
- `RagSession`
- `IndexJob`
## Связанный код
### Files
- `src/app/modules/rag/module.py`
- `src/app/schemas/rag_sessions.py`
- `src/app/schemas/indexing.py`
### Symbols
- `RagModule.public_router.create_rag_session`
- `RagSessionStore.create`
- `IndexingOrchestrator.enqueue_snapshot`
## Связанные документы
- `arch-rag-package`
- `logic-rag-indexing`
- `entity-rag-session`
- `entity-rag-index-job`
## История изменений
| Date | Source | Changes |
|------|--------|---------|
| 2026-03-13 | code | Задокументирован публичный endpoint создания RAG-сессии. |
@@ -0,0 +1,166 @@
---
id: api-rag-session-job
title: Получение статуса и событий задачи индексации
doc_type: api_method
domain: rag
status: draft
owner: system-analyst
source_of_truth: code
related_docs:
- arch-rag-package
- entity-rag-session
- entity-rag-index-job
related_code:
- src/app/modules/rag/module.py
- src/app/modules/rag/job_store.py
- src/app/schemas/rag_sessions.py
entities:
- RagSession
- IndexJob
tags:
- rag
- api
- job-status
- sse
---
# Получение статуса и событий задачи индексации
## Summary
- Purpose: отдать текущее состояние job и поток событий её выполнения в рамках конкретной `RagSession`.
- Actor: внешний клиент модуля RAG.
- Trigger: polling или live-monitoring после запуска snapshot/change indexing.
- Endpoint: `GET /api/rag/sessions/{rag_session_id}/jobs/{index_job_id}` и `GET /api/rag/sessions/{rag_session_id}/jobs/{index_job_id}/events`
- Main entities: `RagSession`, `IndexJob`.
- Main logic: чтение job по id, проверка принадлежности сессии, возврат status payload или SSE stream.
- Main errors: `not_found` при отсутствии job или несовпадении `rag_session_id`.
- Source of truth: `src/app/modules/rag/module.py`, `src/app/modules/rag/job_store.py`.
## Назначение
Документ описывает два связанных метода наблюдения: синхронный status endpoint и потоковый SSE endpoint. Оба работают поверх одной сущности `IndexJob`.
## Контекст
Create и changes endpoints возвращают только стартовый статус задачи, поэтому клиенту нужны отдельные методы для отслеживания выполнения. SSE-поток даёт live progress, а status endpoint нужен для простого polling.
## Технический use case
### Основной сценарий
1. Клиент вызывает status endpoint или открывает SSE stream по `index_job_id`.
2. Endpoint читает job из `IndexJobStore`.
3. Если job отсутствует или принадлежит другой `rag_session_id`, возвращается `not_found`.
4. Status endpoint отдаёт снимок counters и error payload.
5. SSE endpoint подписывается на `EventBus` c `replay=True` и транслирует `index_status`, `index_progress`, `terminal`.
### Альтернативные ветки
- При отсутствии новых событий SSE endpoint каждые 10 секунд отправляет `: keepalive`.
- После события `terminal` поток завершается и отписывается от EventBus.
## Функциональные требования
### Request validation
- `rag_session_id` и `index_job_id` обязательны как path parameters.
- Job должна существовать и принадлежать переданной сессии.
### Processing rules
- Status endpoint не подписывается на события и читает только текущее состояние job.
- SSE endpoint использует `replay=True`, чтобы клиент получил уже опубликованные события.
- Оба метода защищают от доступа к job другой сессии.
### State changes
- Методы не меняют состояние job.
### Side effects
- SSE endpoint создаёт временную подписку на EventBus.
- При завершении или разрыве соединения выполняется `unsubscribe`.
## Contract
### Endpoint
- Method: `GET`
- Path: `/api/rag/sessions/{rag_session_id}/jobs/{index_job_id}` и `/api/rag/sessions/{rag_session_id}/jobs/{index_job_id}/events`
- Auth: определяется внешним слоем приложения.
- Idempotent: да.
- Timeout: status endpoint короткий; SSE stream долгоживущий.
- Retry: polling можно повторять безопасно; SSE можно переподключать.
### Request
| Field | Type | Required | Constraints | Description |
|------|------|----------|-------------|-------------|
| `rag_session_id` | `string` | yes | path param | идентификатор сессии |
| `index_job_id` | `string` | yes | path param | идентификатор задачи |
### Response
| Field | Type | Description |
|------|------|-------------|
| `rag_session_id` | `string` | идентификатор сессии, только для status endpoint |
| `index_job_id` | `string` | идентификатор задачи |
| `status` | `IndexJobStatus` | текущее состояние job |
| `indexed_files` | `integer` | число успешно обработанных файлов |
| `failed_files` | `integer` | число файлов с ошибками |
| `cache_hit_files` | `integer` | число cache hit |
| `cache_miss_files` | `integer` | число cache miss |
| `error` | `object \| null` | ошибка, если job завершилась с `error` |
### External contract refs
- OpenAPI: status endpoint использует `response_model=RagSessionJobResponse`; SSE endpoint отдаёт `text/event-stream`.
- Schema: `RagSessionJobResponse`.
- DTO / serializer: `src/app/schemas/rag_sessions.py`.
- Additional refs: `entity-rag-index-job`.
## Errors
| error_id | http_code | when | client_behavior | retry |
|----------|-----------|------|-----------------|-------|
| `not_found` | `404` | job отсутствует или не принадлежит переданной сессии | проверить id или создать новую задачу | no |
## Нефункциональные требования
### Security
- Проверка `job.rag_session_id == rag_session_id` обязательна для обоих методов.
### Observability
- Logs: отдельные логи чтения статуса не реализованы.
- Metrics: отсутствуют.
- Traces: отсутствуют.
- Audit: история job хранится в `rag_index_jobs`, поток событий в памяти EventBus.
### Reliability
- SSE heartbeat удерживает соединение активным.
- `finally` блок гарантирует `unsubscribe`.
### Performance
- Status endpoint работает как лёгкий запрос к БД.
- SSE stream масштабируется числом активных подписчиков и объёмом событий.
## Связанные блоки логики
- `logic-rag-indexing`
## Связанные сущности
- `RagSession`
- `IndexJob`
## Связанный код
### Files
- `src/app/modules/rag/module.py`
- `src/app/modules/rag/job_store.py`
- `src/app/schemas/rag_sessions.py`
### Symbols
- `RagModule.public_router.rag_session_job`
- `RagModule.public_router.rag_session_job_events`
- `IndexJobStore.get`
## Связанные документы
- `arch-rag-package`
- `entity-rag-session`
- `entity-rag-index-job`
## История изменений
| Date | Source | Changes |
|------|--------|---------|
| 2026-03-13 | code | Задокументированы status и SSE endpoints для наблюдения за indexing job. |
@@ -0,0 +1,214 @@
---
## id: arch-rag-package
title: Пакет RAG
doc_type: architecture_overview
domain: rag
status: draft
owner: system-analyst
source_of_truth: code
related_docs:
- logic-rag-indexing
- logic-rag-retrieval
- entity-rag-session
- entity-rag-index-job
- api-rag-session-create
- api-rag-session-changes
- api-rag-session-job
related_code:
- src/app/modules/rag/module.py
- src/app/modules/rag/services/rag_service.py
- src/app/modules/rag/indexing_service.py
- src/app/modules/rag/persistence/repository.py
- src/app/modules/rag/persistence/schema_repository.py
entities:
- RagSession
- IndexJob
- RagDocument
tags:
- rag
- indexing
- retrieval
- architecture
# Пакет RAG
## Summary
- Scope: модуль индексации проектных файлов, хранения RAG-слоёв и выдачи retrieval-контекста.
- Purpose: построить индекс по документации и Python-коду и дать runtime доступ к релевантным фрагментам.
- 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}`, `/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/*`.
## Назначение
Пакет `rag` отвечает за полный цикл подготовки retrieval-контекста для проекта: принимает снапшоты и изменения файлов, преобразует их в набор атомарных `RagDocument`, векторизует, сохраняет в БД и предоставляет доступ к индексированным данным другим частям системы.
## Контекст
Модуль используется как инфраструктурный слой для agent/runtime. На вход он принимает snapshot и изменения файлов проекта. На выходе формирует устойчивый индекс, ассоциированный с `rag_session_id`, и статус задач индексации, пригодный для опроса и SSE-подписки.
## Границы системы
### In scope
- Создание и хранение `RagSession`.
- Постановка и выполнение задач snapshot/change indexing.
- Индексация markdown-документации в слои `D1-D4`.
- Индексация Python-кода в слои `C0-C4`.
- Кэширование по `repo_id + blob_sha`.
- Сохранение retrieval-документов в `rag_chunks`.
- Выдача статуса задач и событий прогресса.
- Нормализация webhook Gitea/Bitbucket и связывание коммитов со story.
### Out of scope
- Финальная генерация ответа пользователю.
- Оркестрация LLM-диалога.
- Управление git-репозиторием и загрузка файлов из внешних источников.
- Политики маршрутизации intent/runtime вне собственного persistence/retrieval API.
## Архитектурная схема
`RagModule` собирает зависимости модуля и публикует HTTP endpoints. Для индексации он использует `RagSessionStore`, `IndexJobStore`, `IndexingOrchestrator` и `RagService`. `RagService` выбирает docs/code pipeline, обогащает документы метаданными файла, запрашивает embeddings и записывает результат через `RagRepository`. `RagRepository` агрегирует schema/session/job/document/cache/query репозитории.
## Основные модули
| module | responsibility | depends_on | key_code_refs |
| ---------------------- | -------------------------------------------------------- | ------------------------------------------------------------------------ | ----------------------------------------------- |
| `RagModule` | сборка зависимостей, публичный и internal API | `RagService`, `IndexingOrchestrator`, `RagSessionStore`, `IndexJobStore` | `src/app/modules/rag/module.py` |
| `RagService` | синхронная бизнес-логика индексации файлов и cache reuse | docs/code pipeline, embedder, `RagRepository` | `src/app/modules/rag/services/rag_service.py` |
| `IndexingOrchestrator` | асинхронный job lifecycle, retry, project lock, EventBus | `IndexJobStore`, `RagIndexer`, `EventBus`, `RetryExecutor` | `src/app/modules/rag/indexing_service.py` |
| `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` |
## Основные доменные области
- RAG session как граница индекса конкретного проекта или его временного снапшота.
- Index job как жизненный цикл асинхронной индексации и канал наблюдения за прогрессом.
- RagDocument как атом индекса, который попадает в retrieval-хранилище и в cache.
- Repo webhook context как источник commit metadata для story и cache.
## Основные интеграции
| integration | direction | purpose | protocol / transport | related_docs |
| ------------------------ | --------- | --------------------------------------------------- | ---------------------------------- | -------------------------------------------------------------------------- |
| 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 | публичный 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` |
## Основные потоки
### Flow 1
1. Клиент вызывает `POST /api/rag/sessions` с `project_id` и snapshot файлов.
2. `RagSessionStore` создаёт `rag_session_id`, а `IndexingOrchestrator` создаёт `IndexJob`.
3. `RagService` фильтрует файлы, переиспользует cache по `blob_sha` или строит docs/code документы заново.
4. Документы векторизуются, записываются в `rag_chunks`, а job получает финальный статус `done` или `error`.
### Flow 2
1. Клиент вызывает `POST /api/rag/sessions/{rag_session_id}/changes`.
2. `IndexingOrchestrator` сериализует обработку по `rag_session_id`.
3. `RagService` удаляет документы по `delete_paths`, пересобирает upsert-файлы и применяет изменения к индексу.
4. Клиент читает статус и события задачи через job endpoints.
## Архитектурные решения и ограничения
### Key decisions
- Snapshot и incremental indexing используют один и тот же `RagService`, различаясь только стратегией записи.
- Кэш документов привязан к `repo_id + blob_sha`, а не к `rag_session_id`, что позволяет переиспользовать embeddings между сессиями одного проекта.
- Документация и код индексируются разными pipeline, но сохраняются в общую таблицу `rag_chunks`.
- Асинхронность вынесена в `IndexingOrchestrator`, чтобы `RagService` оставался application-service без управления job lifecycle.
### Constraints
- Code indexing поддерживает только Python-файлы.
- Docs indexing ориентирован на markdown и frontmatter YAML.
- HTTP retrieval endpoint в модуле не публикуется.
- Реальное retrieval API доступно через repository/runtime adapters, а не через публичный HTTP endpoint модуля.
### Risks
- Ошибки embeddings или временные сетевые сбои переводят job в `error` только после исчерпания retry.
- Полное `replace_documents` для snapshot удаляет все документы сессии перед вставкой новых.
- Retrieval ranking завязан на SQL-эвристики по layer, lexical match и metadata, поэтому качество зависит от корректности metadata builders.
## Нефункциональные аспекты
### Security
- Публичные endpoints не содержат собственной бизнес-авторизации внутри модуля и полагаются на внешний слой приложения.
### Reliability
- Проектный `asyncio.Lock` предотвращает параллельную индексацию одной `rag_session`.
- `RetryExecutor` повторяет временные сбои индексации.
### Observability
- Logs: `RagService` пишет предупреждения по cache hit/miss и skipped files.
- Metrics: явные метрики не выделены.
- Traces: явная трассировка не реализована.
- Audit: job status сохраняется в БД.
### Performance
- Embeddings отправляются батчами с размером из `RAG_EMBED_BATCH_SIZE`.
- Cache reuse исключает повторную векторизацию неизменённых blob.
### Scalability
- Индекс хранится на уровне SQL-таблиц с векторными полями и индексами по session/layer/path.
- При росте объёма данных узким местом остаются полнотабличные delete/insert по snapshot и SQL sorting retrieval.
## Связанные сущности
- `RagSession`
- `IndexJob`
- `RagDocument`
## Связанный код
### Files
- `src/app/modules/rag/module.py`
- `src/app/modules/rag/services/rag_service.py`
- `src/app/modules/rag/indexing_service.py`
- `src/app/modules/rag/persistence/repository.py`
- `src/app/modules/rag/persistence/schema_repository.py`
### Symbols
- `RagModule`
- `RagService`
- `IndexingOrchestrator`
- `RagRepository`
## Связанные документы
- `logic-rag-indexing`
- `logic-rag-retrieval`
- `entity-rag-session`
- `entity-rag-index-job`
- `api-rag-session-create`
- `api-rag-session-changes`
- `api-rag-session-job`
## История изменений
| Date | Source | Changes |
| ---------- | ------ | ------------------------------------------------------------------- |
| 2026-03-13 | code | Создан обзор архитектуры пакета `rag` на основе текущей реализации. |
+154
View File
@@ -0,0 +1,154 @@
---
id: entity-rag-index-job
title: Сущность IndexJob
doc_type: domain_entity
domain: rag
status: draft
owner: system-analyst
source_of_truth: code
related_docs:
- arch-rag-package
- logic-rag-indexing
- entity-rag-session
- api-rag-session-job
related_code:
- src/app/modules/rag/job_store.py
- src/app/modules/rag/indexing_service.py
- src/app/modules/rag/persistence/job_repository.py
- src/app/modules/rag/persistence/schema_repository.py
entities:
- IndexJob
- RagSession
tags:
- rag
- indexing
- job
- domain-entity
---
# Сущность IndexJob
## Summary
- Domain: rag
- Purpose: представить асинхронную задачу индексации и её наблюдаемый статус.
- Entity role: operational entity для выполнения snapshot/change indexing.
- Main attributes: `index_job_id`, `rag_session_id`, `status`, `indexed_files`, `failed_files`, `cache_hit_files`, `cache_miss_files`, `error`.
- Lifecycle: `queued -> running -> done|error`.
- Invariants: job всегда принадлежит одной `RagSession`, статус хранится как enum `IndexJobStatus`.
- Related APIs: создание job косвенно через session endpoints, чтение через job status endpoint и SSE endpoint.
- Related logic: `IndexingOrchestrator`, retry, EventBus publishing.
- Source of truth: `src/app/modules/rag/job_store.py`, `src/app/modules/rag/indexing_service.py`.
## Назначение
`IndexJob` хранит технический прогресс и итог выполнения индексации. Он нужен, чтобы API модуля мог вернуть результат не синхронно, а через опрос статуса и подписку на события.
## Контекст
Job создаётся на каждую snapshot- или changes-операцию. Сервис индексации обновляет его counters и публикует события прогресса в EventBus под ключом `index_job_id`.
## Роль в доменной модели
Это операционная сущность, которая связывает пользовательский запрос на индексацию с фактическим процессом обработки файлов. Она не хранит сам индекс, но управляет прозрачностью выполнения и ошибками.
## Атрибуты
| attribute | type | required | description | constraints |
|-----------|------|----------|-------------|-------------|
| `index_job_id` | `str` | yes | уникальный идентификатор задачи | primary key, non-empty |
| `rag_session_id` | `str` | yes | ссылка на целевую RAG-сессию | non-empty |
| `status` | `IndexJobStatus` | yes | текущее состояние задачи | `queued`, `running`, `done`, `error` |
| `indexed_files` | `int` | yes | число успешно обработанных файлов | `>= 0` |
| `failed_files` | `int` | yes | число файлов с ошибками | `>= 0` |
| `cache_hit_files` | `int` | yes | число файлов, обслуженных из cache | `>= 0` |
| `cache_miss_files` | `int` | yes | число файлов, потребовавших embeddings | `>= 0` |
| `error` | `ErrorPayload \| None` | no | информация о необработанной временной ошибке после retry | optional |
## Состояния и жизненный цикл
### Основные состояния
- `queued`
- `running`
- `done`
- `error`
### Переходы состояний
1. `IndexJobStore.create` создаёт job в состоянии `queued`.
2. `IndexingOrchestrator._run_with_project_lock` переводит job в `running`.
3. Успешная индексация переводит job в `done` и заполняет counters.
4. Ошибка после исчерпания retry переводит job в `error` и заполняет `ErrorPayload`.
## Инварианты и ограничения
- Job не мигрирует между `rag_session_id`.
- Финальные counters сохраняются в БД перед публикацией terminal event.
- Ошибки уровня `TimeoutError`, `ConnectionError`, `OSError` считаются временными и оборачиваются в `index_retry_exhausted` только после retry exhaustion.
## Связи с другими сущностями
| entity | relation | description |
|--------|----------|-------------|
| `RagSession` | many-to-one | каждая задача относится к одной сессии |
| `RagDocument` | indirect | job обновляет набор документов сессии, но не владеет ими напрямую |
## Использование в системе
### Related API
- `POST /api/rag/sessions`
- `POST /api/rag/sessions/{rag_session_id}/changes`
- `GET /api/rag/sessions/{rag_session_id}/jobs/{index_job_id}`
- `GET /api/rag/sessions/{rag_session_id}/jobs/{index_job_id}/events`
### Related UI
- Прямого UI в репозитории не обнаружено.
### Related logic
- `logic-rag-indexing`
### Related integrations
- EventBus SSE stream
- PostgreSQL таблица `rag_index_jobs`
## Функциональные требования
- Job должна создаваться до запуска фоновой задачи.
- Публичный API обязан проверять принадлежность job указанной `rag_session_id`.
- Progress events должны публиковаться в формате, достаточном для фронта или внешнего клиента.
## Нефункциональные требования
### Audit / history
- `created_at` и `updated_at` сохраняются в таблице `rag_index_jobs`.
### Security
- Доступ к job опирается на проверку связи `job.rag_session_id == requested rag_session_id`.
### Observability
- SSE stream отдаёт `index_status`, `index_progress`, `terminal`.
## Связанный код
### Files
- `src/app/modules/rag/job_store.py`
- `src/app/modules/rag/indexing_service.py`
- `src/app/modules/rag/persistence/job_repository.py`
- `src/app/modules/rag/persistence/schema_repository.py`
### Symbols
- `IndexJob`
- `IndexJobStore.create`
- `IndexJobStore.get`
- `IndexJobStore.save`
- `IndexingOrchestrator._run_with_project_lock`
## Связанные документы
- `arch-rag-package`
- `logic-rag-indexing`
- `entity-rag-session`
- `api-rag-session-job`
## История изменений
| Date | Source | Changes |
|------|--------|---------|
| 2026-03-13 | code | Добавлено описание lifecycle и контракта сущности `IndexJob`. |
@@ -0,0 +1,143 @@
---
id: entity-rag-session
title: Сущность RagSession
doc_type: domain_entity
domain: rag
status: draft
owner: system-analyst
source_of_truth: code
related_docs:
- arch-rag-package
- logic-rag-indexing
- api-rag-session-create
- api-rag-session-changes
- api-rag-session-job
related_code:
- src/app/modules/rag/session_store.py
- src/app/modules/rag/persistence/session_repository.py
- src/app/modules/rag/persistence/schema_repository.py
entities:
- RagSession
tags:
- rag
- session
- domain-entity
---
# Сущность RagSession
## Summary
- Domain: rag
- Purpose: связать индекс и связанные job с конкретным проектом или его рабочим снимком.
- Entity role: корневая сущность области RAG indexing/retrieval.
- Main attributes: `rag_session_id`, `project_id`, `created_at`.
- Lifecycle: создаётся до первой индексации и используется как scope всех retrieval-запросов.
- Invariants: `rag_session_id` уникален, `project_id` обязателен.
- Related APIs: `POST /api/rag/sessions`, `POST /api/rag/sessions/{rag_session_id}/changes`, `GET /api/rag/sessions/{rag_session_id}/jobs/{index_job_id}`.
- Related logic: snapshot indexing, change indexing, retrieval filtering.
- Source of truth: `src/app/modules/rag/session_store.py`, таблица `rag_sessions`.
## Назначение
`RagSession` определяет границу индекса для проекта. Все документы, задачи и retrieval-запросы внутри `rag` привязаны к этой сущности.
## Контекст
Сессия используется и в новом API с UUID, и в legacy/internal режиме, где `project_id` может совпадать с `rag_session_id`. Через неё сервис восстанавливает `repo_id`, который затем участвует в кэшировании документов.
## Роль в доменной модели
`RagSession` является владельцем набора индексированных документов и асинхронных `IndexJob`. Без неё нельзя безопасно выполнять reindex или retrieval, потому что именно она задаёт scope таблицы `rag_chunks`.
## Атрибуты
| attribute | type | required | description | constraints |
|-----------|------|----------|-------------|-------------|
| `rag_session_id` | `str` | yes | уникальный идентификатор сессии | primary key, non-empty |
| `project_id` | `str` | yes | идентификатор проекта или workspace | non-empty |
| `created_at` | `timestamp with time zone` | yes | время создания записи в БД | default `CURRENT_TIMESTAMP` |
## Состояния и жизненный цикл
### Основные состояния
- created
- active
- reused via legacy/internal API
### Переходы состояний
1. `RagSessionStore.create(project_id)` создаёт новую сессию с UUID.
2. `RagSessionStore.put(rag_session_id, project_id)` создаёт или обновляет сессию с заданным ключом.
3. После создания сессия используется для indexing и retrieval до тех пор, пока не будет заменена новым идентификатором на уровне вызывающего сервиса.
## Инварианты и ограничения
- `project_id` не должен быть пустым.
- Для retrieval и indexing используется только один `rag_session_id` за операцию.
- `RagService._resolve_repo_id` использует `project_id` этой сущности как `repo_id` для cache scope.
## Связи с другими сущностями
| entity | relation | description |
|--------|----------|-------------|
| `IndexJob` | one-to-many | одна сессия может иметь много задач индексации |
| `RagDocument` | one-to-many | все записи в `rag_chunks` привязаны к одной сессии |
## Использование в системе
### Related API
- `POST /api/rag/sessions`
- `POST /api/rag/sessions/{rag_session_id}/changes`
- `GET /api/rag/sessions/{rag_session_id}/jobs/{index_job_id}`
### Related UI
- Прямого UI в репозитории не обнаружено.
### Related logic
- `logic-rag-indexing`
- `logic-rag-retrieval`
### Related integrations
- PostgreSQL таблица `rag_sessions`
## Функциональные требования
- Сессия должна создаваться до постановки snapshot job.
- При change indexing запрос должен ссылаться на существующую сессию в новом публичном API.
- Legacy/internal API может создавать запись с предсказуемым `rag_session_id`, равным `project_id`.
## Нефункциональные требования
### Audit / history
- Время создания фиксируется в таблице `rag_sessions`.
### Security
- Отдельных прав доступа на уровне сущности внутри модуля нет.
### Observability
- Основная наблюдаемость сессии идёт через связанные `IndexJob`.
## Связанный код
### Files
- `src/app/modules/rag/session_store.py`
- `src/app/modules/rag/persistence/session_repository.py`
- `src/app/modules/rag/persistence/schema_repository.py`
### Symbols
- `RagSession`
- `RagSessionStore.create`
- `RagSessionStore.put`
- `RagSessionStore.get`
## Связанные документы
- `arch-rag-package`
- `logic-rag-indexing`
- `api-rag-session-create`
- `api-rag-session-changes`
- `api-rag-session-job`
## История изменений
| Date | Source | Changes |
|------|--------|---------|
| 2026-03-13 | code | Добавлено описание сущности `RagSession` и её роли в границах индекса. |
@@ -0,0 +1,164 @@
---
id: logic-rag-indexing
title: Индексация файлов в RAG
doc_type: logic_block
domain: rag
status: draft
owner: system-analyst
source_of_truth: code
related_docs:
- arch-rag-package
- entity-rag-session
- entity-rag-index-job
- api-rag-session-create
- api-rag-session-changes
related_code:
- src/app/modules/rag/indexing_service.py
- src/app/modules/rag/services/rag_service.py
- src/app/modules/rag/indexing/docs/pipeline.py
- src/app/modules/rag/indexing/code/pipeline.py
- src/app/modules/rag/persistence/document_repository.py
entities:
- RagSession
- IndexJob
- RagDocument
tags:
- rag
- indexing
- snapshot
- changes
---
# Индексация файлов в RAG
## Summary
- Purpose: превратить входной список файлов в набор индексируемых `RagDocument` и сохранить их в persistence.
- Trigger: создание RAG-сессии, запрос изменений, internal snapshot/changes endpoints.
- Inputs: `rag_session_id`, файлы snapshot или changed_files, progress callback.
- Outputs: обновлённые записи в `rag_chunks`, `rag_session_chunk_map`, статистика indexed/failed/cache hit/cache miss.
- Main entities: `RagSession`, `IndexJob`, `RagDocument`.
- Main dependencies: `IndexingOrchestrator`, `RagService`, `DocsIndexingPipeline`, `CodeIndexingPipeline`, `GigaChatEmbedder`, `RagRepository`.
- Side effects: SQL delete/insert, cache write, SSE events, job status update.
- Source of truth: `src/app/modules/rag/indexing_service.py`, `src/app/modules/rag/services/rag_service.py`.
## Назначение
Блок обеспечивает управляемую индексацию файлов проекта в многослойный RAG-индекс. Он должен одинаково поддерживать полный snapshot и инкрементальные изменения, не допуская гонок внутри одной `rag_session_id`.
## Контекст
Индексация запускается через HTTP API модуля. `IndexingOrchestrator` отвечает за job lifecycle и progress events, а `RagService` за фактическую переработку файлов в документы. Для уменьшения стоимости embeddings используется cache по содержимому blob.
## Технический use case
### Основной сценарий
1. API создаёт `IndexJob` и передаёт управление в `IndexingOrchestrator`.
2. `IndexingOrchestrator` фильтрует входной набор, ставит статус `running`, публикует стартовое событие и захватывает lock по `rag_session_id`.
3. `RagService` определяет `repo_id`, фильтрует индексируемые файлы, проверяет cache по `blob_sha` и либо переиспользует документы, либо заново строит docs/code слои.
4. Для новых документов `RagService` добавляет file metadata, запрашивает embeddings батчами и сохраняет документы через `RagRepository`.
5. `IndexingOrchestrator` обновляет job counters, публикует финальный `index_status` и `terminal`.
### Альтернативные ветки
- Для `changes` операции с `op=delete` только удаляют документы по `path`, без повторной сборки.
- Если файл не поддержан ни docs-, ни code-pipeline, сервис делает fallback в docs pipeline.
- При временном сбое индексации `RetryExecutor` повторяет операцию; после исчерпания попыток job получает `error`.
## Функциональные требования
### Preconditions
- `rag_session_id` уже существует либо создаётся до запуска indexing job.
- Файлы передаются в виде словарей, совместимых со схемами `FileSnapshot` или `ChangedFile`.
- Для cache reuse у файла должен быть `content_hash` или доступный `content`.
### Processing rules
- Snapshot перед записью выполняет полную замену документов сессии через `replace_documents`.
- Incremental changes отделяет `delete_paths` от upsert-файлов и применяет изменения через `apply_document_changes`.
- `repo_id` выводится из `project_id` у сессии, а при отсутствии сессии fallback равен `rag_session_id`.
- Для поддерживаемого markdown строятся docs-слои `D1-D4`.
- Для поддерживаемого Python-кода строятся code-слои `C0-C4`.
- Каждый документ получает metadata файла: `blob_sha`, `repo_id`, `artifact_type`, `section`, `doc_id`, `owner`, `system_component`, `last_modified`, `staleness_score`.
### Validation rules
- До индексации snapshot/changes проходят фильтрацию через `filter_snapshot_files` и `filter_changes_for_indexing`.
- Пустые или неподходящие файлы исключаются из обрабатываемого набора.
- Вектор сохраняется только если размерность embedding совпадает с размерностью поля `vector` при retrieval.
### Output / result rules
- Результат операции всегда выражается четырьмя счётчиками: `indexed_files`, `failed_files`, `cache_hit_files`, `cache_miss_files`.
- Для snapshot весь набор документов сессии после операции должен соответствовать текущему переданному snapshot.
- Для changes в индексе должны остаться только документы по актуальному состоянию изменённых путей.
### Side effects
- Удаление и вставка строк в `rag_chunks`.
- Запись `rag_session_chunk_map` для документов, имеющих `repo_id` и `blob_sha`.
- Сохранение cache в `rag_blob_cache` и `rag_chunk_cache`.
- Публикация SSE-событий прогресса и завершения задачи.
## Ограничения и условия вызова
- Одновременно может выполняться только одна indexing operation на `rag_session_id`.
- Code indexing работает только для Python файлов, распознаваемых `PythonFileFilter`.
- Docs indexing рассчитывает на markdown с возможным YAML frontmatter.
- Метод `replace_documents` делает жёсткую замену индекса сессии и не подходит для конкурентного merge разных snapshot-источников.
## Нефункциональные требования
### Security
- Модуль не валидирует источник файлов и не выполняет контентную санацию сверх собственных парсеров.
### Observability
- Logs: фиксируются skipped files и режим обработки `cache` / `embed`.
- Metrics: отдельные счётчики не выделены, но статистика сохраняется в job.
- Traces: не реализованы.
- Audit: `rag_index_jobs` и `rag_session_chunk_map` образуют журнал выполнения и происхождения chunk.
### Reliability
- `asyncio.Lock` сериализует операции в рамках одной сессии.
- `RetryExecutor` покрывает временные ошибки `TimeoutError`, `ConnectionError`, `OSError`.
### Performance
- Embeddings обрабатываются батчами.
- Cache hit исключает повторный парсинг и повторный вызов embedder.
## Связанные API / UI / integration points
- `POST /api/rag/sessions`
- `POST /api/rag/sessions/{rag_session_id}/changes`
## Связанные сущности
- `RagSession`
- `IndexJob`
- `RagDocument`
## Связанный код
### Files
- `src/app/modules/rag/indexing_service.py`
- `src/app/modules/rag/services/rag_service.py`
- `src/app/modules/rag/indexing/docs/pipeline.py`
- `src/app/modules/rag/indexing/code/pipeline.py`
- `src/app/modules/rag/persistence/document_repository.py`
### Symbols
- `IndexingOrchestrator.enqueue_snapshot`
- `IndexingOrchestrator.enqueue_changes`
- `IndexingOrchestrator._run_with_project_lock`
- `RagService.index_snapshot`
- `RagService.index_changes`
- `RagService._index_files`
## Связанные документы
- `arch-rag-package`
- `entity-rag-session`
- `entity-rag-index-job`
- `api-rag-session-create`
- `api-rag-session-changes`
## История изменений
| Date | Source | Changes |
|------|--------|---------|
| 2026-03-13 | code | Описана фактическая логика snapshot и incremental indexing пакета `rag`. |
@@ -0,0 +1,150 @@
---
id: logic-rag-retrieval
title: Retrieval и ранжирование RAG-документов
doc_type: logic_block
domain: rag
status: draft
owner: system-analyst
source_of_truth: code
related_docs:
- arch-rag-package
- entity-rag-session
related_code:
- src/app/modules/rag/persistence/repository.py
- src/app/modules/rag/persistence/query_repository.py
- src/app/modules/rag/persistence/retrieval_statement_builder.py
- src/app/modules/rag/contracts/enums.py
entities:
- RagSession
- RagDocument
tags:
- rag
- retrieval
- ranking
- pgvector
---
# Retrieval и ранжирование RAG-документов
## Summary
- Purpose: вернуть релевантные RAG-документы из `rag_chunks` для заданной сессии и набора фильтров.
- Trigger: вызовы runtime adapters и внутренних retrieval-компонентов.
- Inputs: `rag_session_id`, `query_embedding`, `query_text`, `layers`, path filters, preference filters, limit.
- Outputs: список rows с `path`, `content`, `layer`, `title`, `metadata`, `span_start`, `span_end`, ranking fields.
- Main entities: `RagSession`, `RagDocument`.
- Main dependencies: `RagRepository`, `RagQueryRepository`, `RetrievalStatementBuilder`, PostgreSQL/pgvector.
- Side effects: отсутствуют, retrieval только читает БД.
- Source of truth: `src/app/modules/rag/persistence/query_repository.py`, `src/app/modules/rag/persistence/retrieval_statement_builder.py`.
## Назначение
Блок retrieval выбирает из индекса наиболее полезные документы по конкретному `rag_session_id`. Он объединяет в одном SQL векторную близость, lexical match, слой документа и структурные сигналы из metadata.
## Контекст
Публичный HTTP endpoint retrieval внутри `rag` помечен как deprecated, поэтому рабочий доступ к retrieval идёт через repository/runtime adapters. Это означает, что контракт фактически определяется SQL-builder и форматом rows, возвращаемых `RagQueryRepository`.
## Технический use case
### Основной сценарий
1. Клиент runtime вызывает `RagRepository.retrieve(...)`.
2. `RagQueryRepository` строит SQL через `RetrievalStatementBuilder.build_retrieve`.
3. SQL ограничивает поиск текущей `rag_session_id`, при необходимости слоями и path-фильтрами.
4. База сортирует документы по `prefer_bonus`, `test_penalty`, `layer_rank`, `lexical_rank`, `structural_rank`, `distance`.
5. Репозиторий возвращает rows с распарсенным `metadata_json`.
### Альтернативные ветки
- Для lexical fallback по коду используется `retrieve_lexical_code`, который работает только по слою `C0_SOURCE_CHUNKS`.
- Для точного добора файлов используется `retrieve_exact_files`, который читает заданные `path` без векторного ранжирования.
- Если `query_text` не даёт terms, lexical retrieval возвращает пустой результат без выполнения SQL.
## Функциональные требования
### Preconditions
- В `rag_chunks` уже должны существовать документы нужной `rag_session_id`.
- Для vector retrieval embedding документа должен быть ненулевым и совпадать по размерности с query embedding.
### Processing rules
- Базовый фильтр retrieval всегда включает `rag_session_id = :sid`.
- При наличии `layers` запрос ограничивается указанными слоями.
- `path_prefixes` задают include-фильтр по `LIKE prefix%`.
- `exclude_path_prefixes` и `exclude_like_patterns` исключают части дерева путей до сортировки.
- `prefer_path_prefixes` и `prefer_like_patterns` формируют `prefer_bonus`, поднимая приоритет совпавших путей.
- `prefer_non_tests` создаёт `test_penalty`, если путь попадает под test-паттерны.
### Validation rules
- Path filters экранируются для корректной работы `LIKE`.
- `retrieve_exact_files` нормализует и отбрасывает пустые пути до построения SQL.
- `retrieve_lexical_code` не выполняет SQL, если query terms отсутствуют.
### Output / result rules
- Каждый row содержит контент документа и технические поля ранжирования.
- `metadata_json` всегда декодируется в словарь `metadata`.
- Limit применяется на уровне SQL и ограничивает итоговый набор строк.
### Side effects
- Побочных эффектов нет, кроме чтения из БД.
## Ограничения и условия вызова
- Retrieval работает только внутри одной `rag_session_id` и не агрегирует несколько сессий.
- Layer ranking зашит в код SQL-builder и требует явного обновления при появлении новых слоёв.
- Полноценный HTTP retrieval endpoint в модуле не публикуется.
## Нефункциональные требования
### Security
- Retrieval не выполняет маскирование содержимого документов.
### Observability
- Logs: отдельное логирование запросов retrieval не реализовано.
- Metrics: метрики по latency и quality не выделены.
- Traces: отсутствуют.
- Audit: результат зависит только от состояния `rag_chunks` и входных фильтров.
### Reliability
- Пустой или некорректный lexical search безопасно возвращает пустой набор.
- `retrieve_exact_files` работает без embeddings и может использоваться как fallback.
### Performance
- Основной ranking выполняется в одном SQL-запросе.
- Для vector retrieval используются поля `embedding` и индексы по session/layer/path.
## Связанные API / UI / integration points
- 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`
- HTTP retrieval endpoint отсутствует
## Связанные сущности
- `RagSession`
- `RagDocument`
## Связанный код
### Files
- `src/app/modules/rag/persistence/repository.py`
- `src/app/modules/rag/persistence/query_repository.py`
- `src/app/modules/rag/persistence/retrieval_statement_builder.py`
- `src/app/modules/rag/contracts/enums.py`
### Symbols
- `RagRepository.retrieve`
- `RagRepository.retrieve_lexical_code`
- `RagRepository.retrieve_exact_files`
- `RagQueryRepository.retrieve`
- `RetrievalStatementBuilder.build_retrieve`
- `RetrievalStatementBuilder.build_lexical_code`
## Связанные документы
- `arch-rag-package`
- `entity-rag-session`
## История изменений
| Date | Source | Changes |
|------|--------|---------|
| 2026-03-13 | code | Описан фактический retrieval contract и ranking SQL для пакета `rag`. |
+32
View File
@@ -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.
View File
+190
View File
@@ -0,0 +1,190 @@
# Снимок runtime-контура CODE_QA (answer layer)
Документ фиксирует текущее состояние runtime-контура `CODE_QA` после рефакторинга для планирования доработок answer layer. Без предложений по новому дизайну и без implementation brief.
---
## 1. Entry point
- **HTTP:** `POST /api/chat/messages``ChatModule.public_router()``send_message()`.
Файл: `src/app/modules/chat/module.py`, строки 7481.
- **Условие:** при `SIMPLE_CODE_EXPLAIN_ONLY=true` запрос идёт в `CodeExplainChatService.handle_message()` (прямой explain без полного CODE_QA pipeline). При `false` — в оркестратор.
- **Оркестратор:** `ChatOrchestrator.enqueue_message()` создаёт задачу и запускает `_process_task()` → в нём вызывается `self._runtime.run(...)`.
Файл: `src/app/modules/chat/service.py`, строки 4769, 71132.
- **Runtime-адаптер:** `CodeQaRunnerAdapter` реализует `AgentRunner`; в `run()` вызывает `self._executor.execute(user_query=..., rag_session_id=..., files_map=...)` в thread pool.
Файл: `src/app/modules/agent/runtime/code_qa_runner_adapter.py`, строки 2141.
- **Фактическая точка входа CODE_QA:** `AgentRuntimeExecutor.execute()`.
Файл: `src/app/modules/agent/runtime/executor.py`, строка 53.
Создание executor: `application.py`, строка 48 — `_executor = AgentRuntimeExecutor(llm=..., retrieval=...)`.
---
## 2. Runtime pipeline
Цепочка внутри `AgentRuntimeExecutor.execute()` (файл `executor.py`):
| Шаг | Файл | Класс/функция | Роль |
|-----|------|----------------|------|
| 1. Роутинг | `executor.py` | `self._router.route(user_query, ...)` | Intent + sub-intent, query_plan, retrieval_spec, symbol_resolution (pending). |
| 2. Сборка запроса retrieval | `retrieval_request_builder.py` | `build_retrieval_request(router_result, rag_session_id)` | Из `RouterResult` собирается `RetrievalRequest`: query, sub_intent, path_scope, requested_layers, retrieval_spec, constraints, query_plan. |
| 3. Retrieval | `executor.py` | `self._retrieve(state)``RuntimeRetrievalAdapter.retrieve_with_plan()` или `retrieve_exact_files()` для OPEN_FILE | По плану или по точным путям; возвращает `raw_rows` (list[dict]). |
| 4. Догидрация (только FIND_ENTRYPOINTS) | `executor.py` | `_hydrate_entrypoint_sources()` | Дозапрос C0 по путям из C3 entrypoints. |
| 5. Разрешение символа | `executor.py` | `_resolve_symbol(initial, raw_rows)` | По C1_SYMBOL_CATALOG: resolved / ambiguous / not_found; обновляет `state.router_result.symbol_resolution`. |
| 6. Retrieval result | `retrieval_result_builder.py` | `build_retrieval_result(raw_rows, report, symbol_resolution)` | Нормализованный `RetrievalResult`: code_chunks, relations, entrypoints, test_candidates, layer_outcomes и т.д. Для EXPLAIN при not_found/ambiguous — пересборка с пустыми rows (строки 9091 executor). |
| 7. Evidence bundle | `evidence_bundle_builder.py` | `build_evidence_bundle(retrieval_result, router_result)` | `EvidenceBundle`: resolved_sub_intent, resolved_target, code_chunks, relations, entrypoints, test_evidence, retrieval_summary. sufficient/failure_reasons не выставляются здесь. |
| 8. Pre evidence gate | `evidence_gate.py` | `evaluate_evidence(state.evidence_pack)` | По sub_intent проверяет достаточность (target, evidence_count, слои, entrypoints, tests). Выставляет `bundle.sufficient`, возвращает `EvidenceGateDecision`; от этого — `state.answer_mode` (normal/degraded). |
| 9. Answer policy | `policy.py` | `self._answer_policy.decide(router_result, gate_decision)` | Решение: вызывать LLM или короткий ответ (OPEN_FILE not_found, EXPLAIN not_found/ambiguous, gate не прошёл). При `should_call_llm=False` сразу идём в `assemble_final_result` с `decision.answer`. |
| 10. Synthesis input | `answer_synthesis.py` | `build_answer_synthesis_input(user_query, state.evidence_pack)` | Строит `AnswerSynthesisInput`: fast_context, deep_context, evidence_summary, semantic_hints, curated_facts (из answer_fact_curator). |
| 11. Выбор промпта | `prompt_selector.py` | `self._prompt_selector.select(sub_intent=..., answer_mode=...)` | Имя системного промпта по sub_intent (и degraded). |
| 12. Payload | `prompt_payload_builder.py` | `self._payload_builder.build(user_query, synthesis_input, evidence_pack, answer_mode)` | JSON payload для LLM: user_query, resolved_scenario, fast/deep_context, evidence_summary, curated must_mention_*, layer_guide, entrypoints, scenario-specific поля. |
| 13. Генерация черновика | `generator.py` | `self._generator.generate(prompt_name, prompt_payload)` | Вызов `AgentLlmService.generate(prompt_name, payload)` → черновик ответа. |
| 14. Post evidence gate | `post_gate.py` | `self._post_gate.validate(answer, answer_mode, ..., sub_intent, user_query, evidence_pack)` | Проверка черновика по sub_intent (EXPLAIN/ARCHITECTURE/TRACE_FLOW/…), возврат `RuntimeValidationResult(passed, action, reasons)`. |
| 15. Repair (если не passed) | `repair.py` | `self._repair.repair(draft_answer, validation, prompt_payload)` | Один вызов LLM с промптом `code_qa_repair_answer`; повторная валидация; при повторном fail — fallback answer. |
| 16. Финальный результат | `result_assembler.py` | `assemble_final_result(state, draft=..., final_answer=..., ...)` | Сборка `RuntimeFinalResult` и диагностики. |
Sub-intent для CODE_QA задаётся в роутере: `QueryPlanBuilder` использует `SubIntentDetector.detect()` и `_resolve_sub_intent()`; итог в `query_plan.sub_intent`. Ретривал-слои по sub_intent задаются в `RetrievalSpecFactory._with_sub_intent_layers()` (`retrieval_spec_factory.py`).
---
## 3. Answer path
- **Выбор промпта:** `RuntimePromptSelector.select(sub_intent, answer_mode)``src/app/modules/agent/runtime/steps/generation/prompt_selector.py`, строки 1821. При answer_mode in `{"degraded","not_found","insufficient"}` возвращается `code_qa_degraded_answer`, иначе — по `sub_intent` из словаря (fallback `code_qa_explain_answer`).
- **Сборка payload:** `RuntimePromptPayloadBuilder.build()``prompt_payload_builder.py`, строки 21–44. В payload попадают: `user_query`, `resolved_scenario`, `resolved_target`, `answer_mode`, `fast_context`, `deep_context`, `evidence_summary`, `semantic_hints`, `diagnostic_hints`, `retrieval_summary`, `confirmed_entrypoints`, `required_entrypoints`, `layer_guide`, плюс сценарий-специфичные поля из `_scenario_payload(synthesis_input)` (must_mention_*, fact_gaps и т.д.).
- **Draft answer:** создаётся в `executor.py`, строки 242246: `RuntimeDraftAnswer(prompt_name=..., prompt_payload=..., answer=self._generator.generate(...))`.
- **Post-processing:** отдельного шага нет; после генерации сразу идёт post-validation.
- **Repair:** `RuntimeAnswerRepairService.repair()``repair.py`, строки 16–37. Формирует JSON с draft_answer, validation_reasons, repair_focus, prompt_payload и один раз вызывает LLM с `code_qa_repair_answer`.
- **Final text:** в executor: при passed — `final_answer = draft.answer` (или результат repair); при не passed после repair — `_fallback_answer(state)`. Итоговая строка попадает в `RuntimeFinalResult.final_answer` в `assemble_final_result()`.
---
## 4. Prompt selection
- **Где:** `src/app/modules/agent/runtime/steps/generation/prompt_selector.py`, класс `RuntimePromptSelector`, метод `select(sub_intent, answer_mode)`.
- **Правила:**
- answer_mode in `{"degraded","not_found","insufficient"}``code_qa_degraded_answer`.
- Иначе по `sub_intent.upper()` из `_PROMPTS`; при отсутствии ключа — `code_qa_explain_answer`.
- **Используемые имена промптов для целевых sub_intent:**
| sub_intent | prompt name |
|-------------|--------------------------------|
| EXPLAIN | `code_qa_explain_answer` |
| EXPLAIN_LOCAL| `code_qa_explain_local_answer` |
| ARCHITECTURE| `code_qa_architecture_answer` |
| TRACE_FLOW | `code_qa_trace_flow_answer` |
- **Шаблоны:** загружаются по имени из YAML в `AgentLlmService.generate()``PromptLoader.load(name)`; конфиг — `src/app/modules/agent/llm/prompts.yml`. Ключи в YAML совпадают с именами выше (в т.ч. `code_qa_explain_answer`, `code_qa_architecture_answer`, `code_qa_trace_flow_answer`); repair — `code_qa_repair_answer`.
- **Выбор по sub_intent:** да, только через `RuntimePromptSelector.select(sub_intent=state.retrieval_request.sub_intent, ...)` в executor, строка 231.
---
## 5. Evidence-to-answer boundary
- **В answer layer evidence приходит как:**
- `EvidenceBundle` (в state.evidence_pack) и
- `AnswerSynthesisInput` (state.synthesis_input), собранный из bundle в `build_answer_synthesis_input()`.
- **Модели/DTO:**
- `EvidenceBundle`: `contracts.py`, 90106 — resolved_intent, resolved_sub_intent, resolved_target, target_type, code_chunks, relations, entrypoints, test_evidence, evidence_count, retrieval_summary.
- `AnswerSynthesisInput`: `contracts.py`, 109121 — user_question, resolved_scenario, resolved_target, fast_context, deep_context, evidence_summary, semantic_hints, **curated_facts**, evidence_sufficient, diagnostic_hints.
- Curated facts строит `answer_fact_curator.build_curated_answer_facts(bundle)` — словарь с ключами `explain`, `architecture`, `trace_flow` и общими полями (scenario, semantic_hints, relation_count и т.д.).
- **Что реально уходит в payload (prompt_payload_builder):**
- Общее: user_query, resolved_scenario, resolved_target, answer_mode, fast_context, deep_context, evidence_summary, semantic_hints, diagnostic_hints, retrieval_summary, confirmed_entrypoints, required_entrypoints, layer_guide.
- EXPLAIN: must_mention_methods/fields/calls/dependencies/constructor_args/files, must_not_infer_missing_details, fact_gaps (из curated_facts["explain"]).
- ARCHITECTURE: must_mention_components/relations, must_use_relation_verbs, must_avoid_semantic_labels_as_primary_claims, must_not_use_retrieval_labels, fact_gaps (из curated_facts["architecture"]).
- TRACE_FLOW: must_mention_flow_steps/calls/sequence_edges, must_avoid_overclaiming_full_flow, fact_gaps (из curated_facts["trace_flow"]).
- **Curated-поля (answer_fact_curator):**
- explain: required_methods, required_calls, required_fields, required_dependencies, required_constructor_args, required_files, fact_gaps (и др.).
- architecture: required_components, required_relations (source/verb/target/edge_type), required_relation_verbs, required_*_edges, forbidden_labels, fact_gaps.
- trace_flow: required_flow_steps (step, source, verb, target, path, line_span), required_calls, required_sequence_edges, fact_gaps.
То есть в LLM попадает не сырой retrieval, а нормализованный контекст (fast/deep_context, evidence_summary) плюс явные списки «must_mention_*» и fact_gaps по сценарию; для methods/dependencies/relations/flow steps уже есть выделенные curated-поля.
---
## 6. Post-validation / answer quality control
- **Post-evidence gate (runtime):** есть. `RuntimePostEvidenceGate.validate()``src/app/modules/agent/runtime/steps/gates/post/post_gate.py`, строки 39–65. Вызывается после генерации черновика (и после repair — повторно).
- **Answer validator:** это тот же post_gate: проверяет пустой ответ, соответствие answer_mode (degraded/not_found/ambiguous) требуемым формулировкам, длину при degraded, затем для normal — `_normal_answer_reasons()` по sub_intent.
- **Repair loop:** один раунд. При `not validation.passed` и наличии `self._repair` вызывается `repair()`; затем повторный `validate()`; если снова не passed — подставляется `_fallback_answer()` и смена answer_mode (`executor.py`, 281298).
- **Правила по sub_intent (post_gate):**
- **EXPLAIN** (93124): target focus; vagueness (_VAGUE_PHRASES); наличие required_methods/calls/dependencies (хотя бы одна группа); «too_vague_for_explain» при нуле совпадений; semantic_leakage (роли из semantic_hints без опоры на код).
- **ARCHITECTURE** (126150): target focus; vagueness; required_components, required_relations, relation_verbs; forbidden_labels (retrieval artifacts); methods_as_primary_components; «too_vague_for_architecture»; semantic_leakage.
- **TRACE_FLOW** (152171): target focus; vagueness; required_flow_steps и required_calls; _mentions_steps (сначала/затем или нумерация); overclaims (_OPTIMISTIC_TRACE_CLAIMS); «too_vague_for_trace_flow».
- **Technical precision для EXPLAIN:** проверяется косвенно: упоминание методов/вызовов/зависимостей из curated; явной проверки «только факты из кода» по токенам нет.
- **Concrete relations для ARCHITECTURE:** да — `_mentions_relations(answer, relations)` и упоминание verbs.
- **Concrete steps и overclaim для TRACE_FLOW:** да — `_mentions_steps`, `_mentions_relations` по steps, и проверка фраз из _OPTIMISTIC_TRACE_CLAIMS.
---
## 7. Problem sources (что может давать слабые ответы)
- **Payload shaping:** `prompt_payload_builder.py` — если curated_facts пустые или скудные (мало methods/calls/relations/steps), must_mention_* не направляют модель; deep_context обрезается до 30 чанков по 800 символов — возможна потеря важных деталей.
- **Prompts:** `prompts.yml` — длинные общие инструкции; для EXPLAIN/ARCHITECTURE/TRACE_FLOW нет жёсткой привязки к структуре payload (например, «обязательно используй must_mention_flow_steps по порядку»); модель может игнорировать fact_gaps.
- **Evidence normalization:** `answer_fact_curator` — методы/вызовы/relations извлекаются эвристически (regex, C1/C2); при слабом C1/C2 или нестандартных именах curated-списки пустеют → валидатор не к чему привязываться, ответ считается «vague».
- **Weak validation:** `post_gate` — проверки по вхождению подстрок (alias) и по небольшому набору фраз; нет проверки полноты (все ли must_mention_* упомянуты), нет проверки порядка шагов для TRACE_FLOW; semantic_leakage выключается при has_concrete_support, что может пропускать смешанные ответы.
- **Repair policy:** один вызов repair с общим промптом `code_qa_repair_answer` и repair_focus по reasons; при множественных reasons фокус может размываться; после repair при повторном fail сразу fallback — без второго раунда repair.
---
## 8. Minimal intervention points
1. **`src/app/modules/agent/runtime/steps/generation/prompt_payload_builder.py`**
Класс `RuntimePromptPayloadBuilder`, метод `build()` и `_scenario_payload()`.
Контролирует: какие поля и списки (must_mention_*, fact_gaps, layer_guide) попадают в JSON для LLM.
Удобно: один вход в «что видит модель»; можно усилить структуру под EXPLAIN/ARCHITECTURE/TRACE_FLOW без трогания оркестрации.
2. **`src/app/modules/agent/runtime/steps/context/answer_fact_curator.py`**
Функции `_explain_facts()`, `_architecture_facts()`, `_trace_flow_facts()`.
Контролируют: состав и качество curated_facts (required_*, fact_gaps).
Удобно: улучшение извлечения методов/relations/steps напрямую улучшает и payload, и валидацию.
3. **`src/app/modules/agent/runtime/steps/gates/post/post_gate.py`**
Класс `RuntimePostEvidenceGate`, методы `_validate_explain()`, `_validate_architecture()`, `_validate_trace_flow()` и хелперы (`_mentions_fact_group`, `_mentions_relations`, `_mentions_steps`).
Контролирует: критерии прохождения и набор reasons для repair.
Удобно: уже разбито по сценариям; можно ужесточить правила и добавить новые reasons без смены архитектуры.
4. **`src/app/modules/agent/llm/prompts.yml`**
Блоки `code_qa_explain_answer`, `code_qa_architecture_answer`, `code_qa_trace_flow_answer`, `code_qa_repair_answer`.
Контролируют: инструкции для черновика и починки.
Удобно: точечные правки формулировок и явные отсылки к полям payload (must_mention_*, fact_gaps).
5. **`src/app/modules/agent/runtime/steps/generation/prompt_selector.py`**
Класс `RuntimePromptSelector`, словарь `_PROMPTS` и метод `select()`.
Контролирует: какой системный промпт выбирается по sub_intent/answer_mode.
Удобно: введение отдельных промптов для подвидов (например, TRACE_FLOW по типу запроса) без изменения executor.
6. **`src/app/modules/agent/runtime/steps/context/answer_synthesis.py`**
Функция `build_answer_synthesis_input()`, формирование `fast_context` и `deep_context` (в т.ч. фильтр по C4 для EXPLAIN/ARCHITECTURE).
Контролирует: объём и приоритет контекста, передаваемого в synthesis_input.
Удобно: можно менять лимиты, порядок чанков или фильтры слоёв локально.
7. **`src/app/modules/agent/runtime/steps/finalization/repair.py`**
Класс `RuntimeAnswerRepairService`, метод `repair()` и `_repair_focus()`.
Контролирует: как validation.reasons мапятся в repair_focus и что уходит в промпт починки.
Удобно: можно сузить фокус repair под конкретные reasons или добавить приоритизацию без изменения цикла в executor.
---
*Документ описывает только текущую реализацию по коду после рефакторинга.*
+33
View File
@@ -0,0 +1,33 @@
# Agent Rules v1
## 1. Evidence-first
Агент должен формировать документацию только на основе подтвержденных источников: кода, существующей документации, системной аналитики и других явно доступных артефактов. Нельзя додумывать поведение системы, зависимости или бизнес-логику, если они не подтверждаются исходными материалами.
## 2. One Stable Object = One Document
Каждый документ должен описывать только одну устойчивую техническую сущность или один устойчивый аспект системы. Если по сущности могут ссылаться другие документы или она может переиспользоваться, ее нужно выносить в отдельный документ.
## 3. No Semantic Duplication
Документы не должны пересекаться по смыслу и повторять одно и то же содержание. Если одна и та же логика, правило или описание нужны в нескольких местах, агент должен вынести их в отдельный документ и использовать ссылки вместо дублирования текста.
## 4. Explicit Document Type
Для каждого документа агент должен явно определять его тип. На базовом уровне нужно использовать типы `ui_page`, `api_method`, `logic_block` и применять для каждого типа свой шаблон содержания и набор метаданных.
## 5. Hierarchical File Structure
Агент должен строить документацию как иерархию каталогов и файлов, а не как плоский набор страниц. Документы нужно раскладывать по смысловым разделам, например `docs/ui`, `docs/api`, `docs/logic`, `docs/architecture`, чтобы структура отражала архитектуру и упрощала навигацию.
## 6. Required YAML Frontmatter
Каждый документ должен начинаться с единообразного `YAML frontmatter`. В нем обязательно должны быть базовые метаданные документа: стабильный `id`, `title`, `doc_type`, `status`, `source_of_truth`, а также ссылки на связанные документы и кодовые артефакты.
## 7. Correct Internal Decomposition
Содержимое документа должно следовать шаблону своего типа и быть правильно декомпозировано внутри самого документа. Сценарии работы нужно описывать отдельно от детальных правил, контрактов, ограничений и дополнительных требований, чтобы документ оставался читаемым, атомарным и пригодным для индексирования.
## 8. Explicit Links Between Code and Docs
Каждый документ должен быть явно связан как минимум с соответствующим кодом и с соседними документами, если такие связи существуют. Агент должен фиксировать эти связи в метаданных и в тексте документа, чтобы документация образовывала связанную систему знаний, а не набор изолированных файлов.
+323
View File
@@ -0,0 +1,323 @@
# Концепция документации (Strong MVP, без связи с кодом)
## 1. Область применения
Документ описывает систему работы с документацией как самостоятельный слой.
Включает:
- текущее состояние (as-is)
- целевые сценарии использования
- целевую модель документации
- расширенный frontmatter
- базовую структуру документа `frontmatter + Summary + Details`
- RAG-слои (без связи с кодом)
---
# 2. Текущее состояние (As-Is)
## 2.1 Источник истины
- Документация хранится в Confluence
- Основная модель — иерархия страниц
- Навигация через дерево страниц
## 2.2 Структура и связи
- Документы не атомарны
- Одна страница содержит несколько тем
- Используются перекрестные ссылки между страницами
## 2.3 Ограничения
- Нет типизации документов
- Нет структурированного metadata-слоя
- Связи не формализованы
---
# 3. Целевые сценарии использования
## 3.1 Поиск документации
Пользователь формулирует запрос, например: "как работает создание заказа".
Система должна:
- найти релевантные документы
- отобрать наиболее точные фрагменты
- учитывать тип документа, модуль и сущности
Результат:
- короткий список релевантных документов
- возможность перейти в детали
## 3.2 Объяснение (Explain)
Пользователь хочет понять:
- как работает компонент
- что делает API
- как устроен процесс
Система должна:
- взять summary документа
- дополнить его деталями из Details
- дать краткое и точное объяснение без лишнего текста
## 3.3 Поиск по сущности (Entity Lookup)
Пользователь ищет:
- где используется сущность
- какие документы с ней связаны
Система должна:
- найти все документы, где упоминается сущность
- показать связи между ними
## 3.4 Навигация по документации
Пользователь начинает с одного документа и хочет:
- понять контекст
- перейти к связанным частям
Система должна:
- использовать parent/children
- использовать links
- строить осмысленный путь по документации
## 3.5 Генерация документации
Система создает новый документ:
- по шаблону
- с корректным frontmatter
- с заполненными разделами `Summary` и `Details`
Результат:
- документ сразу пригоден для использования
- соответствует структуре системы
## 3.6 Актуализация документации
При изменениях документа:
- система должна обновить только нужные части
- сохранить структуру
- обновить frontmatter и Details
Результат:
- документ остается консистентным
- не происходит деградации структуры
## 3.7 Связывание документации
При создании или обновлении:
- система добавляет связи между документами
- формирует граф знаний
Результат:
- документы не изолированы
- появляется навигация и reasoning
---
# 4. Frontmatter (расширенный контракт)
```yaml
id: api.create_order
type: api_method
name: create_order
title: Create order API
module: orders
layer: application
status: draft
updated_at: 2026-03-20
tags:
- orders
- api
entities:
- Order
- Cart
parent: orders_api
children: []
links:
- type: related_api
target: api.get_order
```
## Назначение ключевых полей
- `id` — стабильный идентификатор документа
- `type` — тип документа
- `title` — человекочитаемое имя
- `module` — модуль или bounded context
- `layer` — слой системы
- `status` — состояние документа
- `updated_at` — дата последнего обновления
- `tags` — фильтрация и поиск
- `entities` — база для entity lookup
- `parent` / `children` — иерархия
- `links` — граф связей
Все сведения о связях, сущностях и навигации должны храниться во frontmatter.
В теле документа отдельные разделы для связей, сущностей и навигации не создаются.
---
# 5. Структура документа (целевая)
Каждый документ состоит из двух смысловых слоев:
- frontmatter
- контент
Контент документа всегда состоит из двух обязательных разделов:
- `# Summary`
- `# Details`
## 5.1 Общие правила структуры
- `Summary` и `Details` всегда имеют заголовок уровня `#`
- других заголовков первого уровня в документе быть не должно
- `Summary` остается кратким и пригодным для explain
- `Details` заменяет прежние разделы `Context` и `Основное описание`
- внутренняя структура `Details` зависит от типа документа
## 5.2 Summary
Краткое описание на 3-6 строк.
Используется для explain, краткого ответа и быстрого понимания сути документа.
## 5.3 Details
Раздел содержит полное содержательное описание документа.
Состав подразделов определяется типом документа.
---
# 6. Типовая структура Details для API
Для документов типа `api_method` раздел `# Details` должен содержать следующие подразделы:
- `## Описание`
- `## Сценарий`
- `## Функциональные требования`
- `## Нефункциональные требования`
- `## Контракт`
## 6.1 Описание
Короткое описание сути метода:
- какую работу он выполняет
- для чего предназначен
## 6.2 Сценарий
Сценарий описывается в формате технического use case и включает:
- название
- предусловия
- триггер
- основной сценарий
- альтернативный сценарий
- обработку ошибок
- постусловие
Правила для сценария:
- сценарий должен быть лаконичным
- сценарий должен быть читаемым человеком
- в сценарии фиксируется суть шага, а не полная техническая реализация
- если условие можно выразить одним предложением, его допустимо оставить в сценарии
- если шаг требует детального описания формирования запроса, обработки ответа или внутренней логики, детали выносятся в функциональные требования
## 6.3 Функциональные требования
Функциональные требования описываются как последовательность требований внутри одного документа.
Формат:
- `FR-1`
- `FR-2`
- `FR-3`
Правила:
- идентификаторы локальны для документа
- на них нельзя ссылаться извне как на сквозные идентификаторы
- каждое требование описывает отдельный обязательный аспект реализации
## 6.4 Нефункциональные требования
Нефункциональные требования описываются аналогично функциональным.
Формат:
- `NFR-1`
- `NFR-2`
- `NFR-3`
Детальный формат записи может быть уточнен правилами конкретного проекта.
## 6.5 Контракт
Контракт должен быть пригоден для последующей сборки OpenAPI-спецификации.
Контракт описывает:
- входные параметры метода API
- выходные параметры
- структуру сообщения, как правило JSON
- обязательность полей
- типы полей
- ограничения на размер и формат
- назначение полей
- правила заполнения полей
- примеры данных
---
# 7. Базовая структура Details для остальных типов документов
Для всех типов документов, кроме `api_method`, на текущем этапе обязателен минимум:
- `# Summary`
- `# Details`
Дополнительные рекомендации по внутренней структуре `Details` для `logic_block`, `architecture_overview`, `domain_entity` и других типов будут заданы отдельно.
---
# 8. RAG слои (только документация)
## D0 — Чанки документов
Полный текст + разбиение.
## D1 — Каталог документов
- `id`
- `type`
- `title`
- `module`
- `tags`
## D2 — Индекс фактов
Факты, извлеченные из документов.
## D3 — Каталог сущностей
- сущности
- документы, где они используются
## D4 — Индекс сценариев (Workflow)
- последовательности действий
- пользовательские сценарии
## D5 — Граф связей
- связи между документами
---
# 9. Итог
Система документации:
- атомарные документы
- строгий frontmatter
- единая структура `Summary + Details`
- типовые правила для API-документов
- разделенные RAG-слои
Это позволяет:
- точно искать
- объяснять
- строить навигацию
- генерировать и обновлять документацию
@@ -0,0 +1,546 @@
# Текущая архитектура тестового пайплайна `pipeline_setup_v3`
Документ предназначен как краткое, но точное описание текущего устройства `pipeline_setup_v3` для внешней модели вроде ChatGPT.
Важно: текущий `pipeline_setup_v3` уже использует реальные runtime-компоненты агента, но по сути остается в первую очередь `code-first` пайплайном. Это особенно заметно в `evidence gate` и в наборе prompt'ов для LLM.
## 1. Общая схема пайплайна
`pipeline_setup_v3` запускает один из трех режимов:
- `router_only`
- `router_rag`
- `full_chain`
Во всех режимах используется `AgentRuntimeAdapter`, который является тестовым адаптером поверх реальных компонентов рантайма.
Общий поток для `full_chain`:
1. Пользовательский запрос
2. `IntentRouterV2`
3. Построение `RetrievalRequest`
4. `RuntimeRetrievalAdapter`
5. Построение нормализованного `RetrievalResult`
6. Сборка `EvidenceBundle`
7. `pre-evidence gate`
8. `RuntimeAnswerPolicy`
9. Вызов LLM через `AgentLlmService`
10. `post-evidence gate`
11. При необходимости `repair`
12. Сборка итогового результата, диагностики и артефактов теста
Ключевая идея: `pipeline_setup_v3` не эмулирует локальный тестовый сценарий вручную, а прогоняет реальные компоненты: роутер, retrieval, runtime executor и LLM.
## 2. Из каких компонентов состоит `pipeline_setup_v3`
### 2.1. Harness уровня тестов
Основные части:
- `tests/pipeline_setup_v3/run.py` — CLI-вход для запуска набора кейсов
- `tests/pipeline_setup_v3/core/runner.py` — оркестратор прогона кейсов
- `tests/pipeline_setup_v3/core/case_loader.py` — загрузка YAML-кейсов
- `tests/pipeline_setup_v3/core/validators.py` — проверка ожиданий
- `tests/pipeline_setup_v3/core/artifacts.py` — запись JSON/Markdown-результатов
- `tests/pipeline_setup_v3/runtime/agent_runtime_adapter.py` — мост к runtime-компонентам приложения
### 2.2. Runtime-компоненты, которые реально вызываются
- `IntentRouterV2`
- `RuntimeRepoContextFactory`
- `RuntimeRetrievalAdapter`
- `AgentRuntimeExecutor`
- `AgentLlmService`
- `PromptLoader`
### 2.3. Два варианта исполнения
#### `router_only`
Проверяет только результат роутера:
- `intent`
- `sub_intent`
- `graph_id`
- `conversation_mode`
RAG и LLM не вызываются.
#### `router_rag`
Проверяет:
- роутер
- retrieval plan
- реальный retrieval
- нормализованный `RetrievalResult`
LLM не вызывается.
#### `full_chain`
Проверяет полный runtime-контур:
- роутер
- retrieval
- evidence bundle
- pre-gate
- answer policy
- LLM
- post-gate
- repair
- итоговый answer/diagnostics
## 3. Компонент: Intent Router
### 3.1. Что это такое
`IntentRouterV2` классифицирует запрос и возвращает структурированный план для retrieval и дальнейшего рантайма.
Он не просто выбирает `intent`, а сразу строит:
- `conversation_mode`
- `query_plan`
- `retrieval_spec`
- `retrieval_constraints`
- `symbol_resolution`
- `evidence_policy`
- `graph_id`
### 3.2. Какие интенты есть сейчас
Сейчас в коде поддерживаются:
- `CODE_QA`
- `DOCUMENTATION_EXPLAIN`
- `GENERATE_DOCS_FROM_CODE`
- `FALLBACK`
### 3.3. Какие саб-интенты есть сейчас
#### Для `CODE_QA`
- `OPEN_FILE`
- `EXPLAIN`
- `EXPLAIN_LOCAL`
- `FIND_TESTS`
- `FIND_ENTRYPOINTS`
- `TRACE_FLOW`
- `ARCHITECTURE`
- `NEXT_STEPS` может появляться как follow-up режим на уровне query planning
#### Для `DOCUMENTATION_EXPLAIN`
- `SYSTEM_FLOW_EXPLAIN`
- `COMPONENT_EXPLAIN`
- `API_METHOD_EXPLAIN`
- `ENTITY_EXPLAIN`
#### Для `FALLBACK`
- `GENERIC_FALLBACK`
### 3.4. Что роутер возвращает на выходе
Результат роутера — это `IntentRouterResult`.
Ключевые поля:
- `intent`
- `retrieval_profile`
- `graph_id`
- `conversation_mode`
- `query_plan`
- `retrieval_spec`
- `retrieval_constraints`
- `symbol_resolution`
- `evidence_policy`
### 3.5. Что входит в `query_plan`
`query_plan` содержит:
- `raw`
- `normalized`
- `sub_intent`
- `negations`
- `expansions`
- `keyword_hints`
- `path_hints`
- `doc_scope_hints`
- `symbol_candidates`
- `symbol_kind_hint`
- `anchors`
Это основной bridge между классификацией запроса и retrieval.
### 3.6. Что входит в `retrieval_spec`
`retrieval_spec` содержит:
- `domains`
- `layer_queries`
- `filters`
- `rerank_profile`
Именно этот объект задает, какие слои RAG должны быть запрошены.
### 3.7. Что важно про текущее состояние
Текущий роутер уже умеет выделять docs-интенты и docs-сабинтенты, но downstream runtime ниже по пайплайну все еще во многом оптимизирован под `CODE_QA`.
Это означает:
- docs routing уже есть
- docs layer selection уже есть
- но `pre/post evidence gate` и prompt selection пока ориентированы в первую очередь на code sub-intents
## 4. Структура RAG
### 4.1. Общая идея
RAG устроен как многослойный индекс. Retrieval работает не по одному единственному типу чанков, а по наборам специализированных слоев.
### 4.2. Code-слои
- `C0_SOURCE_CHUNKS` — сырой код / чанки исходников
- `C1_SYMBOL_CATALOG` — каталог символов
- `C2_DEPENDENCY_GRAPH` — зависимости и связи
- `C3_ENTRYPOINTS` — точки входа, маршруты, handler'ы
- `C4_SEMANTIC_ROLES` — семантические роли и behavioral hints
### 4.3. Docs-слои
- `D0_DOC_CHUNKS` — чанки документов
- `D1_DOCUMENT_CATALOG` — каталог документов
- `D2_FACT_INDEX` — атомарные факты
- `D3_ENTITY_CATALOG` — сущности
- `D4_WORKFLOW_INDEX` — сценарии и workflow
- `D5_RELATION_GRAPH` — связи между документами
### 4.4. Как retrieval связывается с роутером
Роутер возвращает:
- `domains`
- `layer_queries`
- `filters`
- `retrieval_constraints`
Дальше `build_retrieval_request(...)` превращает это в `RetrievalRequest`, который содержит:
- `rag_session_id`
- `query`
- `sub_intent`
- `path_scope`
- `keyword_hints`
- `symbol_candidates`
- `requested_layers`
- `retrieval_spec`
- `retrieval_constraints`
- `query_plan`
### 4.5. Что возвращает retrieval
Сырые строки RAG затем нормализуются в `RetrievalResult`, который содержит:
- `target_symbol_candidates`
- `resolved_symbol`
- `symbol_resolution_status`
- `file_candidates`
- `code_chunks`
- `relations`
- `semantic_hints`
- `entrypoints`
- `test_candidates`
- `layer_outcomes`
- `missing_layers`
- `raw_rows`
- `retrieval_report`
### 4.6. Как из retrieval строится evidence
`build_evidence_bundle(...)` собирает `EvidenceBundle`.
Ключевые поля:
- `resolved_intent`
- `resolved_sub_intent`
- `resolved_target`
- `target_type`
- `target_symbol_candidates`
- `file_candidates`
- `code_chunks`
- `relations`
- `entrypoints`
- `test_evidence`
- `evidence_count`
- `sufficient`
- `failure_reasons`
- `retrieval_summary`
Важно: `evidence_count` сейчас считается по количеству `code_chunks`. Это еще одно подтверждение, что runtime сегодня code-first.
## 5. Evidence Gate
В пайплайне есть два gate'а.
## 5.1. Pre-evidence gate
Расположение:
- `src/app/modules/agent/runtime/steps/gates/pre/evidence_gate.py`
Когда срабатывает:
- после retrieval
- после сборки `EvidenceBundle`
- до вызова LLM
Задача:
- понять, достаточно ли evidence для уверенного ответа
- выдать `passed/failure_reasons/degraded_message`
Как работает сейчас:
- для `OPEN_FILE` требует найденный path/file и хотя бы один `C0` chunk
- для `EXPLAIN` требует target symbol и минимум 2 evidence chunk'а
- для `FIND_TESTS` требует target и хотя бы один test candidate
- для `FIND_ENTRYPOINTS` требует хотя бы один entrypoint
- для остальных сценариев требует минимум 1 evidence
Что возвращает:
- `passed`
- `failure_reasons`
- `degraded_message`
Ограничение:
- логика pre-gate пока написана в терминах code sub-intents
- docs-сценарии там явно не моделированы
## 5.2. Post-evidence gate
Расположение:
- `src/app/modules/agent/runtime/steps/gates/post/post_gate.py`
Когда срабатывает:
- после генерации draft answer
- до возврата финального ответа
Задача:
- проверить groundedness черновика
- убедиться, что ответ действительно опирается на evidence
- решить, нужен ли `repair`
Что проверяет:
- что ответ не пустой
- что degraded/not_found/ambiguous-ответы содержат обязательные guardrail-фразы
- что normal answer не слишком общий
- что упоминаются обязательные факты из curated evidence
- что нет явной semantic leakage или contradictions
Отдельные проверки есть для:
- `FIND_ENTRYPOINTS`
- `EXPLAIN`
- `ARCHITECTURE`
- `TRACE_FLOW`
Если gate не проходит, он возвращает:
- `passed=False`
- `action="repair"`
- список `reasons`
После этого runtime может сделать дополнительный шаг `repair` через LLM.
Ограничение:
- post-gate тоже пока ориентирован на code-oriented sub-intents
- docs-сабинтенты для него еще не описаны отдельными правилами
## 6. Обращение к LLM
### 6.1. Где вызывается LLM
В `pipeline_setup_v3` есть два места использования LLM:
1. В классификации интента внутри `IntentClassifierV2`
2. В финальной генерации ответа внутри `AgentRuntimeExecutor`
### 6.2. Prompt для классификации интента
Используется prompt:
- `rag_intent_router_v2`
Назначение:
- если deterministic rules не дали результата, LLM выбирает интент
Текущий prompt исторически описывает старые имена интентов, поэтому его еще нужно синхронизировать с новым docs/fallback контрактом.
### 6.3. Prompt'ы для генерации ответа
Prompt selector сейчас выбирает:
- `code_qa_architecture_answer`
- `code_qa_explain_answer`
- `code_qa_explain_local_answer`
- `code_qa_find_entrypoints_answer`
- `code_qa_find_tests_answer`
- `code_qa_general_answer`
- `code_qa_open_file_answer`
- `code_qa_trace_flow_answer`
- `code_qa_degraded_answer`
Дополнительно для repair используется:
- `code_qa_repair_answer`
### 6.4. Как строится payload для LLM
Перед вызовом LLM runtime собирает:
- `AnswerSynthesisInput`
- `EvidenceBundle`
- `prompt_payload`
В payload передаются:
- `user_question`
- `resolved_target`
- `answer_mode`
- `evidence_summary`
- `retrieval_summary`
- curated facts
- обязательные для упоминания сущности, методы, связи, шаги и т.д.
То есть LLM не получает просто "вопрос и куски текста", а получает уже структурированный grounded payload.
### 6.5. Что важно про текущее состояние prompt'ов
Сейчас runtime prompt selection и prompt contracts явно заточены под code QA.
Это значит:
- для `CODE_QA` full chain оформлен хорошо
- для `DOCUMENTATION_EXPLAIN` routing и retrieval есть, но отдельного docs answer-prompt слоя пока нет
- для docs full-chain пока не хватает собственных prompt names, prompt payload contract и post-gate правил
## 7. Что именно сейчас проверяет `pipeline_setup_v3`
YAML-кейс может проверять четыре группы ожиданий:
- `router`
- `retrieval`
- `llm`
- `pipeline`
Примеры ожиданий:
- ожидаемый `intent`
- ожидаемый `sub_intent`
- нужные слои в `layers_include`
- что retrieval не пустой
- что в answer есть нужные фразы
- какой `answer_mode` получился
## 8. Ключевые выводы о текущей архитектуре
### 8.1. Что уже сделано хорошо
- `pipeline_setup_v3` работает поверх реальных runtime-компонентов
- есть явный контракт между router → retrieval → evidence → answer
- есть два evidence gate
- есть structured diagnostics
- есть нормализованные типы `RetrievalRequest`, `RetrievalResult`, `EvidenceBundle`
### 8.2. Что остается code-first
- pre-evidence gate
- post-evidence gate
- prompt selector
- набор answer prompts
- часть логики нормализации evidence
### 8.3. Что это значит для docs use case
Сейчас docs use case уже частично внедрен:
- есть docs intent
- есть docs sub-intents
- есть docs layer mapping
- есть docs retrieval profile
Но для полноценного `full_chain` по документации еще не хватает:
- docs-oriented pre-gate правил
- docs-oriented post-gate правил
- docs-specific answer prompts
- docs-specific synthesis contract
- отдельных full-chain test cases для `DOCUMENTATION_EXPLAIN` и `FALLBACK`
## 9. Краткое резюме по компонентам
### Intent Router
Назначение:
- классифицировать запрос
- построить retrieval plan
- задать evidence policy
Выход:
- `IntentRouterResult`
### RAG
Назначение:
- вернуть evidence из многослойного индекса
Выход:
- `RetrievalResult`
- затем `EvidenceBundle`
### Pre-evidence gate
Назначение:
- решить, можно ли вообще уверенно отвечать
Выход:
- `passed/failure_reasons/degraded_message`
### Post-evidence gate
Назначение:
- проверить, grounded ли уже сгенерированный ответ
Выход:
- `return` или `repair`
### LLM
Назначение:
- классификация сложных интентов
- генерация финального ответа
- repair ответа при провале post-gate
Текущий фокус:
- в первую очередь `CODE_QA`
+105
View File
@@ -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, который будет генерировать новые кейсы в нужном формате.
+1
View File
@@ -4,3 +4,4 @@ markers =
router_rag: intent-router -> rag integration pipeline tests router_rag: intent-router -> rag integration pipeline tests
full_chain: intent-router -> rag -> llm integration pipeline tests full_chain: intent-router -> rag -> llm integration pipeline tests
code_qa_eval: CODE_QA golden evaluation harness (fixture + real-adapter; needs DB for full run) code_qa_eval: CODE_QA golden evaluation harness (fixture + real-adapter; needs DB for full run)
docs_qa_eval: DOCS_QA golden evaluation harness
@@ -0,0 +1,265 @@
# Runtime Trace: 20260406-153629-250147960243
- active_rag_session_id: fdf3ff03-81f0-4772-b68e-250147960243
## request
```json
{
"request_id": "req_64906a91cdb6487ca2737a091cdaddab",
"session_id": "as_d60e71ff542642649c81221db325cbcc",
"active_rag_session_id": "fdf3ff03-81f0-4772-b68e-250147960243",
"process_version": "v2",
"created_at": "2026-04-06T15:36:29.264730+00:00",
"message": "Объясни по документации, как работает /health"
}
```
## process.v2
```json
{
"event": "intent_routed",
"routing_domain": "DOCS",
"intent": "DOC_EXPLAIN",
"subintent": "SUMMARY",
"normalized_query": "Объясни по документации, как работает /health",
"target_terms": [
"/health",
"как",
"работает"
],
"anchors": {
"terms": [
"/health",
"как",
"работает"
],
"entity_names": [],
"file_names": [
"/health"
],
"process_domain": null,
"process_subdomain": null
},
"confidence": 1.0,
"routing_mode": "deterministic",
"llm_router_used": false,
"reason_short": "deterministic signal",
"rag_session_id": "fdf3ff03-81f0-4772-b68e-250147960243"
}
```
## process.v2.retrieval_policy
```json
{
"event": "retrieval_plan_resolved",
"profile": "docs_explain_summary",
"layers": [
"D1_DOCUMENT_CATALOG",
"D3_ENTITY_CATALOG",
"D0_DOC_CHUNKS"
],
"limit": 12
}
```
## process.v2.rag_retrieval
```json
{
"event": "rag_rows_fetched",
"profile": "docs_explain_summary",
"row_count": 12,
"rows": [
{
"layer": "D1_DOCUMENT_CATALOG",
"path": "docs/README.md",
"title": "Индекс технической документации test_echo_app",
"document_id": "index.test_echo_app_docs",
"entity_name": "",
"summary_text": "- Purpose: точка входа в техническую документацию сервиса `test_echo_app`.\n- Scope: архитектура, HTTP API control plane, цикл отправки уведомлений, health-модель и каталог ошибок.\n- Canonical structure: `docs/architecture`, `docs/api`, `docs/logic`, `docs/domains`, `docs/errors`.\n- Primary parent doc: [Архитектура Telegram Notify App](./architecture/telegram-notify-app-overview.md).\n- Navigation: ",
"section_path": "",
"content_preview": "- Purpose: точка входа в техническую документацию сервиса `test_echo_app`.\n- Scope: архитектура, HTTP API control plane, цикл отправки уведомлений, health-модель и каталог ошибок.\n- Canonical structure: `docs/architecture`, `docs/api`, `docs/logic`, `docs/domains`, `docs/errors`.\n- Primary parent doc: [Архитектура Telegram Notify App](./architecture/telegram-notify-app-overview.md).\n- Navigation: "
},
{
"layer": "D1_DOCUMENT_CATALOG",
"path": "docs/architecture/telegram-notify-app-overview.md",
"title": "Архитектура Telegram Notify App",
"document_id": "architecture.telegram_notify_app",
"entity_name": "",
"summary_text": "- Purpose: сервис поднимает HTTP control plane и фоновый worker для отправки уведомлений в Telegram.\n- Entry point: `src/telegram_notify_app/main.py`.\n- Main components: `RuntimeManager`, `TelegramControlChannel`, `TelegramNotifyModule`, `TelegramNotifyWorker`, `TelegramSendService`.\n- Configuration: `config/config.yaml` или путь из `CONFIG_PATH`.\n- Related API: [`/health`](../api/health-endpoint.",
"section_path": "",
"content_preview": "- Purpose: сервис поднимает HTTP control plane и фоновый worker для отправки уведомлений в Telegram.\n- Entry point: `src/telegram_notify_app/main.py`.\n- Main components: `RuntimeManager`, `TelegramControlChannel`, `TelegramNotifyModule`, `TelegramNotifyWorker`, `TelegramSendService`.\n- Configuration: `config/config.yaml` или путь из `CONFIG_PATH`.\n- Related API: [`/health`](../api/health-endpoint."
},
{
"layer": "D3_ENTITY_CATALOG",
"path": "docs/architecture/telegram-notify-app-overview.md",
"title": "TelegramNotifyWorker",
"document_id": "architecture.telegram_notify_app",
"entity_name": "TelegramNotifyWorker",
"summary_text": "",
"section_path": "",
"content_preview": "TelegramNotifyWorker"
},
{
"layer": "D3_ENTITY_CATALOG",
"path": "docs/architecture/telegram-notify-app-overview.md",
"title": "TelegramNotifyModule",
"document_id": "architecture.telegram_notify_app",
"entity_name": "TelegramNotifyModule",
"summary_text": "",
"section_path": "",
"content_preview": "TelegramNotifyModule"
},
{
"layer": "D3_ENTITY_CATALOG",
"path": "docs/architecture/telegram-notify-app-overview.md",
"title": "TelegramSendService",
"document_id": "architecture.telegram_notify_app",
"entity_name": "TelegramSendService",
"summary_text": "",
"section_path": "",
"content_preview": "TelegramSendService"
},
{
"layer": "D3_ENTITY_CATALOG",
"path": "docs/architecture/telegram-notify-app-overview.md",
"title": "TelegramControlChannel",
"document_id": "architecture.telegram_notify_app",
"entity_name": "TelegramControlChannel",
"summary_text": "",
"section_path": "",
"content_preview": "TelegramControlChannel"
},
{
"layer": "D3_ENTITY_CATALOG",
"path": "docs/architecture/telegram-notify-app-overview.md",
"title": "RuntimeManager",
"document_id": "architecture.telegram_notify_app",
"entity_name": "RuntimeManager",
"summary_text": "",
"section_path": "",
"content_preview": "RuntimeManager"
},
{
"layer": "D0_DOC_CHUNKS",
"path": "docs/architecture/telegram-notify-app-overview.md",
"title": "architecture.telegram_notify_app:Связанные документы",
"document_id": "architecture.telegram_notify_app",
"entity_name": "",
"summary_text": "",
"section_path": "Архитектура Telegram Notify App > Details > Связанные документы",
"content_preview": "- [API /health](../api/health-endpoint.md)\n- [API /actions/{action}](../api/control-actions-endpoint.md)\n- [API /send](../api/send-message-endpoint.md)\n- [Логика цикла отправки уведомлений](../logic/telegram-notification-loop.md)\n- [Доменная модель runtime health](../domains/runtime-health-entity.md)"
},
{
"layer": "D0_DOC_CHUNKS",
"path": "docs/README.md",
"title": "index.test_echo_app_docs:Навигация",
"document_id": "index.test_echo_app_docs",
"entity_name": "",
"summary_text": "",
"section_path": "Индекс технической документации test_echo_app > Details > Навигация",
"content_preview": "- [Архитектура Telegram Notify App](./architecture/telegram-notify-app-overview.md)\n- [API /health](./api/health-endpoint.md)\n- [API /actions/{action}](./api/control-actions-endpoint.md)\n- [API /send](./api/send-message-endpoint.md)\n- [Логика цикла отправки уведомлений](./logic/telegram-notification-loop.md)\n- [Доменная модель runtime health](./domains/runtime-health-entity.md)\n- [Каталог ошибок]("
},
{
"layer": "D0_DOC_CHUNKS",
"path": "docs/architecture/telegram-notify-app-overview.md",
"title": "architecture.telegram_notify_app:Операторские и мониторинговые клиенты",
"document_id": "architecture.telegram_notify_app",
"entity_name": "",
"summary_text": "",
"section_path": "Архитектура Telegram Notify App > Details > Интеграции > Операторские и мониторинговые клиенты",
"content_preview": "- target: ext.operator_and_probes\n- target_type: external_system\n- direction: inbound\n- interaction: calls\n- via: HTTP `/health`, `/actions/{action}`, `/send`\n- purpose: диагностика, lifecycle-управление и ручная отправка сообщений\n- details:\n - transport: FastAPI + UvicornThreadRunner\n - status_mapping: non-ok health -> HTTP 503"
},
{
"layer": "D0_DOC_CHUNKS",
"path": "docs/README.md",
"title": "index.test_echo_app_docs:Summary",
"document_id": "index.test_echo_app_docs",
"entity_name": "",
"summary_text": "",
"section_path": "Индекс технической документации test_echo_app > Summary",
"content_preview": "- Purpose: точка входа в техническую документацию сервиса `test_echo_app`.\n- Scope: архитектура, HTTP API control plane, цикл отправки уведомлений, health-модель и каталог ошибок.\n- Canonical structure: `docs/architecture`, `docs/api`, `docs/logic`, `docs/domains`, `docs/errors`.\n- Primary parent doc: [Архитектура Telegram Notify App](./architecture/telegram-notify-app-overview.md).\n- Navigation: "
},
{
"layer": "D0_DOC_CHUNKS",
"path": "docs/architecture/telegram-notify-app-overview.md",
"title": "architecture.telegram_notify_app:Контекст",
"document_id": "architecture.telegram_notify_app",
"entity_name": "",
"summary_text": "",
"section_path": "Архитектура Telegram Notify App > Details > Контекст",
"content_preview": "Архитектурный документ описывает состав runtime и связи между контейнероподобными компонентами приложения. Детали контрактов HTTP API вынесены в документы endpoint'ов, а сценарий фоновой отправки и health-модель описаны на отдельных страницах."
}
]
}
```
## process.v2.evidence
```json
{
"event": "evidence_assembled",
"mode": "summary",
"document_count": 1
}
```
## workflow.v2.summary
```json
{
"event": "workflow_started",
"workflow_id": "v2.docs_explain.summary"
}
```
## workflow.v2.summary.llm
```json
{
"event": "request",
"prompt_name": "v2_docs_explain.summary_answer",
"system_prompt": "Ты объясняешь документацию только на основе найденных SUMMARY-блоков.\nИспользуй только факты из входного контекста.\nЕсли информации мало, прямо скажи об этом.\nВ конце перечисли файлы, на которые ты опирался.",
"user_prompt": "Запрос пользователя:\nОбъясни по документации, как работает /health\n\nНайденные SUMMARY-блоки:\n\n1. path: docs/architecture/telegram-notify-app-overview.md\ntitle: Архитектура Telegram Notify App\nsummary: - Purpose: сервис поднимает HTTP control plane и фоновый worker для отправки уведомлений в Telegram.\n- Entry point: `src/telegram_notify_app/main.py`.\n- Main components: `RuntimeManager`, `TelegramControlChannel`, `TelegramNotifyModule`, `TelegramNotifyWorker`, `TelegramSendService`.\n- Configuration: `config/config.yaml` или путь из `CONFIG_PATH`.\n- Related API: [`/health`](../api/health-endpoint.md), [`/actions/{action}`](../api/control-actions-endpoint.md), [`/send`](../api/send-message-endpoint.md).\n- Related logic: [цикл отправки уведомлений](../logic/telegram-notification-loop.md).\n- Related domain: [runtime health](../domains/runtime-health-entity.md).",
"log_context": "agent:req_64906a91cdb6487ca2737a091cdaddab"
}
```
## workflow.v2.summary.llm
```json
{
"event": "response",
"text": "На основе представленной информации, можно сказать следующее:\n\nСервис \"Telegram Notify App\" предоставляет HTTP-интерфейс, включая endpoint `/health`. Эта информация содержится в блоке, где упомянуты связанные API (Related API). Однако подробностей о том, как именно работает данный endpoint, его функциональность или формат ответа, в предоставленных данных нет.\n\nФайлы, на которые я опирался:\n- docs/architecture/telegram-notify-app-overview.md"
}
```
## workflow.v2.summary
```json
{
"event": "workflow_trace_flushed",
"workflow_id": "v2.docs_explain.summary",
"steps": [
{
"step_id": "generate_summary_answer",
"title": "Сборка ответа по summary",
"input": {},
"output": {
"answer_length": 444
}
}
]
}
```
## workflow.v2.summary
```json
{
"event": "workflow_completed",
"workflow_id": "v2.docs_explain.summary"
}
```
## result
```json
{
"status": "done",
"answer": "На основе представленной информации, можно сказать следующее:\n\nСервис \"Telegram Notify App\" предоставляет HTTP-интерфейс, включая endpoint `/health`. Эта информация содержится в блоке, где упомянуты связанные API (Related API). Однако подробностей о том, как именно работает данный endpoint, его функциональность или формат ответа, в предоставленных данных нет.\n\nФайлы, на которые я опирался:\n- docs/architecture/telegram-notify-app-overview.md",
"completed_at": "2026-04-06T15:36:31.411613+00:00"
}
```
@@ -0,0 +1,130 @@
# Runtime Trace: 20260407-175918-b17b76678614
- active_rag_session_id: 94851e51-1514-4a77-9570-b17b76678614
## request
```json
{
"request_id": "req_d9dae665c88b476db700a3f7bd210370",
"session_id": "as_da5ddd4aacd94ec5b7078dd69e06c9c6",
"active_rag_session_id": "94851e51-1514-4a77-9570-b17b76678614",
"process_version": "v1",
"created_at": "2026-04-07T17:59:18.592170+00:00",
"message": "Ты тут?"
}
```
## workflow.v1
```json
{
"event": "workflow_started",
"workflow_id": "v1.flow_main"
}
```
## workflow.v1
```json
{
"event": "step_started",
"workflow_id": "v1.flow_main",
"step_id": "prepare_user_message",
"input": {}
}
```
## workflow.v1
```json
{
"event": "step_completed",
"workflow_id": "v1.flow_main",
"step_id": "prepare_user_message",
"output": {
"prepared_message_length": 7
}
}
```
## workflow.v1
```json
{
"event": "step_started",
"workflow_id": "v1.flow_main",
"step_id": "generate_answer",
"input": {
"prompt_name": "v1_flow_main.answer",
"prepared_message_length": 7
}
}
```
## workflow.v1.llm
```json
{
"event": "request",
"prompt_name": "v1_flow_main.answer",
"system_prompt": "Ты полезный ассистент.\nОтветь на сообщение пользователя по существу.\nНе придумывай факты, если данных недостаточно.\nЕсли пользователь пишет по-русски, отвечай по-русски.",
"user_prompt": "Ты тут?",
"log_context": "agent:req_d9dae665c88b476db700a3f7bd210370"
}
```
## workflow.v1.llm
```json
{
"event": "response",
"text": "Да, я здесь! Чем могу помочь?"
}
```
## workflow.v1
```json
{
"event": "step_completed",
"workflow_id": "v1.flow_main",
"step_id": "generate_answer",
"output": {
"answer_length": 29
}
}
```
## workflow.v1
```json
{
"event": "step_started",
"workflow_id": "v1.flow_main",
"step_id": "finalize_answer",
"input": {
"answer_length_before_strip": 29
}
}
```
## workflow.v1
```json
{
"event": "step_completed",
"workflow_id": "v1.flow_main",
"step_id": "finalize_answer",
"output": {
"answer_length": 29
}
}
```
## workflow.v1
```json
{
"event": "workflow_completed",
"workflow_id": "v1.flow_main"
}
```
## result
```json
{
"status": "done",
"answer": "Да, я здесь! Чем могу помочь?",
"completed_at": "2026-04-07T17:59:19.326182+00:00"
}
```
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,622 @@
# Runtime Trace: 20260407-182058-3f56c69c7290
- active_rag_session_id: c8b893cc-cb13-4493-a6d1-3f56c69c7290
## request
```json
{
"request_id": "req_bab9c8812ac94847bb102cba68516f10",
"session_id": "as_4fdccc9c55c549faad8f3ef379371129",
"active_rag_session_id": "c8b893cc-cb13-4493-a6d1-3f56c69c7290",
"process_version": "v2",
"created_at": "2026-04-07T18:20:58.679614+00:00",
"message": "Как работает метод health?"
}
```
## process.v2
```json
{
"event": "intent_routed",
"routing_domain": "DOCS",
"intent": "DOC_EXPLAIN",
"subintent": "SUMMARY",
"normalized_query": "Как работает метод health?",
"target_terms": [
"метод",
"health"
],
"anchors": {
"entity_names": [],
"file_names": [],
"endpoint_paths": [],
"target_doc_hints": [],
"matched_aliases": [],
"process_domain": null,
"process_subdomain": null,
"signal_types": []
},
"confidence": 0.75,
"routing_mode": "llm_default",
"llm_router_used": true,
"reason_short": "Запрос на понимание работы конкретного метода \"health\".",
"rag_session_id": "c8b893cc-cb13-4493-a6d1-3f56c69c7290"
}
```
## process.v2.pipeline
```json
{
"event": "router_resolved",
"domain": "DOCS",
"intent": "DOC_EXPLAIN",
"subintent": "SUMMARY",
"confidence": 0.75
}
```
## process.v2.pipeline
```json
{
"event": "anchors_extracted",
"signal_types": [],
"endpoint_paths": [],
"target_doc_hints": [],
"matched_aliases": [],
"target_terms": [
"метод",
"health"
]
}
```
## process.v2.pipeline
```json
{
"event": "alias_resolution",
"resolved_aliases": [],
"target_doc_hints": []
}
```
## process.v2.retrieval_policy
```json
{
"event": "retrieval_plan_resolved",
"profile": "docs_summary_generic",
"layers": [
"D1_DOCUMENT_CATALOG",
"D0_DOC_CHUNKS"
],
"limit": 8,
"filters": {
"target_doc_hints": [],
"prefer_path_prefixes": [
"docs/"
],
"prefer_like_patterns": []
}
}
```
## process.v2.pipeline
```json
{
"event": "retrieval_profile_selected",
"profile": "docs_summary_generic",
"layers": [
"D1_DOCUMENT_CATALOG",
"D0_DOC_CHUNKS"
],
"filters": {
"target_doc_hints": [],
"prefer_path_prefixes": [
"docs/"
],
"prefer_like_patterns": []
}
}
```
## process.v2.rag_retrieval
```json
{
"event": "rag_rows_fetched",
"profile": "docs_summary_generic",
"row_count": 8,
"rows": [
{
"layer": "D1_DOCUMENT_CATALOG",
"path": "docs/architecture/telegram-notify-app-overview.md",
"title": "Архитектура Telegram Notify App",
"document_id": "architecture.telegram_notify_app",
"entity_name": "",
"summary_text": "- Purpose: сервис поднимает HTTP control plane и фоновый worker для отправки уведомлений в Telegram.\n- Entry point: `src/telegram_notify_app/main.py`.\n- Main components: `RuntimeManager`, `TelegramControlChannel`, `TelegramNotifyModule`, `TelegramNotifyWorker`, `TelegramSendService`.\n- Configuration: `config/config.yaml` или путь из `CONFIG_PATH`.\n- Related API: [`/health`](../api/health-endpoint.",
"section_path": "",
"content_preview": "- Purpose: сервис поднимает HTTP control plane и фоновый worker для отправки уведомлений в Telegram.\n- Entry point: `src/telegram_notify_app/main.py`.\n- Main components: `RuntimeManager`, `TelegramControlChannel`, `TelegramNotifyModule`, `TelegramNotifyWorker`, `TelegramSendService`.\n- Configuration: `config/config.yaml` или путь из `CONFIG_PATH`.\n- Related API: [`/health`](../api/health-endpoint."
},
{
"layer": "D1_DOCUMENT_CATALOG",
"path": "docs/README.md",
"title": "Индекс технической документации test_echo_app",
"document_id": "index.test_echo_app_docs",
"entity_name": "",
"summary_text": "- Purpose: точка входа в техническую документацию сервиса `test_echo_app`.\n- Scope: архитектура, HTTP API control plane, цикл отправки уведомлений, health-модель и каталог ошибок.\n- Canonical structure: `docs/architecture`, `docs/api`, `docs/logic`, `docs/domains`, `docs/errors`.\n- Primary parent doc: [Архитектура Telegram Notify App](./architecture/telegram-notify-app-overview.md).\n- Navigation: ",
"section_path": "",
"content_preview": "- Purpose: точка входа в техническую документацию сервиса `test_echo_app`.\n- Scope: архитектура, HTTP API control plane, цикл отправки уведомлений, health-модель и каталог ошибок.\n- Canonical structure: `docs/architecture`, `docs/api`, `docs/logic`, `docs/domains`, `docs/errors`.\n- Primary parent doc: [Архитектура Telegram Notify App](./architecture/telegram-notify-app-overview.md).\n- Navigation: "
},
{
"layer": "D0_DOC_CHUNKS",
"path": "docs/architecture/telegram-notify-app-overview.md",
"title": "architecture.telegram_notify_app:Операторские и мониторинговые клиенты",
"document_id": "architecture.telegram_notify_app",
"entity_name": "",
"summary_text": "",
"section_path": "Архитектура Telegram Notify App > Details > Интеграции > Операторские и мониторинговые клиенты",
"content_preview": "- target: ext.operator_and_probes\n- target_type: external_system\n- direction: inbound\n- interaction: calls\n- via: HTTP `/health`, `/actions/{action}`, `/send`\n- purpose: диагностика, lifecycle-управление и ручная отправка сообщений\n- details:\n - transport: FastAPI + UvicornThreadRunner\n - status_mapping: non-ok health -> HTTP 503"
},
{
"layer": "D0_DOC_CHUNKS",
"path": "docs/architecture/telegram-notify-app-overview.md",
"title": "architecture.telegram_notify_app:Связанные документы",
"document_id": "architecture.telegram_notify_app",
"entity_name": "",
"summary_text": "",
"section_path": "Архитектура Telegram Notify App > Details > Связанные документы",
"content_preview": "- [API /health](../api/health-endpoint.md)\n- [API /actions/{action}](../api/control-actions-endpoint.md)\n- [API /send](../api/send-message-endpoint.md)\n- [Логика цикла отправки уведомлений](../logic/telegram-notification-loop.md)\n- [Доменная модель runtime health](../domains/runtime-health-entity.md)"
},
{
"layer": "D0_DOC_CHUNKS",
"path": "docs/README.md",
"title": "index.test_echo_app_docs:Навигация",
"document_id": "index.test_echo_app_docs",
"entity_name": "",
"summary_text": "",
"section_path": "Индекс технической документации test_echo_app > Details > Навигация",
"content_preview": "- [Архитектура Telegram Notify App](./architecture/telegram-notify-app-overview.md)\n- [API /health](./api/health-endpoint.md)\n- [API /actions/{action}](./api/control-actions-endpoint.md)\n- [API /send](./api/send-message-endpoint.md)\n- [Логика цикла отправки уведомлений](./logic/telegram-notification-loop.md)\n- [Доменная модель runtime health](./domains/runtime-health-entity.md)\n- [Каталог ошибок]("
},
{
"layer": "D0_DOC_CHUNKS",
"path": "docs/architecture/telegram-notify-app-overview.md",
"title": "architecture.telegram_notify_app:Summary",
"document_id": "architecture.telegram_notify_app",
"entity_name": "",
"summary_text": "",
"section_path": "Архитектура Telegram Notify App > Summary",
"content_preview": "- Purpose: сервис поднимает HTTP control plane и фоновый worker для отправки уведомлений в Telegram.\n- Entry point: `src/telegram_notify_app/main.py`.\n- Main components: `RuntimeManager`, `TelegramControlChannel`, `TelegramNotifyModule`, `TelegramNotifyWorker`, `TelegramSendService`.\n- Configuration: `config/config.yaml` или путь из `CONFIG_PATH`.\n- Related API: [`/health`](../api/health-endpoint."
},
{
"layer": "D0_DOC_CHUNKS",
"path": "docs/README.md",
"title": "index.test_echo_app_docs:Summary",
"document_id": "index.test_echo_app_docs",
"entity_name": "",
"summary_text": "",
"section_path": "Индекс технической документации test_echo_app > Summary",
"content_preview": "- Purpose: точка входа в техническую документацию сервиса `test_echo_app`.\n- Scope: архитектура, HTTP API control plane, цикл отправки уведомлений, health-модель и каталог ошибок.\n- Canonical structure: `docs/architecture`, `docs/api`, `docs/logic`, `docs/domains`, `docs/errors`.\n- Primary parent doc: [Архитектура Telegram Notify App](./architecture/telegram-notify-app-overview.md).\n- Navigation: "
},
{
"layer": "D0_DOC_CHUNKS",
"path": "docs/architecture/telegram-notify-app-overview.md",
"title": "architecture.telegram_notify_app:Интеграционные сценарии",
"document_id": "architecture.telegram_notify_app",
"entity_name": "",
"summary_text": "",
"section_path": "Архитектура Telegram Notify App > Details > Интеграционные сценарии",
"content_preview": "1. При старте `main()` загружает YAML-конфиг, извлекает host, port и интервал отправки, затем собирает runtime.\n2. `RuntimeManager` регистрирует `TelegramControlChannel` для HTTP control plane.\n3. `TelegramNotifyModule` добавляет `TelegramNotifyWorker` и `TelegramSendService` в runtime.\n4. Внешний клиент вызывает endpoint'ы control plane для health-check, lifecycle-операций или ручной отправки.\n5."
}
]
}
```
## process.v2.pipeline
```json
{
"event": "candidate_generation",
"query": "Как работает метод health?",
"profile": "docs_summary_generic",
"details": {
"target_doc_hints": [],
"candidates_before_ranking": [
"docs/architecture/telegram-notify-app-overview.md",
"docs/README.md",
"docs/architecture/telegram-notify-app-overview.md",
"docs/architecture/telegram-notify-app-overview.md",
"docs/README.md",
"docs/architecture/telegram-notify-app-overview.md",
"docs/README.md",
"docs/architecture/telegram-notify-app-overview.md"
]
},
"resolved_aliases": [],
"target_doc_hints": [],
"candidate_docs_before_ranking": [
{
"layer": "D1_DOCUMENT_CATALOG",
"path": "docs/architecture/telegram-notify-app-overview.md",
"title": "Архитектура Telegram Notify App",
"document_id": "architecture.telegram_notify_app",
"entity_name": "",
"summary_text": "- Purpose: сервис поднимает HTTP control plane и фоновый worker для отправки уведомлений в Telegram.\n- Entry point: `src/telegram_notify_app/main.py`.\n- Main components: `RuntimeManager`, `TelegramControlChannel`, `TelegramNotifyModule`, `TelegramNotifyWorker`, `TelegramSendService`.\n- Configuration: `config/config.yaml` или путь из `CONFIG_PATH`.\n- Related API: [`/health`](../api/health-endpoint.",
"section_path": "",
"content_preview": "- Purpose: сервис поднимает HTTP control plane и фоновый worker для отправки уведомлений в Telegram.\n- Entry point: `src/telegram_notify_app/main.py`.\n- Main components: `RuntimeManager`, `TelegramControlChannel`, `TelegramNotifyModule`, `TelegramNotifyWorker`, `TelegramSendService`.\n- Configuration: `config/config.yaml` или путь из `CONFIG_PATH`.\n- Related API: [`/health`](../api/health-endpoint."
},
{
"layer": "D1_DOCUMENT_CATALOG",
"path": "docs/README.md",
"title": "Индекс технической документации test_echo_app",
"document_id": "index.test_echo_app_docs",
"entity_name": "",
"summary_text": "- Purpose: точка входа в техническую документацию сервиса `test_echo_app`.\n- Scope: архитектура, HTTP API control plane, цикл отправки уведомлений, health-модель и каталог ошибок.\n- Canonical structure: `docs/architecture`, `docs/api`, `docs/logic`, `docs/domains`, `docs/errors`.\n- Primary parent doc: [Архитектура Telegram Notify App](./architecture/telegram-notify-app-overview.md).\n- Navigation: ",
"section_path": "",
"content_preview": "- Purpose: точка входа в техническую документацию сервиса `test_echo_app`.\n- Scope: архитектура, HTTP API control plane, цикл отправки уведомлений, health-модель и каталог ошибок.\n- Canonical structure: `docs/architecture`, `docs/api`, `docs/logic`, `docs/domains`, `docs/errors`.\n- Primary parent doc: [Архитектура Telegram Notify App](./architecture/telegram-notify-app-overview.md).\n- Navigation: "
},
{
"layer": "D0_DOC_CHUNKS",
"path": "docs/architecture/telegram-notify-app-overview.md",
"title": "architecture.telegram_notify_app:Операторские и мониторинговые клиенты",
"document_id": "architecture.telegram_notify_app",
"entity_name": "",
"summary_text": "",
"section_path": "Архитектура Telegram Notify App > Details > Интеграции > Операторские и мониторинговые клиенты",
"content_preview": "- target: ext.operator_and_probes\n- target_type: external_system\n- direction: inbound\n- interaction: calls\n- via: HTTP `/health`, `/actions/{action}`, `/send`\n- purpose: диагностика, lifecycle-управление и ручная отправка сообщений\n- details:\n - transport: FastAPI + UvicornThreadRunner\n - status_mapping: non-ok health -> HTTP 503"
},
{
"layer": "D0_DOC_CHUNKS",
"path": "docs/architecture/telegram-notify-app-overview.md",
"title": "architecture.telegram_notify_app:Связанные документы",
"document_id": "architecture.telegram_notify_app",
"entity_name": "",
"summary_text": "",
"section_path": "Архитектура Telegram Notify App > Details > Связанные документы",
"content_preview": "- [API /health](../api/health-endpoint.md)\n- [API /actions/{action}](../api/control-actions-endpoint.md)\n- [API /send](../api/send-message-endpoint.md)\n- [Логика цикла отправки уведомлений](../logic/telegram-notification-loop.md)\n- [Доменная модель runtime health](../domains/runtime-health-entity.md)"
},
{
"layer": "D0_DOC_CHUNKS",
"path": "docs/README.md",
"title": "index.test_echo_app_docs:Навигация",
"document_id": "index.test_echo_app_docs",
"entity_name": "",
"summary_text": "",
"section_path": "Индекс технической документации test_echo_app > Details > Навигация",
"content_preview": "- [Архитектура Telegram Notify App](./architecture/telegram-notify-app-overview.md)\n- [API /health](./api/health-endpoint.md)\n- [API /actions/{action}](./api/control-actions-endpoint.md)\n- [API /send](./api/send-message-endpoint.md)\n- [Логика цикла отправки уведомлений](./logic/telegram-notification-loop.md)\n- [Доменная модель runtime health](./domains/runtime-health-entity.md)\n- [Каталог ошибок]("
},
{
"layer": "D0_DOC_CHUNKS",
"path": "docs/architecture/telegram-notify-app-overview.md",
"title": "architecture.telegram_notify_app:Summary",
"document_id": "architecture.telegram_notify_app",
"entity_name": "",
"summary_text": "",
"section_path": "Архитектура Telegram Notify App > Summary",
"content_preview": "- Purpose: сервис поднимает HTTP control plane и фоновый worker для отправки уведомлений в Telegram.\n- Entry point: `src/telegram_notify_app/main.py`.\n- Main components: `RuntimeManager`, `TelegramControlChannel`, `TelegramNotifyModule`, `TelegramNotifyWorker`, `TelegramSendService`.\n- Configuration: `config/config.yaml` или путь из `CONFIG_PATH`.\n- Related API: [`/health`](../api/health-endpoint."
},
{
"layer": "D0_DOC_CHUNKS",
"path": "docs/README.md",
"title": "index.test_echo_app_docs:Summary",
"document_id": "index.test_echo_app_docs",
"entity_name": "",
"summary_text": "",
"section_path": "Индекс технической документации test_echo_app > Summary",
"content_preview": "- Purpose: точка входа в техническую документацию сервиса `test_echo_app`.\n- Scope: архитектура, HTTP API control plane, цикл отправки уведомлений, health-модель и каталог ошибок.\n- Canonical structure: `docs/architecture`, `docs/api`, `docs/logic`, `docs/domains`, `docs/errors`.\n- Primary parent doc: [Архитектура Telegram Notify App](./architecture/telegram-notify-app-overview.md).\n- Navigation: "
},
{
"layer": "D0_DOC_CHUNKS",
"path": "docs/architecture/telegram-notify-app-overview.md",
"title": "architecture.telegram_notify_app:Интеграционные сценарии",
"document_id": "architecture.telegram_notify_app",
"entity_name": "",
"summary_text": "",
"section_path": "Архитектура Telegram Notify App > Details > Интеграционные сценарии",
"content_preview": "1. При старте `main()` загружает YAML-конфиг, извлекает host, port и интервал отправки, затем собирает runtime.\n2. `RuntimeManager` регистрирует `TelegramControlChannel` для HTTP control plane.\n3. `TelegramNotifyModule` добавляет `TelegramNotifyWorker` и `TelegramSendService` в runtime.\n4. Внешний клиент вызывает endpoint'ы control plane для health-check, lifecycle-операций или ручной отправки.\n5."
}
],
"sources": {
"seeded": [],
"metadata_lookup": [],
"semantic": [
{
"layer": "D1_DOCUMENT_CATALOG",
"path": "docs/architecture/telegram-notify-app-overview.md",
"title": "Архитектура Telegram Notify App",
"document_id": "architecture.telegram_notify_app",
"entity_name": "",
"summary_text": "- Purpose: сервис поднимает HTTP control plane и фоновый worker для отправки уведомлений в Telegram.\n- Entry point: `src/telegram_notify_app/main.py`.\n- Main components: `RuntimeManager`, `TelegramControlChannel`, `TelegramNotifyModule`, `TelegramNotifyWorker`, `TelegramSendService`.\n- Configuration: `config/config.yaml` или путь из `CONFIG_PATH`.\n- Related API: [`/health`](../api/health-endpoint.",
"section_path": "",
"content_preview": "- Purpose: сервис поднимает HTTP control plane и фоновый worker для отправки уведомлений в Telegram.\n- Entry point: `src/telegram_notify_app/main.py`.\n- Main components: `RuntimeManager`, `TelegramControlChannel`, `TelegramNotifyModule`, `TelegramNotifyWorker`, `TelegramSendService`.\n- Configuration: `config/config.yaml` или путь из `CONFIG_PATH`.\n- Related API: [`/health`](../api/health-endpoint."
},
{
"layer": "D1_DOCUMENT_CATALOG",
"path": "docs/README.md",
"title": "Индекс технической документации test_echo_app",
"document_id": "index.test_echo_app_docs",
"entity_name": "",
"summary_text": "- Purpose: точка входа в техническую документацию сервиса `test_echo_app`.\n- Scope: архитектура, HTTP API control plane, цикл отправки уведомлений, health-модель и каталог ошибок.\n- Canonical structure: `docs/architecture`, `docs/api`, `docs/logic`, `docs/domains`, `docs/errors`.\n- Primary parent doc: [Архитектура Telegram Notify App](./architecture/telegram-notify-app-overview.md).\n- Navigation: ",
"section_path": "",
"content_preview": "- Purpose: точка входа в техническую документацию сервиса `test_echo_app`.\n- Scope: архитектура, HTTP API control plane, цикл отправки уведомлений, health-модель и каталог ошибок.\n- Canonical structure: `docs/architecture`, `docs/api`, `docs/logic`, `docs/domains`, `docs/errors`.\n- Primary parent doc: [Архитектура Telegram Notify App](./architecture/telegram-notify-app-overview.md).\n- Navigation: "
},
{
"layer": "D0_DOC_CHUNKS",
"path": "docs/architecture/telegram-notify-app-overview.md",
"title": "architecture.telegram_notify_app:Операторские и мониторинговые клиенты",
"document_id": "architecture.telegram_notify_app",
"entity_name": "",
"summary_text": "",
"section_path": "Архитектура Telegram Notify App > Details > Интеграции > Операторские и мониторинговые клиенты",
"content_preview": "- target: ext.operator_and_probes\n- target_type: external_system\n- direction: inbound\n- interaction: calls\n- via: HTTP `/health`, `/actions/{action}`, `/send`\n- purpose: диагностика, lifecycle-управление и ручная отправка сообщений\n- details:\n - transport: FastAPI + UvicornThreadRunner\n - status_mapping: non-ok health -> HTTP 503"
},
{
"layer": "D0_DOC_CHUNKS",
"path": "docs/architecture/telegram-notify-app-overview.md",
"title": "architecture.telegram_notify_app:Связанные документы",
"document_id": "architecture.telegram_notify_app",
"entity_name": "",
"summary_text": "",
"section_path": "Архитектура Telegram Notify App > Details > Связанные документы",
"content_preview": "- [API /health](../api/health-endpoint.md)\n- [API /actions/{action}](../api/control-actions-endpoint.md)\n- [API /send](../api/send-message-endpoint.md)\n- [Логика цикла отправки уведомлений](../logic/telegram-notification-loop.md)\n- [Доменная модель runtime health](../domains/runtime-health-entity.md)"
},
{
"layer": "D0_DOC_CHUNKS",
"path": "docs/README.md",
"title": "index.test_echo_app_docs:Навигация",
"document_id": "index.test_echo_app_docs",
"entity_name": "",
"summary_text": "",
"section_path": "Индекс технической документации test_echo_app > Details > Навигация",
"content_preview": "- [Архитектура Telegram Notify App](./architecture/telegram-notify-app-overview.md)\n- [API /health](./api/health-endpoint.md)\n- [API /actions/{action}](./api/control-actions-endpoint.md)\n- [API /send](./api/send-message-endpoint.md)\n- [Логика цикла отправки уведомлений](./logic/telegram-notification-loop.md)\n- [Доменная модель runtime health](./domains/runtime-health-entity.md)\n- [Каталог ошибок]("
}
]
}
}
```
## process.v2.pipeline
```json
{
"event": "retrieval_executed",
"query": "Как работает метод health?",
"profile": "docs_summary_generic",
"row_count": 8,
"target_doc_hints": [],
"top_results": [
{
"layer": "D1_DOCUMENT_CATALOG",
"path": "docs/architecture/telegram-notify-app-overview.md",
"title": "Архитектура Telegram Notify App",
"document_id": "architecture.telegram_notify_app",
"entity_name": "",
"summary_text": "- Purpose: сервис поднимает HTTP control plane и фоновый worker для отправки уведомлений в Telegram.\n- Entry point: `src/telegram_notify_app/main.py`.\n- Main components: `RuntimeManager`, `TelegramControlChannel`, `TelegramNotifyModule`, `TelegramNotifyWorker`, `TelegramSendService`.\n- Configuration: `config/config.yaml` или путь из `CONFIG_PATH`.\n- Related API: [`/health`](../api/health-endpoint.",
"section_path": "",
"content_preview": "- Purpose: сервис поднимает HTTP control plane и фоновый worker для отправки уведомлений в Telegram.\n- Entry point: `src/telegram_notify_app/main.py`.\n- Main components: `RuntimeManager`, `TelegramControlChannel`, `TelegramNotifyModule`, `TelegramNotifyWorker`, `TelegramSendService`.\n- Configuration: `config/config.yaml` или путь из `CONFIG_PATH`.\n- Related API: [`/health`](../api/health-endpoint."
},
{
"layer": "D1_DOCUMENT_CATALOG",
"path": "docs/README.md",
"title": "Индекс технической документации test_echo_app",
"document_id": "index.test_echo_app_docs",
"entity_name": "",
"summary_text": "- Purpose: точка входа в техническую документацию сервиса `test_echo_app`.\n- Scope: архитектура, HTTP API control plane, цикл отправки уведомлений, health-модель и каталог ошибок.\n- Canonical structure: `docs/architecture`, `docs/api`, `docs/logic`, `docs/domains`, `docs/errors`.\n- Primary parent doc: [Архитектура Telegram Notify App](./architecture/telegram-notify-app-overview.md).\n- Navigation: ",
"section_path": "",
"content_preview": "- Purpose: точка входа в техническую документацию сервиса `test_echo_app`.\n- Scope: архитектура, HTTP API control plane, цикл отправки уведомлений, health-модель и каталог ошибок.\n- Canonical structure: `docs/architecture`, `docs/api`, `docs/logic`, `docs/domains`, `docs/errors`.\n- Primary parent doc: [Архитектура Telegram Notify App](./architecture/telegram-notify-app-overview.md).\n- Navigation: "
},
{
"layer": "D0_DOC_CHUNKS",
"path": "docs/architecture/telegram-notify-app-overview.md",
"title": "architecture.telegram_notify_app:Операторские и мониторинговые клиенты",
"document_id": "architecture.telegram_notify_app",
"entity_name": "",
"summary_text": "",
"section_path": "Архитектура Telegram Notify App > Details > Интеграции > Операторские и мониторинговые клиенты",
"content_preview": "- target: ext.operator_and_probes\n- target_type: external_system\n- direction: inbound\n- interaction: calls\n- via: HTTP `/health`, `/actions/{action}`, `/send`\n- purpose: диагностика, lifecycle-управление и ручная отправка сообщений\n- details:\n - transport: FastAPI + UvicornThreadRunner\n - status_mapping: non-ok health -> HTTP 503"
},
{
"layer": "D0_DOC_CHUNKS",
"path": "docs/architecture/telegram-notify-app-overview.md",
"title": "architecture.telegram_notify_app:Связанные документы",
"document_id": "architecture.telegram_notify_app",
"entity_name": "",
"summary_text": "",
"section_path": "Архитектура Telegram Notify App > Details > Связанные документы",
"content_preview": "- [API /health](../api/health-endpoint.md)\n- [API /actions/{action}](../api/control-actions-endpoint.md)\n- [API /send](../api/send-message-endpoint.md)\n- [Логика цикла отправки уведомлений](../logic/telegram-notification-loop.md)\n- [Доменная модель runtime health](../domains/runtime-health-entity.md)"
},
{
"layer": "D0_DOC_CHUNKS",
"path": "docs/README.md",
"title": "index.test_echo_app_docs:Навигация",
"document_id": "index.test_echo_app_docs",
"entity_name": "",
"summary_text": "",
"section_path": "Индекс технической документации test_echo_app > Details > Навигация",
"content_preview": "- [Архитектура Telegram Notify App](./architecture/telegram-notify-app-overview.md)\n- [API /health](./api/health-endpoint.md)\n- [API /actions/{action}](./api/control-actions-endpoint.md)\n- [API /send](./api/send-message-endpoint.md)\n- [Логика цикла отправки уведомлений](./logic/telegram-notification-loop.md)\n- [Доменная модель runtime health](./domains/runtime-health-entity.md)\n- [Каталог ошибок]("
}
]
}
```
## process.v2.evidence
```json
{
"event": "evidence_assembled",
"mode": "summary",
"document_count": 2,
"documents": [
"docs/README.md",
"docs/architecture/telegram-notify-app-overview.md"
]
}
```
## process.v2.pipeline
```json
{
"event": "evidence_assembled",
"mode": "summary",
"primary_doc": "docs/README.md",
"document_count": 2
}
```
## process.v2.pipeline
```json
{
"event": "ranking_explained",
"doc": "docs/README.md",
"score_breakdown": {
"semantic": 20,
"path_match": 0,
"filename_match": 0,
"alias_match": 0,
"anchor_boost": 0,
"target_doc_boost": 0,
"generic_penalty": 0
},
"score": 20,
"match_reason": "semantic_match"
}
```
## process.v2.pipeline
```json
{
"event": "ranking_explained",
"doc": "docs/architecture/telegram-notify-app-overview.md",
"score_breakdown": {
"semantic": 20,
"path_match": 0,
"filename_match": 0,
"alias_match": 0,
"anchor_boost": 0,
"target_doc_boost": 0,
"generic_penalty": 0
},
"score": 20,
"match_reason": "semantic_match"
}
```
## process.v2.pipeline
```json
{
"event": "ranking_explained",
"top_docs_after_ranking": [
{
"doc": "docs/README.md",
"score": 20,
"match_reason": "semantic_match"
},
{
"doc": "docs/architecture/telegram-notify-app-overview.md",
"score": 20,
"match_reason": "semantic_match"
}
],
"ranking_score_breakdown": [
{
"doc": "docs/README.md",
"score_breakdown": {
"semantic": 20,
"path_match": 0,
"filename_match": 0,
"alias_match": 0,
"anchor_boost": 0,
"target_doc_boost": 0,
"generic_penalty": 0
}
},
{
"doc": "docs/architecture/telegram-notify-app-overview.md",
"score_breakdown": {
"semantic": 20,
"path_match": 0,
"filename_match": 0,
"alias_match": 0,
"anchor_boost": 0,
"target_doc_boost": 0,
"generic_penalty": 0
}
}
]
}
```
## process.v2.pipeline
```json
{
"event": "evidence_gate_checked",
"passed": true,
"reason": "target_doc_found",
"answer_mode": "grounded_summary"
}
```
## workflow.v2.summary
```json
{
"event": "workflow_started",
"workflow_id": "v2.docs_explain.summary"
}
```
## workflow.v2.summary.llm
```json
{
"event": "request",
"prompt_name": "v2_docs_explain.summary_answer",
"system_prompt": "Ты объясняешь документацию только на основе найденных SUMMARY-блоков.\nИспользуй только факты из входного контекста.\nЕсли информации мало, прямо скажи об этом и не додумывай детали.\nВ конце перечисли файлы, на которые ты опирался.",
"user_prompt": "Запрос пользователя:\nКак работает метод health?\n\nСигналы запроса:\n{\n \"entity_names\": [],\n \"file_names\": [],\n \"endpoint_paths\": [],\n \"target_doc_hints\": [],\n \"matched_aliases\": [],\n \"process_domain\": null,\n \"process_subdomain\": null,\n \"signal_types\": []\n}\n\nНайденные SUMMARY-блоки:\n\n1. path: docs/README.md\ntitle: Индекс технической документации test_echo_app\nmatch_reason: semantic_match\nsummary: - Purpose: точка входа в техническую документацию сервиса `test_echo_app`.\n- Scope: архитектура, HTTP API control plane, цикл отправки уведомлений, health-модель и каталог ошибок.\n- Canonical structure: `docs/architecture`, `docs/api`, `docs/logic`, `docs/domains`, `docs/errors`.\n- Primary parent doc: [Архитектура Telegram Notify App](./architecture/telegram-notify-app-overview.md).\n- Navigation: документы связаны через `related_docs`, `parent`/`children` и markdown-ссылки без дублирования деталей.\n\n2. path: docs/architecture/telegram-notify-app-overview.md\ntitle: Архитектура Telegram Notify App\nmatch_reason: semantic_match\nsummary: - Purpose: сервис поднимает HTTP control plane и фоновый worker для отправки уведомлений в Telegram.\n- Entry point: `src/telegram_notify_app/main.py`.\n- Main components: `RuntimeManager`, `TelegramControlChannel`, `TelegramNotifyModule`, `TelegramNotifyWorker`, `TelegramSendService`.\n- Configuration: `config/config.yaml` или путь из `CONFIG_PATH`.\n- Related API: [`/health`](../api/health-endpoint.md), [`/actions/{action}`](../api/control-actions-endpoint.md), [`/send`](../api/send-message-endpoint.md).\n- Related logic: [цикл отправки уведомлений](../logic/telegram-notification-loop.md).\n- Related domain: [runtime health](../domains/runtime-health-entity.md).",
"log_context": "agent:req_bab9c8812ac94847bb102cba68516f10"
}
```
## workflow.v2.summary.llm
```json
{
"event": "response",
"text": "На основе представленного контекста невозможно предоставить подробное объяснение работы метода health. \n\nФайлы, на которые я опирался:\n1. docs/README.md\n2. docs/architecture/telegram-notify-app-overview.md"
}
```
## workflow.v2.summary
```json
{
"event": "workflow_trace_flushed",
"workflow_id": "v2.docs_explain.summary",
"steps": [
{
"step_id": "generate_summary_answer",
"title": "Сборка ответа по summary",
"input": {},
"output": {
"answer_length": 205
}
}
]
}
```
## workflow.v2.summary
```json
{
"event": "workflow_completed",
"workflow_id": "v2.docs_explain.summary"
}
```
## process.v2.pipeline
```json
{
"event": "answer_generated",
"answer_mode": "grounded_summary",
"answer_length": 205
}
```
## result
```json
{
"status": "done",
"answer": "На основе представленного контекста невозможно предоставить подробное объяснение работы метода health. \n\nФайлы, на которые я опирался:\n1. docs/README.md\n2. docs/architecture/telegram-notify-app-overview.md",
"completed_at": "2026-04-07T18:21:01.793612+00:00"
}
```
File diff suppressed because it is too large Load Diff
+3
View File
@@ -0,0 +1,3 @@
from app.core.agent.runtime import AgentRuntime
__all__ = ["AgentRuntime"]
+10
View File
@@ -0,0 +1,10 @@
from app.core.agent.processes.base import AgentProcess, ProcessResult
from app.core.agent.processes.v1.process import V1Process
from app.core.agent.processes.v2.process import V2Process
__all__ = [
"AgentProcess",
"ProcessResult",
"V1Process",
"V2Process",
]
+21
View File
@@ -0,0 +1,21 @@
from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from app.core.agent.runtime.execution_context import RuntimeExecutionContext
@dataclass(slots=True)
class ProcessResult:
answer: str = ""
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,4 @@
from app.core.agent.processes.v2.process import V2Process
from app.core.agent.processes.v2.intent_router.router import V2IntentRouter
__all__ = ["V2IntentRouter", "V2Process"]
@@ -0,0 +1,57 @@
from __future__ import annotations
from app.core.agent.processes.v2.models import V2AnchorType, V2RouteAnchors, V2RouteResult, V2Subintent
def anchor_signal_types(route: V2RouteResult) -> set[str]:
texts = _signal_texts(route)
signals: set[str] = set()
if route.subintent == V2Subintent.FIND_FILES:
signals.add(V2AnchorType.FIND_FILES)
if route.anchors.endpoint_paths or _has_any(texts, ("/api/", "api", "endpoint")):
signals.add(V2AnchorType.API_ENDPOINT)
if _has_any(texts, ("/architecture/", "architecture", "arch")):
signals.add(V2AnchorType.ARCHITECTURE)
if _has_any(texts, ("/logic/", "logic", "workflow", "flow", "process")):
signals.add(V2AnchorType.LOGIC_FLOW)
if route.anchors.entity_names or _has_any(texts, ("/domains/", "domain", "entity", "component")):
signals.add(V2AnchorType.DOMAIN_ENTITY)
return signals
def route_anchor_summary(route: V2RouteResult) -> dict[str, object]:
return {
"entity_names": list(route.anchors.entity_names),
"file_names": list(route.anchors.file_names),
"endpoint_paths": list(route.anchors.endpoint_paths),
"target_doc_hints": list(route.anchors.target_doc_hints),
"matched_aliases": list(route.anchors.matched_aliases),
"process_domain": route.anchors.process_domain,
"process_subdomain": route.anchors.process_subdomain,
"signal_types": sorted(anchor_signal_types(route)),
}
def anchors_have_signal(anchors: V2RouteAnchors, signal: str, *, subintent: str | None = None) -> bool:
route = V2RouteResult(
routing_domain="",
intent="",
subintent=subintent or "",
user_query="",
normalized_query="",
anchors=anchors,
)
return signal in anchor_signal_types(route)
def _signal_texts(route: V2RouteResult) -> list[str]:
items = [
*route.anchors.target_doc_hints,
*route.anchors.file_names,
*route.anchors.matched_aliases,
]
return [str(item).strip().lower() for item in items if str(item or "").strip()]
def _has_any(items: list[str], markers: tuple[str, ...]) -> bool:
return any(marker in item for item in items for marker in markers)
@@ -0,0 +1,308 @@
"""Anchor-aware ranking для summary и find-files evidence."""
from __future__ import annotations
import re
from app.core.agent.processes.v2.anchor_signals import anchor_signal_types
from app.core.agent.processes.v2.models import RetrievedFile, RetrievedSummary, V2AnchorType, V2RouteResult
from app.core.agent.processes.v2.retrieval.target_doc_seeding import normalize_doc_path
from app.core.rag.contracts.enums import RagLayer
class DocsEvidenceAssembler:
_API_PATH_PREFIXES = ("docs/api/", "docs/endpoints/", "docs/methods/", "api/", "endpoints/", "methods/")
_GENERIC_DOC_MARKERS = ("readme", "overview", "index", "navigation", "related docs", "catalog")
def assemble_summaries(self, rows: list[dict], route: V2RouteResult) -> list[RetrievedSummary]:
items = self._rank_rows(rows, route, mode="summary")
ranked = [
RetrievedSummary(
path=item["path"],
title=item["title"],
summary=item["summary"],
document_id=item["document_id"],
score=item["score"],
confidence=min(1.0, item["score"] / 1000.0),
match_reason=item["match_reason"],
score_breakdown=item["score_breakdown"],
)
for item in items
if item["summary"] and self._summary_row_allowed(item["row"])
]
if ranked:
ranked[0].is_primary = True
return ranked[:3]
def assemble_files(self, rows: list[dict], route: V2RouteResult) -> list[RetrievedFile]:
items = self._rank_rows(rows, route, mode="find_files")
ranked = [
RetrievedFile(
path=item["path"],
title=item["title"],
document_id=item["document_id"],
score=item["score"],
confidence=min(1.0, item["score"] / 1000.0),
match_reason=item["match_reason"],
score_breakdown=item["score_breakdown"],
)
for item in items
]
if ranked:
ranked[0].is_primary = True
return ranked[:4]
def _rank_rows(self, rows: list[dict], route: V2RouteResult, *, mode: str) -> list[dict]:
seen: set[str] = set()
ranked: list[dict] = []
for row in rows:
path = self._path(row)
if not path or path in seen:
continue
seen.add(path)
breakdown = self._score_breakdown(row, route, mode=mode)
score = sum(breakdown.values())
if score <= 0:
continue
ranked.append(
{
"row": row,
"path": path,
"title": self._title(row, path),
"summary": self._summary(row),
"document_id": self._document_id(row, path),
"score": score,
"score_breakdown": breakdown,
"match_reason": self._match_reason(breakdown),
"is_generic_doc": self._is_generic_doc(path, self._title(row, path), self._summary(row), row),
}
)
ranked.sort(key=lambda item: (-item["score"], item["path"]))
ranked = self._ensure_target_docs_in_top_k(ranked, route, k=4 if mode == "find_files" else 3)
return self._promote_specific_primary(ranked, route)
def _score_breakdown(self, row: dict, route: V2RouteResult, *, mode: str) -> dict[str, int]:
path_raw = self._path(row)
path = path_raw.lower()
filename = path.split("/")[-1]
title = self._title(row, path).lower()
summary = self._summary(row).lower()
entity = self._entity_name(row).lower()
query_tokens = self._query_tokens(route)
path_tokens = self._path_tokens(path)
compact_haystack = {self._compact(path), self._compact(filename), self._compact(title), self._compact(entity)}
breakdown = {
"semantic": 0,
"path_match": 0,
"filename_match": 0,
"alias_match": 0,
"anchor_boost": 0,
"target_doc_boost": 0,
"specificity_boost": 0,
"generic_penalty": 0,
}
if route.intent == "GENERAL_QA":
breakdown["semantic"] += 80
hint_norm_lower = {normalize_doc_path(h).lower() for h in route.anchors.target_doc_hints if str(h or "").strip()}
if normalize_doc_path(path_raw).lower() in hint_norm_lower:
breakdown["target_doc_boost"] += 1000
hint_texts = [str(hint or "").strip().lower() for hint in route.anchors.target_doc_hints if str(hint or "").strip()]
if any(alias.lower() in " ".join([path, title, summary, entity]) for alias in route.anchors.matched_aliases):
breakdown["alias_match"] += 500
for token in query_tokens:
if token in path_tokens:
breakdown["path_match"] += 60
if token and token in filename:
breakdown["filename_match"] += 200
if token and token in summary:
breakdown["semantic"] += 20
if self._compact(token) in compact_haystack:
breakdown["alias_match"] += 250
for hint in hint_texts:
compact_hint = self._compact(hint)
if compact_hint and compact_hint in compact_haystack:
breakdown["target_doc_boost"] += 180
elif hint and hint.strip("/") in " ".join([path, title, summary, entity]):
breakdown["semantic"] += 70
endpoint_text = self._summary(row).lower()
for endpoint in route.anchors.endpoint_paths:
normalized_endpoint = endpoint.strip().lower()
endpoint_slug = normalized_endpoint.strip("/")
if normalized_endpoint and normalized_endpoint in endpoint_text:
breakdown["target_doc_boost"] += 260
if endpoint_slug and endpoint_slug in filename:
breakdown["filename_match"] += 200
if any(endpoint.strip("/").lower() in filename for endpoint in route.anchors.endpoint_paths):
breakdown["filename_match"] += 200
signals = anchor_signal_types(route)
breakdown["anchor_boost"] += self._anchor_boost(path, signals)
breakdown["specificity_boost"] += self._specificity_boost(row, path, title, summary, route)
breakdown["generic_penalty"] += self._generic_penalty(path, signals)
if mode == "find_files":
breakdown["path_match"] *= 3
breakdown["filename_match"] *= 2
breakdown["alias_match"] *= 1
breakdown["semantic"] = max(0, breakdown["semantic"] // 2)
return breakdown
def _anchor_boost(self, path: str, signals: set[str]) -> int:
boost = 0
if V2AnchorType.API_ENDPOINT in signals and path.startswith(self._API_PATH_PREFIXES):
boost += 360
if V2AnchorType.LOGIC_FLOW in signals and path.startswith("docs/logic/"):
boost += 300
if V2AnchorType.DOMAIN_ENTITY in signals and path.startswith("docs/domains/"):
boost += 300
if V2AnchorType.ARCHITECTURE in signals and path.startswith("docs/architecture/"):
boost += 300
if V2AnchorType.FIND_FILES in signals and path.startswith("docs/"):
boost += 120
return boost
def _generic_penalty(self, path: str, signals: set[str]) -> int:
penalty = 0
lowered = path.lower()
if path == "docs/README.md" and V2AnchorType.ARCHITECTURE not in signals:
penalty -= 260
if any(marker in lowered for marker in ("/readme", "readme.md", "/index", "/overview", "/catalog", "/navigation")):
penalty -= 220
if "/architecture/" in path and V2AnchorType.ARCHITECTURE not in signals and signals.intersection(
{V2AnchorType.API_ENDPOINT, V2AnchorType.DOMAIN_ENTITY}
):
penalty -= 150
return penalty
def _ensure_target_docs_in_top_k(self, ranked: list[dict], route: V2RouteResult, *, k: int) -> list[dict]:
if not ranked or not route.anchors.target_doc_hints:
return ranked
top = ranked[:k]
top_paths = {item["path"] for item in top}
top_norm = {normalize_doc_path(p).lower() for p in top_paths if p}
for hint in route.anchors.target_doc_hints:
hn = normalize_doc_path(hint).lower()
if hn in top_norm:
continue
candidate = next(
(item for item in ranked if normalize_doc_path(item["path"]).lower() == hn),
None,
)
if candidate is None:
continue
if len(top) < k:
top.append(candidate)
else:
top[-1] = candidate
top_paths = {item["path"] for item in top}
top_norm = {normalize_doc_path(p).lower() for p in top_paths if p}
remaining = [item for item in ranked if item["path"] not in top_paths]
top.sort(key=lambda item: (-item["score"], item["path"]))
return top + remaining
def _promote_specific_primary(self, ranked: list[dict], route: V2RouteResult) -> list[dict]:
if len(ranked) < 2:
return ranked
first = ranked[0]
if not first.get("is_generic_doc"):
return ranked
promoted = next((item for item in ranked[1:] if not item.get("is_generic_doc") and self._is_specific_candidate(item, route)), None)
if promoted is None:
return ranked
return [promoted] + [item for item in ranked if item["path"] != promoted["path"]]
def _match_reason(self, breakdown: dict[str, int]) -> str:
if breakdown["target_doc_boost"] > 0:
return "exact_path"
if breakdown["alias_match"] > 0:
return "alias_match"
if breakdown["filename_match"] > 0:
return "exact_title"
return "semantic_match"
def _summary_row_allowed(self, row: dict) -> bool:
metadata = dict(row.get("metadata") or {})
if row.get("layer") != RagLayer.DOCS_DOC_CHUNKS:
return True
section = str(metadata.get("section_path") or "").lower()
return "summary" in section or "свод" in section or "overview" in section
def _specificity_boost(self, row: dict, path: str, title: str, summary: str, route: V2RouteResult) -> int:
boost = 0
filename = path.split("/")[-1]
lowered_title = title.lower()
lowered_summary = summary.lower()
if not self._is_generic_doc(path, title, summary, row):
boost += 90
if path.startswith(self._API_PATH_PREFIXES):
boost += 160
if "endpoint" in filename or "endpoint" in lowered_title or "method" in lowered_title:
boost += 120
if row.get("layer") == RagLayer.DOCS_DOC_CHUNKS and not self._looks_like_navigation_chunk(row):
boost += 80
for token in self._query_tokens(route):
if token and token in filename:
boost += 90
if token and token in lowered_title:
boost += 70
if token and token in lowered_summary:
boost += 40
return boost
def _is_specific_candidate(self, item: dict, route: V2RouteResult) -> bool:
breakdown = dict(item.get("score_breakdown") or {})
if breakdown.get("target_doc_boost", 0) > 0:
return True
if breakdown.get("specificity_boost", 0) >= 160:
return True
return V2AnchorType.API_ENDPOINT in anchor_signal_types(route) and item["path"].startswith(self._API_PATH_PREFIXES)
def _is_generic_doc(self, path: str, title: str, summary: str, row: dict) -> bool:
haystack = " ".join([path.lower(), title.lower(), summary.lower()])
if any(marker in haystack for marker in self._GENERIC_DOC_MARKERS):
return True
return self._looks_like_navigation_chunk(row)
def _looks_like_navigation_chunk(self, row: dict) -> bool:
text = self._summary(row).lower()
if not text:
return False
lines = [line.strip() for line in text.splitlines() if line.strip()]
bullet_lines = sum(1 for line in lines if line.startswith(("- ", "* ", "1.", "2.", "3.")))
link_lines = sum(1 for line in lines if "](" in line or line.startswith("docs/"))
if "related docs" in text or "navigation" in text:
return True
return bullet_lines >= 3 or link_lines >= 3
def _query_tokens(self, route: V2RouteResult) -> list[str]:
values = list(route.target_terms) + list(route.anchors.matched_aliases)
tokens: list[str] = []
for item in values:
for token in re.split(r"[^a-zA-Zа-яА-Я0-9]+", str(item).lower()):
if len(token) >= 3:
tokens.append(token)
return list(dict.fromkeys(tokens))
def _path_tokens(self, path: str) -> set[str]:
return {token for token in re.split(r"[^a-zA-Zа-яА-Я0-9]+", path.lower()) if len(token) >= 3}
def _compact(self, value: str) -> str:
return "".join(self._path_tokens(value))
def _path(self, row: dict) -> str:
metadata = dict(row.get("metadata") or {})
raw = str(row.get("path") or metadata.get("source_path") or "").strip()
return normalize_doc_path(raw)
def _title(self, row: dict, path: str) -> str:
metadata = dict(row.get("metadata") or {})
return str(row.get("title") or metadata.get("title") or path).strip()
def _summary(self, row: dict) -> str:
metadata = dict(row.get("metadata") or {})
return str(metadata.get("summary_text") or row.get("content") or "").strip()
def _document_id(self, row: dict, path: str) -> str:
metadata = dict(row.get("metadata") or {})
return str(metadata.get("document_id") or metadata.get("doc_id") or path).strip()
def _entity_name(self, row: dict) -> str:
metadata = dict(row.get("metadata") or {})
return str(metadata.get("entity_name") or "").strip()
@@ -0,0 +1,76 @@
from __future__ import annotations
from dataclasses import dataclass, field
from app.core.agent.processes.v2.anchor_signals import anchor_signal_types
from app.core.agent.processes.v2.models import RetrievedFile, RetrievedSummary, V2AnchorType, V2Intent, V2RouteResult
@dataclass(slots=True)
class EvidenceGateDecision:
passed: bool
answer_mode: str
reason: str
message: str = ""
supporting_paths: list[str] = field(default_factory=list)
class DocsEvidenceGate:
def check_summaries(self, route: V2RouteResult, documents: list[RetrievedSummary]) -> EvidenceGateDecision:
if route.intent == V2Intent.GENERAL_QA:
if documents:
return EvidenceGateDecision(True, "grounded_summary", "general_docs_found")
return EvidenceGateDecision(
False,
"insufficient_evidence",
"general_docs_missing",
"В найденной документации нет достаточной опоры для общего summary по запросу.",
)
if self._has_target_document(route, [item.path for item in documents]):
return EvidenceGateDecision(True, "grounded_summary", "target_doc_found")
return EvidenceGateDecision(
False,
"insufficient_evidence",
"target_doc_missing",
self._summary_insufficiency(route, documents),
[item.path for item in documents[:3]],
)
def check_files(self, route: V2RouteResult, files: list[RetrievedFile]) -> EvidenceGateDecision:
if not files:
return EvidenceGateDecision(
False,
"insufficient_evidence",
"no_file_candidates",
"Не нашёл файлов документации, которые уверенно соответствуют запросу.",
)
if files[0].confidence >= 0.8:
return EvidenceGateDecision(True, "deterministic", "primary_file_confident")
return EvidenceGateDecision(
False,
"deterministic",
"low_confidence_shortlist",
"Нашёл только ближайшие кандидаты по запросу.",
[item.path for item in files[:4]],
)
def _has_target_document(self, route: V2RouteResult, paths: list[str]) -> bool:
if any(path in route.anchors.target_doc_hints for path in paths):
return True
signals = anchor_signal_types(route)
if V2AnchorType.API_ENDPOINT in signals:
return any(path.startswith("docs/api/") for path in paths)
if V2AnchorType.ARCHITECTURE in signals:
return any(path.startswith("docs/architecture/") for path in paths)
if V2AnchorType.LOGIC_FLOW in signals:
return any(path.startswith("docs/logic/") for path in paths)
if V2AnchorType.DOMAIN_ENTITY in signals:
return any(path.startswith("docs/domains/") for path in paths)
return bool(paths)
def _summary_insufficiency(self, route: V2RouteResult, documents: list[RetrievedSummary]) -> str:
base = "В поднятом контексте не найден целевой документ по запросу."
if not documents:
return base
nearby = ", ".join(item.path for item in documents[:3])
return f"{base} Ближайшие документы: {nearby}."
@@ -0,0 +1,8 @@
namespace: v2_general
prompts:
summary_answer: |
Ты делаешь grounded summary только по найденной проектной документации.
Не используй общие знания о том, как обычно устроены системы.
Дай короткий, понятный ответ и опирайся только на входные документы.
Если опоры мало, прямо скажи об этом.
@@ -0,0 +1,3 @@
from app.core.agent.processes.v2.intent_router.router import V2IntentRouter
__all__ = ["V2IntentRouter"]
@@ -0,0 +1,18 @@
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]
@@ -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.processes.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,349 @@
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")
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
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,118 @@
"""Маршрутизация запроса в домен/интент/subintent и якоря для v2."""
from __future__ import annotations
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.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.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.processes.v2.models import V2RouteResult
from app.core.agent.utils.llm import AgentLlmService
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,
) -> 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._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
def route(self, user_query: str) -> V2RouteResult:
normalized_query = self._normalizer.normalize(user_query)
target_terms_analysis = self._target_terms_extractor.extract(normalized_query)
anchor_analysis = self._anchor_extractor.extract(normalized_query, target_terms_analysis)
features = QueryFeatures(
normalized_query=normalized_query,
target_terms=list(target_terms_analysis.target_terms),
endpoint_paths=list(target_terms_analysis.endpoint_paths),
file_names=list(anchor_analysis.anchors.file_names),
matched_aliases=list(target_terms_analysis.matched_aliases),
target_doc_hints=list(anchor_analysis.anchors.target_doc_hints),
file_markers=list(anchor_analysis.file_markers),
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),
)
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)
llm_result = self._apply_deterministic_corrections(llm_result, features)
if llm_result is not None:
confidence = self._confidence_adjuster.adjust(float(llm_result["confidence"]), features)
return V2RouteResult(
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"]),
)
return self._fallback_router.route(
user_query=user_query,
features=features,
anchors=anchor_analysis.anchors,
llm_attempted=llm_attempted,
)
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,
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,
},
)
except Exception:
return None
def _apply_deterministic_corrections(self, candidate: dict | None, features: QueryFeatures) -> dict | None:
if candidate is None:
return None
if candidate.get("routing_domain") == "DOCS" and self._should_force_find_files(features):
corrected = dict(candidate)
corrected["subintent"] = "FIND_FILES"
return corrected
return candidate
def _should_force_find_files(self, features: QueryFeatures) -> bool:
if features.file_markers or features.file_names:
return True
query = features.normalized_query.lower()
return "show doc" in query or "show file" in query or "doc for" in query
@@ -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.processes.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,28 @@
from __future__ import annotations
from app.core.agent.processes.v2.intent_router.models import QueryFeatures
from app.core.agent.processes.v2.models import V2Subintent
class DocsSubintentResolver:
def resolve(self, features: QueryFeatures) -> str | None:
if features.file_markers or self._has_file_like_anchor(features):
return V2Subintent.FIND_FILES
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)
@@ -0,0 +1,86 @@
from __future__ import annotations
from app.core.agent.processes.v2.intent_router.models import QueryFeatures
from app.core.agent.processes.v2.models import V2Domain, V2Intent, V2RouteResult, V2Subintent
class V2FallbackRouter:
def route(
self,
*,
user_query: str,
features: QueryFeatures,
anchors,
llm_attempted: bool,
) -> V2RouteResult:
if features.file_markers:
return self._build_docs_result(
user_query=user_query,
features=features,
anchors=anchors,
subintent=V2Subintent.FIND_FILES,
llm_attempted=llm_attempted,
reason="fallback file markers",
)
if self._has_docs_signal(features):
return self._build_docs_result(
user_query=user_query,
features=features,
anchors=anchors,
subintent=V2Subintent.SUMMARY,
llm_attempted=llm_attempted,
reason="fallback docs summary",
)
return V2RouteResult(
routing_domain=V2Domain.GENERAL,
intent=V2Intent.GENERAL_QA,
subintent=V2Subintent.SUMMARY,
user_query=user_query,
normalized_query=features.normalized_query,
target_terms=features.target_terms,
anchors=anchors,
confidence=0.0,
routing_mode=self._routing_mode(llm_attempted),
llm_router_used=llm_attempted,
reason_short="fallback general summary",
)
def _build_docs_result(
self,
*,
user_query: str,
features: QueryFeatures,
anchors,
subintent: str,
llm_attempted: bool,
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=0.0,
routing_mode=self._routing_mode(llm_attempted),
llm_router_used=llm_attempted,
reason_short=reason,
)
def _has_docs_signal(self, features: QueryFeatures) -> bool:
return any(
(
features.endpoint_paths,
features.target_doc_hints,
features.endpoint_markers,
features.architecture_markers,
features.logic_markers,
features.domain_markers,
features.matched_aliases,
)
)
def _routing_mode(self, llm_attempted: bool) -> str:
return "llm_fallback" if llm_attempted else "deterministic_fallback"
@@ -0,0 +1,45 @@
from __future__ import annotations
import json
from app.core.agent.processes.v2.intent_router.routers.route_catalog import V2RouteCatalog
from app.core.agent.utils.llm import AgentLlmService
class V2LlmRouter:
def __init__(
self,
llm: AgentLlmService,
prompt_name: str = "v2_intent_router.route",
catalog: V2RouteCatalog | None = None,
) -> None:
self._llm = llm
self._prompt_name = prompt_name
self._catalog = catalog or V2RouteCatalog()
def classify(self, *, normalized_query: str, target_terms: list[str], anchors: dict) -> dict | None:
payload = {
"normalized_query": normalized_query,
"target_terms": target_terms,
"anchors": anchors,
"allowed_routes": self._catalog.allowed_routes(),
}
raw = self._llm.generate(
self._prompt_name,
json.dumps(payload, ensure_ascii=False, indent=2),
log_context="v2_intent_router",
)
return self._parse(raw)
def _parse(self, raw: str) -> dict | None:
try:
data = json.loads(str(raw or "").strip())
except json.JSONDecodeError:
return None
return {
"routing_domain": str(data.get("routing_domain") or "").strip(),
"intent": str(data.get("intent") or "").strip(),
"subintent": str(data.get("subintent") or "").strip(),
"confidence": data.get("confidence"),
"reason_short": str(data.get("reason_short") or "").strip(),
}
@@ -0,0 +1,26 @@
namespace: v2_intent_router
prompts:
route: |
Ты выбираешь маршрут для узкого процесса v2.
Основной принцип:
- DOCS / DOC_EXPLAIN / FIND_FILES: запрос просит найти файл, документ или путь.
- DOCS / DOC_EXPLAIN / SUMMARY: запрос просит объяснить документацию, endpoint, архитектуру, процесс или сущность.
- GENERAL / GENERAL_QA / SUMMARY: общий обзорный вопрос без явного запроса к документации.
Используй только маршруты из поля `allowed_routes`.
Верни confidence:
- 0.9-1.0 для явного кейса
- 0.7-0.9 для нормального кейса
- меньше 0.7 для неоднозначного кейса
Ответь только JSON-объектом вида:
{
"routing_domain": "GENERAL" | "DOCS",
"intent": "GENERAL_QA" | "DOC_EXPLAIN",
"subintent": "SUMMARY" | "FIND_FILES",
"confidence": 0.0-1.0,
"reason_short": "короткая причина"
}
Не добавляй markdown, комментарии и текст вне JSON.
@@ -0,0 +1,20 @@
from __future__ import annotations
from app.core.agent.processes.v2.models import V2Domain, V2Intent, V2Subintent
class V2RouteCatalog:
_ALLOWED_ROUTES = (
(V2Domain.DOCS, V2Intent.DOC_EXPLAIN, V2Subintent.FIND_FILES),
(V2Domain.DOCS, V2Intent.DOC_EXPLAIN, V2Subintent.SUMMARY),
(V2Domain.GENERAL, V2Intent.GENERAL_QA, V2Subintent.SUMMARY),
)
def allowed_routes(self) -> list[dict[str, str]]:
return [
{"routing_domain": domain, "intent": intent, "subintent": subintent}
for domain, intent, subintent in self._ALLOWED_ROUTES
]
def is_allowed(self, routing_domain: str, intent: str, subintent: str) -> bool:
return (routing_domain, intent, subintent) in self._ALLOWED_ROUTES
@@ -0,0 +1,34 @@
from __future__ import annotations
from app.core.agent.processes.v2.intent_router.routers.route_catalog import V2RouteCatalog
class V2RouteValidator:
def __init__(self, catalog: V2RouteCatalog | None = None) -> None:
self._catalog = catalog or V2RouteCatalog()
def validate(self, candidate: dict | None) -> dict | None:
if not isinstance(candidate, dict):
return None
routing_domain = self._value(candidate, "routing_domain")
intent = self._value(candidate, "intent")
subintent = self._value(candidate, "subintent")
if not self._catalog.is_allowed(routing_domain, intent, subintent):
return None
return {
"routing_domain": routing_domain,
"intent": intent,
"subintent": subintent,
"confidence": self._coerce_confidence(candidate.get("confidence")),
"reason_short": self._value(candidate, "reason_short"),
}
def _value(self, candidate: dict, key: str) -> str:
return str(candidate.get(key) or "").strip()
def _coerce_confidence(self, value: object) -> float:
try:
confidence = float(value)
except (TypeError, ValueError):
return 0.0
return max(0.0, min(1.0, confidence))
+87
View File
@@ -0,0 +1,87 @@
"""Типы маршрута и выдачи retrieval для процесса v2."""
from __future__ import annotations
from dataclasses import dataclass, field
class V2Domain:
DOCS = "DOCS"
GENERAL = "GENERAL"
class V2Intent:
DOC_EXPLAIN = "DOC_EXPLAIN"
GENERAL_QA = "GENERAL_QA"
class V2Subintent:
SUMMARY = "SUMMARY"
FIND_FILES = "FIND_FILES"
class V2AnchorType:
GENERAL_OVERVIEW = "GENERAL_OVERVIEW"
API_ENDPOINT = "API_ENDPOINT"
ARCHITECTURE = "ARCHITECTURE"
LOGIC_FLOW = "LOGIC_FLOW"
DOMAIN_ENTITY = "DOMAIN_ENTITY"
FIND_FILES = "FIND_FILES"
@dataclass(slots=True)
class V2RouteAnchors:
"""Якоря из запроса для retrieval и downstream."""
entity_names: list[str] = field(default_factory=list)
file_names: list[str] = field(default_factory=list)
endpoint_paths: list[str] = field(default_factory=list)
target_doc_hints: list[str] = field(default_factory=list)
matched_aliases: list[str] = field(default_factory=list)
process_domain: str | None = None
process_subdomain: str | None = None
@dataclass(slots=True)
class V2RouteResult:
routing_domain: str
intent: str
subintent: str
user_query: str
normalized_query: str
target_terms: list[str] = field(default_factory=list)
anchors: V2RouteAnchors = field(default_factory=V2RouteAnchors)
confidence: float = 1.0
routing_mode: str = "deterministic"
llm_router_used: bool = False
reason_short: str = ""
@property
def domain(self) -> str:
"""Совместимость с полем ``domain`` в логах и вызовах."""
return self.routing_domain
@dataclass(slots=True)
class RetrievedSummary:
path: str
title: str
summary: str
document_id: str
score: int
confidence: float = 0.0
match_reason: str = "semantic_match"
is_primary: bool = False
score_breakdown: dict[str, int] = field(default_factory=dict)
@dataclass(slots=True)
class RetrievedFile:
path: str
title: str
document_id: str
score: int
confidence: float
match_reason: str
is_primary: bool = False
score_breakdown: dict[str, int] = field(default_factory=dict)
+304
View File
@@ -0,0 +1,304 @@
"""Процесс v2: роутинг, план retrieval, вызов rag API, сборка evidence и workflow."""
from __future__ import annotations
from app.core.agent.processes.v2.anchor_signals import route_anchor_summary
from app.core.agent.processes.v2.evidence.assembler import DocsEvidenceAssembler
from app.core.agent.processes.v2.evidence.gate import DocsEvidenceGate
from app.core.agent.processes.v2.intent_router import V2IntentRouter
from app.core.agent.processes.v2.models import V2Intent, V2Subintent
from app.core.agent.processes.v2.retrieval import DocsMetadataLookupIndex
from app.core.agent.processes.v2.retrieval.policy_resolver import V2RetrievalPolicyResolver
from app.core.agent.processes.v2.retrieval.target_doc_seeding import (
RagRowIndex,
merge_row_lists,
normalize_doc_path,
normalized_path_set,
row_path,
seed_candidates_from_target_hints,
)
from app.core.agent.processes.v2.retrieval.v2_rag_adapter import V2RagRetrievalAdapter
from app.core.agent.processes.v2.workflows.docs_explain_find_files.context import DocsExplainFindFilesContext
from app.core.agent.processes.v2.workflows.docs_explain_find_files.graph import DocsExplainFindFilesGraph
from app.core.agent.processes.v2.workflows.docs_explain_summary.context import DocsExplainSummaryContext
from app.core.agent.processes.v2.workflows.docs_explain_summary.graph import DocsExplainSummaryGraph
from app.core.agent.processes.v2.workflows.general_summary.context import GeneralSummaryContext
from app.core.agent.processes.v2.workflows.general_summary.graph import GeneralSummaryGraph
from app.core.agent.processes.base import AgentProcess, ProcessResult
from app.core.agent.utils.llm import AgentLlmService
class V2Process(AgentProcess):
version = "v2"
def __init__(
self,
llm: AgentLlmService,
policy_resolver: V2RetrievalPolicyResolver,
rag_adapter: V2RagRetrievalAdapter,
evidence_assembler: DocsEvidenceAssembler,
evidence_gate: DocsEvidenceGate | None = None,
router: V2IntentRouter | None = None,
docs_summary_prompt_name: str = "v2_docs_explain.summary_answer",
general_summary_prompt_name: str = "v2_general.summary_answer",
workflow_llm_enabled: bool = True,
) -> None:
self._router = router or V2IntentRouter()
self._policy_resolver = policy_resolver
self._rag_adapter = rag_adapter
self._evidence_assembler = evidence_assembler
self._evidence_gate = evidence_gate or DocsEvidenceGate()
self._docs_summary_prompt_name = docs_summary_prompt_name
self._general_summary_prompt_name = general_summary_prompt_name
self._workflow_llm_enabled = workflow_llm_enabled
self._summary_graph = DocsExplainSummaryGraph(llm)
self._find_files_graph = DocsExplainFindFilesGraph()
self._general_summary_graph = GeneralSummaryGraph(llm)
async def run(self, context) -> ProcessResult:
route = self._router.route(context.request.message)
rag_session_id = context.session.active_rag_session_id
context.trace.module("process.v2").log(
"intent_routed",
{
"routing_domain": route.routing_domain,
"intent": route.intent,
"subintent": route.subintent,
"normalized_query": route.normalized_query,
"target_terms": route.target_terms,
"anchors": route_anchor_summary(route),
"confidence": route.confidence,
"routing_mode": route.routing_mode,
"llm_router_used": route.llm_router_used,
"reason_short": route.reason_short,
"rag_session_id": rag_session_id,
},
)
self._log_step(
context,
"router_resolved",
{
"domain": route.routing_domain,
"intent": route.intent,
"subintent": route.subintent,
"confidence": route.confidence,
},
)
self._log_step(
context,
"anchors_extracted",
{
"signal_types": route_anchor_summary(route)["signal_types"],
"endpoint_paths": route.anchors.endpoint_paths,
"target_doc_hints": route.anchors.target_doc_hints,
"matched_aliases": route.anchors.matched_aliases,
"target_terms": route.target_terms,
},
)
self._log_step(
context,
"alias_resolution",
{
"resolved_aliases": route.anchors.matched_aliases,
"target_doc_hints": route.anchors.target_doc_hints,
},
)
if not rag_session_id:
if route.intent == V2Intent.GENERAL_QA:
answer = "Не могу собрать grounded summary без активной RAG-сессии с проиндексированной документацией."
self._log_step(context, "evidence_gate_checked", {"passed": False, "reason": "missing_rag_session"})
self._log_step(context, "answer_generated", {"answer_mode": "insufficient_evidence"})
return ProcessResult(answer=answer)
return ProcessResult(answer="Для процесса v2 нужна активная RAG-сессия проекта с проиндексированной документацией.")
plan = self._policy_resolver.resolve(route)
context.trace.module("process.v2.retrieval_policy").log(
"retrieval_plan_resolved",
{"profile": plan.profile, "layers": plan.layers, "limit": plan.limit, "filters": plan.filters},
)
self._log_step(
context,
"retrieval_profile_selected",
{"profile": plan.profile, "layers": plan.layers, "filters": plan.filters},
)
retrieved_rows = await self._rag_adapter.fetch_rows(rag_session_id, route.normalized_query, plan)
metadata_rows = self._metadata_lookup_candidates(retrieved_rows, route)
rows = self._merge_candidate_rows(retrieved_rows, metadata_rows)
rows = seed_candidates_from_target_hints(rows, route.anchors.target_doc_hints, RagRowIndex(rows))
self._print_missing_target_hints(route, rows)
context.trace.module("process.v2.rag_retrieval").log(
"rag_rows_fetched",
{
"profile": plan.profile,
"row_count": len(rows),
"rows": [self._trace_row(row) for row in rows],
},
)
self._log_step(
context,
"candidate_generation",
{
"query": route.user_query,
"profile": plan.profile,
"details": {
"target_doc_hints": list(route.anchors.target_doc_hints),
"candidates_before_ranking": [row_path(row) for row in rows if row_path(row)],
},
"resolved_aliases": route.anchors.matched_aliases,
"target_doc_hints": route.anchors.target_doc_hints,
"candidate_docs_before_ranking": [self._trace_row(row) for row in rows[:8]],
"sources": {
"seeded": [self._trace_row(row) for row in retrieved_rows[:5] if row_path(row) in {normalize_doc_path(h) for h in route.anchors.target_doc_hints}],
"metadata_lookup": [self._trace_row(row) for row in metadata_rows[:5]],
"semantic": [self._trace_row(row) for row in retrieved_rows[:5]],
},
},
)
self._log_step(
context,
"retrieval_executed",
{
"query": route.user_query,
"profile": plan.profile,
"row_count": len(rows),
"target_doc_hints": route.anchors.target_doc_hints,
"top_results": [self._trace_row(row) for row in rows[:5]],
},
)
if route.subintent == V2Subintent.FIND_FILES:
files = self._evidence_assembler.assemble_files(rows, route)
gate = self._evidence_gate.check_files(route, files)
context.trace.module("process.v2.evidence").log(
"evidence_assembled",
{"mode": "find_files", "file_count": len(files), "files": [file.path for file in files]},
)
self._log_step(
context,
"evidence_assembled",
{"mode": "find_files", "primary_file": files[0].path if files else None, "file_count": len(files)},
)
self._log_ranking(context, files)
self._log_step(
context,
"evidence_gate_checked",
{"passed": gate.passed, "reason": gate.reason, "answer_mode": gate.answer_mode},
)
flow_context = DocsExplainFindFilesContext(
runtime=context,
route=route,
rag_session_id=rag_session_id,
files=files,
gate_decision=gate,
)
flow_context = await self._find_files_graph.run(flow_context)
self._log_step(context, "answer_generated", {"answer_mode": gate.answer_mode, "answer_length": len(flow_context.answer)})
return ProcessResult(answer=flow_context.answer)
documents = self._evidence_assembler.assemble_summaries(rows, route)
gate = self._evidence_gate.check_summaries(route, documents)
context.trace.module("process.v2.evidence").log(
"evidence_assembled",
{"mode": "summary", "document_count": len(documents), "documents": [item.path for item in documents]},
)
self._log_step(
context,
"evidence_assembled",
{"mode": "summary", "primary_doc": documents[0].path if documents else None, "document_count": len(documents)},
)
self._log_ranking(context, documents)
self._log_step(
context,
"evidence_gate_checked",
{"passed": gate.passed, "reason": gate.reason, "answer_mode": gate.answer_mode},
)
if route.intent == V2Intent.GENERAL_QA:
flow_context = GeneralSummaryContext(
runtime=context,
route=route,
prompt_name=self._general_summary_prompt_name,
workflow_llm_enabled=self._workflow_llm_enabled,
documents=documents,
gate_decision=gate,
)
flow_context = await self._general_summary_graph.run(flow_context)
self._log_step(context, "answer_generated", {"answer_mode": gate.answer_mode, "answer_length": len(flow_context.answer)})
return ProcessResult(answer=flow_context.answer)
flow_context = DocsExplainSummaryContext(
runtime=context,
route=route,
rag_session_id=rag_session_id,
prompt_name=self._docs_summary_prompt_name,
workflow_llm_enabled=self._workflow_llm_enabled,
documents=documents,
gate_decision=gate,
)
flow_context = await self._summary_graph.run(flow_context)
self._log_step(context, "answer_generated", {"answer_mode": gate.answer_mode, "answer_length": len(flow_context.answer)})
return ProcessResult(answer=flow_context.answer)
def _trace_row(self, row: dict) -> dict[str, object]:
metadata = row.get("metadata") or {}
content = str(row.get("content") or "").strip()
return {
"layer": str(row.get("layer") or ""),
"path": str(row.get("path") or ""),
"title": str(row.get("title") or ""),
"document_id": str(metadata.get("document_id") or metadata.get("doc_id") or ""),
"entity_name": str(metadata.get("entity_name") or ""),
"summary_text": str(metadata.get("summary_text") or "")[:400],
"section_path": str(metadata.get("section_path") or ""),
"content_preview": content[:400],
}
def _log_step(self, context, step: str, payload: dict[str, object]) -> None:
context.trace.module("process.v2.pipeline").log(step, payload)
def _print_missing_target_hints(self, route, rows: list[dict]) -> None:
if not route.anchors.target_doc_hints:
return
candidate_paths = normalized_path_set(rows)
for hint in route.anchors.target_doc_hints:
if not str(hint or "").strip():
continue
normalized = normalize_doc_path(hint)
if not normalized.startswith("docs/") or "." not in normalized.rsplit("/", 1)[-1]:
continue
if normalized not in candidate_paths:
print("ERROR: target doc missing from candidates:", normalized)
def _metadata_lookup_candidates(self, rows: list[dict], route) -> list[dict]:
return DocsMetadataLookupIndex(rows).lookup(route)
def _merge_candidate_rows(self, *groups: list[dict]) -> list[dict]:
return merge_row_lists(*groups)
def _log_ranking(self, context, items: list) -> None:
top_docs: list[dict[str, object]] = []
for item in items[:4]:
top_docs.append(
{
"doc": getattr(item, "path", ""),
"score": getattr(item, "score", 0),
"match_reason": getattr(item, "match_reason", ""),
}
)
context.trace.module("process.v2.pipeline").log(
"ranking_explained",
{
"doc": getattr(item, "path", ""),
"score_breakdown": getattr(item, "score_breakdown", {}),
"score": getattr(item, "score", 0),
"match_reason": getattr(item, "match_reason", ""),
},
)
context.trace.module("process.v2.pipeline").log(
"ranking_explained",
{
"top_docs_after_ranking": top_docs,
"ranking_score_breakdown": [
{
"doc": getattr(item, "path", ""),
"score_breakdown": getattr(item, "score_breakdown", {}),
}
for item in items[:4]
],
},
)
@@ -0,0 +1,8 @@
namespace: v2_docs_explain
prompts:
summary_answer: |
Ты объясняешь документацию только на основе найденных SUMMARY-блоков.
Используй только факты из входного контекста.
Если информации мало, прямо скажи об этом и не додумывай детали.
В конце перечисли файлы, на которые ты опирался.
@@ -0,0 +1,17 @@
from app.core.agent.processes.v2.retrieval.metadata_lookup import DocsMetadataLookupIndex
from app.core.agent.processes.v2.retrieval.policy_resolver import V2RetrievalPolicyResolver
from app.core.agent.processes.v2.retrieval.target_doc_seeding import (
RagRowIndex,
normalize_doc_path,
seed_candidates_from_target_hints,
)
from app.core.agent.processes.v2.retrieval.v2_rag_adapter import V2RagRetrievalAdapter
__all__ = [
"V2RetrievalPolicyResolver",
"V2RagRetrievalAdapter",
"DocsMetadataLookupIndex",
"normalize_doc_path",
"RagRowIndex",
"seed_candidates_from_target_hints",
]
@@ -0,0 +1,66 @@
from __future__ import annotations
import re
from collections import defaultdict
from app.core.agent.processes.v2.models import V2RouteResult
class DocsMetadataLookupIndex:
def __init__(self, rows: list[dict]) -> None:
self._rows_by_path: dict[str, dict] = {}
self._rows_by_basename: dict[str, list[dict]] = defaultdict(list)
self._rows_by_slug: dict[str, list[dict]] = defaultdict(list)
self._rows_by_title_token: dict[str, list[dict]] = defaultdict(list)
self._rows_by_compact: dict[str, list[dict]] = defaultdict(list)
for row in rows:
path = str(row.get("path") or "").strip()
if not path or path in self._rows_by_path:
continue
self._rows_by_path[path] = row
basename = path.split("/")[-1].lower()
slug = basename.removesuffix(".md").removesuffix(".yaml").removesuffix(".yml")
self._rows_by_basename[basename].append(row)
self._rows_by_slug[slug].append(row)
self._rows_by_compact[self._compact(slug)].append(row)
title = str(row.get("title") or "").lower()
for token in self._tokens(title):
self._rows_by_title_token[token].append(row)
self._rows_by_compact[self._compact(title)].append(row)
entity_name = str(dict(row.get("metadata") or {}).get("entity_name") or "").lower()
if entity_name:
self._rows_by_compact[self._compact(entity_name)].append(row)
def lookup(self, route: V2RouteResult) -> list[dict]:
candidates: list[dict] = []
seen: set[str] = set()
for path in route.anchors.target_doc_hints:
self._append(candidates, seen, self._rows_by_path.get(path))
lookup_tokens = list(route.target_terms) + list(route.anchors.matched_aliases) + list(route.anchors.endpoint_paths)
for token in self._tokens(" ".join(lookup_tokens)):
for bucket in (
self._rows_by_basename.get(token, []),
self._rows_by_slug.get(token, []),
self._rows_by_title_token.get(token, []),
):
for row in bucket:
self._append(candidates, seen, row)
for compact in {self._compact(item) for item in lookup_tokens if item}:
for row in self._rows_by_compact.get(compact, []):
self._append(candidates, seen, row)
return candidates
def _append(self, items: list[dict], seen: set[str], row: dict | None) -> None:
if row is None:
return
path = str(row.get("path") or "").strip()
if not path or path in seen:
return
seen.add(path)
items.append(row)
def _tokens(self, value: str) -> list[str]:
return [token for token in re.split(r"[^a-zA-Zа-яА-Я0-9]+", str(value or "").lower()) if len(token) >= 3]
def _compact(self, value: str) -> str:
return "".join(self._tokens(value))

Some files were not shown because too many files have changed in this diff Show More