Compare commits
5 Commits
main
...
8fb76bb331
| Author | SHA1 | Date | |
|---|---|---|---|
| 8fb76bb331 | |||
| bc29d51a29 | |||
| 6b74d410cd | |||
| 5d77ab1a88 | |||
| 0bff171936 |
@@ -0,0 +1,14 @@
|
|||||||
|
# Analysis Assets
|
||||||
|
|
||||||
|
Этот каталог содержит служебные артефакты для аналитической и генеративной работы агента.
|
||||||
|
|
||||||
|
## Структура
|
||||||
|
|
||||||
|
- `rules/` — правила построения документации, frontmatter и шаблоны документов.
|
||||||
|
|
||||||
|
## Назначение
|
||||||
|
|
||||||
|
Каталог `.analysis/` отделен от `docs/`, чтобы:
|
||||||
|
- хранить служебные policy- и template-материалы вне пользовательской документации;
|
||||||
|
- передавать правила в LLM как отдельный policy-context;
|
||||||
|
- не смешивать документацию проекта и внутренние артефакты анализа.
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# Documentation Rules
|
||||||
|
|
||||||
|
## Назначение
|
||||||
|
|
||||||
|
Этот файл фиксирует общие правила формирования, обновления и поддержки технической документации проекта.
|
||||||
|
|
||||||
|
Документация проекта должна создаваться как система атомарных, связанных между собой документов, пригодных:
|
||||||
|
- для чтения человеком;
|
||||||
|
- для сопровождения командой;
|
||||||
|
- для индексирования в RAG;
|
||||||
|
- для автоматического обновления агентом на основе кода и существующих артефактов.
|
||||||
|
|
||||||
|
Этот файл задает:
|
||||||
|
- общие принципы документационной архитектуры;
|
||||||
|
- правила декомпозиции документации;
|
||||||
|
- правила размещения файлов;
|
||||||
|
- требования к связям между документами;
|
||||||
|
- требования к качеству markdown-документов;
|
||||||
|
- правила генерации и обновления документации агентом.
|
||||||
|
|
||||||
|
Детальные шаблоны документов и правила frontmatter описываются отдельно:
|
||||||
|
- `.analysis/rules/frontmatter-rules.md`
|
||||||
|
- `.analysis/rules/templates/*.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Область действия
|
||||||
|
|
||||||
|
Правила из этого файла применяются ко всей проектной документации, размещаемой в:
|
||||||
|
|
||||||
|
```text
|
||||||
|
docs/documentation/
|
||||||
@@ -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>)
|
||||||
|
```
|
||||||
@@ -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 |
|
||||||
|
|------|--------|---------|
|
||||||
|
| | | |
|
||||||
@@ -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 |
|
||||||
|
|------|--------|---------|
|
||||||
|
| | | |
|
||||||
|
```
|
||||||
@@ -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 |
|
||||||
|
|------|--------|---------|
|
||||||
|
| | | |
|
||||||
@@ -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 недостаточно.
|
||||||
@@ -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
|
||||||
Vendored
+25
@@ -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
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# Запросы
|
||||||
|
1. Какие методы апи есть в проекте
|
||||||
|
2. Какие методы апи есть для healthcheck
|
||||||
|
3. Где документация на healthcheck
|
||||||
Binary file not shown.
@@ -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
|
||||||
|
|
||||||
@@ -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 реализуют прикладную логику и контроль качества**.
|
||||||
|
|
||||||
|
Это позволяет одновременно сохранить управляемость системы и обеспечить масштабируемость под новые типы задач.
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
# Intents
|
||||||
|
|
||||||
|
## Domains
|
||||||
|
|
||||||
|
- `DOCS`
|
||||||
|
- `GENERAL`
|
||||||
|
- `CODE` - временно отключен
|
||||||
|
|
||||||
|
## GENERAL
|
||||||
|
|
||||||
|
### Intent `GENERAL_QA`
|
||||||
|
|
||||||
|
Общий интент для вопросов без точного маршрута.
|
||||||
|
В дальнейшем может использоваться как fallback.
|
||||||
|
|
||||||
|
Subintents:
|
||||||
|
- `SUMMARY` - ответы на общие вопросы по SUMMARY
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## DOCS
|
||||||
|
|
||||||
|
### Intent `ARCHITECTURE`
|
||||||
|
|
||||||
|
Обработка вопросов по архитектуре.
|
||||||
|
Subintents пока отсутствуют.
|
||||||
|
Интент запланирован, без реализации.
|
||||||
|
|
||||||
|
### Intent `DOC_EXPLAIN`
|
||||||
|
|
||||||
|
Объяснение по документации.
|
||||||
|
|
||||||
|
Subintents:
|
||||||
|
- `SUMMARY` - краткое объяснение темы по SUMMARY-блокам документации
|
||||||
|
- `FIND_FILES` - поиск файлов с релевантной информацией
|
||||||
|
- `EXPLAIN_API` - объяснение работы метода
|
||||||
|
- `COMPONENT_INTEGRATIONS` - перечень интеграций компонента, API, UI, сущности, внешних систем
|
||||||
|
- `ENTITY_INTEGRATIONS` - перечень интеграций сущности
|
||||||
|
|
||||||
|
В текущем узком MVP реально реализованы только:
|
||||||
|
|
||||||
|
- `SUMMARY`
|
||||||
|
- `FIND_FILES`
|
||||||
|
|
||||||
|
Для запросов по интеграциям целевым retrieval-слоем является `D6_INTEGRATION_INDEX`.
|
||||||
|
|
||||||
|
### Intent `OPENAPI_GENERATION`
|
||||||
|
|
||||||
|
Генерация OpenAPI-спеки.
|
||||||
|
|
||||||
|
Subintents:
|
||||||
|
- `FULL_SPEC` - создание полной спецификации
|
||||||
|
|
||||||
|
### Intent `DOC_GENERATION`
|
||||||
|
|
||||||
|
Редактирование документации.
|
||||||
|
|
||||||
|
Subintents:
|
||||||
|
- `FROM_FEATURE` - создание документации из системной аналитики на фичу
|
||||||
@@ -0,0 +1,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`
|
||||||
@@ -0,0 +1,852 @@
|
|||||||
|
## 1. Формат ведения технической документации агентом
|
||||||
|
|
||||||
|
## 1.1. Общие принципы
|
||||||
|
|
||||||
|
Техническая документация, формируемая агентом, должна строиться как **система атомарных, не пересекающихся по смыслу документов**, связанных между собой явными ссылками.
|
||||||
|
|
||||||
|
Ключевые принципы:
|
||||||
|
- один документ описывает одну сущность или один устойчивый технический аспект;
|
||||||
|
- документ не должен дублировать соседние документы;
|
||||||
|
- общая система знаний должна собираться через ссылки, а не через копипасту;
|
||||||
|
- структура документации должна быть пригодна как для чтения человеком, так и для индексирования в RAG.
|
||||||
|
|
||||||
|
## 1.2. Базовые типы документных единиц
|
||||||
|
|
||||||
|
На первом этапе логично сохранить текущую семантику типов документов, но перенести ее в файловую модель.
|
||||||
|
|
||||||
|
Базовые типы:
|
||||||
|
- `ui_page`
|
||||||
|
- `api_method`
|
||||||
|
- `logic_block`
|
||||||
|
|
||||||
|
Позже могут добавиться:
|
||||||
|
- `architecture_overview`
|
||||||
|
- `integration_doc`
|
||||||
|
- `domain_entity`
|
||||||
|
- `glossary_item`
|
||||||
|
- `index_page`
|
||||||
|
|
||||||
|
## 1.3. Принцип декомпозиции страниц / файлов
|
||||||
|
|
||||||
|
### Один устойчивый объект — один документ
|
||||||
|
Если объект можно переиспользовать или на него могут ссылаться другие документы, его надо выносить в отдельный файл.
|
||||||
|
|
||||||
|
Примеры:
|
||||||
|
- отдельная UI-страница;
|
||||||
|
- отдельный API endpoint;
|
||||||
|
- отдельный блок логики;
|
||||||
|
- отдельный интеграционный сценарий.
|
||||||
|
|
||||||
|
### Документы не должны пересекаться по смыслу
|
||||||
|
Если описание повторяется в нескольких местах, нужно выделять общий документ и ссылаться на него.
|
||||||
|
|
||||||
|
Примеры:
|
||||||
|
- фронтальная страница не должна заново описывать логику API;
|
||||||
|
- документ по API не должен заново раскрывать общую логику переиспользуемого блока;
|
||||||
|
- вместо дублирования должен быть переход по ссылке.
|
||||||
|
|
||||||
|
### Use case и детальные правила живут раздельно
|
||||||
|
Сценарий описывает поток работы, а детали выносятся в функциональные требования, отдельные блоки логики или контрактные описания.
|
||||||
|
|
||||||
|
Это важно и для RAG-индексации:
|
||||||
|
- сценарии индексируются как workflows;
|
||||||
|
- отдельные правила — как facts;
|
||||||
|
- сущности и блоки — как entities.
|
||||||
|
|
||||||
|
## 1.4. Иерархическая организация документации
|
||||||
|
|
||||||
|
Документация должна быть организована как иерархическое дерево каталогов и файлов, а не как набор неструктурированных страниц.
|
||||||
|
|
||||||
|
Пример верхнего уровня:
|
||||||
|
|
||||||
|
```text
|
||||||
|
docs/
|
||||||
|
ui/
|
||||||
|
api/
|
||||||
|
logic/
|
||||||
|
domains/
|
||||||
|
integrations/
|
||||||
|
architecture/
|
||||||
|
glossary/
|
||||||
|
errors/
|
||||||
|
```
|
||||||
|
|
||||||
|
Пример организации:
|
||||||
|
|
||||||
|
```text
|
||||||
|
docs/
|
||||||
|
ui/
|
||||||
|
order-create-page.md
|
||||||
|
order-edit-page.md
|
||||||
|
api/
|
||||||
|
orders-create.md
|
||||||
|
orders-get.md
|
||||||
|
logic/
|
||||||
|
order-validation.md
|
||||||
|
order-enrichment.md
|
||||||
|
architecture/
|
||||||
|
system-overview.md
|
||||||
|
integration-landscape.md
|
||||||
|
errors/
|
||||||
|
catalog.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
## 1.5. Учет связей между документами
|
||||||
|
|
||||||
|
Связи должны быть **явными и поддерживаемыми агентом**.
|
||||||
|
|
||||||
|
Примеры:
|
||||||
|
- UI-страница ссылается на API, который она вызывает;
|
||||||
|
- API-документ ссылается на переиспользуемые логические блоки;
|
||||||
|
- логический блок ссылается на связанные интеграции;
|
||||||
|
- архитектурный обзор ссылается на набор конкретных модулей и документов;
|
||||||
|
- документ по коду может ссылаться на системную аналитику, которая инициировала изменение.
|
||||||
|
|
||||||
|
Именно эта сеть ссылок затем индексируется в слоях:
|
||||||
|
- `D1_DOCUMENT_CATALOG`
|
||||||
|
- `D3_ENTITY_CATALOG`
|
||||||
|
- `D4_WORKFLOW_INDEX`
|
||||||
|
- `D5_REFERENCE_GRAPH`
|
||||||
|
- `D6_DOC_CODE_LINKS`
|
||||||
|
|
||||||
|
## 1.6. Формат markdown-документов
|
||||||
|
|
||||||
|
Основной формат технической документации — `Markdown`.
|
||||||
|
|
||||||
|
Каждый документ состоит из двух частей:
|
||||||
|
1. **YAML frontmatter** — структурные метаданные;
|
||||||
|
2. **Markdown body** — основное содержимое по шаблону.
|
||||||
|
|
||||||
|
## 3.7. YAML frontmatter
|
||||||
|
|
||||||
|
Frontmatter нужен для:
|
||||||
|
- определения типа документа;
|
||||||
|
- идентификации документа;
|
||||||
|
- определения его места в иерархии;
|
||||||
|
- фиксации связей с кодом и другими документами;
|
||||||
|
- выделения сущностей и тегов;
|
||||||
|
- упрощения построения слоев `D1`, `D3`, `D5`, `D6`.
|
||||||
|
|
||||||
|
### Базовый frontmatter
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
id: ui-order-create-page
|
||||||
|
title: Страница создания заказа
|
||||||
|
doc_type: ui_page
|
||||||
|
domain: orders
|
||||||
|
status: draft
|
||||||
|
related_docs:
|
||||||
|
- api-orders-create
|
||||||
|
- logic-order-validation
|
||||||
|
entities:
|
||||||
|
- Order
|
||||||
|
- CreateOrder
|
||||||
|
tags:
|
||||||
|
- ui
|
||||||
|
- orders
|
||||||
|
- creation
|
||||||
|
#owner: system-analyst
|
||||||
|
#source_of_truth: code
|
||||||
|
#related_code:
|
||||||
|
# - src/orders/ui/create_page.tsx
|
||||||
|
# - src/orders/api/orders_controller.py
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
### Обязательные поля
|
||||||
|
|
||||||
|
- `id` — стабильный уникальный идентификатор документа;
|
||||||
|
- `title` — человекочитаемый заголовок;
|
||||||
|
- `doc_type` — тип документа;
|
||||||
|
- `related_docs` — ссылки на связанные документы;
|
||||||
|
- `status` — статус документа;
|
||||||
|
- `domain` - домен фичи (Карточка клиента, Задачи, Сделки, Предложения)
|
||||||
|
- `sub_domain` - поддомен внутри основной сущности (Счета, ЗДА, ECM)
|
||||||
|
|
||||||
|
### Рекомендуемые поля
|
||||||
|
- `owner`
|
||||||
|
- `entities`
|
||||||
|
- `tags`
|
||||||
|
- `parent`
|
||||||
|
- `children`
|
||||||
|
- `feature`
|
||||||
|
- `system_analytics_refs`
|
||||||
|
- `business_refs`
|
||||||
|
- `updated_from`
|
||||||
|
- `reviewers`
|
||||||
|
- `source_of_truth`
|
||||||
|
- `related_code`
|
||||||
|
|
||||||
|
### Допустимые значения `doc_type`
|
||||||
|
- `ui_page`
|
||||||
|
- `api_method`
|
||||||
|
- `logic_block`
|
||||||
|
- `architecture_overview`
|
||||||
|
- `integration_doc`
|
||||||
|
- `domain_entity`
|
||||||
|
- `glossary_item`
|
||||||
|
- `index_page`
|
||||||
|
|
||||||
|
### Допустимые значения `status`
|
||||||
|
- `draft`
|
||||||
|
- `in_review`
|
||||||
|
- `approved`
|
||||||
|
- `outdated`
|
||||||
|
- `generated`
|
||||||
|
- `active`
|
||||||
|
|
||||||
|
### Допустимые значения `source_of_truth`
|
||||||
|
- `code`
|
||||||
|
- `doc`
|
||||||
|
- `system_analysis`
|
||||||
|
- `business_requirements`
|
||||||
|
- `mixed`
|
||||||
|
|
||||||
|
## 1.8. Typed frontmatter для разных типов документов
|
||||||
|
|
||||||
|
У каждого типа документа есть:
|
||||||
|
- **общие поля**;
|
||||||
|
- **тип-специфичные поля**.
|
||||||
|
|
||||||
|
### Пример для `api_method`
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
id: api.create_invoice
|
||||||
|
doc_type: api_method
|
||||||
|
domain: billing
|
||||||
|
title: Создание инвойса
|
||||||
|
|
||||||
|
endpoint: POST /api/v1/invoices
|
||||||
|
auth: USER
|
||||||
|
idempotent: false
|
||||||
|
timeout_ms: 3000
|
||||||
|
|
||||||
|
links:
|
||||||
|
called_by:
|
||||||
|
- ui.invoice_form
|
||||||
|
uses_logic:
|
||||||
|
- logic.invoice_validation
|
||||||
|
writes_db:
|
||||||
|
- db.invoices
|
||||||
|
- db.invoice_items
|
||||||
|
integrates_with:
|
||||||
|
- int.crm_sync
|
||||||
|
|
||||||
|
related_docs:
|
||||||
|
- ui.invoice_form
|
||||||
|
- logic.invoice_validation
|
||||||
|
related_code:
|
||||||
|
- services/billing/api/create_invoice.py
|
||||||
|
entities:
|
||||||
|
- Invoice
|
||||||
|
- CreateInvoice
|
||||||
|
|
||||||
|
tags:
|
||||||
|
- invoice
|
||||||
|
- create
|
||||||
|
- billing
|
||||||
|
status: active
|
||||||
|
version: 1.3
|
||||||
|
source_of_truth: code
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
### Для `api_method` полезно поддерживать
|
||||||
|
|
||||||
|
- `endpoint`
|
||||||
|
- `sup_parameters`
|
||||||
|
- `role_model_actions`
|
||||||
|
- `monitoring_actions`
|
||||||
|
- `audit_actions`
|
||||||
|
- `idempotent`
|
||||||
|
- `timeout_ms`
|
||||||
|
- `links.called_by`
|
||||||
|
- `links.uses_logic`
|
||||||
|
- `links.writes_db`
|
||||||
|
- `links.integrates_with`
|
||||||
|
|
||||||
|
### Для `ui_page` позже полезно поддерживать
|
||||||
|
- `calls_api`
|
||||||
|
- `user_analitycs_actions`
|
||||||
|
- `sup_parameters`
|
||||||
|
- `role_model_actions`
|
||||||
|
- `entry_points`
|
||||||
|
- `uses_logic`
|
||||||
|
|
||||||
|
### Для `logic_block` полезно поддерживать
|
||||||
|
|
||||||
|
- `called_from`
|
||||||
|
- `uses_logic`
|
||||||
|
- `reads_db`
|
||||||
|
- `writes_db`
|
||||||
|
- `integrates_with`
|
||||||
|
|
||||||
|
## 1.9. Двухслойная структура документа: `Summary` + `Details`
|
||||||
|
|
||||||
|
LLM не должна каждый раз тонуть в полном документе. Поэтому каждый документ должен содержать два уровня представления.
|
||||||
|
|
||||||
|
### `Summary`
|
||||||
|
Короткая, строго структурированная спецификация. Это слой **быстрого контекста**.
|
||||||
|
|
||||||
|
Рекомендуемый объем:
|
||||||
|
- примерно 30–60 строк;
|
||||||
|
- без длинных пояснений;
|
||||||
|
- только ключевые факты.
|
||||||
|
|
||||||
|
Пример:
|
||||||
|
|
||||||
|
```md
|
||||||
|
## Summary
|
||||||
|
- Purpose: создание инвойса из формы
|
||||||
|
- Actor: пользователь
|
||||||
|
- Trigger: Submit
|
||||||
|
- Main API: POST /api/v1/invoices (api.create_invoice)
|
||||||
|
- Validation: required fields, amount > 0, date <= today
|
||||||
|
- Errors: 400(field errors), 409(duplicate), 503(retryable)
|
||||||
|
- Analytics: event invoice_submit, invoice_error
|
||||||
|
```
|
||||||
|
|
||||||
|
### `Details`
|
||||||
|
Полное раскрытие объекта:
|
||||||
|
- use case;
|
||||||
|
- функциональные требования;
|
||||||
|
- UI;
|
||||||
|
- API;
|
||||||
|
- integrations;
|
||||||
|
- ошибки;
|
||||||
|
- НФТ;
|
||||||
|
- связи;
|
||||||
|
- кодовые привязки.
|
||||||
|
|
||||||
|
### Блок `## Integrations`
|
||||||
|
|
||||||
|
Если у объекта есть интеграции, они должны быть выделены в отдельный блок `## Integrations`.
|
||||||
|
Интеграции не нужно дублировать во frontmatter.
|
||||||
|
Основное описание хранится в body документа.
|
||||||
|
|
||||||
|
Ожидаемый принцип:
|
||||||
|
- одна интеграция = одна отдельная запись внутри блока;
|
||||||
|
- у интеграции есть краткое имя;
|
||||||
|
- у интеграции есть структурированные атрибуты;
|
||||||
|
- дополнительные детали допускаются в свободной форме через вложенный словарь.
|
||||||
|
|
||||||
|
Рекомендуемые атрибуты интеграции:
|
||||||
|
- `target`
|
||||||
|
- `target_type`
|
||||||
|
- `direction`
|
||||||
|
- `interaction`
|
||||||
|
- `via`
|
||||||
|
- `purpose`
|
||||||
|
- `details`
|
||||||
|
|
||||||
|
Где:
|
||||||
|
- `target` - идентификатор или имя целевого объекта;
|
||||||
|
- `target_type` - тип цели: `api`, `ui`, `entity`, `service`, `queue`, `db`, `external_system`;
|
||||||
|
- `direction` - направление: `inbound`, `outbound`, `bidirectional`;
|
||||||
|
- `interaction` - тип взаимодействия: `calls`, `reads`, `writes`, `emits`, `consumes`, `depends_on`;
|
||||||
|
- `via` - технический канал интеграции;
|
||||||
|
- `purpose` - зачем нужна интеграция;
|
||||||
|
- `details` - словарь с гибкой структурой под дополнительные параметры.
|
||||||
|
|
||||||
|
Пример:
|
||||||
|
|
||||||
|
```md
|
||||||
|
## Integrations
|
||||||
|
|
||||||
|
### Orders API
|
||||||
|
- target: api.orders.create
|
||||||
|
- target_type: api
|
||||||
|
- direction: outbound
|
||||||
|
- interaction: calls
|
||||||
|
- via: POST /api/orders
|
||||||
|
- purpose: создание заказа
|
||||||
|
- details:
|
||||||
|
- auth: service-token
|
||||||
|
- retry: true
|
||||||
|
|
||||||
|
### Order Entity
|
||||||
|
- target: domain.order
|
||||||
|
- target_type: entity
|
||||||
|
- direction: outbound
|
||||||
|
- interaction: writes
|
||||||
|
- via: repository
|
||||||
|
- purpose: сохранение состояния заказа
|
||||||
|
- details:
|
||||||
|
- transaction: required
|
||||||
|
```
|
||||||
|
|
||||||
|
Этот блок должен быть пригоден и для чтения человеком, и для последующего извлечения в отдельный RAG-слой интеграций.
|
||||||
|
|
||||||
|
## 1.10. Общие требования к markdown body
|
||||||
|
|
||||||
|
1. В документе должен быть один `H1`, совпадающий с `title`.
|
||||||
|
2. Основные разделы используют `H2`.
|
||||||
|
3. Подразделы внутри разделов используют `H3`.
|
||||||
|
4. Не должно быть хаотической вложенности заголовков.
|
||||||
|
5. Один раздел должен описывать одну смысловую часть.
|
||||||
|
6. Текст не должен дублировать соседние документы.
|
||||||
|
7. Вместо дублирования должны использоваться явные ссылки на связанные документы.
|
||||||
|
8. Сценарии, правила, ограничения и ссылки на код должны быть отделены друг от друга.
|
||||||
|
|
||||||
|
## 1.11. Базовый каркас markdown-документа
|
||||||
|
|
||||||
|
```md
|
||||||
|
---
|
||||||
|
id: api-orders-create
|
||||||
|
title: Метод создания заказа
|
||||||
|
doc_type: api_method
|
||||||
|
domain: orders
|
||||||
|
status: draft
|
||||||
|
source_of_truth: code
|
||||||
|
related_docs:
|
||||||
|
- logic-order-validation
|
||||||
|
- ui-order-create-page
|
||||||
|
related_code:
|
||||||
|
- src/orders/api/create_order.py
|
||||||
|
entities:
|
||||||
|
- Order
|
||||||
|
- CreateOrder
|
||||||
|
tags:
|
||||||
|
- api
|
||||||
|
- orders
|
||||||
|
---
|
||||||
|
|
||||||
|
# Метод создания заказа
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
- Purpose: создание заказа
|
||||||
|
- Actor: пользователь
|
||||||
|
- Trigger: submit формы
|
||||||
|
- Main API: POST /orders
|
||||||
|
|
||||||
|
## Details
|
||||||
|
### Описание
|
||||||
|
### Технический use case
|
||||||
|
### Функциональные требования
|
||||||
|
### Нефункциональные требования
|
||||||
|
### Контракт
|
||||||
|
|
||||||
|
|
||||||
|
## 3.13. Специализированные шаблоны документов
|
||||||
|
|
||||||
|
### UI Page
|
||||||
|
|
||||||
|
```md
|
||||||
|
# <Название страницы>
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
## Назначение
|
||||||
|
## Контекст
|
||||||
|
## Технический use case
|
||||||
|
## Описание UI
|
||||||
|
## UI Elements
|
||||||
|
## Функциональные требования
|
||||||
|
## Нефункциональные требования
|
||||||
|
## Связанные API
|
||||||
|
## Связанные блоки логики
|
||||||
|
## Связанный код
|
||||||
|
## Связанные документы
|
||||||
|
## История изменений
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Требования к разделу `Описание UI`
|
||||||
|
Для каждого элемента желательно описывать:
|
||||||
|
- тип элемента;
|
||||||
|
- назначение;
|
||||||
|
- источник данных;
|
||||||
|
- default / placeholder;
|
||||||
|
- правила активации;
|
||||||
|
- поведение при взаимодействии;
|
||||||
|
- валидацию.
|
||||||
|
|
||||||
|
#### Требования к разделу `UI Elements`
|
||||||
|
UI-элементы должны храниться в **табличном** или **полуструктурированном** виде.
|
||||||
|
|
||||||
|
Пример:
|
||||||
|
|
||||||
|
```md
|
||||||
|
## UI Elements
|
||||||
|
|
||||||
|
| id | type | label | data_source | validation | behavior |
|
||||||
|
|--------|--------|---------|------------|------------|----------|
|
||||||
|
| amount | input | Amount | local | >0 | enables submit |
|
||||||
|
| submit | button | Create | - | - | calls api.create_invoice |
|
||||||
|
```
|
||||||
|
|
||||||
|
Если модель UI сложная, допустим sidecar-файл `ui_elements.yaml` или `ui_elements.json` рядом с основным документом.
|
||||||
|
|
||||||
|
### API Method
|
||||||
|
|
||||||
|
```md
|
||||||
|
# <Название API метода>
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
## Назначение
|
||||||
|
## Контекст
|
||||||
|
## Технический use case
|
||||||
|
## Функциональные требования
|
||||||
|
## Contract
|
||||||
|
## Integrations
|
||||||
|
## Errors
|
||||||
|
## Нефункциональные требования
|
||||||
|
## Связанные блоки логики
|
||||||
|
## Связанный код
|
||||||
|
## Связанные документы
|
||||||
|
## История изменений
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Требования к разделу `Contract`
|
||||||
|
Контракт может:
|
||||||
|
- быть кратко описан прямо в документе;
|
||||||
|
- ссылаться на OpenAPI;
|
||||||
|
- ссылаться на отдельный контрактный файл.
|
||||||
|
|
||||||
|
Для REST API целевым источником истины должен становиться `OpenAPI`.
|
||||||
|
|
||||||
|
### Reusable Logic Block
|
||||||
|
|
||||||
|
```md
|
||||||
|
# <Название блока логики>
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
## Назначение
|
||||||
|
## Контекст
|
||||||
|
## Технический use case
|
||||||
|
## Функциональные требования
|
||||||
|
## Integrations
|
||||||
|
## Ограничения и условия вызова
|
||||||
|
## Нефункциональные требования
|
||||||
|
## Связанные API / UI / integration points
|
||||||
|
## Связанный код
|
||||||
|
## Связанные документы
|
||||||
|
## История изменений
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3.14. Машинно-читаемые API-контракты
|
||||||
|
|
||||||
|
Для API контрактов **источником истины** должен становиться:
|
||||||
|
- `OpenAPI` — предпочтительно;
|
||||||
|
- либо временно строгий markdown/yaml-контракт, если OpenAPI еще нет.
|
||||||
|
|
||||||
|
Минимальный набор для API-контракта:
|
||||||
|
- `endpoint`
|
||||||
|
- `method`
|
||||||
|
- `request fields`
|
||||||
|
- `required / optional`
|
||||||
|
- `constraints`
|
||||||
|
- `response`
|
||||||
|
- `errors`
|
||||||
|
- `idempotency`
|
||||||
|
- `retry`
|
||||||
|
- `timeout`
|
||||||
|
- `auth`
|
||||||
|
|
||||||
|
## 3.15. Каталог ошибок
|
||||||
|
|
||||||
|
Ошибки, HTTP-коды, retry-правила и клиентское поведение не должны размазываться по разным документам.
|
||||||
|
|
||||||
|
Нужен единый каталог ошибок, например `docs/errors/catalog.yaml`.
|
||||||
|
|
||||||
|
Пример:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
errors:
|
||||||
|
- error_id: invoice_validation_failed
|
||||||
|
http_code: 400
|
||||||
|
internal_code: BILLING_400_01
|
||||||
|
when: invalid request fields
|
||||||
|
client_behavior: show field errors
|
||||||
|
retry: false
|
||||||
|
owner: billing
|
||||||
|
|
||||||
|
- error_id: invoice_duplicate
|
||||||
|
http_code: 409
|
||||||
|
internal_code: BILLING_409_01
|
||||||
|
when: duplicate invoice detected
|
||||||
|
client_behavior: show duplicate warning
|
||||||
|
retry: false
|
||||||
|
owner: billing
|
||||||
|
|
||||||
|
- error_id: crm_sync_unavailable
|
||||||
|
http_code: 503
|
||||||
|
internal_code: BILLING_503_02
|
||||||
|
when: downstream CRM unavailable
|
||||||
|
client_behavior: retry later
|
||||||
|
retry: true
|
||||||
|
owner: billing
|
||||||
|
```
|
||||||
|
|
||||||
|
В API- и logic-документах лучше ссылаться на `error_id`, а не заново подробно описывать каждую ошибку.
|
||||||
|
|
||||||
|
## 3.16. Требования к качеству документа для RAG
|
||||||
|
|
||||||
|
1. **Явные заголовки** — не использовать безымянные блоки текста.
|
||||||
|
2. **Атомарные утверждения** — не смешивать несколько правил в одном пункте, если их можно разделить.
|
||||||
|
3. **Явные сущности** — использовать стабильные названия компонентов, API, модулей, страниц.
|
||||||
|
4. **Явные ссылки** — не писать «этот метод», если можно указать конкретную ссылку или идентификатор.
|
||||||
|
5. **Минимум дублирования** — повторяющийся контент должен заменяться ссылками.
|
||||||
|
6. **Привязка к коду** — если документ описывает кодовый объект, это должно быть явно указано.
|
||||||
|
7. **Разделение сценариев и правил** — workflow и fact-like требования должны быть отделены.
|
||||||
|
|
||||||
|
## 3.17. Как структура markdown помогает RAG
|
||||||
|
|
||||||
|
- `frontmatter` + заголовки → `D1_DOCUMENT_CATALOG`
|
||||||
|
- `entities`, `tags`, устойчивые термины → `D3_ENTITY_CATALOG`
|
||||||
|
- атомарные функциональные и нефункциональные требования → `D2_FACT_INDEX`
|
||||||
|
- `Технический use case` → `D4_WORKFLOW_INDEX`
|
||||||
|
- `related_docs`, явные ссылки → `D5_REFERENCE_GRAPH`
|
||||||
|
- `related_code`, упоминания symbols и файлов → `D6_DOC_CODE_LINKS`
|
||||||
|
- `Summary` → быстрый retrieval и short-form context для LLM
|
||||||
|
|
||||||
|
## 3.18. Принципы генерации документации агентом
|
||||||
|
|
||||||
|
Когда документ пишет агент, он должен:
|
||||||
|
- сначала извлечь evidence из кода, системной аналитики и существующих документов;
|
||||||
|
- определить тип документа;
|
||||||
|
- заполнить frontmatter;
|
||||||
|
- построить markdown body по шаблону;
|
||||||
|
- явно указать связи с кодом и другими документами;
|
||||||
|
- не дублировать уже существующее описание, если можно сослаться на него.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
## 4.4. Layered RAG
|
||||||
|
|
||||||
|
RAG строится как система специализированных слоев для двух основных доменов:
|
||||||
|
- `CODE RAG`
|
||||||
|
- `DOCS RAG`
|
||||||
|
|
||||||
|
Каждый graph извлекает контекст не из одного общего индекса, а из нужного набора слоев в зависимости от intent.
|
||||||
|
|
||||||
|
## 4.5. Evidence gate
|
||||||
|
|
||||||
|
Перед синтезом ответа или документа агент должен проверять, хватает ли опоры.
|
||||||
|
|
||||||
|
Примеры:
|
||||||
|
- найден ли symbol;
|
||||||
|
- найдено ли достаточное количество code chunks;
|
||||||
|
- есть ли supporting relations;
|
||||||
|
- есть ли document evidence;
|
||||||
|
- есть ли docs ↔ code mapping.
|
||||||
|
|
||||||
|
Если опоры недостаточно, агент должен:
|
||||||
|
- деградировать в упрощенный режим;
|
||||||
|
- честно фиксировать неполноту ответа;
|
||||||
|
- при необходимости уходить в fallback.
|
||||||
|
|
||||||
|
## 4.6. Synthesis layer
|
||||||
|
|
||||||
|
На этом этапе LLM:
|
||||||
|
- агрегирует найденные артефакты;
|
||||||
|
- формирует объяснение;
|
||||||
|
- пишет документ;
|
||||||
|
- структурирует результат под нужный шаблон.
|
||||||
|
|
||||||
|
LLM не должна быть основным источником фактов. Фактическая основа должна приходить из RAG и диагностируемого pipeline.
|
||||||
|
|
||||||
|
## 4.7. Diagnostics
|
||||||
|
|
||||||
|
Система должна сохранять диагностический след:
|
||||||
|
- какой graph был выбран;
|
||||||
|
- какие слои использовались;
|
||||||
|
- что было найдено;
|
||||||
|
- где retrieval был слабым;
|
||||||
|
- почему был выбран fallback;
|
||||||
|
- какие evidence стали основой ответа.
|
||||||
|
|
||||||
|
## 4.8. Сценарии: Target Architecture vs MVP-now
|
||||||
|
|
||||||
|
### 4.8.1. Target Architecture
|
||||||
|
|
||||||
|
#### CODE
|
||||||
|
- `OPEN_FILE` — открыть конкретный файл;
|
||||||
|
- `OPEN_SYMBOL` — открыть класс / функцию / метод;
|
||||||
|
- `EXPLAIN` — объяснить, как работает сущность или фрагмент;
|
||||||
|
- `FIND_TESTS` — найти релевантные тесты;
|
||||||
|
- `FIND_ENTRYPOINTS` — найти основные точки входа;
|
||||||
|
- `RELATED_CODE` — найти связанные сущности и ближайший контекст.
|
||||||
|
|
||||||
|
#### DOCS
|
||||||
|
- `DOC_SEARCH` — найти релевантный фрагмент документации;
|
||||||
|
- `DOC_EXPLAIN` — кратко объяснить, что сказано в документации по теме;
|
||||||
|
- `DOC_ENTITY_LOOKUP` — найти разделы, связанные с сущностью или компонентом;
|
||||||
|
- `GENERATE_DOCS_FROM_CODE` — сформировать документацию по коду с нуля для модуля, класса, функции, компонента или сценария.
|
||||||
|
|
||||||
|
#### CROSS-DOMAIN
|
||||||
|
- `FIND_IMPLEMENTATION_BY_DOC` — найти реализацию по описанию;
|
||||||
|
- `FIND_DOC_BY_CODE` — найти документацию по коду;
|
||||||
|
- `COMPARE_DOCS_AND_CODE` — базовое сопоставление документации и реализации.
|
||||||
|
|
||||||
|
#### GENERAL / FALLBACK
|
||||||
|
- `GENERAL_QA` — общий сценарий ответа на вопрос, если домен или интент не удалось определить уверенно.
|
||||||
|
|
||||||
|
### 4.8.2. MVP-now
|
||||||
|
|
||||||
|
В текущем цикле фокус на сценариях:
|
||||||
|
|
||||||
|
- `OPEN_FILE`
|
||||||
|
- `EXPLAIN`
|
||||||
|
- `FIND_TESTS`
|
||||||
|
- `FIND_ENTRYPOINTS`
|
||||||
|
- `GENERAL_QA`
|
||||||
|
|
||||||
|
DOCS и CROSS_DOMAIN остаются частью target architecture; в текущем цикле они не являются обязательной частью test-first MVP.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Структура слоев RAG
|
||||||
|
|
||||||
|
## 5.1. CODE RAG
|
||||||
|
|
||||||
|
### C0 — Source Chunks
|
||||||
|
**Назначение:** базовые фрагменты исходного кода.
|
||||||
|
**Единица:** chunk кода.
|
||||||
|
**Как формируется:** исходные файлы обходятся и режутся на chunk’и с учетом структурных границ.
|
||||||
|
**Статус в MVP:** да.
|
||||||
|
|
||||||
|
### C1 — Symbol Catalog
|
||||||
|
**Назначение:** каталог модулей, классов, функций, методов и других значимых сущностей.
|
||||||
|
**Единица:** symbol.
|
||||||
|
**Как формируется:** из AST и синтаксического разбора кода.
|
||||||
|
**Статус в MVP:** да.
|
||||||
|
|
||||||
|
### C2 — Symbol Relations
|
||||||
|
**Назначение:** связи между symbols.
|
||||||
|
**Единица:** relation.
|
||||||
|
**Как формируется:** вторым проходом по AST и структурным зависимостям.
|
||||||
|
**Статус в MVP:** да, в ограниченном виде.
|
||||||
|
|
||||||
|
### C3 — Entrypoints
|
||||||
|
**Назначение:** каталог точек входа системы.
|
||||||
|
**Единица:** entrypoint.
|
||||||
|
**Как формируется:** специализированными детекторами entrypoint-паттернов.
|
||||||
|
**Статус в MVP:** да, минимально.
|
||||||
|
|
||||||
|
### C4 — Execution Paths
|
||||||
|
**Назначение:** типовые пути исполнения.
|
||||||
|
**Единица:** path.
|
||||||
|
**Как формируется:** поверх `C2` и `C3` через производную трассировку.
|
||||||
|
**Статус в MVP:** нет.
|
||||||
|
|
||||||
|
### C5 — Test Mappings
|
||||||
|
**Назначение:** связи production code ↔ tests.
|
||||||
|
**Единица:** mapping.
|
||||||
|
**Как формируется:** по путям, именам, импортам и конвенциям проекта.
|
||||||
|
**Статус в MVP:** да, минимально.
|
||||||
|
|
||||||
|
### C6 — Code Facts
|
||||||
|
**Назначение:** нормализованные факты из кода.
|
||||||
|
**Единица:** fact.
|
||||||
|
**Как формируется:** поверх `C1–C3` как производный слой.
|
||||||
|
**Статус в MVP:** нет.
|
||||||
|
|
||||||
|
## 5.2. DOCS RAG
|
||||||
|
|
||||||
|
### D0 — Document Chunks
|
||||||
|
**Назначение:** базовые фрагменты документации.
|
||||||
|
**Единица:** document chunk.
|
||||||
|
**Как формируется:** документы нормализуются и режутся на chunk’и с сохранением `section path`.
|
||||||
|
**Статус в MVP:** да.
|
||||||
|
|
||||||
|
### D1 — Document Catalog
|
||||||
|
**Назначение:** каталог документов и разделов.
|
||||||
|
**Единица:** `document node / section node`.
|
||||||
|
**Как формируется:** из структуры документов и их заголовков.
|
||||||
|
**Статус в MVP:** да.
|
||||||
|
|
||||||
|
### D2 — Fact Index
|
||||||
|
**Назначение:** атомарные факты из документации.
|
||||||
|
**Единица:** fact.
|
||||||
|
**Как формируется:** из `D0/D1` через правила, шаблоны и при необходимости LLM extraction с валидацией.
|
||||||
|
**Статус в MVP:** частично.
|
||||||
|
|
||||||
|
### D3 — Entity Catalog
|
||||||
|
**Назначение:** каталог сущностей и понятий документации.
|
||||||
|
**Единица:** entity / concept.
|
||||||
|
**Как формируется:** из устойчивых терминов, заголовков, словарей и нормализации повторяющихся сущностей.
|
||||||
|
**Статус в MVP:** да, минимально.
|
||||||
|
|
||||||
|
### D4 — Workflow Index
|
||||||
|
**Назначение:** процедуры, сценарии, последовательности шагов.
|
||||||
|
**Единица:** workflow.
|
||||||
|
**Как формируется:** из use case, процессных разделов и последовательных описаний шагов.
|
||||||
|
**Статус в MVP:** нет.
|
||||||
|
|
||||||
|
### D5 — Reference Graph
|
||||||
|
**Назначение:** граф ссылок между документами, секциями, сущностями и фактами.
|
||||||
|
**Единица:** reference link.
|
||||||
|
**Как формируется:** из явных и неявных cross-links между документами.
|
||||||
|
**Статус в MVP:** нет.
|
||||||
|
|
||||||
|
### D6 — Doc-Code Links
|
||||||
|
**Назначение:** мост между документацией и кодом.
|
||||||
|
**Единица:** `doc artifact ↔ code artifact link`.
|
||||||
|
**Как формируется:** из имен, aliases, путей, устойчивых терминов и других надежных соответствий.
|
||||||
|
**Статус в MVP:** да, минимально.
|
||||||
|
|
||||||
|
## 5.3. Layer scope: Target Architecture vs MVP-now
|
||||||
|
|
||||||
|
### 5.3.1. Target Architecture
|
||||||
|
|
||||||
|
Полная карта слоёв:
|
||||||
|
|
||||||
|
- **CODE:** C0–C6 (Source Chunks, Symbol Catalog, Symbol Relations, Entrypoints, Execution Paths, Test Mappings, Code Facts)
|
||||||
|
- **DOCS:** D0–D6 (Document Chunks, Document Catalog, Fact Index, Entity Catalog, Workflow Index, Reference Graph, Doc-Code Links)
|
||||||
|
|
||||||
|
### 5.3.2. MVP-now
|
||||||
|
|
||||||
|
**Обязательные сейчас:**
|
||||||
|
|
||||||
|
- `C0_SOURCE_CHUNKS`
|
||||||
|
- `C1_SYMBOL_CATALOG`
|
||||||
|
- `C2_SYMBOL_RELATIONS`
|
||||||
|
- `C3_ENTRYPOINTS`
|
||||||
|
|
||||||
|
**В облегчённом виде:**
|
||||||
|
|
||||||
|
- `C5_TEST_MAPPINGS` или `C5-lite`
|
||||||
|
|
||||||
|
**Не блокируют текущий этап:**
|
||||||
|
|
||||||
|
- `C4_EXECUTION_PATHS`
|
||||||
|
- `C6_CODE_FACTS`
|
||||||
|
- весь docs runtime (слои D0–D6 в исполнении runtime)
|
||||||
|
|
||||||
|
Слои документации остаются частью target architecture; docs retrieval пока не обязателен для текущего code-first milestone.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Итоговая рамка MVP-now
|
||||||
|
|
||||||
|
Сейчас система должна стабильно работать в **test-first** режиме.
|
||||||
|
|
||||||
|
**Фокус:**
|
||||||
|
|
||||||
|
- CODE_QA;
|
||||||
|
- через тесты настраиваются:
|
||||||
|
- intent routing (IntentRouterV2);
|
||||||
|
- layered retrieval;
|
||||||
|
- evidence sufficiency;
|
||||||
|
- answer quality;
|
||||||
|
- diagnostics.
|
||||||
|
|
||||||
|
**Не входят в текущий milestone:**
|
||||||
|
|
||||||
|
- UI-интеграция;
|
||||||
|
- docs runtime;
|
||||||
|
- полная интеграция orchestration переносится на следующий этап после стабилизации test pipeline.
|
||||||
|
|
||||||
|
В целевой архитектуре по-прежнему заложены:
|
||||||
|
- уверенная работа с кодом, symbols, entrypoints, тестами;
|
||||||
|
- ответ по документации и мост docs ↔ code;
|
||||||
|
- генерация документации по коду;
|
||||||
|
- fallback при неуверенном роутинге.
|
||||||
|
|
||||||
|
В MVP-now сознательно **не включаются** самые дорогие части:
|
||||||
|
- полноценные execution paths для всей системы;
|
||||||
|
- богатые fact-индексы по всем доменам;
|
||||||
|
- полный reference graph документации;
|
||||||
|
- глубокая автоматизация подготовки системной аналитики.
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
# MVP: процесс v1
|
||||||
|
|
||||||
|
## 1. Общее описание
|
||||||
|
|
||||||
|
Запрос пользователя обрабатывается цепочкой API → рантайм агента → зарегистрированный процесс версии `v1` → один workflow из трёх последовательных шагов. Процесс **не** обращается к RAG и **не** маршрутизирует интенты: текст сообщения передаётся в LLM по фиксированному промпту. Ответ агента — результат генерации с лёгкой постобработкой (trim).
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
subgraph api [API]
|
||||||
|
RS[RequestService]
|
||||||
|
end
|
||||||
|
subgraph runtime [Agent runtime]
|
||||||
|
AR[AgentRuntime]
|
||||||
|
PR[ProcessRunner]
|
||||||
|
end
|
||||||
|
subgraph v1 [Процесс v1]
|
||||||
|
P1[V1Process]
|
||||||
|
WG[V1FlowMainGraph]
|
||||||
|
end
|
||||||
|
subgraph wf [Workflow v1.flow_main]
|
||||||
|
S1[PrepareUserMessageStep]
|
||||||
|
S2[GenerateAnswerStep]
|
||||||
|
S3[FinalizeAnswerStep]
|
||||||
|
end
|
||||||
|
LLM[AgentLlmService]
|
||||||
|
RS --> AR
|
||||||
|
AR --> PR
|
||||||
|
PR --> P1
|
||||||
|
P1 --> WG
|
||||||
|
WG --> S1 --> S2 --> S3
|
||||||
|
S2 --> LLM
|
||||||
|
```
|
||||||
|
|
||||||
|
Клиент создаёт запрос с `process_version: v1`. `AgentRuntime` поднимает `RuntimeExecutionContext` (запрос, сессия, publisher, trace), выбирает `V1Process` из реестра и вызывает `run`. `V1Process` собирает `V1FlowContext` и прогоняет линейный граф: подготовка текста, один вызов LLM, финализация строки ответа. Итог попадает в `ProcessResult.answer` и дальше в ответ пользователю.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Шаги и контракты
|
||||||
|
|
||||||
|
### 2.1. Вход в процесс: `V1Process.run`
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|--|--|
|
||||||
|
| **Название** | Запуск процесса v1 |
|
||||||
|
| **Задача** | Собрать контекст workflow и выполнить граф до готового ответа. |
|
||||||
|
| **Вход** | `RuntimeExecutionContext`: `request` (в т.ч. `message`), `session`, `publisher`, `trace`. |
|
||||||
|
| **Выход** | `ProcessResult` с полем `answer: str`. |
|
||||||
|
| **Как работает** | Создаётся `V1FlowContext` с `prompt_name` по умолчанию `v1_flow_main.answer`. Вызывается `V1FlowMainGraph.run`. Возвращается ответ из контекста workflow. |
|
||||||
|
|
||||||
|
Код: `src/app/core/agent/processes/v1/process.py`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.2. Шаг workflow: `PrepareUserMessageStep`
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|--|--|
|
||||||
|
| **Название** | Подготовка сообщения пользователя |
|
||||||
|
| **Задача** | Сформировать строку, которая уйдёт в LLM как пользовательский ввод. |
|
||||||
|
| **Вход** | `V1FlowContext` с заполненным `runtime` и `prompt_name`. |
|
||||||
|
| **Выход** | Тот же контекст с `prepared_message: str`. |
|
||||||
|
| **Как работает** | Берётся `context.runtime.request.message` и обрезаются пробелы по краям (`strip`). Результат пишется в `prepared_message`. Других преобразований нет. |
|
||||||
|
|
||||||
|
Код: `src/app/core/agent/processes/v1/workflow/flow_main/steps/prepare_user_message_step.py`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.3. Шаг workflow: `GenerateAnswerStep`
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|--|--|
|
||||||
|
| **Название** | Вызов LLM |
|
||||||
|
| **Задача** | Сгенерировать ответ по выбранному промпту и подготовленному сообщению. |
|
||||||
|
| **Вход** | `V1FlowContext` с `prepared_message`, `prompt_name`, `runtime.trace` для модуля LLM. |
|
||||||
|
| **Выход** | Контекст с `answer: str` (сырой ответ модели). |
|
||||||
|
| **Как работает** | Асинхронно в пуле потоков вызывается `AgentLlmService.generate(prompt_name, prepared_message, ...)`. В trace подключается модуль `workflow.v1.llm`. Идентификатор запроса передаётся в `log_context` для логов. |
|
||||||
|
|
||||||
|
Код: `src/app/core/agent/processes/v1/workflow/flow_main/steps/generate_answer_step.py`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.4. Шаг workflow: `FinalizeAnswerStep`
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|--|--|
|
||||||
|
| **Название** | Финализация ответа |
|
||||||
|
| **Задача** | Нормализовать строку ответа перед выдачей пользователю. |
|
||||||
|
| **Вход** | `V1FlowContext` с заполненным `answer` после LLM. |
|
||||||
|
| **Выход** | Контекст с обновлённым `answer`. |
|
||||||
|
| **Как работает** | К ответу применяется `strip()` по краям. Другой логики нет. |
|
||||||
|
|
||||||
|
Код: `src/app/core/agent/processes/v1/workflow/flow_main/steps/finalize_answer_step.py`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.5. Транспорт: `WorkflowGraph` (v1)
|
||||||
|
|
||||||
|
Граф для v1 использует стандартный `WorkflowGraph`: на каждом шаге пишутся события `workflow_started`, `step_started`, `step_completed`, `workflow_completed` в `runtime_traces` через `context.runtime.trace`.
|
||||||
|
|
||||||
|
Код: `src/app/core/agent/utils/workflow/graph.py`, обёртка `V1FlowMainGraph` в `src/app/core/agent/processes/v1/workflow/flow_main/graph.py`.
|
||||||
@@ -0,0 +1,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
@@ -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}
|
||||||
|
|||||||
@@ -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"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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`. |
|
|
||||||
@@ -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"]
|
|
||||||
@@ -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` на основе текущей реализации. |
|
||||||
@@ -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`. |
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# DOCS Intent Router MVP
|
||||||
|
|
||||||
|
## Supported Intents
|
||||||
|
|
||||||
|
- `DOCS_QA.API_METHOD_EXPLAIN`
|
||||||
|
- `DOCS_DISCOVERY.LIST_API_METHODS`
|
||||||
|
- `DOCS_DISCOVERY.FIND_DOCUMENTS_BY_DOMAIN`
|
||||||
|
- `DOCS_GENERATION.GENERATE_OPENAPI`
|
||||||
|
- `DOCS_FALLBACK.GENERAL_DOCS_QA`
|
||||||
|
|
||||||
|
## Routing Flow
|
||||||
|
|
||||||
|
1. `Stage A`: deterministic pre-routing нормализует запрос, извлекает anchors и scope, считает rule-based confidence.
|
||||||
|
2. `Stage B`: confidence gating пропускает high-confidence кейсы напрямую и эскалирует ambiguous/weak запросы в LLM.
|
||||||
|
3. `Stage C`: LLM classifier выбирает только один из 5 MVP саб-интентов и возвращает строгий JSON.
|
||||||
|
4. После выбора саб-интента router всегда прикрепляет декларативный `retrieval_plan`.
|
||||||
|
|
||||||
|
## Confidence And Escalation
|
||||||
|
|
||||||
|
- `>= 0.8` и без конфликтующих сигналов: `routing_mode=deterministic`.
|
||||||
|
- Ниже порога, при пересечении интентов, слабых anchors или коротком неоднозначном запросе: `routing_mode=llm_assisted`.
|
||||||
|
- Если LLM недоступен или вернул невалидный класс: `routing_mode=llm_fallback` c fallback в `GENERAL_DOCS_QA`.
|
||||||
|
|
||||||
|
## Retrieval Plan Mapping
|
||||||
|
|
||||||
|
- `API_METHOD_EXPLAIN` -> `docs_api_method_explain_v1`
|
||||||
|
- `LIST_API_METHODS` -> `docs_list_api_methods_v1`
|
||||||
|
- `FIND_DOCUMENTS_BY_DOMAIN` -> `docs_find_documents_by_domain_v1`
|
||||||
|
- `GENERATE_OPENAPI` -> `docs_generate_openapi_v1`
|
||||||
|
- `GENERAL_DOCS_QA` -> `docs_general_docs_qa_v1`
|
||||||
|
|
||||||
|
`retrieval_plan` хранится декларативно в `src/app/modules/agent/intent_router_v2/docs_mvp/retrieval_plans.py`, а legacy `retrieval_spec.filters` обогащается теми же anchors и scope для совместимости с текущим runtime.
|
||||||
@@ -0,0 +1,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`, строки 74–81.
|
||||||
|
|
||||||
|
- **Условие:** при `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`, строки 47–69, 71–132.
|
||||||
|
|
||||||
|
- **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`, строки 21–41.
|
||||||
|
|
||||||
|
- **Фактическая точка входа 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 (строки 90–91 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`, строки 18–21. При 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`, строки 242–246: `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`, 90–106 — resolved_intent, resolved_sub_intent, resolved_target, target_type, code_chunks, relations, entrypoints, test_evidence, evidence_count, retrieval_summary.
|
||||||
|
- `AnswerSynthesisInput`: `contracts.py`, 109–121 — 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`, 281–298).
|
||||||
|
|
||||||
|
- **Правила по sub_intent (post_gate):**
|
||||||
|
- **EXPLAIN** (93–124): target focus; vagueness (_VAGUE_PHRASES); наличие required_methods/calls/dependencies (хотя бы одна группа); «too_vague_for_explain» при нуле совпадений; semantic_leakage (роли из semantic_hints без опоры на код).
|
||||||
|
- **ARCHITECTURE** (126–150): 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** (152–171): 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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Документ описывает только текущую реализацию по коду после рефакторинга.*
|
||||||
@@ -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
|
||||||
|
|
||||||
|
Каждый документ должен быть явно связан как минимум с соответствующим кодом и с соседними документами, если такие связи существуют. Агент должен фиксировать эти связи в метаданных и в тексте документа, чтобы документация образовывала связанную систему знаний, а не набор изолированных файлов.
|
||||||
@@ -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`
|
||||||
@@ -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, который будет генерировать новые кейсы в нужном формате.
|
||||||
@@ -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
@@ -0,0 +1,3 @@
|
|||||||
|
from app.core.agent.runtime import AgentRuntime
|
||||||
|
|
||||||
|
__all__ = ["AgentRuntime"]
|
||||||
@@ -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",
|
||||||
|
]
|
||||||
@@ -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))
|
||||||
@@ -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)
|
||||||
@@ -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
Reference in New Issue
Block a user