Compare commits

...

21 Commits

Author SHA1 Message Date
alex 2b807623f1 Фиксирую состояние 2026-04-16 11:37:11 +03:00
alex 77851e99a7 Фиксирую рабочее состояние 2026-04-15 14:20:27 +03:00
alex 7f22a00696 Настройка процесса генерации документации 2026-04-14 16:40:30 +03:00
alex acac19da71 Фисирую состояние 2026-04-10 15:15:13 +03:00
alex 4e3435ad92 Работает агент, поправлены пути 2026-04-10 10:29:17 +03:00
alex 2352f91cd3 ввв 2026-04-09 15:41:07 +03:00
alex f62fb678b8 фиксирую состояние 2026-04-07 21:41:27 +03:00
alex 7387e5cc51 удалил тестовые данные 2026-04-07 21:41:20 +03:00
alex 8b7b72967e Роутер работает нормально в process v2 2026-04-07 14:09:51 +03:00
alex 0a25e42ea1 Подчистил архитектуру приложения v1 работает 2026-04-01 12:28:55 +03:00
alex 51378c5d66 ййй 2026-03-27 15:51:10 +03:00
alex 15586f9a8c Рефакторинг 2026-03-12 23:33:51 +03:00
alex 9066c292de Удаление легаси 2026-03-12 21:11:16 +03:00
alex b1f825e6b9 Фиксируем состояние 2026-03-12 20:40:29 +03:00
alex 095d354112 Фикс состояния 2026-03-12 16:55:30 +03:00
alex 6ba0a18ac9 Фикс состояния 2026-03-12 16:55:23 +03:00
alex 417b8b6f72 Фиксация изменений 2026-03-05 11:03:17 +03:00
alex 1ef0b4d68c Новый раг 2026-03-01 14:21:33 +03:00
alex 2728c07ba9 Обновил readme.md 2026-02-28 09:37:33 +03:00
alex 9f1c67a751 chore: remove .env from tracking 2026-02-27 21:30:22 +03:00
alex e8805ffe29 первый коммит 2026-02-27 21:28:09 +03:00
793 changed files with 44450 additions and 4191 deletions
+14
View File
@@ -0,0 +1,14 @@
# Analysis Assets
Этот каталог содержит служебные артефакты для аналитической и генеративной работы агента.
## Структура
- `rules/` — правила построения документации, frontmatter и шаблоны документов.
## Назначение
Каталог `.analysis/` отделен от `docs/`, чтобы:
- хранить служебные policy- и template-материалы вне пользовательской документации;
- передавать правила в LLM как отдельный policy-context;
- не смешивать документацию проекта и внутренние артефакты анализа.
+32
View File
@@ -0,0 +1,32 @@
# Documentation Rules
## Назначение
Этот файл фиксирует общие правила формирования, обновления и поддержки технической документации проекта.
Документация проекта должна создаваться как система атомарных, связанных между собой документов, пригодных:
- для чтения человеком;
- для сопровождения командой;
- для индексирования в RAG;
- для автоматического обновления агентом на основе кода и существующих артефактов.
Этот файл задает:
- общие принципы документационной архитектуры;
- правила декомпозиции документации;
- правила размещения файлов;
- требования к связям между документами;
- требования к качеству markdown-документов;
- правила генерации и обновления документации агентом.
Детальные шаблоны документов и правила frontmatter описываются отдельно:
- `.analysis/rules/frontmatter-rules.md`
- `.analysis/rules/templates/*.md`
---
## Область действия
Правила из этого файла применяются ко всей проектной документации, размещаемой в:
```text
docs/documentation/
+60
View File
@@ -0,0 +1,60 @@
# Frontmatter Rules
## Назначение
Этот файл фиксирует правила YAML frontmatter для документов в `docs/documentation/`.
Frontmatter обязателен для каждого markdown-документа и нужен для:
- идентификации документа;
- определения типа документа;
- фиксации связей с кодом и другими документами;
- выделения сущностей, тегов и домена;
- поддержки индексирования в RAG.
Общие правила построения документации описаны в:
- `.analysis/rules/documentation-rules.md`
Шаблоны markdown body описаны в:
- `.analysis/rules/templates/*.md`
---
## Общие правила
1. Frontmatter размещается в начале файла.
2. Формат — YAML между двумя строками `---`.
3. Все документы в `docs/documentation/` должны содержать frontmatter.
4. Поля должны быть стабильными и заполняться единообразно.
5. Не использовать произвольные поля без необходимости.
6. Если значение неизвестно и его нельзя уверенно вывести из evidence, поле лучше не заполнять, кроме обязательных полей.
7. Списковые поля должны оформляться как YAML-массивы.
8. Идентификаторы и ссылки должны быть стабильными и пригодными для машинной обработки.
---
## Базовый frontmatter
Каждый документ должен начинаться с frontmatter вида:
```yaml
---
id: api-orders-create
title: Метод создания заказа
doc_type: api_method
domain: orders
status: draft
owner: system-analyst
source_of_truth: code
related_docs:
- ui-order-create-page
- logic-order-validation
related_code:
- src/orders/api/create_order.py
entities:
- Order
- CreateOrder
tags:
- api
- orders
- create
---
@@ -0,0 +1,29 @@
# Rule: Use Document Templates From Fixed Paths
Агент должен создавать и обновлять техническую документацию только с опорой на шаблоны документов, расположенные в `.analysis/rules`.
Если агент формирует новый документ, он обязан:
- определить тип документа;
- выбрать соответствующий шаблон по фиксированному пути;
- сохранить структуру секций и базовых метаданных из шаблона;
- заполнять только те секции, которые подтверждены кодом и артефактами;
- не придумывать новые произвольные форматы, если для типа уже существует шаблон.
Пути к базовым шаблонам:
- `.analysis/rules/legacy/template_ui_page.md`
- `.analysis/rules/legacy/template_api_method.md`
- `.analysis/rules/legacy/template_logic_block.md`
Правило выбора шаблона:
- для документа типа `ui_page` использовать `.analysis/rules/legacy/template_ui_page.md`
- для документа типа `api_method` использовать `.analysis/rules/legacy/template_api_method.md`
- для документа типа `logic_block` использовать `.analysis/rules/legacy/template_logic_block.md`
Если для нужного типа шаблон отсутствует, агент должен:
1. использовать ближайший подходящий существующий шаблон как временную основу;
2. явно сохранить тип документа в `YAML frontmatter`;
3. не смешивать в одном документе несколько независимых сущностей.
@@ -0,0 +1,89 @@
# Template: api_method
```md
---
id: api-<stable-id>
title: <Human-readable title>
doc_type: api_method
status: draft
source_of_truth: code
domain: <domain-name>
owner: system-analyst
endpoint: <METHOD /path>
auth: <auth-mode-or-unknown>
idempotent: <true-or-false>
related_docs:
- <doc-id>
related_code:
- <path/to/file>
entities:
- <EntityName>
tags:
- api
---
# <API Method Title>
## Purpose
Кратко опиши, какую системную задачу решает метод.
## Endpoint Summary
- Endpoint: `<METHOD /path>`
- Auth: `<auth-mode>`
- Idempotent: `<true/false>`
- Triggered by: `<ui/system/integration if known>`
## Technical Use Case
Опиши пошагово обработку запроса:
- вход в endpoint;
- ключевые проверки;
- вызовы логики;
- обращения к БД и внешним системам;
- формирование ответа.
## Functional Requirements
Вынеси сюда подтвержденные правила, которые дополняют основной сценарий:
- валидации;
- branching logic;
- побочные эффекты;
- ограничения по данным;
- условия ошибок.
## Request and Response Contract
Опиши контракт в кратком виде или дай ссылку на OpenAPI / контрактный файл.
## Related Logic Blocks
- [<Logic block title>](<path-or-doc-link>)
## Data Access and Integrations
- Reads DB: `<if known>`
- Writes DB: `<if known>`
- Integrates with: `<if known>`
## Non-Functional Requirements
Укажи только подтвержденные НФТ:
- timeout;
- audit;
- monitoring;
- security;
- idempotency rules.
## Related Code
- `<path/to/file>`
## Related Documents
- [<Related document>](<path-or-doc-link>)
```
@@ -0,0 +1,71 @@
# Template: logic_block
```md
---
id: logic-<stable-id>
title: <Human-readable title>
doc_type: logic_block
status: draft
source_of_truth: code
domain: <domain-name>
owner: system-analyst
related_docs:
- <doc-id>
related_code:
- <path/to/file>
entities:
- <EntityName>
tags:
- logic
---
# <Logic Block Title>
## Purpose
Кратко опиши, какую переиспользуемую или устойчивую логику реализует блок.
## Where Used
- Called from: `<ui/api/jobs/services if known>`
- Used by: `<list of known callers>`
## Technical Use Case
Опиши пошагово, как работает логический блок:
- входные данные;
- ключевые проверки;
- преобразования;
- обращения к данным;
- результат работы.
## Functional Requirements
Вынеси сюда устойчивые правила и ограничения:
- бизнес-правила;
- проверки;
- ветвления;
- ограничения на вход и выход;
- условия отказа.
## Dependencies
- Uses logic: `<other logic blocks if known>`
- Reads DB: `<if known>`
- Writes DB: `<if known>`
- Integrates with: `<if known>`
## Error Cases
Опиши значимые ошибки и условия их возникновения, если они подтверждены кодом.
## Related Code
- `<path/to/file>`
## Related Documents
- [<Related document>](<path-or-doc-link>)
```
@@ -0,0 +1,82 @@
# Template: ui_page
```md
---
id: ui-<stable-id>
title: <Human-readable title>
doc_type: ui_page
status: draft
source_of_truth: code
domain: <domain-name>
owner: system-analyst
related_docs:
- <doc-id>
related_code:
- <path/to/file>
entities:
- <EntityName>
tags:
- ui
---
# <Page Title>
## Purpose
Кратко опиши, какую пользовательскую задачу решает страница.
## Route and Entry Points
- Route: `<route-if-known>`
- Entry points: `<where user comes from>`
## Technical Use Case
Опиши пошаговый сценарий работы страницы как поток действий и системных реакций.
## UI Structure
Перечисли основные UI-элементы и для каждого укажи:
- назначение;
- источник данных;
- значение по умолчанию или placeholder;
- условия доступности или активации;
- поведение при взаимодействии;
- правила валидации.
## Functional Requirements
Вынеси сюда детальные правила, которые не стоит перегружать в use case:
- вызовы API;
- обработку ответов;
- локальные правила отображения;
- условия переходов;
- feature toggles.
## Related APIs
- [<API document title>](<path-or-doc-link>)
## Related Logic Blocks
- [<Logic block title>](<path-or-doc-link>)
## Non-Functional Requirements
Укажи НФТ, если они подтверждены:
- analytics events;
- observability;
- feature toggles;
- security constraints.
## Related Code
- `<path/to/file>`
## Related Documents
- [<Related document>](<path-or-doc-link>)
```
+115
View File
@@ -0,0 +1,115 @@
# {{title}}
## Summary
- Purpose:
- Actor:
- Trigger:
- Endpoint:
- Main entities:
- Main logic:
- Main errors:
- Source of truth:
## Назначение
## Контекст
## Технический use case
### Основной сценарий
1.
2.
3.
### Альтернативные ветки
-
-
## Функциональные требования
### Request validation
-
### Processing rules
-
### State changes
-
### Side effects
-
## Contract
### Endpoint
- Method:
- Path:
- Auth:
- Idempotent:
- Timeout:
- Retry:
### Request
| Field | Type | Required | Constraints | Description |
|------|------|----------|-------------|-------------|
| | | | | |
### Response
| Field | Type | Description |
|------|------|-------------|
| | | |
### External contract refs
- OpenAPI:
- Schema:
- DTO / serializer:
- Additional refs:
## Errors
| error_id | http_code | when | client_behavior | retry |
|----------|-----------|------|-----------------|-------|
| | | | | |
## Нефункциональные требования
### Security
-
### Observability
- Logs:
- Metrics:
- Traces:
- Audit:
### Reliability
-
-
### Performance
-
## Связанные блоки логики
-
## Связанные сущности
-
## Связанный код
### Files
-
### Symbols
-
## Связанные документы
-
## История изменений
| Date | Source | Changes |
|------|--------|---------|
| | | |
@@ -0,0 +1,105 @@
# {{title}}
## Summary
- Scope:
- Purpose:
- Main modules:
- Main domains:
- Main integrations:
- Key entrypoints:
- Key data flows:
- Source of truth:
## Назначение
## Контекст
## Границы системы
### In scope
-
### Out of scope
-
## Архитектурная схема
## Основные модули
| module | responsibility | depends_on | key_code_refs |
|--------|----------------|------------|---------------|
| | | | |
## Основные доменные области
-
-
## Основные интеграции
| integration | direction | purpose | protocol / transport | related_docs |
|-------------|-----------|---------|----------------------|--------------|
| | | | | |
## Основные потоки
### Flow 1
1.
2.
3.
### Flow 2
1.
2.
3.
## Архитектурные решения и ограничения
### Key decisions
-
### Constraints
-
### Risks
-
## Нефункциональные аспекты
### Security
-
### Reliability
-
### Observability
- Logs:
- Metrics:
- Traces:
- Audit:
### Performance
-
### Scalability
-
## Связанные сущности
-
## Связанный код
### Files
-
### Symbols
-
## Связанные документы
-
## История изменений
| Date | Source | Changes |
|------|--------|---------|
| | | |
@@ -0,0 +1,92 @@
# {{title}}
## Summary
- Domain:
- Purpose:
- Entity role:
- Main attributes:
- Lifecycle:
- Invariants:
- Related APIs:
- Related logic:
- Source of truth:
## Назначение
## Контекст
## Роль в доменной модели
## Атрибуты
| attribute | type | required | description | constraints |
|-----------|------|----------|-------------|-------------|
| | | | | |
## Состояния и жизненный цикл
### Основные состояния
-
### Переходы состояний
1.
2.
3.
## Инварианты и ограничения
-
-
## Связи с другими сущностями
| entity | relation | description |
|--------|----------|-------------|
| | | |
## Использование в системе
### Related API
-
### Related UI
-
### Related logic
-
### Related integrations
-
## Функциональные требования
-
-
## Нефункциональные требования
### Audit / history
-
### Security
-
### Observability
-
## Связанный код
### Files
-
### Symbols
-
## Связанные документы
-
## История изменений
| Date | Source | Changes |
|------|--------|---------|
| | | |
+93
View File
@@ -0,0 +1,93 @@
```md
# {{title}}
## Summary
- Purpose:
- Trigger:
- Inputs:
- Outputs:
- Main entities:
- Main dependencies:
- Side effects:
- Source of truth:
## Назначение
## Контекст
## Технический use case
### Основной сценарий
1.
2.
3.
### Альтернативные ветки
-
-
## Функциональные требования
### Preconditions
-
### Processing rules
-
### Validation rules
-
### Output / result rules
-
### Side effects
-
## Ограничения и условия вызова
-
-
## Нефункциональные требования
### Security
-
### Observability
- Logs:
- Metrics:
- Traces:
- Audit:
### Reliability
-
-
### Performance
-
## Связанные API / UI / integration points
-
## Связанные сущности
-
## Связанный код
### Files
-
### Symbols
-
## Связанные документы
-
## История изменений
| Date | Source | Changes |
|------|--------|---------|
| | | |
```
+97
View File
@@ -0,0 +1,97 @@
# {{title}}
## Summary
- Purpose:
- Actor:
- Trigger:
- Route:
- Main API:
- Main entities:
- Main logic:
- Main states:
- Source of truth:
## Назначение
## Контекст
## Технический use case
### Основной сценарий
1.
2.
3.
### Альтернативные ветки
-
-
## Описание UI
## UI Elements
| id | type | label | data_source | default / placeholder | validation | behavior |
|----|------|-------|-------------|------------------------|------------|----------|
| | | | | | | |
## Функциональные требования
### Input rules
-
### State rules
-
### Navigation rules
-
### Client-side validation
-
## Нефункциональные требования
### Security
-
### Observability
- Logs:
- Metrics:
- Traces:
- Analytics:
### Accessibility
-
### Performance
-
### Feature toggles
-
## Связанные API
-
## Связанные блоки логики
-
## Связанные сущности
-
## Связанный код
### Files
-
### Symbols
-
## Связанные документы
-
## История изменений
| Date | Source | Changes |
|------|--------|---------|
| | | |
+15
View File
@@ -0,0 +1,15 @@
---
alwaysApply: true
---
При задачах на создание или обновление документации всегда:
1. Читай .analysis/rules/documentation-rules.md, .analysis/rules/frontmatter-rules.md и нужный шаблон из .analysis/rules/templates/.
2. Создавай и обновляй документы только в docs/documentation/.
3. Не создавай дублей: сначала ищи существующий документ, потом обновляй его.
4. Соблюдай принцип: один документ = одна сущность / один устойчивый аспект.
5. Каждый документ должен иметь YAML frontmatter, обязательные разделы Summary и Details и структуру по шаблону.
6. Все связи фиксируй явно: related_docs, related_code, entities, tags и typed-поля.
7. Используй только подтвержденный evidence из кода, контрактов, конфигов и существующей документации.
8. Не дублируй содержание между документами — используй ссылки.
9. Явно указывай связанный код и связанные документы.
10. Не выдумывай факты, если evidence недостаточно.
+12
View File
@@ -0,0 +1,12 @@
.env
.venv
__pycache__
# Runtime agent traces (local only; written by RequestTraceLogger)
runtime_traces/
# Pipeline harness: per-run artifacts (md/json from tests.pipeline_setup_v3/v4)
tests/**/test_runs/**/*.md
tests/**/test_runs/**/*.json
tests/**/test_results/**/*.md
tests/**/test_results/**/*.json
+25
View File
@@ -0,0 +1,25 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Agent Backend: Uvicorn (Debug)",
"type": "python",
"request": "launch",
"module": "uvicorn",
"args": [
"app.main:app",
"--host",
"0.0.0.0",
"--port",
"15000"
],
"cwd": "${workspaceFolder}",
"envFile": "${workspaceFolder}/.env",
"env": {
"PYTHONPATH": "${workspaceFolder}/src"
},
"console": "integratedTerminal",
"justMyCode": false
}
]
}
+5
View File
@@ -0,0 +1,5 @@
{
"files.exclude": {
"**/__pycache__": true
}
}
+3 -2
View File
@@ -3,12 +3,13 @@ FROM python:3.12-slim
WORKDIR /app WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE=1 \ ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 PYTHONUNBUFFERED=1 \
PYTHONPATH=/app/src
COPY requirements.txt ./ COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt RUN pip install --no-cache-dir -r requirements.txt
COPY app ./app COPY src ./src
EXPOSE 15000 EXPOSE 15000
+1174 -108
View File
File diff suppressed because it is too large Load Diff
+4
View File
@@ -0,0 +1,4 @@
# Запросы
1. Какие методы апи есть в проекте
2. Какие методы апи есть для healthcheck
3. Где документация на healthcheck
BIN
View File
Binary file not shown.
+126
View File
@@ -0,0 +1,126 @@
# Процессы работы с документацией (AS IS / TO BE)
## Основные артефакты системной аналитики
Системные аналитики работают с 3 артефактами:
- бизнес-требованиями
- системной аналитикой
- технической документацией
### Бизнес требования
Описывает бизнес и пользовательские требования, пользователькие use case, макеты экранов.
Сейчас не всегда оформляется как отдельный документ, часто этот шаг пропускается и требования фиксируются сразу в документе системной аналитики.
### Системная аналитика
Документ описыватет изменения в автоматизированной системе. Пишется системными аналитиками для разработчиков и тестировщиков. Так же этот документ проходит согласование с экспертами по архитектуре, безопасности, сопровождению.
Может описывать как целиком процесс (в случае реализации с нуля), так и инкремент, который вносит небольшие изменения в существующие процессы.
В данном документе содкржится вся информация по сути вносимых изменений, но отсутствует контекст о текущей реализации системы.
Состоит из разделов:
- Цели - короткое описание какую проблему и для кого решаем.
- Процесс AS IS и TO BE - фокус на изменения с точки зрения бизнес функций, без технической детализации.
- Ограничения - ограничения и допущения в реализации.
- Архитектура - описывает схему уровня контейнеров, основной фокус на интеграции между контейнерами и интеграционные сценарии.
- Функциональные требования - описывают изменения в системе.
- Нефункциональные требования - требования к аудиту, мониторингу, фичетоглам, пользовтелькой аналитике.
### Техническая документация
Техническая документация описывает реализацию системы. Эта информация используется командой разработки при проектировании и реализации новых фичей, понимании как работает система. Артефакт живет чуть впереди кода
Представялет из себя иерархическую модель документов, сейчас реализованную в конфлюенсе.
Есть несколько типов страниц, каждая из которы описывает определенный тип функциональности
- UI страницы
- API методы
- БД
- Логические блоки
#### UI страницы
Описывают экран на UI.
**Декомпозиция**
Как правило на страницу с описанием выносится целый макет/страница фронтального приложения, с одной основной интеграцией и опционально вспомогательными интеграциями.
Например - форма создания сущности. Есть вспомогательгные методы для полученяи правочников, использующихся при заполнении полей на форме, и вызов оснвного метода создания сущности.
Таким образом приложение декомпозируется на отдельные экраны, коотры свызываются между собой последовательно, но сами по себе являются независимыми
**Состав описания**
Все разделы обязательны.
Страница с описанием содержит:
- Краткое описание
- Технический use case
- Описание макета с декомпозицией на компоненты + их поведение
- Функциональные требования - описание интеграций и логики, специфичной для этой формы UI
- Нефункциональные требования - фичетоглы и события пользовательской аналитики
#### API методы
**Декомпозиция**
На каждый метод API заводится отдельная страница.
**Состав описания**
Все разделы обязательны.
Страница с описанием содержит:
- Краткое описание
- Технический use case
- Функциональные требования - описание интеграций и логики, специфичной для этой формы UI
- Нефункциональные требования - фичетоглы и события пользовательской аналитики
- Контракт метода - описание запроса и ответа. Для ответа так же приводится описание как заполнять поля.
#### БД
**Декомпозиция**
Сейсас это только странциа с описанием таблица. На каждую таблицу заводится отдельная страница.
**Состав описания**
Все разделы обязательны.
Страница с описанием содержит:
- Краткое описание
- Таблица с офисанием физической модели данных
#### Логические блоки
**Декомпозиция**
На отдельную страницу может быть вынесен общий переиспользуемый блок логики. Это позволяет не дублировать его на страницах документации. Как правило соответствует реализации общего компонента в коде.
**Состав описания**
Часть разделов в описании может отсутствовать.
- Краткое описание
- Технический use case
- Функциональные требования - описание интеграций и логики, специфичной для этой формы UI
- Нефункциональные требования - фичетоглы и события пользовательской аналитики
#### Прочие особенности процесса
##### Описание технических use cases
Сценарий описывает основные шаги процесса в разрезе участников, все технические детали, если их нельзя описать одним предложением, выносятся в разделы функциональных требований, нефункциональных требований, или даются ссылки на другие страницы (как правило это страницы с логическими блоками).
В технических use cases приводятся ссылки на страницы с описнаием вызываемых методов API. Особенно это актуально для страниц фронта, т.к. он использует наши методы API, которые есть в документации. Для интеграций с другими АС как правило приводистя ссылка на описание конфлюенса.
## AS IS
Сейчас все артефакты ведутся в конфлюенс. Одна страница содержит описанием одного аретфакта (бизнес требования, системная аналитика, страница документации), страницы организованы иерархически, используюстя ссылки для обозначения связей.
Проблемы:
- документация со временем теряет актуальность
- отсутствие автоматизации
- ручное ведение
---
## TO BE
Целевое состояние:
- аналитик продолжает писать артефакты бизнес-требований и системной аналитики
- агент генерирует и обновляет документацию по странице системной аналитики
- документация становится инженерным артефактом, который ведется в GIT
### Форматы
- Markdown
- OpenAPI
- Mermaid / PlantUML
### Роль агента
- использование документации как базы знаний - как для ответов на вопросы, так и для проектирования изменений в системе.
- внесение изменений в документацию по артефактам системной аналитики
- генерация из документации спецификаций OPENAPI и JSON-schema
+235
View File
@@ -0,0 +1,235 @@
Ниже обновленная версия с учетом гибридной модели интент роутера.
---
## 1. Концепция агента
Агент проектируется как intent-driven система для работы с кодом и документацией, где пользовательский запрос сначала нормализуется и интерпретируется, затем по нему извлекается релевантный контекст из многослойного RAG, после чего специализированный task workflow выполняет целевую задачу. Агент не является единым “умным чатом”: логика разделена на маршрутизацию, retrieval и специализированные execution workflows. Проверка evidence, вызовы LLM и правила сборки ответа находятся внутри task workflows и зависят от типа задачи.
---
## 2. Компонентная модель
```mermaid
flowchart LR
IDE[IDE Plugin / Client] --> API[API Layer]
API --> IR[IntentRouter V3]
IR --> RAG[Retrieval RAG]
RAG --> TW1[Task Workflow: Documentation Explain]
RAG --> TW2[Task Workflow: OpenAPI Generation]
RAG --> TW3[Task Workflow: Documentation Generation]
RAG --> TWN[Other Specialized Task Workflows]
TW1 --> OUT[Response / Artifact]
TW2 --> OUT
TW3 --> OUT
TWN --> OUT
```
---
## 3. Основной flow процесса
### Основной процесс
1. Пользователь отправляет запрос через IDE plugin или другой клиент.
2. `API Layer` принимает запрос и передает его в агент.
3. `IntentRouter V3`:
* нормализует запрос;
* детерминированно извлекает ключевые артефакты;
* с помощью LLM определяет тип задачи и параметры обработки;
* формирует параметры retrieval.
4. Выполняется извлечение данных из `Retrieval RAG`.
5. Извлеченный контекст передается в соответствующий `Task Workflow`.
6. Внутри workflow выполняется:
* подготовка контекста;
* evidence-проверки;
* вызовы LLM;
* формирование результата.
7. Результат возвращается пользователю.
### Sequence diagram
```mermaid
sequenceDiagram
participant User as User / IDE Plugin
participant API as API Layer
participant Router as IntentRouter V3
participant RAG as Retrieval RAG
participant WF as Task Workflow
User->>API: request
API->>Router: agent call
Router->>Router: normalize + extract artifacts
Router->>Router: LLM routing (task / intent)
Router->>RAG: retrieval request
RAG-->>Router: retrieved context
Router->>WF: route result + context
WF->>WF: evidence logic + LLM calls
WF-->>API: final result
API-->>User: response
```
---
## 4. Описание компонентов
### 4.1. IDE Plugin / Client
**Задача**
Точка входа пользователя в агент.
**Как устроен**
Любой внешний клиент (IDE plugin, web UI и др.), который отправляет запрос и получает результат.
**Почему так**
Агент изначально проектируется как backend-система, независимая от интерфейса.
---
### 4.2. API Layer
**Задача**
Обеспечивает внешний интерфейс взаимодействия с агентом.
**Как устроен**
Принимает запрос, валидирует его и передает во внутренний pipeline, затем возвращает результат.
**Почему так**
Позволяет изолировать транспортный слой от логики агента.
---
### 4.3. IntentRouter V3
**Задача**
Определяет, как должен обрабатываться пользовательский запрос и какой сценарий выполнения применить.
**Как устроен**
Гибридная модель из двух частей:
#### 1. Детерминированный слой
Выполняет:
* нормализацию запроса;
* извлечение ключевых артефактов:
* домены;
* типы сущностей (API, entity, component и т.д.);
* явные ссылки (endpoint, путь, имя);
* выделение базовых сигналов (например: explain / list / generate).
Этот слой задает **жесткие рамки интерпретации запроса**.
#### 2. LLM-роутинг
Использует:
* нормализованный запрос;
* извлеченные артефакты;
* описание доступных типов задач;
и определяет:
* тип задачи;
* общий сценарий обработки;
* параметры retrieval;
* ожидаемую форму ответа.
#### Итог
Router формирует:
* параметры retrieval;
* тип task workflow;
* контекст для дальнейшего выполнения.
**Почему решение такое**
Ранее использовался более детерминированный подход с фиксированными сценариями, который хорошо работал в узком наборе задач, но плохо масштабируется. Полностью LLM-based роутинг, наоборот, дает гибкость, но теряет предсказуемость и управляемость.
Поэтому выбран гибридный подход:
* детерминированный слой фиксирует ключевые артефакты и ограничения;
* LLM выполняет гибкую интерпретацию задачи.
Это позволяет:
* сохранить управляемость и стабильность;
* избежать взрывного роста количества сценариев;
* поддерживать сложные и нетиповые запросы.
---
### 4.4. Retrieval RAG
**Задача**
Извлечь релевантный контекст для выполнения задачи.
**Как устроен**
Многослойная система хранения знаний (код, документация, факты, связи), из которой извлекается структурированный контекст в зависимости от параметров, заданных роутером.
**Почему так**
Разные задачи требуют разных типов данных, поэтому используется слойная модель вместо плоского поиска.
---
### 4.5. Task Workflows
**Задача**
Реализуют прикладную логику выполнения конкретного типа задачи.
**Как устроены**
Набор специализированных workflows, например:
* объяснение по документации;
* генерация OpenAPI;
* генерация документации;
* другие сценарии.
Внутри workflow находятся:
* обработка контекста;
* evidence-проверки;
* вызовы LLM;
* сборка результата.
**Почему так**
Логика проверки данных и генерации сильно зависит от задачи, поэтому она инкапсулируется в отдельных workflows, а не в одном универсальном слое.
---
### 4.6. Output / Artifact
**Задача**
Вернуть результат пользователю.
**Как устроен**
Может быть:
* текстовый ответ;
* структурированный список;
* OpenAPI спецификация;
* документация;
* иной артефакт.
**Почему так**
Агент должен поддерживать не только ответы, но и генерацию инженерных артефактов.
---
## Итог
Обновленная архитектура строится на следующем принципе:
* **детерминированное извлечение ключевых артефактов** задает рамки;
* **LLM выполняет гибкий роутинг внутри этих рамок**;
* **retrieval обеспечивает данные**;
* **task workflows реализуют прикладную логику и контроль качества**.
Это позволяет одновременно сохранить управляемость системы и обеспечить масштабируемость под новые типы задач.
+59
View File
@@ -0,0 +1,59 @@
# Intents
## Domains
- `DOCS`
- `GENERAL`
- `CODE` - временно отключен
## GENERAL
### Intent `GENERAL_QA`
Общий интент для вопросов без точного маршрута.
В дальнейшем может использоваться как fallback.
Subintents:
- `SUMMARY` - ответы на общие вопросы по SUMMARY
## DOCS
### Intent `ARCHITECTURE`
Обработка вопросов по архитектуре.
Subintents пока отсутствуют.
Интент запланирован, без реализации.
### Intent `DOC_EXPLAIN`
Объяснение по документации.
Subintents:
- `SUMMARY` - краткое объяснение темы по SUMMARY-блокам документации
- `FIND_FILES` - поиск файлов с релевантной информацией
- `EXPLAIN_API` - объяснение работы метода
- `COMPONENT_INTEGRATIONS` - перечень интеграций компонента, API, UI, сущности, внешних систем
- `ENTITY_INTEGRATIONS` - перечень интеграций сущности
В текущем узком MVP реально реализованы только:
- `SUMMARY`
- `FIND_FILES`
Для запросов по интеграциям целевым retrieval-слоем является `D6_INTEGRATION_INDEX`.
### Intent `OPENAPI_GENERATION`
Генерация OpenAPI-спеки.
Subintents:
- `FULL_SPEC` - создание полной спецификации
### Intent `DOC_GENERATION`
Редактирование документации.
Subintents:
- `FROM_FEATURE` - создание документации из системной аналитики на фичу
+345
View File
@@ -0,0 +1,345 @@
# RAG
## Состояние as is
RAG сейчас используется как общее ядро индексации и retrieval по коду и документации.
Основной storage - `rag_session` и многослойный индекс в БД.
## Основные части
- `RagService` - фасад индексации и retrieval
- `DocsIndexingPipeline` - индексация документации
- `CodeIndexingPipeline` - индексация кода
- `RagRepository` - persistence и retrieval
- `IntentRouterV2` - планирование retrieval: слои, фильтры, ограничения
- `RuntimeRetrievalAdapter` - выполнение retrieval в runtime
## Индексация
Индексация идет по двум направлениям:
- `DOCS`
- `CODE`
На вход подается snapshot или changes.
Для каждого файла выбирается подходящий pipeline.
На выходе формируются документы по слоям и сохраняются в RAG-хранилище.
## Структура БД
Все слои сохраняются в общую таблицу `rag_chunks`.
### Общие поля по слоям
| Поле БД | Назначение |
|---|---|
| `rag_session_id` | идентификатор сессии индексации |
| `path` | путь исходного файла |
| `content` | основной текст записи для retrieval |
| `layer` | идентификатор слоя |
| `title` | короткий заголовок записи |
| `lang` | язык исходного содержимого, в основном для code-слоев |
| `repo_id` | идентификатор репозитория или проекта |
| `commit_sha` | версия кода или документов на момент индексации |
| `span_start`, `span_end` | диапазон строк в исходном файле, если он есть |
| `embedding` | векторное представление записи |
| `metadata_json` | структурированные атрибуты конкретного слоя |
### Поля со смыслом слоя
Смысл конкретного слоя хранится в `metadata_json`.
Именно эти атрибуты определяют, какой объект был извлечен и как его интерпретировать в retrieval.
Домены и поддомены должны храниться в `metadata_json` явно.
## Слои DOCS
### `D0_DOC_CHUNKS`
Задача:
Хранит текстовые фрагменты документации для retrieval по содержимому разделов.
Формирование:
Документ сначала разбирается на frontmatter и body, затем body режется на секции через markdown chunker.
Для каждой секции создается отдельная запись слоя.
Нарезка идет по разделам документа.
Только в fallback-сценарии, когда markdown-структура не найдена, используется нарезка по фиксированным текстовым чанкам.
Фиксация в БД:
| Атрибут в `metadata_json` | Описание | Источник |
|---|---|---|
| `document_id` | идентификатор документа-источника | `frontmatter.id`, иначе путь файла |
| `type` | тип документа из frontmatter | `frontmatter.type` |
| `module` | модуль документа | `frontmatter.module` |
| `domain` | домен документа | `frontmatter.domain` |
| `subdomain` | поддомен документа | `frontmatter.subdomain` |
| `tags` | теги документа | `frontmatter.tags` |
| `section_path` | полный путь секции в иерархии заголовков | результат `MarkdownDocChunker` |
| `section_title` | заголовок текущей секции | результат `MarkdownDocChunker` |
| `order` | порядок секции внутри документа | результат `MarkdownDocChunker` |
| `doc_kind` | классификация документа, например `readme`, `spec`, `runbook` | `DocsClassifier.classify(path)` |
| `source_path` | исходный путь документа | путь файла |
| `artifact_type` | тип артефакта, здесь `DOCS` | константа builder |
Связанные классы:
`DocsIndexingPipeline`, `DocsContentParser`, `MarkdownDocChunker`, `DocsDocumentBuilder`
### `D1_DOCUMENT_CATALOG`
Задача:
Хранит карточку документа как точку входа в документ и его краткое описание.
Формирование:
Источник данных - frontmatter `as is`, summary и doc kind, вычисленный классификатором документации.
В `metadata_json` копируются все `key-value` из frontmatter без нормализации и без fallback для frontmatter-атрибутов.
Дополнительно в `metadata_json` добавляются служебные поля `source_path`, `summary_text`, `doc_kind`.
Атрибут `document_id` добавляется только при наличии `frontmatter.id` (fallback до пути файла не применяется).
В `content` попадает summary документа, а не склейка всех частей документа в сплошной текст.
Фиксация в БД:
| Атрибут в `metadata_json` | Описание | Источник |
|---|---|---|
| `*` frontmatter fields | все поля frontmatter в исходном виде | frontmatter документа |
| `document_id` | идентификатор документа, добавляется только если в frontmatter есть `id` | `frontmatter.id` |
| `source_path` | исходный путь документа | путь файла |
| `summary_text` | краткое содержание документа | секция `# Summary` |
| `doc_kind` | классификация документа, например `readme`, `spec`, `runbook` | `DocsClassifier.classify(path)` |
Связанные классы:
`DocsIndexingPipeline`, `DocsFrontmatterParser`, `DocsClassifier`, `DocsContentParser`, `DocsDocumentBuilder`
### `D2_FACT_INDEX`
Задача:
Хранит атомарные факты в форме `subject-predicate-object` для точного retrieval по утверждениям.
Формирование:
Факты извлекаются из frontmatter и секций документа, после чего каждая найденная тройка превращается в отдельную запись.
Фиксация в БД:
| Атрибут в `metadata_json` | Описание | Источник |
|---|---|---|
| `fact_id` | идентификатор факта | вычисляется builder из содержимого факта и пути |
| `subject_id` | субъект факта | `DocsFactExtractor` |
| `predicate` | предикат или тип связи | `DocsFactExtractor` |
| `object` | значение или объект факта | `DocsFactExtractor` |
| `object_ref` | ссылка на объект, если она выделена отдельно | `DocsFactExtractor` |
| `anchor` | место в документе, откуда взят факт | `DocsFactExtractor` |
| `tags` | теги факта | `DocsFactExtractor` |
| `source_path` | исходный путь документа | путь файла |
Связанные классы:
`DocsIndexingPipeline`, `DocsFactExtractor`, `DocsDocumentBuilder`
### `D3_ENTITY_CATALOG`
Задача:
Хранит сущности, найденные в документации, чтобы искать документы и связи вокруг конкретной сущности.
Формирование:
Сущности извлекаются из frontmatter документа, после чего каждая сущность сохраняется отдельной записью.
Фиксация в БД:
| Атрибут в `metadata_json` | Описание | Источник |
|---|---|---|
| `entity_name` | имя сущности | `DocsEntityExtractor` |
| `document_id` | идентификатор документа, где найдена сущность | `frontmatter.id`, иначе путь файла |
| `document_type` | тип документа-источника | `frontmatter.type` |
| `module` | модуль документа | `frontmatter.module` |
| `domain` | домен документа | `frontmatter.domain` |
| `subdomain` | поддомен документа | `frontmatter.subdomain` |
| `tags` | теги документа или сущности | `frontmatter.tags` |
| `source_path` | исходный путь документа | путь файла |
Связанные классы:
`DocsIndexingPipeline`, `DocsEntityExtractor`, `DocsDocumentBuilder`
### `D4_WORKFLOW_INDEX`
Задача:
Хранит workflow и сценарии из документации для ответов про flow, шаги и жизненный цикл процесса.
Формирование:
Workflow извлекаются из detail sections документа и сохраняются как отдельные сценарии.
Извлечение идет из структуры `Details -> ## Сценарий`.
Фиксация в БД:
| Атрибут в `metadata_json` | Описание | Источник |
|---|---|---|
| `workflow_id` | идентификатор сценария | вычисляется builder из названия, anchor и документа |
| `document_id` | идентификатор документа-источника | `frontmatter.id`, иначе путь файла |
| `workflow_name` | название сценария | блок `Details -> ## Сценарий -> **Название**` |
| `preconditions` | предусловия сценария | блок `Details -> ## Сценарий -> **Предусловия**` |
| `trigger` | триггер или событие запуска | блок `Details -> ## Сценарий -> **Триггер**` |
| `main_flow` | основной сценарий | блок `Details -> ## Сценарий -> **Основной сценарий**` |
| `alternative_flow` | альтернативные ветки | блок `Details -> ## Сценарий -> **Альтернативный сценарий**` |
| `error_handling` | обработка ошибок | блок `Details -> ## Сценарий -> **Обработка ошибок**` |
| `postconditions` | постусловия | блок `Details -> ## Сценарий -> **Постусловие**` |
| `source_path` | исходный путь документа | путь файла |
Связанные классы:
`DocsIndexingPipeline`, `DocsWorkflowExtractor`, `DocsDocumentBuilder`
### `D5_RELATION_GRAPH`
Задача:
Хранит связи между документами и сущностями документации для navigation и related docs retrieval.
Формирование:
Связи извлекаются из frontmatter и сохраняются как отдельные relation edges.
Фиксация в БД:
| Атрибут в `metadata_json` | Описание | Источник |
|---|---|---|
| `relation_id` | идентификатор связи | вычисляется builder из source, target, relation type и anchor |
| `source_id` | источник связи | `frontmatter.id` или source документа в extractor |
| `relation_type` | тип связи | `DocsRelationExtractor` |
| `target_id` | целевой объект связи | `DocsRelationExtractor` |
| `anchor` | место в документе, где обнаружена связь | `DocsRelationExtractor` |
| `source_path` | исходный путь документа | путь файла |
Связанные классы:
`DocsIndexingPipeline`, `DocsRelationExtractor`, `DocsDocumentBuilder`
### `D6_INTEGRATION_INDEX`
Задача:
Хранит прикладные интеграции компонента, API, UI, сущности или внешней системы.
Используется для ответов на вопросы вида "какие интеграции есть у компонента".
Формирование:
Интеграции извлекаются из блока `## Integrations` документа.
Одна интеграция должна превращаться в отдельную запись слоя.
Описание интеграции может быть развернутым, а структурированные атрибуты должны выделяться в словарь.
Фиксация в БД:
| Атрибут в `metadata_json` | Описание | Источник |
|---|---|---|
| `integration_id` | идентификатор интеграции | вычисляется builder из source, target и anchor |
| `source_id` | идентификатор объекта, для которого описана интеграция | `frontmatter.id` документа-источника |
| `source_type` | тип исходного объекта | `frontmatter.doc_type` |
| `target` | целевой объект интеграции | блок `## Integrations` |
| `target_type` | тип целевого объекта, например `api`, `ui`, `entity`, `service`, `external_system` | блок `## Integrations` |
| `direction` | направление интеграции | блок `## Integrations` |
| `interaction` | тип взаимодействия | блок `## Integrations` |
| `via` | технический канал интеграции | блок `## Integrations` |
| `purpose` | назначение интеграции | блок `## Integrations` |
| `details` | дополнительные атрибуты интеграции в виде словаря | блок `## Integrations` |
| `domain` | домен документа | `frontmatter.domain` |
| `subdomain` | поддомен документа | `frontmatter.subdomain` |
| `source_path` | исходный путь документа | путь файла |
| `anchor` | место в документе, где описана интеграция | блок `## Integrations` |
Связанные классы:
`DocsIndexingPipeline`, `DocsIntegrationExtractor`, `DocsDocumentBuilder`
## Слои CODE
### `C0_SOURCE_CHUNKS`
Задача:
Хранит фрагменты исходного кода как базовый слой для цитирования, explain и точечной догрузки кода.
Формирование:
Исходный файл режется на кодовые чанки, и для каждого чанка создается отдельная запись.
Фиксация в БД:
| Атрибут в `metadata_json` | Описание | Источник |
|---|---|---|
| `chunk_index` | порядковый номер чанка в файле | индекс чанка при нарезке |
| `chunk_type` | тип чанка, например функция, класс или текстовый блок | `CodeTextChunker` |
| `module_or_unit` | модуль, к которому относится chunk | вычисляется из пути файла |
| `is_test` | признак тестового файла | `is_test_path(path)` |
Связанные классы:
`CodeIndexingPipeline`, `CodeTextChunker`, `CodeTextDocumentBuilder`
### `C1_SYMBOL_CATALOG`
Задача:
Хранит символы кода: классы, функции и методы. Используется для поиска по именам и структуре кода.
Формирование:
Символы извлекаются `SymbolExtractor`, и каждый символ сохраняется как отдельная запись.
Фиксация в БД:
| Атрибут в `metadata_json` | Описание | Источник |
|---|---|---|
| `symbol_id` | идентификатор символа | `SymbolExtractor` |
| `qname` | полное квалифицированное имя | `SymbolExtractor` |
| `kind` | тип символа: класс, функция, метод | `SymbolExtractor` |
| `signature` | сигнатура символа | `SymbolExtractor` |
| `parent_symbol_id` | родительский символ | `SymbolExtractor` |
| `package_or_module` | модуль или пакет символа | вычисляется из пути файла |
| `is_test` | признак тестового файла | `is_test_path(path)` |
Связанные классы:
`CodeIndexingPipeline`, `PythonAstParser`, `SymbolExtractor`, `SymbolDocumentBuilder`
### `C2_DEPENDENCY_GRAPH`
Задача:
Хранит связи между символами кода: вызовы, импорты, наследование. Используется для анализа зависимостей и flow.
Формирование:
Связи строятся `EdgeExtractor` по AST и списку символов, после чего каждая связь сохраняется отдельной записью.
Фиксация в БД:
| Атрибут в `metadata_json` | Описание | Источник |
|---|---|---|
| `edge_id` | идентификатор связи | `EdgeExtractor` |
| `edge_type` | тип связи: вызов, импорт, наследование | `EdgeExtractor` |
| `src_symbol_id` | исходный символ | `EdgeExtractor` |
| `src_qname` | полное имя исходного символа | `EdgeExtractor` |
| `dst_symbol_id` | целевой символ, если он разрешен | `EdgeExtractor` |
| `dst_ref` | текстовая ссылка на целевой символ | `EdgeExtractor` |
| `resolution` | статус разрешения связи | `EdgeExtractor` |
| `is_test` | признак тестового файла | `is_test_path(path)` |
Связанные классы:
`CodeIndexingPipeline`, `EdgeExtractor`, `EdgeDocumentBuilder`
### `C3_ENTRYPOINTS`
Задача:
Хранит точки входа приложения: HTTP routes, CLI commands и другие entrypoints.
Формирование:
Детекторы ищут HTTP и CLI точки входа по символам файла, после чего каждый найденный entrypoint сохраняется отдельной записью.
Фиксация в БД:
| Атрибут в `metadata_json` | Описание | Источник |
|---|---|---|
| `entry_id` | идентификатор точки входа | detector entrypoint model |
| `entry_type` | тип точки входа | detector entrypoint model |
| `framework` | framework, в котором найдена точка входа | detector entrypoint model |
| `route_or_command` | route или команда | detector entrypoint model |
| `handler_symbol_id` | идентификатор обработчика | detector entrypoint model |
| `handler_symbol` | имя обработчика | detector entrypoint model |
| `declaring_symbol` | символ, в котором объявлен entrypoint | detector entrypoint model |
| `entrypoint_kind` | вид точки входа | detector entrypoint model |
| `http_method` | HTTP-метод | detector entrypoint model |
| `route_path` | путь маршрута | detector entrypoint model |
| `decorator_text` | текст декоратора или объявления | detector entrypoint model |
| `summary_text` | краткое описание точки входа | detector entrypoint model |
| `is_test` | признак тестового файла | `is_test_path(path)` |
| `lang_payload` | дополнительные данные детектора | detector metadata |
| `artifact_type` | тип артефакта, здесь `CODE` | константа builder |
Связанные классы:
`CodeIndexingPipeline`, `EntrypointDetectorRegistry`, `FastApiEntrypointDetector`, `FlaskEntrypointDetector`, `TyperClickEntrypointDetector`, `EntrypointDocumentBuilder`
### `C4_SEMANTIC_ROLES`
Задача:
Слой объявлен в enum и retrieval-планах как слой семантических ролей кода.
Формирование:
Слой формируется на основе символов, связей, dataflow slices и execution traces.
В текущем runtime этот слой не используется как активный маршрут, так как домен `CODE` отключен.
Фиксация в БД:
Смысловые атрибуты слоя сохраняются в `rag_chunks.metadata_json`.
Точное краткое описание состава этих атрибутов в текущем документе пока не зафиксировано.
Связанные классы:
`CodeIndexingPipeline`, `SemanticRoleBuilder`, `SemanticRoleDocumentBuilder`
@@ -0,0 +1,289 @@
## 1. Формат ведения технической документации агентом
## 1.1. Общие принципы
Техническая документация, формируемая агентом, должна строиться как система атомарных, не пересекающихся по смыслу документов, связанных между собой явными ссылками.
Ключевые принципы:
- один документ описывает одну сущность или один устойчивый технический аспект;
- документ не должен дублировать соседние документы;
- общая система знаний должна собираться через ссылки, а не через копипасту;
- структура документации должна быть пригодна как для чтения человеком, так и для индексирования в RAG.
## 1.2. Требования к заголовкам
- Заголовок должен отражать только суть раздела.
- Заголовок не должен содержать метаданные (`id`, `doc_type`, `application`, `platform`, `domain`, `sub_domain`).
- Метаданные указываются отдельными строками в теле раздела или в YAML frontmatter.
Пример:
- правильно: `## 6.2 Метод UFS получения списка заказов`
- неправильно: `## 6.2 Блок api_method (id=..., platform=ufs)`
## 1.3. Базовые типы документных единиц
Базовые типы:
- `ui_page`
- `api_method`
- `logic_block`
Дополнительно могут использоваться:
- `architecture_overview`
- `integration_doc`
- `domain_entity`
- `glossary_item`
- `index_page`
## 1.4. Принцип декомпозиции страниц / файлов
### Один устойчивый объект - один документ
Если объект можно переиспользовать или на него могут ссылаться другие документы, его нужно выносить в отдельный файл.
### Документы не должны пересекаться по смыслу
Если описание повторяется в нескольких местах, нужно выделять общий документ и ссылаться на него.
### Use case и детали живут раздельно
Сценарий описывает поток работы, а детали выносятся в функциональные требования, отдельные блоки логики или контрактные описания.
## 1.5. Иерархическая организация документации
Документация должна быть организована как иерархическое дерево каталогов и файлов.
Пример:
```text
docs/
ui/
api/
logic/
domains/
integrations/
architecture/
glossary/
errors/
```
## 1.6. Учет связей между документами
Связи должны быть явными.
Примеры:
- UI-страница ссылается на вызываемые API;
- API-документ ссылается на используемые блоки логики;
- логический блок ссылается на интеграции;
- документ по коду ссылается на системную аналитику, инициировавшую изменения.
## 1.7. Формат markdown-документов
Каждый документ состоит из:
1. YAML frontmatter;
2. Markdown body.
## 1.8. YAML frontmatter
### Обязательные поля
- `id`
- `title`
- `doc_type`
- `status`
- `domain`
- `sub_domain`
- `related_docs`
### Рекомендуемые поля
- `owner`
- `entities`
- `tags`
- `feature`
- `system_analytics_refs`
- `source_of_truth`
- `related_code`
### Допустимые значения `doc_type`
- `ui_page`
- `api_method`
- `logic_block`
- `architecture_overview`
- `integration_doc`
- `domain_entity`
- `glossary_item`
- `index_page`
### Допустимые значения `status`
- `draft`
- `in_review`
- `approved`
- `outdated`
- `generated`
- `active`
## 1.9. Синхронизация с системной аналитикой
Техническая документация строится на основе системной аналитики (features).
Обязательно учитывать:
- концептуальный уровень аналитики;
- детализацию технической документации;
- согласованность терминов, ролей и интеграционных цепочек.
Если атрибуты или детали отсутствуют в аналитике:
- определить их из текста аналитики;
- дополнить данными из репозитория (код, контракты, существующие документы);
- зафиксировать итог в документации как явные метаданные и требования.
## 1.10. Формат body-разделов для блока изменений
Для секции изменений (по аналогии с разделом `6` в аналитике) использовать единый формат.
Под корнем секции изменений указывать общие атрибуты:
- `domain`
- `sub_domain`
Для каждого подраздела `X.Y` указывать метаданные строками сразу после заголовка:
- `id`
- `doc_type`
- `application`
- `platform`
## 1.11. Различие аналитики и документации
- Аналитика - концептуальный уровень, упрощенный use case.
- Документация - детальный инженерный уровень.
Для документации:
- технический use case должен быть детализированным;
- функциональные требования расширяют use case и описывают детали интеграций, логики и поведения;
- функциональные требования не должны копировать шаги сценария без добавления новой информации.
Источник правил:
- `src/app/core/agent/processes/v2/doc_rules_v2/common-elements/tech-use-case.md`
- `src/app/core/agent/processes/v2/doc_rules_v2/common-elements/fr.md`
## 1.12. Требования к `ui_page`
Обязательная структура:
- `### Технический use case`
- `### Требования к UI`
- `### Функциональные требования`
- `### Нефункциональные требования`
### Требования к UI
Внутри обязательно отдельно описывать каждую форму UI:
- табличное представление;
- пустой список (empty state);
- ошибка (error state).
Обязательные правила:
- если есть интеграция, обязательно описать показ ошибки;
- если показывается список, обязательно описать показ отсутствия данных.
### UI-элементы
UI-поля и элементы в документации описываются строго в таблицах.
Обязательные колонки (заполнять там, где применимо):
- `Код элемента`
- `Название и описание`
- `Данные`
- `Поведение`
- `Валидация`
## 1.13. Пользовательская аналитика для `ui_page`
События пользовательской аналитики оформляются таблицей:
- `Название события`
- `Описание`
- `Точка вызова`
- `Payload`
## 1.14. Требования к `api_method`
Обязательная структура:
- `### Технический use case`
- `### Функциональные требования`
- `### Нефункциональные требования`
- `### Контракт`
### Технический use case
Оформляется детально по правилам `tech-use-case.md`.
Обязательные части:
- название
- предусловия
- триггер
- основной сценарий
- альтернативный сценарий
- обработка ошибок
- постусловие
### Функциональные требования
Оформляются по правилам `fr.md`:
- формат `FR.<номер>. <Название>`;
- FR расширяют use case;
- FR не дублируют шаги сценария без дополнительной ценности;
- для интеграционных шагов FR обязательны.
## 1.15. Нефункциональные требования для `api_method`
Разделять на подразделы:
- `#### Аудит` (если применимо)
- `#### Мониторинг`
### Мониторинг
Оформлять таблицей:
- `Метрика`
- `Описание`
- `Условие срабатывания`
Правила:
- в условиях указывать, при каких состояниях фиксируется событие;
- не использовать формулировку вида «точка измерения = метод»;
- базово закладывать метрики:
- `<METRIC_NAME>_SUCCESS`
- `<METRIC_NAME>_FAIL`
- `<METRIC_NAME>_BUSINESS_ERROR`
## 1.16. Распределение ответственности по слоям
- Проверка ролевой модели пользователя обычно выполняется в `ufs`.
- Для `pprb` аудит может не фиксироваться, если это согласовано правилами домена.
- Если проверка ролей вынесена в `ufs`, не дублировать этот шаг в use case `pprb`.
## 1.17. Контракты API
Контракт может быть:
- в markdown-таблицах;
- в OpenAPI;
- в отдельном контрактном файле.
Для markdown-контракта минимум:
- endpoint/method;
- request fields;
- required/optional;
- constraints;
- response;
- errors;
- auth;
- retry;
- timeout;
- idempotency.
## 1.18. Integrations-блок
Если у документа есть интеграции, выделять отдельный `## Integrations`.
Рекомендуемые атрибуты интеграции:
- `target`
- `target_type`
- `direction`
- `interaction`
- `via`
- `purpose`
- `details`
## 1.19. Общие требования к markdown body
- В документе должен быть один `H1`, совпадающий с `title`.
- Основные разделы - `H2`, подразделы - `H3`.
- Не допускать хаотичной вложенности заголовков.
- Вместо дублирования использовать ссылки на связанные документы.
- Сценарии, правила, ограничения и кодовые привязки держать раздельно.
@@ -0,0 +1,212 @@
# Системная аналитика
## Общее описание
Документ описывает изменения в автоматизированной системе. Пишется системными аналитиками для разработчиков и тестировщиков и проходит согласование с экспертами по архитектуре, безопасности и сопровождению.
Документ может описывать как новый процесс, так и инкремент доработки существующей функциональности.
## Требования к заголовкам
- Заголовок должен отражать суть раздела.
- Заголовок не должен содержать лишнюю информацию, которая относится к метаданным (id, doc_type, platform, application и т.д.).
- Метаданные указываются отдельными строками в теле раздела.
## Состав документа
Каждый раздел верхнего уровня оформляется заголовком уровня `#`.
### 1. Цели
- Коротко описать, какую проблему и для кого решаем.
- 1-2 предложения.
- Не дублировать критерии приемки.
### 2. Процесс AS IS и TO BE
- Фокус на пользовательских и бизнес-изменениях.
- Не указывать технические детали (платформы, API, внутренние интеграции).
### 3. Ограничения
- Ограничения и допущения в техническом и бизнесовом плане.
### 4. Критерии приемки
- Описывать с точки зрения пользователя.
- Не добавлять технические детали (платформы, API, внутренние компоненты).
### 5. Архитектура
Нужно указать:
- схему контейнеров,
- таблицу интеграций,
- сквозные интеграционные сценарии.
Слои:
- `ui` - web-приложение, клиент.
- `ufs` - BFF: аутентификация/авторизация, агрегация и маппинг данных.
- `pprb` - backend: API, БД, логика жизненного цикла сущностей.
#### Диаграмма
Mermaid-диаграмма должна содержать:
- основные контейнеры,
- названия приложений и платформ,
- интеграции между приложениями,
- названия вызываемых endpoint или топиков.
#### Таблица интеграций
Обязательные колонки:
- Код
- Название endpoint/топика
- Источник данных
- Потребитель данных
- Инициатор вызова
- Передаваемые данные
#### Сквозной интеграционный сценарий
- Нумерованный список вызовов вида: «Компонент 1 вызывает endpoint в Компонент 2».
- Только интеграционная цепочка, без детального разбора логики.
### 6. Описание изменений
Раздел состоит из подразделов уровня `##` (например, `6.1`, `6.2`, `6.3`).
Под корнем раздела `# 6` указываются общие метаданные:
- `domain`
- `sub_domain`
Для каждого раздела `6.x` обязательно указывать метаданные строками сразу после заголовка:
- `id`
- `doc_type`
- `application`
- `platform`
Дополнительные метаданные для случаев изменения существующей документации:
- `action`
- `target_doc_id`
- `target_path`
#### 6.x для `ui_page`
Обязательная структура:
- `### Технический use case (тезисно)`
- `### Требования к UI`
- `### Функциональные требования`
- `### Нефункциональные требования`
Требования к разделу `### Требования к UI`:
- Внутри нужно отдельно описывать каждую UI-форму.
- Если есть интеграция, обязательно описать, как показывается ошибка.
- Если показываем список, обязательно описать, как показывается отсутствие данных.
Рекомендуемая детализация UI-форм:
- табличное представление,
- пустой список (empty state),
- ошибка (error state).
Правила описания UI-полей:
- Поля описывать списком (не таблицей).
- Общие правила (например, read-only, поведение при пустом значении) выносить в общий блок, не дублировать для каждого поля.
Отдельно нужно различать два сценария описания:
1. Если описывается новая UI-страница или новая самостоятельная UI-форма, раздел оформляется полноценно по шаблону `ui_page`.
- Нужно дать достаточный контекст для разработки и тестирования.
- Нужно подробно описывать структуру формы, состояния отображения, поведение полей, ошибки, empty state и пользовательские действия.
2. Если описывается доработка уже существующей страницы или существующей UI-формы, не нужно повторно копировать полное описание из действующей документации.
- Нужно учитывать уже существующее описание страницы в документации и аналитике.
- В аналитике нужно явно указать, что именно меняется в существующем сценарии: что добавляется, редактируется или удаляется.
- Нужно указывать точку изменения: в какой существующей странице, форме, блоке или сценарии вносится изменение.
- Нужно ссылаться на существующий документ или раздел, где базовое поведение уже описано.
- Нужно описывать только delta изменений, достаточную для реализации доработки и актуализации документации.
- Полное описание существующей страницы в таком разделе не дублируется.
- Для такой доработки в metadata нужно явно указывать `action: update`.
- Если изменение должно попасть в уже существующий markdown-документ, нужно явно указывать `target_doc_id` и/или `target_path`.
- `target_doc_id` должен совпадать с `id` существующего документа, который требуется обновить.
- Если `target_doc_id`/`target_path` не указаны, агент может ошибочно интерпретировать раздел как создание нового документа.
Нефункциональные требования для `ui_page`:
- пользовательская аналитика оформляется таблицей с колонками:
- `Название события`
- `Описание`
- `Точка вызова`
- `Payload`
#### 6.x для `api_method`
Обязательная структура:
- `### Технический use case (тезисно)`
- `### Функциональные требования`
- `### Нефункциональные требования`
- `### Контракт метода`
Правило для функциональных требований:
- Если дополнительных требований нет (дублируют сценарий), писать: `Не выявлены`.
Нефункциональные требования:
- Разделять на подразделы:
- `#### Аудит` (если применимо)
- `#### Мониторинг`
Для `Мониторинг` использовать таблицу с колонками:
- `Метрика`
- `Описание`
- `Условие срабатывания`
Важно:
- В мониторинге описывать условия срабатывания, а не «точку измерения = метод».
- Базово закладывать 3 метрики:
- `<METRIC_NAME>_SUCCESS`
- `<METRIC_NAME>_FAIL`
- `<METRIC_NAME>_BUSINESS_ERROR`
Контракт метода:
- Для запроса: таблица параметров (`header/query/path`) с колонками: название, тип параметра, тип данных, обязательность, описание, пример.
- Для тела JSON (если есть): структура отдельной таблицей.
- Для ответа JSON: таблица с колонками: название, тип данных, обязательность, описание, заполнение, пример.
#### 6.x для `logic_block`
Обязательная структура:
- `### Технический use case (тезисно)`
- `### Функциональные требования`
- `### Нефункциональные требования`
`logic_block` удобно использовать для фиксации точечных изменений существующего сценария, если раздел не описывает новую самостоятельную страницу или новую самостоятельную форму, а только уточняет delta к уже существующей документации.
Если точечное изменение должно изменить существующий документ другого типа, `logic_block` для этого использовать нельзя. В этом случае metadata раздела должна указывать тип и идентификатор целевого существующего документа, который требуется обновить.
## Дополнительные правила по слоям
- Проверка ролевой модели пользователя обычно выполняется на уровне `ufs`.
- Для `pprb` аудит может не фиксироваться, если это правило принято для конкретной фичи/домена.
- Если проверка ролей вынесена в `ufs`, не дублировать этот шаг в сценарии `pprb`.
## Термины
- Аудит: события, которые фиксируют действия пользователя и позволяют ответить на вопрос «кто, что, когда сделал».
- Мониторинг: технические события/метрики для контроля стабильности и поиска сбоев.
+100
View File
@@ -0,0 +1,100 @@
# MVP: процесс v1
## 1. Общее описание
Запрос пользователя обрабатывается цепочкой API → рантайм агента → зарегистрированный процесс версии `v1` → один workflow из трёх последовательных шагов. Процесс **не** обращается к RAG и **не** маршрутизирует интенты: текст сообщения передаётся в LLM по фиксированному промпту. Ответ агента — результат генерации с лёгкой постобработкой (trim).
```mermaid
flowchart LR
subgraph api [API]
RS[RequestService]
end
subgraph runtime [Agent runtime]
AR[AgentRuntime]
PR[ProcessRunner]
end
subgraph v1 [Процесс v1]
P1[V1Process]
WG[V1FlowMainGraph]
end
subgraph wf [Workflow v1.flow_main]
S1[PrepareUserMessageStep]
S2[GenerateAnswerStep]
S3[FinalizeAnswerStep]
end
LLM[AgentLlmService]
RS --> AR
AR --> PR
PR --> P1
P1 --> WG
WG --> S1 --> S2 --> S3
S2 --> LLM
```
Клиент создаёт запрос с `process_version: v1`. `AgentRuntime` поднимает `RuntimeExecutionContext` (запрос, сессия, publisher, trace), выбирает `V1Process` из реестра и вызывает `run`. `V1Process` собирает `V1FlowContext` и прогоняет линейный граф: подготовка текста, один вызов LLM, финализация строки ответа. Итог попадает в `ProcessResult.answer` и дальше в ответ пользователю.
---
## 2. Шаги и контракты
### 2.1. Вход в процесс: `V1Process.run`
| | |
|--|--|
| **Название** | Запуск процесса v1 |
| **Задача** | Собрать контекст workflow и выполнить граф до готового ответа. |
| **Вход** | `RuntimeExecutionContext`: `request` (в т.ч. `message`), `session`, `publisher`, `trace`. |
| **Выход** | `ProcessResult` с полем `answer: str`. |
| **Как работает** | Создаётся `V1FlowContext` с `prompt_name` по умолчанию `v1_flow_main.answer`. Вызывается `V1FlowMainGraph.run`. Возвращается ответ из контекста workflow. |
Код: `src/app/core/agent/processes/v1/process.py`.
---
### 2.2. Шаг workflow: `PrepareUserMessageStep`
| | |
|--|--|
| **Название** | Подготовка сообщения пользователя |
| **Задача** | Сформировать строку, которая уйдёт в LLM как пользовательский ввод. |
| **Вход** | `V1FlowContext` с заполненным `runtime` и `prompt_name`. |
| **Выход** | Тот же контекст с `prepared_message: str`. |
| **Как работает** | Берётся `context.runtime.request.message` и обрезаются пробелы по краям (`strip`). Результат пишется в `prepared_message`. Других преобразований нет. |
Код: `src/app/core/agent/processes/v1/workflow/flow_main/steps/prepare_user_message_step.py`.
---
### 2.3. Шаг workflow: `GenerateAnswerStep`
| | |
|--|--|
| **Название** | Вызов LLM |
| **Задача** | Сгенерировать ответ по выбранному промпту и подготовленному сообщению. |
| **Вход** | `V1FlowContext` с `prepared_message`, `prompt_name`, `runtime.trace` для модуля LLM. |
| **Выход** | Контекст с `answer: str` (сырой ответ модели). |
| **Как работает** | Асинхронно в пуле потоков вызывается `AgentLlmService.generate(prompt_name, prepared_message, ...)`. В trace подключается модуль `workflow.v1.llm`. Идентификатор запроса передаётся в `log_context` для логов. |
Код: `src/app/core/agent/processes/v1/workflow/flow_main/steps/generate_answer_step.py`.
---
### 2.4. Шаг workflow: `FinalizeAnswerStep`
| | |
|--|--|
| **Название** | Финализация ответа |
| **Задача** | Нормализовать строку ответа перед выдачей пользователю. |
| **Вход** | `V1FlowContext` с заполненным `answer` после LLM. |
| **Выход** | Контекст с обновлённым `answer`. |
| **Как работает** | К ответу применяется `strip()` по краям. Другой логики нет. |
Код: `src/app/core/agent/processes/v1/workflow/flow_main/steps/finalize_answer_step.py`.
---
### 2.5. Транспорт: `WorkflowGraph` (v1)
Граф для v1 использует стандартный `WorkflowGraph`: на каждом шаге пишутся события `workflow_started`, `step_started`, `step_completed`, `workflow_completed` в `runtime_traces` через `context.runtime.trace`.
Код: `src/app/core/agent/utils/workflow/graph.py`, обёртка `V1FlowMainGraph` в `src/app/core/agent/processes/v1/workflow/flow_main/graph.py`.
+33
View File
@@ -0,0 +1,33 @@
Нужно реализовать 2 вещи
Создать процесс внесения изменений в файл документации
Создать контекст этого процесса
Контекст наполнять атрибутами
что-то явно задано, фоллбэк через ллм
Написать тестовую аналитику - круд над сущностью
фронт, ефс, ппрб
Все в своей БД
Атрибуты сущности задать в требованиях
Аналитика имеет структуру
Внутри модули - один модуль на правку одного файла.
Модуль извлекается из аналитики парсером и из него формируется задача на редактирование файла
если парсер не сработал - фоллбэк ан ллм
Процесс редактирования работает стандартно
+220
View File
@@ -0,0 +1,220 @@
# MVP: процесс v2
## 1. Общее описание
Процесс v2 в текущем MVP ориентирован в первую очередь на **документацию проекта**, но роутер также поддерживает `GENERAL / GENERAL_QA / SUMMARY` для общих обзорных вопросов. Для документных веток нужна активная RAG-сессия с проиндексированными документами.
Это **узкий MVP**, а не полная target architecture. Поддерживаются три маршрута:
- `GENERAL`
- `GENERAL_QA`
- `SUMMARY`
- `DOCS`
- `DOC_EXPLAIN`
- `SUMMARY`
- `FIND_FILES`
Запрос проходит следующие смысловые этапы:
1. проверка готовности сессии;
2. intent routing;
3. формирование retrieval-параметров;
4. retrieval из `DOCS RAG`;
5. минимальная сборка evidence;
6. запуск task-focused workflow нужной ветки;
7. формирование ответа.
```mermaid
flowchart TB
subgraph api [API]
RS[RequestService]
end
subgraph runtime [Agent runtime]
AR[AgentRuntime]
PR[ProcessRunner]
end
subgraph v2 [Процесс v2]
P2[V2Process]
IR[V2IntentRouter]
POL[V2RetrievalPolicyResolver]
AD[V2RagRetrievalAdapter]
RSR[RagSessionRetriever]
ASM[DocsEvidenceAssembler]
end
subgraph rag [Пакет rag]
RR[RagRepository]
end
subgraph wf [Workflow]
SUM[DocsExplainSummaryGraph]
FF[DocsExplainFindFilesGraph]
end
LLM[AgentLlmService]
RS --> AR --> PR --> P2
P2 --> IR --> POL --> AD --> RSR --> RR
AD --> ASM
ASM --> SUM
ASM --> FF
SUM --> LLM
```
Клиент указывает `process_version: v2`. Без `active_rag_session_id` в сессии процесс возвращает сообщение об ошибке. Иначе выполняется цепочка:
маршрутизация → `RetrievalPlan` → retrieval строк из `DOCS RAG` → минимальная сборка evidence → ветвление по `subintent` → запуск workflow.
### Реализованные домены, интенты и сабинтенты
В коде заданы константы `V2Domain`, `V2Intent`, `V2Subintent`. Сейчас процесс intentionally ограничен одной рабочей областью.
| Уровень | Значение (строка) | Реализация |
|--------|-------------------|------------|
| **Домен (routing_domain)** | `DOCS` | Единственный поддерживаемый домен: документация проекта. |
| **Интент** | `DOC_EXPLAIN` | Единственный интент: объяснение по документации. |
| **Сабинтент** | `SUMMARY` | Объяснение темы по SUMMARY-блокам документации. |
| **Сабинтент** | `FIND_FILES` | Поиск путей к документам, где описана нужная сущность или тема. |
Итого в текущем MVP реализована **одна** рабочая тройка домен×интент: `DOCS` + `DOC_EXPLAIN`, с **двумя** ветками по сабинтенту.
---
## 2. Этапы вне workflow (внутри `V2Process.run`)
### 2.1. `V2IntentRouter.route`
| | |
|--|--|
| **Название** | Маршрутизация запроса (v2) |
| **Задача** | Определить домен, интент, subintent и извлечь якоря из текста. |
| **Вход** | `user_query: str` (текст сообщения пользователя). |
| **Выход** | `V2RouteResult`: `routing_domain`, `intent`, `subintent`, `user_query`, `normalized_query`, `target_terms`, `anchors` (`V2RouteAnchors`), `confidence`. |
| **Как работает** | Router реализован по схеме **LLM-first**: `normalization``target_terms`/`anchors extraction``LLM router``deterministic validator``fallback`. LLM является **основным селектором маршрута**. Deterministic-слой больше не выбирает маршрут по умолчанию: он отвечает только за extraction, валидацию enum/комбинаций и fallback при сломанном или невалидном ответе LLM. В trace пишется событие `intent_routed`. |
Код: `src/app/core/agent/processes/v2/intent_router/router.py`, `modules/normalizer.py`, `modules/target_terms.py`, `modules/anchors.py`, `routers/llm.py`, `routers/validator.py`, `routers/fallback.py`.
---
### 2.2. `V2RetrievalPolicyResolver.resolve`
| | |
|--|--|
| **Название** | Политика retrieval для v2 |
| **Задача** | По результату роутинга выбрать профиль, список слоёв RAG и лимит строк выдачи. |
| **Вход** | `V2RouteResult`. |
| **Выход** | `RetrievalPlan`: `profile`, `layers`, `limit`, опционально `filters`. |
| **Как работает** | Это отдельный смысловой шаг между routing и retrieval. Он не ходит в БД и не извлекает данные, а только подготавливает параметры поиска. Для `FIND_FILES` выбирается один профиль слоёв и лимит, для `SUMMARY` — другой. Лог: `retrieval_plan_resolved`. |
Код: `src/app/core/agent/processes/v2/retrieval/policy_resolver.py`.
---
### 2.3. `V2RagRetrievalAdapter` → `RagSessionRetriever.retrieve`
| | |
|--|--|
| **Название** | Загрузка сырых строк из RAG по плану |
| **Задача** | Делегировать поиск в единственную реализацию retrieval в пакете `rag`. |
| **Вход** | `rag_session_id`, `query_text` (нормализованный запрос), `RetrievalPlan`. |
| **Выход** | `list[dict]` — строки чанков в формате `RagRepository.retrieve` (поля `path`, `layer`, `metadata`, и т.д.). |
| **Как работает** | Выполняется retrieval по уже сформированному плану: профиль, список слоёв и лимит. На этом шаге происходит только извлечение сырых строк из `DOCS RAG`. Лог: `rag_rows_fetched`. |
Код адаптера: `src/app/core/agent/processes/v2/retrieval/v2_rag_adapter.py`.
Код API: `src/app/core/rag/retrieval/session_retriever.py`.
---
### 2.4. `DocsEvidenceAssembler`
| | |
|--|--|
| **Название** | Сборка evidence для задачи |
| **Задача** | Превратить сырые строки retrieval в списки summary или кандидатов файлов с дедупом и скорингом. |
| **Вход** | Список строк `rows`, `V2RouteResult` (для `target_terms`). |
| **Выход** | `list[RetrievedSummary]` или `list[RetrievedFile]`. |
| **Как работает** | Это **минимальная evidence-проверка**, достаточная для MVP. Для `SUMMARY` отбрасываются записи без summary-текста и summary-like секции, затем применяется дедуп и простой скоринг по терминам. Для `FIND_FILES` остаются только релевантные пути документов, также с дедупом и простым скорингом. Здесь нет сложной многоступенчатой валидации: задача шага — отфильтровать очевидный шум и передать в workflow компактное evidence. Лог: `evidence_assembled`. |
Код: `src/app/core/agent/processes/v2/evidence/assembler.py`.
---
## 3. Шаги workflow
Текущие workflow являются **task-focused**: каждая ветка решает одну узкую прикладную задачу и не содержит общей универсальной логики для всех типов вопросов.
### 3.1. Ветка `SUMMARY`: `GenerateSummaryAnswerStep`
| | |
|--|--|
| **Название** | Сборка ответа по summary |
| **Задача** | Сформировать ответ пользователю по найденным SUMMARY-блокам или сообщить об отсутствии. |
| **Вход** | `DocsExplainSummaryContext`: `runtime`, `route`, `rag_session_id`, `prompt_name`, `documents` (список `RetrievedSummary`). |
| **Выход** | Контекст с `answer: str`, `prompt_input` при успешном вызове LLM. |
| **Как работает** | Workflow получает уже отобранные summary-документы. Если документов нет — возвращает честный fallback-ответ. Иначе собирает prompt input из запроса пользователя и найденных summary-блоков и вызывает LLM. Workflow не занимается retrieval и не строит retrieval-план: он решает только задачу генерации ответа по уже подготовленному evidence. |
Код: `src/app/core/agent/processes/v2/workflows/docs_explain_summary/steps/generate_summary_answer_step.py`.
Граф: `DocsExplainSummaryGraph` (`V2WorkflowGraph`).
---
### 3.2. Ветка `FIND_FILES`: `FinalizeFindFilesAnswerStep`
| | |
|--|--|
| **Название** | Сборка списка файлов |
| **Задача** | Вывести пользователю markdown-список путей к файлам документации. |
| **Вход** | `DocsExplainFindFilesContext`: `runtime`, `route`, `rag_session_id`, `files` (`RetrievedFile`). |
| **Выход** | Контекст с `answer: str`. |
| **Как работает** | Workflow получает уже собранный список файлов и формирует финальный ответ. Если файлов нет — возвращает fallback. Если файлы есть — отдает детерминированный список путей. Эта ветка intentionally не использует LLM, потому что задача сводится к выдаче путей, а не к генерации объяснения. |
Код: `src/app/core/agent/processes/v2/workflows/docs_explain_find_files/steps/finalize_find_files_answer_step.py`.
Граф: `DocsExplainFindFilesGraph` (`V2WorkflowGraph`).
---
### 3.3. Транспорт: `V2WorkflowGraph`
| | |
|--|--|
| **Название** | Workflow v2 с буфером trace |
| **Задача** | Выполнить шаги без пошаговых `step_started`/`step_completed` в trace; один раз сбросить сводку. |
| **Вход** | Контекст workflow (`DocsExplainSummaryContext` или `DocsExplainFindFilesContext`). |
| **Выход** | Обновлённый контекст. |
| **Как работает** | Для каждого шага: `trace_input` до `run`, затем `run`, затем `trace_output`; записи копятся в список. В trace уходят `workflow_started`, затем `workflow_trace_flushed` с массивом шагов, затем `workflow_completed`. Статусы пользователю публикуются через `publisher` как и раньше. |
Код: `src/app/core/agent/processes/v2/workflows/v2_workflow_graph.py`.
---
## 4. Сборка в приложении
В `ModularApplication` создаются `RagSessionRetriever`, `V2RagRetrievalAdapter`, `V2RetrievalPolicyResolver`, `DocsEvidenceAssembler` и передаются в `V2Process` (см. `src/app/core/application.py`).
---
## 5. Итоговая концептуальная схема текущего MVP
В концептуальном виде текущий `v2` работает так:
1. **Session check**
Проверка, что есть активная RAG-сессия проекта.
2. **LLM-first intent routing**
Нормализация, extraction (`target_terms`, `anchors`), затем основной выбор маршрута через LLM.
3. **Deterministic validation + fallback**
Проверка enum/комбинации маршрута и fallback только если LLM не ответил или вернул невалидный маршрут.
4. **Retrieval parameter planning**
Формирование профиля поиска, слоёв и лимитов.
5. **RAG retrieval**
Загрузка сырых строк из `DOCS RAG`.
6. **Minimal evidence assembly**
Дедуп, базовый скоринг, отбор полезных summary или файлов.
7. **Task-focused workflow**
Узкая ветка `SUMMARY` или `FIND_FILES`.
8. **Final response**
Либо explanation через LLM, либо детерминированный список файлов.
Это и есть актуальная архитектура **узкого MVP**, синхронизированная с текущей реализацией.
@@ -0,0 +1,346 @@
# V2IntentRouter Architecture
## 1. Архитектура
Текущий `V2IntentRouter` реализован как **LLM-first router**.
Deterministic-слой не выбирает маршрут по умолчанию и используется только для:
- preprocessing
- validation ответа LLM
- fallback, если LLM не ответил или вернул невалидный маршрут
Актуальные компоненты:
- `router.py`
Главная точка входа и оркестратор пайплайна.
- `modules/normalizer.py`
Нормализация текста запроса в `normalized_query`.
- `modules/target_terms.py`
Извлечение retrieval-oriented `target_terms`, `endpoint_paths`, `matched_aliases`, `alias_docs`.
- `modules/anchors.py`
Извлечение `anchors` и marker-сигналов для fallback и downstream retrieval.
- `routers/route_catalog.py`
Каталог допустимых маршрутов (`allowed_routes`).
- `routers/llm.py`
Основной LLM-router. Получает нормализованный запрос, `target_terms`, `anchors` и список допустимых маршрутов.
- `routers/validator.py`
Deterministic validator для enum-значений, комбинации маршрута и базовой нормализации `confidence`.
- `routers/confidence.py`
Пост-обработка confidence после ответа LLM.
- `routers/fallback.py`
Fallback-маршрутизация, если LLM не ответил или ответ не прошёл validator.
- `routers/prompts.yml`
Prompt-контракт для LLM-router.
## 2. Контракт
### Вход
- `user_query: str`
### Выход
`V2RouteResult`:
- `routing_domain: str`
- `intent: str`
- `subintent: str`
- `user_query: str`
- `normalized_query: str`
- `target_terms: list[str]`
- `anchors: V2RouteAnchors`
- `confidence: float`
- `routing_mode: str`
- `llm_router_used: bool`
- `reason_short: str`
`V2RouteAnchors`:
- `entity_names: list[str]`
- `file_names: list[str]`
- `endpoint_paths: list[str]`
- `target_doc_hints: list[str]`
- `matched_aliases: list[str]`
- `process_domain: str | None`
- `process_subdomain: str | None`
## 3. Поддерживаемые домены, интенты и сабинтенты
### Домены
- `DOCS`
- `GENERAL`
### Интенты
- `DOC_EXPLAIN`
- `GENERAL_QA`
### Сабинтенты
- `SUMMARY`
- `FIND_FILES`
### Допустимые маршруты
- `GENERAL / GENERAL_QA / SUMMARY`
- `DOCS / DOC_EXPLAIN / SUMMARY`
- `DOCS / DOC_EXPLAIN / FIND_FILES`
Эти маршруты централизованно заданы в `routers/route_catalog.py`.
## 4. Актуальный флоу
Пайплайн обработки запроса:
1. `router.py` принимает `user_query`.
2. `modules/normalizer.py` строит `normalized_query`.
3. `modules/target_terms.py` извлекает:
- `target_terms`
- `endpoint_paths`
- `matched_aliases`
- `alias_docs`
4. `modules/anchors.py` строит:
- `anchors`
- `file_markers`
- `architecture_markers`
- `logic_markers`
- `domain_markers`
- `endpoint_markers`
5. `router.py` собирает `QueryFeatures`.
6. `routers/llm.py` вызывается как **основной селектор маршрута**.
7. `routers/validator.py` проверяет:
- что значения входят в допустимые enum
- что комбинация маршрута разрешена
- что `confidence` можно привести к `float`
8. `routers/confidence.py` корректирует confidence на основе силы сигналов.
9. Если ответ LLM валиден, возвращается `V2RouteResult` с `routing_mode="llm_default"`.
10. Если LLM не ответил, вернул сломанный JSON или невалидный маршрут, `routers/fallback.py` строит fallback route:
- `FIND_FILES`, если есть `file_markers`
- `DOCS / DOC_EXPLAIN / SUMMARY`, если есть docs-oriented anchors
- иначе `GENERAL / GENERAL_QA / SUMMARY`
## 5. Компоненты по флоу
### `router.py`
- Задача
Оркестрировать полный routing pipeline.
- Как решает
Последовательно вызывает:
- normalizer
- target terms extractor
- anchor extractor
- LLM router
- validator
- confidence adjuster
- fallback router
- Вход
`user_query: str`
- Выход
`V2RouteResult`
### `modules/normalizer.py`
- Задача
Привести запрос к стабильной форме для анализа.
- Как решает
Схлопывает лишние пробелы через `" ".join(...split())`.
- Вход
`user_query: str`
- Выход
`normalized_query: str`
### `modules/target_terms.py`
- Задача
Построить **чистое retrieval-поле** `target_terms`.
- Как решает
Использует позитивную модель отбора и включает в `target_terms` только:
- endpoint paths
- identifier-like tokens
- alias canonical terms
- domain terms
Исключаются:
- question words
- intent words
- filler/noisy words
- marker words
- короткие токены `< 3`, если это не endpoint или alias
- битые path-like токены
Дополнительно:
- lowercase
- trim punctuation по краям
- dedupe
- ограничение до `7` элементов
- приоритет: endpoints → identifiers → aliases → domain terms
- Вход
`normalized_query: str`
- Выход
`TargetTermsAnalysis`:
- `target_terms`
- `endpoint_paths`
- `matched_aliases`
- `alias_docs`
### `modules/anchors.py`
- Задача
Построить `anchors` и marker-сигналы, не смешивая их с `target_terms`.
- Как решает
Извлекает:
- `entity_names` из PascalCase-like токенов
- `file_names` только по жёстким правилам:
- `*.md`, `*.yaml`, `*.yml`, `*.json`
- `docs/...`, `doc/...`, `documentation/...`
- `endpoint_paths` из `TargetTermsAnalysis`
- `target_doc_hints` из alias docs, endpoint map и marker-сигналов
Marker-сигналы живут отдельно:
- `file_markers`
- `architecture_markers`
- `logic_markers`
- `domain_markers`
- `endpoint_markers`
- Вход
- `normalized_query: str`
- `TargetTermsAnalysis`
- Выход
`AnchorAnalysis`
### `routers/route_catalog.py`
- Задача
Держать один источник истины для допустимых маршрутов.
- Как решает
Возвращает:
- список `allowed_routes` для payload LLM
- проверку допустимости комбинации `routing_domain + intent + subintent`
### `routers/llm.py`
- Задача
Выбрать маршрут через LLM как основной селектор.
- Как решает
Формирует JSON payload из:
- `normalized_query`
- `target_terms`
- `anchors`
- `allowed_routes`
Затем:
- вызывает LLM
- парсит JSON
- возвращает сырой candidate route без deterministic business-routing
- Вход
- `normalized_query: str`
- `target_terms: list[str]`
- `anchors: dict`
- Выход
`dict | None`
### `routers/validator.py`
- Задача
Deterministic validation ответа LLM.
- Как решает
Проверяет:
- что `routing_domain`, `intent`, `subintent` заполнены
- что комбинация маршрута входит в `route_catalog`
- что `confidence` можно привести к числу
- Вход
`dict | None`
- Выход
Валидированный `dict | None`
### `routers/confidence.py`
- Задача
Сделать confidence осмысленным после ответа LLM.
- Как решает
Корректирует confidence:
- `-0.1`, если нет strong anchors
- `-0.1`, если запрос короткий или vague
- `+0.05`, если есть явный signal (`file_markers`, `endpoint_paths`, `endpoint_markers`)
- затем clamp в диапазон `0.0..1.0`
- Вход
- `confidence: float`
- `QueryFeatures`
- Выход
`confidence: float`
### `routers/fallback.py`
- Задача
Построить deterministic fallback, если LLM невалиден.
- Как решает
Правила:
- есть `file_markers``DOCS / DOC_EXPLAIN / FIND_FILES`
- есть docs-signals (`endpoint_paths`, `target_doc_hints`, `matched_aliases`, marker groups) → `DOCS / DOC_EXPLAIN / SUMMARY`
- иначе → `GENERAL / GENERAL_QA / SUMMARY`
- Вход
- `user_query: str`
- `QueryFeatures`
- `anchors: V2RouteAnchors`
- `llm_attempted: bool`
- Выход
`V2RouteResult`
### `routers/prompts.yml`
- Задача
Задать LLM-router контракт ответа и guidance по confidence.
- Как решает
Ограничивает модель только `allowed_routes` и требует JSON с полями:
- `routing_domain`
- `intent`
- `subintent`
- `confidence`
- `reason_short`
## 6. Ключевые инварианты
- LLM является default router.
- Deterministic-слой не принимает основной routing decision.
- `target_terms` содержат только retrieval-useful terms.
- `anchors` не содержат `terms`.
- `/health` и другие endpoint paths не должны попадать в `file_names`, если это не файл с расширением.
- `file_names` содержат только реальные file/doc paths.
- Fallback используется только если LLM недоступен или вернул невалидный маршрут.
@@ -0,0 +1,316 @@
# V2RetrievalPolicyResolver Architecture
## 1. Роль компонента
`V2RetrievalPolicyResolver` это deterministic bridge между `V2IntentRouter` и docs-RAG retrieval.
Компонент работает поверх уже готового `V2RouteResult` и не делает повторную интерпретацию пользовательского текста:
- не вызывает LLM;
- не меняет `intent` и `subintent`;
- не ранжирует документы;
- не собирает evidence.
Его задача: собрать один `RetrievalPlan` с полями:
- `profile`
- `layers`
- `limit`
- `filters`
## 2. Зависимости
Актуальная реализация опирается на:
- `src/app/core/agent/processes/v2/retrieval/policy_resolver.py`
- `src/app/core/agent/processes/v2/anchor_signals.py`
- `src/app/core/agent/processes/v2/models.py`
- `src/app/core/rag/contracts/enums.py`
- `src/app/core/agent/processes/v2/retrieval/v2_rag_adapter.py`
- `src/app/core/rag/retrieval/session_retriever.py`
- `src/app/core/rag/persistence/repository.py`
- `src/app/core/rag/persistence/query_repository.py`
- `src/app/core/rag/persistence/retrieval_statement_builder.py`
## 3. Входной контракт
Resolver использует:
- `route.intent`
- `route.subintent`
- `route.anchors.entity_names`
- `route.anchors.file_names`
- `route.anchors.endpoint_paths`
- `route.anchors.target_doc_hints`
- `route.anchors.matched_aliases`
- `route.anchors.process_domain`
- `route.anchors.process_subdomain`
`route.target_terms` в текущей реализации profile/filter branching не влияет.
## 4. Верхнеуровневый branching
`resolve(route)` имеет три ветки:
1. `GENERAL_QA` -> `general_qa_grounded_summary`
2. `FIND_FILES` -> `file_lookup`
3. иначе -> docs summary branch
Инварианты:
- `GENERAL_QA` всегда остаётся general profile;
- `FIND_FILES` всегда остаётся `file_lookup`;
- resolver всегда возвращает один валидный `RetrievalPlan`.
## 5. Внутренняя декомпозиция
Текущая реализация разбита на два helper-класса.
### `_AnchorTermCollector`
Собирает термы для `prefer_like_patterns`.
Источники:
- basename из `target_doc_hints`
- `endpoint_paths`
- `file_names`
- `entity_names`
- `matched_aliases`
- `process_domain`
- `process_subdomain`
Все значения нормализуются в lower-case и превращаются в SQL-like patterns вида `"%term%"`.
Для `FIND_FILES` действует отдельное правило:
- если есть `target_doc_hints`, `prefer_like_patterns` строится только по basename hints;
- иначе используется общий набор collected terms.
### `_RouteFilterBuilder`
Собирает `filters` для трёх веток:
- `general_filters(route)`
- `summary_filters(route)`
- `find_files_filters(route)`
Дополнительно содержит path selection:
- `_summary_prefixes(route)`
- `_find_files_prefixes(route)`
- `_find_files_prefer_prefixes(route)`
## 6. Signal detection
Summary profile и часть path preferences зависят от `anchor_signal_types(route)`.
Сигналы вычисляются так:
- `FIND_FILES`
- если `route.subintent == FIND_FILES`
- `API_ENDPOINT`
- если есть `endpoint_paths`
- или в `target_doc_hints` / `file_names` / `matched_aliases` встречаются маркеры `"/api/"`, `"api"`, `"endpoint"`
- `ARCHITECTURE`
- если в `target_doc_hints` / `file_names` / `matched_aliases` встречаются `"/architecture/"`, `"architecture"`, `"arch"`
- `LOGIC_FLOW`
- если в `target_doc_hints` / `file_names` / `matched_aliases` встречаются `"/logic/"`, `"logic"`, `"workflow"`, `"flow"`, `"process"`
- `DOMAIN_ENTITY`
- если есть `entity_names`
- или в `target_doc_hints` / `file_names` / `matched_aliases` встречаются `"/domains/"`, `"domain"`, `"entity"`, `"component"`
Важно:
- `process_domain` и `process_subdomain` сейчас **не участвуют** в signal detection;
- они влияют только на filters и `prefer_like_patterns`.
## 7. Summary profile selection
Метод `_summary_profile(route)` использует:
- `meaningful = anchor_signal_types(route) - {FIND_FILES}`
Правило:
- если meaningful signal не ровно один -> `docs_summary_generic`
- если ровно один:
- `API_ENDPOINT` -> `docs_summary_api_endpoint`
- `ARCHITECTURE` -> `docs_summary_architecture`
- `LOGIC_FLOW` -> `docs_summary_logic_flow`
- `DOMAIN_ENTITY` -> `docs_summary_domain_entity`
Следствие:
- конфликт API + architecture -> generic;
- API + entity -> generic;
- weak/no signals -> generic.
## 8. Profiles, layers, limits
### `general_qa_grounded_summary`
- condition: `route.intent == GENERAL_QA`
- layers: `[D1_DOCUMENT_CATALOG, D0_DOC_CHUNKS]`
- limit: `8`
### `file_lookup`
- condition: `route.subintent == FIND_FILES`
- layers: `[D1_DOCUMENT_CATALOG, D3_ENTITY_CATALOG]`
- limit: `12`
### `docs_summary_api_endpoint`
- layers: `[D1_DOCUMENT_CATALOG, D2_FACT_INDEX, D0_DOC_CHUNKS]`
- limit: `8`
### `docs_summary_logic_flow`
- layers: `[D4_WORKFLOW_INDEX, D1_DOCUMENT_CATALOG, D0_DOC_CHUNKS]`
- limit: `8`
### `docs_summary_domain_entity`
- layers: `[D3_ENTITY_CATALOG, D1_DOCUMENT_CATALOG, D0_DOC_CHUNKS]`
- limit: `8`
### `docs_summary_architecture`
- layers: `[D1_DOCUMENT_CATALOG, D5_RELATION_GRAPH, D0_DOC_CHUNKS]`
- limit: `8`
### `docs_summary_generic`
- layers: `[D1_DOCUMENT_CATALOG, D0_DOC_CHUNKS]`
- limit: `8`
## 9. Filters by branch
### General branch
`general_filters(route)` возвращает:
- `prefer_path_prefixes = ["docs/architecture/", "docs/"]`
- `prefer_like_patterns = ["%readme.md%", "%overview%"]`
- `target_doc_hints = list(route.anchors.target_doc_hints)`
Это обзорный, но не узкий plan: hard `path_prefixes` здесь нет.
### Summary branch
`summary_filters(route)` всегда включает:
- `target_doc_hints`
- `metadata.domain`, если есть `process_domain`
- `metadata.subdomain`, если есть `process_subdomain`
- `prefer_path_prefixes`
- `prefer_like_patterns`
Дополнительно:
- если есть `API_ENDPOINT` signal, добавляется hard `path_prefixes = ["docs/api/", "docs/"]`
`prefer_path_prefixes` для summary:
- API -> `["docs/api/", "docs/"]`
- ARCHITECTURE -> `["docs/architecture/", "docs/"]`
- LOGIC_FLOW -> `["docs/logic/", "docs/architecture/", "docs/"]`
- DOMAIN_ENTITY -> `["docs/domains/", "docs/", "docs/api/"]`
- empty signals -> `["docs/"]`
Если сигналов несколько, prefixes объединяются и dedupe-ятся с сохранением порядка.
### FIND_FILES branch
`find_files_filters(route)` всегда включает:
- `target_doc_hints`
- `metadata.domain`, если есть `process_domain`
- `metadata.subdomain`, если есть `process_subdomain`
- `path_prefixes`
- `prefer_path_prefixes`
- `prefer_like_patterns`
`path_prefixes` для `FIND_FILES` выбираются по приоритету:
1. директории из `target_doc_hints`
2. директории из `file_names`, если путь начинается с `docs/`
3. signal-based fallback:
- API -> `["docs/api/", "docs/"]`
- ARCHITECTURE -> `["docs/architecture/", "docs/"]`
- LOGIC_FLOW -> `["docs/logic/", "docs/"]`
- DOMAIN_ENTITY -> `["docs/domains/", "docs/"]`
4. default -> `["docs/"]`
`prefer_path_prefixes` для `FIND_FILES`:
- начинается с `path_prefixes`
- если есть `process_domain` или `process_subdomain`, дополнительно добавляет:
- `"docs/domains/"`
- `"docs/logic/"`
## 10. Hard и soft сигналы в текущей реализации
В терминах текущего кода:
Hard-ish / narrowing filters:
- `path_prefixes`
- `metadata.domain`
- `metadata.subdomain`
Soft preferences:
- `prefer_path_prefixes`
- `prefer_like_patterns`
Отдельно:
- `target_doc_hints` всегда сохраняются в `RetrievalPlan.filters`, но **не маппятся напрямую** в `RagRepository.retrieve(...)` как SQL hard filter.
То есть сейчас `target_doc_hints` это не прямой DB filter, а downstream anchor для других шагов пайплайна и для deterministic exact-doc seeding logic.
## 11. Интеграция с retrieval stack
Следующий слой после resolver теперь исполняет plan не напрямую в `V2Process`, а через `V2RagRetrievalAdapter`.
`V2RagRetrievalAdapter.fetch_rows(...)` использует `RetrievalPlan` так:
- читает `filters["target_doc_hints"]` из самого плана;
- делает exact-path seed через `retrieve_exact_files(...)`;
- для missing hints делает substring fallback через `retrieve_chunks_by_path_substrings(...)`;
- затем делает обычный semantic retrieve через `RagSessionRetriever.retrieve(...)`;
- объединяет exact / substring / semantic rows через dedupe merge.
Это важный сдвиг: execution strategy теперь зависит от **контракта `RetrievalPlan`**, а не от скрытой route-specific логики внутри `V2Process`.
`RagSessionRetriever._map_filters()` прокидывает в `RagRepository.retrieve(...)`:
- `path_prefixes`
- `exclude_path_prefixes`
- `exclude_like_patterns`
- `prefer_path_prefixes`
- `prefer_like_patterns`
- `prefer_non_tests`
- `metadata_domain` из `filters["metadata.domain"]`
- `metadata_subdomain` из `filters["metadata.subdomain"]`
`RetrievalStatementBuilder.build_retrieve(...)` добавляет SQL predicates:
- `lower(metadata_json->>'domain') = :metadata_domain`
- `lower(metadata_json->>'subdomain') = :metadata_subdomain`
Таким образом:
- `process_domain/process_subdomain` реально участвуют в retrieval query;
- `target_doc_hints` реально участвуют в retrieval execution strategy на уровне adapter;
- `V2RetrievalPolicyResolver` определяет plan contract, а следующий шаг исполняет этот contract более буквально.
## 12. Актуальные ограничения
- Логика полностью deterministic.
- `target_terms` сейчас не участвуют в branching resolver.
- `process_domain/process_subdomain` не влияют на summary profile selection.
- API signal добавляет `path_prefixes` даже в generic summary, если среди конфликтующих сигналов присутствует API.
- `target_doc_hints` не являются прямым SQL filter внутри обычного `retrieve`, но используются adapter-уровнем для exact-path / substring seeding до semantic retrieval.
+37
View File
@@ -0,0 +1,37 @@
# Documentation Rules V3
Этот каталог содержит правила генерации технической документации из системной аналитики.
## Цель
- синхронизировать требования к документации с требованиями к аналитике (`04. Analitycs artefacts - features.md`);
- сохранить детальность техдокументации по сравнению с аналитикой;
- убрать дублирование структуры и manifest-слоя между разными файлами;
- собирать итоговый промпт из модулей: глобальные правила + template с manifest + блоки.
## Структура
- `documentation-rules.md` — верхнеуровневый регламент и порядок сборки.
- `global/` — общие правила (заголовки, frontmatter, слой ответственности, мост аналитика->документация).
- `common-elements/` — правила для общих блоков (`summary`, `details`, `use case`, `FR`, `NFR`, `UI`, `Contract`).
- `templates/` — единственный источник истины для структуры итоговой страницы и manifest-метаданных типа документа.
## Принцип сборки
Для конкретного документа агент собирает единый набор правил из:
1. `documentation-rules.md`
2. `global/*.md`
3. `templates/<doc_type>.template.md`
4. `common-elements/*.md`, указанных в frontmatter template
## Правило без дублирования
- `templates/` отвечают за структуру документа, порядок разделов и manifest-метаданные типа.
- `common-elements/` отвечают только за правила написания конкретного раздела.
- отдельный слой `types/` не нужен, если для типа документа используется один основной template.
## Формат template-manifest
Manifest оформляется в YAML frontmatter самого template.
Обязательные поля manifest:
- `doc_type`
- `required_common_elements`
Рекомендуемые поля:
- `special_rules`
@@ -0,0 +1,28 @@
# API Contract Rules
Этот rule описывает только тело секции `### Контракт`.
## Обязательные части
- request parameters (`header/query/path`)
- request body (если применимо)
- response body
- errors
- auth
- timeout
- retry/idempotency (если применимо)
## Правила заголовков внутри тела секции
- Не повторять заголовок `Контракт`.
- Запрещено выводить `## Контракт` и `### Контракт` внутри тела секции.
- Если нужны подзаголовки, использовать только уровень ниже родительской секции: `#### Запрос`, `#### Ответ`, `#### Ошибки`, `#### Auth`, `#### Timeout`, `#### Retry/Idempotency`.
## Табличный формат
Для request/response таблицы должны содержать:
- название
- тип данных
- обязательность
- описание
- пример
Для response дополнительно:
- заполнение (mapping/логика источника данных)
@@ -0,0 +1,17 @@
# DB Columns Rules
## Формат
Структура таблицы оформляется таблицей.
## Обязательные колонки
- `Поле`
- `Тип`
- `Nullable`
- `Описание`
- `Источник заполнения`
- `Использование`
## Правила
- перечислять все ключевые поля таблицы;
- для служебных полей (`id`, `created_at`, `updated_at`, `deleted_at`) явно описывать назначение;
- если тип или nullable не заданы в аналитике, допускается инженерное предположение с рабочим вариантом.
@@ -0,0 +1,16 @@
# DB Constraints Rules
## Что включать
- primary key;
- unique constraints;
- foreign keys;
- важные индексы;
- бизнес-ограничения на уровне БД.
## Формат
- списком или таблицей;
- для каждого индекса и ограничения писать, зачем оно нужно.
## Правила
- если индекс нужен для сценария чтения/пагинации, это должно быть явно сказано;
- если точные названия индексов неизвестны, можно использовать осмысленные проектные названия.
@@ -0,0 +1,12 @@
# DB Table Purpose Rules
## Что описывать
- назначение таблицы;
- в каком сценарии она используется;
- кто является владельцем данных;
- является ли таблица источником истины или производным хранилищем.
## Формат
- 1-3 абзаца без воды;
- явно указывать доменную сущность, которую хранит таблица;
- если сделаны допущения по БД, фиксировать их отдельной фразой.
@@ -0,0 +1,11 @@
# DB Usage Rules
## Что описывать
- какие API / logic block / batch job используют таблицу;
- какие операции выполняются: read / insert / update / delete;
- как таблица участвует в пользовательском сценарии.
## Правила
- ссылки на связанные документы давать по `doc_id` или path;
- не дублировать полный use case, а показывать роль таблицы в сценарии;
- если таблица используется для пагинации, фильтрации или сортировки, это нужно отметить явно.
@@ -0,0 +1,10 @@
# Details Rules
## Назначение
Этот файл задает общие правила для секции `## Details`.
## Правила
- `Details` оформляется как `## Details`.
- Внутри `Details` используются заголовки уровня `###` и ниже.
- Структура `Details` определяется template типа документа.
- В `Details` не нужно дублировать навигацию и связи, если они уже есть во frontmatter.
@@ -0,0 +1,34 @@
# Functional Requirements Rules
Этот rule описывает только тело секции `### Функциональные требования`.
## Формат
- `FR.<номер>. <Название>`
- Нумерация инкрементальная внутри документа.
## Правила
- FR расширяют шаги сценария.
- FR не копируют шаги сценария без добавления новой информации.
- Для интеграционных шагов FR обязательны.
- Если в сценарии есть вызов внешнего API / сервиса / БД, нужен отдельный FR на интеграцию.
- Запрещено повторять заголовок `### Функциональные требования` внутри тела секции.
## FR для интеграционных шагов
Для интеграционного FR обязательно раскрывать:
- как формируется запрос;
- откуда берется каждый значимый атрибут запроса;
- какой downstream вызывается;
- какой ответ считается успешным;
- какие ответы и ситуации считаются бизнес-ошибкой;
- какие ситуации считаются технической ошибкой;
- как downstream-ответ маппится в контракт текущего слоя.
## FR для шагов доступа к БД
Если шаг читает или пишет БД, FR должен по возможности включать:
- таблицу или набор таблиц;
- логику фильтрации;
- логику сортировки;
- логику пагинации;
- пример SQL или близкий к рабочему псевдо-SQL.
Если СУБД и диалект не заданы, допускается сделать рабочее предположение и явно зафиксировать его.
@@ -0,0 +1,20 @@
# Non-Functional Requirements Rules
## Для api_method
- Подразделы:
- `#### Аудит` (если применимо)
- `#### Мониторинг`
## Мониторинг
Оформлять таблицей:
- `Метрика`
- `Описание`
- `Условие срабатывания`
Запрещено:
- использовать «точка измерения = метод» вместо условий срабатывания.
Базовые суффиксы метрик:
- `_SUCCESS`
- `_FAIL`
- `_BUSINESS_ERROR`
@@ -0,0 +1,15 @@
# SQL Example Rules
## Назначение
Секция показывает пример рабочего SQL для основного сценария использования таблицы.
## Правила
- SQL должен быть близок к рабочему, а не абстрактным псевдокодом;
- если диалект БД не указан, допускается выбрать наиболее вероятный вариант и явно зафиксировать допущение;
- пример должен отражать реальный сценарий документа: чтение, вставка, обновление или агрегация;
- для read-сценариев по возможности показывать фильтрацию, сортировку и пагинацию;
- если есть join, нужно кратко пояснить, зачем он нужен.
## Формат
- fenced code block с указанием `sql`;
- под кодом 1-3 поясняющих bullets о ключевых условиях, индексах и параметрах.
@@ -0,0 +1,10 @@
# Summary Rules
## Назначение
Этот файл задает правила для секции `## Summary`.
## Правила
- `Summary` должен быть коротким слоем быстрого контекста.
- `Summary` должен объяснять суть документа без длинных деталей.
- Предпочтительный формат: краткий список ключевых фактов.
- `Summary` не должен дублировать `Details`.
@@ -0,0 +1,19 @@
# Tech Use Case Rules
Этот rule описывает только тело секции `### Технический use case`.
## Обязательные части
- название
- предусловия
- триггер
- основной сценарий
- альтернативный сценарий
- обработка ошибок
- постусловие
## Правила шага
- Один шаг = одно предложение до 15-20 слов.
- Формат шага: смысловое действие + техническая реализация (endpoint/топик/операция).
- Длинные технические детали выносить в FR и ссылаться на FR из шага.
- Для интеграционных шагов описание обработки ошибок обязательно.
- Запрещено повторять заголовок `### Технический use case` внутри тела секции.
@@ -0,0 +1,22 @@
# UI Requirements Rules
## Структура блока
- `### Требования к UI`
- Внутри обязательно отдельные формы:
- табличное представление
- пустой список (empty state)
- ошибка (error state)
## Обязательные правила
- Если есть интеграция, обязательно описывать показ ошибки.
- Если есть список, обязательно описывать показ отсутствия данных.
## Описание UI-элементов
UI-элементы описываются строго в таблице.
Обязательные колонки (где применимо):
- `Код элемента`
- `Название и описание`
- `Данные`
- `Поведение`
- `Валидация`
@@ -0,0 +1,7 @@
# User Analytics Rules
События пользовательской аналитики оформлять таблицей:
- `Название события`
- `Описание`
- `Точка вызова`
- `Payload`
@@ -0,0 +1,56 @@
# Documentation Rules V3
## 1. Общий контракт
- Документация строится на основе системной аналитики, но на более детальном уровне.
- Заголовки отражают только суть раздела; метаданные в заголовках запрещены.
- Метаданные указываются во frontmatter и/или отдельными строками в body.
- Структура документа определяется только template соответствующего типа.
- Правила написания конкретного раздела определяются только соответствующим `common-elements` файлом.
- Manifest типа документа хранится во frontmatter соответствующего template.
- Генератор секции всегда пишет только тело секции, а не сам заголовок секции.
- Дублирование заголовков запрещено: нельзя повторно выводить заголовок текущей секции внутри ее тела.
- Если template уже содержит `### <Заголовок секции>`, то внутри тела допустимы только подзаголовки более глубокого уровня (`####` и ниже).
- Нельзя повышать уровень заголовка внутри тела секции до `##` или повторять `###` с тем же названием секции.
## 2. Источники требований
При генерации документа учитывать:
- `/Users/alex/Dev_projects_v2/ai driven app process/v2/agent/_process/04. Analitycs artefacts - documentation.md`
- `/Users/alex/Dev_projects_v2/ai driven app process/v2/agent/_process/04. Analitycs artefacts - features.md`
- правила v2 из `src/app/core/agent/processes/v2/doc_rules_v2`
## 3. Разрыв аналитика vs документация
- Аналитика: концептуальная, укрупненная.
- Документация: технически детальная.
- Технический use case в документации не копирует аналитический 1-в-1, а детализирует его.
- Функциональные требования расширяют сценарий и не дублируют шаги без новой информации.
## 4. Заполнение пробелов
Если атрибуты/детали отсутствуют в аналитике:
1. восстановить из формулировок аналитики;
2. уточнить по репозиторию (код, контракты, существующие документы);
3. зафиксировать в документации явно.
## 5. Сборка итогового промпта
1. Загрузить global-правила.
2. Загрузить template типа документа.
3. Прочитать YAML frontmatter template как manifest.
4. Загрузить общие блоки, указанные в manifest.
5. Применить body template как единственный источник структуры.
5. Проверить чек-лист совместимости с аналитикой (domain/sub_domain, роли слоев, интеграции, ошибки).
## 6. Специальные инварианты для `api_method`
- Во frontmatter обязательно должно присутствовать поле `endpoint`.
- Внутри `## Details` секция `### Контракт` должна присутствовать ровно один раз.
- Внутри тела секции `### Контракт` запрещено повторять заголовки `## Контракт` и `### Контракт`.
- Внутри `### Технический use case` запрещено повторять заголовок `### Технический use case`.
- Внутри `### Функциональные требования` запрещено повторять заголовок `### Функциональные требования`.
## 7. Формат manifest типа документа
Manifest типа документа хранится во frontmatter `templates/<doc_type>.template.md`.
Минимальная схема:
- `doc_type`
- `required_common_elements`
Дополнительно можно указывать:
- `special_rules`
@@ -0,0 +1,10 @@
# Analytics to Documentation Mapping
## Принцип
- Системная аналитика задает «что».
- Документация детализирует «как».
## Маппинг
- Из раздела архитектуры аналитики переносить контейнеры, интеграции и цепочки вызовов.
- Из раздела изменений аналитики строить отдельные технические страницы (`ui_page`, `api_method`, `logic_block`).
- Если в аналитике упрощенный use case, в документации раскрывать полный технический сценарий по правилам `tech-use-case.md`.
+67
View File
@@ -0,0 +1,67 @@
# Правила определения путей файлов
Текущая happy-path реализация строит путь документа по фиксированному шаблону:
`docs/<domain>/<platform>/<doc_type>/<doc_id>.md`
Пример:
`docs/orders/pprb/ui_page/orders.ui.list.md`
## Источники атрибутов
Для построения пути используются четыре основных атрибута:
- `domain`
- `application`
- `platform`
- `doc_type`
- `id` как `doc_id`
Если атрибуты явно указаны в подразделе `6.x`, нужно использовать их.
Если атрибут не указан, он может быть взят из общих метаданных аналитики или определен fallback-логикой.
## Нормализация сегментов
Каждый сегмент пути нормализуется одинаково:
- значение переводится в lowercase;
- все символы, кроме `a-z`, `0-9`, `.`, `_`, `-`, заменяются на `-`;
- ведущие и хвостовые `.` и `-` удаляются.
Примеры нормализации:
- `Payment Status` -> `payment-status`
- `UFS Orders` -> `ufs-orders`
- `crm.mobile` -> `crm.mobile`
## Значения по умолчанию
Если после нормализации сегмент пустой, используются fallback-значения:
- корневая папка: `domain`, иначе `application`, иначе `common`
- `platform` -> `web`
- `doc_type` -> `misc`
- `doc_id` -> `untitled`
## Что важно в текущей версии
- для корневой папки сначала используется `domain`;
- если `domain` не задан, используется `application`;
- `sub_domain` сейчас не участвует в построении пути;
- операции `create`, `update`, `delete` работают с одним и тем же правилом вычисления пути;
- специальных исключений для разных типов документов пока нет;
- отдельные каталоги для `pprb`, `ufs`, `web` задаются только через значение `platform`.
## Практическое правило для агента
Если нужно предложить или определить путь новой страницы, агент должен:
1. определить `application`;
2. определить `domain`;
3. определить `platform`;
4. определить `doc_type`;
5. определить стабильный `doc_id`;
6. взять корневую папку как `domain`, а если он пустой, то `application`;
7. нормализовать все сегменты;
8. собрать путь по шаблону `docs/<root>/<platform>/<doc_type>/<doc_id>.md`.
@@ -0,0 +1,37 @@
# Frontmatter Rules
## Обязательные поля
```yaml
id: string
title: string
doc_type: string
domain: string
sub_domain: string
related_docs: []
status: string
```
## Рекомендуемые поля
```yaml
tags: []
entities: []
source_of_truth: string
related_code: []
system_analytics_refs: []
```
## Дополнительные обязательные поля по типам документов
- Для `doc_type: api_method` поле `endpoint` обязательно.
- Значение `endpoint` должно содержать HTTP-метод и путь, например: `GET /orders/{orderId}`.
- Если в аналитике endpoint указан в заголовке раздела, use case, контракте или интеграционной схеме, его нужно перенести во frontmatter и не опускать.
## Body-метаданные для секции изменений
Под корнем секции изменений указывать:
- `domain`
- `sub_domain`
Для каждого подраздела `X.Y` указывать строками:
- `id`
- `doc_type`
- `application`
- `platform`
@@ -0,0 +1,10 @@
# Header Rules
## Правила
- Заголовок описывает только смысл раздела.
- Не включать в заголовок: `id`, `doc_type`, `application`, `platform`, `domain`, `sub_domain`.
- Метаданные указываются отдельными строками ниже заголовка или во frontmatter.
## Пример
- Правильно: `## 6.2 Метод UFS получения списка заказов`
- Неправильно: `## 6.2 Блок api_method (id=..., platform=ufs)`
@@ -0,0 +1,10 @@
# Layer Responsibility
- `ui`: отображение, UX, запуск пользовательских сценариев.
- `ufs`: авторизация/аутентификация, агрегация, маппинг, оркестрация вызовов.
- `pprb`: API, БД, доменная логика backend.
## Правила согласованности
- Проверка ролевой модели пользователя обычно фиксируется на уровне `ufs`.
- Если проверка роли вынесена в `ufs`, в `pprb`-сценарии не дублировать этот шаг.
- Аудит для `pprb` может отсутствовать, если это явно принято для домена/фичи.
@@ -0,0 +1,34 @@
---
doc_type: api_method
required_common_elements:
- common-elements/summary.md
- common-elements/details.md
- common-elements/tech-use-case.md
- common-elements/fr.md
- common-elements/nfr.md
- common-elements/api-contract.md
special_rules:
- Технический use case детализируется по `common-elements/tech-use-case.md`.
- FR расширяют use case и не дублируют шаги сценария без новой информации.
- Для интеграционных шагов FR обязательны.
---
# <title>
## Summary
Правила оформления: `../common-elements/summary.md`
## Details
Правила оформления: `../common-elements/details.md`
### Технический use case
Правила оформления: `../common-elements/tech-use-case.md`
### Функциональные требования
Правила оформления: `../common-elements/fr.md`
### Нефункциональные требования
Правила оформления: `../common-elements/nfr.md`
### Контракт
Правила оформления: `../common-elements/api-contract.md`
@@ -0,0 +1,38 @@
---
doc_type: db_table
required_common_elements:
- common-elements/summary.md
- common-elements/details.md
- common-elements/db-purpose.md
- common-elements/db-columns.md
- common-elements/db-constraints.md
- common-elements/db-usage.md
- common-elements/sql-example.md
special_rules:
- Документ описывает одну физическую таблицу БД или materialized view.
- Нужно фиксировать назначение таблицы, поля, ограничения, индексы, связи и сценарии использования.
- Если точные детали БД не заданы, допустимо сделать рабочие инженерные допущения и явно записать их в документ.
---
# <title>
## Summary
Правила оформления: `../common-elements/summary.md`
## Details
Правила оформления: `../common-elements/details.md`
### Назначение таблицы
Правила оформления: `../common-elements/db-purpose.md`
### Структура таблицы
Правила оформления: `../common-elements/db-columns.md`
### Ограничения и индексы
Правила оформления: `../common-elements/db-constraints.md`
### Использование в сценариях
Правила оформления: `../common-elements/db-usage.md`
### Пример SQL
Правила оформления: `../common-elements/sql-example.md`
@@ -0,0 +1,28 @@
---
doc_type: logic_block
required_common_elements:
- common-elements/summary.md
- common-elements/details.md
- common-elements/tech-use-case.md
- common-elements/fr.md
- common-elements/nfr.md
special_rules:
- Logic block описывает переиспользуемую логику без дублирования UI/API деталей.
---
# <title>
## Summary
Правила оформления: `../common-elements/summary.md`
## Details
Правила оформления: `../common-elements/details.md`
### Технический use case
Правила оформления: `../common-elements/tech-use-case.md`
### Функциональные требования
Правила оформления: `../common-elements/fr.md`
### Нефункциональные требования
Правила оформления: `../common-elements/nfr.md`
@@ -0,0 +1,33 @@
---
doc_type: ui_page
required_common_elements:
- common-elements/summary.md
- common-elements/details.md
- common-elements/tech-use-case.md
- common-elements/ui-requirements.md
- common-elements/fr.md
- common-elements/user-analytics.md
special_rules:
- Для списочных страниц обязательно описывать табличное представление, empty state и error state.
- UI-элементы описываются в таблицах по правилам `common-elements/ui-requirements.md`.
---
# <title>
## Summary
Правила оформления: `../common-elements/summary.md`
## Details
Правила оформления: `../common-elements/details.md`
### Технический use case
Правила оформления: `../common-elements/tech-use-case.md`
### Требования к UI
Правила оформления: `../common-elements/ui-requirements.md`
### Функциональные требования
Правила оформления: `../common-elements/fr.md`
### Нефункциональные требования
Правила оформления: `../common-elements/user-analytics.md`
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
-38
View File
@@ -1,38 +0,0 @@
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.core.error_handlers import register_error_handlers
from app.modules.application import ModularApplication
def create_app() -> FastAPI:
app = FastAPI(title="Agent Backend MVP", version="0.1.0")
modules = ModularApplication()
app.state.modules = modules
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=False,
allow_methods=["*"],
allow_headers=["*"],
)
app.include_router(modules.chat.public_router())
app.include_router(modules.rag.public_router())
app.include_router(modules.rag.internal_router())
app.include_router(modules.agent.internal_router())
register_error_handlers(app)
@app.on_event("startup")
async def startup() -> None:
modules.startup()
@app.get("/health")
async def health() -> dict:
return {"status": "ok"}
return app
app = create_app()
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
-20
View File
@@ -1,20 +0,0 @@
from app.core.constants import SUPPORTED_SCHEMA_VERSION
from app.core.exceptions import AppError
from app.schemas.changeset import ChangeItem, ChangeSetPayload
from app.schemas.common import ModuleName
class ChangeSetValidator:
def validate(self, task_id: str, changeset: list[ChangeItem]) -> list[ChangeItem]:
payload = ChangeSetPayload(
schema_version=SUPPORTED_SCHEMA_VERSION,
task_id=task_id,
changeset=changeset,
)
if payload.schema_version != SUPPORTED_SCHEMA_VERSION:
raise AppError(
"unsupported_schema",
f"Unsupported schema version: {payload.schema_version}",
ModuleName.AGENT,
)
return payload.changeset
-20
View File
@@ -1,20 +0,0 @@
from datetime import datetime, timezone
from urllib.parse import urlparse
from uuid import uuid4
from app.core.exceptions import AppError
from app.schemas.common import ModuleName
class ConfluenceService:
async def fetch_page(self, url: str) -> dict:
parsed = urlparse(url)
if not parsed.scheme.startswith("http"):
raise AppError("invalid_url", "Invalid Confluence URL", ModuleName.CONFLUENCE)
return {
"page_id": str(uuid4()),
"title": "Confluence page",
"content_markdown": f"Fetched content from {url}",
"version": 1,
"fetched_at": datetime.now(timezone.utc).isoformat(),
}
@@ -1,11 +0,0 @@
from app.modules.agent.engine.graphs.base_graph import BaseGraphFactory
from app.modules.agent.engine.graphs.docs_graph import DocsGraphFactory
from app.modules.agent.engine.graphs.project_edits_graph import ProjectEditsGraphFactory
from app.modules.agent.engine.graphs.project_qa_graph import ProjectQaGraphFactory
__all__ = [
"BaseGraphFactory",
"DocsGraphFactory",
"ProjectEditsGraphFactory",
"ProjectQaGraphFactory",
]
@@ -1,58 +0,0 @@
from langgraph.graph import END, START, StateGraph
from app.modules.agent.engine.graphs.progress import emit_progress_sync
from app.modules.agent.llm import AgentLlmService
from app.modules.agent.engine.graphs.state import AgentGraphState
class BaseGraphFactory:
def __init__(self, llm: AgentLlmService) -> None:
self._llm = llm
def build(self, checkpointer=None):
graph = StateGraph(AgentGraphState)
graph.add_node("context", self._context_node)
graph.add_node("answer", self._answer_node)
graph.add_edge(START, "context")
graph.add_edge("context", "answer")
graph.add_edge("answer", END)
return graph.compile(checkpointer=checkpointer)
def _context_node(self, state: AgentGraphState) -> dict:
emit_progress_sync(
state,
stage="graph.default.context",
message="Готовлю контекст ответа по данным запроса.",
)
rag = state.get("rag_context", "")
conf = state.get("confluence_context", "")
emit_progress_sync(
state,
stage="graph.default.context.done",
message="Контекст собран, перехожу к формированию ответа.",
)
return {"rag_context": rag, "confluence_context": conf}
def _answer_node(self, state: AgentGraphState) -> dict:
emit_progress_sync(
state,
stage="graph.default.answer",
message="Формирую текст ответа для пользователя.",
)
msg = state.get("message", "")
rag = state.get("rag_context", "")
conf = state.get("confluence_context", "")
user_input = "\n\n".join(
[
f"User request:\n{msg}",
f"RAG context:\n{rag}",
f"Confluence context:\n{conf}",
]
)
answer = self._llm.generate("general_answer", user_input)
emit_progress_sync(
state,
stage="graph.default.answer.done",
message="Черновик ответа подготовлен.",
)
return {"answer": answer}
@@ -1,26 +0,0 @@
from pathlib import Path
import os
class DocsExamplesLoader:
def __init__(self, prompts_dir: Path | None = None) -> None:
base = prompts_dir or Path(__file__).resolve().parents[2] / "prompts"
env_override = os.getenv("AGENT_PROMPTS_DIR", "").strip()
root = Path(env_override) if env_override else base
self._examples_dir = root / "docs_examples"
def load_bundle(self, *, max_files: int = 6, max_chars_per_file: int = 1800) -> str:
if not self._examples_dir.is_dir():
return ""
files = sorted(
[p for p in self._examples_dir.iterdir() if p.is_file() and p.suffix.lower() in {".md", ".txt"}],
key=lambda p: p.name.lower(),
)[:max_files]
chunks: list[str] = []
for path in files:
content = path.read_text(encoding="utf-8", errors="ignore").strip()
if not content:
continue
excerpt = content[:max_chars_per_file].strip()
chunks.append(f"### Example: {path.name}\n{excerpt}")
return "\n\n".join(chunks).strip()
@@ -1,128 +0,0 @@
from langgraph.graph import END, START, StateGraph
import logging
from app.modules.agent.engine.graphs.file_targeting import FileTargeting
from app.modules.agent.engine.graphs.docs_graph_logic import DocsContentComposer, DocsContextAnalyzer
from app.modules.agent.engine.graphs.progress import emit_progress_sync
from app.modules.agent.engine.graphs.state import AgentGraphState
from app.modules.agent.llm import AgentLlmService
LOGGER = logging.getLogger(__name__)
class DocsGraphFactory:
_max_validation_attempts = 2
def __init__(self, llm: AgentLlmService) -> None:
self._targeting = FileTargeting()
self._analyzer = DocsContextAnalyzer(llm, self._targeting)
self._composer = DocsContentComposer(llm, self._targeting)
def build(self, checkpointer=None):
graph = StateGraph(AgentGraphState)
graph.add_node("collect_code_context", self._collect_code_context)
graph.add_node("detect_existing_docs", self._detect_existing_docs)
graph.add_node("decide_strategy", self._decide_strategy)
graph.add_node("load_rules_and_examples", self._load_rules_and_examples)
graph.add_node("plan_incremental_changes", self._plan_incremental_changes)
graph.add_node("plan_new_document", self._plan_new_document)
graph.add_node("generate_doc_content", self._generate_doc_content)
graph.add_node("self_check", self._self_check)
graph.add_node("build_changeset", self._build_changeset)
graph.add_node("summarize_result", self._summarize_result)
graph.add_edge(START, "collect_code_context")
graph.add_edge("collect_code_context", "detect_existing_docs")
graph.add_edge("detect_existing_docs", "decide_strategy")
graph.add_edge("decide_strategy", "load_rules_and_examples")
graph.add_conditional_edges(
"load_rules_and_examples",
self._route_after_rules_loading,
{
"incremental": "plan_incremental_changes",
"from_scratch": "plan_new_document",
},
)
graph.add_edge("plan_incremental_changes", "generate_doc_content")
graph.add_edge("plan_new_document", "generate_doc_content")
graph.add_edge("generate_doc_content", "self_check")
graph.add_conditional_edges(
"self_check",
self._route_after_self_check,
{"retry": "generate_doc_content", "ready": "build_changeset"},
)
graph.add_edge("build_changeset", "summarize_result")
graph.add_edge("summarize_result", END)
return graph.compile(checkpointer=checkpointer)
def _collect_code_context(self, state: AgentGraphState) -> dict:
return self._run_node(state, "collect_code_context", "Собираю контекст кода и файлов.", self._analyzer.collect_code_context)
def _detect_existing_docs(self, state: AgentGraphState) -> dict:
return self._run_node(
state,
"detect_existing_docs",
"Определяю, есть ли существующая документация проекта.",
self._analyzer.detect_existing_docs,
)
def _decide_strategy(self, state: AgentGraphState) -> dict:
return self._run_node(state, "decide_strategy", "Выбираю стратегию: инкремент или генерация с нуля.", self._analyzer.decide_strategy)
def _load_rules_and_examples(self, state: AgentGraphState) -> dict:
return self._run_node(
state,
"load_rules_and_examples",
"Загружаю правила и примеры формата документации.",
self._composer.load_rules_and_examples,
)
def _plan_incremental_changes(self, state: AgentGraphState) -> dict:
return self._run_node(
state,
"plan_incremental_changes",
"Планирую точечные изменения в существующей документации.",
lambda st: self._composer.plan_incremental_changes(st, self._analyzer),
)
def _plan_new_document(self, state: AgentGraphState) -> dict:
return self._run_node(state, "plan_new_document", "Проектирую структуру новой документации.", self._composer.plan_new_document)
def _generate_doc_content(self, state: AgentGraphState) -> dict:
return self._run_node(state, "generate_doc_content", "Генерирую содержимое документации.", self._composer.generate_doc_content)
def _self_check(self, state: AgentGraphState) -> dict:
return self._run_node(state, "self_check", "Проверяю соответствие результата правилам.", self._composer.self_check)
def _build_changeset(self, state: AgentGraphState) -> dict:
return self._run_node(state, "build_changeset", "Формирую итоговый набор изменений файлов.", self._composer.build_changeset)
def _summarize_result(self, state: AgentGraphState) -> dict:
return self._run_node(
state,
"summarize_result",
"Формирую краткий обзор выполненных действий и измененных файлов.",
self._composer.build_execution_summary,
)
def _route_after_rules_loading(self, state: AgentGraphState) -> str:
if state.get("docs_strategy") == "incremental_update":
return "incremental"
return "from_scratch"
def _route_after_self_check(self, state: AgentGraphState) -> str:
if state.get("validation_passed"):
return "ready"
attempts = int(state.get("validation_attempts", 0) or 0)
return "ready" if attempts >= self._max_validation_attempts else "retry"
def _run_node(self, state: AgentGraphState, node_name: str, message: str, fn):
emit_progress_sync(state, stage=f"graph.docs.{node_name}", message=message)
try:
result = fn(state)
emit_progress_sync(state, stage=f"graph.docs.{node_name}.done", message=f"Шаг '{node_name}' завершен.")
LOGGER.warning("docs graph node completed: node=%s keys=%s", node_name, sorted(result.keys()))
return result
except Exception:
LOGGER.exception("docs graph node failed: node=%s", node_name)
raise
@@ -1,519 +0,0 @@
import json
from difflib import SequenceMatcher
from app.modules.agent.engine.graphs.docs_examples_loader import DocsExamplesLoader
from app.modules.agent.engine.graphs.file_targeting import FileTargeting
from app.modules.agent.engine.graphs.state import AgentGraphState
from app.modules.agent.llm import AgentLlmService
from app.schemas.changeset import ChangeItem
import logging
LOGGER = logging.getLogger(__name__)
class DocsContextAnalyzer:
def __init__(self, llm: AgentLlmService, targeting: FileTargeting) -> None:
self._llm = llm
self._targeting = targeting
def collect_code_context(self, state: AgentGraphState) -> dict:
message = state.get("message", "")
files_map = state.get("files_map", {}) or {}
requested_path = self._targeting.extract_target_path(message)
target_file = self._targeting.lookup_file(files_map, requested_path) if requested_path else None
docs_candidates = self._collect_doc_candidates(files_map)
target_path = str((target_file or {}).get("path") or (requested_path or "")).strip() or ""
return {
"docs_candidates": docs_candidates,
"target_path": target_path,
"target_file_content": str((target_file or {}).get("content", "")),
"target_file_hash": str((target_file or {}).get("content_hash", "")),
"validation_attempts": 0,
}
def detect_existing_docs(self, state: AgentGraphState) -> dict:
docs_candidates = state.get("docs_candidates", []) or []
if not docs_candidates:
return {
"existing_docs_detected": False,
"existing_docs_summary": "No documentation files detected in current project context.",
}
snippets = "\n\n".join(
[
f"Path: {item.get('path', '')}\nSnippet:\n{self._shorten(item.get('content', ''), 500)}"
for item in docs_candidates[:8]
]
)
user_input = "\n\n".join(
[
f"User request:\n{state.get('message', '')}",
f"Requested target path:\n{state.get('target_path', '') or '(not specified)'}",
f"Detected documentation candidates:\n{snippets}",
]
)
raw = self._llm.generate("docs_detect", user_input)
exists = self.parse_bool_marker(raw, "exists", default=True)
summary = self.parse_text_marker(raw, "summary", default="Documentation files detected.")
return {"existing_docs_detected": exists, "existing_docs_summary": summary}
def decide_strategy(self, state: AgentGraphState) -> dict:
message = (state.get("message", "") or "").lower()
if any(token in message for token in ("с нуля", "from scratch", "new documentation", "создай документацию")):
return {"docs_strategy": "from_scratch"}
if any(token in message for token in ("дополни", "обнови документацию", "extend docs", "update docs")):
return {"docs_strategy": "incremental_update"}
user_input = "\n\n".join(
[
f"User request:\n{state.get('message', '')}",
f"Existing docs detected:\n{state.get('existing_docs_detected', False)}",
f"Existing docs summary:\n{state.get('existing_docs_summary', '')}",
]
)
raw = self._llm.generate("docs_strategy", user_input)
strategy = self.parse_text_marker(raw, "strategy", default="").lower()
if strategy not in {"incremental_update", "from_scratch"}:
strategy = "incremental_update" if state.get("existing_docs_detected", False) else "from_scratch"
return {"docs_strategy": strategy}
def resolve_target_for_incremental(self, state: AgentGraphState) -> tuple[str, dict | None]:
files_map = state.get("files_map", {}) or {}
preferred_path = state.get("target_path", "")
preferred = self._targeting.lookup_file(files_map, preferred_path)
if preferred:
return str(preferred.get("path") or preferred_path), preferred
candidates = state.get("docs_candidates", []) or []
if candidates:
first_path = str(candidates[0].get("path", ""))
resolved = self._targeting.lookup_file(files_map, first_path) or candidates[0]
return first_path, resolved
fallback = preferred_path.strip() or "docs/AGENT_DRAFT.md"
return fallback, None
def _collect_doc_candidates(self, files_map: dict[str, dict]) -> list[dict]:
candidates: list[dict] = []
for raw_path, payload in files_map.items():
path = str(raw_path or "").replace("\\", "/").strip()
if not path:
continue
low = path.lower()
is_doc = low.startswith("docs/") or low.endswith(".md") or low.endswith(".rst") or "/readme" in low or low.startswith("readme")
if not is_doc:
continue
candidates.append(
{
"path": str(payload.get("path") or path),
"content": str(payload.get("content", "")),
"content_hash": str(payload.get("content_hash", "")),
}
)
candidates.sort(key=lambda item: (0 if str(item.get("path", "")).lower().startswith("docs/") else 1, str(item.get("path", "")).lower()))
return candidates
def _shorten(self, text: str, max_chars: int) -> str:
value = (text or "").strip()
if len(value) <= max_chars:
return value
return value[:max_chars].rstrip() + "\n...[truncated]"
@staticmethod
def parse_bool_marker(text: str, marker: str, *, default: bool) -> bool:
value = DocsContextAnalyzer.parse_text_marker(text, marker, default="")
if not value:
return default
token = value.split()[0].strip().lower()
if token in {"yes", "true", "1", "да"}:
return True
if token in {"no", "false", "0", "нет"}:
return False
return default
@staticmethod
def parse_text_marker(text: str, marker: str, *, default: str) -> str:
low_marker = f"{marker.lower()}:"
for line in (text or "").splitlines():
raw = line.strip()
if raw.lower().startswith(low_marker):
return raw.split(":", 1)[1].strip()
return default
class DocsBundleFormatter:
def shorten(self, text: str, max_chars: int) -> str:
value = (text or "").strip()
if len(value) <= max_chars:
return value
return value[:max_chars].rstrip() + "\n...[truncated]"
def normalize_file_output(self, text: str) -> str:
value = (text or "").strip()
if value.startswith("```") and value.endswith("```"):
lines = value.splitlines()
if len(lines) >= 3:
return "\n".join(lines[1:-1]).strip()
return value
def parse_docs_bundle(self, raw_text: str) -> list[dict]:
text = (raw_text or "").strip()
if not text:
return []
candidate = self.normalize_file_output(text)
parsed = self._parse_json_candidate(candidate)
if parsed is None:
start = candidate.find("{")
end = candidate.rfind("}")
if start != -1 and end > start:
parsed = self._parse_json_candidate(candidate[start : end + 1])
if parsed is None:
return []
files: list[dict]
if isinstance(parsed, dict):
raw_files = parsed.get("files")
files = raw_files if isinstance(raw_files, list) else []
elif isinstance(parsed, list):
files = parsed
else:
files = []
out: list[dict] = []
seen: set[str] = set()
for item in files:
if not isinstance(item, dict):
continue
path = str(item.get("path", "")).replace("\\", "/").strip()
content = str(item.get("content", ""))
if not path or not content.strip():
continue
if path in seen:
continue
seen.add(path)
out.append(
{
"path": path,
"content": content,
"reason": str(item.get("reason", "")).strip(),
}
)
return out
def bundle_has_required_structure(self, bundle: list[dict]) -> bool:
if not bundle:
return False
has_api = any(str(item.get("path", "")).replace("\\", "/").startswith("docs/api/") for item in bundle)
has_logic = any(str(item.get("path", "")).replace("\\", "/").startswith("docs/logic/") for item in bundle)
return has_api and has_logic
def similarity(self, original: str, updated: str) -> float:
return SequenceMatcher(None, original or "", updated or "").ratio()
def line_change_ratio(self, original: str, updated: str) -> float:
orig_lines = (original or "").splitlines()
new_lines = (updated or "").splitlines()
if not orig_lines and not new_lines:
return 0.0
matcher = SequenceMatcher(None, orig_lines, new_lines)
changed = 0
for tag, i1, i2, j1, j2 in matcher.get_opcodes():
if tag == "equal":
continue
changed += max(i2 - i1, j2 - j1)
total = max(len(orig_lines), len(new_lines), 1)
return changed / total
def added_headings(self, original: str, updated: str) -> int:
old_heads = {line.strip() for line in (original or "").splitlines() if line.strip().startswith("#")}
new_heads = {line.strip() for line in (updated or "").splitlines() if line.strip().startswith("#")}
return len(new_heads - old_heads)
def collapse_whitespace(self, text: str) -> str:
return " ".join((text or "").split())
def _parse_json_candidate(self, text: str):
try:
return json.loads(text)
except Exception:
return None
class DocsContentComposer:
def __init__(self, llm: AgentLlmService, targeting: FileTargeting) -> None:
self._llm = llm
self._targeting = targeting
self._examples = DocsExamplesLoader()
self._bundle = DocsBundleFormatter()
def load_rules_and_examples(self, _state: AgentGraphState) -> dict:
return {"rules_bundle": self._examples.load_bundle()}
def plan_incremental_changes(self, state: AgentGraphState, analyzer: DocsContextAnalyzer) -> dict:
target_path, target = analyzer.resolve_target_for_incremental(state)
user_input = "\n\n".join(
[
"Strategy: incremental_update",
f"User request:\n{state.get('message', '')}",
f"Target path:\n{target_path}",
f"Current target content:\n{self._bundle.shorten((target or {}).get('content', ''), 3000)}",
f"RAG context:\n{self._bundle.shorten(state.get('rag_context', ''), 6000)}",
f"Examples bundle:\n{state.get('rules_bundle', '')}",
]
)
plan = self._llm.generate("docs_plan_sections", user_input)
return {
"doc_plan": plan,
"target_path": target_path,
"target_file_content": str((target or {}).get("content", "")),
"target_file_hash": str((target or {}).get("content_hash", "")),
}
def plan_new_document(self, state: AgentGraphState) -> dict:
target_path = state.get("target_path", "").strip() or "docs/AGENT_DRAFT.md"
user_input = "\n\n".join(
[
"Strategy: from_scratch",
f"User request:\n{state.get('message', '')}",
f"Target path:\n{target_path}",
f"RAG context:\n{self._bundle.shorten(state.get('rag_context', ''), 6000)}",
f"Examples bundle:\n{state.get('rules_bundle', '')}",
]
)
plan = self._llm.generate("docs_plan_sections", user_input)
return {"doc_plan": plan, "target_path": target_path, "target_file_content": "", "target_file_hash": ""}
def generate_doc_content(self, state: AgentGraphState) -> dict:
user_input = "\n\n".join(
[
f"Strategy:\n{state.get('docs_strategy', 'from_scratch')}",
f"User request:\n{state.get('message', '')}",
f"Target path:\n{state.get('target_path', '')}",
f"Document plan:\n{state.get('doc_plan', '')}",
f"Current target content:\n{self._bundle.shorten(state.get('target_file_content', ''), 3500)}",
f"RAG context:\n{self._bundle.shorten(state.get('rag_context', ''), 7000)}",
f"Examples bundle:\n{state.get('rules_bundle', '')}",
]
)
raw = self._llm.generate("docs_generation", user_input)
bundle = self._bundle.parse_docs_bundle(raw)
if bundle:
first_content = str(bundle[0].get("content", "")).strip()
return {"generated_docs_bundle": bundle, "generated_doc": first_content}
content = self._bundle.normalize_file_output(raw)
return {"generated_docs_bundle": [], "generated_doc": content}
def self_check(self, state: AgentGraphState) -> dict:
attempts = int(state.get("validation_attempts", 0) or 0) + 1
bundle = state.get("generated_docs_bundle", []) or []
generated = state.get("generated_doc", "")
if not generated.strip() and not bundle:
return {
"validation_attempts": attempts,
"validation_passed": False,
"validation_feedback": "Generated document is empty.",
}
strategy = state.get("docs_strategy", "from_scratch")
if strategy == "from_scratch" and not self._bundle.bundle_has_required_structure(bundle):
return {
"validation_attempts": attempts,
"validation_passed": False,
"validation_feedback": "Bundle must include both docs/api and docs/logic for from_scratch strategy.",
}
if strategy == "incremental_update":
if bundle and len(bundle) > 1 and not self._is_broad_rewrite_request(str(state.get("message", ""))):
return {
"validation_attempts": attempts,
"validation_passed": False,
"validation_feedback": "Incremental update should not touch multiple files without explicit broad rewrite request.",
}
original = str(state.get("target_file_content", ""))
broad = self._is_broad_rewrite_request(str(state.get("message", "")))
if original and generated:
if self._bundle.collapse_whitespace(original) == self._bundle.collapse_whitespace(generated):
return {
"validation_attempts": attempts,
"validation_passed": False,
"validation_feedback": "Only formatting/whitespace changes detected.",
}
similarity = self._bundle.similarity(original, generated)
change_ratio = self._bundle.line_change_ratio(original, generated)
added_headings = self._bundle.added_headings(original, generated)
min_similarity = 0.75 if broad else 0.9
max_change_ratio = 0.7 if broad else 0.35
if similarity < min_similarity:
return {
"validation_attempts": attempts,
"validation_passed": False,
"validation_feedback": f"Incremental update is too broad (similarity={similarity:.2f}).",
}
if change_ratio > max_change_ratio:
return {
"validation_attempts": attempts,
"validation_passed": False,
"validation_feedback": f"Incremental update changes too many lines (change_ratio={change_ratio:.2f}).",
}
if not broad and added_headings > 0:
return {
"validation_attempts": attempts,
"validation_passed": False,
"validation_feedback": "New section headings were added outside requested scope.",
}
bundle_text = "\n".join([f"- {item.get('path', '')}" for item in bundle[:30]])
user_input = "\n\n".join(
[
f"Strategy:\n{strategy}",
f"User request:\n{state.get('message', '')}",
f"Document plan:\n{state.get('doc_plan', '')}",
f"Generated file paths:\n{bundle_text or '(single-file mode)'}",
f"Generated document:\n{generated}",
]
)
raw = self._llm.generate("docs_self_check", user_input)
passed = DocsContextAnalyzer.parse_bool_marker(raw, "pass", default=False)
feedback = DocsContextAnalyzer.parse_text_marker(raw, "feedback", default="No validation feedback provided.")
return {"validation_attempts": attempts, "validation_passed": passed, "validation_feedback": feedback}
def build_changeset(self, state: AgentGraphState) -> dict:
files_map = state.get("files_map", {}) or {}
bundle = state.get("generated_docs_bundle", []) or []
strategy = state.get("docs_strategy", "from_scratch")
if strategy == "from_scratch" and not self._bundle.bundle_has_required_structure(bundle):
LOGGER.warning(
"build_changeset fallback bundle used: strategy=%s bundle_items=%s",
strategy,
len(bundle),
)
bundle = self._build_fallback_bundle_from_text(state.get("generated_doc", ""))
if bundle:
changes: list[ChangeItem] = []
for item in bundle:
path = str(item.get("path", "")).replace("\\", "/").strip()
content = str(item.get("content", ""))
if not path or not content.strip():
continue
target = self._targeting.lookup_file(files_map, path)
reason = str(item.get("reason", "")).strip() or f"Documentation {strategy}: generated file from structured bundle."
if target and target.get("content_hash"):
changes.append(
ChangeItem(
op="update",
path=str(target.get("path") or path),
base_hash=str(target.get("content_hash", "")),
proposed_content=content,
reason=reason,
)
)
else:
changes.append(
ChangeItem(
op="create",
path=path,
proposed_content=content,
reason=reason,
)
)
if changes:
return {"changeset": changes}
target_path = (state.get("target_path", "") or "").strip() or "docs/AGENT_DRAFT.md"
target = self._targeting.lookup_file(files_map, target_path)
content = state.get("generated_doc", "")
if target and target.get("content_hash"):
change = ChangeItem(
op="update",
path=str(target.get("path") or target_path),
base_hash=str(target.get("content_hash", "")),
proposed_content=content,
reason=f"Documentation {strategy}: update existing document increment.",
)
else:
change = ChangeItem(
op="create",
path=target_path,
proposed_content=content,
reason=f"Documentation {strategy}: create document from current project context.",
)
return {"changeset": [change]}
def build_execution_summary(self, state: AgentGraphState) -> dict:
changeset = state.get("changeset", []) or []
if not changeset:
return {"answer": "Документация не была изменена: итоговый changeset пуст."}
file_lines = self._format_changed_files(changeset)
user_input = "\n\n".join(
[
f"User request:\n{state.get('message', '')}",
f"Documentation strategy:\n{state.get('docs_strategy', 'from_scratch')}",
f"Document plan:\n{state.get('doc_plan', '')}",
f"Validation feedback:\n{state.get('validation_feedback', '')}",
f"Changed files:\n{file_lines}",
]
)
try:
summary = self._llm.generate("docs_execution_summary", user_input).strip()
except Exception:
summary = ""
if not summary:
summary = self._build_fallback_summary(state, file_lines)
return {"answer": summary}
def _build_fallback_bundle_from_text(self, text: str) -> list[dict]:
content = (text or "").strip()
if not content:
content = (
"# Project Documentation Draft\n\n"
"## Overview\n"
"Documentation draft was generated, but structured sections require уточнение.\n"
)
return [
{
"path": "docs/logic/project_overview.md",
"content": content,
"reason": "Fallback: generated structured logic document from non-JSON model output.",
},
{
"path": "docs/api/README.md",
"content": (
"# API Methods\n\n"
"This file is a fallback placeholder for API method documentation.\n\n"
"## Next Step\n"
"- Add one file per API method under `docs/api/`.\n"
),
"reason": "Fallback: ensure required docs/api structure exists.",
},
]
def _format_changed_files(self, changeset: list[ChangeItem]) -> str:
lines: list[str] = []
for item in changeset[:30]:
lines.append(f"- {item.op.value} {item.path}: {item.reason}")
return "\n".join(lines)
def _build_fallback_summary(self, state: AgentGraphState, file_lines: str) -> str:
request = (state.get("message", "") or "").strip()
return "\n".join(
[
"Выполненные действия:",
f"- Обработан запрос: {request or '(пустой запрос)'}",
f"- Применена стратегия документации: {state.get('docs_strategy', 'from_scratch')}",
"- Сформирован и проверен changeset для документации.",
"",
"Измененные файлы:",
file_lines or "- (нет изменений)",
]
)
def _is_broad_rewrite_request(self, message: str) -> bool:
low = (message or "").lower()
markers = (
"перепиши",
"полностью",
"целиком",
"с нуля",
"full rewrite",
"rewrite all",
"реорганизуй",
)
return any(marker in low for marker in markers)
@@ -1,28 +0,0 @@
import re
class FileTargeting:
_path_pattern = re.compile(r"([A-Za-z0-9_.\-/]+?\.[A-Za-z0-9_]+)")
def extract_target_path(self, message: str) -> str | None:
text = (message or "").replace("\\", "/")
candidates = self._path_pattern.findall(text)
if not candidates:
return None
for candidate in candidates:
cleaned = candidate.strip("`'\".,:;()[]{}")
if "/" in cleaned or cleaned.startswith("."):
return cleaned
return candidates[0].strip("`'\".,:;()[]{}")
def lookup_file(self, files_map: dict[str, dict], path: str | None) -> dict | None:
if not path:
return None
normalized = path.replace("\\", "/")
if normalized in files_map:
return files_map[normalized]
low = normalized.lower()
for key, value in files_map.items():
if key.lower() == low:
return value
return None
@@ -1,44 +0,0 @@
from collections.abc import Awaitable, Callable
import inspect
import asyncio
from app.modules.agent.engine.graphs.progress_registry import progress_registry
from app.modules.agent.engine.graphs.state import AgentGraphState
ProgressCallback = Callable[[str, str, str, dict | None], Awaitable[None] | None]
async def emit_progress(
state: AgentGraphState,
*,
stage: str,
message: str,
kind: str = "task_progress",
meta: dict | None = None,
) -> None:
callback = progress_registry.get(state.get("progress_key"))
if callback is None:
return
result = callback(stage, message, kind, meta or {})
if inspect.isawaitable(result):
await result
def emit_progress_sync(
state: AgentGraphState,
*,
stage: str,
message: str,
kind: str = "task_progress",
meta: dict | None = None,
) -> None:
callback = progress_registry.get(state.get("progress_key"))
if callback is None:
return
result = callback(stage, message, kind, meta or {})
if inspect.isawaitable(result):
try:
loop = asyncio.get_running_loop()
loop.create_task(result)
except RuntimeError:
pass
@@ -1,27 +0,0 @@
from collections.abc import Awaitable, Callable
from threading import Lock
ProgressCallback = Callable[[str, str, str, dict | None], Awaitable[None] | None]
class ProgressRegistry:
def __init__(self) -> None:
self._items: dict[str, ProgressCallback] = {}
self._lock = Lock()
def register(self, key: str, callback: ProgressCallback) -> None:
with self._lock:
self._items[key] = callback
def get(self, key: str | None) -> ProgressCallback | None:
if not key:
return None
with self._lock:
return self._items.get(key)
def unregister(self, key: str) -> None:
with self._lock:
self._items.pop(key, None)
progress_registry = ProgressRegistry()
@@ -1,79 +0,0 @@
from langgraph.graph import END, START, StateGraph
from app.modules.agent.engine.graphs.progress import emit_progress_sync
from app.modules.agent.engine.graphs.project_edits_logic import ProjectEditsLogic
from app.modules.agent.engine.graphs.state import AgentGraphState
from app.modules.agent.llm import AgentLlmService
class ProjectEditsGraphFactory:
_max_validation_attempts = 2
def __init__(self, llm: AgentLlmService) -> None:
self._logic = ProjectEditsLogic(llm)
def build(self, checkpointer=None):
graph = StateGraph(AgentGraphState)
graph.add_node("collect_context", self._collect_context)
graph.add_node("plan_changes", self._plan_changes)
graph.add_node("generate_changeset", self._generate_changeset)
graph.add_node("self_check", self._self_check)
graph.add_node("build_result", self._build_result)
graph.add_edge(START, "collect_context")
graph.add_edge("collect_context", "plan_changes")
graph.add_edge("plan_changes", "generate_changeset")
graph.add_edge("generate_changeset", "self_check")
graph.add_conditional_edges(
"self_check",
self._route_after_self_check,
{"retry": "generate_changeset", "ready": "build_result"},
)
graph.add_edge("build_result", END)
return graph.compile(checkpointer=checkpointer)
def _collect_context(self, state: AgentGraphState) -> dict:
emit_progress_sync(
state,
stage="graph.project_edits.collect_context",
message="Собираю контекст и релевантные файлы для правок.",
)
return self._logic.collect_context(state)
def _plan_changes(self, state: AgentGraphState) -> dict:
emit_progress_sync(
state,
stage="graph.project_edits.plan_changes",
message="Определяю, что именно нужно изменить и в каких файлах.",
)
return self._logic.plan_changes(state)
def _generate_changeset(self, state: AgentGraphState) -> dict:
emit_progress_sync(
state,
stage="graph.project_edits.generate_changeset",
message="Формирую предлагаемые правки по выбранным файлам.",
)
return self._logic.generate_changeset(state)
def _self_check(self, state: AgentGraphState) -> dict:
emit_progress_sync(
state,
stage="graph.project_edits.self_check",
message="Проверяю, что правки соответствуют запросу и не трогают лишнее.",
)
return self._logic.self_check(state)
def _build_result(self, state: AgentGraphState) -> dict:
emit_progress_sync(
state,
stage="graph.project_edits.build_result",
message="Формирую итоговый changeset и краткий обзор.",
)
return self._logic.build_result(state)
def _route_after_self_check(self, state: AgentGraphState) -> str:
if state.get("validation_passed"):
return "ready"
attempts = int(state.get("validation_attempts", 0) or 0)
return "ready" if attempts >= self._max_validation_attempts else "retry"
@@ -1,271 +0,0 @@
import json
from difflib import SequenceMatcher
import re
from app.modules.agent.engine.graphs.file_targeting import FileTargeting
from app.modules.agent.engine.graphs.state import AgentGraphState
from app.modules.agent.llm import AgentLlmService
from app.schemas.changeset import ChangeItem
class ProjectEditsSupport:
def __init__(self, max_context_files: int = 12, max_preview_chars: int = 2500) -> None:
self._max_context_files = max_context_files
self._max_preview_chars = max_preview_chars
def pick_relevant_files(self, message: str, files_map: dict[str, dict]) -> list[dict]:
tokens = {x for x in (message or "").lower().replace("/", " ").split() if len(x) >= 4}
scored: list[tuple[int, dict]] = []
for path, payload in files_map.items():
content = str(payload.get("content", ""))
score = 0
low_path = path.lower()
low_content = content.lower()
for token in tokens:
if token in low_path:
score += 3
if token in low_content:
score += 1
scored.append((score, self.as_candidate(payload)))
scored.sort(key=lambda x: (-x[0], x[1]["path"]))
return [item for _, item in scored[: self._max_context_files]]
def as_candidate(self, payload: dict) -> dict:
return {
"path": str(payload.get("path", "")).replace("\\", "/"),
"content": str(payload.get("content", "")),
"content_hash": str(payload.get("content_hash", "")),
}
def build_summary(self, state: AgentGraphState, changeset: list[ChangeItem]) -> str:
if not changeset:
return "Правки не сформированы: changeset пуст."
lines = [
"Выполненные действия:",
f"- Проанализирован запрос: {state.get('message', '')}",
"- Собран контекст проекта и выбран набор файлов для правок.",
f"- Проведен self-check: {state.get('validation_feedback', 'без замечаний')}",
"",
"Измененные файлы:",
]
for item in changeset[:30]:
lines.append(f"- {item.op.value} {item.path}: {item.reason}")
return "\n".join(lines)
def normalize_file_output(self, text: str) -> str:
value = (text or "").strip()
if value.startswith("```") and value.endswith("```"):
lines = value.splitlines()
if len(lines) >= 3:
return "\n".join(lines[1:-1]).strip()
return value
def parse_json(self, raw: str):
text = self.normalize_file_output(raw)
try:
return json.loads(text)
except Exception:
return {}
def similarity(self, original: str, updated: str) -> float:
return SequenceMatcher(None, original or "", updated or "").ratio()
def shorten(self, text: str, max_chars: int | None = None) -> str:
limit = max_chars or self._max_preview_chars
value = (text or "").strip()
if len(value) <= limit:
return value
return value[:limit].rstrip() + "\n...[truncated]"
def collapse_whitespace(self, text: str) -> str:
return re.sub(r"\s+", " ", (text or "").strip())
def line_change_ratio(self, original: str, updated: str) -> float:
orig_lines = (original or "").splitlines()
new_lines = (updated or "").splitlines()
if not orig_lines and not new_lines:
return 0.0
matcher = SequenceMatcher(None, orig_lines, new_lines)
changed = 0
for tag, i1, i2, j1, j2 in matcher.get_opcodes():
if tag == "equal":
continue
changed += max(i2 - i1, j2 - j1)
total = max(len(orig_lines), len(new_lines), 1)
return changed / total
def added_headings(self, original: str, updated: str) -> int:
old_heads = {line.strip() for line in (original or "").splitlines() if line.strip().startswith("#")}
new_heads = {line.strip() for line in (updated or "").splitlines() if line.strip().startswith("#")}
return len(new_heads - old_heads)
class ProjectEditsLogic:
def __init__(self, llm: AgentLlmService) -> None:
self._llm = llm
self._targeting = FileTargeting()
self._support = ProjectEditsSupport()
def collect_context(self, state: AgentGraphState) -> dict:
message = state.get("message", "")
files_map = state.get("files_map", {}) or {}
requested_path = self._targeting.extract_target_path(message)
preferred = self._targeting.lookup_file(files_map, requested_path) if requested_path else None
candidates = self._support.pick_relevant_files(message, files_map)
if preferred and not any(x["path"] == preferred.get("path") for x in candidates):
candidates.insert(0, self._support.as_candidate(preferred))
return {
"edits_requested_path": str((preferred or {}).get("path") or (requested_path or "")).strip(),
"edits_context_files": candidates[:12],
"validation_attempts": 0,
}
def plan_changes(self, state: AgentGraphState) -> dict:
context_files = state.get("edits_context_files", []) or []
user_input = json.dumps(
{
"request": state.get("message", ""),
"requested_path": state.get("edits_requested_path", ""),
"context_files": [
{
"path": item.get("path", ""),
"content_preview": self._support.shorten(str(item.get("content", ""))),
}
for item in context_files
],
},
ensure_ascii=False,
)
parsed = self._support.parse_json(self._llm.generate("project_edits_plan", user_input))
files = parsed.get("files", []) if isinstance(parsed, dict) else []
planned: list[dict] = []
for item in files[:8] if isinstance(files, list) else []:
if not isinstance(item, dict):
continue
path = str(item.get("path", "")).replace("\\", "/").strip()
if not path:
continue
planned.append(
{
"path": path,
"reason": str(item.get("reason", "")).strip() or "Requested user adjustment.",
}
)
if not planned:
fallback_path = state.get("edits_requested_path", "").strip() or "docs/REQUESTED_UPDATES.md"
planned = [{"path": fallback_path, "reason": "Fallback path from user request."}]
return {"edits_plan": planned}
def generate_changeset(self, state: AgentGraphState) -> dict:
files_map = state.get("files_map", {}) or {}
planned = state.get("edits_plan", []) or []
changeset: list[ChangeItem] = []
for item in planned:
path = str(item.get("path", "")).replace("\\", "/").strip()
if not path:
continue
current = self._targeting.lookup_file(files_map, path)
current_content = str((current or {}).get("content", ""))
user_input = json.dumps(
{
"request": state.get("message", ""),
"path": path,
"reason": item.get("reason", ""),
"current_content": current_content,
"previous_validation_feedback": state.get("validation_feedback", ""),
"rag_context": self._support.shorten(state.get("rag_context", ""), 5000),
"confluence_context": self._support.shorten(state.get("confluence_context", ""), 5000),
"instruction": "Modify only required parts and preserve unrelated content unchanged.",
},
ensure_ascii=False,
)
raw = self._llm.generate("project_edits_apply", user_input).strip()
normalized = self._support.normalize_file_output(raw)
if not normalized:
continue
if current:
if normalized == current_content:
continue
if self._support.collapse_whitespace(normalized) == self._support.collapse_whitespace(current_content):
continue
reason = str(item.get("reason", "")).strip() or "User-requested update."
if current and current.get("content_hash"):
changeset.append(
ChangeItem(
op="update",
path=str(current.get("path") or path),
base_hash=str(current.get("content_hash", "")),
proposed_content=normalized,
reason=reason,
)
)
else:
changeset.append(ChangeItem(op="create", path=path, proposed_content=normalized, reason=reason))
return {"changeset": changeset}
def self_check(self, state: AgentGraphState) -> dict:
attempts = int(state.get("validation_attempts", 0) or 0) + 1
changeset = state.get("changeset", []) or []
files_map = state.get("files_map", {}) or {}
is_broad_rewrite = self._is_broad_rewrite_request(str(state.get("message", "")))
if not changeset:
return {"validation_attempts": attempts, "validation_passed": False, "validation_feedback": "Generated changeset is empty."}
for item in changeset:
if item.op.value != "update":
continue
source = self._targeting.lookup_file(files_map, item.path)
if not source:
continue
original = str(source.get("content", ""))
proposed = item.proposed_content or ""
similarity = self._support.similarity(original, proposed)
change_ratio = self._support.line_change_ratio(original, proposed)
headings_added = self._support.added_headings(original, proposed)
min_similarity = 0.75 if is_broad_rewrite else 0.9
max_change_ratio = 0.7 if is_broad_rewrite else 0.35
if similarity < min_similarity:
return {
"validation_attempts": attempts,
"validation_passed": False,
"validation_feedback": f"File {item.path} changed too aggressively (similarity={similarity:.2f}).",
}
if change_ratio > max_change_ratio:
return {
"validation_attempts": attempts,
"validation_passed": False,
"validation_feedback": f"File {item.path} changed too broadly (change_ratio={change_ratio:.2f}).",
}
if not is_broad_rewrite and headings_added > 0:
return {
"validation_attempts": attempts,
"validation_passed": False,
"validation_feedback": f"File {item.path} adds new sections outside requested scope.",
}
payload = {
"request": state.get("message", ""),
"changeset": [{"op": x.op.value, "path": x.path, "reason": x.reason} for x in changeset[:20]],
"rule": "Changes must match request and avoid unrelated modifications.",
}
parsed = self._support.parse_json(self._llm.generate("project_edits_self_check", json.dumps(payload, ensure_ascii=False)))
passed = bool(parsed.get("pass")) if isinstance(parsed, dict) else False
feedback = str(parsed.get("feedback", "")).strip() if isinstance(parsed, dict) else ""
return {"validation_attempts": attempts, "validation_passed": passed, "validation_feedback": feedback or "No feedback provided."}
def build_result(self, state: AgentGraphState) -> dict:
changeset = state.get("changeset", []) or []
return {"changeset": changeset, "answer": self._support.build_summary(state, changeset)}
def _is_broad_rewrite_request(self, message: str) -> bool:
low = (message or "").lower()
markers = (
"перепиши",
"полностью",
"целиком",
"с нуля",
"full rewrite",
"rewrite all",
"реорганизуй документ",
)
return any(marker in low for marker in markers)
@@ -1,38 +0,0 @@
from langgraph.graph import END, START, StateGraph
from app.modules.agent.engine.graphs.progress import emit_progress_sync
from app.modules.agent.engine.graphs.state import AgentGraphState
from app.modules.agent.llm import AgentLlmService
class ProjectQaGraphFactory:
def __init__(self, llm: AgentLlmService) -> None:
self._llm = llm
def build(self, checkpointer=None):
graph = StateGraph(AgentGraphState)
graph.add_node("answer", self._answer_node)
graph.add_edge(START, "answer")
graph.add_edge("answer", END)
return graph.compile(checkpointer=checkpointer)
def _answer_node(self, state: AgentGraphState) -> dict:
emit_progress_sync(
state,
stage="graph.project_qa.answer",
message="Готовлю ответ по контексту текущего проекта.",
)
user_input = "\n\n".join(
[
f"User request:\n{state.get('message', '')}",
f"RAG context:\n{state.get('rag_context', '')}",
f"Confluence context:\n{state.get('confluence_context', '')}",
]
)
answer = self._llm.generate("project_answer", user_input)
emit_progress_sync(
state,
stage="graph.project_qa.answer.done",
message="Ответ по проекту сформирован.",
)
return {"answer": answer}

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