Initial import NDC_1C

This commit is contained in:
dctouch 2026-03-26 10:38:25 +03:00
commit a162d77ef7
2943 changed files with 3615871 additions and 0 deletions

26
.env.example Normal file
View File

@ -0,0 +1,26 @@
ONEC_BASE_URL=http://localhost
ONEC_INFOBASE=AccountingBase
ONEC_USERNAME=readonly_user
ONEC_PASSWORD=change_me
ONEC_ODATA_PATH=/odata/standard.odata/
ONEC_TIMEOUT=30
ONEC_VERIFY_TLS=false
ONEC_PROBE_ENTITY_SETS=
ONEC_PROBE_TOP=5
ONEC_FOXY_PATH=/hs/AppEndpoint/
ONEC_FOXY_EXCHANGE=Self
ONEC_FOXY_OPERATION=Query
ONEC_FOXY_TYPE=SYNC
ONEC_FOXY_PAYLOAD_JSON={}
ONEC_FOXY_REPLYTO=
ONEC_FOXY_APPID=
ONEC_FOXY_CORRELATION_ID=
CANONICAL_DB_URL=sqlite:///X:/1C/NDC_1C/data/canonical_store.db
REFRESH_DEFAULT_LIMIT_PER_SET=200
REFRESH_DEFAULT_ENTITY_KEYWORDS=document,posting,movement,register,account,counterparty,contract,organization,subconto,item,warehouse
FEATURE_BASELINE_WINDOW_HOURS=24
ANOMALY_STALE_REFRESH_THRESHOLD_HOURS=6
FEATURE_ENTITY_SCAN_LIMIT=200000
RISK_MEDIUM_THRESHOLD=0.45
RISK_HIGH_THRESHOLD=0.75
RISK_ANOMALY_SCAN_LIMIT=5000

1
.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
*.dt filter=lfs diff=lfs merge=lfs -text

10
.gitignore vendored Normal file
View File

@ -0,0 +1,10 @@
.env
.venv/
__pycache__/
*.pyc
logs/*.xml
logs/*.json
logs/*.txt
tmp/*
.pytest_cache/
**/node_modules/

324
IN/1S_MCP_Toolkit_TZ.md Normal file
View File

@ -0,0 +1,324 @@
# 1S MCP Toolkit — ТЗ на установку и первичный PoC
## 1. Контекст и цель
Нужно поднять **локальный, изолированный runtime-мост** к уже существующей 1С-базе без передачи данных во внешние сервисы и без изменения операционной логики бухгалтерии.
Цель этого этапа:
- проверить, можно ли использовать **1c-mcp-toolkit** как **живой read-only semantic bridge** рядом с существующей 1С;
- получить не только доступ к OData, но и более глубокий runtime-доступ к метаданным, объектам и запросам;
- подтвердить, что мост можно запустить **без merge чужой тяжёлой конфигурации** в базу и без ломки клиентского контура;
- проверить, подходит ли этот инструмент как основа для:
- runtime-анализа бухгалтерии;
- фоновых аналитических прогонов;
- интерактивных запросов ассистента;
- построения полной семантической картины по документам, движениям, счетам, субконто и связям.
Ожидаемый результат этапа:
- поднят локальный экземпляр **1c-mcp-toolkit**;
- подтверждён запуск на нашей платформе 1С;
- проверены read-only методы;
- подтверждено или опровергнуто, что toolkit пригоден как основной runtime-мост.
---
## 2. Почему рассматривается именно 1c-mcp-toolkit
`ROCTUP/1c-mcp-toolkit` позиционируется как мост через внешнюю обработку `.epf`, с встроенным HTTP-сервером, без публикации 1С-сервера и без COM-соединения как обязательного требования. В репозитории также заявлены:
- MCP Server;
- local REST API;
- `execute_query`;
- `get_metadata`;
- `get_object_by_link`;
- анонимизация/токенизация сущностей.
Для нашей архитектуры это важно, потому что инструмент выглядит как:
- **минимально инвазивный**;
- **локально разворачиваемый**;
- **не требующий передачи данных наружу**;
- **не требующий тяжёлого внедрения в конфигурацию**;
- потенциально дающий более глубокий доступ, чем OData.
---
## 3. Наша архитектурная позиция
### 3.1. Что мы делаем
Мы строим **аналитическую надстройку** над существующей 1С.
### 3.2. Что мы не делаем
**Ни на каком этапе этого проекта мы не пишем в клиентскую 1С-базу операционные данные.**
**Мы не редактируем документы, не проводим документы, не меняем движения, не меняем регистры, не меняем бухгалтерскую логику и не берём на себя ответственность за операционный контур 1С.**
Это принципиальное и жёсткое ограничение проекта.
### 3.3. Допустимый формат работы
Допустим только:
- read-only доступ;
- выполнение запросов на чтение;
- чтение метаданных;
- чтение объектов по ссылкам;
- чтение связей и аналитик;
- локальный runtime-мост рядом с 1С.
---
## 4. Главный риск и как с ним работать
В toolkit есть не только read-only методы, но и возможность выполнения произвольного кода (`execute_code`) через интерфейс проекта. Это **неприемлемо** для нашего проекта как рабочий режим.
### Жёсткое правило:
Использовать **только read-only методы**, в первую очередь:
- `get_metadata`
- `get_object_by_link`
- `execute_query`
- и иные методы чтения, если они не меняют состояние базы.
### Категорически запрещено:
- `execute_code`
- любые write-операции
- любые операции, создающие или изменяющие объекты
- любые действия, меняющие состояние базы
### Требование к внедрению:
- организационно запретить использование write/mutation-вызовов;
- сетево не публиковать мост наружу;
- запускать только в локальном/изолированном контуре;
- ограничить доступ к endpoint только локальной машине или доверенной внутренней сети;
- использовать отдельного технического пользователя 1С.
---
## 5. Совместимость и текущая гипотеза
По документации проекта явно фигурирует совместимость с диапазоном `8.2.13+ / 8.3.25`. У нас платформа:
- **1С:Предприятие 8.3.27.1936**
- конфигурация: **Бухгалтерия предприятия, редакция 2.0 (2.0.67.20)**
Это значит:
- явного подтверждения `8.3.27` в документации нет;
- явного запрета на `8.3.27` тоже нет;
- значит нужен **практический smoke test**.
Текущая гипотеза:
**1c-mcp-toolkit — приоритетный PoC-кандидат, но совместимость с 8.3.27.1936 должна быть подтверждена на стенде.**
---
## 6. Что конкретно мы хотим получить от этой штуки
Инструмент должен, по возможности, дать:
### 6.1. Discovery / introspection
- список сущностей и типов;
- метаданные;
- структуру объектов;
- типы ссылок;
- поля и их типы;
- навигацию по объектам.
### 6.2. Runtime access
- чтение документа по ссылке;
- чтение движений/регистров;
- чтение проводок;
- чтение аналитик `subconto[1..3]`;
- выполнение произвольных **read-only** запросов на языке запросов 1С.
### 6.3. Semantic bridge
- возможность проходить цепочки:
- `document -> posting`
- `posting -> debit/credit account`
- `posting -> subconto[1..3]`
- `counterparty -> contract -> documents`
- `account -> movements -> balance`
### 6.4. Privacy / masking
- анонимизация сущностей, если нужно для AI-слоя;
- токенизация чувствительных идентификаторов без передачи наружу.
---
## 7. Что должен сделать Codex
### 7.1. Подготовка
1. Скачать репозиторий:
- <https://github.com/ROCTUP/1c-mcp-toolkit>
2. Изучить:
- `README.md`
- `README_FULL.md`
- `ANONYMIZATION.md`
3. Зафиксировать:
- способ запуска;
- требуемые компоненты;
- какой файл `.epf` нужен для запуска;
- какой локальный endpoint должен подняться.
### 7.2. Проверка состава репозитория
Codex должен определить:
- где лежит финальный `.epf` или как он собирается;
- нужен ли предварительный build;
- нужен ли OneScript / .NET / Node / Python runtime вокруг проекта;
- какие environment variables обязательны.
### 7.3. Подготовка стенда
Codex должен подготовить:
- локальную инструкцию запуска под нашу Windows-машину;
- чеклист запуска рядом с уже существующей 1С;
- список того, что пользователь должен сделать руками в 1С, если это необходимо.
### 7.4. Запуск PoC
Codex должен:
1. Поднять toolkit локально;
2. Проверить, что endpoint отвечает;
3. Проверить read-only методы;
4. Провести smoke test по метаданным;
5. Провести test query;
6. Проверить пригодность для наших бухгалтерских цепочек.
---
## 8. Предварительные ограничения для Codex
Codex **не должен**:
- пытаться встроить toolkit в конфигурацию 1С без явной необходимости;
- включать write-режимы;
- использовать `execute_code`;
- публиковать endpoint во внешний интернет;
- открывать доступ неограниченному числу клиентов;
- менять конфигурацию продовой 1С.
Codex **должен**:
- ориентироваться только на изолированный контур;
- трактовать проект как read-only analytics bridge;
- сначала подтвердить локальный запуск;
- все сомнительные действия помечать как `manual required`.
---
## 9. Практический план установки
### Этап A. Подготовка артефактов
1. Клонировать репозиторий.
2. Найти финальный `.epf` или build-инструкцию.
3. Проверить наличие:
- примеров запуска;
- конфигурации endpoint;
- описания REST/MCP режима.
### Этап B. Локальный запуск
1. Открыть `.epf` в нашей тестовой 1С.
2. Попробовать поднять встроенный сервер toolkit.
3. Зафиксировать:
- URL;
- порт;
- режим запуска;
- тип авторизации.
### Этап C. Smoke test
Проверить:
- `get_metadata`
- `get_object_by_link`
- `execute_query` на безопасном select
- наличие анонимизации
- отсутствие необходимости публикации 1С через IIS для базового запуска, если toolkit реально работает через свой внутренний сервер
### Этап D. Семантический PoC
Проверить, можно ли через toolkit достать:
- документ + движения;
- счёт + проводки;
- `subconto[1..3]`;
- реальное сальдо и его расшифровку;
- метаданные по нужным бухгалтерским объектам.
---
## 10. Что считать успехом
### Минимальный успех
- toolkit запускается на 8.3.27.1936;
- endpoint отвечает локально;
- `get_metadata` работает;
- `execute_query` на чтение работает.
### Рабочий успех
- toolkit позволяет читать нужные бухгалтерские сущности;
- toolkit позволяет проходить критические цепочки;
- toolkit может использоваться как runtime semantic bridge рядом с OData.
### Провал PoC
- toolkit не стартует на 8.3.27;
- toolkit требует неприемлемых модификаций 1С;
- toolkit не даёт read-only универсального доступа;
- toolkit usable only through dangerous mutation/code paths.
---
## 11. Deliverables от Codex
Codex должен вернуть:
1. `toolkit_inventory.md`
- состав проекта
- способ запуска
- файлы и зависимости
2. `toolkit_install_runbook.md`
- пошаговая инструкция запуска под наш стенд
3. `toolkit_smoke_test_report.md`
- что завелось
- что не завелось
- ошибки
- версия 1С
- endpoint
4. `toolkit_semantic_probe_report.md`
- что доступно по метаданным
- какие цепочки реально читаются
- где остались провалы
5. `toolkit_decision_note.md`
- verdict:
- `adopt`
- `adopt with restrictions`
- `reject`
---
## 12. Отдельный акцент для Codex
Нужно смотреть на проект **не как на “чат-надстройку”**, а как на **универсальный runtime-мост**.
Нас интересует не “умеет ли он ответить на 5 сценариев”, а:
- даёт ли он живой доступ к актуальной 1С;
- даёт ли он runtime-метаданные;
- даёт ли он доступ к семантике;
- можно ли его использовать как базовый нижний слой для:
- онтологии;
- графа связей;
- фоновой аналитики;
- ассистента.
---
## 13. Ссылки
- GitHub: <https://github.com/ROCTUP/1c-mcp-toolkit>
- README FULL: <https://github.com/ROCTUP/1c-mcp-toolkit/blob/main/README_FULL.md>
- Anonymization: <https://github.com/ROCTUP/1c-mcp-toolkit/blob/main/ANONYMIZATION.md>
---
## 14. Резюме
На текущем этапе `1c-mcp-toolkit`**лучший кандидат для первого серьёзного PoC** под runtime semantic bridge рядом с существующей 1С, потому что он обещает:
- локальный запуск;
- минимальную инвазивность;
- read-oriented runtime access;
- query/metadata layer;
- анонимизацию.
Но это всё должно быть **доказано практическим запуском на нашей версии 1С**.
Главное правило этапа:
**никаких write/mutation-операций, никакого изменения операционного контура 1С, только read-only аналитическая надстройка.**

326
IN/1с-план апи.md Normal file
View File

@ -0,0 +1,326 @@
Да, теперь картина понятная.
У тебя сейчас:
* **1С:Предприятие 8.3.27**
* **Бухгалтерия предприятия, редакция 2.0**
* база в режиме **файловая**
* клиент **толстый**
* каталог базы: `x:\1C\База бухгалтерии`
Это значит: **быстрее всего вам идти через публикацию базы на веб-сервере и включение стандартного OData/REST**, а потом уже поверх этого делать свой backend. Платформа 1С умеет автоматически формировать REST-интерфейс после публикации решения на веб-сервере; через него можно получать списки документов, справочников, записей регистров, в том числе с фильтрами, а также читать/создавать/изменять объекты. 1С прямо называет автоматически генерируемый REST/OData основным инструментом интеграции со сторонними системами. ([v8.1c.ru][1])
## Что делать дальше
### План на 2 этапа
**Этап А — быстро получить доступ к данным без ручных выгрузок**
Поднимаете **OData/REST**.
**Этап Б — сделать нормальный боевой коннектор**
Поверх OData делаете **свой Node-сервис**, который:
* тянет данные по расписанию,
* хранит слепки и дельты,
* нормализует 1С-структуру,
* отдаёт уже удобное API вашему ассистенту.
А когда станет ясно, какие именно бухгалтерские сценарии нужны, добавляете в 1С **HTTP-сервисы** для кастомных запросов. 1С поддерживает создание собственных HTTP-сервисов; они удобны как лёгкие RPC/REST-эндпоинты, где вы сами формируете ответ встроенным языком. ([v8.1c.ru][2])
---
# Практический пошаговый план
## Шаг 1. Зафиксировать цель интеграции
Сразу не делать “полный двусторонний API”.
На старте нужен режим:
**read-only + синхронизация в вашу систему**
То есть:
* ничего в 1С не пишем;
* только читаем;
* строим внешний аналитический/операционный слой.
Это резко снижает риск.
---
## Шаг 2. Проверить, есть ли доступ к Конфигуратору
Вот это критично.
Без Конфигуратора или 1С-разработчика, который умеет:
* публиковать базу на веб-сервере,
* настраивать доступ,
* включать OData,
* при необходимости добавлять HTTP-сервисы,
вы не поедете.
### Что нужно спросить у 1С-ника
Одной фразой:
> Нужно опубликовать файловую базу 1С на локальном веб-сервере и включить стандартный интерфейс OData на чтение.
Это уже предметная задача.
---
## Шаг 3. Поднять веб-публикацию базы
REST/OData в 1С работает **после публикации прикладного решения на веб-сервере**. Это прямо официальный базовый принцип. ([v8.1c.ru][1])
### Что это значит practically
Ваш 1С-ник должен:
* открыть **Конфигуратор**;
* опубликовать базу на **IIS** или **Apache**;
* включить публикацию стандартного интерфейса OData;
* выдать вам URL.
Обычно итог выглядит концептуально так:
* база была локальной файловой;
* стала доступна по HTTP внутри сети, например:
* `http://<server>/<base>/odata/...`
* или похожий путь, который сформирует публикация.
Я сейчас не даю точный шаблон URL, потому что он зависит от имени публикации и настроек веб-сервера.
---
## Шаг 4. Включить OData только для чтения и только для нужных сущностей
В Библиотеке стандартных подсистем у 1С есть отдельный механизм настройки доступа к данным через стандартный интерфейс OData. ([v8.1c.ru][3])
### Что открывать наружу на старте
Не всё подряд. Только:
* документы покупателей
* документы поставщиков
* банковские выписки / платежные документы
* кассовые документы
* журнал проводок / регистр бухгалтерии, если доступен через публикацию
* контрагенты
* договоры контрагентов
* номенклатура
* статьи затрат
* статьи ДДС
* прочие доходы и расходы
То есть тот минимум, который у тебя уже руками выгружался.
---
## Шаг 5. Проверить OData простым тестом
После публикации проверяете не “сложный сценарий”, а очень простой:
1. Открывается ли metadata/корень сервиса.
2. Возвращается ли список, например, контрагентов или документов.
3. Работают ли фильтры по дате.
Официально REST/OData в 1С рассчитан как раз на получение списков документов, справочников и записей регистров с фильтрами. ([v8.1c.ru][1])
---
## Шаг 6. Между 1С и ассистентом ставите свой backend
Вот это обязательно.
### Архитектура
**1С (OData) → ваш sync-service → Postgres / storage → ассистент / UI**
### Почему так
Потому что нельзя строить продукт так:
* UI каждый раз бьёт прямо в 1С;
* ассистент напрямую лезет в 1С;
* куча запросов летит в боевую бухгалтерскую базу.
Это будет и хрупко, и больно.
### Что делает sync-service
* хранит `last_sync_at`
* тянет данные пачками
* складывает сырые JSON/таблицы
* нормализует в свои сущности
* считает дельты
* строит витрины:
* документы
* движения
* контрагенты
* объяснение суммы
* хвосты
* изменения за день
---
## Шаг 7. Не делать realtime на старте
Это важно.
На старте не нужен “живой поток каждую секунду”.
Нужен режим:
* раз в 5 минут,
* или раз в 15 минут,
* или по кнопке “обновить”.
У 1С всё есть для интеграции и синхронизации по расписанию, и БСП отдельно поддерживает синхронизацию по требованию и в автоматическом режиме по расписанию. ([v8.1c.ru][3])
Для бухгалтерии этого обычно достаточно.
---
## Шаг 8. После OData — добавить HTTP-сервисы под умные сценарии
Когда базовый доступ заработает, вы быстро упрётесь в то, что OData даёт “сырые объекты”, а вам нужны бизнес-запросы:
* дай документы, изменённые после последней синхронизации;
* дай расхождения по контрагентам;
* дай хвосты перед закрытием;
* дай цепочку, из чего сложилась сумма;
* дай нетипичные ручные операции.
Вот под это уже делаются **HTTP-сервисы внутри 1С**, потому что 1С позволяет создавать произвольные HTTP-сервисы и самим формировать тело ответа. ([v8.1c.ru][2])
---
# Что конкретно делать тебе сейчас
Вот совсем без воды.
## Прямо сейчас
Составь и передай 1С-разработчику такой список задач:
### ТЗ-минимум
1. **Опубликовать файловую базу 1С на локальном веб-сервере**.
2. **Включить стандартный REST/OData-интерфейс**.
3. **Настроить отдельного пользователя только на чтение** для интеграции.
4. **Ограничить опубликованные сущности** стартовым списком:
* контрагенты
* договоры контрагентов
* номенклатура
* статьи затрат
* статьи ДДС
* документы покупателей
* документы поставщиков
* банковские документы
* кассовые документы
* проводки / регистры бухучёта, если доступны через публикацию
5. **Отдать тестовый URL и учётку** для проверки из внешней системы.
### ТЗ-второй очереди
6. Добавить **HTTP-сервисы** под кастомные выборки:
* изменения за период,
* дельта после последней синхронизации,
* список незакрытых хвостов,
* проводки, повлиявшие на сумму,
* документы закрытия месяца.
---
# Что будете делать вы в Node.DC
## Первая версия коннектора
Делаете сервис с 4 методами:
* `syncReferenceData()`
тянет справочники
* `syncDocuments(from, to)`
тянет документы по периоду
* `syncAccountingMovements(from, to)`
тянет движения/проводки
* `getDelta(since)`
тянет только изменения с момента последней синхронизации
### В хранилище
Минимум такие таблицы:
* `onec_counterparties`
* `onec_contracts`
* `onec_items`
* `onec_cashflow_articles`
* `onec_cost_articles`
* `onec_documents_raw`
* `onec_postings_raw`
* `onec_sync_log`
---
# Какой результат должен быть через 12 дня работы
Не “полный AI для бухгалтерии”.
А вот такой checkpoint:
### Должно заработать
* автоматически читаем из 1С контрагентов;
* автоматически читаем документы за день/месяц;
* автоматически читаем проводки/движения;
* без ручных Excel;
* данные падают в ваше хранилище;
* можно сделать первый экран:
* “что изменилось с последнего sync”
* “какие документы/движения добавились”
* “какие контрагенты/суммы затронуты”
Если этого нет — дальше думать про ассистента рано.
---
# Самая короткая стратегия
## Что не делать
* не строить ассистента поверх ручных выгрузок;
* не лезть сразу в онтологию и сложные смыслы;
* не пытаться “читать SQL-таблицы 1С в лоб”.
## Что делать
* публикация базы,
* OData,
* read-only пользователь,
* sync-service,
* своё хранилище,
* потом HTTP-сервисы под умные кейсы.
---
# Твой следующий практический ход
Открой у себя и проверь, есть ли вообще **Конфигуратор 1С** для этой базы.
Если есть — это следующий рабочий шаг.
Если его нет, значит нужен человек с доступом к конфигуратору/администрированию.
Если хочешь, я следующим сообщением соберу тебе **готовое короткое ТЗ для 1С-разработчика** — буквально списком, что именно ему нужно сделать, без лишней теории.
[1]: https://v8.1c.ru/platforma/rest-interfeys/ "REST интерфейс | Интеграция - платформа 1С:Предприятие"
[2]: https://v8.1c.ru/platforma/http-servisy/ "HTTP-сервисы | Интеграция - платформа 1С:Предприятие"
[3]: https://v8.1c.ru/tekhnologii/standartnye-biblioteki/1s-biblioteka-standartnykh-podsistem/integratsiya-s-drugimi-prilozheniyami-i-podsistemami/ "Интеграция с другими приложениями и подсистемами | 1С:Библиотека стандартных подсистем"

View File

@ -0,0 +1,501 @@
# ТЗ — Accounting Analytics Layer Architecture
## 1. Назначение документа
Документ фиксирует архитектуру следующего слоя системы после успешного PoC runtime-моста к 1С.
Цель этапа:
- спроектировать **полный аналитический контур** поверх живого read-only runtime bridge;
- развести **live-доступ к 1С** и **тяжёлую аналитику**;
- исключить ошибочную архитектуру, при которой LLM в реальном времени пытается анализировать всю бухгалтерию напрямую через 1С;
- определить слои, режимы работы, refresh-модель, canonical schema, anomaly engine и роль ассистента.
---
## 2. Контекст
На предыдущем этапе подтверждён рабочий runtime semantic bridge к 1С:
- read-only контур поднят;
- доказаны цепочки `document -> posting -> account`;
- доказаны цепочки `posting -> subconto[1..3]`;
- доказана расшифровка saldo через движения;
- bridge принят со статусом `adopt with restrictions`.
Это означает:
- живой доступ к бухгалтерскому смыслу есть;
- но **полный аналитический контур нельзя строить как чатовый runtime-проход по всей 1С**;
- нужен отдельный слой аналитики, кэша, baseline, anomaly detection и исторического анализа.
---
## 3. Главный архитектурный принцип
Система должна быть разделена на **разные режимы работы**, а не пытаться решать всё одной и той же цепочкой `LLM -> 1С -> JSON -> LLM`.
### 3.1. Режим A — Live Query
Используется для:
- точечных запросов;
- drill-down;
- проверки одного факта;
- открытия документа;
- расшифровки проводки;
- объяснения сальдо;
- прохода по конкретной цепочке связей.
### 3.2. Режим B — Batch Analytics
Используется для:
- анализа всего периода;
- анализа всех счетов;
- поиска аномалий;
- сравнения закрытых и открытых периодов;
- поиска поведенческих паттернов;
- выявления разрывов, дыр, хвостов и коллизий;
- исторического анализа по месяцам/кварталам/годам.
### 3.3. Режим C — Interactive Assistant
Используется для общения с пользователем:
- понимает намерение;
- определяет, нужен live-доступ или уже есть готовый аналитический результат;
- маршрутизирует запрос;
- объясняет результат человеку.
---
## 4. Целевая архитектура
## Layer 1. 1С Source Layer
Источник истины:
- документы;
- проводки;
- регистры;
- планы счетов;
- субконто;
- остатки;
- обороты;
- закрытые и открытые периоды.
### Жёсткое правило
**Ничего в клиентскую 1С не пишем.**
- не редактируем документы;
- не проводим документы;
- не меняем регистры;
- не меняем конфигурацию ради аналитики;
- не строим операционный write-layer.
---
## Layer 2. Runtime Semantic Bridge
Основной live-мост к 1С.
На текущем этапе:
- `1c-mcp-toolkit`
- только read-only
- только локально / в изоляции
- без `execute_code`
- только approved read methods
### Назначение слоя
- читать метаданные;
- выполнять безопасные read-only запросы;
- читать документы, объекты, движения, счета, субконто;
- обеспечивать live drill-down по конкретным фактам.
### Не назначение слоя
- не использовать как движок тяжёлой аналитики по всей компании в реальном времени;
- не использовать как единственное хранилище семантики;
- не использовать как вычислительный комбайн для whole-slice анализа.
---
## Layer 3. Extraction / Refresh Layer
Контролируемый слой извлечения данных из 1С в аналитическое хранилище.
### Задачи
- запускать штатные extraction jobs;
- читать данные из 1С пакетно и инкрементально;
- формировать canonical payloads;
- обновлять аналитическое хранилище;
- минимизировать нагрузку на live-runtime.
### Режимы извлечения
1. **Initial Historical Load**
- первичная загрузка истории;
- по месяцам / кварталам / годам;
- по закрытым периодам.
2. **Incremental Refresh**
- догрузка изменений по открытому периоду;
- обновление новых и изменившихся документов и движений.
3. **Targeted Refresh**
- загрузка конкретного счета;
- загрузка конкретного контрагента;
- загрузка конкретного периода;
- загрузка конкретного документа.
### Требование
Extraction layer должен быть:
- воспроизводимым;
- логируемым;
- перезапускаемым;
- устойчивым к частичным сбоям.
---
## Layer 4. Canonical Accounting Store
Нормализованное хранилище бухгалтерского смысла.
Это не “снапшот ради снапшота”, а основной аналитический слой.
### Базовые сущности
- `Document`
- `Posting`
- `RegisterMovement`
- `Account`
- `SubcontoEntity`
- `Counterparty`
- `Contract`
- `Organization`
- `Item`
- `Warehouse`
- `Period`
- `BalanceSnapshot`
- `TurnoverSnapshot`
- `EntityState`
- `AnomalySignal`
- `RiskPattern`
### Базовые связи
- `Document -> creates -> Posting`
- `Posting -> debit -> Account`
- `Posting -> credit -> Account`
- `Posting -> hasSubconto1 -> SubcontoEntity`
- `Posting -> hasSubconto2 -> SubcontoEntity`
- `Posting -> hasSubconto3 -> SubcontoEntity`
- `Posting -> belongsTo -> Organization`
- `Posting -> belongsToPeriod -> Period`
- `Counterparty -> hasContract -> Contract`
- `Account -> hasBalanceSnapshot -> BalanceSnapshot`
- `Period -> hasTurnoverSnapshot -> TurnoverSnapshot`
### Требование
Canonical store должен быть:
- семантически стабильным;
- независимым от сырой структуры 1С;
- пригодным для SQL/graph/pattern-анализа;
- пригодным для feed в feature/anomaly engine.
---
## Layer 5. Feature Store / Analytics Store
Слой производных признаков, baseline и аналитических индикаторов.
### Что здесь хранится
- признаки по счетам;
- признаки по контрагентам;
- признаки по документам;
- частотные поведенческие паттерны;
- baseline по периодам;
- отклонения от baseline;
- anomaly flags;
- drift signals;
- risk score;
- derived relation signals.
### Примеры признаков
- нетипичная корреспонденция счетов;
- новый тип субконто в периоде;
- резкий рост активности по счету;
- аномальное увеличение оборота;
- повторяющиеся нестандартные проводки;
- контрагент, резко изменивший тип операций;
- хвосты и незакрытые цепочки;
- проводки, не похожие на исторический baseline.
---
## Layer 6. Analytical Engine
Вычислительный слой, который считает и сравнивает, но не является LLM.
### Что делает engine
- rule-based проверки;
- статистические проверки;
- anomaly detection;
- baseline modelling;
- drift analysis;
- pattern mining;
- graph analysis;
- scoring;
- объяснение причинности через расчётные модели.
### Что engine не делает
- не общается с пользователем напрямую;
- не генерирует красивый текст ответа;
- не выполняет роль conversational assistant.
### Режимы работы
- по расписанию;
- по ручному запуску;
- по триггеру;
- по событию;
- по запросу от assistant-layer, если нет свежего результата.
---
## Layer 7. Assistant / Orchestrator Layer
LLM-слой и логика маршрутизации.
### Роль слоя
- принимать пользовательский вопрос;
- определять тип вопроса;
- решать, где лежит ответ:
- в analytics store,
- в live bridge,
- или нужен новый batch-run;
- объяснять результат человеку.
### Что LLM не должна делать
- не должна перебирать всю 1С в реальном времени;
- не должна сама быть anomaly engine;
- не должна получать целиком весь срез компании в промпт;
- не должна по каждому тяжёлому вопросу инициировать полное живое сканирование 1С.
### Что LLM должна делать
- классифицировать вопрос;
- маршрутизировать запрос;
- выбирать инструменты;
- суммировать результаты;
- превращать аналитические факты в понятный ответ.
---
## 5. Почему нельзя делать всё через live bridge
### Проблема
Если делать всё как:
`LLM -> toolkit -> 1С -> JSON -> LLM`
то на тяжёлых вопросах система будет:
- медленной;
- дорогой;
- хрупкой;
- плохо воспроизводимой;
- чувствительной к latency;
- зависимой от размера данных и количества round-trip операций.
### Особенно плохо это работает для запросов типа:
- “проанализируй весь срез на сегодня”;
- “найди все аномалии по всем счетам”;
- “сравни компанию за 10 лет по месяцам”;
- “найди все техкомпании-прокладки и аномальные паттерны”.
### Следствие
Live bridge должен использоваться **точечно**, а тяжёлый анализ должен идти через отдельный аналитический слой.
---
## 6. Как должна работать система на практике
### Сценарий 1. Точечный вопрос
Пользователь:
> Что по счёту 68.02?
Маршрут:
1. assistant проверяет analytics store;
2. если есть готовый balance/anomaly context — использует его;
3. если нужен drill-down — идёт через runtime bridge;
4. возвращает объяснение.
### Сценарий 2. Массовый анализ
Пользователь:
> Проанализируй весь текущий срез и найди аномалии.
Маршрут:
1. assistant не идёт напрямую в 1С по всей базе;
2. проверяет, есть ли свежий batch-run;
3. если есть — отдаёт summary;
4. если нет — запускает analytic job;
5. после завершения показывает результаты и даёт drill-down.
### Сценарий 3. Историческое сравнение
Пользователь:
> Сравни поведение компании по кварталам за 5 лет.
Маршрут:
1. используются historical baseline и snapshots;
2. analytical engine считает изменения;
3. assistant объясняет выводы.
---
## 7. Модель обновления данных
### 7.1. Закрытые периоды
- загружаются один раз;
- считаются эталоном;
- меняются только по отдельному регламенту.
### 7.2. Открытые периоды
- обновляются инкрементально;
- по расписанию;
- по событию;
- по ручной кнопке;
- по запросу при необходимости.
### 7.3. Refresh granularity
Поддержать:
- period-level refresh;
- account-level refresh;
- document-level refresh;
- counterparty-level refresh.
---
## 8. Что хранить и что не хранить
### Хранить
- canonical facts;
- balance snapshots;
- turnover snapshots;
- derived features;
- anomaly signals;
- relation graph edges;
- baseline state;
- analytical summaries.
### Не хранить бездумно
- полный сырой JSON от каждого live-вызова навсегда;
- дубли всей 1С на каждый пользовательский запрос;
- полный срез компании в контексте LLM.
---
## 9. Технологический стек (рекомендуемый уровень)
### Runtime bridge
- `1c-mcp-toolkit`
### Extraction / jobs
- Python
- job runner / scheduler
- очередь задач по необходимости
### Canonical store
- PostgreSQL как основной store
### Heavy analytics
- DuckDB / Polars / Pandas для batch-прогонов
- при росте объёмов — возможен переход на более тяжёлый аналитический backend
### Assistant layer
- OpenAI / локальная LLM
- tool calling
- оркестрация между analytics store и runtime bridge
---
## 10. Ограничения безопасности
### Жёсткие ограничения
- только read-only доступ к 1С;
- никакого `execute_code`;
- никакой записи в 1С;
- никакой модификации конфигурации продовой 1С;
- endpoint не публикуется наружу;
- доступ только из локального / доверенного контура;
- отдельный технический пользователь 1С.
### Важно
Этот проект является **аналитической надстройкой**, а не операционным контуром.
---
## 11. Этапы реализации
## Stage 1. Фиксация runtime bridge
- зафиксировать `1c-mcp-toolkit` как основной live semantic bridge;
- описать approved methods;
- зафиксировать guardrails.
## Stage 2. Canonical schema
- спроектировать canonical accounting schema;
- описать сущности, связи, ключи и snapshots.
## Stage 3. Historical loader
- реализовать initial historical load;
- загрузить закрытые периоды;
- собрать baseline history.
## Stage 4. Incremental refresh
- реализовать обновление открытого периода;
- определить правила инкрементальной синхронизации.
## Stage 5. Feature / anomaly engine
- реализовать rule engine;
- реализовать baseline comparison;
- реализовать anomaly signals и risk patterns.
## Stage 6. Assistant orchestration
- реализовать маршрутизацию вопроса;
- выбор между analytics store и live bridge;
- сформировать интерактивный слой ответов.
---
## 12. Deliverables
На выходе этапа должны появиться:
1. `accounting_canonical_schema.md`
2. `analytics_store_design.md`
3. `refresh_strategy.md`
4. `historical_load_plan.md`
5. `incremental_refresh_plan.md`
6. `anomaly_engine_spec.md`
7. `assistant_orchestration_spec.md`
8. `security_guardrails_readonly.md`
---
## 13. Критерии приёмки
Этап считается успешным, если:
1. live bridge используется только для точечных запросов и drill-down;
2. тяжёлая аналитика вынесена в отдельный analytical layer;
3. закрытые периоды загружаются как baseline;
4. открытые периоды обновляются инкрементально;
5. аномалии и паттерны считаются вне LLM;
6. assistant отвечает быстро на типовые вопросы за счёт analytics store;
7. assistant может идти в live bridge только по необходимости;
8. система не требует write-доступа в 1С.
---
## 14. Резюме
`1c-mcp-toolkit` решает важнейшую задачу:
он даёт **живой runtime semantic bridge** к 1С.
Но он **не должен** быть единственным аналитическим слоем.
Полный аналитический контур строится так:
**1С -> Runtime Semantic Bridge -> Canonical Accounting Store -> Feature / Anomaly Engine -> Assistant Orchestrator**
И рядом:
**1С -> OData Discovery Layer**
Именно такая архитектура:
- масштабируется;
- не убивает производительность;
- не делает LLM вычислительным ядром;
- позволяет анализировать большие срезы, периоды и паттерны;
- сохраняет живой доступ к актуальной 1С для точечных drill-down сценариев.

View File

@ -0,0 +1,289 @@
Да, теперь уже видно не просто то, что “онтология просажена”, а **почему именно она просажена и как именно её чинить**.
Главный вывод после догрузки: у вас ломается не “граф вообще”, а **семаническая нормализация ссылок**. Текущий mapping-слой построен слишком грубой эвристикой: все ссылки превращаются в один relation `reference`, а `target_entity` определяется почти только по подстроке в имени поля. Из-за этого система формально строит links, но содержательно не понимает, кто есть кто. Отсюда и 1016 `Unknown`-relations из 2618, при semantic coverage всего 61.19%, хотя link coverage 100%.
## Где именно источник поломки
Сейчас правило такое:
* если поле оканчивается на `_Key`, `ref`, или выглядит как GUID — оно становится link;
* relation всегда один: `reference`;
* target_entity угадывается по имени поля;
* если не угадали по нескольким словам вроде `контраг`, `договор`, `счет`, `организ`, `документ`, то ставится `Unknown`.
Это и есть корневая ошибка архитектуры. Для бухгалтерского контура такой подход слишком бедный, потому что:
* `Recorder` — это не просто “reference”, а **источник движения / документ-регистратор**;
* `Ref` в журнале — это не просто “reference”, а **journal entry points to concrete document**;
* оставщик_Key` и окупатель_Key` — не generic refs, а **роли контрагентов**;
* `Ответственный_Key` — не generic ref, а **actor / responsible person**;
* `Валюта_Key` — это вообще не `Document`, а отдельная сущность валюты;
* `Склад_Key`, одразделениеДт_Key`, `ФизЛицо_Key`, `СтатьяДвиженияДенежныхСредств_Key` требуют собственных классов, а не падения в `Unknown`.
## Что ломается на реальных примерах
### 1) `Recorder` в регистрах НДС
В НДС-регистрах `Recorder` приходит вместе с `Recorder_Type`, например:
* `Recorder_Type = StandardODATA.Document_ПоступлениеТоваровУслуг`
* либо `Recorder_Type = StandardODATA.Document_СчетФактураПолученный`
* либо `Recorder_Type = StandardODATA.Document_РеализацияТоваровУслуг`.
Но в links это всё равно уходит как `target_entity = Unknown`.
Это критично, потому что для бухгалтерской аналитики регистр без корректного регистратора — почти полуслепая запись. Вы теряете причинную цепочку:
**движение регистра → документ-регистратор → контрагент/договор/товар/сумма**.
### 2) `СчетФактура` ошибочно уезжает в `Account`
В НДС-регистрах поле `СчетФактура` по смыслу ссылается на документ, а не на бухгалтерский счёт. Но из-за эвристики “если в имени есть `счет``Account`” оно типизируется как `Account`. Это уже не просто unknown, а **ложноположительное сопоставление**.
То есть у вас часть связей не просто потеряна, а неверно искажена.
### 3) `Ref` в журналах документов
В `DocumentJournal_БанковскиеВыписки` есть:
* `Ref`
* `Ref_Type = StandardODATA.Document_СписаниеСРасчетногоСчета`
или `...ПоступлениеНаРасчетныйСчет`.
Но link по `Ref` уходит в `Unknown`, хотя это должен быть прямой указатель на конкретный документ. Именно поэтому журналы дают много unknown и source_id=unknown.
### 4) Валюта маппится неверно
В `СписаниеСРасчетногоСчета` поле `ВалютаДокумента_Key` в одном из образцов уходит в `Document`, просто потому что в имени есть слово `документа`, хотя это ссылка на валюту. Аналогичная проблема в журналах по `Валюта_Key`, где поле уходит в `Unknown`.
### 5) Ролевые контрагенты не разведены
В регистрах есть оставщик_Key`, окупатель_Key`, оговорКонтрагента_Key`.
Сейчас:
* оставщик_Key` и окупатель_Key` часто падают в `Unknown`,
* оговорКонтрагента_Key` обычно распознаётся как `Contract`,
но без явной роли в relation.
В результате граф знает, что “что-то связано с контрагентом/договором”, но не знает:
* это supplier или buyer,
* это основной контрагент документа или договор расчётов,
* это связь документа, журнала или регистра.
### 6) `Ответственный_Key`, `ФизЛицо_Key`, `Склад_Key`, `СтатьяДвиженияДенежныхСредств_Key`
Эти поля в топе проблемных, потому что для них вообще нет соответствующих сущностей в базовой canonical-модели. В текущем ядре у вас classes: `Organization`, `Counterparty`, `Contract`, `Account`, `Subconto`, `Document`, `Posting`, `RegisterMovement`, `Period`, плюс `CanonicalEntity`. Но нет нормальных классов для people/employee, warehouse, currency, cashflow article, department, item/product. Поэтому всё это закономерно валится в `Unknown`.
## Значит ли это, что надо “подкручивать только relation rules”?
Нет. Тут нужно чинить **сразу три слоя**:
### A. Расширять canonical classes
Минимально надо добавить:
* `EmployeeOrUser` / `ResponsiblePerson`
* `Currency`
* `Warehouse`
* `CashflowArticle`
* `Department`
* `Individual`
* `Item` / `Nomenclature`
* `BankAccount`
* `TaxRegisterRecord` или более общий `RegisterRecord`
* `InvoiceDocument` / `FacturaDocument` как подтип документа, если хотите потом делать объяснения по НДС аккуратнее.
Без этого вы можете переписать relation rules хоть десять раз, но часть полей всё равно некуда будет положить.
### B. Уходить от одного relation `reference`
Нужен не один `reference`, а словарь осмысленных relations. Минимальный стартовый набор я бы делал такой:
**Документы / журналы**
* `journal_refers_to_document`
* `document_belongs_to_organization`
* `document_has_counterparty`
* `document_has_contract`
* `document_has_currency`
* `document_has_warehouse`
* `document_has_responsible`
* `document_has_cashflow_article`
* `document_has_bank_account`
**Регистры**
* `register_recorded_by_document`
* `register_relates_to_supplier`
* `register_relates_to_buyer`
* `register_relates_to_invoice`
* `register_relates_to_vat_account`
* `register_relates_to_contract`
* `register_relates_to_organization`
**Финансовые документы**
* `payment_relates_to_counterparty`
* `payment_relates_to_bank_account`
* `payment_relates_to_cashflow_article`
* `payment_relates_to_individual`
* `payment_relates_to_department`
**Строки табличных частей**
* `document_line_has_item`
* `document_line_has_account`
* `document_line_has_vat_account`
* `document_line_has_expense_account`
* `document_line_has_income_account`
### C. Менять сам принцип типизации
Не по имени поля alone, а по комбинации:
1. `source_entity`
2. `source_field`
3. `*_Type` рядом
4. наличие `navigationLinkUrl`
5. контекст набора полей вокруг записи.
Именно это даст переносимость между разными 1С-контурами, а не ручную подгонку под июнь 2020, чего у вас как раз требует ТЗ.
## Как я бы правил правила маппинга
### Правило 1. `*_Type` имеет приоритет над эвристикой имени
Если есть:
* `Recorder_Type`
* `Ref_Type`
* `СчетФактура_Type`
* окументОплаты_Type`
* и т.п.,
то target_entity нужно определять не по имени поля, а по значению type.
Например:
* `StandardODATA.Document_*``Document`
* `StandardODATA.Catalog_Контрагенты``Counterparty`
* `StandardODATA.Catalog_Склады``Warehouse`
* `StandardODATA.Catalog_Валюты``Currency`
* `StandardODATA.Catalog_ФизическиеЛица``Individual`
* `StandardODATA.Catalog_СтатьиДвиженияДенежныхСредств``CashflowArticle`.
Это автоматически лечит большую часть `Recorder`, `Ref` и typed-полей.
### Правило 2. Для конкретных полей — словарь приоритетных semantic mappings
Нужен явный field dictionary, например:
* `Recorder` → relation `register_recorded_by_document`, target `Document`
* `Ref` в `DocumentJournal_*` → relation `journal_refers_to_document`, target `Document`
* оставщик_Key` → relation `*_relates_to_supplier`, target `Counterparty`
* окупатель_Key` → relation `*_relates_to_buyer`, target `Counterparty`
* `Ответственный_Key` → relation `*_has_responsible`, target `ResponsiblePerson`
* `Валюта_Key` / `ВалютаДокумента_Key` → relation `*_has_currency`, target `Currency`
* `Склад_Key` → relation `*_has_warehouse`, target `Warehouse`
* `СтатьяДвиженияДенежныхСредств_Key` → relation `*_has_cashflow_article`, target `CashflowArticle`
* `ФизЛицо_Key` → relation `*_relates_to_individual`, target `Individual`
* анковскийСчет_Key` / `СчетОрганизации_Key` → target `BankAccount`.
### Правило 3. `СчетФактура` — специальный case
Поле `СчетФактура` нельзя по общему правилу отправлять в `Account`.
Если рядом есть `СчетФактура_Type = StandardODATA.Document_*`, то это relation к документу:
* `register_relates_to_invoice`
* target `Document`.
### Правило 4. Нулевые GUID не создавать как обычные бизнес-связи
`00000000-0000-0000-0000-000000000000` сейчас плодит мусорные links. Их лучше:
* либо не писать в canonical links вообще,
* либо писать как `null_reference` / `empty_reference` технического типа, отдельно от семанических relations.
Это сразу уменьшит шум в графе и не будет создавать фальшивых “связей с нулевым контрагентом”.
### Правило 5. `source_id` для register records нельзя оставлять `unknown`
Для регистров, где нет `Ref_Key`, нужно собирать составной ключ, например:
* `source_entity + Recorder + Recorder_Type + LineNumber + Period`
или аналог.
Иначе у вас 358 записей с `source_id=unknown`, и это разрушает стабильное переиспользование сущности при повторных загрузках.
## Приоритеты ремонта по очереди
### Приоритет 1 — `Recorder`, `Ref`, `СчетФактура`, оставщик_Key`, окупатель_Key`
Это даст самый большой выигрыш по semantic coverage, потому что именно эти поля сейчас массово ломают регистры, журналы и ключевые документы.
### Приоритет 2 — `Ответственный_Key`, `Валюта_Key`, `ВалютаДокумента_Key`
Это быстро снижает долю `Unknown` в журналах и платежных документах, плюс улучшает explainability по “кто оформлял / в какой валюте”.
### Приоритет 3 — `СтатьяДвиженияДенежныхСредств_Key`, `ФизЛицо_Key`, `Склад_Key`, одразделениеДт_Key`
Это уже следующий слой качества, особенно для операционных и зарплатных/кассовых сценариев.
## Что это даст benchmarkу
Если вы сделаете только этот ontology repair, без переписывания всего остального, я ожидаю:
* существенное падение `unknown_relations`;
* рост semantic coverage заметно выше текущих 61%;
* лучшую связность сценариев `document ↔ register ↔ journal`;
* меньше ошибочных ответов по cross-entity вопросам;
* меньше ложной уверенности в store-only ответах, потому что router будет видеть более богатую и корректную семантику.
Особенно выиграют вопросы вида:
* связать документ и проводки,
* связать контрагента, договор и документ,
* объяснить происхождение записи НДС,
* пройти от банковской выписки к первичному документу.
## Практический план, что делать прямо сейчас
Я бы формализовал следующий этап так:
**Шаг 1. Переписать mapper architecture**
* ввести `entity_type_resolver`
* ввести `relation_resolver`
* ввести `null_guid_filter`
* ввести `composite_source_id_builder` для register records.
**Шаг 2. Сделать словарь field semantics**
Отдельная таблица:
* `source_entity_pattern`
* `field_name`
* `optional_type_field`
* `target_class`
* `relation_name`
* `allow_zero_guid`
* `priority`.
Это даст переносимость и управляемость.
**Шаг 3. Расширить canonical classes**
Хотя бы теми классами, которые сейчас массово падают в `Unknown`.
**Шаг 4. Переингестить июнь 2020**
После изменения rules — заново посадить slice и пересчитать:
* unknown_relations,
* semantic_coverage_pct,
* relation_types_total,
* top problematic fields.
**Шаг 5. Только потом — дочищать router**
Потому что router сейчас частично страдает вторично: он опирается на недотипизированный граф. Сначала надо дать ему нормальную семаническую подложку.
## Мой жёсткий вывод
Сейчас у вас не “плохая онтология”, а **слишком примитивный canonical mapper для домена уровня 1С-бухгалтерии**. Он годится как стартовый универсальный мост, но не как слой, который должен объяснять бухгалтерскую причинность. Проблема не в snapshotе июня 2020 — он как раз полезен и достаточно показателен. Проблема в том, что mapping-слой ещё не различает:
* тип ссылки,
* бизнес-роль ссылки,
* typed reference,
* пустую ссылку,
* и специальные 1С-паттерны вроде `Recorder/Ref/СчетФактура`.
Следующим сообщением я могу собрать тебе уже **прямо готовое ТЗ на ремонт ontology/mapping-слоя**: с таблицей новых canonical classes, таблицей relation types и таблицей правил для `Recorder`, `Ref`, `Поставщик`, `Покупатель`, `Ответственный`, `Валюта`, `Склад`, `СтатьяДДС`.

View File

@ -0,0 +1,673 @@
# ТЗ: базовая онтология 1С бухгалтерии + проверка полноты доступа и цепочек связей
**Версия:** 0.1
**Дата:** 2026-03-22
**Контур:** тестовая копия 1С, опубликованная по OData
**База:** `X:\1C\База бухгалтерии`
**Публикация:** `http://localhost/buh_test/odata/standard.odata/`
---
## 1. Цель этапа
Нужно решить две задачи одновременно:
1. Сформировать **базовую каноническую онтологию бухгалтерского контура 1С** для будущего ассистента, графа знаний и MCP-слоя.
2. Провести **жёсткую проверку полноты и связности текущего OData-доступа**, чтобы понять:
- достаточно ли его для MVP и последующего роста;
- или OData годится только как быстрый транспорт / discovery-слой;
- или нужно переходить к более глубокому способу доступа к данным 1С.
Ключевой принцип этапа: нас интересует **не перечень сущностей сам по себе**, а возможность восстановить **операционный бухгалтерский смысл**:
- какой документ породил движение;
- какие проводки легли на какие счета;
- какие субконто, контрагенты, договоры, организации и банковские счета участвуют в цепочке;
- можно ли объяснять остатки, обороты, сальдо, расхождения и основания операций.
---
## 2. Что уже подтверждено на текущем шаге
### 2.1. Инфраструктура поднята
- 1С опубликована на IIS.
- Веб-клиент открывается.
- OData endpoint отвечает.
- `$metadata` отвечает и отдаёт модель сущностей.
### 2.2. По текущему инвентарю уже видны ключевые семейства объектов
Подтверждено наличие:
- `Document_*`
- `Catalog_*`
- `AccountingRegister_Хозрасчетный`
- `AccumulationRegister_*`
- `ChartOfAccounts_Хозрасчетный`
- `InformationRegister_*`
### 2.3. По снапшоту entity sets видно, что модель большая и не пустая
Сейчас опубликовано:
- всего сущностей: **1189**
- `Document`: **517**
- `Catalog`: **162**
- `InformationRegister`: **270**
- `AccumulationRegister`: **102**
- `AccountingRegister`: **2**
- `ChartOfAccounts`: **1**
### 2.4. Уже есть признаки связности
В `$metadata` видны `NavigationProperty`, а значит модель содержит не только плоские entity sets, но и формализованные связи между объектами.
---
## 3. Главный вывод по идеологии
### 3.1. Не строим “онтологию всей 1С” как прямую копию entity sets
Это плохой путь, потому что:
- в 1С слишком много технических и вторичных сущностей;
- прямое зеркало OData быстро превратится в шум;
- ассистенту нужна не свалка OData-объектов, а понятный слой бухгалтерского смысла.
### 3.2. Строим двухслойную модель
#### Слой А. Каноническая онтология бухгалтерии
Это **главный рабочий слой**, на котором потом живут:
- граф связей;
- семантика для ассистента;
- поиск отклонений;
- сверки;
- explainability;
- MCP-инструменты.
#### Слой B. Mapping / adapter layer к 1С OData
Это слой соответствий:
- какие OData entity sets соответствуют каноническим сущностям;
- какие поля и ключи заполняют каноническую модель;
- какие связи можно восстановить напрямую, а какие только вычислением;
- где OData достаточно, а где нужен более глубокий коннектор.
---
## 4. Базовая онтология v0.1
Ниже — минимальный, но уже полезный состав сущностей, который должен закрыть бухгалтерский MVP и диагностику связности.
### 4.1. Core facts
#### 1. `Posting`
Базовая атомарная бухгалтерская запись / движение.
Поля:
- `posting_id`
- `period`
- `organization_id`
- `document_id`
- `debit_account_id`
- `credit_account_id`
- `amount`
- `currency_id`
- `content`
- `subconto_1`
- `subconto_2`
- `subconto_3`
- `source_register`
- `is_active`
#### 2. `Document`
Первичный / операционный документ 1С.
Поля:
- `document_id`
- `document_type`
- `number`
- `date`
- `organization_id`
- `counterparty_id`
- `contract_id`
- `author_id`
- `posted_flag`
- `comment`
- `source_entity_set`
#### 3. `RegisterMovement`
Универсальное представление движений по регистрам, если нужно отделить их от бухгалтерской проводки.
Поля:
- `movement_id`
- `register_type`
- `register_name`
- `period`
- `recorder_document_id`
- `organization_id`
- `dimensions`
- `resources`
- `is_active`
---
### 4.2. Main dimensions
#### 4. `Account`
Счёт бухгалтерского учёта.
Поля:
- `account_id`
- `code`
- `name`
- `parent_account_id`
- `is_off_balance`
- `kind`
- `subconto_profile`
#### 5. `SubcontoType`
Вид аналитики по счёту.
Поля:
- `subconto_type_id`
- `name`
- `code`
- `chart_reference`
#### 6. `Organization`
Организация.
#### 7. `Counterparty`
Контрагент.
#### 8. `Contract`
Договор контрагента.
#### 9. `BankAccount`
Банковский счёт.
#### 10. `Currency`
Валюта.
#### 11. `Item`
Номенклатура / товар / услуга.
#### 12. `Warehouse`
Склад, если нужен товарный / операционный контур.
#### 13. `Employee`
Сотрудник / физлицо / автор / ответственный.
#### 14. `Tax`
Налог / вид налогового обязательства.
#### 15. `CostItem`
Статья затрат / ДДС / аналитика затрат, если есть в базе.
---
## 5. Канонические связи
Это ядро графа, которое обязательно надо уметь строить.
### 5.1. Документно-проводочный слой
- `Document -> creates -> Posting`
- `Document -> creates -> RegisterMovement`
- `RegisterMovement -> derivedInto -> Posting` *(если между регистром и проводкой есть вычислимый мост)*
- `Posting -> basedOn -> Document`
### 5.2. Бухгалтерский слой
- `Posting -> debitAccount -> Account`
- `Posting -> creditAccount -> Account`
- `Posting -> hasCurrency -> Currency`
- `Posting -> belongsTo -> Organization`
- `Posting -> usesSubcontoType -> SubcontoType`
- `Posting -> referencesCounterparty -> Counterparty`
- `Posting -> referencesContract -> Contract`
- `Posting -> referencesBankAccount -> BankAccount`
- `Posting -> referencesItem -> Item`
### 5.3. Справочный слой
- `Counterparty -> hasContract -> Contract`
- `Organization -> ownsBankAccount -> BankAccount`
- `Account -> allowsSubcontoType -> SubcontoType`
- `Document -> referencesCounterparty -> Counterparty`
- `Document -> referencesContract -> Contract`
- `Document -> referencesOrganization -> Organization`
- `Document -> referencesWarehouse -> Warehouse`
- `Document -> referencesItem -> Item`
---
## 6. Mapping 1С OData -> каноническая онтология
Ниже — стартовый mapping, который нужно завести как отдельную таблицу / YAML / JSON-конфиг.
### 6.1. Сущности верхнего приоритета
| 1С OData entity | Каноническая сущность | Приоритет |
|---|---|---:|
| `AccountingRegister_Хозрасчетный` | `Posting` / `RegisterMovement` | высокий |
| `AccountingRegister_Хозрасчетный_RecordType` | `Posting` | высокий |
| `ChartOfAccounts_Хозрасчетный` | `Account` | высокий |
| `Catalog_Контрагенты` | `Counterparty` | высокий |
| `Catalog_ДоговорыКонтрагентов` | `Contract` | высокий |
| `Catalog_Организации` | `Organization` | высокий |
| `Catalog_БанковскиеСчета` | `BankAccount` | высокий |
| `Catalog_Валюты` | `Currency` | высокий |
| `Catalog_Номенклатура` | `Item` | высокий |
| `Document_ПоступлениеТоваровУслуг` | `Document(type=goods_receipt)` | высокий |
| `Document_РеализацияТоваровУслуг` | `Document(type=sales)` | высокий |
| `Document_СписаниеСРасчетногоСчета` | `Document(type=bank_out)` | высокий |
| `Document_ПоступлениеНаРасчетныйСчет` | `Document(type=bank_in)` | высокий |
| `Document_ОперацияБух` | `Document(type=manual_entry)` | высокий |
### 6.2. Табличные части документов
Все entity sets вида:
- `Document_*_Товары`
- `Document_*_Услуги`
- `Document_*_Оплата`
- `Document_*_Поставщики`
- `Document_*_Контрагенты`
- `Document_*_СчетаРасчетов`
рассматриваются как:
- либо `DocumentLine`
- либо специализированные дочерние узлы `DocumentItemLine`, `PaymentLine`, `SettlementLine`
### 6.3. Накопительные и информационные регистры
- `AccumulationRegister_*` -> `RegisterMovement`
- `InformationRegister_*` -> `ReferenceFact` / `OperationalAttribute` / `RegisterMovement` в зависимости от смысла
Важно: не пытаться все `InformationRegister_*` сразу тянуть в онтологию. Сначала только те, которые реально участвуют в бухгалтерских вопросах.
---
## 7. Формат хранения онтологии на MVP
На этом этапе онтология должна храниться **не в RDF первым делом**, а в практичном инженерном виде.
### 7.1. Рекомендуемый формат
#### Вариант A. YAML / JSON schema + mapping tables
Нужно завести:
- `ontology/entities.yaml`
- `ontology/relations.yaml`
- `ontology/mapping_1c_odata.yaml`
- `ontology/probe_matrix.yaml`
#### Вариант B. Таблицы в Postgres / SQLite
Минимально:
- `ontology_entities`
- `ontology_relations`
- `source_entity_mappings`
- `probe_results`
- `probe_relationships`
### 7.2. Что должно быть у каждой сущности
- `canonical_name`
- `description`
- `source_entity_sets`
- `required_fields`
- `optional_fields`
- `key_fields`
- `candidate_relationships`
- `probe_status`
### 7.3. Что должно быть у каждой связи
- `relation_name`
- `from_entity`
- `to_entity`
- `semantic_meaning`
- `source_evidence`
- `reconstruction_method`
- `confidence`
- `probe_status`
---
## 8. Главная задача этапа: проверка цепочек
Это ключевой блок. Именно он отвечает на вопрос: **достаточен ли OData в принципе**.
### 8.1. Цель probe-этапа
Нужно не просто прочитать объекты, а доказать, что через текущий доступ можно восстанавливать бухгалтерские зависимости.
### 8.2. Критически важные цепочки
#### Цепочка A. Документ реализации -> проводки -> счета -> субконто
Проверить:
- читается ли `Document_РеализацияТоваровУслуг`;
- есть ли у документа ключ / ссылка / recorder;
- можно ли выйти на `AccountingRegister_Хозрасчетный`;
- можно ли получить Дт/Кт;
- можно ли получить аналитику по контрагенту / договору / номенклатуре.
**Ожидаемый вывод:** можно ли объяснить, как документ реализации отразился в бухучёте.
#### Цепочка B. Документ поступления -> проводки -> счета расчётов / товаров / НДС
Проверить:
- `Document_ПоступлениеТоваровУслуг`
- табличные части товара / услуг
- связь с проводками
- связь со счетами расчётов и учёта
**Ожидаемый вывод:** можно ли строить контроль по закупкам и закрытию обязательств.
#### Цепочка C. Контрагент -> договор -> документы -> движения
Проверить:
- `Catalog_Контрагенты`
- `Catalog_ДоговорыКонтрагентов`
- документы по контрагенту
- движения по контрагенту
- расчёты / закрытие / сальдо
**Ожидаемый вывод:** можно ли строить “паспорт взаиморасчётов” по контрагенту.
#### Цепочка D. Банковский документ -> банк / счёт -> движения
Проверить:
- `Document_СписаниеСРасчетногоСчета`
- `Document_ПоступлениеНаРасчетныйСчет`
- `Catalog_БанковскиеСчета`
- проводки по 51/52 и связанные аналитики
**Ожидаемый вывод:** можно ли строить сценарии контроля денежных потоков.
#### Цепочка E. Manual entry -> хозрасчетный регистр -> explainability
Проверить:
- `Document_ОперацияБух`
- его табличные части / регистровые части
- связь с `AccountingRegister_Хозрасчетный`
**Ожидаемый вывод:** можно ли объяснить ручную бухгалтерскую операцию как граф.
#### Цепочка F. План счетов -> виды субконто -> проводки
Проверить:
- `ChartOfAccounts_Хозрасчетный`
- `ChartOfCharacteristicTypes_ВидыСубконтоХозрасчетные`
- соответствие аналитик конкретным счетам
**Ожидаемый вывод:** можно ли восстанавливать бухгалтерскую логику, а не просто номера счетов.
---
## 9. Как проверять цепочки технически
### 9.1. Шаг 1. Скачать и распарсить `$metadata`
Codex / интеграционный слой должен:
- распарсить `EntityType`;
- распарсить `NavigationProperty`;
- выделить ключи;
- собрать список кандидатов на связи;
- пометить, какие связи подтверждены схемой, а какие только по названию.
### 9.2. Шаг 2. Построить реестр приоритетных сущностей
Сформировать shortlist из 3060 сущностей верхнего приоритета, а не пытаться сразу охватить все 1189.
### 9.3. Шаг 3. Выполнить пробные GET-запросы
Для каждой ключевой сущности сделать:
- `$top=5`
- `$select=` для ключевых полей
- `$filter=` по периоду / номеру / организации
- по возможности `$expand=` для навигационных свойств
### 9.4. Шаг 4. Для каждой цепочки собрать “relationship evidence”
У каждой связи должны быть доказательства:
- подтверждена метаданными;
- подтверждена по реальным данным;
- восстановлена косвенно по ключам;
- не восстанавливается текущим методом.
### 9.5. Шаг 5. Завести матрицу полноты
Для каждой канонической связи нужно выставить статус:
- `direct` — есть прямая навигация или поле-ссылка;
- `derivable` — можно восстановить вычислением / join;
- `opaque` — связь предполагается, но текущим OData не извлекается надёжно;
- `missing` — данных недостаточно.
---
## 10. Критерии принятия решения: OData достаточно или нет
### 10.1. OData остаётся основным методом, если
Подтверждается следующее:
- можно пройти минимум 4 из 6 ключевых цепочек;
- можно reliably получить документ, связанные движения, счета и аналитику;
- `ChartOfAccounts` и субконто доступны в форме, пригодной для восстановления логики;
- write не нужен для MVP;
- explainability можно собирать поверх OData + канонической модели.
### 10.2. OData остаётся только discovery / transport-слоем, если
- сущности читаются, но связи восстанавливаются только частично;
- для регистров не хватает полей / ключей;
- невозможно объяснить проводку от документа до аналитики;
- часть критичных связей нужно вычислять с высокой хрупкостью.
В этом случае:
- OData используется для discovery, metadata и части чтения;
- рядом проектируется более глубокий коннектор.
### 10.3. Нужно сразу идти к более жирному методу, если
Подтверждается хотя бы одно:
- document -> posting не восстанавливается надёжно;
- posting -> debit/credit/subconto недоступны в пригодном виде;
- регистры отдают неполную картину;
- часть нужной бухгалтерской логики вообще не видна через OData;
- восстановление связей превращается в гадание по именам полей.
---
## 11. Что считать “более жирным методом”
Это не raw SQL первым делом, а набор вариантов по возрастанию глубины.
### Вариант 1. OData + внутренние HTTP/OData helper endpoints
Когда OData в целом нормален, но не хватает готовых агрегатов.
Что делаем:
- оставляем OData как базу;
- добавляем внутренние сервисы / обработку, которые возвращают уже собранные цепочки;
- ассистент работает поверх канонического слоя.
### Вариант 2. Агент / расширение внутри 1С
Когда OData не даёт достаточно связности или глубины.
Что даёт:
- доступ к объектной модели 1С;
- возможность отдавать наружу уже осмысленные выборки;
- меньше потерь смысла, чем в OData-only.
### Вариант 3. Глубокий коннектор к базе / внутренним механизмам 1С
Используется только если предыдущие уровни не закрывают задачу.
Важно:
- raw SQL по 1С не должен быть первым выбором для бизнес-семантики;
- но может понадобиться как дополнительный диагностический путь, если нужно убедиться, что OData реально скрывает часть данных.
### Вариант 4. Комбинированный режим
Наиболее вероятный реалистичный итог:
- OData — для discovery, metadata, части чтения;
- внутренний адаптер / сервис 1С — для критичных бухгалтерских цепочек и explainability;
- канонический слой — единый контракт для ассистента и MCP.
---
## 12. Deliverables этого этапа
### 12.1. Документы
Нужно получить:
1. `ontology_v0_1.md`
- список сущностей;
- список связей;
- смысл каждой сущности и связи.
2. `mapping_1c_odata_v0_1.md`
- соответствие OData entity sets к каноническим сущностям.
3. `probe_matrix.md`
- перечень проверяемых цепочек;
- статус прохождения;
- direct / derivable / opaque / missing.
4. `decision_report_oddata_vs_deeper_access.md`
- вердикт по OData;
- список ограничений;
- список причин, по которым нужен или не нужен более глубокий метод.
### 12.2. Технические артефакты
1. Скрипт парсинга `$metadata`
2. Список приоритетных entity sets
3. Набор probe-запросов к OData
4. JSON / YAML со схемой сущностей и связей
5. Отчёт с примерами реальных цепочек
---
## 13. Этапы работ
### Этап 1. Discovery и нормализация модели
Задачи:
- скачать `$metadata`;
- распарсить EntityType, keys, NavigationProperty;
- сформировать shortlist priority entity sets;
- собрать первый mapping.
Результат:
- черновик онтологии;
- черновик карты связей.
### Этап 2. Формирование базовой онтологии
Задачи:
- утвердить канонические сущности;
- утвердить канонические связи;
- выделить ядро бухгалтерской модели;
- зафиксировать обязательные поля.
Результат:
- `ontology_v0_1.md`
- `mapping_1c_odata_v0_1.md`
### Этап 3. Probe-проверка цепочек
Задачи:
- прогнать ключевые document/posting/account/subconto цепочки;
- проверить контрагентов, договоры, расчёты, банк;
- собрать evidence по каждой связи.
Результат:
- `probe_matrix.md`
- `relationship_evidence.json`
### Этап 4. Вердикт по OData
Задачи:
- определить, хватает ли OData для MVP;
- определить, нужен ли гибридный слой;
- определить, надо ли идти глубже в 1С.
Результат:
- `decision_report_oddata_vs_deeper_access.md`
### Этап 5. Подготовка к MCP / assistant layer
Задачи:
- на базе подтверждённой онтологии описать инструменты MCP;
- выделить сценарии запросов;
- построить explainable ответы поверх канонической модели.
Результат:
- спецификация инструментов MCP;
- список первых assistant-сценариев.
---
## 14. Что должен сделать Codex на этом этапе
### 14.1. Обязательные задачи
1. Скачать и распарсить `$metadata`.
2. Собрать shortlist сущностей высокого приоритета.
3. Построить базовую онтологию v0.1.
4. Построить mapping `1C OData -> ontology`.
5. Выполнить probe-цепочки.
6. Для каждой связи выдать статус:
- `direct`
- `derivable`
- `opaque`
- `missing`
7. Сформировать инженерный вывод:
- OData достаточно;
- OData частично достаточно;
- OData недостаточно и нужен deeper access.
### 14.2. Что нельзя делать
- Нельзя объявлять OData “достаточным” только потому, что читаются entity sets.
- Нельзя считать перечень сущностей онтологией.
- Нельзя делать вывод о полноте без проверки document/posting/account/subconto цепочек.
- Нельзя уходить в raw SQL как в основной semantic-layer без проверки объектной модели 1С.
---
## 15. Критерий успеха этапа
Этап считается успешным, если по его итогу есть:
1. Формализованная базовая онтология бухгалтерского ядра.
2. Подтверждённый mapping из 1С OData в канонический слой.
3. Матрица связей с evidence по каждой критичной цепочке.
4. Честный вердикт:
- хватает ли OData;
- где OData заканчивается;
- нужен ли следующий, более глубокий способ доступа.
Главный смысл этапа: **не доказать любой ценой, что OData подходит**, а быстро и жёстко определить его реальный потолок.
---
## 16. Рекомендация по следующему шагу
Следующим практическим шагом нужно запускать именно **Этап 1 + Этап 2 + первые probe-запросы** в одном спринте:
1. распарсить `$metadata`;
2. собрать `ontology_v0_1`;
3. выбрать 6 ключевых цепочек;
4. прогнать их;
5. выдать вердикт по полноте OData.
И только после этого принимать решение:
- остаёмся на OData,
- идём в гибрид,
- или сразу проектируем более глубокий коннектор внутрь 1С.

View File

@ -0,0 +1,105 @@
# ТЗ v0.2 SHORT: базовая онтология + deep access check
**Дата:** 2026-03-22
**Контур:** `http://localhost/buh_test/odata/standard.odata/`
**Режим:** только read-only
## 1. Цель этапа
Нужно получить инженерный вердикт, достаточно ли текущего OData для MVP-слоя ассистента:
1. Построить базовую каноническую онтологию бухгалтерского ядра.
2. Проверить не только чтение сущностей, а восстановление цепочек смысла:
- документ -> проводка -> счет/аналитика
- контрагент/договор -> документы -> движения
## 2. Нефункциональные требования (обязательные)
1. Только read-only доступ.
2. Любые write-операции запрещены политикой роли 1С.
3. В отчёте обязателен негативный тест:
- `POST` -> `403/405/401`
- `PATCH` -> `403/405/401`
- `DELETE` -> `403/405/401`
Без этого этап не считается закрытым.
## 3. Каноническая модель v0.1
### 3.1 Primary fact
`Posting` — основной факт учёта (single source of accounting truth на MVP).
### 3.2 Secondary fact
`RegisterMovement` — вспомогательный слой трассировки, не второй независимый источник истины.
### 3.3 Core dimensions
`Document`, `Account`, `SubcontoType`, `Counterparty`, `Contract`, `Organization`, `BankAccount`, `Currency`, `Item`.
## 4. Mapping правила
1. Один source entity set не должен одновременно создавать независимые `Posting` и `RegisterMovement` без явного правила приоритета.
2. Для каждого маппинга фиксируются:
- `direct` (есть явный link/navigation),
- `derivable` (можно восстановить надежным join),
- `opaque`,
- `missing`.
## 5. Probe-подход
Для каждой приоритетной сущности:
1. `top=5`
2. `select` по ключевым полям
3. `filter` по периоду/организации
4. при наличии — `expand`
Если сущность вернула `0` строк:
1. повторить с расширенным периодом;
2. повторить без фильтра;
3. только потом ставить `missing/opaque`.
## 6. Критичные цепочки (обязательные)
### 6.1 Обязательные без вариантов
1. `Document_РеализацияТоваровУслуг -> Posting -> Account/Subconto`
2. `ChartOfAccounts_Хозрасчетный -> SubcontoType -> Posting`
### 6.2 Дополнительные (минимум 2 из 4)
1. `Document_ПоступлениеТоваровУслуг -> Posting`
2. `Counterparty -> Contract -> Documents -> Movements`
3. `Bank docs (in/out) -> BankAccount -> Posting`
4. `Document_ОперацияБух -> Posting`
## 7. Правило решения по OData
### OData достаточен
Если обе обязательные цепочки имеют статус не ниже `derivable`, и минимум 2 дополнительные цепочки тоже не ниже `derivable`.
### OData частично достаточен (гибрид)
Если чтение сущностей стабильное, но хотя бы одна обязательная цепочка `opaque`.
### OData недостаточен
Если любая обязательная цепочка `missing`.
## 8. Deliverables (обязательные)
1. `docs/ontology_v0_1.md`
2. `docs/mapping_1c_odata_v0_1.md`
3. `docs/probe_matrix.md`
4. `docs/decision_report_odata_vs_deeper_access.md`
5. `docs/relationship_evidence.json`
## 9. Что нельзя делать
1. Нельзя считать “много сущностей” доказательством пригодности.
2. Нельзя объявлять OData достаточным без проверки цепочек.
3. Нельзя допускать запись в 1С в рамках этого этапа.

592
IN/TZ_1C_OData_MCP_MVP.md Normal file
View File

@ -0,0 +1,592 @@
# ТЗ: MVP-мост между 1С и AI-ассистентом бухгалтерии через OData → канонический слой → MCP
## 1. Контекст и цель
### 1.1. Что дано
- Есть отдельная **изолированная копия** базы 1С для экспериментов. Это **не прод**, но контур должен быть приближен к реальному.
- По предоставленным скриншотам рабочая среда на Windows организована так:
- `X:\1C\8.3.27.1936` — установленная платформа 1С.
- `X:\1C\NDC_1C` — целевой отдельный контур под нашу интеграцию и сервисы.
- `X:\1C\База бухгалтерии` — файловая база 1С.
- Внутри `X:\1C\База бухгалтерии` присутствует `1Cv8.1CD`, то есть сейчас это **файловая информационная база**, а не SQL-серверная.
- Есть `.dt`-выгрузка и уже развёрнутая копия базы.
- Есть Windows-машина, 1С 8.3, Visual Studio Code / Codex, но **нет 1С-разработчика**.
### 1.2. Что нужно получить
Нужно построить **прототип интеграционного контура**, в котором:
1. 1С доступна **только на чтение**.
2. Внешний сервис может **самостоятельно получать данные из 1С**, без ручных выгрузок таблиц и отчётов.
3. Слой интеграции видит не только отдельные сущности, но и **их взаимосвязи**.
4. Поверх данных можно строить:
- каноническую модель / "онтологию" бухгалтерии,
- поисково-аналитический слой,
- в дальнейшем — **MCP-сервер** для ассистента.
5. Решение должно быть **минимально инвазивным** к базе и пригодным для перехода от тестового контура к боевому сценарию.
### 1.3. Почему выбран такой путь
По материалам исследования, для подобного MVP ключевой поворот — уйти от ручных выгрузок и перейти к модели **постоянного доступа к данным**, с канонической моделью поверх 1С и прикладными сценариями типа сверок, поиска оснований проводок и контроля аномалий. Для старта быстрее всего использовать **OData** как стандартный интерфейс доступа к сущностям 1С, а дальше уже строить внешний адаптер и AI-слой. fileciteturn4file0
Также в исследовании отмечено, что готовой практичной OSS-онтологии 1С-бухгалтерии почти нет, поэтому MVP надо строить вокруг **собственной канонической модели**: `Posting`, `Document`, `Account`, `Subconto`, `Counterparty`, `Contract` и т. д. fileciteturn4file0turn4file1
---
## 2. Целевая архитектура MVP
### 2.1. Архитектурный принцип
На первом этапе делаем не "AI внутри 1С", а **внешний безопасный мост**:
```text
1С (read-only) -> OData -> Python adapter -> Canonical layer -> MCP/API -> Codex / ассистент
```
### 2.2. Почему именно так
Это даёт:
- минимум вмешательства в конфигурацию 1С;
- быстрый запуск на копии базы;
- возможность проверить, насколько OData реально раскрывает объекты и связи;
- возможность потом заменить или усилить внешний слой, не ломая 1С;
- прямой путь к MCP через OData bridge.
Исследование прямо указывает, что MVP-дружелюбный путь — это **OData → внешний коннектор → канонический слой**, а MCP-мосты можно использовать как ускоритель для AI-слоя поверх живых данных. fileciteturn4file0turn4file1
### 2.3. Что считается успешным результатом этапа
Успех этапа = на Windows-машине развёрнут рабочий read-only контур, который умеет:
- подключаться к 1С без ручных выгрузок;
- перечислять доступные сущности 1С через OData metadata;
- читать основные объекты бухгалтерии;
- извлекать связи между документами, проводками, счетами, контрагентами и договорами;
- отдавать эти данные наружу через локальный API / MCP-интерфейс.
---
## 3. Основная гипотеза и ограничения
### 3.1. Основная гипотеза
Гипотеза этапа: **стандартный интерфейс 1С OData даст достаточно данных и связей**, чтобы:
1. построить MVP-каноническую модель;
2. дать ассистенту доступ к прикладным сущностям;
3. подготовить переход к MCP без разработки тяжёлой подсистемы внутри 1С.
### 3.2. Что надо доказать тестами
Нужно проверить:
- доступны ли через OData основные типы сущностей;
- доступны ли регистры, документы и ссылочные поля;
- можно ли строить цепочки типа:
- документ -> движения / проводки,
- проводка -> счёт / субконто,
- документ -> контрагент / договор,
- документ -> основание / связанный объект,
- отчётный вопрос -> набор OData-запросов.
### 3.3. Что считаем риском
Главный риск: OData отдаёт только часть сущностей или отдаёт их слишком "плоско", без достаточной глубины взаимосвязей. В этом случае OData остаётся полезным как быстрый тестовый слой, но для production-уровня может понадобиться более жёсткая схема: HTTP-сервисы 1С, шлюз `1c-gateway` или "агент внутри 1С" через интеграционные подсистемы. fileciteturn4file0turn4file1
---
## 4. Подготовка среды для работы
### 4.1. Структура каталогов
Использовать следующий контур:
```text
X:\1C\
8.3.27.1936\ # платформа 1С
База бухгалтерии\ # файловая база-копия с 1Cv8.1CD
NDC_1C\ # наш рабочий контур интеграции
```
### 4.2. Что создать в `X:\1C\NDC_1C`
Нужно подготовить структуру проекта:
```text
X:\1C\NDC_1C\
docs\
scripts\
logs\
config\
odata_probe\
canonical_layer\
mcp\
tests\
tmp\
```
### 4.3. Что должно быть установлено на машине
Обязательно:
- Windows с доступом к текущей файловой базе 1С.
- 1С 8.3.27.x.
- Python 3.11+.
- Git.
- VS Code.
- curl или PowerShell Invoke-WebRequest.
Желательно:
- Postman или Bruno для проверки HTTP/OData.
- jq для разбора JSON.
- Docker Desktop — опционально, если позже будем поднимать отдельные сервисы контейнерами.
### 4.4. Правило доступа
Сразу зафиксировать архитектурный принцип:
- **никакой записи в 1С** на этапе MVP;
- только **read-only** пользователь / сервисный доступ;
- никакого выполнения операций изменения документов, проведения, перепроведения, записи справочников.
Если библиотека или мост по умолчанию поддерживает `create/edit`, все такие операции должны быть **запрещены на уровне конфигурации доступа и на уровне кода адаптера**. Это особенно важно, поскольку `belov38/1c-odata` демонстрирует не только чтение, но и create/edit-сценарии. Исследование рекомендует использовать read-only контур и тестировать мост именно как безопасный канал чтения. fileciteturn4file0
### 4.5. Что нужно сделать в 1С до начала кода
1. Определить, можно ли включить / опубликовать OData для этой базы.
2. Создать или выделить отдельного технического пользователя.
3. Ограничить ему права до чтения.
4. Зафиксировать, какая именно конфигурация используется:
- Бухгалтерия предприятия, редакция 2.0 / 3.0 или иная;
- файловая или серверная база;
- версия платформы;
- список основных подсистем.
5. В Конфигураторе открыть конфигурацию и зафиксировать наличие:
- документов,
- справочников,
- регистров бухгалтерии,
- регистров накопления,
- планов счетов,
- общих модулей,
- ролей.
Результат этого шага — короткий технический отчёт `docs/1c_inventory.md`.
---
## 5. Техническая стратегия реализации
### 5.1. Этап 1 — OData probe
Первая задача — не строить сразу продукт, а быстро проверить: **что реально видит OData в этой базе**.
Для этого делаем маленький сервис / набор скриптов `odata_probe`, который:
- подключается к OData endpoint;
- тянет `$metadata`;
- перечисляет entity sets;
- пробует читать первые записи по ключевым сущностям;
- логирует ошибки доступа и ограничения.
### 5.2. Этап 2 — canonical layer
После OData probe строим не "полную онтологию мира", а **каноническую схему MVP**.
Минимальный состав сущностей:
- `Organization`
- `Document`
- `Posting`
- `Account`
- `Subconto`
- `Counterparty`
- `Contract`
- `BankAccount`
- `Item`
- `Department`
- `Period`
- `RegisterMovement`
Это соответствует подходу из исследования: вместо поиска готовой 1С-онтологии нужно строить свою минимальную модель вокруг проводки, документа и измерений. fileciteturn4file0turn4file1
### 5.3. Этап 3 — прикладной query/API слой
После нормализации поднять API, которое отвечает не сырыми OData-объектами, а прикладными выборками:
- документы за период;
- проводки по счёту;
- движения по контрагенту;
- документы по договору;
- цепочка связей по документу;
- объяснение остатка по счёту;
- поиск нетипичных корреспонденций.
### 5.4. Этап 4 — MCP слой
После подтверждения, что OData даёт достаточную глубину, добавить MCP-слой.
На этом этапе возможны два пути:
1. **Быстрый мост от OData к MCP**.
2. Собственный MCP-сервер поверх уже нормализованного API.
Для MVP начать с готового моста, но не считать его финальным production-решением.
---
## 6. Инструменты и репозитории, которые берём в работу
### 6.1. Базовый инструмент первого этапа
#### `belov38/1c-odata`
GitHub: `https://github.com/belov38/1c-odata` citeturn910914search0turn910914search4
Зачем берём:
- самый быстрый путь проверить 1С OData без разработки тяжёлого клиента;
- подходит для Python-пробника и первых сервисных вызовов;
- позволяет быстро читать сущности и проверять фильтры/select/top/skip.
Что важно:
- библиотека умеет и запись, поэтому использовать её **только в режиме чтения**, с ограничением на уровне прав пользователя и без вызова write-операций.
### 6.2. Инструмент второго этапа, если OData окажется рабочим
#### `SysUtils/1c-gateway`
GitHub: `https://github.com/SysUtils/1c-gateway` citeturn910914search3
Зачем нужен:
- если захочется более типизированный фасад;
- если понадобится GraphQL / gRPC / нормальный middleware над OData;
- если нужно преобразовать кириллицу и сырой OData в более удобный internal API.
### 6.3. Кандидаты на MCP-слой
#### `oisee/odata_mcp_go`
GitHub: `https://github.com/oisee/odata_mcp_go` citeturn910914search2turn910914search18
#### `lemaiwo/odata-mcp-proxy`
GitHub: `https://github.com/lemaiwo/odata-mcp-proxy` (репозиторий подтверждён в стороннем описании экосистемы MCP/OData) citeturn910914search9turn910914search13
Зачем нужны:
- если OData endpoint живой, можно быстро превратить его в MCP tools;
- это даст ассистенту возможность ходить по данным как по инструментам, а не как по файлам;
- это ускоряет прототип и позволяет проверить формат взаимодействия ассистента с живой системой.
### 6.4. Резервный путь, если OData не взлетит
Как fallback держать в уме:
- FoxyLink;
- Universal Tools;
- Connector.
Их не брать первым шагом, но держать как план B, если OData не даст нужную глубину или окажется недоступен. Исследование прямо раскладывает эти инструменты как сценарий "когда OData недоступен или неудобен". fileciteturn4file0turn4file1
---
## 7. Что именно нужно сделать Codex'у
### 7.1. Создать рабочую структуру проекта
В `X:\1C\NDC_1C` развернуть каркас:
- `docs/`
- `scripts/`
- `config/`
- `odata_probe/`
- `canonical_layer/`
- `mcp/`
- `tests/`
- `logs/`
### 7.2. Подготовить Python-окружение
Примерно так:
```powershell
cd X:\1C\NDC_1C
py -3.11 -m venv .venv
.\.venv\Scripts\activate
python -m pip install --upgrade pip
pip install odata1cw requests lxml pydantic fastapi uvicorn python-dotenv
```
Если пакет `odata1cw` не ставится из PyPI, Codex должен:
1. клонировать `belov38/1c-odata`;
2. проверить способ локальной установки;
3. зафиксировать реальный рабочий способ установки в `docs/setup.md`.
### 7.3. Создать `.env.example`
Нужен шаблон параметров:
```env
ONEC_BASE_URL=http://localhost
ONEC_INFOBASE=AccountingBase
ONEC_USERNAME=readonly_user
ONEC_PASSWORD=change_me
ONEC_ODATA_PATH=/odata/standard.odata/
ONEC_TIMEOUT=30
ONEC_VERIFY_TLS=false
```
Если OData будет опубликован не на localhost, а на другой машине / IIS / Apache, переменные должны это учитывать.
### 7.4. Написать OData probe
Создать минимальный набор скриптов:
- `odata_probe/fetch_metadata.py`
- `odata_probe/list_entity_sets.py`
- `odata_probe/probe_entities.py`
- `odata_probe/dump_sample_links.py`
#### `fetch_metadata.py`
Должен:
- сходить в `$metadata`;
- сохранить XML в `logs/metadata.xml`;
- вытащить список entity sets;
- сохранить summary в `logs/entity_sets.json`.
#### `list_entity_sets.py`
Должен:
- прочитать metadata;
- вывести сущности в консоль и в файл;
- отметить подозрительно важные сущности по ключевым словам:
- `Документ`
- `Справочник`
- `Регистр`
- `ПланСчетов`
- `Хозрасчетный`
- `Контрагенты`
- `Договоры`
- `Организации`
- `БанковскиеСчета`
#### `probe_entities.py`
Должен:
- по списку ключевых сущностей сделать запросы `top=3..10`;
- проверить, какие поля реально приходят;
- определить, есть ли ссылочные поля на другие сущности;
- сохранить результаты в `logs/probe_report.json`.
#### `dump_sample_links.py`
Должен:
- выбрать несколько сущностей;
- собрать карту полей, которые выглядят как ссылки / GUID / navigation fields;
- показать, можно ли двигаться от документа к связанным объектам.
### 7.5. Реализовать канонический слой
Создать `canonical_layer/models.py` и `canonical_layer/mappers.py`.
Нужна первая версия канонической схемы:
```python
Organization
Counterparty
Contract
Account
Subconto
Document
Posting
RegisterMovement
Period
```
Для каждой сущности описать:
- `source_entity`
- `source_id`
- `display_name`
- ключевые реквизиты
- набор ссылок на связанные сущности
### 7.6. Реализовать тестовый API
Создать FastAPI-приложение `canonical_layer/app.py`.
Минимальные эндпоинты:
- `GET /health`
- `GET /metadata/entity-sets`
- `GET /documents?from=...&to=...`
- `GET /documents/{id}`
- `GET /postings?account=...&from=...&to=...`
- `GET /counterparties/{id}/documents`
- `GET /graph/document/{id}`
`/graph/document/{id}` должен быть первым прикладным доказательством, что система видит **связи**, а не только плоские записи.
### 7.7. Подготовить черновой MCP-слой
В папке `mcp/` подготовить два варианта:
#### Вариант A — через готовый OData→MCP мост
- описать конфиг для `oisee/odata_mcp_go` или `lemaiwo/odata-mcp-proxy`;
- проверить, можно ли поднять read-only инструменты поверх OData.
#### Вариант B — собственный MCP поверх канонического API
Если готовый мост не подходит, заложить каркас собственного MCP-сервера, который работает уже не с OData напрямую, а с нашими нормализованными эндпоинтами.
---
## 8. Подробное описание этапов работ
### Этап A. Инвентаризация 1С-копии
**Цель:** понять, что именно перед нами и к чему мы подключаемся.
Нужно:
1. В Конфигураторе открыть конфигурацию.
2. Зафиксировать список подсистем и ключевых объектов.
3. Составить инвентаризационный файл:
- тип базы: файловая;
- путь к базе;
- версия платформы;
- название конфигурации;
- основные разделы учёта.
**Результат:** `docs/1c_inventory.md`
### Этап B. Включение и проверка OData
**Цель:** доказать, что стандартный интерфейс OData вообще доступен.
Нужно:
1. Настроить публикацию OData.
2. Убедиться, что endpoint отвечает.
3. Проверить доступ к `$metadata`.
4. Протестировать авторизацию read-only пользователем.
**Результат:** `logs/metadata.xml`, `logs/entity_sets.json`
### Этап C. Пробный доступ к сущностям
**Цель:** понять, достаточно ли глубины для бухгалтерского AI-слоя.
Нужно проверить доступ хотя бы к части:
- организации;
- контрагенты;
- договоры;
- документы;
- планы счетов;
- движения / регистры / проводки.
**Результат:** `logs/probe_report.json`
### Этап D. Тест связей
**Цель:** понять, можно ли строить граф связей.
Нужно доказать минимум такие переходы:
- от документа к контрагенту;
- от документа к договору;
- от документа к организации;
- от документа к движениям / проводкам;
- от проводки к счетам и аналитикам.
**Результат:** `docs/linkage_report.md`
### Этап E. Каноническая модель
**Цель:** зафиксировать минимальную внутреннюю онтологию MVP.
Нужно:
- описать сущности;
- описать поля;
- описать связи;
- описать правила маппинга из OData в канонический слой.
**Результат:** `docs/canonical_model.md`
### Этап F. API и прикладные сценарии
**Цель:** доказать практическую применимость.
Нужно реализовать минимум 5 сценариев:
1. Найти документы по периоду.
2. Найти проводки по счёту.
3. Показать документы контрагента.
4. Показать граф связей документа.
5. Подготовить базовый сценарий "объясни остаток / основание проводки".
**Результат:** рабочий FastAPI-прототип.
### Этап G. MVP MCP
**Цель:** подготовить переход к ассистенту.
Нужно:
- проверить готовый OData→MCP мост;
- если взлетает — задокументировать способ запуска;
- если не взлетает — заложить собственный MCP поверх канонического API.
**Результат:** `docs/mcp_strategy.md`
---
## 9. Что именно надо проверить по OData
Это ключевой блок, потому что от него зависит весь следующий путь.
### 9.1. Проверки доступа
Нужно ответить на вопросы:
- отвечает ли `$metadata`;
- сколько entity sets публикуется;
- есть ли ограничения по ролям;
- не режет ли OData важные сущности;
- не отваливаются ли запросы по таймауту.
### 9.2. Проверки структуры
Нужно понять:
- насколько имена сущностей и полей понятны;
- есть ли navigation properties / ссылочные поля;
- можно ли восстановить структуру документа и его связей;
- как представлены планы счетов и аналитики.
### 9.3. Проверки пригодности для ассистента
Нужно ответить на главный вопрос:
> OData даёт только плоские сущности или даёт достаточно данных и связей, чтобы строить рассуждение и навигацию по бухгалтерскому контексту?
Если ответ **да**, то OData остаётся базовым мостом.
Если ответ **частично**, OData остаётся probe-слоем, а production-слой проектируется через middleware или собственный сервис.
Если ответ **нет**, идти в fallback: HTTP-сервисы 1С / агент в 1С / 1c-gateway / интеграционные подсистемы.
---
## 10. Что считать успешным исходом тестов
### Успех уровня 1
- OData опубликован.
- `belov38/1c-odata` подключается.
- metadata читается.
- entity sets видны.
### Успех уровня 2
- читаются ключевые бухгалтерские сущности;
- у сущностей видны связующие поля;
- можно собрать простую цепочку связей.
### Успех уровня 3
- построен канонический слой;
- API возвращает прикладные сущности;
- можно показать `document graph` и базовый `posting trace`.
### Успех уровня 4
- MCP-слой может ходить по этим данным как по инструментам;
- ассистент получает не статическую выгрузку, а живой read-only доступ к слою.
---
## 11. Что не делать на этом этапе
Не делать:
- прямую запись в базу;
- любые write-операции через OData;
- попытку сразу завязать ассистента на прод;
- попытку сразу построить "идеальную онтологию всего предприятия";
- перенос всех данных в огромную SQL-схему без проверки ценности;
- тяжёлое внедрение внутренних 1С-подсистем до проверки, что OData реально недостаточен.
---
## 12. Прикладные сценарии MVP, на которые надо ориентироваться
Исследование рекомендует строить MVP вокруг 35 операционно полезных сценариев, а не вокруг абстрактного "чат-бота про бухгалтерию". Наиболее подходящие сценарии здесь такие: fileciteturn4file0turn4file1
1. **Найти документы и проводки за период.**
2. **Показать движения по счёту / контрагенту / договору.**
3. **Показать, какие документы сформировали остаток или отклонение.**
4. **Найти нетипичные корреспонденции / аномальные паттерны.**
5. **Найти цепочку “документ -> основание -> движения -> аналитики”.**
Именно эти сценарии лучше использовать как критерий, подходит ли выбранный мост для будущего ассистента.
---
## 13. Deliverables
По итогам первой итерации Codex должен выдать:
1. `docs/1c_inventory.md`
2. `docs/setup.md`
3. `docs/canonical_model.md`
4. `docs/linkage_report.md`
5. `docs/mcp_strategy.md`
6. `logs/metadata.xml`
7. `logs/entity_sets.json`
8. `logs/probe_report.json`
9. Python-код OData probe
10. FastAPI-прототип канонического слоя
11. Черновик конфигурации MCP-моста
---
## 14. Краткое управленческое решение по стеку
На текущем этапе решение такое:
- **Стартуем с `belov38/1c-odata`** как самого короткого пути проверить 1С OData. citeturn910914search0turn910914search4
- **Строим внешний read-only адаптер**, а не тяжёлый агент внутри 1С. Это соответствует MVP-подходу из исследования. fileciteturn4file0turn4file1
- **Проверяем глубину связей**, а не просто чтение сущностей.
- **Плавно идём к MCP**, но сначала через доказательство пригодности OData.
- Если OData не даст нужной глубины, **переходим к более жёсткому слою**: `1c-gateway`, HTTP-сервисы 1С или агент внутри 1С. fileciteturn4file0turn4file1
---
## 15. Команда Codex'у в одном абзаце
Развернуть в `X:\1C\NDC_1C` read-only MVP-контур интеграции с 1С на базе OData. Сначала доказать техническую доступность и полноту `belov38/1c-odata`: получить metadata, перечислить сущности, протестировать доступ к ключевым объектам бухгалтерии и проверить, видны ли взаимосвязи между документами, проводками, счетами, контрагентами и договорами. Затем построить минимальный канонический слой и локальный API. После этого подготовить черновой MCP-слой. Главный критерий — не просто чтение записей, а способность строить прикладную навигацию и reasoning по живому read-only слою данных.

479
IN/TZ_ASSISTANT_MODE.md Normal file
View File

@ -0,0 +1,479 @@
Да, это разумный ход.
Ты сейчас предлагаешь **не добивать eval в вакууме**, а **замкнуть рабочий контур**:
* вопрос от человека;
* декомпозиция;
* выбор маршрута;
* поход в базу;
* сбор результата;
* человекочитаемый ответ в диалоговом окне.
И это реально полезнее следующего слоя “искусственной чистоты”, потому что вы уже уткнулись в потолок слепых прогонов.
## Что фиксируем по стратегии
### 1. Разметку не отменяем, а ставим на паузу
Не выбрасываем идею, а **откладываем до появления 3040 реальных полевых вопросов**.
То есть:
* synthetic eval остаётся как вспомогательный контур;
* основной следующий шаг — не policy-lab, а **рабочий диалоговый режим**.
### 2. Новая цель этапа
Не “ещё лучше понять язык на тестах”, а:
**собрать первый end-to-end assistant loop поверх уже существующей decomposition-логики.**
### 3. Что даст этот этап
Он даст не абстрактные проценты, а понимание:
* как вообще ощущается система в диалоге;
* где она тупит реально;
* где поиск не дотягивает;
* где ответ не собирается;
* где декомпозиция полезна;
* где интерфейс мешает;
* где нужен второй проход LLM на финальную сборку ответа.
И вот это уже будет очень ценная конкретика.
---
# ТЗ: переход к диалоговому режиму поверх текущего decomposition-контура
## 1. Цель этапа
Добавить в текущую тестовую GUI **второй режим работы — Assistant Mode**, чтобы можно было:
* задать вопрос в диалоговом интерфейсе;
* прогнать его через текущую decomposition / normalization pipeline;
* выполнить поиск/маршрутизацию по существующим путям;
* собрать ответ;
* показать ответ в человекочитаемом диалоговом формате.
Текущий режим decomposition/debugging при этом **сохраняется**.
---
## 2. Что фиксируем как решение
### Решение
На текущем этапе:
* **останавливаем расширение synthetic-разметки и synthetic-eval**;
* **не тратим время на дальнейшие искусственные прогоны без боевых вопросов**;
* переключаемся на **сборку диалогового рабочего контура**;
* позже, после получения полевых вопросов, возвращаемся к:
* разметке,
* policy eval,
* уточнению маршрутов и fallbackов.
Комментарий:
Это не отказ от eval-подхода.
Это смена приоритета: сначала — usable loop, потом — field-based hardening.
---
## 3. Что должно получиться на выходе
В GUI должны быть доступны **два режима**:
### A. `Decomposition Mode`
Сохраняется текущий debug-режим:
* raw input;
* decomposition output;
* fragments;
* flags;
* route status;
* fallback status;
* trace/debug панели.
### B. `Assistant Mode`
Новый диалоговый режим:
* чатоподобное окно;
* ввод сообщения;
* отправка сообщения;
* выполнение pipeline;
* получение итогового ответа;
* отображение ответа как сообщения ассистента;
* опционально раскрытие технических деталей.
---
## 4. Главная идея Assistant Mode
Assistant Mode не должен быть “просто ещё одним текстовым полем”.
Он должен работать как поверхностный пользовательский интерфейс над уже существующей внутренней логикой:
**User message → decomposition → scope filter → route → retrieval/query → answer composition → assistant reply**
Важно:
* decomposition слой остаётся;
* deterministic routing остаётся;
* fallback policy остаётся;
* просто поверх этого появляется **человеческий вход и человеческий выход**.
---
## 5. Поведение Assistant Mode
## 5.1. Вход
Пользователь пишет сообщение в обычном диалоговом окне.
Примеры:
* один вопрос;
* длинный вопрос;
* сомнение;
* мягкая формулировка;
* несколько связанных мыслей;
* в будущем — multi-intent.
## 5.2. Внутренний пайплайн
После отправки сообщения система должна:
1. прогнать сообщение через `normalizer_v2.x`;
2. получить fragments / flags / scope;
3. отбросить out-of-scope части;
4. определить routed / no-route / clarification;
5. если есть исполнимые fragments — отправить их в backend retrieval/query layer;
6. получить промежуточные результаты;
7. собрать из них **человекочитаемый ответ**;
8. показать его пользователю в чате.
## 5.3. Выход
На экране пользователь должен видеть не JSON и не route flags, а нормальный ответ.
---
## 6. Два режима интерфейса
## 6.1. Переключатель режима
В интерфейсе нужен явный переключатель:
* `Assistant`
* `Decomposition`
Вариант реализации:
* toggle;
* tabs;
* segmented control.
## 6.2. Требование
Состояние режимов должно быть разделено, но использовать общий backend.
То есть:
* один и тот же backend pipeline;
* разные UI-представления.
---
## 7. Assistant Mode — состав интерфейса
## 7.1. Основная область
Чатоподобная лента сообщений:
* сообщения пользователя;
* сообщения ассистента;
* системные статусы по желанию.
## 7.2. Поле ввода
Обычное текстовое поле + кнопка отправки.
## 7.3. Состояния ответа
Во время выполнения показывать:
* `Разбираю запрос`
* `Проверяю контур`
* `Определяю маршрут`
* `Ищу данные`
* `Собираю ответ`
Можно в упрощённом виде.
## 7.4. Дополнительная панель
Опционально:
* кнопка `Показать разбор`
* которая раскрывает:
* fragments,
* route,
* fallback,
* scope,
* trace id.
Комментарий:
По умолчанию это не должно мешать диалогу, но для отладки очень полезно.
---
## 8. Логика ответа в Assistant Mode
## 8.1. Если вопрос in-scope и routed
Ассистент должен сформировать ответ по результатам retrieval/query.
Ответ должен быть:
* человекочитаемый;
* собранный под вопрос;
* без сырого технического мусора.
## 8.2. Если вопрос требует clarification
Ассистент задаёт уточняющий вопрос в чат.
Пример:
* “Могу проверить это в контуре компании, но нужно уточнить период или участок учёта.”
## 8.3. Если вопрос out-of-scope
Ассистент отвечает вежливым fallbackом.
Пример:
* “Я работаю только с данными и бухгалтерическим контуром текущей компании. Этот вопрос относится к общей бухгалтерской практике, а не к данным вашей базы.”
## 8.4. Если вопрос частично in-scope
Ассистент отвечает по допустимой части и отдельно помечает, что остальное вне доступного контура.
---
## 9. Нужен отдельный слой answer composition
Это важно.
Сейчас decomposition и routing — это ещё не финальный ответ человеку.
Нужен отдельный блок:
### `answer_composer`
Он получает:
* исходный user message;
* fragments;
* route results;
* retrieved data/result payloads;
* fallback states;
и собирает:
* один связный ответ для человека.
Комментарий:
Да, ты правильно понимаешь:
если исходное сообщение человека было многослойным, ответ не должен приходить как 5 несвязанных кусков JSON.
Нужен **второй проход на human-readable response**.
---
## 10. Что не делаем сейчас
На этом этапе **не делаем**:
* финальный production-grade assistant;
* память диалога на 100 ходов;
* суперсложную agent orchestration;
* автоматическую доразметку eval;
* полную полевую разметку synthetic наборов;
* бесконечную шлифовку policy.
Сейчас задача — **замкнуть usable loop**.
---
## 11. Что backend должен уметь
Нужен единый assistant pipeline endpoint.
Примерно так:
### `POST /api/assistant/message`
Вход:
* user message
* session id
* mode = assistant
Внутри:
* run normalizer
* resolve execution state
* route fragments
* call retrieval/query handlers
* compose answer
* return assistant response + debug payload
Выход:
```json
{
"ok": true,
"assistant_reply": "string",
"conversation_item": {...},
"debug": {
"trace_id": "...",
"fragments": [...],
"fallback_type": "...",
"route_summary": [...]
}
}
```
---
## 12. Что сделать в GUI технически
## 12.1. Новая вкладка / режим
Добавить Assistant Mode без удаления текущего Decomposition Mode.
## 12.2. История диалога
Сделать хранение сообщений в рамках текущей сессии:
* user messages;
* assistant replies;
* debug ids.
Пока достаточно session-scoped памяти без сложного persistence.
## 12.3. Debug drawer
У каждого ответа можно открыть:
* decomposition;
* fragments;
* scope;
* routes;
* fallback;
* trace_id.
---
## 13. Что логировать
Нужно логировать для каждого сообщения:
* `session_id`
* `message_id`
* `user_message`
* `normalizer_output`
* `resolved_execution_state`
* `routes`
* `fallback_type`
* `retrieval/query payloads`
* `assistant_reply`
* `trace_id`
Комментарий:
Это потом очень пригодится, когда пойдут реальные полевые вопросы.
---
## 14. Что использовать как тестовые данные сейчас
Пока нет боевых полевых 3040 вопросов, допускается использовать:
* уже собранные сложные synthetic вопросы;
* ваши старые troublesome вопросы;
* 1020 кривых пользовательских формулировок.
Но это уже только для smoke test UI и pipeline, не для финального truth-eval.
---
## 15. Что будет следующим этапом после этого
После того как Assistant Mode заработает, следующий этап:
* собрать 3040 боевых полевых вопросов;
* прогнать их уже через реальный диалоговый контур;
* сохранить логи;
* на их основе вернуться к:
* разметке,
* eval policy,
* hardening маршрутов,
* fine-tuning clarification/no-route policy.
То есть сейчас мы строим **инструмент для получения настоящей конкретики**, а не выдуманной.
---
## 16. Артефакты, которые должен выдать Codex
1. `docs/assistant_mode_spec.md`
2. обновлённый GUI с переключателем:
* `Assistant`
* `Decomposition`
3. backend endpoint для assistant loop
4. `answer_composer` слой
5. session-based chat history
6. debug drawer / expandable technical view
7. `docs/assistant_mode_flow.md`
8. `docs/known_limits_before_field_eval.md`
---
## 17. Критерии приёмки
Этап считается принятым, если:
1. в GUI есть два режима:
* decomposition
* assistant
2. assistant mode позволяет:
* ввести вопрос,
* прогнать pipeline,
* получить финальный человекочитаемый ответ
3. out-of-scope / clarification / partial fallback работают в диалоге
4. decomposition mode не сломан
5. debug-информация доступна, но не мешает основному UX
6. история сообщений в рамках сессии работает
7. можно использовать контур как рабочую тестовую песочницу для будущих полевых вопросов
---
## 18. Короткий итог
Сейчас правильный ход такой:
**останавливаем blind eval hardening,
не тратим силы на synthetic-разметку,
и переводим систему в usable dialog mode,
чтобы уже на реальных вопросах смотреть, где она работает, а где нет.**
---
Если хочешь, я следующим сообщением сделаю ещё **короткую версию этого ТЗ для прямой вставки в Codex**, без пояснений и комментариев.

View File

@ -0,0 +1,589 @@
TZ_Assistant_Mode-GLOBAL-REPORT.md
собери в папку "X:\1C\NDC_1C\docs\ARCH\6_global_report" отчетные данные по текущему статусу проекта
Да, это правильный ход.
Сейчас вам уже мало смотреть на отдельные баги типа `executeHybrid` или на качество одного ответа. Нужна **единая архитектурная картина**, чтобы понять не “где подкрутить”, а **чего в системе в принципе не хватает**, чтобы она могла стать нормальным бухгалтерским ассистентом, а не набором частично работающих слоёв.
Ниже даю тебе именно **ёмкие критерии и структуру запроса на полный архитектурный отчёт**.
То есть это не “как улучшить конкретный кусок”, а **что надо запросить у команды / системы**, чтобы по итогам отчёта можно было принять нормальные решения.
---
# Что должен закрыть полный архитектурный отчёт
Отчёт должен отвечать не на вопрос
**“какие модули есть”**,
а на вопрос
**“почему система в текущем виде не всегда может дать осмысленный бухгалтерский ответ и что именно для этого не хватает”**.
Поэтому отчёт должен покрывать **9 контуров**:
1. **данные**
2. **онтология / модель предметной области**
3. **retrieval / query execution**
4. **LLM-слой и decomposition**
5. **answer synthesis / explanation**
6. **память / state / session continuity**
7. **orchestration / routing / control policy**
8. **качество / observability / eval**
9. **эволюционная архитектура — что можно наращивать дальше**
---
# 1. Контур данных: что реально доступно ассистенту
Здесь нужен не список таблиц, а ответ на вопрос:
**какой фактический объём бухгалтерской реальности видит система и в каком виде**.
Нужно запросить:
### 1.1. Источники данных
* какие именно данные приходят из 1С;
* какие каналы/срезы доступны;
* что получается live-запросом;
* что через снапшот;
* что кэшируется;
* что теряется при преобразовании.
### 1.2. Глубина данных
* видит ли система только документы;
* видит ли проводки;
* видит ли регистры;
* видит ли связи между документами;
* видит ли lifecycle объектов;
* видит ли статусы проведения, закрытия, списания, амортизации, зачета, вычета и т.д.
### 1.3. Полнота данных
* есть ли зоны, где ассистент видит только верхний слой сущностей, но не видит фактическую причинную механику;
* какие поля не доезжают;
* какие сущности представлены плоско;
* какие важные признаки отбрасываются.
### 1.4. Нормализация данных
* как 1С-данные преобразуются во внутреннюю модель;
* где происходит агрегирование;
* где исчезает детализация;
* где теряются связи;
* где всё схлопывается в “контрагент + документы + операции”.
### Главный критерий этого раздела:
**может ли система из текущих данных вообще построить причинное бухгалтерское объяснение, или на вход LLM уже попадает обеднённый полуфабрикат?**
---
# 2. Онтология / предметная модель
Это, возможно, вообще ключевой блок.
Нужен ответ на вопрос:
**в какой предметной модели система мыслит бухгалтерскую реальность — в модели плоских сущностей или в модели связанной бухгалтерской логики?**
Нужно запросить:
### 2.1. Набор базовых сущностей
* документ
* проводка
* движение регистра
* контрагент
* договор
* организация
* счет / субсчет
* объект ОС
* РБП
* номенклатура
* склад
* статья
* период
* регламентная операция
* закрытие
* налоговый объект
* и т.д.
### 2.2. Набор связей между сущностями
* документ → проводка
* документ → регистр
* платёж → расчёт
* ОС → амортизация
* РБП → график списания
* счёт-фактура → НДС
* закрытие периода → затрагиваемые остатки / хвосты
* договор → цепочка первички
* контрагент → серия проблем
* и т.д.
### 2.3. Lifecycle-модель
Это очень важно.
Нужно понять:
есть ли в системе формальная модель стадий жизни объекта?
Например:
* создан
* проведён
* не проведён
* частично связан
* закрыт
* не закрыт
* завис
* закрыт не тем документом
* перенесён
* сторнирован
* повторяется
* конфликтует с соседним контуром
Если этого нет, то ассистент почти неизбежно будет отвечать поверхностно.
### 2.4. Словарь аномалий и конфликтов
Нужно понять:
есть ли формальный словарь предметных дефектов, или всё пока описывается общими лейблами типа:
* broken lifecycle
* missing link
* posting mismatch
Нужно увидеть, есть ли **бухгалтерски интерпретируемые типы дефектов**:
* оплата есть, обязательство не закрылось;
* карточка ОС не согласована с начислениями;
* РБП живёт дольше ожидаемого срока;
* документ отражён, но не продолжен по цепочке;
* движение денег есть, документный контур не подтверждён;
* закрытие месяца зависит от хвоста;
* и т.д.
### Главный критерий раздела:
**есть ли у вас предметная модель, на которой вообще можно строить reasoning, или пока есть только data-access + тонкий semantic layer сверху?**
---
# 3. Retrieval / query execution
Это уже не про “видим данные”, а про “можем ли мы по вопросу вытащить нужный контекст”.
Нужно запросить:
### 3.1. Полный retrieval pipeline
* что делает normalizer;
* что делает decomposition;
* как строится semantic retrieval profile;
* какие executors существуют;
* как выбирается executor;
* какие фильтры реально исполняются;
* где fallback на широкий scan;
* где есть hard constraints, а где soft hints.
### 3.2. Глубина narrowing
По каждому типу запроса:
* насколько реально сужается массив;
* какие признаки обязательны;
* какие просто повышают score;
* как система защищается от “почти весь датасет”.
### 3.3. Единица retrieval-ответа
Очень важный вопрос.
Нужно выяснить:
retrieval возвращает в качестве top:
* контрагентов?
* документы?
* проводки?
* цепочки?
* problem clusters?
* risk groups?
Пока у вас по симптомам видно, что слишком часто top unit — это `контрагент`, а не `проблема`.
### 3.4. Evidence design
Какие доказательства retrieval возвращает наверх:
* ids и counts
* признаки
* связи
* missing links
* lifecycle gaps
* конфликтующие документы
* period impact
* cross-domain inconsistency
### Главный критерий раздела:
**способен ли retrieval вернуть не просто релевантные сущности, а релевантные причинные узлы, из которых можно собрать человеческое объяснение?**
---
# 4. LLM-слой и decomposition
Здесь нужен не “какая модель стоит”, а ответ на вопрос:
**что именно LLM делает в архитектуре и чего она сейчас не умеет делать из-за ограничений входа/контракта?**
Нужно запросить:
### 4.1. Функция LLM в системе
Разделить:
* интерпретация запроса;
* decomposition;
* routing;
* constraint extraction;
* result synthesis;
* explanation generation;
* follow-up policy;
* clarification policy.
### 4.2. Формат входа в LLM
Критично.
Нужно увидеть:
* что именно модель получает на вход;
* какие данные о вопросе;
* какие данные о контексте сессии;
* какие retrieval results;
* насколько они детальны;
* есть ли нормальная структура evidence;
* есть ли бизнес-интерпретация на входе или только raw items.
### 4.3. Потери на decomposition
Нужно понять:
* как измеряется полнота покрытия;
* где теряются части многосоставного вопроса;
* как различаются requirements и fragments;
* как работает clarification;
* как помечается uncovered intent.
### 4.4. Ограничения LLM
Нужно явно получить список:
* что модель не может infer-ить из текущего входа;
* какие объяснения она вынуждена делать шаблонно;
* в каких случаях она видит слишком мало данных;
* где она подменяет reasoning общими фразами.
### Главный критерий раздела:
**проблема в том, что модель “недостаточно умная”, или в том, что ей подают архитектурно бедный и плохо структурированный контекст?**
---
# 5. Answer synthesis / explanation layer
Это отдельный контур, и по вашим симптомам он сейчас один из самых слабых.
Нужно запросить:
### 5.1. Полный answer assembly pipeline
* как из retrieval result строится ответ;
* какие поля composer реально использует;
* что игнорирует;
* как выбирается top unit;
* как выбирается структура ответа.
### 5.2. Типы объяснений
Нужно понять, умеет ли система различать:
* direct factual answer;
* anomaly explanation;
* causal chain explanation;
* risk ranking explanation;
* period impact explanation;
* cross-entity inconsistency explanation.
### 5.3. Что именно делает ответ generic
Попросить прямо выделить:
* какие части explanation template переиспользуются почти без изменений;
* где нет case-specific wording;
* где не хватает данных для конкретизации;
* где composer всё ещё пишет “по совокупности факторов”.
### 5.4. Единица ответа
Очень критичный вопрос:
система отвечает через:
* объект?
* сущность?
* контрагента?
* цепочку?
* конфликт?
* механизм нарушения?
Нужно понять, **какая базовая единица ответа** сейчас используется и подходит ли она вообще для бухгалтерского ассистента.
### Главный критерий раздела:
**может ли текущий explanation layer собирать конкретный бухгалтерский вывод по кейсу, или он пока только красиво пересказывает retrieval labels?**
---
# 6. Память / state / session continuity
Если вы хотите действительно полезного ассистента, без этого дальше будет потолок.
Нужно запросить:
### 6.1. Что система помнит внутри сессии
* предыдущие вопросы;
* активный предмет разговора;
* выбранный период;
* выбранный счёт;
* текущую цепочку анализа;
* уже найденные проблемы;
* уже проверенные гипотезы.
### 6.2. Есть ли рабочая conversational state model
Например:
* current focus
* current accounting domain
* open hypotheses
* active entities
* unresolved questions
* current period
* comparison baseline
### 6.3. Может ли ассистент строить многоходовой анализ
Например:
* сначала нашли хвосты,
* потом сузили до причин,
* потом проверили влияние на закрытие периода,
* потом посмотрели соседний контур.
Если state model слабая, ассистент каждый раз будет начинать почти с нуля.
### Главный критерий раздела:
**это уже исследовательский ассистент с рабочим state, или пока stateless Q&A с небольшим контекстом последних сообщений?**
---
# 7. Orchestration / routing / control policy
Здесь надо понять, как система принимает решения о том, **что делать дальше**, а не только как отвечает.
Нужно запросить:
### 7.1. Политика выбора режима
Когда система:
* отвечает сразу;
* делает partial;
* просит уточнение;
* запускает дополнительный retrieval;
* анализирует соседние ветки;
* прекращает поиск.
### 7.2. Может ли система делать iterative reasoning
Например:
* гипотеза 1 → не подтвердилась;
* проверяем гипотезу 2;
* сравниваем два соседних контура;
* усиливаем evidence;
* перестраиваем ranking.
### 7.3. Может ли система сама инициировать дополнительный шаг
Не в смысле галлюцинировать, а в смысле:
“по текущим данным видно только банк, но без проверки расчётного контура вывод будет слабым — запускаем соседнюю проверку”.
### 7.4. Есть ли control policy поверх LLM
Или всё сейчас слишком жёстко route-driven.
### Главный критерий раздела:
**это линейный пайплайн или уже управляемый исследовательский контур?**
---
# 8. Качество / observability / eval
Нужно запросить не только логи, а систему оценки.
### 8.1. Как вы меряете качество
Отдельно:
* intent understanding;
* coverage completeness;
* retrieval precision;
* retrieval differentiation;
* grounding;
* explanation usefulness;
* accountant usefulness;
* false out_of_scope;
* false confidence;
* generic explanation rate.
### 8.2. Как вы меряете полезность именно для бухгалтера
Очень важно.
Нужны eval-критерии уровня:
* понятно ли, почему объект попал в ответ;
* понятно ли, что именно сломано;
* понятно ли, что делать дальше;
* не нужно ли открывать debug, чтобы понять суть.
### 8.3. Набор канонических сценариев
Нужно запросить список контрольных кейсов:
* 51 / банк / не в платеже проблема;
* 60 / хвосты / повторяемый контрагент;
* 97 / lifecycle anomaly;
* ОС / карточка vs начисления;
* НДС / соседний контур;
* период / влияние на закрытие;
* многосоставной запрос;
* транслит;
* follow-up внутри одной темы.
### Главный критерий раздела:
**вы меряете архитектуру по реально полезным сценариям или только по техническим индикаторам успешного pipeline?**
---
# 9. Эволюционная архитектура
Здесь нужен взгляд вперёд.
Нужно запросить:
### 9.1. Какие сущности или слои сейчас отсутствуют
Например:
* полноценная ontology graph layer;
* lifecycle engine;
* problem cluster layer;
* accountant-facing explanation schema;
* working memory / investigation state;
* hypothesis engine;
* cross-branch exploration policy;
* domain-specific anomaly catalog;
* semantic diff between similar queries.
### 9.2. Где текущая архитектура имеет потолок
Прямо попросить указать:
* что невозможно получить без изменения data model;
* что невозможно получить без изменения ontology;
* что невозможно получить без смены retrieval unit;
* что невозможно получить без нового state layer;
* что невозможно получить без multi-step orchestration.
### 9.3. Что является локальной доработкой, а что архитектурным шагом
Чтобы не лечить системную проблему очередной латкой.
### Главный критерий раздела:
**какие следующие изменения действительно приближают к конечной цели, а какие просто временно уменьшают боль?**
---
# В каком виде просить отчёт
Я бы просил не “свободный архитектурный рассказ”, а вот так:
## Формат ответа по каждому разделу:
1. **Как устроено сейчас**
2. **Что реально работает**
3. **Какие ограничения есть**
4. **Что из-за этого не может сделать ассистент**
5. **Какие симптомы это уже порождает в диалогах**
6. **Что нужно изменить локально**
7. **Что нужно изменить архитектурно**
8. **Приоритет: P0 / P1 / P2**
Это очень важно. Иначе тебе пришлют презентацию, а не диагностический отчёт.
---
# Самые важные вопросы, без которых отчёт будет бессмысленным
Если хочешь совсем сжато, то вот 12 ключевых вопросов, которые должны быть закрыты обязательно:
1. **Какие именно данные реально доезжают до ассистента из 1С, и где они теряют детализацию?**
2. **Есть ли у системы полноценная предметная модель бухгалтерских сущностей, связей и lifecycle?**
3. **Какая единица retrieval-результата считается основной: контрагент, документ, цепочка, проблема, кластер?**
4. **Какие constraints реально исполняет retrieval и где он расползается в слишком широкий scan?**
5. **Что именно LLM получает на вход перед финальным ответом?**
6. **Что теряется на decomposition и как измеряется полнота покрытия вопроса?**
7. **Почему explanation остаётся generic даже когда retrieval стал лучше?**
8. **Может ли система строить объяснение механизма проблемы, а не только перечислять признаки?**
9. **Есть ли рабочая state/memory model для многоходового бухгалтерского анализа?**
10. **Может ли система сама проверять соседние ветки, если это нужно для сильного вывода?**
11. **Как измеряется не техническая успешность, а реальная полезность ответа для бухгалтера?**
12. **Какие архитектурные сущности сейчас отсутствуют и без них потолок не пробить?**
---
# Что я подозреваю заранее, ещё до отчёта
С высокой вероятностью отчёт покажет одну или несколько таких проблем:
* данные до LLM доезжают уже **слишком обеднёнными**;
* retrieval возвращает **не ту единицу анализа**;
* ontology/lifecycle слой пока **слишком тонкий**;
* composer живёт на **generic explanation templates**;
* state/investigation memory почти нет;
* система всё ещё слишком **route-driven**, а не investigation-driven;
* нет отдельного слоя **problem clusters / causal nodes**;
* нет механизма **сравнения соседних веток учёта** как штатной операции.
Если это подтвердится, тогда уже можно будет не просто “улучшать ассистента”, а проектировать следующий архитектурный шаг нормально.
---
# Готовый текст запроса на отчёт
Ниже текст, который можно почти без правок отправить команде.
**Нужен полный архитектурный отчёт по текущей реализации бухгалтерского ассистента не в формате описания модулей, а в формате диагностической картины системы.
Отчёт должен показать, как устроены и где ограничены: data layer, ontology / lifecycle model, retrieval / execution, LLM decomposition, answer synthesis, state / memory, orchestration policy, observability / eval.
По каждому разделу нужно показать: как устроено сейчас, что реально работает, какие ограничения есть, что из-за этого ассистент не может сделать, какие симптомы уже видны в диалогах, какие нужны локальные изменения, какие нужны архитектурные изменения, и приоритет изменений P0/P1/P2.
Отдельно нужно ответить на ключевые вопросы: какие данные реально доезжают до LLM; где теряется детализация; есть ли полноценная модель сущностей, связей и lifecycle; какая единица retrieval используется как основная; что теряется на decomposition; почему explanation остаётся generic; есть ли state для многоходового анализа; может ли система исследовать соседние ветки учёта; какие архитектурные сущности отсутствуют и без них невозможно выйти на качественного ассистента.**
---
Если хочешь, я следующим сообщением сделаю из этого уже **сверхкороткий checklist на 1 экран**, чтобы ты просто кинул его как задачу на отчёт.

View File

@ -0,0 +1,772 @@
TZ_Assistant_Mode_retrievalLAYER.md
Да. Ниже даю **расширенный набор правил для retrieval/execution layer** именно под бухгалтерский контекст, чтобы `executeHybrid` и соседние executors не жили на одном `GUID` и одном `account_hint`, а реально тащили **избыточный набор ограничений и признаков**. Это прямо ложится на ваш текущий контур `route-specific retrieval` и `unified result schema`, который уже зафиксирован в vNext ТЗ.
Это не замена normalizer, а **расширение retrieval contract**: LLM/normalizer должен отдавать не только route, но и плотный набор бухгалтерских ограничителей, а retrieval обязан их исполнять.
---
# Расширение правил retrieval-слоя под бухгалтерский контекст
## 1. Главный принцип
Для бухгалтерского ассистента **нельзя** строить retrieval только по:
* `guid`
* `account_hint`
* общему route
* одной сущности верхнего уровня
Retrieval должен работать по **многослойному бухгалтерскому профилю запроса**.
Минимальный обязательный набор слоёв:
1. **счета / субсчета**
2. **тип участка учёта**
3. **тип документов**
4. **тип движения**
5. **тип связи между сущностями**
6. **тип аномалии / конфликта**
7. **период**
8. **контрагент / договор / объект / номенклатура / ОС / статья / подразделение**
9. **стадия жизненного цикла**
10. **исключающие условия**
11. **приоритет ранжирования**
12. **ожидаемый тип объяснения**
---
## 2. Новый обязательный retrieval contract
Каждый routed fragment должен передаваться в executor не в виде “route + hints”, а в виде расширенного `semantic_retrieval_profile`.
Примерно так:
```json
{
"fragment_id": "F1",
"route": "hybrid_store_plus_live",
"query_subject": "bank_settlement_mismatch",
"account_scope": ["51", "60"],
"subaccount_scope": [],
"domain_scope": ["bank", "settlements", "supplier_payments"],
"document_types": ["bank_statement", "payment_order", "receipt", "settlement_document"],
"entity_types": ["counterparty", "contract", "document", "posting"],
"period_scope": {
"from": null,
"to": null,
"granularity": "month"
},
"relation_patterns": [
"document_to_posting",
"payment_to_settlement",
"statement_to_document"
],
"lifecycle_stage_filters": [
"created",
"posted",
"closed",
"reconciled"
],
"anomaly_patterns": [
"missing_link",
"wrong_document_type",
"broken_closure",
"posting_mismatch"
],
"ranking_basis": [
"financial_impact",
"repeatability",
"closure_risk"
],
"excluded_interpretations": [
"simple_payment_delay",
"amount_only_anomaly"
],
"explanation_focus": [
"why_selected",
"where_chain_breaks",
"what_business_risk"
]
}
```
---
## 3. Общие правила для всех retrieval executorов
## 3.1. Поиск всегда строится не по одному измерению, а по пересечению признаков
Использовать одновременно:
* счёт
* тип документа
* статус документа
* связанную проводку
* период
* предметную область
* relation pattern
* anomaly pattern
Недопустимо:
если нет `guid`, снимать ограничения и лить в ответ весь массив.
Если `guid` нет, retrieval должен переходить на **semantic narrowing**, а не на `full scan without accounting discrimination`.
---
## 3.2. Любой account hint должен разворачиваться в бухгалтерский контекст
Если в вопросе или normalizer output есть `51`, этого недостаточно.
Нужно автоматически достраивать:
* bank domain
* возможные документы: выписка, платежное поручение, списание с р/с, поступление на р/с
* возможные связи: банк → документ → проводка → расчётный контур
* возможные конфликты: документ есть / нет, проводка есть / нет, не тот тип документа, неверный контур закрытия
То есть `51` — это не фильтр, а **семантический вход в набор поисковых правил**.
---
## 3.3. Если вопрос про аномалию, нельзя ранжировать только по сумме
Если пользователь не просил explicitly “самые большие суммы”, то anomaly retrieval обязан учитывать не только money magnitude, но и:
* отсутствие связанной записи;
* рассогласование между документом и проводкой;
* нарушение жизненного цикла;
* повторяемость отклонения;
* влияние на закрытие периода;
* несоответствие карточки и движения;
* конфликт между участками учёта.
---
## 3.4. Если вопрос про цепочку, retrieval должен искать разрыв, а не просто соседние документы
Для `hybrid_store_plus_live` поиск обязан различать:
* наличие документов в цепочке;
* корректность связей между ними;
* полноту прохождения этапов;
* расхождение статусов;
* конфликт проводки и первички;
* отсутствие ожидаемого продолжения;
* наличие “ложного закрытия” не тем документом.
Это соответствует самой идее `hybrid_store_plus_live` в вашем vNext ТЗ: causal / cross-entity / chain queries должны возвращать связи, документы, оплаты, проводки и признаки разрыва цепочки.
---
# 4. Расширенные бухгалтерские измерения для retrieval
## 4.1. Слой счетов
Нужно учитывать не только основной счёт, но и:
* корреспондирующие счета;
* типовой соседний контур;
* признак “счёт упомянут явно” vs “счёт выведен косвенно”;
* приоритет основного счёта;
* исключённые счета.
### Примеры:
* `51` → банк, движения ДС, документы банка, платежи, выписки
* `60` → поставщики, расчёты, закрытие обязательств
* `62` → покупатели, выручка, оплаты, задолженность
* `76` → прочие расчёты, подвешенные взаимосвязи, нестандартные хвосты
* `97` → расходы будущих периодов, сроки списания, график, корректность продолжения жизненного цикла
* `01/02/08`ОС, ввод в эксплуатацию, амортизация, капитальные вложения, карточка объекта
* `19/68` → НДС, принятие к вычету, книга покупок/продаж, закрытие налогового контура
* `10/41/43` → ТМЦ, товар, выпуск, остатки, движение запасов
* `20/23/25/26/44` → затраты, распределение, закрытие, себестоимость
---
## 4.2. Слой участков учёта
Для каждого запроса нужно фиксировать `domain_scope`:
* банк
* поставщики
* покупатели
* склад / запасы
* ОС
* НМА
* РБП
* НДС
* зарплата
* затраты
* закрытие периода
* прочие расчёты
* договорной контур
* авансы
* налоги
* денежные документы
* касса
* производство
Это нужно, чтобы один и тот же счёт не тянул нерелевантный участок.
---
## 4.3. Слой документов
Retrieval обязан учитывать тип документа как first-class filter.
Категории:
* банковская выписка
* платежное поручение
* поступление товаров/услуг
* реализация
* счет-фактура
* корректировка
* акт сверки
* авансовый отчет
* кассовый ордер
* ввод в эксплуатацию ОС
* принятие к учету ОС
* начисление амортизации
* списание РБП
* закрытие месяца
* регламентная операция
* ручная операция
* корректировка долга
* перемещение / списание / оприходование ТМЦ
Если пользователь спрашивает про “не тем документом закрыто”, retrieval обязан искать **ошибочный тип документа в ожидаемой цепочке**, а не просто все связанные документы.
---
## 4.4. Слой сущностей
Поддерживать entity filters:
* контрагент
* договор
* организация
* документ
* проводка
* номенклатура
* склад
* подразделение
* сотрудник
* объект ОС
* статья затрат
* проект
* заказ
* налоговая запись
* регистр/движение
* период закрытия
Если вопрос multi-entity, retrieval обязан различать **главную сущность** и **подтверждающие сущности**.
---
## 4.5. Слой жизненного цикла
Очень важный слой.
Почти все хорошие бухгалтерские аномалии — это не “не та сумма”, а **сломанный lifecycle**.
Для каждой сущности retrieval должен пытаться определить стадию:
* создан
* проведён
* не проведён
* частично связан
* закрыт
* не закрыт
* сторнирован
* отменён
* завис
* повторён
* перенесён
* закрыт не тем документом
* есть продолжение / нет продолжения
* цепочка оборвана
* финальный статус противоречит промежуточным движениям
Это особенно критично для:
* 97
* ОС
* поставщики/авансы
* НДС
* закрытие периода
* расчёты по 60/62/76
---
# 5. Набор anomaly patterns, которые надо заложить в retrieval
Ниже даю именно как **словарь аномалий**, который можно использовать в `semantic_retrieval_profile.anomaly_patterns`.
## 5.1. Missing link
Ожидаемая связь между сущностями отсутствует.
Примеры:
* документ есть, проводки нет;
* платёж есть, закрывающего документа нет;
* ОС есть, начисления есть/нет, но карточка не бьётся;
* запись в одном контуре есть, в смежном нет.
## 5.2. Wrong document type
Закрытие, отражение или продолжение прошло не тем типом документа.
## 5.3. Broken lifecycle
Объект застрял между стадиями.
## 5.4. Posting mismatch
Проводка не соответствует документу, счёту, участку или ожидаемому направлению движения.
## 5.5. Cross-domain inconsistency
Один участок говорит одно, другой — другое.
Примеры:
* банк подтверждает движение, расчётный контур не закрылся;
* документ прихода есть, складское движение не совпадает;
* ОС в карточке в одном статусе, амортизационный контур в другом.
## 5.6. Closure risk
Запись влияет на корректность закрытия периода.
## 5.7. Repeated anomaly
Одна и та же проблема повторяется серийно.
## 5.8. Silent orphan
Запись выглядит спокойной по сумме, но сиротская по связям и жизненному циклу.
## 5.9. Amount-independent risk
Риск не по величине суммы, а по структуре нарушения.
## 5.10. Manual intervention suspicion
Есть признаки ручной коррекции, нестандартной операции или обходного закрытия.
---
# 6. Правила дифференциации по ключевым бухгалтерским зонам
## 6.1. Банк / 51
Если запрос касается 51, retrieval должен автоматически учитывать:
* банковские документы;
* платежные поручения;
* выписки;
* приход/расход ДС;
* связанный расчётный контур;
* закрытие обязательства;
* аванс / оплата / зачет;
* корреспондирующие счета;
* совпадение назначения платежа и учетного отражения;
* факт отражения в выписке;
* факт проведения документа;
* разрыв между движением денег и закрытием расчета.
### Специальные паттерны:
* оплата есть, закрытия обязательства нет;
* выписка есть, документ-основание не найден;
* движение пошло не в тот договор / не в того контрагента;
* банк закрылся, а расчеты зависли;
* проблема не в платеже, а в документе, который интерпретировал платёж.
---
## 6.2. Поставщики / 60
Учитывать:
* контрагент;
* договор;
* документ поступления;
* оплата;
* зачет аванса;
* закрытие обязательства;
* просроченный хвост;
* расхождение по актам/счётам/поступлению/оплате;
* зависшие незакрытые позиции;
* повторяемость по контрагенту;
* влияние на закрытие периода.
### Специальные паттерны:
* хвост без движения после оплаты;
* поступление есть, расчёт не закрылся;
* аванс есть, поставка не бьётся;
* несколько документов спорят за одну оплату;
* один контрагент серийно создаёт одинаковый дефект.
---
## 6.3. Покупатели / 62
Учитывать:
* реализация;
* оплата;
* аванс;
* зачет;
* дебиторка;
* договор;
* закрытие выручки;
* документ основания;
* банковое подтверждение;
* НДС-связка.
### Паттерны:
* оплата есть, реализация не закрылась;
* аванс завис;
* дебиторка выглядит искусственно живой;
* проводка есть, но цепочка первички неполна.
---
## 6.4. Прочие расчёты / 76
Зона повышенного риска.
Retrieval должен относиться к 76 как к зоне, где часто лежат:
* нестандартные хвосты;
* временные костыли;
* переходные записи;
* плохо закрытые взаимосвязи.
Нужны более жёсткие правила на:
* давность;
* отсутствие продолжения;
* слабую связанность;
* нетипичный тип документа;
* ручные операции;
* странные корреспонденции.
---
## 6.5. РБП / 97
Если запрос про 97, retrieval обязан смотреть не только остаток и сумму, но и:
* дату возникновения;
* срок списания;
* график списания;
* факт начала списания;
* факт завершения списания;
* разрыв между сроком и поведением записи;
* документ-источник;
* связанный объект/основание;
* продолжение жизненного цикла.
### Паттерны:
* запись давно живёт, но логика списания не выполняется;
* срок наступил, движения нет;
* объект уже должен был перейти в другой статус;
* карточка/основание есть, но дальше жизни нет;
* запись выглядит закрытой формально, но по связанным движениям не завершена.
---
## 6.6. ОС / 01 / 02 / 08
Retrieval обязан учитывать:
* карточку ОС;
* документ принятия к учету;
* ввод в эксплуатацию;
* инвентарный объект;
* амортизацию;
* счет 08 как источник;
* корректность перехода 08 → 01;
* статус эксплуатации;
* списание/выбытие;
* соответствие карточки и начислений.
### Паттерны:
* объект заведен, но lifecycle не доведён;
* амортизация идет/не идет в противоречии со статусом;
* карточка есть, движений недостаточно;
* ввод в эксплуатацию и отражение по счетам не совпадают;
* объект сиротский по связям.
---
## 6.7. НДС / 19 / 68
Учитывать:
* счёт-фактура;
* поступление/реализация;
* принятие к вычету;
* книга покупок/продаж;
* период вычета;
* отражение в налоговом контуре;
* связь с первичкой;
* расхождение между бухгалтерским и налоговым отражением.
### Паттерны:
* НДС висит без нормального продолжения;
* документ есть, вычет не бьётся;
* вычет попал не в тот период;
* запись живёт без подтверждающей цепочки;
* топ по НДС не должен подменять вопросы про ОС или банк.
---
## 6.8. Закрытие периода
Если пользователь спрашивает о проблемах, “ломающих период”, retrieval должен повышать вес признаков:
* незакрытый хвост;
* конфликт между участками;
* документ/движение в граничных датах;
* сиротские остатки;
* серия однотипных нарушений;
* объекты, затрагивающие регламентные операции;
* следы ручных коррекций;
* зависшие авансы/расчёты/РБП/ОС.
---
# 7. Правила relation patterns для hybrid / chain retrieval
Для `executeHybrid` нужны предопределённые relation patterns.
## 7.1. payment_to_settlement
Платёж ↔ расчёт ↔ закрытие обязательства
## 7.2. document_to_posting
Документ ↔ проводка
## 7.3. statement_to_document
Банковская выписка ↔ внутренний документ
## 7.4. asset_card_to_depreciation
Карточка ОС ↔ начисление амортизации
## 7.5. deferred_expense_to_writeoff
РБП ↔ график/списание
## 7.6. invoice_to_vat
Первичка ↔ счёт-фактура ↔ НДС
## 7.7. receipt_to_stock_movement
Поступление ↔ складское движение
## 7.8. contract_to_documents
Договор ↔ серия документов / хвостов / закрытий
Если route chain-based, executor должен явно понимать, **какой relation pattern проверяет**, а не просто тянуть “всё около темы”.
---
# 8. Правила ранжирования
Нужен не один ranking score, а составной.
## 8.1. Базовые факторы ранжирования
* финансовое влияние
* давность
* повторяемость
* степень незавершённости lifecycle
* количество сломанных связей
* влияние на закрытие периода
* пересечение с несколькими участками учёта
* типовая/нетиповая проводка
* ручное вмешательство
* наличие противоречащих документов
* отсутствие ожидаемого продолжения
## 8.2. Важное правило
Если пользователь спрашивает “не по сумме, а по риску”, то фактор суммы не должен быть доминирующим.
---
# 9. Excluded interpretations — обязательный блок
Очень полезный слой.
Чтобы retrieval не ехал в ближайшую похожую тему, надо явно передавать, **что нельзя считать ответом**.
Примеры:
* не считать любую большую сумму аномалией;
* не сводить вопрос про ОС к НДС;
* не сводить вопрос про 51 к рейтингу контрагентов вообще;
* не считать любой незакрытый документ доказательством хвоста без проверки связи;
* не отвечать на вопрос про lifecycle просто списком документов;
* не подменять “не тем документом закрыто” на “есть документы по контрагенту”.
---
# 10. Что добавить в executeHybrid прямо практически
Ниже уже почти как техзадание на реализацию.
## 10.1. Если GUID нет, не брать весь набор данных
Вместо этого строить фильтрацию по пересечению:
* `account_scope`
* `domain_scope`
* `document_types`
* `entity_types`
* `relation_patterns`
* `anomaly_patterns`
* `period_scope`
* `excluded_interpretations`
## 10.2. Ввести `query_subject`
Категории верхнего уровня, например:
* `bank_settlement_mismatch`
* `supplier_tail_analysis`
* `customer_closure_gap`
* `deferred_expense_lifecycle_anomaly`
* `fixed_asset_card_mismatch`
* `vat_chain_conflict`
* `period_closure_risk`
* `cross_entity_breakage`
* `document_posting_conflict`
## 10.3. Ввести `explanation_focus`
Чтобы retrieval сразу собирал evidence под тип ответа:
* `why_selected`
* `where_chain_breaks`
* `why_risky`
* `why_not_closed`
* `why_ranked_high`
* `what_conflicts_with_what`
## 10.4. Ввести `evidence pack`
На каждый top item возвращать не только сущность, но и:
* ключевой счёт
* документ
* статус
* связанная проводка
* отсутствующая связь
* lifecycle gap
* period impact
* selection_reason
* business_interpretation
---
# 11. Что добавить в result schema
Расширить unified result schema, чтобы answer composer мог собирать нормальный ответ по сути, а не по name/count/id. Это согласуется с вашим целевым слоем `Result normalization` и `Final Answer Composer`.
```json
{
"fragment_id": "F1",
"route": "hybrid_store_plus_live",
"status": "ok",
"result_type": "chain",
"items": [
{
"entity_type": "counterparty",
"entity_id": "....",
"label": "ООО ...",
"account_context": ["51", "60"],
"document_context": ["bank_statement", "payment_order"],
"selection_reason": [
"оплата отражена",
"закрывающий документ не подтвержден",
"цепочка расчета оборвана"
],
"risk_factors": [
"broken_lifecycle",
"missing_link",
"closure_risk"
],
"business_interpretation": "деньги прошли, но обязательство не закрылось корректно",
"evidence": [],
"confidence": "medium"
}
],
"summary": {
"ranking_basis": ["closure_risk", "repeatability", "financial_impact"]
},
"errors": []
}
```
---
# 12. Минимальный набор бухгалтерских правил, который я бы считал must-have
Если прямо урезать до ядра, то retrieval должен обязательно учитывать:
1. `account_scope`
2. `domain_scope`
3. `document_types`
4. `entity_types`
5. `relation_patterns`
6. `anomaly_patterns`
7. `lifecycle_stage_filters`
8. `period_scope`
9. `excluded_interpretations`
10. `ranking_basis`
11. `explanation_focus`
Без этого вы снова получите:
нормализатор понял вопрос,
route выбрали правильно,
а retrieval принёс один и тот же массив “вокруг темы”.
---
# 13. Короткая формулировка для команды
Если тебе нужен короткий месседж в работу, то вот он:
**Нужно расширить executeHybrid и соседние retrieval executors от модели “GUID-or-full-scan” к модели “semantic retrieval profile”.
Фильтрация должна строиться не только по счету, но и по участку учёта, типам документов, типам сущностей, relation patterns, lifecycle stage, anomaly patterns, excluded interpretations и ranking basis.
Если GUID отсутствует, executor не имеет права брать весь датасет; он обязан делать semantic narrowing по бухгалтерскому профилю запроса.**
---
# 14. Что я бы добавил ещё сверх этого
Отдельно заложить в retrieval слой:
* транслит-нормализацию бухгалтерских терминов до исполнения;
* словарь синонимов по участкам учёта;
* бухгалтерские alias-паттерны: “хвост”, “зависло”, “не бьётся”, “не тем закрылось”, “жизни не случилось”, “сломало период”, “сирота”, “висит”, “криво пошло”;
* map “бытовой язык → retrieval constraints”.
Это уже не про answer composer, а про то, чтобы retrieval получал нормальный предметный профиль даже из кривого человеческого запроса.

View File

@ -0,0 +1,541 @@
TZ_Assistant_Mode_vNext.md
## Переход от route-plan ответа к нормальному диалоговому ответу по данным базы
---
## 1. Цель этапа
Превратить текущий Assistant Mode из технического интерфейса, который показывает:
* decomposition,
* route plan,
* trace,
* sandbox-пояснения,
в рабочий диалоговый режим, который:
* принимает вопрос пользователя;
* прогоняет его через normalizer/decomposition;
* выполняет реальный retrieval/query по маршруту;
* получает фактические данные;
* собирает **человекочитаемый ответ на русском языке**;
* показывает debug только как дополнительный раскрываемый слой.
---
## 2. Текущее состояние
Сейчас Assistant Mode работает как **planner/debug shell**:
**что уже есть:**
* ввод вопроса;
* decomposition / route selection;
* trace id;
* debug drawer;
* session-based диалоговая оболочка.
**что отсутствует:**
* реальный factual retrieval по данным базы;
* route-specific execution с возвратом нормальных данных;
* финальный answer composition по результатам retrieval;
* русскоязычный user-facing reply policy;
* разделение user-answer и technical-debug.
Комментарий:
Сейчас пользователь получает “planned routes / sandbox retrieval mode / trace”,
а должен получать ответ по сути вопроса.
---
## 3. Новая целевая архитектура
Нужен полный контур:
**User message → Normalizer → Execution Planner → Route-specific Retrieval → Result Normalization → Final Answer Composer → Assistant Reply**
---
## 4. Что должно происходить после сообщения пользователя
## 4.1. Шаг 1 — Normalization
Оставляем текущий `normalizer_v2.x`:
* scope filter;
* fragments;
* execution readiness;
* route status;
* clarification/out-of-scope detection.
## 4.2. Шаг 2 — Execution planning
Для каждого исполнимого fragment:
* определить маршрут;
* собрать execution plan;
* передать fragment в route-specific executor.
## 4.3. Шаг 3 — Real retrieval / query
Вместо sandbox plan нужно выполнять **реальный вызов backend data layer**:
* к 1С / хранилищу / API / query handlers;
* получать фактические данные;
* нормализовать их в общий result schema.
## 4.4. Шаг 4 — Final answer composition
На основе:
* user message,
* fragments,
* data results,
* fallback state
нужно собрать **один нормальный ответ для пользователя**.
---
# 5. Главный принцип нового режима
## Пользователь не должен видеть:
* planned routes;
* store canonical selected;
* sandbox retrieval mode;
* raw trace explanation;
* транслит;
* служебные внутренние названия маршрутов.
## Пользователь должен видеть:
* нормальный русский ответ;
* если данных нет — понятное объяснение;
* если нужен clarification — уточняющий вопрос;
* если часть вопроса вне контура — вежливое ограничение;
* при желании — кнопку “Показать технический разбор”.
---
# 6. Новый слой: Route-specific factual retrieval
---
## 6.1. Для каждого route нужен executor
Нужно реализовать route executors как отдельные backend handlers.
### A. `hybrid_store_plus_live`
Для causal / cross-entity / chain queries:
* искать связанные сущности;
* возвращать:
* контрагент/объект,
* документы,
* оплаты,
* проводки,
* подтверждающие связи,
* признаки разрыва цепочки.
### B. `store_feature_risk`
Для anomaly / rule-check / suspicious scan:
* искать проблемные объекты/записи;
* возвращать:
* сущность,
* причина попадания,
* риск-признак,
* релевантные поля.
### C. `batch_refresh_then_store`
Для overview / ranking / top / period risk:
* возвращать агрегаты;
* топы;
* summary blocks;
* приоритеты проверки;
* зоны риска.
### D. `store_canonical`
Для canonical factual path:
* выдавать прямую фактическую информацию по каноническому учётному представлению.
### E. `live_mcp_drilldown` (если уже подключён)
Для точечного drilldown:
* конкретный документ;
* конкретная проводка;
* конкретный объект;
* source-of-record.
---
## 6.2. Все executors должны возвращать unified result schema
Нужен единый формат результата, чтобы answer composer не зависел от route руками.
Примерно так:
```json
{
"fragment_id": "F1",
"route": "hybrid_store_plus_live",
"status": "ok | empty | partial | error",
"result_type": "list | summary | object | chain | ranking",
"items": [],
"summary": {},
"evidence": [],
"errors": []
}
```
---
# 7. Новый слой: Result normalization
Перед финальной генерацией ответа retrieval-результаты нужно привести в нормализованный вид.
Нужен слой:
* `normalize_retrieval_result(fragment, raw_backend_result)`
Задача:
* привести все маршруты к общему формату;
* отдать composer понятный и предсказуемый payload.
---
# 8. Новый слой: Final Answer Composer
Это ключевой этап.
## 8.1. Что он получает
На вход:
* `user_message`
* `normalized_query_v2_x`
* `resolved_fragments`
* `retrieval_results`
* `fallback state`
* `session context`
## 8.2. Что он должен делать
Собирать **один связный user-facing ответ**.
### Если один fragment
Отвечать прямо по вопросу.
### Если несколько fragments
Собирать один ответ с понятной структурой:
* сначала общий вывод;
* потом блоки по подзадачам;
* без ощущения, что это 5 несвязанных кусков.
### Если часть вопроса не выполнима
Отвечать по доступной части и мягко пояснять ограничения.
---
## 8.3. Ответ должен быть на русском
Жёсткое требование:
* никакого транслита;
* никаких англоязычных route names в тексте;
* никаких внутренних служебных формулировок.
---
## 8.4. Ответ не должен быть “сухой отладкой”
Нельзя выводить пользователю:
* `planned routes`
* `trace`
* `store canonical selected`
* `fallback_type=...`
Это всё — только в debug drawer.
---
# 9. Типы user-facing ответов
---
## 9.1. Normal factual reply
Когда данные найдены.
Пример:
* перечисление поставщиков с хвостами;
* список проблемных объектов;
* summary по рискам;
* ranking.
## 9.2. Empty result reply
Когда запрос корректен, но ничего не найдено.
Пример:
> По текущим данным явных проблемных записей по этому условию не найдено.
## 9.3. Partial reply
Когда по части вопроса данные найдены, по части — нет.
## 9.4. Clarification reply
Когда нужно уточнение.
## 9.5. Out-of-scope reply
Когда вопрос не про базу компании.
## 9.6. Backend error reply
Когда retrieval сломался технически.
Пример:
> Не удалось получить данные из контура. Попробуйте повторить запрос или уточнить формулировку.
---
# 10. Нужно разделить два уровня ответа
---
## 10.1. User Reply
То, что человек видит в чате.
## 10.2. Debug Payload
То, что раскрывается по кнопке:
* fragments
* execution readiness
* route status
* no_route_reason
* retrieval status
* trace id
Это должно жить отдельно.
---
# 11. Что изменить в GUI
---
## 11.1. Assistant message bubble
В ответе ассистента должен отображаться:
* нормальный текст;
* опционально таблица/список результатов, если уместно;
* без технического мусора.
## 11.2. Debug toggle
Кнопка:
* `Показать разбор`
или
* `Технические детали`
Внутри:
* decomposition;
* routes;
* fragments;
* trace id;
* backend status;
* fallback state.
## 11.3. Loading states
Во время выполнения:
* `Разбираю запрос`
* `Ищу данные`
* `Собираю ответ`
---
# 12. Что изменить в backend API
Нужен нормальный assistant endpoint, который возвращает уже готовый user reply.
## Пример
`POST /api/assistant/message`
### Request
```json
{
"session_id": "...",
"message": "...",
"mode": "assistant",
"period_hint": "...",
"business_context": "..."
}
```
### Response
```json
{
"ok": true,
"assistant_reply": "человекочитаемый ответ",
"reply_type": "factual | clarification | out_of_scope | partial | empty | error",
"debug": {
"trace_id": "...",
"fragments": [...],
"routes": [...],
"retrieval_status": [...]
}
}
```
---
# 13. Что логировать
Для каждого сообщения логировать:
* `session_id`
* `message_id`
* `user_message`
* `normalizer_output`
* `execution_plan`
* `retrieval_calls`
* `retrieval_results_raw`
* `retrieval_results_normalized`
* `assistant_reply`
* `reply_type`
* `trace_id`
Это пригодится потом для field-based hardening.
---
# 14. Минимальный MVP этого этапа
Чтобы не расползтись, можно сделать MVP так:
### Этап MVP-1
Сделать хотя бы 2 полноценных route executor:
* `hybrid_store_plus_live`
* `store_feature_risk`
И уже через них показать нормальный user-facing reply.
### Этап MVP-2
Добавить:
* `batch_refresh_then_store`
* `store_canonical`
### Этап MVP-3
Дополировать:
* partial replies
* better formatting
* richer debug drawer
---
# 15. Формат ответа ассистента
Ответ должен быть:
* на русском;
* коротким, но содержательным;
* с понятной привязкой к вопросу;
* без внутренних названий маршрутов.
### Пример плохого ответа
> Planned routes: store canonical. Sandbox mode. Trace...
### Пример допустимого ответа
> По текущему запросу найдены проблемные хвосты по нескольким поставщикам.
> Основные расхождения связаны с оплатами без корректного закрытия и неполной связкой документов.
> Ниже показаны контрагенты и проблемные цепочки.
---
# 16. Что НЕ делать на этом этапе
Не надо сейчас:
* строить суперсложную агентную систему;
* делать память на долгий диалог;
* пытаться решить все типы route сразу идеально;
* снова уходить в synthetic eval hardening;
* показывать пользователю внутреннюю кухню как основной результат.
---
# 17. Критерии приёмки
Этап считается принятым, если:
1. Assistant Mode возвращает **русский человекочитаемый ответ**, а не route plan;
2. debug остаётся доступен, но вынесен отдельно;
3. хотя бы для 2 routeов работает полный factual loop:
* question → retrieval → factual reply;
4. out-of-scope / clarification / partial fallback корректно отображаются в чате;
5. decomposition mode не сломан;
6. транслит и технический мусор исчезли из основного ответа.
---
# 18. Артефакты, которые должен выдать Codex
1. `docs/assistant_mode_vnext_spec.md`
2. `docs/route_executor_contracts.md`
3. `docs/final_answer_composer_spec.md`
4. backend endpoint для assistant reply
5. unified retrieval result schema
6. GUI update для user-facing reply + debug drawer
7. `docs/known_limits_current_routes.md`
---
# 19. Короткий итог
Следующий шаг — это уже **не normalizer**, а:
**сделать так, чтобы ассистент после нормализации реально ходил за фактами и отвечал человеку по сути, а не показывал внутренний план работы.**
Если хочешь, я следующим сообщением сделаю ещё **короткую жёсткую версию этого ТЗ для вставки в Codex**.

View File

@ -0,0 +1,438 @@
TZ_Assistant_Mode_vNext2.md
Да, даю уже в формате **жёсткой вставки в ТЗ**.
Ниже блок, который логично добавлять как отдельный раздел после текущего `Final Answer Composer / answer policy`, потому что в текущем контуре уже есть правильная базовая архитектура, но composer пока остаётся operational, retrieval — sandbox/stubbed, а пользовательский ответ не обязан доказывать основание отбора и полноту покрытия вопроса. Это прямо следует из текущего vNext ТЗ и архитектурного отчёта.
---
# Дополнение к ТЗ: explainable answer layer и контроль полноты декомпозиции
## 1. Цель дополнения
Закрыть два критических продуктовых дефекта Assistant Mode:
1. ассистент выдаёт формально человекочитаемый, но смыслово пустой ответ без объяснения, почему именно эти объекты, записи или цепочки были отобраны;
2. ассистент теряет части многосоставного пользовательского запроса на этапе decomposition/routing и отвечает только на отдельный фрагмент, создавая ложное ощущение, что задача решена.
Цель этого этапа — сделать так, чтобы Assistant Mode:
* отвечал **по смыслу бухгалтерского вопроса**;
* объяснял **основания отбора и вывода**;
* гарантировал **покрытие всех существенных частей запроса**;
* не подменял пользовательский вопрос ближайшим технически доступным маршрутом.
---
## 2. Что фиксируем как обязательный продуктовый контракт
Считать ответ ассистента корректным можно только тогда, когда одновременно выполнены четыре условия:
1. ответ относится именно к предмету вопроса пользователя, а не к соседнему маршруту;
2. ответ объясняет, **почему** найденные записи/объекты/контрагенты попали в результат;
3. ответ показывает, **на каких признаках и фактах** основан вывод;
4. ответ закрывает **все существенные части** многосоставного запроса либо явно помечает, что часть вопроса не покрыта.
Если хотя бы одно из этих условий не выполнено, ответ считается непригодным даже при технически успешном pipeline.
---
## 3. Новый обязательный слой: Explainable Answer Layer
После `Result Normalization` и до финального `Assistant Reply` должен быть отдельный обязательный слой:
**Explainable Answer Layer**
Новый полный контур:
**User message → Normalizer → Requirement Extraction → Execution Planning → Route-specific Retrieval → Result Normalization → Explainable Answer Layer → Final Answer Composer → Assistant Reply**
Задача этого слоя:
* восстановить пользовательское намерение в нормальной человекочитаемой форме;
* проверить полноту покрытия вопроса;
* проверить предметную релевантность ответа;
* собрать не просто summary, а **объяснимый вывод**.
---
## 4. Новый обязательный контракт декомпозиции
### 4.1. Декомпозиция должна выделять не только fragments, но и requirements
Нужно разделить два уровня:
### A. `requirements`
Смысловые требования пользователя.
Примеры:
* найти поставщиков с хвостами;
* объяснить, почему они считаются проблемными;
* показать, где именно разрыв по цепочке;
* отделить аномалии по риску от аномалий по сумме.
### B. `execution_fragments`
Технические единицы исполнения, которые маршрутизируются по route.
Важно:
один `requirement` может порождать несколько `execution_fragments`;
несколько fragments могут обслуживать один requirement.
Без этого разделения многосоставные вопросы будут и дальше схлопываться в один ближайший route.
---
### 4.2. Для каждого пользовательского сообщения нужен coverage report
После decomposition система обязана формировать объект:
```json
{
"requirements_total": 0,
"requirements_covered": 0,
"requirements_uncovered": [],
"requirements_partially_covered": [],
"clarification_needed_for": [],
"out_of_scope_requirements": []
}
```
Это внутренний контракт pipeline.
Без него нельзя считать, что система поняла вопрос.
---
### 4.3. Запрещён silent loss of intent
Если в исходном сообщении было несколько требований, а в execution plan попала только часть, система не имеет права:
* молча проигнорировать остальные части;
* выдавать ответ так, будто задача полностью выполнена;
* считать вопрос закрытым.
В этом случае допустимы только три режима:
1. **полный ответ** — если покрыто всё;
2. **partial answer** — если покрыта только часть и это явно сказано;
3. **clarification** — если без уточнения нельзя корректно закрыть все требования.
---
## 5. Новый обязательный контракт retrieval result
Текущий unified result schema нужно расширить. Сейчас в нём есть хороший базовый каркас, но для объяснимого ответа его недостаточно.
Каждый route executor должен возвращать не только данные, но и признаки, объясняющие включение объекта в ответ.
### Обязательные поля результата
```json
{
"fragment_id": "F1",
"requirement_ids": ["R1"],
"route": "store_feature_risk",
"status": "ok | empty | partial | error",
"result_type": "list | summary | object | chain | ranking",
"items": [],
"summary": {},
"evidence": [],
"why_included": [],
"selection_reason": [],
"risk_factors": [],
"business_interpretation": [],
"confidence": "high | medium | low",
"limitations": [],
"errors": []
}
```
---
### 5.1. Смысл новых полей
`why_included`
Почему объект вообще попал в выборку.
`selection_reason`
По каким конкретным правилам, фильтрам, признакам или сопоставлениям он был отобран.
`risk_factors`
Какие именно признаки риска, расхождения, аномалии или разрыва цепочки обнаружены.
`business_interpretation`
Как это интерпретируется в бухгалтерском или операционном смысле.
`confidence`
Оценка уверенности результата.
`limitations`
Что система не смогла подтвердить или где данные неполны.
---
## 6. Новый обязательный слой: Answer Grounding Check
Перед выдачей финального ответа должен выполняться `Answer Grounding Check`.
### Его задача:
проверить, что итоговый ответ действительно опирается на retrieval results и не съехал по предметной области.
### Проверяемые условия:
1. ключевые сущности ответа присутствуют в retrieval results;
2. ключевые выводы ответа подтверждены `evidence` и `selection_reason`;
3. предмет ответа совпадает с предметом вопроса;
4. если пользователь спрашивал про ОС, ответ не может быть собран по НДС;
5. если пользователь спрашивал про 97 счёт, ответ не может уйти в общий рейтинг контрагентов без объяснения связи;
6. если пользователь спрашивал про аномалию, ответ обязан назвать признак аномалии.
Если проверка не проходит, ответ не отправляется в user-facing канал и должен перейти в один из fallback-режимов:
* clarification;
* partial with limitation;
* technical error;
* no-grounded-answer.
---
## 7. Новый обязательный стандарт user-facing ответа
Каждый нормальный factual answer должен содержать не просто summary, а следующие смысловые элементы:
### 7.1. Итоговый вывод
Короткий ответ на главный вопрос.
### 7.2. Основание отбора
Почему именно эти объекты попали в ответ.
### 7.3. Подтверждающие признаки
Какие факты, поля, несоответствия, связи или разрывы обнаружены.
### 7.4. Практический смысл
Что это означает для бухгалтера или что именно здесь выглядит проблемным.
### 7.5. Ограничения
Что не удалось проверить полностью, если такое есть.
### 7.6. Следующее действие
Что стоит посмотреть или проверить дальше.
---
### 7.2. Требование к стилю ответа
Ответ должен быть:
* на русском;
* коротким, но содержательным;
* предметным;
* без route names;
* без debug-лексики;
* без пустых формулировок типа “по запросу найдены данные”;
* без перечисления ссылок без пояснения, зачем они показаны.
---
## 8. Обязательные шаблоны смысловой структуры ответа
### 8.1. Для anomaly / risk / suspicious scan
Ответ обязан содержать:
* что именно признано аномалией;
* по каким признакам это признано аномалией;
* чем запись отличается от нормального поведения;
* какие объекты наиболее рискованные;
* в чём риск состоит практически.
### 8.2. Для chain / causal / cross-entity explanation
Ответ обязан содержать:
* какую цепочку система проверяла;
* где именно найден разрыв или конфликт;
* какие сущности участвуют в цепочке;
* почему это считается хвостом, разрывом или зависшей ситуацией.
### 8.3. Для ranking / overview / top
Ответ обязан содержать:
* принцип ранжирования;
* почему верхние позиции оказались наверху;
* какие признаки сильнее всего влияли на приоритет.
### 8.4. Для canonical factual reply
Ответ обязан содержать:
* прямой ответ по объекту;
* источник/основание;
* при необходимости — уточнение статуса, периода, документа, связанной записи.
---
## 9. Новый обязательный формат partial answer
Если закрыта не вся задача, partial reply обязан быть структурирован так:
1. что удалось проверить;
2. что именно не покрыто;
3. почему не покрыто;
4. нужно ли уточнение;
5. можно ли продолжить по оставшейся части.
Недопустимо:
выдать ответ по одному фрагменту без явной маркировки того, что другие части вопроса потеряны.
---
## 10. Новые reply types
К текущему набору reply/fallback типов нужно добавить или закрепить отдельно:
* `factual`
* `factual_with_explanation`
* `partial_coverage`
* `clarification_required`
* `empty_but_valid`
* `no_grounded_answer`
* `route_mismatch_blocked`
* `backend_error`
Это нужно, чтобы различать:
просто технически успешный ответ и
семантически валидный ответ с объяснимым основанием.
---
## 11. Что нужно изменить в логировании
К уже существующим логам надо добавить поля, которые позволят анализировать именно смысловую пригодность ответа, а не только route/fallback. Сейчас логирование уже есть, но его надо расширить для field hardening.
### Добавить в structured log:
* `requirements_extracted`
* `requirements_total`
* `requirements_covered`
* `requirements_uncovered`
* `coverage_status`
* `answer_grounding_status`
* `reply_semantic_type`
* `why_included_summary`
* `selection_reason_summary`
* `route_subject_match`
* `clarification_target`
* `dropped_intent_segments`
---
## 12. Что нужно изменить в UI
Debug drawer остаётся, но в основном ответе надо показывать не только итог, а и короткий explainable summary.
### В bubble ответа ассистента показывать:
* краткий вывод;
* короткий блок “почему это попало в ответ”;
* по необходимости — список объектов;
* по необходимости — блок “что проверить дальше”.
### В debug drawer переносить:
* raw fragments;
* route selection;
* trace id;
* fallback state;
* raw retrieval payloads;
* coverage report;
* grounding check result.
---
## 13. Новые критерии приёмки этапа
Этап считается принятым только если выполнены все условия:
1. ассистент отвечает по предмету вопроса, а не по ближайшему соседнему route;
2. каждый factual answer содержит объяснение, почему объект/запись/контрагент попал в ответ;
3. для каждого многосоставного вопроса система фиксирует coverage report;
4. система не теряет части вопроса молча;
5. если покрыта только часть — это явно отмечено в ответе;
6. если предмет ответа съехал — ответ блокируется как `route_mismatch_blocked` или `no_grounded_answer`;
7. anomaly-ответы объясняют признак аномалии, а не только выводят список сущностей;
8. chain-ответы объясняют разрыв цепочки, а не только перечисляют документы;
9. в основном ответе нет route names, trace, sandbox wording и другого внутреннего мусора;
10. debug остаётся доступен отдельно и не ломает основной UX.
---
## 14. Минимальный MVP этого дополнения
### MVP-1
Внедрить:
* `requirements` + `coverage report`;
* расширенный unified result schema;
* `Answer Grounding Check`;
* explainable answer contract хотя бы для:
* `store_feature_risk`
* `hybrid_store_plus_live`
### MVP-2
Добавить explainable templates для:
* `batch_refresh_then_store`
* `store_canonical`
### MVP-3
Дополировать:
* richer explanation formatting;
* confidence/limitations;
* better partial answer behavior;
* трассировку semantic failures.
---
## 15. Что считать провалом этапа
Этап не считается выполненным, если сохраняется хотя бы один из сценариев:
* пользователь спросил про аномалию, а получил список без признаков аномальности;
* пользователь задал многосоставной вопрос, а ассистент ответил только на один фрагмент без явной маркировки;
* ответ предметно съехал в другой участок учёта;
* ответ содержит ссылки, route names, trace или sandbox-пояснения вместо нормального объяснения;
* ассистент дал формально связный, но необоснованный вывод без основания отбора.
---
## 16. Короткий итог для разработчика
Задача следующего этапа — не просто улучшить текст ответа.
Задача этапа:
* сделать декомпозицию **полной**, а не частичной;
* сделать ответ **объяснимым**, а не декоративно-человекочитаемым;
* заставить ассистента **доказывать**, почему он выбрал именно эти записи;
* запретить выдачу ответа, если система не может подтвердить предметную релевантность и полноту покрытия вопроса.
---
Если хочешь, следующим сообщением я сразу соберу ещё и **сверхкороткую версию для Codex/разработчика на 1 экран**, без пояснений, только требования и acceptance criteria.

619
IN/TZ_Benchmark_v2.md Normal file
View File

@ -0,0 +1,619 @@
Да, давай сделаем следующий прогон уже **не “на каноничных вопросиках”**, а на более злой, предметной и местами провокационной выборке — ближе к тому, как человек реально разговаривает с бухгалтерским ассистентом. И да, архитектуру отчёта тоже сразу зафиксируем, чтобы на выходе был **один читаемый MD-файл**, который одновременно:
* удобно читать человеку,
* удобно парсить машине,
* удобно сравнивать между прогонами.
Опираюсь на выжимку из разговора с главбухом: ядро ценности — не ввод первички, а контроль, сверка, поиск расхождений, анализ счетов, предзакрытие периода и вопросно-ответный интерфейс поверх нормализованной модели 1С.
---
# Что предлагаю сделать
Новый прогон собрать как **Benchmark v2 / Creative Stress Run**, где вопросы будут:
* более длинные;
* более “человеческие”;
* с доменной неоднозначностью;
* с причинно-следственными цепочками;
* с несколькими сущностями сразу;
* с формулировками не “из теста”, а ближе к живому главбуху.
И на выходе генерировать **один отчёт `.md`** следующего типа:
---
# Архитектура итогового отчёта
## Имя файла
`benchmark_creative_stress_run_accounting_assistant_YYYY-MM-DD.md`
---
## Структура файла
### 1. Паспорт прогона
Короткий верхний блок:
```md
# Creative Stress Benchmark Run — Accounting Assistant
## Паспорт
- run_id: creative_stress_run_2026-03-23
- dataset_version: semantic_v2 + router_fix
- questions_total: 40
- benchmark_profile: creative_hard_human_like
- generated_from: accounting_automation_structured_notes
- mode: validation / stress / pilot-readiness
- executor: <pipeline or person>
- overall_status: pass / pass_with_notes / fail
```
### 2. Executive summary
Коротко по-человечески:
* что проверяли;
* что в целом показал прогон;
* где система сильна;
* где ещё спотыкается;
* готовность к следующему этапу.
### 3. Сводные метрики
Машиночитаемо и читаемо одновременно:
```md
## Сводные метрики
- route_mismatch_count:
- degraded_answers_count:
- batch_route_count:
- live_mcp_drilldown_count:
- hybrid_store_plus_live_count:
- store_canonical_count:
- store_feature_risk_count:
- avg_latency_ms:
- p95_latency_ms:
- pass_rate:
- strongest_zone:
- weakest_zone:
```
### 4. Сводка по классам вопросов
Разбивка:
* heavy_analytical
* cross_entity
* drilldown_explain
* period_close_risk
* document_reconciliation
* rule_based_account_control
* anomaly_probe
* ambiguous_human_query
### 5. Детальный блок Q/A
И вот это главный кусок. Для каждого вопроса — отдельный кейс в одном и том же формате.
---
# Формат одного кейса в отчёте
Вот рекомендованный шаблон.
```md
---
question_id: QH-01
question_class: cross_entity
difficulty: hard
domain_tags: [60, 62, сверка, документы, проводки, period_close]
expected_route: hybrid_store_plus_live
actual_route:
route_match:
latency_ms:
decision_flags:
needs_exact_object_trace:
needs_causal_chain:
needs_cross_entity_join:
needs_full_period_aggregation:
needs_ranking:
needs_anomaly_summary:
needs_runtime_truth:
freshness_sensitive:
ambiguous_object_scope:
store_sufficiency_confident:
precomputed_aggregate_available:
store_sufficiency:
canonical_sufficient:
feature_sufficient:
risk_sufficient:
freshness_ok:
aggregate_level_ok:
ranking_ready:
explanation_ready:
reason_codes: []
answer_quality:
status: pass / partial / fail
confidence: high / medium / low
degraded: false
---
## QH-01. [Короткое название кейса]
**Вопрос:**
Покажи, по каким покупателям у нас на конец июня висят отгрузки без оплаты, и сразу свяжи это с реализациями, договорами и проводками, чтобы было видно, где именно хвост и чем он подтверждается.
**Что хотел проверить этот вопрос:**
Проверка связки 62 ↔ 90 ↔ документы реализации ↔ оплаты ↔ договоры ↔ проводки, то есть не плоский факт, а причинная цепочка.
**Почему вопрос сложный:**
Требует одновременно:
- cross-entity join,
- causal stitching,
- period-end cut,
- объяснимую цепочку источников.
**Как система должна была это понять:**
Это не simple factual и не чистый trend.
Это causal cross-entity scenario → ожидается `hybrid_store_plus_live`.
**Куда ожидали маршрут:**
`hybrid_store_plus_live`
**Куда реально пошёл маршрут:**
`...`
**Почему это корректно / некорректно:**
Короткий человеческий комментарий.
**Краткий ход решения системы:**
1. Определила периодный срез.
2. Определила сущности: покупатель, реализация, оплата, договор, проводка.
3. Проверила sufficiency canonical store.
4. Поняла, что нужен causal join.
5. Пошла в hybrid.
**Что ожидали увидеть в ответе:**
- список покупателей;
- сумма хвоста;
- документы реализации;
- отсутствие / наличие оплат;
- связка с договором;
- подтверждающие проводки;
- указание проблемного периода.
**Что реально получили:**
Короткий summary ответа.
**Вердикт по кейсу:**
pass / partial / fail
**Замечания:**
Что докрутить, если что.
```
---
# Почему такой формат хороший
Он одновременно:
* удобен для ручного чтения;
* достаточно структурирован для diff между прогонами;
* хранит route-логику рядом с человеческим комментарием;
* позволяет потом быстро делать выжимку по классам ошибок;
* пригоден как для внутреннего техотчёта, так и для демонстрации зрелости системы.
---
# Новый набор вопросов для Creative Stress Run
Ниже даю **40 вопросов**, уже заметно более жёстких и ближе к реальному бухгалтерам-режиму. Они построены из тех болей, что были в выжимке: 01/02, 97, 41, 51, 60, 62, 90, 10, сверки, незакрытые хвосты, предзакрытие периода, ошибки дат, отсутствие документов, причинная цепочка.
---
## Блок A. Предзакрытие периода
### QH-01
Собери мне предзакрытие июня по-человечески: какие счета сейчас выглядят самыми проблемными, где хвосты, где незакрытые куски и что надо проверить в первую очередь, если период закрывать сегодня.
**Класс:** heavy_analytical
**Ожидаемый route:** `batch_refresh_then_store`
### QH-02
Покажи не просто проблемные счета за июнь, а объясни, почему именно они попали в риск-зону: чем это вызвано, какими документами и какими типовыми ошибками это может быть связано.
**Класс:** heavy_analytical
**Ожидаемый route:** `batch_refresh_then_store`
### QH-03
Сделай рейтинг самых опасных хвостов на конец июня, чтобы было видно, какие из них могут реально аукнуться при сдаче отчётности, а какие просто технический шум.
**Класс:** heavy_analytical
**Ожидаемый route:** `batch_refresh_then_store`
### QH-04
Есть ли у нас на конец июня такие расхождения, которые визуально небольшие, но по природе похожи на системную ошибку, а не на разовый косяк?
**Класс:** heavy_analytical / anomaly_probe
**Ожидаемый route:** `batch_refresh_then_store`
### QH-05
Если бы ты был главбухом и у тебя было полчаса перед закрытием периода, какие пять зон ты бы открыл первыми и почему?
**Класс:** heavy_analytical + explain
**Ожидаемый route:** `batch_refresh_then_store`
---
## Блок B. 60 / 62 / сверки / хвосты
### QH-06
Покажи, по каким поставщикам у нас не бьются взаиморасчёты, и разложи это не только по суммам, а по конкретным документам, оплатам и отсутствующим закрывающим.
**Класс:** cross_entity
**Ожидаемый route:** `hybrid_store_plus_live`
### QH-07
По каким покупателям у нас есть отгрузка, но нет оплаты или нормального закрытия, и как это выглядит в цепочке документ → проводка → взаиморасчёт?
**Класс:** cross_entity
**Ожидаемый route:** `hybrid_store_plus_live`
### QH-08
Где по 60 и 62 счётам у нас не просто долг висит, а есть ощущение, что отсутствует какой-то первичный документ, из-за чего цепочка развалилась?
**Класс:** cross_entity / explain
**Ожидаемый route:** `hybrid_store_plus_live`
### QH-09
Покажи мне контрагентов, по которым расхождение выглядит подозрительно: то есть не обычная задержка, а именно такая история, где документы, оплаты и сальдо друг другу не соответствуют.
**Класс:** cross_entity / anomaly_probe
**Ожидаемый route:** `hybrid_store_plus_live`
### QH-10
Сделай список контрагентов, по которым акт сверки явно напрашивается в первую очередь, и покажи, из каких документов и хвостов это складывается.
**Класс:** cross_entity / period_close_risk
**Ожидаемый route:** `hybrid_store_plus_live`
---
## Блок C. 41 / товары / приход / реализация
### QH-11
Где товар уже ушёл в реализацию, а нормального прихода под него в учёте не видно, и как эта проблема выглядит по датам, документам и поставщикам?
**Класс:** cross_entity
**Ожидаемый route:** `hybrid_store_plus_live`
### QH-12
Найди мне такие продажи, где себестоимость или приход выглядят подозрительно, как будто накладная запоздала, документ не дошёл или дата заведена криво.
**Класс:** cross_entity / anomaly_probe
**Ожидаемый route:** `hybrid_store_plus_live`
### QH-13
Покажи отрицательные или аномальные остатки по товарам не просто списком, а с объяснением, какая цепочка событий к этому привела.
**Класс:** drilldown_explain / cross_entity
**Ожидаемый route:** `hybrid_store_plus_live`
### QH-14
Есть ли такие товарные позиции, где проблема тянется не один раз, а повторяется по схожему паттерну: продажа раньше прихода, странный разрыв по датам, неподложенные документы?
**Класс:** heavy_analytical / anomaly_probe
**Ожидаемый route:** `batch_refresh_then_store`
### QH-15
Покажи, какие складские или товарные хвосты на конец июня больше всего искажают картину периода.
**Класс:** heavy_analytical
**Ожидаемый route:** `batch_refresh_then_store`
---
## Блок D. 97 / расходы будущих периодов
### QH-16
По каким расходам будущих периодов у нас задан срок так, что списание либо идёт криво, либо вообще не должно было так выглядеть?
**Класс:** rule_based_account_control
**Ожидаемый route:** `store_feature_risk`
### QH-17
Покажи мне расходы будущих периодов, где ошибка, скорее всего, сидит в дате начала, окончания или вообще в неверно указанном годе.
**Класс:** rule_based_account_control / anomaly_probe
**Ожидаемый route:** `store_feature_risk`
### QH-18
Есть ли такие записи на 97 счёте, которые вроде заведены, но по ним нет нормального ежемесячного списания, и с какими исходными документами это связано?
**Класс:** cross_entity / explain
**Ожидаемый route:** `hybrid_store_plus_live`
### QH-19
Покажи банковские гарантии, лицензии и похожие истории, которые сидят на 97, но выглядят так, будто срок действия и срок списания друг другу противоречат.
**Класс:** rule_based_account_control / cross_entity
**Ожидаемый route:** `hybrid_store_plus_live`
### QH-20
Сделай мне обзор по 97 счёту: что там сейчас выглядит как реальный риск периода, а что просто требует внимания, но не горит.
**Класс:** heavy_analytical
**Ожидаемый route:** `batch_refresh_then_store`
---
## Блок E. 01 / 02 / ОС / амортизация
### QH-21
Какие основные средства выглядят заведёнными так, будто амортизационная группа или срок амортизации выбраны сомнительно?
**Класс:** rule_based_account_control
**Ожидаемый route:** `store_feature_risk`
### QH-22
Покажи карточки ОС, где параметры похожи на ручную ошибку: группа одна, срок другой, логика начисления не бьётся.
**Класс:** rule_based_account_control / anomaly_probe
**Ожидаемый route:** `store_feature_risk`
### QH-23
Есть ли у нас объекты, по которым амортизация должна была идти, но не идёт, и чем это подтверждается в карточке и движениях?
**Класс:** cross_entity / explain
**Ожидаемый route:** `hybrid_store_plus_live`
### QH-24
Покажи самые подозрительные случаи по ОС, которые потенциально опасны не потому, что сумма большая, а потому что логика учёта объекта выглядит криво.
**Класс:** heavy_analytical / explain
**Ожидаемый route:** `batch_refresh_then_store`
### QH-25
Есть ли системный паттерн ошибок в заведении основных средств, а не просто единичные косяки?
**Класс:** heavy_analytical / anomaly_probe
**Ожидаемый route:** `batch_refresh_then_store`
---
## Блок F. 51 / банк / выписки / отражение в учёте
### QH-26
Покажи движения по банку, где есть ощущение, что выписка, документ и проводка не собрались в нормальную цепочку.
**Класс:** cross_entity
**Ожидаемый route:** `hybrid_store_plus_live`
### QH-27
Есть ли у нас банковские движения, которые визуально выглядят нормально, но по логике счёта 51 оставляют некорректный хвост или ломают инвариант счёта?
**Класс:** rule_based_account_control / anomaly_probe
**Ожидаемый route:** `store_feature_risk`
### QH-28
Найди операции по банку, где, скорее всего, проблема не в сумме, а в том, что что-то не было проведено или не туда легло.
**Класс:** drilldown_explain / cross_entity
**Ожидаемый route:** `hybrid_store_plus_live`
### QH-29
Сделай мне список самых подозрительных банковских кейсов июня с коротким объяснением по каждому: в чём суть риска и что бы ты проверил руками первым.
**Класс:** heavy_analytical
**Ожидаемый route:** `batch_refresh_then_store`
### QH-30
Покажи, где движение по банку есть, а нормального отражения в учёте не видно, либо наоборот — в учёте что-то есть, а банковской логики под этим не хватает.
**Класс:** cross_entity
**Ожидаемый route:** `hybrid_store_plus_live`
---
## Блок G. 90 / 62 / реализация и неоплата
### QH-31
Какие реализации на конец июня зависли без оплаты и уже выглядят не как нормальная отсрочка, а как потенциально проблемный хвост?
**Класс:** heavy_analytical / cross_entity
**Ожидаемый route:** `batch_refresh_then_store`
### QH-32
Покажи связку реализация → оплата → договор → проводки по самым крупным незакрытым отгрузкам.
**Класс:** cross_entity
**Ожидаемый route:** `hybrid_store_plus_live`
### QH-33
Где по реализации хвост образовался не из-за отсутствия денег как таковых, а из-за разрыва документов, дат или отражения?
**Класс:** cross_entity / explain
**Ожидаемый route:** `hybrid_store_plus_live`
### QH-34
Сделай рейтинг самых неприятных незакрытых реализаций: чтобы было видно и сумму, и возраст хвоста, и признаки, что это именно проблема учёта, а не обычная операционка.
**Класс:** heavy_analytical
**Ожидаемый route:** `batch_refresh_then_store`
### QH-35
Покажи мне такие кейсы по 90/62, где всё выглядит почти нормально, но если копнуть глубже, видно, что период закрывается на кривой связке.
**Класс:** anomaly_probe / cross_entity
**Ожидаемый route:** `hybrid_store_plus_live`
---
## Блок H. 10 / материалы / списание
### QH-36
Что сейчас лежит на 10 счёте так, будто это уже давно просится в списание или хотя бы в ручную проверку?
**Класс:** rule_based_account_control
**Ожидаемый route:** `store_feature_risk`
### QH-37
Покажи материалы, по которым можно заподозрить нелогичный остаток: они висят, но движения и хозяйственная логика вокруг них выглядят странно.
**Класс:** rule_based_account_control / anomaly_probe
**Ожидаемый route:** `store_feature_risk`
### QH-38
Свяжи по нескольким подозрительным материалам остаток, документы, движения и возможный сценарий, почему они зависли.
**Класс:** cross_entity / explain
**Ожидаемый route:** `hybrid_store_plus_live`
---
## Блок I. Специально неоднозначные человеческие вопросы
### QH-39
Где у нас вообще “пахнет ручной ошибкой”, а не нормальной хозяйственной жизнью? Не по одному счёту, а в целом по июню.
**Класс:** heavy_analytical / anomaly_probe / ambiguous_human_query
**Ожидаемый route:** `batch_refresh_then_store`
### QH-40
Что сейчас в июне выглядит как история, которую бухгалтер потом будет долго и нервно распутывать, если не проверить заранее?
**Класс:** heavy_analytical / period_close_risk / ambiguous_human_query
**Ожидаемый route:** `batch_refresh_then_store`
---
# Как я предлагаю оформить ответы в прогоне
Не просто “вопрос — route — pass/fail”, а именно **гибридный стиль**:
## Для человека
* нормальный текст вопроса;
* короткое объяснение, что этот вопрос вообще проверяет;
* почему он сложный;
* короткий человеческий вердикт по кейсу.
## Для машины
* `question_id`
* `question_class`
* `expected_route`
* `actual_route`
* `route_match`
* `decision_flags`
* `store_sufficiency`
* `latency_ms`
* `answer_quality.status`
* `degraded`
* `reason_codes`
Это как раз даст вам отчёт, который можно:
* читать глазами,
* diffать между версиями,
* позже использовать как validation corpus.
---
# Очень важная рекомендация
Я бы в этот новый прогон добавил **ещё одно поле**, которого раньше, скорее всего, не хватало:
```md
**Проверяемая бухгалтерская гипотеза:**
Какую именно предметную гипотезу должен был подтвердить или опровергнуть ассистент.
```
Пример:
* “по 97 счёту ошибка сидит в дате начала/окончания”;
* “по 41 счёту продажа опередила приход”;
* “по 60/62 счёту хвост образован отсутствием документа, а не просто неоплатой”.
Это сильно улучшит читаемость именно для предметников.
---
# Мой практический совет по следующему прогону
Сделайте следующий прогон не на всех 40 сразу, а в два прохода:
**Проход 1 — 15 вопросов**
* QH-01, 03, 06, 07, 11, 16, 18, 21, 23, 26, 29, 31, 33, 39, 40
Это даст:
* heavy,
* cross-entity,
* anomaly,
* rule-based,
* ambiguous human questions.
**Проход 2 — все 40**
После того как увидите, где он ещё хромает на творческих и длинных формулировках.
---
# Короткий вывод
Да, следующий benchmark надо делать уже не “академическим”, а **человекообразным и предметно-злым**.
И лучший формат отчёта для этого — **один MD-файл с карточками кейсов**, где рядом живут:
* вопрос,
* предметный смысл,
* ожидаемый маршрут,
* фактический маршрут,
* краткий ход рассуждения системы,
* и итоговый вердикт по качеству ответа.
Вся эта логика напрямую вытекает из разговора с главбухом: ваша ценность — это контрольный и аналитический слой по реальным бухгалтерским болям, а не игрушечные вопросы “найди документ номер N”.
Если хочешь, следующим сообщением я сразу превращу это в **готовую болванку отчёта в markdown**, уже целиком, чтобы вы просто вставили туда результаты прогона.

View File

@ -0,0 +1,405 @@
# ТЗ для Codex — Validation Run по Accounting Analytics Layer, Orchestration и LLM-like Benchmark
## 1. Назначение документа
Документ фиксирует следующий этап работ после успешного подтверждения runtime semantic bridge к 1С.
Цель этапа:
- довести **жирный monthly slice** до полноценно посаженного аналитического слоя;
- провести **жёсткую верификацию онтологии и mapping-слоя**;
- формализовать **оркестрацию источников данных**;
- подготовить и выполнить **контролируемый benchmark-run** по набору бухгалтерских вопросов;
- выполнить этот benchmark **не на реальной 4o mini**, а через Codex в режиме максимально близкой эмуляции её поведения;
- получить подробный инженерный отчёт о качестве маршрутизации, скорости и полноте ответов.
---
## 2. Контекст
На предыдущих этапах подтверждено:
- runtime semantic bridge работает;
- read-only режим подтверждён;
- критические цепочки `document -> posting -> account`, `posting -> subconto[1..3]`, `saldo -> movements` доказаны;
- `1c-mcp-toolkit` принят со статусом `adopt with restrictions`;
- общая архитектура analytics layer спроектирована, но orchestration-service и насыщение store ещё не доведены до production-ready уровня.
Сейчас задача не в том, чтобы искать новый мост, а в том, чтобы:
1. проверить, что analytics-layer действительно сел на реальный жирный срез;
2. проверить, что ontology и mapping не врут;
3. задать правила оркестрации;
4. провести честный тест пользовательских вопросов.
---
## 3. Главная цель этапа
Этап должен ответить на четыре вопроса:
### Вопрос 1
Насколько качественно и полно **жирный месячный snapshot** садится в canonical/feature/risk слои?
### Вопрос 2
Насколько онтология и mapping-layer действительно покрывают:
- сущности 1С;
- взаимосвязи между ними;
- типизацию связей;
- критические бухгалтерские цепочки?
### Вопрос 3
Можно ли формализовать orchestration так, чтобы система:
- не делала лишних тяжёлых live-вызовов;
- не слала LLM в неправильные источники;
- умела различать простые, сложные, batch-heavy и drill-down сценарии?
### Вопрос 4
Как система ведёт себя на реальном наборе бухгалтерских вопросов по времени, маршруту и качеству ответа, если прогон имитирует поведение `4o mini` без фактического расхода дорогих токенов?
---
## 4. Общая задача для Codex
Codex должен выполнить полный цикл:
1. взять **один жирный monthly slice** как эталонный тестовый срез;
2. довести его до корректной посадки в canonical/feature/risk store;
3. провести **жёсткий ontology + mapping audit**;
4. формализовать orchestration policy;
5. подготовить benchmark-runner под список бухгалтерских вопросов;
6. выполнить benchmark-run в режиме **LLM-like simulation**, максимально приближенном к `4o mini`;
7. вернуть детальный инженерный отчёт.
---
## 5. Жирный slice — обязательный эталон
### Требование
Взять **один жирный месячный snapshot** как основной тестовый срез.
Текущий приоритет:
- июнь 2020, если именно он уже зафиксирован как наиболее объёмный и показательный.
### Что требуется по slice
- посадить его в canonical store полностью;
- посадить в feature store;
- прогнать через risk/anomaly слой;
- убедиться, что slice даёт не просто хранение, а реальный материал для аналитики.
### Что проверить
- количество сущностей;
- количество связей;
- количество типизированных связей;
- количество `Unknown` и иных деградированных target/source entity;
- сколько объектов попало в canonical;
- сколько признаков/feature записано;
- сколько anomaly/risk сигналов получилось;
- какие классы сущностей реально покрыты.
---
## 6. Жёсткая проверка онтологии и mapping
## 6.1. Цель
Провести взрослую верификацию ontology/mapping-слоя.
### Нельзя ограничиться:
- “сущности вообще есть”;
- “связи вообще есть”.
### Нужно проверить:
- корректна ли типизация сущностей;
- корректна ли типизация связей;
- насколько стабильно ложатся документы, проводки, счета, субконто, контрагенты, договоры, склады, номенклатура, регистры;
- насколько корректно разрешаются ссылки;
- нет ли систематических ложных сопоставлений;
- где ontology/mapping врёт.
## 6.2. Обязательные метрики ontology audit
Codex должен посчитать и отдать:
- общее число entity classes;
- число covered entity classes;
- число uncovered entity classes;
- число relation types;
- число корректно типизированных relations;
- число `Unknown` и аналогичных деградированных маппингов;
- число конфликтных mappings;
- процент link coverage;
- процент semantic coverage;
- список самых проблемных типов сущностей и связей.
## 6.3. Жёсткая цель
Онтология должна быть:
- переносимой;
- не привязанной к одной компании;
- пригодной для других 1С-контуров, как минимум того же класса;
- не собранной “под один snapshot вручную”.
---
## 7. Формализация оркестрации
## 7.1. Цель
Сформировать правила orchestration layer:
- что считается простым запросом;
- что считается drill-down запросом;
- что считается heavy analytical запросом;
- когда идти в OData;
- когда идти в MCP/runtime bridge;
- когда работать по snapshot/canonical store;
- когда использовать feature/risk/anomaly store;
- когда запускать batch refresh/analysis.
## 7.2. Источники, которые надо развести
Нужно развести по ролям:
### A. OData
Использовать как:
- discovery layer;
- быстрый широкий обзор структуры;
- fallback для типовых сущностей, если это быстрее и дешевле;
- лёгкий слой навигации.
### B. MCP / Runtime Bridge
Использовать как:
- live semantic bridge;
- drill-down;
- доступ к документу, проводке, счёту, subconto, saldo;
- точечные запросы;
- ситуации, когда нужен актуальный runtime truth.
### C. Snapshots / Canonical Store
Использовать как:
- основной слой для already-loaded истории;
- источник для быстрых аналитических ответов;
- источник для cross-period анализа;
- источник для вопросов, где не нужна свежесть в секунду.
### D. Feature / Risk / Anomaly Store
Использовать как:
- быстрый источник готовых аналитических выводов;
- anomaly-first ответы;
- risk-based маршрутизацию;
- summarized insight layer.
## 7.3. Обязательный результат
Codex должен оформить:
- **decision tree оркестрации**;
- список правил маршрутизации;
- источники по приоритету;
- fallback logic;
- timeout budget;
- max retrieval budget;
- политика retry/replan;
- политика отказа от неадекватно тяжёлых live-запросов.
---
## 8. LLM-like benchmark без реальной 4o mini
## 8.1. Главная идея
Нужно выполнить benchmark-run так, чтобы Codex **приблизил своё поведение к профилю `4o mini`**, но без реального расхода токенов OpenAI.
## 8.2. Важно
Это не “настоящая 4o mini”.
Это controlled emulation / simulation.
## 8.3. Требование к Codex
Codex должен надеть на себя **ограничения и поведенческие правила**, максимально похожие на рабочий режим дешёвой быстрой модели:
### Ограничения по мышлению и маршрутизации
- не уходить в бесконечную глубину;
- делать компактную декомпозицию вопроса;
- не генерировать чрезмерно сложные планы, если вопрос можно решить проще;
- отдавать предпочтение уже существующим store-layer ответам, если они достаточны;
- использовать live bridge только по необходимости;
- минимизировать число round-trips;
- минимизировать контекст;
- не строить “идеальную академическую” стратегию, если хватит практичной.
### Ограничения по ответу
- отвечать короче и практичнее;
- не делать длинных философских объяснений;
- давать инженерно полезный ответ;
- явно помечать, когда ответ построен на store, а когда на live data.
### Ограничения по стоимости / latency simulation
Codex должен действовать так, как будто:
- запрос дорогой;
- шаги надо экономить;
- retrieval budget ограничен;
- слишком тяжёлые запросы надо сворачивать или бить на этапы.
## 8.4. Что нужно получить
Не “магически стать 4o mini”, а:
- получить benchmark в **сравнимом дешёвом профиле рассуждения**.
---
## 9. Набор бухгалтерских вопросов
Codex должен прогнать набор **35 тестовых бухгалтерских вопросов**.
### Требования к набору
Вопросы должны быть:
- разношёрстные;
- от простых к тяжёлым;
- от конкретных к общим;
- от точечных к аналитическим;
- от single-object к whole-slice;
- покрывать разные маршруты системы.
### Классы вопросов
Нужно включить:
1. **Простые factual**
- по документу;
- по счёту;
- по проводке;
- по контрагенту;
- по договору.
2. **Explain / drill-down**
- объяснить saldo;
- объяснить движение;
- показать цепочку документа;
- показать, почему запись попала туда.
3. **Cross-entity**
- связать документ и проводки;
- контрагента и договор;
- номенклатуру и склад;
- регистр и первичный документ.
4. **Period / trend**
- изменения за месяц;
- сравнение с предыдущим периодом;
- необычные движения;
- всплески активности.
5. **Anomaly / control**
- найти аномалии;
- найти нетипичные корреспонденции;
- найти незакрытые хвосты;
- найти коллизии.
6. **Heavy analytical**
- анализ всего среза;
- анализ всех счетов;
- анализ компаний/контрагентов;
- поиск отклонений от baseline.
7. **Ambiguous / fuzzy**
- криво сформулированные вопросы;
- вопросы с недостатком контекста;
- вопросы, где нужна маршрутизация и уточнение.
---
## 10. Что должен вернуть benchmark
Для **каждого** вопроса Codex должен вернуть запись со следующими полями:
1. `question_id`
2. `question_text`
3. `question_class`
4. `expected_route`
5. `actual_route`
6. `sources_used`
7. `refresh_needed`
8. `latency_ms`
9. `planning_time_ms`
10. `retrieval_time_ms`
11. `response_generation_time_ms`
12. `context_size`
13. `answer_text`
14. `answer_quality_assessment`
15. `route_quality_assessment`
16. `issues_detected`
17. `recommended_fix`
---
## 11. Отчёт по итогам benchmark-run
Codex должен собрать **максимально подробный отчёт**, где будут:
### 11.1. Общая статистика
- число вопросов;
- среднее время ответа;
- медиана;
- p90;
- p95;
- средний размер контекста;
- число live-route вопросов;
- число store-route вопросов;
- число неправильных маршрутизаций;
- число деградированных ответов.
### 11.2. Анализ качества оркестрации
- где маршрутизация сработала правильно;
- где OData использован правильно;
- где MCP использован правильно;
- где snapshot/store было бы лучше;
- где система дёрнула не тот источник;
- где слишком дорого / долго.
### 11.3. Анализ онтологии и мэппинга
- где ontology помогла;
- где mapping сломал ответ;
- какие типы сущностей и связей проблемны;
- какие relation patterns надо чинить.
### 11.4. Итоговые рекомендации
- что надо чинить первым;
- что можно отложить;
- какие маршруты уже production-usable;
- какие маршруты нельзя пока давать живому пользователю.
---
## 12. Deliverables
Codex должен вернуть:
1. `slice_ingestion_report.md`
2. `ontology_mapping_audit.md`
3. `orchestration_policy_spec.md`
4. `llm_like_simulation_profile.md`
5. `benchmark_questions_35.md`
6. `benchmark_run_results.json`
7. `benchmark_run_report.md`
8. `benchmark_route_analysis.md`
9. `benchmark_final_verdict.md`
---
## 13. Критерии приёмки
Этап считается успешным, если:
1. жирный slice полноценно посажен в canonical/feature/risk store;
2. ontology + mapping прошли жёсткий аудит;
3. orchestration policy формализована;
4. Codex выполнил benchmark-run в LLM-like профиле;
5. по каждому вопросу есть route trace, timing и quality trace;
6. можно честно сказать:
- какие маршруты уже хорошие;
- какие требуют доработки;
- где bottleneck;
- где слабое место онтологии;
- где слабое место оркестрации.
---
## 14. Резюме
Это не этап “подключили LLM и надеемся на чудо”.
Это этап:
- доведения жирного среза;
- взрослой проверки ontology/mapping;
- формализации orchestration;
- controlled benchmark-run с модель-подобным профилем;
- инженерной оценки готовности системы к реальному пользовательскому контуру.
Главная задача:
**получить честную, измеримую и трассируемую картину того, насколько система готова к следующему шагу.**

View File

@ -0,0 +1,413 @@
TZ_LLM_Normalizer_v1.1.1.md
Да, ты правильно понимаешь.
Сейчас у вас набор **типизированный и управляемый**:
* кейсы уже разложены по классам;
* видно, где именно остаточные промахи;
* промахи уже не системные, а **пограничные**;
* значит, текущий этап — это именно **шлифовка на известных типах**, а не поиск всей правды о мире.
Именно поэтому сейчас есть смысл делать **v1.1.1 polish**:
не большой новый этап, а аккуратную доводку по 35 остаточным паттернам, с минимальным риском деградации и минимальным бюджетом.
---
# Главное про риск деградации
Риск деградации тут **есть**, но он уже локальный, не катастрофический.
## Откуда риск
Сейчас normalizer уже хорошо держит:
* schema,
* route,
* causal semantics,
* cross-entity.
Если начать слишком агрессивно править promptы, можно:
* случайно сломать уже хорошие `cross_entity`;
* ухудшить `route_hint_accuracy`;
* вернуть лишние переэскалации;
* попортить confidence-policy.
## Поэтому правильный режим такой
Нужно делать не “ещё один большой тюнинг”, а:
* **не трогать то, что уже даёт 100%**
* править только:
* `period_close_risk` vs `heavy_analytical`
* `drilldown_explain` vs лишний `needs_cross_entity_join`
* `anomaly_probe` vs лишний `batch_refresh_then_store`
То есть это именно **surgical patch**, а не новая версия всего normalizerа.
---
# ТЗ для Codex: Normalizer v1.1.1 Polish
## 1. Цель этапа
Провести точечную доводку `normalizer_v1_1` до `normalizer_v1_1_1`, чтобы улучшить остаточные показатели **без деградации уже работающих классов** и без дорогих повторных прогонов.
Цели:
* убрать оставшиеся пограничные mismatchи;
* улучшить согласованность `intent_class` и `requires`;
* уменьшить лишнюю route escalation;
* сохранить текущие сильные результаты:
* `schema_validation_pass_rate = 100`
* `route_hint_accuracy = 96.67`
* `cross_entity = 100%`
* `high_confidence_error_rate = 0`
---
## 2. Scope этапа
### В scope входит
* точечный forensic-аудит остаточных mismatchов;
* минимальная правка taxonomy logic;
* минимальная правка few-shot и developer prompt;
* контрольный микро-прогон на узком наборе кейсов;
* оценка риска деградации относительно v1.1.
### В scope не входит
* новый большой eval на 30+ новых кейсах;
* переписывание всей схемы;
* переписывание всего prompt manager;
* новая архитектура normalizer;
* массовый prompt sweep;
* расширение доменного словаря во все стороны.
---
## 3. Что именно надо чинить
## Паттерн 1. `period_close_risk` уезжает в `heavy_analytical`
### Симптом
Вопросы про предзакрытие/конец периода/что взорвётся в последний день:
* понимаются по route правильно;
* но получают `intent_class = heavy_analytical` вместо `period_close_risk`.
### Что нужно сделать
Уточнить taxonomy:
Если вопрос содержит смысл:
* предзакрытие,
* конец периода,
* перед сдачей отчетности,
* в последний день,
* что взорвётся,
* что критично перед закрытием,
* где опасные хвосты перед закрытием,
то primary intent должен быть:
* `period_close_risk`
Даже если при этом:
* нужен обзор,
* нужна приоритизация,
* нужен batch route.
### Важно
`period_close_risk` не должен исчезать внутрь `heavy_analytical`.
`heavy_analytical` — это обзорный/рейтинговый класс.
`period_close_risk` — это специальный предметный класс с периодной угрозой.
---
## Паттерн 2. В точечном drilldown лишне поднимается `needs_cross_entity_join = true`
### Симптом
Для вопросов вида:
* “покажи документ №... и связанную проводку”
* “покажи карточку конкретной операции и связанную проводку”
маршрут правильный:
* `live_mcp_drilldown`
intent тоже правильный:
* `drilldown_explain`
Но в `requires` появляется лишнее:
* `needs_cross_entity_join = true`
### Что нужно сделать
Уточнить правило:
Если вопрос:
* про **один конкретный объект**
* и у него есть идентификатор / номер / ref / карточка / конкретная операция / конкретный документ,
* и задача — точечно показать source-of-record / связанную проводку / один связанный объект,
то:
* `needs_exact_object_trace = true`
* `needs_runtime_truth = true`
* `needs_cross_entity_join = false`
Даже если в вопросе упоминается “связанная проводка”.
### Логика
Связанный объект в точечном drilldown — это ещё не multi-entity join в смысле causal report.
---
## Паттерн 3. `anomaly_probe` местами переэскалирует в `batch_refresh_then_store`
### Симптом
Есть вопрос уровня:
* “где по июню выглядит подозрительно, но без точечного документа, просто дай зоны риска”
Ожидание:
* `anomaly_probe`
* `store_feature_risk`
Факт:
* `anomaly_probe`
* но route уходит в `batch_refresh_then_store`
### Что нужно сделать
Уточнить правило route hint:
Если:
* вопрос аномальный / риск-ориентированный,
* не требует ranking,
* не требует whole-company overview,
* не требует full prioritized review,
* не требует batch-scale summary,
то:
* route должен оставаться `store_feature_risk`
Даже если:
* есть `needs_period_cut = true`
* есть общая риск-лексика.
### Логика
Сам по себе “июнь” или “зоны риска” не должен автоматически толкать всё в batch.
---
## 4. Что надо поменять в promptах
## 4.1 Developer prompt
Нужно добавить 3 точечных блока правил:
### Блок A. `period_close_risk`
```text
Если вопрос явно относится к предзакрытию, закрытию периода, сдаче отчетности, концу месяца или рискам последнего дня, primary intent_class должен быть period_close_risk, а не heavy_analytical, даже если требуется обзор или приоритизация.
```
### Блок B. exact drilldown
```text
Если вопрос относится к одному конкретному документу, операции, номеру, ref или объекту и просит показать связанную проводку или источник, это exact object trace. В таком случае needs_cross_entity_join не поднимается, если не требуется анализ множества сущностей или нескольких кейсов.
```
### Блок C. anomaly without heavy batch
```text
Если вопрос просит показать зоны риска, подозрительные случаи или аномалии без рейтинга, без full overview и без company-wide aggregation, route_hint должен быть store_feature_risk, а не batch_refresh_then_store.
```
---
## 4.2 Few-shot
Добавить **ровно 3 новых few-shot**:
### Few-shot 1
`period_close_risk` vs `heavy_analytical`
Пример:
* “Перед закрытием периода что у нас может взорваться в последний день?”
Ожидание:
* `intent_class = period_close_risk`
* `route_hint = batch_refresh_then_store`
### Few-shot 2
точечный drilldown без `needs_cross_entity_join`
Пример:
* “Покажи документ №TRX-88 и связанную проводку по 51”
Ожидание:
* `intent_class = drilldown_explain`
* `needs_exact_object_trace = true`
* `needs_cross_entity_join = false`
* `route_hint = live_mcp_drilldown`
### Few-shot 3
anomaly without batch escalation
Пример:
* “Где по июню выглядит подозрительно, просто дай зоны риска без детального разбора”
Ожидание:
* `intent_class = anomaly_probe`
* `route_hint = store_feature_risk`
---
## 5. Что нельзя трогать
Codex запрещено:
* ухудшать cross-entity rules;
* переписывать causal language interpretation;
* менять schema;
* трогать confidence logic, если это не требуется точечно;
* добавлять много новых few-shot;
* делать большие перестройки domain prompt.
Сильные зоны v1.1:
* `cross_entity`
* `heavy_analytical`
* `rule_based_account_control`
* общий `route_hint`
Их надо **сохранять**, а не “переосмыслять”.
---
## 6. Прогоны и бюджет
### Режим
Только **микро-проверка**, очень экономно.
### Лимит
* максимум **5 API-вызовов** на весь этап polish;
* один кейс = один запрос;
* без повторов, кроме технического fail;
* `temperature = 0`
### Контрольный набор
Проверить только эти кейсы:
1. `NQ-008`
2. `V11-DD-005`
3. `V11-OT-003`
4. `V11-OT-004`
5. `V11-OT-005`
### Зачем именно они
Они покрывают все три остаточных паттерна:
* drilldown overspecification,
* taxonomy drift,
* route over-escalation.
---
## 7. Что должен выдать Codex
### Артефакты
1. `docs/normalizer_v1_1_1_patch_notes.md`
2. обновлённый:
* `prompts/developer/normalizer_v1_1_1.txt`
* `prompts/fewshot/normalizer_fewshot_v1_1_1.txt`
3. `reports/normalizer_v1_1_1_micro_eval.json`
4. `reports/normalizer_v1_1_1_micro_eval.md`
### В patch notes обязательно указать
* что именно было изменено;
* какие 3 паттерна лечили;
* почему изменения безопасны;
* сколько API-вызовов реально потрачено;
* были ли ретраи;
* что улучшилось;
* что осталось как есть.
---
## 8. Критерии приёмки
Этап считается принятым, если:
1. не превышен лимит в 5 запросов;
2. schema validation остаётся 100;
3. `NQ-008` и `V11-DD-005` больше не поднимают лишний `needs_cross_entity_join`;
4. `V11-OT-003` и `V11-OT-005` получают `intent_class = period_close_risk`;
5. `V11-OT-004` получает `route_hint = store_feature_risk`;
6. не возникает деградации на уже сильных классах;
7. patch описан прозрачно и без “магических” неясных изменений.
---
## 9. Честная цель этапа
Не надо ставить задачу “добить всё до 99”.
Правильная цель этого этапа:
* подчистить остаточные пограничные ошибки,
* не поломать working core,
* зафиксировать устойчивую v1.1.1,
* после этого уже собирать **новую пачку других кейсов** на следующем этапе.
То есть сейчас:
**не расширение мира, а полировка известной карты.**
---
## 10. Короткая версия для Codex
Сделать v1.1.1 patch:
* точечно поправить `period_close_risk` vs `heavy_analytical`,
* убрать лишний `needs_cross_entity_join` в точечном drilldown,
* убрать лишнюю batch-эскалацию для мягкого anomaly-probe,
* добавить 3 few-shot,
* сделать микро-прогон на 5 кейсах,
* не потратить больше 5 запросов,
* не трогать сильные части модели.

View File

@ -0,0 +1,42 @@
TZ_LLM_Normalizer_v1.1.2.1
30 новых вопросов для ревью бухгалтером
Блок 1. Поставщики / покупатели / взаиморасчёты
По каким поставщикам у нас на конец месяца остались хвосты, которые уже не похожи на обычную задержку документов, а выглядят как реальная проблема в цепочке?
Где по покупателям у нас висит история “отгрузили — денег нет — закрытия нет”, и по каким контрагентам это уже требует ручной проверки?
Покажи контрагентов, по которым сальдо у нас, скорее всего, не совпадёт с их актом сверки, если его запросить прямо сейчас.
Где у нас есть оплаты, но не хватает документов, которые должны были закрыть взаиморасчёты?
По каким контрагентам, наоборот, документы есть, а нормального закрытия оплатами не видно?
Есть ли такие зависшие авансы, которые уже давно надо было либо закрыть, либо хотя бы перепроверить руками?
Блок 2. Реализация / неоплата / 90 + 62
Какие реализации на конец периода выглядят так, будто они зависли и будут портить картину по выручке, если их не проверить заранее?
По каким отгрузкам видно, что проблема не просто в том, что клиент не оплатил, а в том, что сама связка документов собрана криво?
Покажи реализации, где хвост выглядит особенно неприятно: сумма не маленькая, возраст хвоста уже заметный, и при этом не видно нормального завершения цепочки.
Где по 90/62 история похожа на “вроде всё проведено, но если копнуть, закрытие держится на кривой связке”?
Есть ли случаи, где реализация попала в период, а подтверждающие документы или оплата до сих пор живут в какой-то полуразобранной логике?
По каким продажам на конец месяца видно, что бухгалтер потом будет долго распутывать, почему всё это не сошлось нормально?
Блок 3. Банк / выписки / счёт 51
Какие банковские движения выглядят так, будто выписка есть, а нормального отражения в учёте под ней не хватает?
Где по банку можно заподозрить, что документ и проводка вроде есть, но логика операции всё равно не собрана в нормальную цепочку?
Есть ли движения по счёту 51, которые выглядят корректно по сумме, но по смыслу оставляют после себя подозрительный хвост?
Покажи банковские кейсы, где, скорее всего, проблема не в платеже как таковом, а в том, что он не туда лёг или не тем документом закрылся.
Где банк и бухгалтерский контур, скорее всего, расходятся не по одной строке, а по паттерну, который уже начинает повторяться?
Блок 4. Товары / склад / приход / реализация / счёт 41
Какие товарные позиции выглядят так, будто их уже продавали, а нормального прихода под них в базе не видно?
Где по товарам у нас отрицательные или подозрительные остатки, которые, скорее всего, связаны не с жизнью, а с ошибкой в учёте?
Есть ли случаи, где приход и реализация вроде есть оба, но даты между ними выглядят так, будто кто-то завёл документы задним числом или с ошибкой?
Покажи товарные хвосты, которые сильнее всего искажают картину периода и требуют проверки до закрытия месяца.
Где по складу и реализации видно, что себестоимость продажи подтверждена слабо или вообще опирается на кривую цепочку?
Блок 5. Материалы / счёт 10
Что сейчас лежит на 10 счёте так, будто это уже давно надо было либо списать, либо хотя бы проверить, почему оно до сих пор висит?
Есть ли материалы, по которым остаток выглядит нелогично: движения были, хозяйственная логика слабая, а в учёте всё ещё что-то торчит?
Покажи позиции по материалам, где возможен эффект “вроде сумма не огромная, но учётная логика выглядит криво”.
Блок 6. Расходы будущих периодов / счёт 97
Какие записи на 97 счёте больше всего похожи на ошибку в датах начала, конца или самом сроке списания?
Есть ли такие расходы будущих периодов, которые заведены, но по ним не видно нормальной ежемесячной жизни, как будто запись повисла сама по себе?
Покажи кейсы по 97 счёту, где срок документа и срок списания визуально противоречат друг другу.
Блок 7. Основные средства / амортизация / 0102
Есть ли основные средства, по которым параметры карточки выглядят так, будто амортизацию им задали не по логике объекта, а “как получилось”?
Покажи объекты ОС, где риск не в сумме, а в том, что карточка и логика начисления выглядят подозрительно и могут аукнуться позже.

View File

@ -0,0 +1,451 @@
TZ_LLM_Normalizer_v1.1.2.md
Ниже даю **ТЗ на `normalizer_v1.1.2`** с комментариями — так, чтобы его можно было и отдать Codex, и потом нормально архивировать как понятный инженерный документ.
---
# ТЗ: `normalizer_v1.1.2`
## Точечная доводка границы `heavy_analytical``period_close_risk`
---
## 1. Контекст этапа
На версии `normalizer_v1.1.1` удалось очень сильно улучшить рабочие свойства normalizerа как предроутерного слоя:
* `schema_validation_pass_rate = 100`
* `route_hint_accuracy = 100`
* `causal_flag_accuracy = 100`
* `cross_entity = 100%`
* `drilldown_explain = 100%`
* `rule_based_account_control = 100%`
* `period_close_risk = 100%`
Однако при этом появился побочный перекос в taxonomy:
* `intent_class_accuracy = 90` вместо `93.33` на `v1.1`
* `heavy_analytical = 40%`
* `high_confidence_error_rate = 3.33` вместо `0`
Комментарий:
Это означает, что версия `v1.1.1` **улучшила поведение normalizerа как route/cause normalizer**, но при этом **слишком агрессивно начала относить heavy обзорные вопросы к `period_close_risk`**, если в формулировке есть слова про закрытие периода, отчётность или предзакрытие.
---
## 2. Что именно произошло
Проблема локализована и понятна.
### Текущий корневой дефект
В `developer prompt v1.1.1` есть правило, которое слишком жёстко задаёт:
> если вопрос относится к предзакрытию, закрытию периода, сдаче отчётности, концу месяца или рискам последнего дня, primary intent должен быть `period_close_risk`, даже если требуется обзор или приоритизация.
Комментарий:
Это правило было добавлено, чтобы вылечить потерю `period_close_risk` внутри `heavy_analytical`.
Но в текущем виде оно **перетянуло слишком много heavy overview вопросов в `period_close_risk`**.
### Дополнительный фактор
В few-shot наборе есть пример на `period_close_risk`, но **нет достаточного симметричного набора примеров**, который бы показывал модели:
* когда вопрос остаётся `heavy_analytical`,
* даже если он сформулирован “перед закрытием периода” или “перед сдачей отчётности”.
Комментарий:
То есть у модели появился сильный positive-trigger на `period_close_risk`, но нет хорошего отрицательного противовеса.
---
## 3. Цель `v1.1.2`
Сделать **узкий и безопасный patch**, который:
1. сохранит:
* `route_hint_accuracy = 100`
* `causal_flag_accuracy = 100`
* `schema_validation_pass_rate = 100`
* сильные результаты по `cross_entity`, `drilldown_explain`, `rule_based_account_control`
2. исправит:
* taxonomy drift между `heavy_analytical` и `period_close_risk`
* лишнюю high-confidence оценку на пограничных кейсах
3. не приведёт к деградации уже сильных зон.
Комментарий:
Это **не новый большой этап**, а именно **surgical patch**.
Менять нужно **только границу intent-class**, а не общую архитектуру normalizerа.
---
## 4. Scope этапа
### В scope входит
* корректировка одного участка `developer prompt`;
* добавление 2 симметричных few-shot примеров для `heavy_analytical`;
* добавление 1 confidence-rule для пограничных кейсов;
* очень короткий контрольный прогон на 5 кейсах.
### В scope не входит
* изменение schema;
* изменение route logic;
* изменение cross-entity правил;
* изменение drilldown правил;
* изменение anomaly patch;
* новый большой eval-run на 30+ кейсов;
* новые данные/срезы/онтология.
Комментарий:
Данные и база сейчас не нужны.
Проблема находится **целиком в prompt/taxonomy layer**.
---
## 5. Что именно нужно исправить
---
## 5.1. Паттерн A: `heavy_analytical` ошибочно уходит в `period_close_risk`
### Симптом
На `v1.1.1` следующие кейсы получили неправильный `intent_class`:
* `NQ-002`
* `NQ-007`
* `V11-HA-004`
Во всех этих кейсах:
* route правильный: `batch_refresh_then_store`
* causal/requires нормальные
* ошибка только в том, что `intent_class` стал `period_close_risk` вместо `heavy_analytical`
### Природа ошибки
Вопросы содержат:
* контекст закрытия периода,
* июнь / предзакрытие / отчётность,
но по сути просят:
* обзор,
* рейтинг,
* summary,
* общую картину,
* концентрацию ошибок,
* приоритизацию.
Такие вопросы должны оставаться `heavy_analytical`.
Комментарий:
`period_close_risk` — это не “любой вопрос про период”, а **специальный класс про угрозу срыва закрытия**.
Если в центре вопроса **обзор / рейтинг / агрегированный срез**, это должен быть `heavy_analytical`.
---
## 5.2. Паттерн B: high confidence на пограничном кейсе
### Симптом
В кейсе `NQ-002`:
* intent ошибочный,
* route правильный,
* confidence = `high`
### Природа ошибки
Пограничные вопросы между:
* `heavy_analytical`
* `period_close_risk`
не должны получать `high confidence`, если модель не различает класс уверенно.
Комментарий:
Даже если route совпадает, **ошибочно высокая уверенность на спорном intent — это риск для последующего управления качеством**.
Эту часть нужно приглушить.
---
# 6. Конкретные изменения для Codex
---
## 6.1. Изменить `developer prompt`
### Файл
`prompts/developer/normalizer_v1_1_2.txt`
### Что сделать
Переписать правило про `period_close_risk`.
### Текущее проблемное правило
Смысл текущего правила:
> если вопрос относится к предзакрытию/закрытию/отчётности/последнему дню, primary intent = `period_close_risk`, даже если нужен обзор или приоритизация.
### Новая правильная формулировка
Вставить вместо него такой блок:
```text
If a question is explicitly about period close risk, pre-close danger, last-day closing failure, reporting deadline risk, or threats that can break the close process itself, use `period_close_risk` as the primary intent_class.
However, if the main purpose of the question is:
- ranking,
- top issues,
- overview,
- concentration of errors,
- summary,
- prioritized review list,
- company-wide analytical review,
then the primary intent_class should remain `heavy_analytical`, even if the question is phrased in the context of month-end close or reporting preparation.
```
### Дополнительное уточнение
Добавить ещё один отдельный блок:
```text
`heavy_analytical` has priority over `period_close_risk` when the question asks for:
- ranking,
- top-N,
- overview,
- summary,
- company-wide picture,
- prioritized analytical review,
even if the wording mentions closing period, reporting, or pre-close context.
Use `period_close_risk` only when the core of the question is the risk of failing or destabilizing the close process itself.
```
Комментарий:
Это главное исправление всей версии `v1.1.2`.
Ничего сильнее менять не надо.
---
## 6.2. Добавить 2 симметричных few-shot примера
### Файл
`prompts/fewshot/normalizer_fewshot_v1_1_2.txt`
### Что сделать
Оставить existing few-shot на `period_close_risk`, но добавить **два примера-контрвеса**.
---
### Few-shot 1 — heavy remains heavy
```text
Q: Сделай рейтинг самых рисковых хвостов перед закрытием периода за июнь.
Expected:
{
"intent_class": "heavy_analytical",
"requires": {
"needs_cross_entity_join": false,
"needs_causal_chain": false,
"needs_exact_object_trace": false,
"needs_ranking": true,
"needs_anomaly_summary": false,
"needs_runtime_truth": false,
"needs_period_cut": true,
"needs_evidence": false
},
"expected_output_shape": "ranked_list",
"route_hint": "batch_refresh_then_store"
}
```
### Few-shot 2 — overview remains heavy
```text
Q: Дай обзорный риск-срез перед сдачей отчетности: где максимальная концентрация ошибок.
Expected:
{
"intent_class": "heavy_analytical",
"requires": {
"needs_cross_entity_join": false,
"needs_causal_chain": false,
"needs_exact_object_trace": false,
"needs_ranking": true,
"needs_anomaly_summary": true,
"needs_runtime_truth": false,
"needs_period_cut": true,
"needs_evidence": false
},
"expected_output_shape": "anomaly_summary",
"route_hint": "batch_refresh_then_store"
}
```
### Existing few-shot to keep
Оставить пример:
```text
Q: Перед закрытием периода что у нас может взорваться в последний день?
Expected intent_class: period_close_risk
```
Комментарий:
Эти два новых примера нужны не потому, что модель “тупая”, а потому, что в `v1.1.1` у неё был сильный пример только в одну сторону.
`v1.1.2` должен сделать границу симметричной.
---
## 6.3. Добавить confidence-ограничение
### Файл
`prompts/developer/normalizer_v1_1_2.txt`
### Что сделать
Добавить явное правило:
```text
If a question is plausibly on the boundary between `heavy_analytical` and `period_close_risk`, do not assign `confidence.overall = high`.
Use `medium` unless the wording strongly and unambiguously centers on close-process failure risk rather than analytical overview.
```
Комментарий:
Это страховка от кейса `NQ-002`, где route был правильный, но модель слишком самоуверенно выбрала спорный intent.
---
# 7. Что нельзя менять
Codex **запрещено**:
1. менять schema;
2. менять route rules;
3. трогать cross-entity prompt logic;
4. трогать drilldown prompt logic;
5. трогать anomaly/store_feature_risk patch;
6. добавлять много новых few-shot;
7. переписывать весь domain prompt;
8. делать большие prompt sweepы;
9. делать повторные API-запросы без необходимости.
Комментарий:
`v1.1.1` очень хорош по route/cause behavior.
Наша задача — **не поломать working core ради косметики taxonomy**.
---
# 8. Контрольный прогон
## Режим
Только микро-прогон.
## Лимит
* максимум **5 API-вызовов**
* один кейс = один запрос
* без повторов
* `temperature = 0`
## Проверить только эти кейсы
1. `NQ-002`
2. `NQ-007`
3. `V11-HA-004`
4. `V11-OT-003`
5. `V11-OT-005`
Комментарий:
Этого достаточно.
Три кейса проверяют boundary heavy/period-close.
Два кейса проверяют, что `period_close_risk` мы не сломали обратно.
---
# 9. Ожидаемый результат
## Минимально acceptable
* `NQ-002``heavy_analytical`
* `NQ-007``heavy_analytical`
* `V11-HA-004``heavy_analytical`
* `V11-OT-003` остаётся `period_close_risk`
* `V11-OT-005` остаётся `period_close_risk`
## Дополнительно желательно
* на пограничных кейсах confidence не `high`, а `medium`
Комментарий:
Если эти 5 кейсов проходят, значит `v1.1.2` делает ровно то, что нужно, и не требует нового полного 30-case eval прямо сейчас.
---
# 10. Артефакты, которые должен выдать Codex
1. `docs/normalizer_v1_1_2_patch_notes.md`
2. `prompts/developer/normalizer_v1_1_2.txt`
3. `prompts/fewshot/normalizer_fewshot_v1_1_2.txt`
4. `reports/normalizer_v1_1_2_micro_eval.json`
5. `reports/normalizer_v1_1_2_micro_eval.md`
---
# 11. Что должно быть в patch notes
В `normalizer_v1_1_2_patch_notes.md` обязательно описать:
* какая именно проблема была в `v1.1.1`;
* почему она возникла;
* какие prompt changes внесены;
* какие few-shot added;
* почему изменения безопасны;
* сколько API-вызовов потрачено;
* были ли ретраи;
* результаты микро-прогона;
* что осталось на будущее.
Комментарий:
Это нужно именно для архива и последующего контроля версий, чтобы потом было понятно, **почему `v1.1.2` вообще появился**.
---
# 12. Приёмка этапа
Этап считается принятым, если:
1. не превышен лимит в 5 API-вызовов;
2. `NQ-002`, `NQ-007`, `V11-HA-004` получают `intent_class = heavy_analytical`;
3. `V11-OT-003` и `V11-OT-005` остаются `period_close_risk`;
4. не возвращается лишний `high confidence` на пограничные heavy/period-close кейсы;
5. изменения зафиксированы прозрачно;
6. не сломаны уже сильные route/cause части normalizerа.
---
# 13. Короткий смысл этапа
`v1.1.2` — это **не улучшение всей системы**, а **локальная шлифовка последнего taxonomy-boundary**.
По сути задача одна:
> Сохранить идеальное route/cause поведение `v1.1.1`,
> но вернуть правильное именование там, где heavy overview был ошибочно засосан в period_close_risk.
---
Если хочешь, я следующим сообщением сделаю ещё **ультра-короткую версию этого ТЗ для прямой вставки в Codex**, буквально как job prompt без пояснений.

544
IN/TZ_LLM_Normalizer_v1.md Normal file
View File

@ -0,0 +1,544 @@
TZ_LLM_Normalizer_v1.md
Ниже даю **жёсткое ТЗ для Codex** на точечную доводку LLM-normalizer с очень экономным режимом прогонов.
---
# ТЗ для Codex: точечная доводка LLM Normalizer v1 → v1.1
## 1. Цель этапа
Довести текущий LLM-normalizer до более стабильного качества на реальных бухгалтерских человеческих запросах, **не раздувая бюджет на API-прогоны**.
Главная цель этапа:
* поднять качество нормализации;
* убрать текущие semantic-промахи по `intent_class`, `route_hint` и `causal flags`;
* сохранить 100% schema validation;
* сделать это **через точечные изменения prompt/few-shot/eval**, без массовых дорогих прогонов.
---
## 2. Текущий статус
Текущий eval дал:
* `schema_validation_pass_rate = 100`
* `intent_class_accuracy = 72.73`
* `route_hint_accuracy = 90.91`
* `causal_flag_accuracy = 81.82`
* `high_confidence_error_rate = 9.09`
Проблемные кейсы:
* `NQ-004`
* `NQ-008`
* `NQ-009`
Тип проблемы:
* schema уже держится хорошо;
* route_hint уже близок к рабочему;
* основное слабое место — `intent_class`;
* часть ошибок связана с неправильной трактовкой causal/cross-entity языка;
* минимум один кейс даёт ошибку route при при этом пойманной causal-семантике.
---
## 3. Главный принцип этапа
### Очень важно
Не делать дорогую “перестрелку запросами”.
Нельзя:
* гонять большие автоматические sweepы;
* отправлять много повторов на один и тот же кейс;
* делать temperature-sampling по 1020 вариантов;
* прогонять сотни запросов на каждую мелкую правку.
Нужно:
* сделать **точечную forensic-доработку**;
* разобрать 3 проблемных кейса;
* внести минимальные, но сильные изменения;
* прогнать **ровно один запрос на кейс** в контрольном eval-наборе;
* максимум 30 запросов на финальный контроль.
---
## 4. Budget constraints / лимиты на прогоны
Codex обязан соблюдать жёсткий лимит.
### Допустимый лимит API-вызовов на этот этап
* до **10** вызовов на forensic/ручную проверку;
* до **30** вызовов на финальный eval-run;
* итого целевой потолок: **не более 40 внешних LLM-запросов** на весь этап.
### Правила
1. Один кейс = один запрос.
2. Не делать повторные запросы на тот же кейс без явной необходимости.
3. Ретраи разрешены только:
* при техническом fail,
* при невалидном JSON,
* не более 1 повтора на кейс.
4. Не делать random sampling.
5. `temperature = 0` на всех eval-запусках.
6. Не делать “прогоны для красоты” после достижения приемлемого результата.
---
## 5. Что нужно сделать по шагам
## Этап A. Forensic-аудит проблемных кейсов
### Задача A1
Разобрать вручную кейсы:
* `NQ-004`
* `NQ-008`
* `NQ-009`
### Что нужно собрать по каждому кейсу
Для каждого кейса составить мини-таблицу:
* `case_id`
* `raw_question`
* `expected.intent_class`
* `actual.intent_class`
* `expected.route_hint`
* `actual.route_hint`
* `expected.requires`
* `actual.requires`
* `какие признаки модель не увидела`
* `какие признаки модель увидела лишние`
* `предполагаемая причина ошибки`
* `какая минимальная правка должна это исправить`
### Ожидаемый результат
Файл:
`docs/normalizer_forensic_audit_v1_1.md`
### Ограничение по вызовам
Новые API-вызовы для этого этапа делать только если не хватает уже существующих trace/result-данных.
Цель: по возможности **0 новых запросов**, максимум **3**.
---
## Этап B. Точечная доработка taxonomy и route logic в prompt-слое
### Задача B1
Уточнить `developer prompt` так, чтобы он:
* жёстче различал:
* `cross_entity`
* `anomaly_probe`
* `rule_based_account_control`
* `drilldown_explain`
* `ambiguous_human_query`
* не сваливал causal cross-entity в соседние классы.
### Задача B2
Добавить/исправить правила приоритетов:
#### Приоритет 1
Если вопрос требует связать:
* документы,
* оплаты,
* проводки,
* закрывающие,
* договоры,
* регистры,
* даты,
* подтверждение цепочки,
то это **не** `simple_factual` и обычно **не** `store_feature_risk`,
а causal multi-entity scenario.
#### Приоритет 2
Если вопрос про множество кейсов, даже если просит “объяснить”, это не `needs_exact_object_trace`, если нет одного конкретного документа/проводки/объекта.
#### Приоритет 3
Если в вопросе есть риск/аномалия-лексика, но одновременно есть document/payment/posting chain, то приоритет у causal cross-entity semantics, а не у risk-bucket.
#### Приоритет 4
`ambiguous_human_query` использовать только когда вопрос действительно не раскладывается в конкретный intent-class, а не как ленивый fallback.
### Задача B3
Уточнить `domain prompt`:
* расширить словарь фраз:
* “не бьётся”
* “не сходится”
* “не видно”
* “не собралось”
* “повисло”
* “хвост”
* “разложи по документам / оплатам / закрывающим”
* “чем подтверждается”
* “где ошибка в цепочке”
* “что пошло криво”
* привязать их к causal semantics.
### Ожидаемый результат
Обновить:
* `prompts/developer/normalizer_v1_1.txt`
* `prompts/domain/normalizer_domain_v1_1.txt`
---
## Этап C. Few-shot patch вместо большого переписывания
### Задача C1
Не переписывать весь prompt заново.
Добавить **только 57 новых few-shot примеров**, которые закрывают пограничные случаи.
### Обязательные типы новых few-shot
Нужно минимум по одному примеру на каждый паттерн:
1. `cross_entity` vs `anomaly_probe`
2. `cross_entity` vs `rule_based_account_control`
3. `cross_entity multiple explain` vs `drilldown_explain`
4. `causal human language` + `risk words`
5. `ambiguous human wording`, которое всё равно надо класть в нормальный intent-class
6. `rule-based control` без causal chain
7. `heavy overview` без точечного explain
### Требование
Few-shot должны быть короткими.
Не делать огромные простыни.
### Ожидаемый результат
Обновить:
* `prompts/fewshot/normalizer_fewshot_v1_1.txt`
---
## Этап D. Ужесточить confidence policy
### Задача D1
Снизить долю high-confidence ошибок.
### Что нужно сделать
Добавить в developer prompt правило:
Модель не должна ставить:
* `confidence.overall = high`
* `confidence.route_hint = high`
если одновременно:
* есть ambiguity,
* route зависит от тонкого различия между соседними классами,
* вопрос длинный и многослойный,
* модель не уверена в period scope,
* causal semantics частично восстановлена, но не полностью.
### Цель
Снизить `high_confidence_error_rate`.
### Ожидаемый результат
Правка внутри:
* `developer prompt`
* опционально: post-validation rule на backend, который помечает suspicious confidence
---
## Этап E. Подготовить экономный eval-набор из 30 кейсов
### Задача E1
Собрать один контрольный eval-набор:
`eval_cases/normalizer_eval_v1_1_30cases.json`
### Размер
Ровно **30 кейсов**, не больше.
### Ограничение
Один кейс = один запрос.
### Состав набора
Сделать сбалансированно:
* `cross_entity` — 10 кейсов
* `heavy_analytical` — 5 кейсов
* `drilldown_explain` — 5 кейсов
* `rule_based_account_control` — 5 кейсов
* `anomaly_probe / ambiguous_human_query / period_close_risk` — 5 кейсов
### Обязательные условия
* включить `NQ-004`, `NQ-008`, `NQ-009` в переработанном виде или их исходные кейсы;
* включить минимум 5 человеческих формулировок из creative-stress стиля;
* не делать дубли почти одинаковых вопросов.
---
## Этап F. Сделать один контрольный прогон
### Задача F1
Запустить **один** eval-run по 30 кейсам.
### Правила прогона
* `temperature = 0`
* один запрос на кейс
* без multi-sampling
* без повторов, кроме:
* технического fail
* invalid JSON
* максимум 1 retry на кейс
### Ожидаемый файл отчёта
`reports/normalizer_eval_v1_1_run.md`
### Ожидаемый JSON
`reports/normalizer_eval_v1_1_run.json`
---
## 6. Что нужно измерять в финальном отчёте
В отчёт обязательно вывести:
* `cases_total`
* `schema_validation_pass_rate`
* `intent_class_accuracy`
* `route_hint_accuracy`
* `causal_flag_accuracy`
* `high_confidence_error_rate`
Дополнительно:
* accuracy по каждому классу:
* `cross_entity`
* `heavy_analytical`
* `drilldown_explain`
* `rule_based_account_control`
* `anomaly_probe`
* список всех mismatchов
* короткий комментарий по каждому mismatchу
* сравнение **до / после** относительно текущих baseline-метрик
---
## 7. Целевые метрики этапа
Ниже не “идеальный мир”, а реальные целевые ориентиры.
### Минимально приемлемо
* `schema_validation_pass_rate >= 95`
* `intent_class_accuracy >= 85`
* `route_hint_accuracy >= 92`
* `causal_flag_accuracy >= 88`
* `high_confidence_error_rate <= 7`
### Хороший результат
* `schema_validation_pass_rate >= 98`
* `intent_class_accuracy >= 88`
* `route_hint_accuracy >= 94`
* `causal_flag_accuracy >= 90`
* `high_confidence_error_rate <= 5`
### Отличный результат
* `schema_validation_pass_rate = 100`
* `intent_class_accuracy >= 90`
* `route_hint_accuracy >= 95`
* `causal_flag_accuracy >= 92`
* `high_confidence_error_rate <= 3`
### Важно
Цель “95+ везде” можно держать как aspirational target, но Codex не должен ради этого устраивать дорогую перестрелку запросами. Сначала нужен максимально дешёвый и умный рост качества.
---
## 8. Что нельзя делать
Codex **запрещено**:
1. Делать массовый prompt sweep.
2. Прогонять десятки вариантов одного и того же кейса.
3. Использовать temperature > 0 для eval.
4. Делать скрытые повторные запросы “на всякий случай”.
5. Увеличивать eval set выше 30 кейсов без явной необходимости.
6. Пытаться лечить всё переписыванием backend-логики, если проблема решается prompt/few-shot таксономией.
7. Ломать уже рабочую schema validation ради intent tuning.
---
## 9. Что нужно поправить в коде
Codex должен проверить и при необходимости обновить:
### A. Prompt manager
* версионирование promptов:
* `normalizer_v1`
* `normalizer_v1_1`
* возможность быстро переключать presets
### B. Eval runner
* добавить режим:
* `single-pass-strict`
* который гарантирует:
* один запрос на кейс,
* без повторов,
* лог явных retries
### C. Report generator
* добавить сравнение baseline vs current
* отдельно выводить mismatch table
* отдельно выводить bad confidence cases
### D. Storage / trace
* сохранить привязку:
* `case_id`
* `trace_id`
* `prompt_version`
* `schema_version`
* `model`
* `request_count_for_case`
Это нужно, чтобы контролировать бюджет реально.
---
## 10. Какие артефакты должен выдать Codex
Codex обязан выдать:
1. `docs/normalizer_forensic_audit_v1_1.md`
2. обновлённые prompt-файлы:
* `prompts/developer/normalizer_v1_1.txt`
* `prompts/domain/normalizer_domain_v1_1.txt`
* `prompts/fewshot/normalizer_fewshot_v1_1.txt`
3. новый eval-набор:
* `eval_cases/normalizer_eval_v1_1_30cases.json`
4. обновлённый экономный eval runner
5. отчёты:
* `reports/normalizer_eval_v1_1_run.md`
* `reports/normalizer_eval_v1_1_run.json`
6. краткий changelog:
* `docs/normalizer_v1_1_changes.md`
---
## 11. Формат changelog
В changelog обязательно указать:
* что именно было изменено в promptах;
* какие linguistic patterns добавлены;
* какие few-shot кейсы добавлены;
* какие кейсы были проблемными в baseline;
* сколько API-вызовов было потрачено на этап;
* итоговые метрики до/после;
* что осталось проблемным после тюнинга.
---
## 12. Приёмка этапа
Этап считается принятым, если одновременно выполнены условия:
1. Не превышен лимит внешних API-вызовов:
* желательно до 40,
* жёсткий потолок 45 только при техфейлах.
2. Есть forensic-аудит 3 проблемных baseline-кейсов.
3. Есть обновлённые prompt/few-shot файлы.
4. Есть новый eval-набор из 30 кейсов.
5. Есть один финальный eval-run.
6. Schema validation не просела.
7. `route_hint_accuracy` не стала хуже baseline.
8. `intent_class_accuracy` выросла заметно относительно baseline.
9. `high_confidence_error_rate` не вырос, а лучше — снизился.
10. В отчёте есть честный список оставшихся mismatchов.
---
## 13. Короткий practical summary для Codex
Что делать по сути:
1. Разобрать 3 плохих кейса.
2. Точечно усилить taxonomy и causal-language interpretation.
3. Добавить 57 сильных few-shot примеров.
4. Не трогать лишнего.
5. Собрать 30-кейсовый eval set.
6. Прогнать его **одним проходом**.
7. Сравнить с baseline.
8. Выдать отчёт и changelog.
9. Не жечь бюджет.
---
## 14. Самый важный акцент
Главная задача Codex сейчас — **не сделать “идеальную исследовательскую систему”**, а сделать **дешёвую и умную доводку** уже рабочего normalizerа.
То есть нужно:
* чинить только то, что реально болит;
* не трогать то, что уже работает;
* не плодить дорогие прогоны;
* улучшать качество через forensic + prompt/few-shot patching.
---

View File

@ -0,0 +1,453 @@
TZ_LLM_Normalizer_v2.0.1.md
Ниже даю **ТЗ на `Normalizer v2.0.1`** — именно на снижение лишних `clarification`, без отката к старой хрупкой схеме.
---
# ТЗ: `Normalizer v2.0.1`
## Снижение избыточных clarification в decomposition-first схеме
---
## 1. Контекст
На `normalizer_v2` новая архитектура показала правильное направление:
* `schema_validation_pass_rate = 100`
* `scope_in_scope_rate = 100`
* `out_of_scope_fragment_rate = 0`
* `routed_fragment_rate = 100`
Но при этом:
* `clarification_required_rate = 80%`
Комментарий:
Это означает, что система уже умеет:
* не путать контур,
* не ломать схему,
* не раздувать сообщения на лишние фрагменты,
но пока **слишком часто боится исполнять нормальные in-scope вопросы без уточнения**.
---
## 2. Цель этапа
Сделать так, чтобы `v2`:
1. сохранял текущие сильные свойства:
* стабильную schema;
* корректный scope filter;
* decomposition-first архитектуру;
* безопасное поведение;
2. но реже уходил в unnecessary clarification на одноходовых бухгалтерских вопросах;
3. различал:
* реально неисполняемый вопрос,
* и вопрос, который можно исполнить с мягкими допущениями.
---
## 3. Главный принцип этапа
### Сейчас
Логика, похоже, такая:
> если вопрос не идеально конкретен, лучше clarification.
### Что нужно
Новая логика:
> если вопрос в контуре и уже достаточно понятен для выбора рабочего маршрута, его надо исполнять без clarification.
То есть `clarification` должен быть:
* не дефолтным safe fallback,
* а **редким и оправданным режимом**.
---
## 4. Что нужно ввести
---
## 4.1. Новый статус исполнимости
Вместо бинарной схемы:
* `executable`
* `needs_clarification`
нужно ввести три уровня:
### A. `executable`
Вопрос достаточно определён. Можно сразу идти дальше.
### B. `executable_with_soft_assumptions`
Вопрос не идеален, но:
* контур понятен,
* объектный узел понятен,
* тип задачи понятен,
* маршрут понятен,
* уточнение не критично для старта.
В этом случае **clarification не нужен**.
### C. `needs_clarification`
Вопрос слишком неопределённый, чтобы надёжно:
* выбрать маршрут,
* определить объект/период,
* или понять, какая именно задача ставится.
Комментарий:
Это главное изменение всей версии `v2.0.1`.
---
## 4.2. Новый флаг в `normalized_query_v2`
Добавить fragment-level поле:
```json
{
"execution_readiness": "executable | executable_with_soft_assumptions | needs_clarification",
"clarification_reason": "string | null"
}
```
Комментарий:
Сейчас у вас, судя по поведению, этого промежуточного уровня не хватает.
---
# 5. Правила, когда НЕ надо спрашивать уточнение
Нужно жёстко зафиксировать случаи, когда система должна **идти в исполнение**, даже если вопрос не идеально формален.
---
## 5.1. Вопрос уже in-scope и понятен по бизнес-узлу
Если вопрос:
* явно относится к текущей компании;
* относится к бухгалтерскому контуру;
* содержит бухгалтерский объект или проблемный узел;
* понятен по типу задачи,
то clarification не нужен.
Примеры:
* “зависшие авансы”
* “подозрительные остатки по 10”
* “хвосты по реализации”
* “подозрительные банковские движения”
* “записи по 97, которые повисли”
* “объекты ОС с кривой логикой начисления”
Комментарий:
Даже если человек говорит неидеально, **это уже нормальный рабочий запрос**.
---
## 5.2. Если хватает для route, не надо просить идеальную конкретику
Если уже можно определить:
* это rule-based,
* это anomaly,
* это cross-entity,
* это heavy overview,
то clarification не нужен только потому, что вопрос сформулирован “по-человечески”.
То есть:
* “выглядит подозрительно”
* “висит”
* “криво”
* “не сходится”
* “повисло”
* “аукнется позже”
не должны автоматически вести к clarification.
---
## 5.3. Если период можно взять из контекста сеанса — не спрашивать лишнее
Если в сессии уже есть:
* активный период,
* или дефолтный рабочий месяц,
* или системный `current_analysis_period`,
то fragment может идти как `executable_with_soft_assumptions`.
Комментарий:
Не надо мучить юзера вопросом “уточните период”, если в рабочем контуре и так есть активный месяц.
---
## 5.4. Если вопрос явно просит “найди проблемные места”, clarification не нужен
Вопросы вида:
* “где проблема”
* “покажи подозрительные”
* “что висит”
* “что не сходится”
* “что аукнется”
* “что требует проверки”
должны идти без уточнения, если:
* понятен участок учета,
* понятен тип проблемы,
* и нет конфликтующих предметных смыслов.
---
# 6. Когда clarification действительно нужен
Нужно сузить этот режим до реально сложных случаев.
Clarification нужен, если:
### 6.1.
Непонятно, вопрос вообще про бухгалтерский контур или нет.
### 6.2.
Непонятно, про какой объект/участок учёта идёт речь вообще.
Пример:
* “чекни, что там у нас не так”
### 6.3.
В сообщении несколько задач, но они конфликтуют и не раскладываются надёжно.
### 6.4.
Для исполнения критично не хватает периода, а его нельзя безопасно вывести из контекста.
### 6.5.
Вопрос смешивает:
* данные компании,
* и общую бухгалтерскую теорию / законы / абстрактные советы.
---
# 7. Что нужно поменять в promptах
---
## 7.1. Developer prompt
Добавить блок:
```text
If a fragment is clearly in-scope, maps to a recognizable accounting problem area, and provides enough information to choose a working route, do not mark it as clarification-required.
Use `execution_readiness = executable_with_soft_assumptions` when the request is operationally understandable even if some details are implicit.
Only use `needs_clarification` when missing information blocks reliable execution or routing.
```
---
## 7.2. Отдельно прописать мягкие рабочие запросы
Добавить правило:
```text
Questions phrased in human, vague, or colloquial accounting language (for example: “what is hanging”, “what looks suspicious”, “what may blow up later”, “what does not converge”, “what is crooked here”) should still be executable if the accounting area and business problem are understandable.
```
---
## 7.3. Не требовать лишней формальности
Добавить правило:
```text
Do not require document IDs, exact periods, or exact objects when the user is clearly asking for a scan, review, anomaly search, rule check, or problem-area analysis.
```
---
# 8. Что поменять в коде
---
## 8.1. Добавить `execution_readiness`
Новый enum:
* `executable`
* `executable_with_soft_assumptions`
* `needs_clarification`
---
## 8.2. Clarification policy runner
Сделать отдельную функцию:
```text
decide_fragment_execution_policy(fragment, session_context) -> execution_policy
```
Она должна проверять:
* in-scope ли фрагмент;
* понятен ли бизнес-узел;
* можно ли выбрать route;
* есть ли активный период/контекст;
* критична ли недосказанность.
---
## 8.3. Clarification не должен срабатывать напрямую из prompt без post-check
То есть:
* LLM может пометить `needs_clarification`,
* но код должен ещё раз проверить:
* а правда ли вопрос неисполняемый?
* или это просто мягкий человеческий запрос, который можно обработать?
Комментарий:
Это важно, чтобы не возвращать system behavior обратно в “LLM испугалась — всё, стоп”.
---
# 9. Что нужно в GUI
Добавить в output:
### A. `execution_readiness`
По каждому фрагменту.
### B. `clarification_reason`
Чтобы видеть, почему система решила тормозить.
### C. `soft_assumption_used`
Например:
* `period_from_session_context`
* `company_scope_defaulted`
* `problem_scan_mode_enabled`
Это поможет отлаживать поведение без гадания.
---
# 10. Новый eval для `v2.0.1`
Нужен отдельный маленький eval-набор:
не на multi-intent, а на **clarification policy**.
## Размер
1520 кейсов.
## Состав
* 10 одноходовых in-scope вопросов, которые **не должны** требовать clarification;
* 5 реально ambiguous вопросов, которые **должны** требовать clarification;
* 35 смешанных или out-of-scope вопросов.
## Метрики
Добавить:
* `clarification_precision`
* `clarification_recall`
* `false_clarification_rate`
* `executable_with_soft_assumptions_rate`
---
# 11. Целевые показатели этапа
Минимально приемлемо:
* `clarification_required_rate <= 50%` на таких одноходовых бухгалтерских вопросах
* `false_clarification_rate` заметно ниже текущего
* scope/schema не деградируют
Хорошо:
* `clarification_required_rate <= 35%`
Очень хорошо:
* `clarification_required_rate <= 25%`
Комментарий:
Не надо ставить сразу “0 clarification”.
Clarification нужен, просто не должен быть режимом по умолчанию.
---
# 12. Что нельзя делать
Codex запрещено:
1. возвращать старую route-first схему `v1.x`;
2. выключать clarification совсем;
3. снова делать один большой `intent_class` центром мира;
4. жёстко захардкодить эти 10 вопросов;
5. тюнить только под конкретную десятку вместо общей policy.
---
# 13. Артефакты
Codex должен выдать:
1. `docs/normalizer_v2_0_1_spec.md`
2. `prompts/developer/normalizer_v2_0_1.txt`
3. `schemas/normalized_query_v2_0_1.json`
4. `docs/clarification_policy.md`
5. `reports/v2_0_1_clarification_eval_plan.md`
---
# 14. Короткий смысл этапа
`v2.0.1` — это не улучшение “понимания бухгалтерии”.
Это **настройка порога между**:
* “вопрос уже можно исполнять”
* и
* “надо тормознуть и спросить уточнение”
Сейчас система слишком часто тормозит.
Нужно сделать её **осторожной, но рабочей**, а не просто осторожной.
---
Если хочешь, я следующим сообщением дам ещё **ультра-короткий job prompt для Codex** на этот этап.

View File

@ -0,0 +1,467 @@
TZ_LLM_Normalizer_v2.0.2.md
Ниже даю **ТЗ на новый вариант — `Normalizer v2.0.2`**.
Это уже не про “ещё лучше понять бухгалтерию”, а про **доводку устойчивости исполнения** после последнего жирного прогона.
---
# ТЗ: `Normalizer v2.0.2`
## Stability / No-Route / Schema Hardening
---
## 1. Контекст этапа
После перехода на `v2.0.1` система показала правильный вектор:
* `schema_validation_pass_rate = 96.15`
* `scope_in_scope_rate = 92.31`
* `clarification_required_rate = 15.38`
* `out_of_scope` отрабатывается
* нормальные in-scope вопросы в большинстве случаев проходят без лишнего clarification
Комментарий:
Это уже лучше, чем бесконечный prompt-tuning `v1.x`, потому что новая схема стала:
* устойчивее к новым формулировкам;
* лучше разделять допустимый/недопустимый контур;
* меньше душить нормальные вопросы уточнениями.
Но после жирного прогона видны три конкретные проблемы:
1. `schema_validation_pass_rate` упала с 100 до 96.15
2. `no_route_fragment_rate = 20.69` — слишком много
3. часть внутренних состояний (`fallback_type`, `execution_readiness`, `soft_assumptions`) согласованы неидеально
---
## 2. Цель этапа
Довести `v2.0.1` до более устойчивой рабочей версии `v2.0.2`, не меняя базовую архитектуру.
Цели:
* вернуть `schema_validation_pass_rate` к `100`
* разобраться с природой `no_route`
* сделать внутреннюю policy-логику согласованной
* не сломать уже хорошие свойства:
* scope gating
* out-of-scope filtering
* умеренный clarification rate
* decomposition-first подход
---
## 3. Главный принцип этапа
На этом этапе **не надо**:
* снова тюнить под конкретные 26 вопросов;
* переписывать всю prompt-логику;
* возвращаться к `v1.x`;
* пытаться “угадать всё лучше через ещё один prompt”.
Нужно:
* сделать систему **строже и честнее на уровне исполнения**;
* понять, где `no_route` правильный, а где это недонастроенный mapping;
* сделать output pipeline согласованным и стабильным.
Комментарий:
Это уже этап **операционной жёсткости**, а не “семантической магии”.
---
# 4. Что именно надо чинить
---
## 4.1. Проблема A: schema validation не 100
### Симптом
В последнем прогоне:
* `schema_validation_pass_rate = 96.15`
* кейс `BQ-001` не прошёл валидацию
* был retry (`request_count_for_case = 2`)
### Требование
Нужно вернуть:
* `schema_validation_pass_rate = 100`
### Что сделать
1. Разобрать `BQ-001` целиком:
* raw question
* raw model output
* parsed object
* validation error
* retry output
2. Выявить точную причину:
* field missing
* wrong enum
* wrong nested type
* prompt overflow / malformed json
* parser inconsistency
3. Исправить причину на системном уровне, а не точечно под кейс.
### Ожидаемый артефакт
Файл:
`docs/v2_0_2_schema_forensic.md`
Комментарий:
Schema loss даже в одном кейсе — это нельзя игнорировать.
Это технический риск уровня “сломается в проде в неожиданный момент”.
---
## 4.2. Проблема B: слишком много `no_route`
### Симптом
* `no_route_fragment_rate = 20.69`
* `route_distribution.no_route = 6`
### Требование
Разделить все `no_route` случаи на две категории:
### A. Legit no-route
Когда реально нельзя безопасно отправить дальше:
* fragment out-of-scope;
* fragment слишком неопределённый;
* fragment требует clarification;
* fragment семантически распознан, но не относится к исполняемому бухгалтерскому действию.
### B. Missing route mapping
Когда fragment уже in-scope и достаточно понятен, но код не умеет подобрать маршрут.
### Что сделать
1. Выгрузить все fragments с `no_route`
2. Для каждого сделать ручную классификацию:
* `legit_no_route`
* `missing_mapping`
3. Если это `missing_mapping`, добавить deterministic mapping rule
4. Если это `legit_no_route`, зафиксировать policy явно
### Ожидаемый артефакт
Файл:
`docs/v2_0_2_no_route_audit.md`
Комментарий:
Сейчас `no_route` — это ещё слишком “чёрный ящик”.
Нужно сделать так, чтобы каждый такой случай был объясним.
---
## 4.3. Проблема C: несогласованность execution state
### Симптом
Есть кейсы, где:
* `fallback_type = none`
* `predicted_clarification_required = false`
* но `executable_with_soft_assumptions_fragments = 0`
Например:
* `BQ-007`
* `BQ-024`
### Что это значит
Внутренняя state machine не полностью согласована.
### Требование
Согласовать следующие статусы между собой:
* `domain_relevance`
* `execution_readiness`
* `predicted_clarification_required`
* `fallback_type`
* `route_decision`
* `soft_assumption_used`
### Целевая логика
Если fragment:
* in-scope
* не clarification
* не out-of-scope
* и route выбран
то должно быть явно видно:
* либо `execution_readiness = executable`
* либо `execution_readiness = executable_with_soft_assumptions`
Не должно быть “исполняем, но readiness не поднят”.
### Ожидаемый артефакт
Файл:
`docs/v2_0_2_execution_state_machine.md`
---
# 5. Что изменить в схеме
## 5.1. Явно ввести `execution_readiness`
Если ещё не введено как обязательное поле — сделать обязательным.
```json id="mgbtkn"
{
"execution_readiness": "executable | executable_with_soft_assumptions | needs_clarification | no_route"
}
```
## 5.2. Явно ввести `route_status`
```json id="6sdd3j"
{
"route_status": "routed | no_route"
}
```
## 5.3. Явно ввести `no_route_reason`
Если `route_status = no_route`, обязателен reason:
```json id="y0eqbg"
{
"no_route_reason": "out_of_scope | insufficient_specificity | missing_mapping | unsupported_fragment_type"
}
```
Комментарий:
Это очень важно.
Без этого вы будете бесконечно смотреть на `no_route` как на аморфную массу.
---
# 6. Что изменить в коде
---
## 6.1. Добавить post-normalization state resolver
Сделать отдельный кодовый слой после ответа LLM:
```text id="kcynfd"
resolve_fragment_execution_state(fragment, session_context) -> resolved_fragment
```
Он должен:
1. нормализовать readiness;
2. нормализовать route_status;
3. ставить `no_route_reason`;
4. приводить `fallback_type` в согласованное состояние.
---
## 6.2. Ввести deterministic `no_route` policy
Если fragment:
* in-scope
* и не clarification
* и есть достаточно route-critical flags
то `no_route` запрещён.
В таком случае должен выбираться маршрут.
`no_route` разрешён только если:
* fragment реально вне контура;
* fragment реально недостаточно определён;
* fragment unsupported by current route map.
---
## 6.3. Добавить trace completeness check
Сейчас у вас раньше был кейс с пустым trace view.
Нужно проверить, что для каждого run сохраняется:
* raw input
* raw model output
* parsed fragments
* resolved execution state
* final route per fragment
Если trace неполный — логировать это как системную ошибку.
---
# 7. Что изменить в promptах
На этом этапе prompt менять минимально.
Нужно только:
* добавить требование, чтобы fragment-level output был полным и непротиворечивым;
* не трогать общий semantic слой без нужды.
### Добавить в developer prompt
```text id="xlg5ut"
Every in-scope fragment must produce a consistent execution state.
If a fragment is routable, mark it as executable or executable_with_soft_assumptions.
Do not leave routable fragments in an unresolved execution state.
If a fragment cannot be routed, provide an explicit no_route_reason.
```
Комментарий:
Это не semantic tuning, а дисциплина outputа.
---
# 8. Новый eval для `v2.0.2`
Нужен уже не просто набор “сложных вопросов”, а **размеченный eval по policy**.
## 8.1. Состав eval
Собрать 20 кейсов:
### Блок A — routable in-scope
8 кейсов
Ожидание:
* in-scope
* no clarification
* route selected
* no `no_route`
### Блок B — legit clarification
4 кейса
Ожидание:
* in-scope
* clarification needed
### Блок C — out-of-scope
4 кейса
Ожидание:
* out-of-scope
* no route
* fallback out_of_scope
### Блок D — borderline / soft assumptions
4 кейса
Ожидание:
* in-scope
* executable_with_soft_assumptions
* route selected
---
## 8.2. Новые метрики
Обязательно считать:
* `schema_validation_pass_rate`
* `scope_detection_accuracy`
* `route_resolution_accuracy`
* `no_route_precision`
* `false_no_route_rate`
* `execution_state_consistency_rate`
* `clarification_precision`
* `clarification_recall`
---
# 9. Целевые показатели
Минимально приемлемо:
* `schema_validation_pass_rate = 100`
* `execution_state_consistency_rate >= 95`
* `false_no_route_rate <= 10`
* `route_resolution_accuracy` заметно выше текущего
* `clarification_required_rate` не выше текущего на in-scope одноходовых кейсах
Хорошо:
* `false_no_route_rate <= 5`
* `execution_state_consistency_rate = 100`
---
# 10. Что нельзя делать
Codex запрещено:
1. снова возвращать один главный `intent_class` как основу всего;
2. снова лечить всё через few-shot под эти 26 вопросов;
3. занижать `no_route` искусственно, просто проставляя маршрут куда угодно;
4. выключать fallback/out-of-scope;
5. игнорировать единичный schema fail как “неважный”.
---
# 11. Артефакты
Codex должен выдать:
1. `docs/v2_0_2_schema_forensic.md`
2. `docs/v2_0_2_no_route_audit.md`
3. `docs/v2_0_2_execution_state_machine.md`
4. `schemas/normalized_query_v2_0_2.json`
5. `prompts/developer/normalizer_v2_0_2.txt`
6. `reports/v2_0_2_eval_plan.md`
---
# 12. Короткий смысл этапа
`v2.0.2` — это версия не про “лучше понимать язык”, а про:
* **не ломать схему**
* **не терять route без объяснения**
* **сделать внутренние состояния честными и жёсткими**
* **перестать иметь серые зоны между clarification / no-route / executable**
---
# 13. Практический итог
После `v2.0.2` вы должны получить систему, где для каждого fragment всегда понятно:
* он в контуре или нет;
* он исполним или нет;
* если не исполним — почему;
* если исполним — с какими допущениями;
* какой route выбран;
* если route не выбран — по какой строго указанной причине.
Это уже будет нормальный инженерный фундамент, а не “LLM что-то там решила”.
---

585
IN/TZ_LLM_Normalizer_v2.md Normal file
View File

@ -0,0 +1,585 @@
TZ_LLM_Normalizer_v2.md
Да, всё, картина понятна.
И по тому, что ты сейчас написал, **ТЗ уже можно делать нормально**, без новых срезов 1С.
Главное, что я из твоего сообщения беру как опору:
* **стабильная база у вас — `v1.1.2`**;
* `v1.1.2.1` показал, что бесконечный prompt-tuning — тупик;
* **допустимый контур** у вас определяется не “всей бухгалтерией мира”, а так:
* вопросы **по текущему предприятию**,
* вопросы **по его данным**,
* вопросы **по сущностям вашей онтологии / учётного контура**;
* **недопустимый контур**:
* абстрактная бухгалтерия,
* законы, формы, “как вообще по ФСБУ/НК РФ”,
* вопросы не про текущее предприятие,
* вопросы не про данные этой базы;
* если вопрос не по контуру — **в 1С не идём**;
* если пользователь пишет “потоком”, система должна:
* разложить это на куски,
* отфильтровать мусор,
* оставить только валидные task fragments,
* уже из них собрать человекочитаемый ответ.
Это уже достаточно, чтобы зафиксировать **правильное ТЗ на `Normalizer v2 / Decomposition-first Pre-Router`**.
---
# ТЗ: `Normalizer v2`
## Decomposition-first pre-router для бухгалтерического ассистента
---
## 1. Цель этапа
Перестроить текущий LLM-normalizer из схемы:
**сырой вопрос → один intent_class → один route_hint**
в более устойчивую схему:
**сырой вопрос → decomposition → semantic fragments → route-critical flags → deterministic routing in code**
Цель:
* перестать бесконечно дотюнивать prompt под каждую новую пачку вопросов;
* устойчиво обрабатывать:
* свободную человеческую речь,
* длинные сообщения,
* смешанные запросы,
* multi-intent сообщения,
* “поток сознания” пользователя;
* не допускать поход в бухгалтерский контур для вопросов вне допустимой области.
Комментарий:
Это уже не “ещё один prompt patch”, а **смена архитектуры нормализации**.
---
## 2. Главная идея новой схемы
LLM больше **не должна пытаться угадать один главный класс вопроса** как основу всей логики.
Вместо этого LLM должна:
1. определить, относится ли сообщение к допустимому бухгалтерскому контуру;
2. разложить сообщение на один или несколько **task fragments**;
3. для каждого фрагмента вернуть:
* смысловые признаки,
* флаги,
* границы уверенности,
* domain relevance;
4. передать это в код;
5. код уже сам решает:
* пускать ли это в 1С-контур;
* какому маршруту это соответствует;
* нужно ли разбивать ответ на несколько частей;
* нужно ли вернуть fallback/уточнение.
Комментарий:
То есть LLM здесь — **semantic parser**,
а не “магический final decision engine”.
---
## 3. Что именно было не так в `v1.x`
На `v1.x` одна модель одновременно пыталась:
* понять intent;
* понять causal;
* понять route;
* понять period;
* понять output shape;
* выбрать один taxonomy label.
На стабильном типизированном наборе это работало хорошо.
На новых 30 вопросах всё поплыло:
* `intent_class_accuracy = 70`
* `route_hint_accuracy = 80`
* `causal_flag_accuracy = 60`
Комментарий:
Это не значит, что LLM плохая.
Это значит, что **схема слишком хрупкая**:
она слишком зависит от того, как именно человек сформулировал вопрос.
---
## 4. Новая архитектура v2
---
## 4.1. Вход
Входом является **сырое сообщение пользователя**, которое может быть:
* коротким;
* длинным;
* многочастным;
* неструктурированным;
* частично бухгалтерским, частично нет;
* содержать:
* вопрос,
* сомнение,
* гипотезу,
* просьбу проверить,
* комментарий,
* мусор,
* лирическое отступление.
---
## 4.2. Выход LLM
LLM должна возвращать **не один итоговый intent**, а structured decomposition.
### Ключевая идея
На выходе нужен объект вида:
```json
{
"schema_version": "normalized_query_v2",
"message_in_scope": true,
"scope_confidence": "high",
"fragments": [...],
"discarded_fragments": [...],
"global_notes": {...}
}
```
---
## 4.3. Фрагменты
Каждый фрагмент — это самостоятельный кандидат на задачу.
Пример:
```json
{
"fragment_id": "F1",
"raw_fragment_text": "по поставщикам не бьются взаиморасчеты",
"normalized_fragment_text": "Проверка расхождений по взаиморасчетам с поставщиками",
"domain_relevance": "in_scope",
"business_scope": "company_specific_accounting",
"entity_hints": ["supplier", "settlements", "payments", "documents"],
"account_hints": ["60"],
"time_scope": {
"type": "explicit",
"value": "2020-06",
"confidence": "medium"
},
"flags": {
"has_multi_entity_scope": true,
"asks_for_chain_explanation": true,
"asks_for_ranking_or_top": false,
"asks_for_period_summary": false,
"asks_for_rule_check": false,
"asks_for_anomaly_scan": false,
"asks_for_exact_object_trace": false,
"asks_for_evidence": true
},
"candidate_labels": ["cross_entity"],
"confidence": "medium"
}
```
Комментарий:
Фрагмент — это не “ответ”, а **единица смыслового разбора**.
---
# 5. Главный принцип: domain gating
Это критично.
## 5.1. Что считается допустимым контуром
Допустимыми считаются только вопросы:
1. про **текущее предприятие**;
2. про **его данные**;
3. про сущности и связи внутри вашей онтологии/учётного контура;
4. про:
* документы,
* проводки,
* взаиморасчёты,
* остатки,
* хвосты,
* закрытие периода,
* аномалии,
* правила учёта,
* признаки ошибок,
* проблемные связи в конкретной базе.
Комментарий:
То есть допустимость определяется **не общей темой “бухгалтерия”**, а связью с **данными и онтологией текущего предприятия**.
## 5.2. Что считается недопустимым контуром
Недопустимыми считаются:
* общие вопросы по бухгалтерии;
* законы, кодексы, формы, инструкции “вообще”;
* абстрактные вопросы без опоры на данные предприятия;
* вопросы не про текущее предприятие;
* бытовой трёп;
* оффтоп;
* всё, что не маппится в ваш учётный контур.
## 5.3. Обязательное правило
Если фрагмент вне контура:
* он **не должен** идти в 1С / retrieval / analytics pipeline;
* по нему возвращается safe fallback.
---
# 6. Что должно происходить с “потоком сознания”
Если пользователь пишет длинно и хаотично, система не должна пытаться сделать вид, что это один аккуратный вопрос.
Она должна:
1. разбить сообщение на фрагменты;
2. определить для каждого:
* это task fragment или шум;
* in-scope или out-of-scope;
* требует ли похода в контур;
3. собрать **valid task set**;
4. выполнить только допустимые части;
5. на выходе сделать один человекочитаемый ответ, где:
* каждая допустимая часть обработана;
* недопустимые части вежливо отбиты.
Комментарий:
Да, конечный ответ должен быть **связанным по диалекту**, а не “пять сухих буллетов из ада”.
Но это уже задача **response composer**, а не нормализатора.
---
# 7. Новый output contract: `normalized_query_v2`
---
## 7.1. Верхний уровень
```json
{
"schema_version": "normalized_query_v2",
"user_message_raw": "string",
"message_in_scope": true,
"scope_confidence": "high | medium | low",
"contains_multiple_tasks": true,
"fragments": [],
"discarded_fragments": [],
"global_notes": {
"needs_clarification": false,
"clarification_reason": null
}
}
```
---
## 7.2. Поля fragment-level
Для каждого фрагмента:
```json
{
"fragment_id": "F1",
"raw_fragment_text": "string",
"normalized_fragment_text": "string",
"domain_relevance": "in_scope | out_of_scope | unclear",
"business_scope": "company_specific_accounting | generic_accounting | offtopic | unclear",
"entity_hints": ["string"],
"account_hints": ["string"],
"document_hints": ["string"],
"register_hints": ["string"],
"time_scope": {
"type": "explicit | inferred | missing",
"value": "string | null",
"confidence": "high | medium | low"
},
"flags": {
"has_multi_entity_scope": true,
"asks_for_chain_explanation": true,
"asks_for_ranking_or_top": false,
"asks_for_period_summary": false,
"asks_for_rule_check": false,
"asks_for_anomaly_scan": false,
"asks_for_exact_object_trace": false,
"asks_for_evidence": false,
"mentions_period_close_context": false
},
"candidate_labels": ["cross_entity", "period_close_risk"],
"confidence": "high | medium | low"
}
```
Комментарий:
Обрати внимание:
здесь **нет обязательного одного `intent_class`**.
Есть `candidate_labels` и `flags`.
Это ключевое изменение v2.
---
# 8. Маршрутизация больше не выбирается LLM напрямую
## 8.1. Что делает LLM
LLM только возвращает decomposition + flags.
## 8.2. Что делает код
Код детерминированно выбирает route на основе flags.
### Пример логики
* если `asks_for_exact_object_trace = true``live_mcp_drilldown`
* если `asks_for_ranking_or_top = true` или `asks_for_period_summary = true``batch_refresh_then_store`
* если `has_multi_entity_scope = true` и `asks_for_chain_explanation = true``hybrid_store_plus_live`
* если `asks_for_rule_check = true` и нет causal chain → `store_feature_risk`
* если `asks_for_anomaly_scan = true` и нет heavy/causal признаков → `store_feature_risk`
* если fragment out-of-scope → no-route / fallback
Комментарий:
Это главный способ уйти от бесконечной гонки за “идеальным prompt”.
---
# 9. Fallback policy
Нужно формализовать три типа fallback.
## 9.1. Out-of-scope fallback
Если вопрос не относится к данным текущего предприятия и его учётному контуру:
Пример ответа:
> Я работаю только с данными и бухгалтерическим контуром текущей компании.
> Вопрос не относится к данным этого предприятия или не попадает в доступную предметную область.
## 9.2. Clarification fallback
Если вопрос в контуре, но недостаточно определён:
Пример ответа:
> Могу проверить это в контуре компании, но нужно уточнить период, документ, счёт или участок, который нужно смотреть.
## 9.3. Partial fallback
Если сообщение смешанное:
* часть in-scope,
* часть out-of-scope.
Пример:
> Проверю часть запроса, которая относится к данным компании.
> Остальная часть выходит за пределы доступного бухгалтерического контура.
Комментарий:
Формулировки должны быть:
* профессиональные,
* не сухие как бетон,
* без “писечки-попочки”,
* но и без канцелярита.
---
# 10. Что делать с сообщением, если там несколько задач
Нужно в коде реализовать `message execution planner`.
## 10.1. Что он делает
* получает fragments;
* выбрасывает out-of-scope;
* группирует in-scope fragments;
* решает:
* это один aggregated response,
* или серия mini-answers;
* передаёт дальше route decisions по каждому фрагменту.
## 10.2. Важное правило
Если пользователь навалил 4 задачи в одном сообщении,
система **не должна** насильно сводить это к одному intent.
---
# 11. Что нужно реализовать в GUI / playground
Текущую GUI можно использовать, но её надо расширить под v2.
Нужны новые вкладки:
### A. Fragment View
Показывает:
* сколько фрагментов выделено;
* тексты фрагментов;
* какие отброшены;
* какие in-scope.
### B. Scope View
Показывает:
* global in-scope / out-of-scope;
* business_scope;
* clarification need.
### C. Flags View
Показывает по каждому фрагменту:
* `has_multi_entity_scope`
* `asks_for_chain_explanation`
* `asks_for_ranking_or_top`
* `asks_for_period_summary`
* `asks_for_rule_check`
* `asks_for_anomaly_scan`
* `asks_for_exact_object_trace`
* `asks_for_evidence`
### D. Route Simulation
Показывает уже не LLM hint, а **что выбрал deterministic code**.
---
# 12. Какие данные нужны для реализации
## Для написания ТЗ — достаточно текущего контекста
Новых срезов базы не нужно.
## Для реализации желательно иметь
1. `normalizer_v1.1.2` prompt set
2. calibration set
3. новая challenge-30
4. 23 живых примера длинного “потока”
5. текущие fallback-тексты, если уже есть
Комментарий:
То есть не данные предприятия нужны, а **данные по самому языковому поведению пользователя**.
---
# 13. Новый eval подход
Нужно перестать мерить всё только через `intent_class_accuracy`.
## Основные метрики v2
* `schema_validation_pass_rate`
* `scope_detection_accuracy`
* `fragment_split_accuracy`
* `out_of_scope_filter_accuracy`
* `route_flag_accuracy`
* `route_decision_accuracy`
* `false_cross_entity_activation_rate`
* `false_causal_activation_rate`
* `multi-intent_handling_accuracy`
Комментарий:
Вот это уже будет реальная метрика устойчивости.
---
# 14. Что нельзя делать
Codex запрещено:
1. снова пытаться лечить всё одним prompt patch;
2. сохранять один mandatory `intent_class` как ядро всей логики;
3. отправлять out-of-scope вопросы в бухгалтерский контур;
4. смешивать decomposition и final user answer в один слой;
5. делать дорогие массовые прогоны на старте.
---
# 15. Ограничения по бюджету
Этап проектировать экономно.
## Допустимый режим
* сначала сделать spec + schema + deterministic router rules;
* потом сделать маленький pilot eval;
* не жечь API на sweepы.
Комментарий:
Сейчас самый умный путь — **архитектурное переустройство**, а не дорогая перестрелка запросами.
---
# 16. Артефакты, которые должен выдать Codex
1. `docs/normalizer_v2_spec.md`
2. `schemas/normalized_query_v2.json`
3. `docs/domain_scope_policy.md`
4. `docs/fallback_policy.md`
5. `docs/fragment_execution_policy.md`
6. `prompts/developer/normalizer_v2.txt`
7. `prompts/fewshot/normalizer_v2.txt`
8. `reports/v2_pilot_eval_plan.md`
---
# 17. Ключевой смысл этапа
Нужно уйти от философии:
> “давайте подгоним модель под очередные 30 вопросов”
к философии:
> “давайте сделаем такую схему, где новые 30 вопросов не ломают систему, потому что система опирается не на один магический ярлык, а на decomposition + flags + deterministic routing.”
---
# 18. Очень короткий вывод
Сейчас **не надо** пытаться добить новую тридцатку patchем.
Сейчас надо:
* зафиксировать `v1.1.2` как стабильный baseline;
* новую тридцатку считать challenge set;
* перейти к `Normalizer v2`, где:
* multi-label,
* fragment decomposition,
* scope filter,
* deterministic routing after LLM.
---
Если хочешь, следующим сообщением я могу дать тебе **ещё более жёсткую короткую версию этого ТЗ для Codex**, уже как job prompt без пояснительного мяса.

View File

@ -0,0 +1,545 @@
Ниже даю ТЗ прямо текстом, без файлов.
# ТЗ: Router / Orchestration Fix для бухгалтерского ассистента
## 1. Контекст этапа
Ontology/mapping слой после semantic-v2 ремонта вытащен из красной зоны: `semantic_coverage_pct` вырос с `61.1917` до `94.9279`, `unknown_links` снизились с `1016` до `102`, `relation_types_total` вырос с `1` до `25`, `covered_entity_classes`с `33` до `42`, `source_id_unknown` ушёл в `0`. Regression tests: `17 passed`.
При этом orchestration/router слой остаётся узким местом. В benchmark по 35 вопросам:
* `route_mismatch_count = 7`
* `degraded_answers_count = 0`
* `batch_route_count = 0`
* `store_feature_risk` используется 19 раз
* `live_mcp_drilldown` — 4 раза
* `hybrid_store_plus_live` — 4 раза
* `store_canonical` — 8 раз.
Остаточные промахи распределены так:
* `heavy_analytical = 4`
* `cross_entity = 2`
* `drilldown_explain = 1`.
Основная confusion matrix:
* `batch_refresh_then_store -> store_feature_risk: 4`
* `hybrid_store_plus_live -> store_canonical: 2`
* `live_mcp_drilldown -> hybrid_store_plus_live: 1`.
Сама policy уже описывает правильную целевую логику:
* exact object trace → `live_mcp_drilldown`
* simple factual in loaded slice → `store_canonical`
* trend/anomaly/risk → `store_feature_risk`
* heavy whole-slice with freshness gap → `batch_refresh_then_store`
* low confidence fallback → `hybrid_store_plus_live`.
Проблема текущего этапа: policy на бумаге правильная, но runtime-router выбирает route слишком грубо и слишком рано downcastит сложные запросы в store-only ветки.
---
## 2. Цель этапа
Довести router/orchestration слой до состояния, в котором:
1. heavy whole-slice запросы реально уходят в `batch_refresh_then_store`, а не в `store_feature_risk`;
2. cross-entity causal/join запросы не downcastятся в `store_canonical`;
3. точечный drilldown по конкретному объекту стабильно уходит в `live_mcp_drilldown`;
4. появляется прозрачный decision log по каждому route-решению;
5. `batch_refresh_then_store` перестаёт быть декларацией и начинает реально исполняться в runtime.
---
## 3. Scope работ
В scope этапа входят:
* refactor query classifier / route selector;
* внедрение decision flags;
* внедрение store sufficiency checker;
* внедрение explicit route guards;
* реализация runtime handoff для `batch_refresh_then_store`;
* внедрение route explanation logging;
* повторный benchmark-run на тех же 35 вопросах.
В scope не входят:
* новый overhaul ontology/mapping слоя;
* новый redesign canonical model;
* расширение snapshot domain beyond June-2020;
* тяжёлая оптимизация latency сверх baseline этого этапа.
Ontology уже починен до уровня, достаточного для данного этапа; оставшиеся unknown-поля точечные и не являются основным блокером для router-фазы.
---
## 4. Проблемы, которые надо исправить
### 4.1 Heavy analytical downcast
Сейчас 4 heavy-вопроса, которые должны идти в `batch_refresh_then_store`, реально уходят в `store_feature_risk`. Это касается whole-slice heavy analytical запросов, включая:
* `Полный риск-срез за июнь`
* `Рейтинг риск-счетов`
* `Рейтинг риск-контрагентов`
* `Company anomaly summary`.
Это противоречит policy, где для `heavy_analytical` задан приоритет `batch_refresh_then_store -> feature_store -> risk_store`.
### 4.2 Cross-entity downcast
Два cross-entity вопроса ожидают `hybrid_store_plus_live`, но реально уходят в `store_canonical`:
* `Свяжи документы покупателей и проводки`
* `Свяжи контрагентов, договоры и проводки`.
Это означает, что router недостаточно хорошо распознаёт chain-join / causal cross-entity shape и преждевременно считает, что canonical store уже достаточен.
### 4.3 Drilldown boundary bug
Один drilldown вопрос ожидает `live_mcp_drilldown`, но реально идёт в `hybrid_store_plus_live`. Это означает, что граница между exact object trace и mixed live/store explain сейчас определена недостаточно жёстко.
### 4.4 Отсутствие реально работающего batch runtime
`batch_route_count = 0`, хотя benchmark содержит heavy-вопросы, ожидающие `batch_refresh_then_store`. Это прямо подтверждает, что batch-маршрут не активируется как реальный runtime-path.
---
## 5. Требуемая архитектура решения
### 5.1 Query classifier v2
Нужно внедрить отдельный слой decision classification перед выбором маршрута.
Требуемый интерфейс:
```python
def classify_query_for_route(
question_text: str,
parsed_intent: dict,
store_metadata: dict,
) -> RouteDecisionFlags:
...
```
Classifier обязан вычислять минимум такие flags:
```python
@dataclass
class RouteDecisionFlags:
needs_exact_object_trace: bool
needs_causal_chain: bool
needs_cross_entity_join: bool
needs_full_period_aggregation: bool
needs_ranking: bool
needs_anomaly_summary: bool
needs_runtime_truth: bool
freshness_sensitive: bool
ambiguous_object_scope: bool
store_sufficiency_confident: bool
precomputed_aggregate_available: bool
```
Назначение: отделить semantic classification вопроса от конечного route selection. Сейчас они, по сути, схлопнуты, из-за чего router переоценивает store sufficiency и недооценивает heavy/cross-entity требования. Это видно по текущим mismatchам.
---
## 6. Правила классификации
### 6.1 `live_mcp_drilldown`
Запрос должен классифицироваться в `live_mcp_drilldown`, если одновременно выполняется:
* запрос относится к exact object trace, posting chain, source-of-record или object-level why/explain;
* объект явно указан или легко нормализуется до конкретного документа/проводки/строки/регистра/субконто;
* ответ требует runtime evidence, а не только store summary.
Это согласуется с policy: “exact object trace or posting chain -> live_mcp_drilldown”.
Триггеры:
* “почему именно эта проводка”
* “источник этой строки”
* “цепочка документ -> проводки -> субконто”
* “почему выбрано это субконто”
* “документ по номеру и его ссылка”.
### 6.2 `hybrid_store_plus_live`
Запрос должен классифицироваться в `hybrid_store_plus_live`, если:
* требуется cross-entity join между 3+ сущностями;
* требуется causal stitching по нескольким источникам;
* store сам по себе не гарантирует достаточную explainability;
* но запрос не является exact object trace в узком смысле.
Триггеры:
* документ ↔ проводки
* контрагент ↔ договор ↔ проводки
* регистр ↔ первичный документ
* explain через движения без указания одного точечного source-of-record.
### 6.3 `store_canonical`
Разрешён только для:
* simple factual within loaded slice;
* материализованных и однозначных canonical facts;
* cross-entity вопросов без causal stitching и без runtime evidence need.
Запрещён для:
* chain joins;
* “свяжи X и Y через Z”;
* source-of-record explanations;
* heavy full-period aggregations.
### 6.4 `store_feature_risk`
Разрешён только для:
* trend / anomaly / risk вопросов;
* ambiguous fuzzy risk/tax questions;
* heavy-вопросов только при явном наличии готового и свежего precomputed aggregate.
Не должен быть default-веткой для heavy_analytical whole-slice вопросов.
### 6.5 `batch_refresh_then_store`
Обязателен для heavy whole-slice вопросов, если:
* нужен full-period aggregation;
* нужен ranking;
* нужен company-wide anomaly/risk summary;
* нужна свежесть выше текущего store snapshot;
* нет подтверждённого precomputed aggregate нужного уровня.
---
## 7. Store sufficiency checker
Нужно реализовать отдельный модуль проверки достаточности store перед выбором маршрута.
Требуемый интерфейс:
```python
def check_store_sufficiency(
question_shape: RouteDecisionFlags,
store_metadata: dict,
) -> StoreSufficiencyResult:
...
```
Минимальная модель результата:
```python
@dataclass
class StoreSufficiencyResult:
canonical_sufficient: bool
feature_sufficient: bool
risk_sufficient: bool
freshness_ok: bool
aggregate_level_ok: bool
ranking_ready: bool
explanation_ready: bool
reason_codes: list[str]
```
Checker обязан учитывать:
* покрытие периода;
* наличие нужной группировки;
* наличие precomputed ranking aggregate;
* наличие risk/anomaly aggregate нужного уровня;
* freshness snapshot;
* достаточность store для explainability.
Ключевая цель — убрать ложные решения вида:
* “store_feature_risk и так хватит”
* “store_canonical и так хватит”
в тех кейсах, где store на самом деле не покрывает shape запроса. Именно это сейчас ломает heavy и cross-entity маршруты.
---
## 8. Explicit route guards
После classifier и sufficiency-checker route должен выбираться не scoringом “в среднем”, а guard-правилами.
Требуемая логика:
```python
def choose_route(flags: RouteDecisionFlags, suff: StoreSufficiencyResult) -> str:
if flags.needs_exact_object_trace:
return "live_mcp_drilldown"
if flags.needs_full_period_aggregation or flags.needs_ranking or flags.needs_anomaly_summary:
if not (flags.precomputed_aggregate_available and suff.freshness_ok and suff.aggregate_level_ok):
return "batch_refresh_then_store"
if flags.needs_cross_entity_join and flags.needs_causal_chain:
if not suff.explanation_ready:
return "hybrid_store_plus_live"
if suff.feature_sufficient and not flags.needs_runtime_truth and (
flags.needs_anomaly_summary or parsed_as_trend_or_risk
):
return "store_feature_risk"
if suff.canonical_sufficient and not flags.needs_causal_chain:
return "store_canonical"
return "hybrid_store_plus_live"
```
Смысл:
* drilldown имеет жёсткий приоритет;
* heavy whole-slice с ranking/summary и недостаточным aggregate обязан уйти в batch;
* causal cross-entity не имеет права downcastиться в `store_canonical`;
* fallback остаётся `hybrid_store_plus_live`.
---
## 9. Runtime batch handoff
Нужно реализовать реальный handoff для `batch_refresh_then_store`.
Требуемый поток:
1. Router выбирает `batch_refresh_then_store`.
2. Создаётся orchestration job с типом `refresh_and_answer`.
3. Job:
* проверяет актуальность snapshot/feature/risk stores;
* при необходимости пересчитывает refresh/features/risk;
* фиксирует run ids;
* передаёт управление answer synthesis.
4. Answer synthesis читает уже обновлённые stores и формирует ответ.
5. Route/log сохраняют факт batch execution.
Минимальный payload job:
```json
{
"job_type": "refresh_and_answer",
"question_id": "Q28",
"slice_window": "2020-06",
"requested_outputs": ["feature_store", "risk_store"],
"reason": ["needs_full_period_aggregation", "needs_ranking", "aggregate_not_sufficient"]
}
```
Обязательное требование: `batch_refresh_then_store` должен стать исполняемым runtime-path, а не декларацией в policy. Сейчас это не так, что видно по `batch_route_count = 0`.
---
## 10. Route explanation logging
Нужно добавить прозрачный лог принятия маршрута.
Требуемый формат:
```python
@dataclass
class RouteDecisionLog:
question_id: str
question_text: str
parsed_class: str
decision_flags: dict
sufficiency_snapshot: dict
candidate_routes: list[str]
rejected_routes: dict[str, str]
chosen_route: str
execution_mode: str
batch_job_id: str | None
```
Пример лог-записи:
```json
{
"question_id": "Q30",
"parsed_class": "heavy_analytical",
"decision_flags": {
"needs_full_period_aggregation": true,
"needs_ranking": false,
"needs_anomaly_summary": true,
"needs_runtime_truth": false
},
"sufficiency_snapshot": {
"feature_sufficient": false,
"risk_sufficient": false,
"freshness_ok": false,
"aggregate_level_ok": false
},
"rejected_routes": {
"store_feature_risk": "aggregate_not_sufficient",
"store_canonical": "wrong_query_shape"
},
"chosen_route": "batch_refresh_then_store"
}
```
Это нужно для последующего дебага benchmark mismatchов. Иначе команда и дальше будет видеть только конечный маршрут, но не причину выбора.
---
## 11. Конкретные ошибки benchmark, которые должны быть закрыты
### 11.1 Heavy
Нужно закрыть как минимум:
* Q26 `Полный риск-срез за июнь`
* Q27 `Рейтинг риск-счетов`
* Q28 `Рейтинг риск-контрагентов`
* Q30 `Company anomaly summary`
Ожидаемый результат: эти вопросы должны идти в `batch_refresh_then_store`, а не в `store_feature_risk`.
### 11.2 Cross-entity
Нужно закрыть:
* Q11 `Свяжи документы покупателей и проводки`
* Q12 `Свяжи контрагентов, договоры и проводки`
Ожидаемый результат: `hybrid_store_plus_live`, а не `store_canonical`.
### 11.3 Drilldown
Нужно закрыть 1 mismatch по границе `live_mcp_drilldown` / `hybrid_store_plus_live`.
Ожидаемый результат: точечный object-trace вопрос не должен уходить в hybrid, если вопрос явно указывает на source-of-record / posting chain.
---
## 12. Предлагаемая структура модулей
### Вариант минимальной структуры
`router/query_classifier.py`
* `classify_query_for_route()`
* правила выделения decision flags
`router/store_sufficiency.py`
* `check_store_sufficiency()`
* оценка достаточности canonical / feature / risk stores
`router/route_selector.py`
* `choose_route()`
* explicit route guards
`orchestration/batch_runtime.py`
* `enqueue_refresh_and_answer_job()`
* `run_refresh_and_answer_job()`
`router/decision_log.py`
* `build_route_decision_log()`
* сериализация route decision trace
`tests/test_router_decision_flags.py`
* флаги для benchmark-вопросов
`tests/test_store_sufficiency.py`
* sufficiency cases
`tests/test_route_guards.py`
* guard logic
`tests/test_batch_runtime_handoff.py`
* batch execution path
---
## 13. Требования к тестам
Нужно добавить unit + integration тесты.
### Unit tests
Покрыть:
* correct flag extraction for heavy queries;
* correct flag extraction for drilldown queries;
* correct flag extraction for cross-entity causal queries;
* store sufficiency false/true cases;
* route guards for each route;
* batch handoff payload building.
### Integration tests
Покрыть:
* heavy query without ready aggregate → `batch_refresh_then_store`
* heavy query with ready fresh aggregate → `store_feature_risk`
* cross-entity causal query → `hybrid_store_plus_live`
* exact object trace → `live_mcp_drilldown`
* simple factual in loaded slice → `store_canonical`
Отдельно нужно прогнать benchmark subset по вопросам:
* Q06Q12
* Q26Q30
потому что именно там сейчас сосредоточены остаточные mismatchи.
---
## 14. Acceptance criteria
Этап считается принятым, если после повторного benchmark-run достигнуты условия:
* `route_mismatch_count <= 2` вместо текущих `7`
* `heavy_analytical mismatches = 0`
* `cross_entity mismatches = 0`
* `drilldown_explain mismatches <= 1`, целевое — `0`
* `batch_route_count > 0`
* `degraded_answers_count = 0`
* `adopt_with_improvements` сохраняется минимум, желательно с качественным улучшением рекомендаций
* route decision logs сохраняются для всех 35 benchmark-вопросов.
Латентность допускается немного выше baseline по heavy-вопросам, так как batch route по профилю дороже: у него baseline retrieval около `1240 ms` против `190 ms` у `store_feature_risk`. Но рост должен быть локализован только на heavy-path и не должен ломать обычные store-first сценарии.
---
## 15. Нефункциональные требования
Нужно сохранить текущие плюсы системы:
* `store-first retrieval policy`;
* bounded context;
* отсутствие деградированных ответов;
* отсутствие uncapped heavy live scans.
Запрещается:
* заменять batch на тяжёлый live-scan;
* насильно переводить все cross-entity вопросы в live;
* ломать простые store-only factual ответы;
* убирать fallback `hybrid_store_plus_live`.
---
## 16. Порядок реализации
Сначала сделать classifier v2 и decision flags. Потом — store sufficiency checker. После этого — route guards. Затем — runtime handoff для batch. Потом — decision logging. И только после этого — повторный benchmark-run и точечная подстройка порогов по остаточным кейсам. Такой порядок минимизирует расползание архитектуры и даёт сразу диагностируемую систему.
---
## 17. Ожидаемый результат этапа
После выполнения этого ТЗ система должна перейти из состояния:
* “ontology уже хорошая, но router маршрутизирует местами по упрощённой логике”
в состояние:
* “ontology + router + orchestration согласованы между собой, а route choice воспроизводим, объясним и исполняется реально, включая batch”.

View File

@ -0,0 +1,234 @@
# 260323_ACCOUNTING_AGENT_GUI_INTEGRATION_GUIDELINES
Дата: 23.03.2026 (MSK)
Контур: NodeDC (`dc_node`) / бухгалтерский агент (черновой GUI + временный монолитный backend)
Статус: интеграционные рекомендации для внешнего разработчика
---
## 1. Цель документа
Зафиксировать единые требования к GUI и backend бухгалтерского агента, который разрабатывается изолированно, чтобы:
1. интеграция в текущий `Node.js`-проект прошла без болезненных переделок;
2. архитектура оставалась совместимой с текущей моделью NodeDC (`UI -> runtime -> OPS`);
3. переход с временного монолита на будущую runtime-оркестрацию был обратимым и предсказуемым.
---
## 2. Базовые принципы (обязательные)
1. Сначала совместимость, потом скорость: любое быстрое решение допускается только если не ломает будущую интеграцию.
2. GUI и backend делаются как изолируемый модуль, а не как форк всей платформы.
3. Контракты API/событий/статусов должны быть стабильными с первой версии.
4. Нельзя смешивать UI-логику, runtime-логику исполнения и слой хранения в один неразделимый блок.
5. Пользовательская терминология в интерфейсе: использовать `NDC` (не `N8N`) в текстах UI.
6. Технические идентификаторы не переименовывать: routes, env vars, payload keys, event names, internal symbols остаются техническими.
---
## 3. Технологический профиль (что нужно соблюдать)
### 3.1 Frontend
1. Совместимый стек: `React 18` + `TypeScript` + подход, совместимый с `Vite`.
2. Архитектурно: компонентный UI с разделением на контейнеры данных и презентационные блоки.
3. Все сетевые вызовы через единый API-клиент-слой (не `fetch` хаотично по компонентам).
4. Состояние:
- локальное UI-состояние отдельно;
- серверное состояние отдельно (runs, statuses, results, trace).
### 3.2 Backend
1. Совместимый стек: `Node.js` + `Express` + JSON API.
2. Формат тела запроса: поддержка `application/json` и `application/*+json`.
3. Для хранения/аналитики ориентироваться на модель, совместимую с Postgres-онтологией (`session/run/result/event`).
4. Временный runtime монолитный, но за абстракцией (см. раздел 8).
### 3.3 Форматы
1. Время только в `ISO 8601`.
2. Денежные поля и суммы: явная валюта + числовой формат без двусмысленностей.
3. Ошибки API: структурированный ответ с `code`, `message`, `details`.
4. Любой статус должен быть машинно-читаемым и человеко-понятным.
---
## 4. Архитектурные границы ответственности
1. GUI отвечает за:
- запуск/остановку;
- отображение состояния;
- фильтры/настройки;
- операторские действия.
2. Backend отвечает за:
- исполнение сценария;
- очередь и жизненный цикл задач;
- запись результатов и ошибок;
- выдачу данных для монитора.
3. Слой данных отвечает за:
- историчность;
- идемпотентность результатов;
- диагностируемость событий.
4. GUI не должен содержать тяжелую бизнес-обработку бухгалтерских документов/очередей.
---
## 5. Минимальный API-контракт (для безболезненной интеграции)
Рекомендуется выделить отдельный namespace, например `/api/accounting-agent/*`, и зафиксировать минимум:
1. `POST /runs/start`
- старт сессии/прогона;
- возвращает `sessionId`, `runId`, `status`.
2. `POST /runs/finish`
- нормализованное завершение `DONE/FAILED/CANCELLED`;
- причина/метаданные ошибки при неуспехе.
3. `GET /runs`
- список прогонов с фильтрами.
4. `GET /runs/:runId`
- карточка прогона + агрегированный статус.
5. `POST /tasks/enqueue`
- постановка бухгалтерской задачи в очередь.
6. `POST /tasks/claim` (или эквивалент)
- получение следующей задачи worker-частью.
7. `POST /tasks/:taskId/complete`
- успешное завершение с результатом.
8. `POST /tasks/:taskId/error`
- завершение с нормализованной ошибкой.
9. `GET /results`
- витрина результатов для GUI/монитора.
10. `GET /trace/run/:runId`
- лента событий для диагностики.
11. `GET /health`
- техническое здоровье сервиса.
Важно:
1. Контракты payload фиксируются версией (`v1`) и не ломаются без явной миграции.
2. Поля `sessionId/runId/taskId` обязательны в событиях и логах.
---
## 6. Канон статусов и жизненного цикла
Единый словарь статусов для GUI и backend:
1. `NONE`
2. `QUEUED`
3. `RUNNING`
4. `DONE`
5. `ERROR`
6. `STALE`
7. `CANCELLED`
Правила:
1. Терминальные статусы (`DONE/ERROR/CANCELLED`) имеют приоритет и не откатываются в `RUNNING`.
2. Повторный запуск не перетирает историю, а создаёт новую сущность запуска/задачи.
3. Для каждого перехода статуса обязателен `updatedAt` и источник изменения (`source`).
---
## 7. Требования к GUI бухгалтерского агента
### 7.1 Что обязательно в первой версии
1. Панель запуска и остановки.
2. Панель прогонов (`runs`) со статусом, временем, инициатором.
3. Панель результатов (`results`) с фильтрами и быстрым поиском.
4. Панель ошибок/исключений.
5. Панель трассировки (`trace`) по выбранному `runId`.
6. Явная индикация: что обновляется в реальном времени, а что по ручному refresh.
### 7.2 UX-правила
1. Никаких скрытых магических фильтров, влияющих на выдачу без отображения в UI.
2. Любое действие пользователя должно иметь видимый эффект: `queued/running/done/error`.
3. Длинные операции не блокируют интерфейс целиком.
4. Ошибки показываются в 2 слоя:
- коротко для оператора;
- подробно для диагностики (код, traceId/runId).
### 7.3 Терминология UI
1. Во всех пользовательских текстах использовать `NDC`.
2. Не использовать `N8N` в текстах интерфейса.
3. В техническом коде/маршрутах внутренние названия не переписывать автоматически.
---
## 8. Требования к временному монолитному backend (с прицелом на замену runtime)
Чтобы потом перейти к отдельному runtime/оркестрации без переписывания GUI:
1. Ввести слой-адаптер исполнения (runtime adapter) как отдельный модуль.
2. GUI/backend общаются с этим адаптером через стабильный внутренний контракт.
3. В монолитной версии адаптер реализован локально.
4. В будущей версии адаптер можно заменить на внешний runtime (без смены GUI API).
5. Очередь/ретраи/лимиты должны быть инкапсулированы в backend, а не в GUI.
Минимальные способности адаптера исполнения:
1. `startRun`;
2. `stopRun`;
3. `enqueueTask`;
4. `claimTask`;
5. `completeTask`;
6. `failTask`;
7. `getRunState`;
8. `getRunTrace`.
---
## 9. Наблюдаемость, логирование, диагностика
1. Логи в структурированном формате JSON.
2. Обязательные поля логов: `timestamp`, `level`, `service`, `sessionId`, `runId`, `taskId`, `eventType`.
3. Для ошибок обязательны: `errorCode`, `errorMessage`, `stack` (где применимо).
4. Должна быть возможность получить trace по `runId` без ручного доступа к серверу.
5. Учитывать рабочую таймзону проекта: `Europe/Moscow` в операционных отчётах.
---
## 10. Безопасность и эксплуатационные правила
1. В секретах/ключах не хранить чувствительные данные в открытых логах.
2. Любые внешние интеграции — через конфигурацию окружения, без хардкода.
3. Все потенциально тяжелые операции ограничить по timeout/retry.
4. Обязательны idempotency-защиты для endpoint-ов старта/завершения/записи результатов.
---
## 11. Definition of Done для подрядчика
Задача считается готовой, если выполнено всё:
1. GUI поднимается и работает локально в связке с backend.
2. Есть запуск/остановка/мониторинг прогонов.
3. Статусы и жизненный цикл реализованы по канону раздела 6.
4. API покрывает минимальный контракт раздела 5.
5. Есть trace-диагностика по `runId`.
6. Ошибки и логирование структурированы.
7. Терминология UI приведена к `NDC`.
8. Подтверждена обратимая миграция с монолита на внешний runtime-адаптер без смены UI-контрактов.
---
## 12. Интеграционный чеклист перед вливанием в `dc_node`
1. Совпадает ли словарь статусов с существующим монитором.
2. Присутствуют ли `sessionId/runId` во всех критичных сущностях и событиях.
3. Нет ли скрытых зависимостей GUI от конкретной монолитной реализации runtime.
4. Не дублирует ли модуль существующие механизмы хранения/трассировки.
5. Не конфликтуют ли route-префиксы и форматы ответов с текущим API-паттерном.
6. Можно ли подключить модуль в NodeDC поэтапно (feature flag / scoped rollout).
---
## 13. Короткий итог для разработчика GUI
1. Делай интерфейс и backend как автономный модуль, но по контрактам, совместимым с NodeDC.
2. Сейчас runtime может быть монолитным, но обязан быть спрятан за адаптером.
3. UI должен опираться на стабильные статусы, `run/session`-идентификаторы и наблюдаемую trace-модель.
4. Всё, что пользователь видит, называем через `NDC`.

View File

@ -0,0 +1,840 @@
AI СЛОЙ ДЛЯ НАРМАЛИЗАЦИИ ЗАПРОСОВ ОТ ЮЗЕРА - ГУЙ ДОЛЖЕН БЫТЬ ПОЛНОСТЬЮ РУИФИЦИРОВАН !!!
Ниже — **конкретное ТЗ для Codex** на первый этап интеграции LLM в контур бухгалтерского ассистента.
Основа решения: использовать **Responses API**, а не legacy-путь Assistants, и заставить модель возвращать **строго структурированный JSON** через structured outputs / JSON schema. API-ключ нельзя светить в client-side коде, поэтому даже для localhost-стенда запросы должны идти через локальный backend-proxy. ([OpenAI Платформа][1])
---
# ТЗ: LLM Normalizer Playground + Pre-Router Normalization Layer
## 1. Цель этапа
Реализовать локальный тестовый стенд и базовый backend-слой для интеграции LLM в качестве **нормализатора человеческих бухгалтерских запросов**.
LLM на этом этапе **не отвечает за финальный бухгалтерский ответ** и **не ходит напрямую в данные**.
LLM отвечает только за:
* разбор человеческого запроса;
* выделение сущностей, периода, типа задачи и причинно-следственной формы;
* возврат строго структурированного JSON;
* подготовку нормализованного input для существующего router/orchestration слоя.
---
## 2. Бизнес-смысл этапа
Текущий deterministic router хорошо работает на каноничных и полуструктурированных вопросах, но заметно хуже справляется с длинными человеческими формулировками, особенно в cross-entity и causal explain сценариях.
Цель этого этапа — не заменить router, а поставить перед ним **semantic front-end**, который переводит живой язык пользователя в нормализованный внутренний контракт.
Новая целевая цепочка:
**Пользователь → LLM Normalizer → Normalized Query JSON → Existing Router → Existing Retrieval/Stores/Batch → Final Answer Layer**
---
## 3. Что должно получиться на выходе
После реализации должен существовать рабочий localhost-стенд, в котором можно:
* вставить API key;
* выбрать модель;
* редактировать системный / developer prompt;
* редактировать domain prompt;
* ввести человеческий бухгалтерский вопрос;
* отправить запрос в OpenAI через локальный backend;
* получить:
* raw model output,
* normalized JSON output,
* parsed route hint,
* confidence,
* token usage,
* latency,
* trace/log записи;
* сохранить результат как тест-кейс для дальнейшей оценки.
---
## 4. Технологическое решение
### 4.1 API-путь
Использовать **Responses API** как основной способ интеграции. Это соответствует текущему рекомендуемому стеку OpenAI для новых интеграций. ([OpenAI Платформа][1])
### 4.2 Формат ответа модели
Использовать **structured outputs / JSON schema response format**, чтобы модель возвращала не свободный текст, а строго валидируемый JSON-объект. ([OpenAI Платформа][1])
### 4.3 Безопасность ключа
API key не должен использоваться напрямую из фронта.
Ключ должен передаваться в локальный backend-proxy и использоваться только серверной частью. В документации OpenAI прямо сказано, что API key — секрет и его нельзя светить в браузере или клиентском коде. ([OpenAI Платформа][2])
### 4.4 Целевая модель первого этапа
Для первого этапа предусмотреть параметризуемую модель, по умолчанию — `gpt-4o-mini` как fast/cheap normalizer-кандидат. Конкретный model id должен быть настраиваемым из UI, без хардкода в коде.
---
## 5. Scope этапа
### В scope входит
* localhost GUI playground;
* локальный backend-proxy для OpenAI API;
* normalizer prompt system;
* structured JSON schema;
* вызов Responses API;
* валидация JSON-ответа;
* логирование запросов/ответов;
* нормализация route hints;
* сохранение trace;
* базовый eval-mode на заранее заданном наборе вопросов.
### В scope не входит
* полноценный production chat;
* tool use;
* live access к 1С данным;
* финальный answer synthesis;
* agentic orchestration;
* самостоятельный выбор model toolchain;
* дообучение модели;
* интеграция в production UI Node/DC.
---
## 6. Архитектура решения
## 6.1 Общая схема
### Компоненты
1. **Frontend Playground**
2. **Local Backend Proxy**
3. **Prompt Manager**
4. **Normalizer Service**
5. **Schema Validator**
6. **Trace Logger**
7. **Optional Eval Runner**
8. **Route Hint Adapter** для передачи результата в существующий router
### Поток
1. Пользователь вводит сырой вопрос.
2. Frontend отправляет payload на local backend.
3. Backend собирает prompt + schema + user input.
4. Backend вызывает Responses API.
5. Backend получает structured JSON.
6. Backend валидирует JSON по schema.
7. Backend вычисляет route hint summary.
8. Backend возвращает UI:
* raw response,
* normalized JSON,
* parse status,
* validation status,
* usage,
* latency,
* trace id.
---
## 7. Требования к GUI
Нужен простой localhost GUI. Подойдёт React/Vite/Next localhost-only интерфейс или любой быстрый web UI.
## 7.1 Обязательные блоки интерфейса
### A. Connection Panel
Поля:
* `OpenAI API Key`
* `Model ID`
* `Base URL` (опционально, по умолчанию OpenAI)
* `Temperature`
* `Max output tokens`
Кнопки:
* `Save local session config`
* `Test connection`
### B. Prompt Panel
Отдельные текстовые области:
* `System Prompt`
* `Developer / Instruction Prompt`
* `Domain Prompt`
* `Schema Notes` (опционально)
* `Few-shot examples` (опционально)
Кнопки:
* `Load preset`
* `Save preset`
* `Diff with previous`
* `Reset to default`
### C. User Query Panel
Поля:
* `Raw user question`
* `Optional period context`
* `Optional business context`
* `Optional expected route` (для eval mode)
Кнопки:
* `Normalize`
* `Normalize + Save as test case`
### D. Output Panel
Вкладки:
* `Normalized JSON`
* `Raw model output`
* `Route hint summary`
* `Validation`
* `Logs`
### E. Runtime Metrics Panel
Показывать:
* `trace_id`
* `request_started_at`
* `request_finished_at`
* `latency_ms`
* `input_tokens`
* `output_tokens`
* `total_tokens`
* `validation_status`
* `confidence`
* `schema_version`
* `prompt_version`
### F. History Panel
Список прошлых запросов:
* timestamp
* shortened question
* model
* confidence
* validation pass/fail
* route hint
* save status
---
## 8. Требования к backend
Backend должен быть локальным HTTP-сервисом.
Подойдут:
* Node.js + Express / Fastify
или
* Python + FastAPI
Рекомендуемый вариант для скорости: **Node.js + Fastify**.
## 8.1 Обязательные endpointы
### `POST /api/openai/test-connection`
Проверка, что ключ рабочий и Responses API доступен.
### `POST /api/normalize`
Основной endpoint для нормализации вопроса.
Вход:
```json
{
"apiKey": "string",
"model": "gpt-4o-mini",
"temperature": 0,
"systemPrompt": "string",
"developerPrompt": "string",
"domainPrompt": "string",
"schemaVersion": "v1",
"userQuestion": "string",
"context": {
"period_hint": "2020-06",
"business_context": "optional"
}
}
```
Выход:
```json
{
"trace_id": "string",
"ok": true,
"normalized": { ... },
"route_hint_summary": { ... },
"raw_model_output": "string or object",
"validation": {
"passed": true,
"errors": []
},
"usage": {
"input_tokens": 0,
"output_tokens": 0,
"total_tokens": 0
},
"latency_ms": 0,
"prompt_version": "normalizer_v1",
"schema_version": "v1"
}
```
### `POST /api/eval/run`
Запуск серии нормализаций на тестовом наборе.
### `GET /api/history`
История запросов.
### `GET /api/history/:trace_id`
Получение полного trace.
### `POST /api/presets/save`
Сохранение набора promptов / конфигурации.
### `GET /api/presets`
Получение доступных prompt presets.
---
## 9. Центральная сущность: Normalized Query JSON
Это ключевой контракт между LLM и вашим deterministic router.
## 9.1 Schema version
Первая версия:
* `schema_version = "normalized_query_v1"`
## 9.2 Обязательные поля
```json
{
"schema_version": "normalized_query_v1",
"user_question_raw": "string",
"normalized_question": "string",
"intent_class": "heavy_analytical | cross_entity | drilldown_explain | rule_based_account_control | anomaly_probe | period_close_risk | ambiguous_human_query | simple_factual",
"business_problem_type": "string",
"domain_entities": ["string"],
"accounts_mentioned": ["string"],
"documents_mentioned": ["string"],
"registers_mentioned": ["string"],
"period_scope": {
"type": "explicit | inferred | missing",
"value": "string | null",
"confidence": "high | medium | low"
},
"requires": {
"needs_cross_entity_join": true,
"needs_causal_chain": true,
"needs_exact_object_trace": false,
"needs_ranking": false,
"needs_anomaly_summary": false,
"needs_runtime_truth": false,
"needs_period_cut": true,
"needs_evidence": true
},
"expected_output_shape": "ranked_list | evidence_chain | anomaly_summary | point_answer | reconciliation_report | prioritized_review_list",
"route_hint": "store_canonical | store_feature_risk | hybrid_store_plus_live | live_mcp_drilldown | batch_refresh_then_store",
"ambiguities": [
{
"field": "period_scope",
"reason": "month not specified",
"severity": "medium"
}
],
"confidence": {
"overall": "high | medium | low",
"intent_class": "high | medium | low",
"route_hint": "high | medium | low"
}
}
```
---
## 10. Семанические требования к normalizer
LLM normalizer обязан:
### 10.1 Не отвечать на бухгалтерский вопрос по сути
Она не должна возвращать:
* “вероятно, проблема в 62 счёте”
* “скорее всего, не хватает оплаты”
Она должна возвращать **нормализованную интерпретацию**, а не бизнес-ответ.
### 10.2 Выделять причинно-следственную форму
Если вопрос содержит конструкции типа:
* “не бьётся”
* “не собралось в цепочку”
* “разложи по документам, оплатам, закрывающим”
* “чем подтверждается”
* “почему висит хвост”
* “не видно прихода под реализацию”
LLM должна поднимать:
* `needs_cross_entity_join = true`
* `needs_causal_chain = true`
* часто `needs_evidence = true`
### 10.3 Отдельно распознавать exact object trace
Если вопрос про:
* конкретный документ,
* конкретную проводку,
* конкретную строку,
* конкретный объект,
* конкретный номер / ref / line / posting
Тогда поднимать:
* `needs_exact_object_trace = true`
### 10.4 Различать множественный explain и точечный drilldown
Если вопрос:
* “по нескольким материалам покажи, почему зависли”
* “по каким кейсам это происходит”
* “по каким поставщикам не бьётся”
Это **не** exact drilldown.
Это множественный causal explain → чаще `hybrid_store_plus_live`.
### 10.5 Не путать risk-лексику с risk-route
Если в вопросе есть слова:
* “риск”
* “проблема”
* “аномалия”
* “опасный”
но одновременно есть document/payment/posting chain semantics,
то normalizer не должен автоматически относить это к `store_feature_risk`.
Он должен сохранять causal cross-entity приоритет.
---
## 11. Prompt system
Нужна управляемая prompt-архитектура.
## 11.1 Структура promptов
Разделить prompt минимум на 3 уровня:
### `system_prompt`
Общие жесткие правила:
* ты не отвечаешь на бухгалтерский вопрос;
* ты возвращаешь только JSON;
* ты обязан следовать schema;
* ты не выдумываешь период, если его нельзя разумно вывести;
* ты явно помечаешь ambiguity.
### `developer_prompt`
Правила нормализации:
* классификация типов вопросов;
* различение heavy/cross-entity/drilldown;
* правила выбора route_hint;
* правила confidence;
* правила causal semantics.
### `domain_prompt`
Предметная специфика:
* словарь бухгалтерских формулировок;
* названия счетов;
* доменные сущности;
* типовые паттерны “хвост”, “не бьётся”, “акт сверки”, “закрывающие”, “реализация без оплаты”, “продажа раньше прихода”, “ошибка даты”, “97 счёт”, “ОС”, “банковская выписка” и т.д.
## 11.2 Few-shot examples
Добавить отдельный блок примеров:
* raw question
* expected normalized JSON fragment
Минимум 1015 примеров на старт.
---
## 12. Route Hint Adapter
Нужен слой, который переводит normalized JSON в input существующего router.
## 12.1 Требования
На основе `normalized_query_v1` строить объект:
```json
{
"intent_class": "cross_entity",
"decision_flags": {
"needs_cross_entity_join": true,
"needs_causal_chain": true,
"needs_exact_object_trace": false,
"needs_ranking": false,
"needs_anomaly_summary": false,
"needs_runtime_truth": false
},
"route_hint": "hybrid_store_plus_live",
"confidence": "medium",
"entities": { ... },
"period_scope": { ... }
}
```
## 12.2 Поведение
Этот adapter пока только готовит payload и показывает его в UI.
Интеграция в боевой router может быть следующей фазой.
---
## 13. Логирование и traceability
Каждая нормализация должна логироваться.
## 13.1 Что сохранять
* `trace_id`
* timestamp
* model
* prompt_version
* schema_version
* raw user question
* context
* raw request payload
* raw model response
* parsed normalized JSON
* validation result
* route_hint
* confidence
* token usage
* latency
* optional expected route
* optional eval label
## 13.2 Формат хранения
Хранить локально в JSONL или SQLite.
Рекомендуемый вариант:
* `data/normalizer_traces/*.json`
или
* `data/normalizer.sqlite`
---
## 14. Eval mode
Нужен режим оценки качества normalizer.
## 14.1 Источник eval-набора
Собрать eval corpus из:
* каноничных benchmark-вопросов;
* creative stress вопросов;
* реальных человеческих фраз из диалогов.
## 14.2 Формат тест-кейса
```json
{
"case_id": "NQ-001",
"raw_question": "Где у нас не бьются взаиморасчёты по поставщикам...",
"expected": {
"intent_class": "cross_entity",
"route_hint": "hybrid_store_plus_live",
"requires": {
"needs_cross_entity_join": true,
"needs_causal_chain": true
},
"accounts_mentioned": ["60"],
"expected_output_shape": "reconciliation_report"
}
}
```
## 14.3 Метрики eval
Считать:
* `intent_class_accuracy`
* `route_hint_accuracy`
* `causal_flag_accuracy`
* `period_scope_accuracy`
* `entity_extraction_accuracy`
* `schema_validation_pass_rate`
* `high_confidence_error_rate`
---
## 15. Валидация structured output
## 15.1 Обязательное правило
Если модель вернула невалидный JSON:
* backend не должен silently repair;
* backend должен помечать `validation.passed = false`;
* UI должен показывать ошибку;
* запись должна логироваться.
## 15.2 Допустимый soft-recovery
Разрешён только один controlled retry:
* при invalid JSON
* с дополнительной server-side инструкцией “return valid JSON strictly matching schema”
Но не больше 1 повтора на запрос.
---
## 16. Security requirements
### 16.1 Обязательные требования
* ключ не хранить в localStorage в открытом виде;
* ключ не отправлять в сторонние домены;
* ключ не логировать в trace;
* ключ не возвращать во frontend response;
* backend должен редактировать чувствительные поля из логов.
### 16.2 Допустимый режим для localhost
Разрешается:
* временно держать ключ в памяти backend-процесса;
* или вводить ключ вручную на сессию без персистентного хранения.
---
## 17. Нефункциональные требования
* UI должен быть пригоден для localhost-тестов без production hardening.
* Нормализация одного запроса должна быть достаточно быстрой для интерактивной работы.
* Код должен быть модульным, чтобы потом normalizer можно было вынести из playground в основной backend.
* Все schema и prompts должны быть версионируемыми.
* Архитектура должна позволять заменить модель без переписывания всего пайплайна.
---
## 18. Предлагаемая структура проекта
```text
llm_normalizer/
frontend/
src/
components/
pages/
api/
state/
package.json
backend/
src/
server.ts
routes/
normalize.ts
presets.ts
history.ts
eval.ts
testConnection.ts
services/
openaiResponsesClient.ts
normalizerService.ts
promptBuilder.ts
schemaValidator.ts
routeHintAdapter.ts
traceLogger.ts
schemas/
normalized_query_v1.json
prompts/
system/
developer/
domain/
fewshot/
storage/
types/
package.json
data/
presets/
traces/
eval_cases/
docs/
README.md
API.md
SCHEMA.md
PROMPTS.md
```
---
## 19. Acceptance criteria
Этап считается принятым, если выполнены все условия:
### Функционально
* localhost GUI запускается;
* можно ввести ключ, модель и вопрос;
* backend успешно вызывает Responses API; ([OpenAI Платформа][1])
* модель возвращает structured JSON по schema; ([OpenAI Платформа][1])
* JSON проходит валидацию минимум в 90% тестовых кейсов;
* сохраняются traces;
* route hint summary отображается в UI;
* есть история запросов;
* есть eval-mode на локальном наборе кейсов.
### Архитектурно
* frontend не ходит напрямую в OpenAI API с ключом; ([OpenAI Платформа][2])
* prompts версионируются;
* schema версионируется;
* normalizer отделён от router;
* код модульный и пригоден к дальнейшей интеграции.
### Качественно
На стартовом eval-наборе целевые ориентиры:
* `schema_validation_pass_rate >= 90%`
* `intent_class_accuracy >= 80%`
* `route_hint_accuracy >= 75%`
* `causal_flag_accuracy >= 80%`
* `high_confidence_error_rate <= 10%`
---
## 20. Порядок реализации
### Этап 1
Поднять backend-proxy и test connection.
### Этап 2
Сделать GUI с input/output блоками.
### Этап 3
Сделать prompt manager и schema loader.
### Этап 4
Сделать `POST /api/normalize` и structured JSON parsing.
### Этап 5
Сделать route hint adapter и validation panel.
### Этап 6
Сделать history + trace logging.
### Этап 7
Сделать eval mode и загрузку тестовых кейсов.
### Этап 8
Подготовить README и пример prompt presets.
---
## 21. Что Codex должен сгенерировать
Codex должен выдать:
1. Полностью рабочий localhost-проект.
2. Frontend + backend.
3. JSON schema `normalized_query_v1`.
4. Стартовые prompt-файлы:
* system
* developer
* domain
* fewshot
5. Примеры test cases.
6. README с инструкцией запуска.
7. Пример `.env.example`.
8. Пример trace storage.
9. Минимальные тесты:
* schema validation
* route hint adapter
* normalize endpoint
* prompt builder
---
## 22. Ключевое архитектурное правило
Самое важное правило этого этапа:
**LLM — это semantic front-end, а не исполнитель бизнес-логики.**
LLM должна:
* нормализовать,
* структурировать,
* классифицировать,
* подсказывать route hint.
LLM не должна:
* сама принимать окончательное orchestration-решение;
* сама лезть в данные;
* сама становиться вашим retrieval engine.
---
Если хочешь, следующим сообщением я дам **сразу вторую часть ТЗ**: уже не про playground, а про **встраивание normalizerа в существующий router/orchestration pipeline**.
[1]: https://platform.openai.com/docs/api-reference/responses/list?ref=test-ippon.ghost.io&utm_source=chatgpt.com "Responses | OpenAI API Reference"
[2]: https://platform.openai.com/docs/api-reference/chat/create%23chat-create-temperature?utm_source=chatgpt.com "Chat Completions | OpenAI API Reference"

BIN
IN/Альтернатива+ БП.dt (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -0,0 +1,245 @@
Быстрая MVPинтеграция с 1С и «умная» автоматизация бухгалтерии: аудит готовых OSSмодулей, онтологий и коммерческих AIпаттернов
Контекст и «физика» задачи: что реально нужно автоматизировать, чтобы снять нагрузку с операционки
В бухгалтерии с высокой операционкой боль редко в том, что «никто не знает ключевых клиентов». Чаще боль — в бесконечных повторяющихся циклах: ввод/проверка первички, разнесение и отражение в учёте, сверки, закрытие периода, контроль ошибок/аномалий, подготовка пояснений и подбор «первоисточников» под конкретные проводки (и наоборот). Коммерческий рынок автоматизации это подтверждает: самые «денежные» категории — RecordtoReport (закрытие/сверки/журнальные операции) и ProcuretoPay (инвойсы/АП/сопоставление/контроль расходов), плюс анализ рисков по главной книге (антифрод/аномалии/контроль качества учёта).
Для вашей ситуации (15 бухгалтеров, ежедневные изменения, ручные выгрузки «убивают») ключевой поворот — перестать думать “выгрузками” и перейти к модели:
постоянный доступ к данным (OData/HTTPсервисы/прокси‑шлюз), чтобы ассистент/система могли задавать запросы самостоятельно;
каноническая модель данных (проводка/документ/субконто/контрагент/банк/налоги), чтобы поверх 1С строить аналитику, контроль и подсказки;
типовые «линии ценности» (value lines): авто‑сверка/контроль, авто‑выявление отклонений, полу‑автоматизация закрытия, поиск первички под проводку, и наоборот.
Ниже — что уже существует «из коробки» и в OSS, чтобы не тонуть в разработке с нуля.
Готовые способы получать данные из 1С без ручных выгрузок
Стандартный путь: OData (RESTдоступ к объектам 1С)
У 1С:Предприятие есть стандартный RESTинтерфейс на базе OData («Стандартный интерфейс OData»), предназначенный для доступа к данным из внешних приложений (по сути — отдача справочников/документов/регистров как сущностей с фильтрами/выборками).
OData — открытый стандарт (OASIS) для построения/потребления REST API с поддержкой типового queryязыка (filter/select/orderby/top/skip и т. п.).
Практическое следствие для MVP: если вы публикуете нужные сущности в OData, то дальше любой сервис (Node/Python/Go) может автоматически забирать то, что раньше вы выгружали руками.
Альтернативный путь: HTTPсервисы внутри 1С
У платформы есть механизм HTTPсервисов (серверные обработчики HTTP запросов), что позволяет сделать «ваш» API поверх конкретной базы/конфигурации. Это сильнее, чем OData, когда нужно вернуть «готовые» агрегаты (например, «ОСВ по счету 51 за период» как один JSON), а не сырые таблицы.
В MVPлогике обычно выбирают так:
OData — быстрее включить, меньше кастомной логики.
HTTPсервис — больше свободы и производительности для прикладных ответов «как бухгалтер привык».
Большая практическая проблема: «без 1Сника» и без ломки конфигурации
Вы прямо описали реальность: 1Сника нет, а базу «трогать страшно». Поэтому в MVP важно иметь варианты, которые требуют минимум внедрения на стороне 1С.
Ниже — OSSинструменты, которые это упрощают.
Что уже есть опенсорсного на GitHub для интеграции с 1С и вокруг OData
Внешние клиенты/обёртки над 1С OData (быстрые коннекторы)
Pythonобёртка над 1С OData:
belov38/1c-odata — «1C v8 OData wrapper», публикуется как пакет odata1cw, показывает примеры query/select/filter/top/skip, а также create/edit. Лицензия Unlicense.
PHPклиенты для 1С OData:
kilylabs/odata-1c, mihpa/odata-1c-php — клиенты для обращения к 1С через OData (полезно как референс реализации протокола/нюансов).
Зачем это вам, если вы на Node?
Потому что для MVP можно быстро поднять микросервис на Python (12 дня), который:
читает 1С по OData (готовая обёртка),
нормализует данные в вашу каноническую схему,
кладёт в Postgres,
и отдаёт уже «чистые» API/вьюхи наружу.
Вы прямо сказали, что не обязательно «всё размазывать в нодовой истории» — этот вариант как раз про это.
Шлюзы/посредники: OData → GraphQL/gRPC, генерация типизированного API
Есть проекты, которые решают главную боль OData в бою: кириллица/названия полей/типизация/удобство запросов.
SysUtils/1c-gateway — Goпроект: может работать как библиотека для 1С OData, как gRPC middleware и как GraphQL middleware; генерирует методы и типы по метаданным, использует словари для транслитерации (types.json/fields.json). Лицензия Apache2.0.
Это полезно, если вы хотите дать своим внутренним сервисам нормальный API, а не «голый» OData, и снизить порог для продуктовых команд.
Внутри1С инструменты для интеграций (когда нужен «агент» на стороне 1С)
Если окажется, что OData недостаточно (права, скорость, специфика конфигурации), то в MVP можно пойти «от 1С наружу» — поставить в базу инструмент, который умеет:
выполнять задания,
выгружать в JSON/CSV/XML,
по расписанию пушить наружу.
Тут есть два очень практичных OSSкандидата:
FoxyLink (FoxyLinkIO/FoxyLink)
Подсистема для разработки интеграций и выполнения задач на кластере 1С: в README прямо описаны сценарии «вывод отчётов в JSON/CSV/XML», «интеграция с BI», «fireandforget jobs», «экспорт данных с произвольной иерархией», «plugins support». Лицензия AGPL3.0.
Universal Tools для 1С (Universal-Tools-1C-Word-Edition/tools_ui_1c_we)
Набор универсальных обработок/отчётов и админ‑инструментов; внутри списка возможностей есть важные штуки:
просмотр структуры хранилища/таблиц и связей с метаданными,
Query Console (выполнение запросов),
HTTP Request Console и Web Services Console,
«Universal data exchange in XML format … with direct upload via HTTP service»,
и даже включён Connector как «Requestsподобный HTTPклиент для мира 1С». Лицензия GPL3.0.
Отдельно сам Connector как библиотека: vbondarevsky/Connector — удобный HTTPклиент для 1С (JSON, формы, файлы, auth, retries и т. п.), Apache2.0.
Это практически готовый «сетевой слой» для агента в 1С, если вы решите пушить данные наружу.
Реальные «прикладные» примеры интеграций на стороне 1С
Полезны как референсы того, как компании реально решают интеграцию расширениями:
dodobrands/1c-accounting-export — репозиторий с обработкой/расширениями (.epf, .cfe) и документацией; по README видно, что проект — про развитие интеграционного расширения и добавление функциональности («реализация товаров и услуг», авторизация, уведомление пользователей). Лицензия Apache2.0.
Каталоги опенсорса по 1С, чтобы не искать «вслепую»
artbear/awesome-1c и его «data/README» — большой индекс репозиториев (скрипты OneScript, devopsутилиты, интеграционные инструменты и т. п.).
Полезно для быстрого скрининга: вы находите «класс задач» (интеграции/выгрузки/ETL/администрирование) и берёте ближайшие по зрелости проекты.
Онтологии и «готовая разметка»: что реально существует (и чего почти нет) для бухгалтерии и как это использовать в MVP
Реальность рынка: готовой «1Сонтологии бухгалтерии» в OSS почти нет
В opensource мире много финансовых онтологий, но они либо про финансовую индустрию, либо про регуляторику, либо про исторические бухгалтерские документы. Практичной «готовой онтологии 1Спроводок/документов для РСБУ» в формате «подключил и поехал» — почти не наблюдается (в отличие от интеграционных библиотек).
Поэтому MVP обычно строят на принципе:
каноническая модель проводок и документов (минимальная, но очень полезная),
словарь/маппинг к стандартам (если нужно дальше расширять).
Что можно взять как «базовый слой смысла» без фантазий
XBRL GL (Global Ledger)
Это стандартный подход к представлению данных главной книги/транзакционных систем как мост к отчётности; описание архитектуры GLтаксономии опубликовано XBRL.
Для вас это не про «внедрить XBRL», а про готовую семантическую рамку:
сущности типа «account», «entry», «transaction», «source document», «dimensions»,
логика doubleentry,
переносимость между системами.
FIBO (Financial Industry Business Ontology)
FIBO — открытая онтология фин. домена; у неё есть модуль Accounting (общие бухгалтерские концепции, валюты и т. п.).
В MVP FIBO полезна, если вы хотите потом выйти в knowledgegraph/семантику «выше уровня проводки» (например, фин. инструменты/контракты). Но для «операционки бухгалтерии» FIBO обычно слишком широкая.
Bookkeeping ontology (RDF) для описания бухгалтерских записей
GVogeler/bookkeeping — онтология для RDFописания бухгалтерских документов/записей (исторический учёт), основана на модели REA (ResourceEventActor), лицензия Apache2.0.
Её сила — чистая модель «событие‑ресурс‑актёр», но для 1Спрактики вам всё равно придётся делать адаптер.
Что это даёт практично: «онтология» как каноническая схема MVP
Вместо попытки найти «готовую онтологию 1С», быстрый MVP строится так:
Fact: Posting (проводка)
ключ: дата/период, Дт‑счёт, Кт‑счёт, сумма, валюта, документ‑основание, организация, подразделение, комментарий/содержание.
Это прямо то, что у вас уже есть в журнале проводок.
Fact: Document (документ)
тип, номер, дата, контрагент, договор, склад/номенклатура (если есть), статус проведения, автор.
Dimension: Accounts / Subconto / Counterparty / Contract / Item / Bank account / Cost item
и связи, которые нужны для сверок и контроля.
Эта «онтология» минимальна, но под неё идеально ложатся задачи контроля закрытия/сверок/аномалий.
“AI для бухгалтерии”: что уже продают и за что платят, без фантазий
Ниже — ключевые классы решений, которые рынок реально покупает, с привязкой к конкретным продуктам (чтобы вы могли «подсмотреть логику»).
RecordtoReport: закрытие периода, сверки, журнальные операции
BlackLine — автоматизация сверок и процессов закрытия: шаблоны, workflow, дашборды; позиционирование как automated account reconciliations для ускорения close.
FloQast — управление close + автоматизация части сверок и контроль отклонений; продуктовые страницы описывают оркестрацию задач close и «AIpowered close management».
Trintech Cadency — платформа R2R: close management, account reconciliation, journal entry management/automation, compliance, в т. ч. через API.
Паттерн “что делает AI/автоматизация”:
авто‑сопоставление транзакций/сверка (matching),
контроль отклонений (variance/flux analysis),
оркестрация задач закрытия и «узкие места»,
контроль и документация журнальных операций.
ProcuretoPay (AP): автономная обработка инвойсов и сопоставления
Vic.ai — «AIfirst AP automation», заявляет высокую долю notouch invoice processing и автоматизацию обработки и кодирования инвойсов.
Tipalti — AP automation, в т. ч. «AI Smart Scan» для извлечения данных с инвойсов/строк и включение этого в workflow approvals/matching.
AppZen — «Autonomous AP» и автоматизация инвойсов/PO matching/аудит, плюс отдельный фокус на expense audit (см. ниже).
Паттерн “за что платят”:
распознавание и структурирование первички,
предиктивное «проводочное» кодирование (GL coding),
2/3/4way matching (инвойсPOпоставкаконтракт),
анти‑дубли/анти‑ошибки до оплаты,
сокращение ручных касаний и ускорение обработки.
Expense audit / контроль расходов
AppZen Expense Audit — автоматизированная проверка расходов (дубли, политика, рисковые транзакции), плюс комплаенс‑проверки.
Аналитика главной книги: аномалии, риск‑скоринг, антифрод
MindBridge — позиционируется как general ledger analysis / anomaly detection / risk scoring для аудита и финансовых команд.
“Assistant / agents inside ERP”: то, куда рынок идёт в 20252026
Крупные ERPэкосистемы двигаются в сторону роль‑ориентированных AIагентов, которые работают поверх данных ERP и офисных инструментов:
Microsoft 365 Copilot for Finance (rolebased agent): фокус на работе с финданными в Excel и связке с ERP, ускорение задач и «cut down time on manual repetitive tasks».
Copilot в Dynamics 365 Business Central — встроенный помощник в ERP.
SAP Joule — AI assistant/agents, позиционируется как помощник с агентами по ключевым функциям, встроенный в SAPландшафт.
Intuit Assist / Intuit AI — генеративный ассистент и «AI agents» для задач финансов/бухучёта в экосистеме QuickBooks.
Это важно для вас не как «конкуренты», а как шаблон продукта: люди платят, когда ассистент не просто «болтает», а:
подключён к данным,
умеет исполнять действия (сверить, найти, сформировать, объяснить),
оставляет след и контроль (audit trail),
снижает ручную работу в конкретных операциях.
Российский контур: распознавание первички и ЭДО как “вход” в автоматизацию
1С:Распознавание первичных документов (1С:РПД) — сервис, который «превращает бумажные документы в документы базы 1С», распознаёт и сопоставляет поставщиков/покупателей/номенклатуру с объектами базы.
Saby (СБИС) — продвигает распознавание первички и помощника для обработки документов/данных.
Контур.Диадок — ЭДО с интеграцией «из 1С … и других систем» и открытой APIдокументацией.
ELMA365 + ENTERA — пример «распознавание первичных документов» как отдельное решение в ECM/BPMконтуре.
Практический вывод: в РФ часто начинают автоматизацию с входящего потока документов (распознавание/ЭДО), а «умный» контроль и закрытие подключают сверху, когда данные уже цифровые.
Быстрый путь к MVP без переписывания всего: OSSархитектуры, которые можно собрать из готовых блоков
Базовый MVPконтур “OData → нормализация → БД → ответы/ассистент”
Включаете доступ к данным через стандартный OData интерфейс 1С.
Снаружи поднимаете сервис‑коннектор:
либо на Python на базе odata1cw (быстрее старт),
либо на Node с ODataклиентом (например, lightodata),
либо Goшлюзом (если сразу хотите GraphQL/gRPC фасад).
Коннектор складывает данные в вашу БД и делает каноническое представление «проводка/документ/измерения».
Ваши автоматизации (n8n/бек/агенты) работают уже не с 1С напрямую, а с вашей БД/вьюхами.
Почему это MVPдружелюбно: минимальная логика на стороне 1С, максимум — снаружи.
Если нужен «AIслой» поверх живых данных: MCPмосты для OData как готовый ускоритель
Появился целый класс opensource инструментов: OData ↔ MCP bridge, чтобы AIклиент мог разговаривать с системой как с набором “tools” (а не как с выгрузками).
oisee/odata_mcp_go — Goмост OData→MCP: автоматически генерирует инструменты по OData metadata, поддерживает OData v2/v4, разные транспорты, режим readonly, и даже “universal tool mode” для больших сервисов (чтобы не взрывать контекст тысячами tools). MIT.
lemaiwo/odata-mcp-proxy — configdriven MCPсервер на Node/TypeScript, который экспонирует OData/REST как MCPtools; описывает dual transport (stdio/HTTP), конфигурацию entity sets/operations, auto token management (в SAPконтексте через destinations). MIT.
Каталог “SAP MCP Servers and Skills” показывает, что вокруг MCP уже формируется экосистема и есть списки/репозитории, которые можно брать как референс‑паттерны.
Как это переносится на 1С: если ваша 1С отдаёт OData, то любой OData→MCP bridge становится «универсальным адаптером», позволяя ассистенту:
выполнять запросы,
фильтровать,
вытаскивать связанные сущности,
и (при необходимости) писать обратно — строго в рамках доступных operations.
Это не отменяет вашу БД/канонический слой, но может резко ускорить прототип «ассистента для бухгалтерии», потому что «инструменты» появляются автоматически из метаданных, без ручной разработки API.
Когда OData недоступен или неудобен: “агент внутри 1С
Если по факту OData не получается (права/конфигурация/ограничения), то MVP можно собрать на стороне 1С из готовых подсистем:
FoxyLink как подсистема интеграционных задач и выгрузок.
Universal Tools как пакет админ‑инструментов, включая обмен с фильтрами и HTTPзагрузку/выгрузку.
Connector как сетевой слой пуш‑интеграции из 1С.
Это потребует аккуратного внедрения (обработка/расширение), но зато минимизирует требования «настроить веб‑публикацию OData».
Практическая «карта ценности» для вашего продукта: какие сценарии автоматизации дадут операционный профит
Чтобы команда бухгалтерии реально «почувствовала» выгоду, MVP лучше строить вокруг 35 сценариев, которые повторяются ежедневно/ежемесячно и имеют понятный KPI.
Контур сверок и контроля качества учёта
Прямой аналог того, за что платят в closeплатформах: автоматизация сверок, объяснение расхождений, сбор доказательств, контроль статуса.
На ваших данных (журнал проводок + ОСВ) это превращается в:
«сверь 51 с выпиской/банком» (если есть импорт банка),
«проверь закрытие 62/60 по ключевым контрагентам»,
«найди нетипичные корреспонденции»,
«подними проводки без первички/без основания».
Выявление аномалий и нетипичных паттернов
Коммерческий референс — подходы MindBridge (риск‑скоринг/аномалии по ledger).
В MVP это можно сделать проще: правила + статистика:
всплески ручных проводок,
«конец месяца» концентрация,
необычные корреспонденции,
повторяющиеся корректировки,
дробление сумм/дубли.
Автопомощник “найди первичку/объясни проводку”
Это боль, которую бухгалтерия редко формулирует как запрос, но она съедает время:
«почему эта сумма на счёте», «какие документы дали этот остаток», «покажи проводки, которые сформировали отклонение».
Если у вас есть постоянный доступ к данным 1С (а не выгрузки), это становится queryзадачей, а не ручным расследованием.
Входящий поток документов: распознавание / сопоставление / авто‑проведение
Это самый «обкатанный» класс решений в РФ (1С:РПД, СБИС), и его легко использовать как участок пилота — потому что эффективность измерима количеством документов и временем ввода.
Но если ваша цель именно «снять операционку» без покупки платных SaaS, то для MVP можно начать не с OCR, а с контроля и поиска по уже введённым данным, а OCR оставить как следующий шаг.
Связка с ЭДО как «дешёвая победа»
ЭДО‑системы типа Диадока раскрываются через API и уже являются источником «цифровой первички».
Если бухгалтерия активно использует ЭДО, то можно строить сверку «документы в ЭДО ↔ отражение в 1С», что даёт мгновенную пользу (пропуски, дубляжи, несостыковки).

196
README.md Normal file
View File

@ -0,0 +1,196 @@
# 1C OData MVP Bridge
Read-only MVP bridge between 1C and AI assistant workflows:
1. OData probe (`odata_probe/`)
2. Canonical model and mappers (`canonical_layer/`)
3. FastAPI layer (`canonical_layer/app.py`)
4. Draft MCP configuration (`mcp/`)
## Quick start
```powershell
cd X:\1C\NDC_1C
$Conda = Join-Path $env:USERPROFILE "miniconda3\Scripts\conda.exe"
if (-not (Test-Path $Conda)) { $Conda = Join-Path $env:USERPROFILE "Miniconda3\Scripts\conda.exe" }
& $Conda create -y -n ndc_1c_mvp python=3.11
$EnvPython = Join-Path $env:USERPROFILE "miniconda3\envs\ndc_1c_mvp\python.exe"
if (-not (Test-Path $EnvPython)) { $EnvPython = Join-Path $env:USERPROFILE "Miniconda3\envs\ndc_1c_mvp\python.exe" }
& $EnvPython -m pip install -r requirements.txt
copy .env.example .env
```
Run probe:
```powershell
& $EnvPython -m odata_probe.fetch_metadata
& $EnvPython -m odata_probe.list_entity_sets
& $EnvPython -m odata_probe.probe_entities
& $EnvPython -m odata_probe.dump_sample_links
& $EnvPython scripts/deep_probe_subconto_join.py
& $EnvPython scripts/deep_probe_subconto.py
& $EnvPython scripts/recon_slot3_gap.py
& $EnvPython scripts/deep_probe_accounting_mvp_gate.py
& $EnvPython scripts/check_deeper_access_readiness.py
```
`deep_probe_subconto_join.py` is the regression check for the critical join:
`Document lines (Ref_Key + LineNumber) <-> Accounting register (Recorder + LineNumber)`.
`deep_probe_accounting_mvp_gate.py` runs 3 hard MVP checks and writes a pass/fail verdict.
`recon_slot3_gap.py` focuses on slot `3` evidence quality and writes `logs/slot3_recon_report.json`.
`check_deeper_access_readiness.py` writes `logs/deeper_access_readiness.json` with one snapshot of gate/slot3/tool-artifact readiness.
`scripts/run_probe.ps1` fails intentionally when the MVP gate verdict is not sufficient.
Run FoxyLink endpoint smoke probe:
```powershell
& $EnvPython scripts/foxylink_probe_endpoint.py
& $EnvPython scripts/foxylink_probe_endpoint.py --strict
```
or:
```powershell
powershell -ExecutionPolicy Bypass -File .\scripts\run_foxylink_probe.ps1
```
`foxylink_probe_endpoint.py` writes `logs/foxylink_probe_report.json` with URL,
status, classification (`reachable`, `auth_failed`, `endpoint_not_found_or_not_published`, etc.),
and response preview.
One-command run:
```powershell
powershell -ExecutionPolicy Bypass -File .\scripts\run_probe.ps1
```
Run API:
```powershell
& $EnvPython -m uvicorn canonical_layer.app:app --reload --host 127.0.0.1 --port 8000
```
Run canonical refresh (Layer 3 + Layer 4 MVP):
```powershell
& $EnvPython scripts/run_refresh.py --mode historical --limit-per-set 200
& $EnvPython scripts/run_refresh.py --mode incremental --from-date 2026-01-01T00:00:00 --limit-per-set 200
& $EnvPython scripts/run_refresh.py --mode targeted --target-id 68.02 --limit-per-set 200
```
or one-command PowerShell wrapper:
```powershell
powershell -ExecutionPolicy Bypass -File .\scripts\run_refresh.ps1 -Mode incremental -FromDate 2026-01-01T00:00:00
```
Refresh summary is written to `logs/refresh_last_run.json`.
Canonical store defaults to local sqlite:
`CANONICAL_DB_URL=sqlite:///X:/1C/NDC_1C/data/canonical_store.db`
and can be switched to PostgreSQL with a standard SQLAlchemy URL.
Run feature/anomaly engine (Layer 5 MVP):
```powershell
& $EnvPython scripts/run_features.py --top-account-tokens 20
& $EnvPython scripts/run_features.py --strict
```
PowerShell wrapper:
```powershell
powershell -ExecutionPolicy Bypass -File .\scripts\run_features.ps1 -Strict
```
Feature summary is written to `logs/features_last_run.json`.
API endpoints for the new layer:
- `POST /features/run`
- `GET /features/stats`
- `GET /features/runs`
- `GET /features/metrics`
- `GET /features/anomalies`
Run risk engine (Layer 6 MVP):
```powershell
& $EnvPython scripts/run_risk.py --strict
```
PowerShell wrapper:
```powershell
powershell -ExecutionPolicy Bypass -File .\scripts\run_risk.ps1 -Strict
```
Risk summary is written to `logs/risk_last_run.json`.
API endpoints:
- `POST /risk/run`
- `GET /risk/stats`
- `GET /risk/runs`
- `GET /risk/patterns`
Find busiest pre-reporting period and export dense snapshot (for example for 2020):
```powershell
& $EnvPython scripts/run_pre_report_snapshot.py --year 2020 --strict
```
PowerShell wrapper:
```powershell
powershell -ExecutionPolicy Bypass -File .\scripts\run_pre_report_snapshot.ps1 -Year 2020 -Strict
```
Default scan range:
- start: `2020-01-01T00:00:00Z`
- end (reporting deadline): `2021-03-31T23:59:59Z`
Outputs:
- activity profile: `logs/pre_report_activity_2020.json`
- selected-period snapshot: `logs/pre_report_snapshot_2020_<window>.json`
Run semantic mapper v2 remap on an existing snapshot and compare before/after:
```powershell
& $EnvPython scripts/remap_snapshot_semantic_v2.py
```
Outputs:
- remapped snapshot: `logs/pre_report_snapshot_2020_2020-06_semantic_v2.json`
- metrics delta: `logs/pre_report_snapshot_2020_2020-06_semantic_v2_metrics.json`
Run validation on remapped snapshot:
```powershell
& $EnvPython scripts/run_validation_accounting_analytics.py --snapshot-path logs/pre_report_snapshot_2020_2020-06_semantic_v2.json --output-dir docs/ARCH/validation_run_2026-03-23_semantic_v2 --strict
```
Refresh 2020 export package (ontology, rules, focused samples):
```powershell
& $EnvPython scripts/export_arch_2020_package.py
```
Output directory:
- `docs/ARCH/2020экспорт`
Run router/orchestration fix benchmark profile:
```powershell
& $EnvPython scripts/run_validation_accounting_analytics.py --snapshot-path logs/pre_report_snapshot_2020_2020-06_semantic_v2.json --output-dir docs/ARCH/validation_run_2026-03-23_router_fix --strict
```
Router fix report:
- `docs/ARCH/router_orchestration_fix_report_2026-03-23.md`
## Important
This project is designed for read-only 1C access.
Write operations in OData must remain disabled by 1C roles and by integration policy.

View File

@ -0,0 +1,2 @@
"""Canonical layer package for normalized accounting entities."""

261
canonical_layer/app.py Normal file
View File

@ -0,0 +1,261 @@
from __future__ import annotations
from datetime import datetime, timezone
from typing import Any
from fastapi import FastAPI, HTTPException, Query
from pydantic import BaseModel, Field
from canonical_layer.features import FeatureService
from canonical_layer.refresh import REFRESH_MODES, RefreshService
from canonical_layer.risk import RiskService
from canonical_layer.service import CanonicalService
app = FastAPI(
title="1C Canonical Layer MVP",
version="0.1.0",
description="Read-only API over 1C OData probe and canonical mapping layer.",
)
service = CanonicalService.build()
refresh_service = RefreshService.build()
feature_service = FeatureService.build()
risk_service = RiskService.build()
class RefreshRunRequest(BaseModel):
mode: str = Field(default="incremental")
date_from: str | None = None
date_to: str | None = None
target_id: str | None = None
limit_per_set: int | None = Field(default=None, ge=1, le=5000)
entity_sets: list[str] | None = None
keywords: list[str] | None = None
class FeatureRunRequest(BaseModel):
baseline_window_hours: int | None = Field(default=None, ge=1, le=720)
stale_refresh_threshold_hours: int | None = Field(default=None, ge=1, le=720)
top_account_tokens: int = Field(default=20, ge=1, le=200)
entity_limit: int | None = Field(default=None, ge=1, le=200000)
class RiskRunRequest(BaseModel):
source_feature_run_id: str | None = None
anomaly_limit: int | None = Field(default=None, ge=1, le=20000)
@app.get("/health")
def health() -> dict[str, str]:
return {
"status": "ok",
"timestamp": datetime.now(timezone.utc).isoformat(),
}
@app.get("/metadata/entity-sets")
def metadata_entity_sets() -> dict[str, Any]:
return service.list_entity_sets()
@app.get("/documents")
def list_documents(
from_date: str | None = Query(default=None, alias="from"),
to_date: str | None = Query(default=None, alias="to"),
limit: int = Query(default=100, ge=1, le=500),
) -> dict[str, Any]:
items = service.get_documents(date_from=from_date, date_to=to_date, limit=limit)
return {
"total": len(items),
"items": [item.model_dump() for item in items],
}
@app.get("/documents/{document_id}")
def get_document(document_id: str) -> dict[str, Any]:
document = service.get_document(document_id)
if document is None:
raise HTTPException(status_code=404, detail="Document not found in current probe window")
return document.model_dump()
@app.get("/postings")
def list_postings(
account: str | None = Query(default=None),
from_date: str | None = Query(default=None, alias="from"),
to_date: str | None = Query(default=None, alias="to"),
limit: int = Query(default=100, ge=1, le=500),
) -> dict[str, Any]:
items = service.get_postings(account=account, date_from=from_date, date_to=to_date, limit=limit)
return {
"total": len(items),
"items": [item.model_dump() for item in items],
}
@app.get("/counterparties/{counterparty_id}/documents")
def counterparty_documents(
counterparty_id: str,
limit: int = Query(default=100, ge=1, le=500),
) -> dict[str, Any]:
items = service.get_counterparty_documents(counterparty_id=counterparty_id, limit=limit)
return {
"counterparty_id": counterparty_id,
"total": len(items),
"items": [item.model_dump() for item in items],
}
@app.get("/graph/document/{document_id}")
def document_graph(document_id: str) -> dict[str, Any]:
return service.build_document_graph(document_id)
@app.get("/store/stats")
def store_stats() -> dict[str, Any]:
return refresh_service.store_stats()
@app.get("/refresh/runs")
def refresh_runs(limit: int = Query(default=20, ge=1, le=200)) -> dict[str, Any]:
runs = refresh_service.list_recent_runs(limit=limit)
return {
"total": len(runs),
"items": runs,
}
@app.post("/refresh/run")
def run_refresh(request: RefreshRunRequest) -> dict[str, Any]:
mode = request.mode.strip().lower()
if mode not in REFRESH_MODES:
raise HTTPException(status_code=400, detail=f"Unsupported mode '{request.mode}'")
result = refresh_service.run_refresh(
mode=mode,
date_from=request.date_from,
date_to=request.date_to,
target_id=request.target_id,
limit_per_set=request.limit_per_set,
requested_entity_sets=request.entity_sets,
entity_keywords=request.keywords,
)
payload = result.to_dict()
payload["store_stats"] = refresh_service.store_stats()
return payload
@app.get("/features/stats")
def feature_stats() -> dict[str, Any]:
return feature_service.stats()
@app.get("/features/runs")
def feature_runs(limit: int = Query(default=20, ge=1, le=200)) -> dict[str, Any]:
runs = feature_service.list_recent_runs(limit=limit)
return {
"total": len(runs),
"items": runs,
}
@app.get("/features/metrics")
def feature_metrics(
limit: int = Query(default=200, ge=1, le=2000),
metric_key: str | None = Query(default=None),
scope: str | None = Query(default=None),
run_id: str | None = Query(default=None),
) -> dict[str, Any]:
items = feature_service.list_metrics(
limit=limit,
metric_key=metric_key,
scope=scope,
run_id=run_id,
)
return {
"total": len(items),
"items": items,
}
@app.get("/features/anomalies")
def feature_anomalies(
limit: int = Query(default=200, ge=1, le=2000),
severity: str | None = Query(default=None),
active_only: bool = Query(default=True),
run_id: str | None = Query(default=None),
) -> dict[str, Any]:
items = feature_service.list_anomalies(
limit=limit,
severity=severity,
active_only=active_only,
run_id=run_id,
)
return {
"total": len(items),
"items": items,
}
@app.post("/features/run")
def run_features(request: FeatureRunRequest) -> dict[str, Any]:
result = feature_service.run_feature_engine(
baseline_window_hours=request.baseline_window_hours,
stale_refresh_threshold_hours=request.stale_refresh_threshold_hours,
top_account_tokens=request.top_account_tokens,
entity_limit=request.entity_limit,
)
payload = result.to_dict()
payload["feature_store_stats"] = feature_service.stats()
payload["store_stats"] = refresh_service.store_stats()
return payload
@app.get("/risk/stats")
def risk_stats() -> dict[str, Any]:
return risk_service.stats()
@app.get("/risk/runs")
def risk_runs(limit: int = Query(default=20, ge=1, le=200)) -> dict[str, Any]:
runs = risk_service.list_recent_runs(limit=limit)
return {
"total": len(runs),
"items": runs,
}
@app.get("/risk/patterns")
def risk_patterns(
limit: int = Query(default=200, ge=1, le=2000),
severity: str | None = Query(default=None),
active_only: bool = Query(default=True),
run_id: str | None = Query(default=None),
pattern_key: str | None = Query(default=None),
scope: str | None = Query(default=None),
) -> dict[str, Any]:
items = risk_service.list_patterns(
limit=limit,
severity=severity,
active_only=active_only,
run_id=run_id,
pattern_key=pattern_key,
scope=scope,
)
return {
"total": len(items),
"items": items,
}
@app.post("/risk/run")
def run_risk(request: RiskRunRequest) -> dict[str, Any]:
result = risk_service.run_risk_engine(
source_feature_run_id=request.source_feature_run_id,
anomaly_limit=request.anomaly_limit,
)
payload = result.to_dict()
payload["risk_store_stats"] = risk_service.stats()
payload["feature_store_stats"] = feature_service.stats()
payload["store_stats"] = refresh_service.store_stats()
return payload

454
canonical_layer/features.py Normal file
View File

@ -0,0 +1,454 @@
from __future__ import annotations
from collections import defaultdict
from dataclasses import dataclass
from datetime import datetime, timezone
import json
import math
import re
from typing import Any
from canonical_layer.store import CanonicalStore
from config.settings import OneCSettings, load_settings
ACCOUNT_TOKEN_RE = re.compile(r"\b\d{2}(?:\.\d{2})?\b")
@dataclass
class FeatureEngineResult:
run_id: str
status: str
baseline_window_hours: int
stale_refresh_threshold_hours: int
entities_total: int
metrics_written: int
anomalies_written: int
error_message: str | None = None
def to_dict(self) -> dict[str, Any]:
return {
"run_id": self.run_id,
"status": self.status,
"baseline_window_hours": self.baseline_window_hours,
"stale_refresh_threshold_hours": self.stale_refresh_threshold_hours,
"entities_total": self.entities_total,
"metrics_written": self.metrics_written,
"anomalies_written": self.anomalies_written,
"error_message": self.error_message,
}
class FeatureService:
def __init__(self, *, settings: OneCSettings, store: CanonicalStore) -> None:
self.settings = settings
self.store = store
self.store.ensure_created()
@classmethod
def build(cls) -> "FeatureService":
settings = load_settings()
store = CanonicalStore(settings.canonical_db_url)
return cls(settings=settings, store=store)
@staticmethod
def _stddev(values: list[int]) -> float:
if len(values) <= 1:
return 0.0
mean = sum(values) / len(values)
variance = sum((value - mean) ** 2 for value in values) / len(values)
return math.sqrt(variance)
def _latest_previous_successful_run_id(self, *, current_run_id: str) -> str | None:
runs = self.store.list_recent_feature_runs(limit=50)
for run in runs:
if run.get("run_id") == current_run_id:
continue
if run.get("status") == "success":
return str(run.get("run_id"))
return None
def run_feature_engine(
self,
*,
baseline_window_hours: int | None = None,
stale_refresh_threshold_hours: int | None = None,
top_account_tokens: int = 20,
entity_limit: int | None = None,
) -> FeatureEngineResult:
baseline_hours = baseline_window_hours or self.settings.feature_default_baseline_window_hours
stale_hours = stale_refresh_threshold_hours or self.settings.anomaly_stale_refresh_threshold_hours
scan_limit = entity_limit or self.settings.feature_entity_scan_limit
run_id = self.store.start_feature_run(baseline_window_hours=baseline_hours)
now = datetime.now(timezone.utc)
try:
entities = self.store.iter_entities_for_features(limit=scan_limit)
link_counts = self.store.link_counts_by_source()
per_set_count: dict[str, int] = defaultdict(int)
per_set_empty_display: dict[str, int] = defaultdict(int)
per_set_link_sum: dict[str, int] = defaultdict(int)
account_token_count: dict[str, int] = defaultdict(int)
entity_link_items: list[dict[str, Any]] = []
metrics: list[dict[str, Any]] = []
anomalies: list[dict[str, Any]] = []
for entity in entities:
source_entity = str(entity.get("source_entity", "unknown"))
source_id = str(entity.get("source_id", ""))
display_name = str(entity.get("display_name", "")).strip()
attributes = entity.get("attributes", {})
per_set_count[source_entity] += 1
if not display_name:
per_set_empty_display[source_entity] += 1
link_count = int(link_counts.get((source_entity, source_id), 0))
per_set_link_sum[source_entity] += link_count
entity_link_items.append(
{
"source_entity": source_entity,
"source_id": source_id,
"display_name": display_name,
"link_count": link_count,
}
)
searchable_blob = f"{display_name} {source_id} {json.dumps(attributes, ensure_ascii=False)}"
for token in ACCOUNT_TOKEN_RE.findall(searchable_blob):
account_token_count[token] += 1
entities_total = len(entities)
links_total = sum(item["link_count"] for item in entity_link_items)
entity_sets_total = len(per_set_count)
avg_links_per_entity = (links_total / entities_total) if entities_total else 0.0
metrics.append(
{
"metric_key": "canonical_entities_total",
"scope": "global",
"scope_id": "",
"metric_type": "gauge",
"metric_value": float(entities_total),
"attributes": {},
}
)
metrics.append(
{
"metric_key": "canonical_links_total",
"scope": "global",
"scope_id": "",
"metric_type": "gauge",
"metric_value": float(links_total),
"attributes": {},
}
)
metrics.append(
{
"metric_key": "canonical_entity_sets_total",
"scope": "global",
"scope_id": "",
"metric_type": "gauge",
"metric_value": float(entity_sets_total),
"attributes": {},
}
)
metrics.append(
{
"metric_key": "avg_links_per_entity",
"scope": "global",
"scope_id": "",
"metric_type": "gauge",
"metric_value": float(avg_links_per_entity),
"attributes": {},
}
)
if entities_total == 0:
anomalies.append(
{
"signal_type": "no_canonical_data",
"severity": "high",
"scope": "global",
"scope_id": "",
"score": 1.0,
"details": {"reason": "canonical_entities table is empty"},
}
)
for source_entity in sorted(per_set_count):
count = per_set_count[source_entity]
empty_count = per_set_empty_display.get(source_entity, 0)
empty_share = (empty_count / count) if count else 0.0
avg_links_local = (per_set_link_sum.get(source_entity, 0) / count) if count else 0.0
metrics.append(
{
"metric_key": "entity_count",
"scope": "source_entity",
"scope_id": source_entity,
"metric_type": "gauge",
"metric_value": float(count),
"attributes": {},
}
)
metrics.append(
{
"metric_key": "avg_links_per_entity",
"scope": "source_entity",
"scope_id": source_entity,
"metric_type": "gauge",
"metric_value": float(avg_links_local),
"attributes": {},
}
)
metrics.append(
{
"metric_key": "empty_display_share",
"scope": "source_entity",
"scope_id": source_entity,
"metric_type": "ratio",
"metric_value": float(empty_share),
"attributes": {"empty_count": empty_count, "total": count},
}
)
if count >= 50 and empty_share >= 0.2:
anomalies.append(
{
"signal_type": "empty_display_share_high",
"severity": "medium",
"scope": "source_entity",
"scope_id": source_entity,
"score": float(empty_share),
"details": {"empty_count": empty_count, "total": count},
}
)
top_tokens = sorted(account_token_count.items(), key=lambda item: (-item[1], item[0]))[:top_account_tokens]
for token, token_count in top_tokens:
metrics.append(
{
"metric_key": "account_token_frequency",
"scope": "account_token",
"scope_id": token,
"metric_type": "gauge",
"metric_value": float(token_count),
"attributes": {},
}
)
link_values = [item["link_count"] for item in entity_link_items if item["link_count"] > 0]
if link_values:
mean_links = sum(link_values) / len(link_values)
std_links = self._stddev(link_values)
high_link_threshold = max(10, int(mean_links + (3.0 * std_links)))
else:
high_link_threshold = 10
metrics.append(
{
"metric_key": "high_link_threshold",
"scope": "global",
"scope_id": "",
"metric_type": "gauge",
"metric_value": float(high_link_threshold),
"attributes": {},
}
)
suspicious = [
item for item in entity_link_items
if item["link_count"] >= high_link_threshold
]
suspicious.sort(key=lambda item: item["link_count"], reverse=True)
for item in suspicious[:50]:
score = float(item["link_count"]) / float(high_link_threshold) if high_link_threshold else 0.0
severity = "high" if score >= 2.0 else "medium"
anomalies.append(
{
"signal_type": "high_link_degree",
"severity": severity,
"scope": item["source_entity"],
"scope_id": item["source_id"],
"score": score,
"details": {
"link_count": item["link_count"],
"threshold": high_link_threshold,
"display_name": item["display_name"],
},
}
)
latest_refresh = self.store.latest_refresh_finished_at()
if latest_refresh is None:
anomalies.append(
{
"signal_type": "missing_refresh_baseline",
"severity": "high",
"scope": "global",
"scope_id": "",
"score": 1.0,
"details": {"reason": "no successful refresh run found"},
}
)
else:
if latest_refresh.tzinfo is None:
latest_refresh = latest_refresh.replace(tzinfo=timezone.utc)
age_hours = max(0.0, (now - latest_refresh).total_seconds() / 3600.0)
metrics.append(
{
"metric_key": "refresh_age_hours",
"scope": "global",
"scope_id": "",
"metric_type": "gauge",
"metric_value": float(age_hours),
"attributes": {"latest_refresh_finished_at": latest_refresh.isoformat()},
}
)
if age_hours > stale_hours:
anomalies.append(
{
"signal_type": "stale_refresh",
"severity": "high",
"scope": "global",
"scope_id": "",
"score": float(age_hours / stale_hours) if stale_hours else float(age_hours),
"details": {
"age_hours": age_hours,
"threshold_hours": stale_hours,
"latest_refresh_finished_at": latest_refresh.isoformat(),
},
}
)
previous_run_id = self._latest_previous_successful_run_id(current_run_id=run_id)
if previous_run_id:
prev_entity_count_rows = self.store.list_feature_metrics(
limit=5000,
metric_key="entity_count",
scope="source_entity",
run_id=previous_run_id,
)
prev_counts = {
str(row.get("scope_id", "")): float(row.get("metric_value", 0.0))
for row in prev_entity_count_rows
}
for source_entity, current_count in per_set_count.items():
previous_count = prev_counts.get(source_entity)
if previous_count is None or previous_count <= 0:
continue
drift_ratio = (float(current_count) - previous_count) / previous_count
metrics.append(
{
"metric_key": "entity_count_drift_ratio",
"scope": "source_entity",
"scope_id": source_entity,
"metric_type": "ratio",
"metric_value": float(drift_ratio),
"attributes": {
"previous_count": previous_count,
"current_count": current_count,
"previous_run_id": previous_run_id,
},
}
)
if abs(drift_ratio) >= 0.3 and abs(float(current_count) - previous_count) >= 10:
anomalies.append(
{
"signal_type": "entity_count_drift",
"severity": "high" if abs(drift_ratio) >= 1.0 else "medium",
"scope": "source_entity",
"scope_id": source_entity,
"score": float(abs(drift_ratio)),
"details": {
"previous_count": previous_count,
"current_count": current_count,
"drift_ratio": drift_ratio,
"previous_run_id": previous_run_id,
},
}
)
metrics_written, anomalies_written = self.store.replace_feature_results(
run_id=run_id,
metrics=metrics,
anomalies=anomalies,
)
details = {
"entity_sets_total": entity_sets_total,
"top_account_tokens": [{"token": token, "count": count} for token, count in top_tokens],
}
self.store.finish_feature_run(
run_id=run_id,
status="success",
entities_total=entities_total,
metrics_written=metrics_written,
anomalies_written=anomalies_written,
details=details,
)
return FeatureEngineResult(
run_id=run_id,
status="success",
baseline_window_hours=baseline_hours,
stale_refresh_threshold_hours=stale_hours,
entities_total=entities_total,
metrics_written=metrics_written,
anomalies_written=anomalies_written,
)
except Exception as exc:
self.store.finish_feature_run(
run_id=run_id,
status="failed",
entities_total=0,
metrics_written=0,
anomalies_written=0,
details={},
error_message=str(exc),
)
return FeatureEngineResult(
run_id=run_id,
status="failed",
baseline_window_hours=baseline_hours,
stale_refresh_threshold_hours=stale_hours,
entities_total=0,
metrics_written=0,
anomalies_written=0,
error_message=str(exc),
)
def list_recent_runs(self, limit: int = 20) -> list[dict[str, Any]]:
return self.store.list_recent_feature_runs(limit=limit)
def list_metrics(
self,
*,
limit: int = 200,
metric_key: str | None = None,
scope: str | None = None,
run_id: str | None = None,
) -> list[dict[str, Any]]:
return self.store.list_feature_metrics(limit=limit, metric_key=metric_key, scope=scope, run_id=run_id)
def list_anomalies(
self,
*,
limit: int = 200,
severity: str | None = None,
active_only: bool = True,
run_id: str | None = None,
) -> list[dict[str, Any]]:
return self.store.list_anomaly_signals(
limit=limit,
severity=severity,
active_only=active_only,
run_id=run_id,
)
def stats(self) -> dict[str, Any]:
return self.store.feature_store_stats()

472
canonical_layer/mappers.py Normal file
View File

@ -0,0 +1,472 @@
from __future__ import annotations
import hashlib
import json
import re
from typing import Any
from canonical_layer.models import (
Account,
BankAccount,
CanonicalEntity,
CashflowArticle,
Contract,
Counterparty,
Currency,
Department,
Document,
EntityLink,
Individual,
InvoiceDocument,
Item,
Organization,
Period,
Posting,
RegisterMovement,
RegisterRecord,
ResponsiblePerson,
Subconto,
Warehouse,
)
GUID_RE = re.compile(
r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"
)
ZERO_GUID = "00000000-0000-0000-0000-000000000000"
MISSING_SOURCE_IDS = {"", "unknown", "none", "null", "n/a", "nan"}
SOURCE_ID_FIELDS = ("Ref_Key", "Ref", "ID", "Id", "id", "Key")
DISPLAY_FIELDS = (
"Description",
"Presentation",
"Number",
"Code",
"Наименование",
"Представление",
)
def _normalize_text(value: Any) -> str:
return str(value or "").strip()
def _normalize_key(value: str) -> str:
lowered = value.strip().lower()
return re.sub(r"[^a-zа-я0-9_]+", "", lowered)
def _is_guid(value: Any) -> bool:
if not isinstance(value, str):
return False
return bool(GUID_RE.match(value.strip()))
def _is_zero_guid(value: str) -> bool:
return value.strip().lower() == ZERO_GUID
def _pick_first(record: dict[str, Any], field_names: tuple[str, ...], default: str) -> str:
for field in field_names:
value = record.get(field)
normalized = _normalize_text(value)
if normalized:
return normalized
return default
def _reference_type_field(record: dict[str, Any], field: str) -> tuple[str | None, str | None]:
candidates = (
f"{field}_Type",
f"{field}Type",
)
for candidate in candidates:
if candidate in record:
text = _normalize_text(record.get(candidate))
if text:
return candidate, text
return None, None
def _build_composite_source_id(entity_set: str, record: dict[str, Any]) -> str:
composite_payload = {
"entity_set": entity_set,
"Recorder": _normalize_text(record.get("Recorder")),
"Recorder_Type": _normalize_text(record.get("Recorder_Type")),
"Ref": _normalize_text(record.get("Ref")),
"Ref_Type": _normalize_text(record.get("Ref_Type")),
"LineNumber": _normalize_text(record.get("LineNumber")),
"Period": _normalize_text(record.get("Period")),
"Date": _normalize_text(record.get("Date")),
}
digest = hashlib.sha1(
json.dumps(composite_payload, ensure_ascii=False, sort_keys=True).encode("utf-8")
).hexdigest()
return f"cmp:{digest}"
def _guess_entity_from_type_hint(type_hint: str) -> str | None:
text = _normalize_key(type_hint)
if not text:
return None
if "document_" in text or "документ" in text:
if "счетфактур" in text or "invoice" in text:
return "InvoiceDocument"
return "Document"
if "catalog_" in text or "справочник" in text:
if "контрагент" in text or "counterparty" in text:
return "Counterparty"
if "договор" in text or "contract" in text:
return "Contract"
if "валют" in text or "currency" in text:
return "Currency"
if "склад" in text or "warehouse" in text:
return "Warehouse"
if "физическоелицо" in text or "физлиц" in text or "individual" in text:
return "Individual"
if "статьядвиженияденежныхсредств" in text or "cashflow" in text:
return "CashflowArticle"
if "подраздел" in text or "department" in text:
return "Department"
if "номенклатур" in text or "item" in text or "product" in text:
return "Item"
if "пользоват" in text or "сотрудник" in text or "employee" in text or "user" in text:
return "ResponsiblePerson"
if "банковскиесчета" in text or "bankaccount" in text:
return "BankAccount"
if "организац" in text or "organization" in text:
return "Organization"
if "счета" in text or "account" in text:
return "Account"
if "accumulationregister_" in text or "informationregister_" in text or "accountingregister_" in text:
return "RegisterRecord"
return None
def _is_document_journal(entity_set: str) -> bool:
lowered = entity_set.lower()
return "documentjournal_" in lowered or "журнал" in lowered
def _is_register_entity_set(entity_set: str) -> bool:
lowered = entity_set.lower()
return (
"register" in lowered
or "регистр" in lowered
or "accumulation" in lowered
or "informationregister_" in lowered
or "accountingregister_" in lowered
)
def _is_document_entity_set(entity_set: str) -> bool:
lowered = entity_set.lower()
return "document_" in lowered or "document" in lowered or "документ" in lowered
def _field_role(field: str) -> str | None:
key = _normalize_key(field)
if key in {"recorder", "регистратор"}:
return "recorder"
if key == "ref":
return "ref"
if "счетфактур" in key or "invoice" in key:
return "invoice"
if "поставщик" in key or "supplier" in key:
return "supplier"
if "покупатель" in key or "buyer" in key or "customer" in key:
return "buyer"
if "контрагент" in key or "counterparty" in key:
return "counterparty"
if "договор" in key or "contract" in key:
return "contract"
if "организац" in key or "organization" in key:
return "organization"
if "ответствен" in key or "responsible" in key:
return "responsible"
if "валют" in key or "currency" in key:
return "currency"
if "склад" in key or "warehouse" in key:
return "warehouse"
if "статьядвиженияденежныхсредств" in key or "cashflow" in key:
return "cashflow_article"
if "физлиц" in key or "individual" in key or "person" in key:
return "individual"
if "подраздел" in key or "department" in key:
return "department"
if "банковскисчет" in key or "банковскийсчет" in key or "bankaccount" in key:
return "bank_account"
if "счеторганизац" in key or "organizationaccount" in key:
return "bank_account"
if "номенклатур" in key or "товар" in key or "item" in key or "product" in key:
return "item"
if "счет" in key or "account" in key:
return "account"
return None
def _role_target(role: str) -> str:
mapping = {
"recorder": "Document",
"ref": "Document",
"invoice": "InvoiceDocument",
"supplier": "Counterparty",
"buyer": "Counterparty",
"counterparty": "Counterparty",
"contract": "Contract",
"organization": "Organization",
"responsible": "ResponsiblePerson",
"currency": "Currency",
"warehouse": "Warehouse",
"cashflow_article": "CashflowArticle",
"individual": "Individual",
"department": "Department",
"bank_account": "BankAccount",
"item": "Item",
"account": "Account",
}
return mapping.get(role, "Unknown")
def _role_relation(entity_set: str, role: str) -> str:
if _is_document_journal(entity_set):
if role == "ref":
return "journal_refers_to_document"
if role == "currency":
return "journal_has_currency"
return f"journal_{role}"
if _is_register_entity_set(entity_set):
mapping = {
"recorder": "register_recorded_by_document",
"invoice": "register_relates_to_invoice",
"supplier": "register_relates_to_supplier",
"buyer": "register_relates_to_buyer",
"counterparty": "register_relates_to_counterparty",
"contract": "register_relates_to_contract",
"organization": "register_relates_to_organization",
"currency": "register_relates_to_currency",
"warehouse": "register_relates_to_warehouse",
"bank_account": "register_relates_to_bank_account",
"item": "register_relates_to_item",
"account": "register_relates_to_account",
"department": "register_relates_to_department",
"individual": "register_relates_to_individual",
"cashflow_article": "register_relates_to_cashflow_article",
"responsible": "register_has_responsible",
}
return mapping.get(role, "register_reference")
if _is_document_entity_set(entity_set):
mapping = {
"counterparty": "document_has_counterparty",
"contract": "document_has_contract",
"organization": "document_belongs_to_organization",
"responsible": "document_has_responsible",
"currency": "document_has_currency",
"warehouse": "document_has_warehouse",
"cashflow_article": "document_has_cashflow_article",
"bank_account": "document_has_bank_account",
"department": "document_has_department",
"individual": "document_relates_to_individual",
"invoice": "document_relates_to_invoice",
"item": "document_line_has_item",
"account": "document_line_has_account",
"supplier": "document_has_supplier",
"buyer": "document_has_buyer",
}
return mapping.get(role, "document_reference")
return "reference"
def _guess_target_entity(field: str) -> str:
role = _field_role(field)
if role is None:
return "Unknown"
return _role_target(role)
def _is_reference_candidate(record: dict[str, Any], field: str, value: Any) -> bool:
if field in SOURCE_ID_FIELDS and field != "Ref":
return False
if field.endswith("@navigationLinkUrl"):
return False
if field.endswith("_Type"):
return False
if isinstance(value, (dict, list)):
return False
normalized = _normalize_text(value)
if not normalized:
return False
if field.endswith("_Key"):
return True
if field.lower().endswith("ref"):
return True
if _is_guid(normalized):
return True
if _field_role(field) is not None:
return True
type_field, _ = _reference_type_field(record, field)
if type_field:
return True
return False
def _resolve_relation_and_target(
*,
entity_set: str,
field: str,
value: str,
record: dict[str, Any],
) -> tuple[str, str]:
role = _field_role(field)
type_field, type_hint = _reference_type_field(record, field)
target_from_type = _guess_entity_from_type_hint(type_hint or "")
if type_field and role in {"recorder", "ref"} and target_from_type == "InvoiceDocument":
# Recorder/Ref should still point at document-level nodes.
target_from_type = "Document"
if role is None:
relation = "reference"
else:
relation = _role_relation(entity_set, role)
if target_from_type:
target_entity = target_from_type
elif role is not None:
target_entity = _role_target(role)
else:
target_entity = _guess_target_entity(field)
if target_entity == "Unknown":
relation = "reference"
if _is_zero_guid(value):
return "null_reference", target_entity
return relation, target_entity
def _extract_links(entity_set: str, record: dict[str, Any]) -> list[EntityLink]:
links: list[EntityLink] = []
for field, raw_value in record.items():
if not _is_reference_candidate(record, field, raw_value):
continue
text_value = _normalize_text(raw_value)
if not text_value:
continue
if _is_zero_guid(text_value):
# Keep empty/null references out of canonical graph relations.
continue
relation, target_entity = _resolve_relation_and_target(
entity_set=entity_set,
field=field,
value=text_value,
record=record,
)
links.append(
EntityLink(
relation=relation,
target_entity=target_entity,
target_id=text_value,
source_field=field,
)
)
return links
def _entity_cls_for_set(entity_set: str) -> type[CanonicalEntity]:
lowered = entity_set.lower()
if "счетфактур" in lowered or "invoice" in lowered:
return InvoiceDocument
if "документ" in lowered or "document" in lowered:
return Document
if "контраг" in lowered or "counterparty" in lowered:
return Counterparty
if "договор" in lowered or "contract" in lowered:
return Contract
if "банковск" in lowered and "счет" in lowered:
return BankAccount
if "валют" in lowered or "currency" in lowered:
return Currency
if "склад" in lowered or "warehouse" in lowered:
return Warehouse
if "подраздел" in lowered or "department" in lowered:
return Department
if "физлиц" in lowered or "individual" in lowered:
return Individual
if "номенклатур" in lowered or "item" in lowered or "product" in lowered:
return Item
if "ответствен" in lowered or "пользоват" in lowered or "employee" in lowered:
return ResponsiblePerson
if "статьядвиженияденежныхсредств" in lowered or "cashflow" in lowered:
return CashflowArticle
if "счет" in lowered or "account" in lowered:
return Account
if "субконто" in lowered or "subconto" in lowered:
return Subconto
if "движ" in lowered or "movement" in lowered:
return RegisterMovement
if "провод" in lowered or "posting" in lowered:
return Posting
if "регистр" in lowered or "register" in lowered:
return RegisterRecord
if "период" in lowered or "period" in lowered:
return Period
if "организ" in lowered or "organization" in lowered:
return Organization
return CanonicalEntity
def _normalize_source_id(value: Any) -> str:
text = _normalize_text(value)
if text.lower() in MISSING_SOURCE_IDS:
return ""
return text
def map_record(entity_set: str, record: dict[str, Any]) -> CanonicalEntity:
source_id = _normalize_source_id(_pick_first(record, SOURCE_ID_FIELDS, default=""))
if not source_id:
source_id = _build_composite_source_id(entity_set, record)
display_name = _pick_first(record, DISPLAY_FIELDS, default=source_id)
canonical_cls = _entity_cls_for_set(entity_set)
return canonical_cls(
source_entity=entity_set,
source_id=source_id,
display_name=display_name,
attributes=record,
links=_extract_links(entity_set, record),
)
def map_records(entity_set: str, records: list[dict[str, Any]]) -> list[CanonicalEntity]:
return [map_record(entity_set, record) for record in records]
def canonical_relation_rule_catalog() -> list[dict[str, str]]:
return [
{"context": "register", "role": "recorder", "relation": "register_recorded_by_document"},
{"context": "journal", "role": "ref", "relation": "journal_refers_to_document"},
{"context": "document", "role": "counterparty", "relation": "document_has_counterparty"},
{"context": "document", "role": "contract", "relation": "document_has_contract"},
{"context": "document", "role": "organization", "relation": "document_belongs_to_organization"},
{"context": "document", "role": "responsible", "relation": "document_has_responsible"},
{"context": "document", "role": "currency", "relation": "document_has_currency"},
{"context": "document", "role": "warehouse", "relation": "document_has_warehouse"},
{"context": "document", "role": "cashflow_article", "relation": "document_has_cashflow_article"},
{"context": "document", "role": "bank_account", "relation": "document_has_bank_account"},
{"context": "register", "role": "supplier", "relation": "register_relates_to_supplier"},
{"context": "register", "role": "buyer", "relation": "register_relates_to_buyer"},
{"context": "register", "role": "invoice", "relation": "register_relates_to_invoice"},
{"context": "register", "role": "contract", "relation": "register_relates_to_contract"},
{"context": "register", "role": "organization", "relation": "register_relates_to_organization"},
{"context": "register", "role": "account", "relation": "register_relates_to_account"},
{"context": "register", "role": "item", "relation": "register_relates_to_item"},
]

96
canonical_layer/models.py Normal file
View File

@ -0,0 +1,96 @@
from __future__ import annotations
from typing import Any
from pydantic import BaseModel, Field
class EntityLink(BaseModel):
relation: str
target_entity: str
target_id: str
source_field: str | None = None
class CanonicalEntity(BaseModel):
source_entity: str
source_id: str
display_name: str
attributes: dict[str, Any] = Field(default_factory=dict)
links: list[EntityLink] = Field(default_factory=list)
class Organization(CanonicalEntity):
pass
class Counterparty(CanonicalEntity):
pass
class Contract(CanonicalEntity):
pass
class Account(CanonicalEntity):
pass
class Subconto(CanonicalEntity):
pass
class ResponsiblePerson(CanonicalEntity):
pass
class Currency(CanonicalEntity):
pass
class Warehouse(CanonicalEntity):
pass
class CashflowArticle(CanonicalEntity):
pass
class Department(CanonicalEntity):
pass
class Individual(CanonicalEntity):
pass
class Item(CanonicalEntity):
pass
class BankAccount(CanonicalEntity):
pass
class Document(CanonicalEntity):
pass
class InvoiceDocument(Document):
pass
class Posting(CanonicalEntity):
pass
class RegisterMovement(CanonicalEntity):
pass
class RegisterRecord(CanonicalEntity):
pass
class Period(CanonicalEntity):
pass

View File

@ -0,0 +1,95 @@
from __future__ import annotations
from datetime import datetime, timedelta, timezone
import re
from typing import Any
ODATA_DATE_RE = re.compile(r"/Date\((?P<millis>-?\d+)")
DATE_CANDIDATES = ("Date", "Дата", "Period", "Период", "PostedAt", "posted_at")
def normalize_dt(value: datetime) -> datetime:
if value.tzinfo is None:
return value.replace(tzinfo=timezone.utc)
return value.astimezone(timezone.utc)
def parse_dt(raw: str | None) -> datetime | None:
if raw is None:
return None
text = raw.strip()
if not text:
return None
match = ODATA_DATE_RE.search(text)
if match:
millis = int(match.group("millis"))
return datetime.fromtimestamp(millis / 1000, tz=timezone.utc)
for candidate in (text.replace("Z", "+00:00"), text):
try:
return normalize_dt(datetime.fromisoformat(candidate))
except ValueError:
continue
return None
def parse_record_datetime(record: dict[str, Any]) -> datetime | None:
for field in DATE_CANDIDATES:
value = record.get(field)
if value is None:
continue
parsed = parse_dt(str(value))
if parsed is not None:
return parsed
return None
def first_day_of_month(value: datetime) -> datetime:
normalized = normalize_dt(value)
return normalized.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
def month_bounds(value: datetime) -> tuple[datetime, datetime]:
start = first_day_of_month(value)
if start.month == 12:
end = start.replace(year=start.year + 1, month=1)
else:
end = start.replace(month=start.month + 1)
return start, end
def iso_week_bounds(value: datetime) -> tuple[datetime, datetime]:
normalized = normalize_dt(value).replace(hour=0, minute=0, second=0, microsecond=0)
start = normalized - timedelta(days=normalized.weekday())
end = start + timedelta(days=7)
return start, end
def window_key(value: datetime, *, granularity: str) -> str:
dt = normalize_dt(value)
if granularity == "month":
return dt.strftime("%Y-%m")
if granularity == "week":
year, week, _ = dt.isocalendar()
return f"{year}-W{week:02d}"
raise ValueError(f"Unsupported granularity: {granularity}")
def window_bounds_from_key(key: str, *, granularity: str) -> tuple[datetime, datetime]:
if granularity == "month":
start = normalize_dt(datetime.strptime(key, "%Y-%m"))
if start.month == 12:
end = start.replace(year=start.year + 1, month=1)
else:
end = start.replace(month=start.month + 1)
return start, end
if granularity == "week":
year_raw, week_raw = key.split("-W")
year = int(year_raw)
week = int(week_raw)
start = normalize_dt(datetime.fromisocalendar(year, week, 1))
end = start + timedelta(days=7)
return start, end
raise ValueError(f"Unsupported granularity: {granularity}")

296
canonical_layer/refresh.py Normal file
View File

@ -0,0 +1,296 @@
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime
import json
import re
from typing import Any
from canonical_layer.mappers import map_records
from canonical_layer.store import CanonicalStore
from config.client import ODataClient, extract_entity_sets
from config.settings import LOGS_DIR, OneCSettings, load_settings
ODATA_DATE_RE = re.compile(r"/Date\((?P<millis>-?\d+)")
REFRESH_MODES = {"historical", "incremental", "targeted"}
def _parse_dt(raw: str | None) -> datetime | None:
if raw is None:
return None
text = raw.strip()
if not text:
return None
match = ODATA_DATE_RE.search(text)
if match:
millis = int(match.group("millis"))
return datetime.fromtimestamp(millis / 1000)
for candidate in (text.replace("Z", "+00:00"), text):
try:
return datetime.fromisoformat(candidate)
except ValueError:
continue
return None
def _in_date_range(record: dict[str, Any], date_from: datetime | None, date_to: datetime | None) -> bool:
if date_from is None and date_to is None:
return True
date_candidates = ("Date", "Дата", "Period", "Период", "PostedAt", "posted_at")
record_dt: datetime | None = None
for field in date_candidates:
value = record.get(field)
if value is None:
continue
record_dt = _parse_dt(str(value))
if record_dt is not None:
break
if record_dt is None:
return True
if date_from and record_dt < date_from:
return False
if date_to and record_dt > date_to:
return False
return True
def _unique_preserve_order(items: list[str]) -> list[str]:
result: list[str] = []
seen: set[str] = set()
for item in items:
cleaned = item.strip()
if not cleaned or cleaned in seen:
continue
seen.add(cleaned)
result.append(cleaned)
return result
@dataclass
class RefreshResult:
run_id: str
mode: str
status: str
requested_entity_sets: list[str]
successful_entity_sets: list[str]
failed_entity_sets: list[dict[str, str]]
date_from: str | None
date_to: str | None
target_id: str | None
limit_per_set: int
records_read: int
entities_written: int
links_written: int
checkpoints_updated: int
def to_dict(self) -> dict[str, Any]:
return {
"run_id": self.run_id,
"mode": self.mode,
"status": self.status,
"requested_entity_sets": self.requested_entity_sets,
"successful_entity_sets": self.successful_entity_sets,
"failed_entity_sets": self.failed_entity_sets,
"date_from": self.date_from,
"date_to": self.date_to,
"target_id": self.target_id,
"limit_per_set": self.limit_per_set,
"records_read": self.records_read,
"entities_written": self.entities_written,
"links_written": self.links_written,
"checkpoints_updated": self.checkpoints_updated,
}
class RefreshService:
def __init__(self, *, settings: OneCSettings, client: ODataClient, store: CanonicalStore) -> None:
self.settings = settings
self.client = client
self.store = store
self.store.ensure_created()
@classmethod
def build(cls) -> "RefreshService":
settings = load_settings()
client = ODataClient(settings)
store = CanonicalStore(settings.canonical_db_url)
return cls(settings=settings, client=client, store=store)
def _load_entity_sets(self) -> list[dict[str, str]]:
entity_sets_file = LOGS_DIR / "entity_sets.json"
if entity_sets_file.exists():
payload = json.loads(entity_sets_file.read_text(encoding="utf-8"))
entity_sets = payload.get("entity_sets", [])
if isinstance(entity_sets, list):
return [item for item in entity_sets if isinstance(item, dict)]
metadata_file = LOGS_DIR / "metadata.xml"
if metadata_file.exists():
metadata_xml = metadata_file.read_text(encoding="utf-8")
return extract_entity_sets(metadata_xml)
metadata_xml = self.client.fetch_metadata()
metadata_file.parent.mkdir(parents=True, exist_ok=True)
metadata_file.write_text(metadata_xml, encoding="utf-8")
return extract_entity_sets(metadata_xml)
def _resolve_entity_sets(
self,
*,
requested_entity_sets: list[str] | None,
entity_keywords: list[str] | None,
) -> list[str]:
if requested_entity_sets:
return _unique_preserve_order(requested_entity_sets)
entity_sets = self._load_entity_sets()
names = [str(item.get("name", "")).strip() for item in entity_sets if str(item.get("name", "")).strip()]
if not names:
return []
keywords_source = entity_keywords or list(self.settings.refresh_default_entity_keywords)
keywords = [keyword.strip().lower() for keyword in keywords_source if keyword.strip()]
if not keywords:
return names
matched = [name for name in names if any(keyword in name.lower() for keyword in keywords)]
if matched:
return _unique_preserve_order(matched)
return names
def run_refresh(
self,
*,
mode: str,
date_from: str | None = None,
date_to: str | None = None,
target_id: str | None = None,
limit_per_set: int | None = None,
requested_entity_sets: list[str] | None = None,
entity_keywords: list[str] | None = None,
) -> RefreshResult:
normalized_mode = mode.strip().lower()
if normalized_mode not in REFRESH_MODES:
raise ValueError(f"Unsupported refresh mode: {mode}")
resolved_limit = limit_per_set or self.settings.refresh_default_limit_per_set
resolved_sets = self._resolve_entity_sets(
requested_entity_sets=requested_entity_sets,
entity_keywords=entity_keywords,
)
if not resolved_sets:
raise RuntimeError("No entity sets resolved for refresh")
run_id = self.store.start_refresh_run(
mode=normalized_mode,
requested_entity_sets=resolved_sets,
date_from=date_from,
date_to=date_to,
limit_per_set=resolved_limit,
)
parsed_date_from = _parse_dt(date_from)
parsed_date_to = _parse_dt(date_to)
safe_target = target_id.strip() if target_id else None
successful_sets: list[str] = []
failed_sets: list[dict[str, str]] = []
records_read = 0
entities_written = 0
links_written = 0
per_set_stats: list[dict[str, Any]] = []
for entity_set in resolved_sets:
try:
records = self.client.read_entity_set_records(entity_set, top=resolved_limit)
except Exception as exc:
failed_sets.append({"entity_set": entity_set, "error": str(exc)})
continue
records_read += len(records)
filtered_records = records
if parsed_date_from or parsed_date_to:
filtered_records = [
row for row in filtered_records if _in_date_range(row, parsed_date_from, parsed_date_to)
]
if normalized_mode == "targeted" and safe_target:
lowered_target = safe_target.lower()
filtered_records = [
row
for row in filtered_records
if lowered_target in json.dumps(row, ensure_ascii=False).lower()
]
mapped = map_records(entity_set, filtered_records)
entity_delta, link_delta = self.store.upsert_entities(run_id=run_id, entities=mapped)
entities_written += entity_delta
links_written += link_delta
successful_sets.append(entity_set)
per_set_stats.append(
{
"entity_set": entity_set,
"records_read": len(records),
"records_after_filters": len(filtered_records),
"entities_written": entity_delta,
"links_written": link_delta,
}
)
checkpoints_updated = self.store.update_checkpoints(
run_id=run_id,
entity_sets=successful_sets,
date_from=date_from,
date_to=date_to,
)
status = "success"
error_message: str | None = None
if successful_sets and failed_sets:
status = "partial_success"
elif failed_sets and not successful_sets:
status = "failed"
error_message = "; ".join(f"{item['entity_set']}: {item['error']}" for item in failed_sets[:3])
details = {
"per_set": per_set_stats,
"failed_sets": failed_sets,
}
self.store.finish_refresh_run(
run_id=run_id,
status=status,
records_read=records_read,
entities_written=entities_written,
links_written=links_written,
checkpoints_updated=checkpoints_updated,
details=details,
error_message=error_message,
)
return RefreshResult(
run_id=run_id,
mode=normalized_mode,
status=status,
requested_entity_sets=resolved_sets,
successful_entity_sets=successful_sets,
failed_entity_sets=failed_sets,
date_from=date_from,
date_to=date_to,
target_id=safe_target,
limit_per_set=resolved_limit,
records_read=records_read,
entities_written=entities_written,
links_written=links_written,
checkpoints_updated=checkpoints_updated,
)
def list_recent_runs(self, limit: int = 20) -> list[dict[str, Any]]:
return self.store.list_recent_runs(limit=limit)
def store_stats(self) -> dict[str, Any]:
return self.store.store_stats()

307
canonical_layer/risk.py Normal file
View File

@ -0,0 +1,307 @@
from __future__ import annotations
from collections import defaultdict
from dataclasses import dataclass
from typing import Any
from canonical_layer.store import CanonicalStore
from config.settings import OneCSettings, load_settings
SIGNAL_TO_DOMAIN = {
"stale_refresh": "operational_freshness",
"missing_refresh_baseline": "operational_freshness",
"no_canonical_data": "operational_freshness",
"high_link_degree": "suspicious_link_hub",
"entity_count_drift": "structural_drift",
"empty_display_share_high": "data_quality",
}
DOMAIN_PATTERN_KEY = {
"operational_freshness": "operational_freshness_risk",
"suspicious_link_hub": "suspicious_link_hub_risk",
"structural_drift": "structural_drift_risk",
"data_quality": "data_quality_risk",
"miscellaneous": "miscellaneous_risk",
}
DOMAIN_WEIGHTS = {
"operational_freshness": 0.35,
"suspicious_link_hub": 0.25,
"structural_drift": 0.25,
"data_quality": 0.15,
"miscellaneous": 0.20,
}
SEVERITY_BASE = {
"low": 0.25,
"medium": 0.50,
"high": 0.80,
"critical": 0.95,
}
@dataclass
class RiskEngineResult:
run_id: str
status: str
source_feature_run_id: str | None
active_anomalies_scanned: int
patterns_written: int
global_score: float
error_message: str | None = None
def to_dict(self) -> dict[str, Any]:
return {
"run_id": self.run_id,
"status": self.status,
"source_feature_run_id": self.source_feature_run_id,
"active_anomalies_scanned": self.active_anomalies_scanned,
"patterns_written": self.patterns_written,
"global_score": self.global_score,
"error_message": self.error_message,
}
class RiskService:
def __init__(self, *, settings: OneCSettings, store: CanonicalStore) -> None:
self.settings = settings
self.store = store
self.store.ensure_created()
@classmethod
def build(cls) -> "RiskService":
settings = load_settings()
store = CanonicalStore(settings.canonical_db_url)
return cls(settings=settings, store=store)
def _severity_from_score(self, score: float) -> str:
if score >= self.settings.risk_high_threshold:
return "high"
if score >= self.settings.risk_medium_threshold:
return "medium"
return "low"
@staticmethod
def _normalize_score(raw_score: float) -> float:
safe = max(0.0, raw_score)
if safe <= 1.0:
return safe
if safe >= 3.0:
return 1.0
return safe / 3.0
def _signal_score(self, anomaly: dict[str, Any]) -> float:
severity = str(anomaly.get("severity", "medium")).lower()
base = SEVERITY_BASE.get(severity, 0.50)
raw = self._normalize_score(float(anomaly.get("score", 0.0)))
return min(1.0, (0.45 * base) + (0.55 * raw))
def _build_domain_pattern(self, domain: str, anomalies: list[dict[str, Any]]) -> dict[str, Any]:
signal_scores = [self._signal_score(item) for item in anomalies]
average_score = (sum(signal_scores) / len(signal_scores)) if signal_scores else 0.0
max_score = max(signal_scores) if signal_scores else 0.0
density_bonus = min(0.30, max(0, len(anomalies) - 1) * 0.03)
domain_score = min(1.0, (0.60 * max_score) + (0.40 * average_score) + density_bonus)
confidence = min(1.0, 0.55 + min(0.40, len(anomalies) * 0.05))
severity = self._severity_from_score(domain_score)
by_signal_type: dict[str, int] = defaultdict(int)
for item in anomalies:
by_signal_type[str(item.get("signal_type", "unknown"))] += 1
top_examples = sorted(
anomalies,
key=lambda item: float(item.get("score", 0.0)),
reverse=True,
)[:5]
examples_payload = [
{
"signal_type": item.get("signal_type"),
"severity": item.get("severity"),
"scope": item.get("scope"),
"scope_id": item.get("scope_id"),
"score": item.get("score"),
"details": item.get("details", {}),
}
for item in top_examples
]
return {
"pattern_key": DOMAIN_PATTERN_KEY.get(domain, "miscellaneous_risk"),
"severity": severity,
"scope": "domain",
"scope_id": domain,
"score": round(domain_score, 6),
"confidence": round(confidence, 6),
"details": {
"anomalies_count": len(anomalies),
"signal_types": dict(sorted(by_signal_type.items(), key=lambda item: item[0])),
"top_examples": examples_payload,
},
}
def _compute_global_score(self, domain_patterns: list[dict[str, Any]]) -> float:
if not domain_patterns:
return 0.05
weighted_sum = 0.0
weight_total = 0.0
for item in domain_patterns:
domain = str(item.get("scope_id", "miscellaneous"))
weight = DOMAIN_WEIGHTS.get(domain, DOMAIN_WEIGHTS["miscellaneous"])
score = float(item.get("score", 0.0))
weighted_sum += weight * score
weight_total += weight
if weight_total <= 0.0:
return 0.05
return min(1.0, weighted_sum / weight_total)
def run_risk_engine(
self,
*,
source_feature_run_id: str | None = None,
anomaly_limit: int | None = None,
) -> RiskEngineResult:
selected_feature_run_id = source_feature_run_id
if not selected_feature_run_id:
latest = self.store.latest_successful_feature_run()
if latest is not None:
selected_feature_run_id = str(latest.get("run_id"))
run_id = self.store.start_risk_run(source_feature_run_id=selected_feature_run_id)
safe_limit = anomaly_limit or self.settings.risk_anomaly_scan_limit
try:
if selected_feature_run_id:
anomalies = self.store.list_anomaly_signals(
limit=safe_limit,
active_only=False,
run_id=selected_feature_run_id,
)
else:
anomalies = []
grouped: dict[str, list[dict[str, Any]]] = defaultdict(list)
for anomaly in anomalies:
signal_type = str(anomaly.get("signal_type", "unknown"))
domain = SIGNAL_TO_DOMAIN.get(signal_type, "miscellaneous")
grouped[domain].append(anomaly)
domain_patterns: list[dict[str, Any]] = []
for domain, domain_anomalies in grouped.items():
if not domain_anomalies:
continue
domain_patterns.append(self._build_domain_pattern(domain, domain_anomalies))
if not selected_feature_run_id:
domain_patterns.append(
{
"pattern_key": DOMAIN_PATTERN_KEY["operational_freshness"],
"severity": "high",
"scope": "domain",
"scope_id": "operational_freshness",
"score": 0.90,
"confidence": 0.95,
"details": {
"anomalies_count": 0,
"signal_types": {},
"top_examples": [],
"reason": "No successful feature run found",
},
}
)
global_score = self._compute_global_score(domain_patterns)
global_severity = self._severity_from_score(global_score)
global_pattern = {
"pattern_key": "global_risk_summary",
"severity": global_severity,
"scope": "global",
"scope_id": "",
"score": round(global_score, 6),
"confidence": round(min(1.0, 0.60 + 0.05 * len(domain_patterns)), 6),
"details": {
"source_feature_run_id": selected_feature_run_id,
"domain_scores": [
{
"domain": item.get("scope_id"),
"score": item.get("score"),
"severity": item.get("severity"),
}
for item in sorted(
domain_patterns,
key=lambda item: float(item.get("score", 0.0)),
reverse=True,
)
],
"anomalies_scanned": len(anomalies),
},
}
all_patterns = [global_pattern] + domain_patterns
patterns_written = self.store.replace_risk_patterns(run_id=run_id, patterns=all_patterns)
self.store.finish_risk_run(
run_id=run_id,
status="success",
patterns_written=patterns_written,
global_score=global_score,
details={
"source_feature_run_id": selected_feature_run_id,
"anomalies_scanned": len(anomalies),
"domains_total": len(domain_patterns),
},
)
return RiskEngineResult(
run_id=run_id,
status="success",
source_feature_run_id=selected_feature_run_id,
active_anomalies_scanned=len(anomalies),
patterns_written=patterns_written,
global_score=round(global_score, 6),
)
except Exception as exc:
self.store.finish_risk_run(
run_id=run_id,
status="failed",
patterns_written=0,
global_score=0.0,
details={},
error_message=str(exc),
)
return RiskEngineResult(
run_id=run_id,
status="failed",
source_feature_run_id=selected_feature_run_id,
active_anomalies_scanned=0,
patterns_written=0,
global_score=0.0,
error_message=str(exc),
)
def list_recent_runs(self, limit: int = 20) -> list[dict[str, Any]]:
return self.store.list_recent_risk_runs(limit=limit)
def list_patterns(
self,
*,
limit: int = 200,
severity: str | None = None,
active_only: bool = True,
run_id: str | None = None,
pattern_key: str | None = None,
scope: str | None = None,
) -> list[dict[str, Any]]:
return self.store.list_risk_patterns(
limit=limit,
severity=severity,
active_only=active_only,
run_id=run_id,
pattern_key=pattern_key,
scope=scope,
)
def stats(self) -> dict[str, Any]:
return self.store.risk_store_stats()

241
canonical_layer/service.py Normal file
View File

@ -0,0 +1,241 @@
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime
import json
from pathlib import Path
import re
from typing import Any
from config.client import ODataClient, extract_entity_sets
from config.settings import LOGS_DIR, load_settings
from canonical_layer.mappers import map_record, map_records
from canonical_layer.models import CanonicalEntity
ODATA_DATE_RE = re.compile(r"/Date\((?P<millis>-?\d+)")
def _parse_dt(raw: str | None) -> datetime | None:
if raw is None:
return None
text = raw.strip()
if not text:
return None
match = ODATA_DATE_RE.search(text)
if match:
millis = int(match.group("millis"))
return datetime.fromtimestamp(millis / 1000)
for candidate in (text.replace("Z", "+00:00"), text):
try:
return datetime.fromisoformat(candidate)
except ValueError:
continue
return None
def _in_date_range(record: dict[str, Any], date_from: datetime | None, date_to: datetime | None) -> bool:
if date_from is None and date_to is None:
return True
date_candidates = ("Date", "Дата", "Period", "Период", "PostedAt", "posted_at")
record_dt: datetime | None = None
for field in date_candidates:
value = record.get(field)
if value is None:
continue
record_dt = _parse_dt(str(value))
if record_dt is not None:
break
if record_dt is None:
return True
if date_from and record_dt < date_from:
return False
if date_to and record_dt > date_to:
return False
return True
@dataclass
class CanonicalService:
client: ODataClient
@classmethod
def build(cls) -> "CanonicalService":
settings = load_settings()
return cls(client=ODataClient(settings))
def _load_entity_sets(self) -> list[dict[str, str]]:
entity_sets_file = LOGS_DIR / "entity_sets.json"
if entity_sets_file.exists():
payload = json.loads(entity_sets_file.read_text(encoding="utf-8"))
entity_sets = payload.get("entity_sets", [])
if isinstance(entity_sets, list):
return [item for item in entity_sets if isinstance(item, dict)]
metadata_file = LOGS_DIR / "metadata.xml"
if metadata_file.exists():
metadata_xml = metadata_file.read_text(encoding="utf-8")
return extract_entity_sets(metadata_xml)
return []
def _sets_by_keywords(self, keywords: tuple[str, ...], limit: int = 10) -> list[str]:
names: list[str] = []
for item in self._load_entity_sets():
name = str(item.get("name", ""))
lowered = name.lower()
if any(keyword in lowered for keyword in keywords):
names.append(name)
return names[:limit]
def _safe_read(self, entity_set: str, top: int, extra_params: dict[str, Any] | None = None) -> list[dict[str, Any]]:
try:
return self.client.read_entity_set_records(entity_set, top=top, extra_params=extra_params)
except Exception:
return []
def list_entity_sets(self) -> dict[str, Any]:
entity_sets = self._load_entity_sets()
return {
"total": len(entity_sets),
"entity_sets": entity_sets,
}
def get_documents(self, date_from: str | None, date_to: str | None, limit: int = 100) -> list[CanonicalEntity]:
from_dt = _parse_dt(date_from)
to_dt = _parse_dt(date_to)
document_sets = self._sets_by_keywords(("документ", "document"), limit=5)
all_records: list[CanonicalEntity] = []
per_set_limit = max(3, limit // max(len(document_sets), 1))
for entity_set in document_sets:
records = self._safe_read(entity_set, top=per_set_limit)
filtered = [row for row in records if _in_date_range(row, from_dt, to_dt)]
all_records.extend(map_records(entity_set, filtered))
return all_records[:limit]
def get_document(self, source_id: str) -> CanonicalEntity | None:
document_sets = self._sets_by_keywords(("документ", "document"), limit=8)
for entity_set in document_sets:
records = self._safe_read(entity_set, top=50)
for row in records:
for key in ("Ref_Key", "ID", "Id", "id"):
if str(row.get(key, "")).strip() == source_id:
return map_record(entity_set, row)
return None
def get_postings(
self,
account: str | None,
date_from: str | None,
date_to: str | None,
limit: int = 100,
) -> list[CanonicalEntity]:
from_dt = _parse_dt(date_from)
to_dt = _parse_dt(date_to)
posting_sets = self._sets_by_keywords(("провод", "posting", "хозрасчет", "регистр"), limit=6)
output: list[CanonicalEntity] = []
per_set_limit = max(3, limit // max(len(posting_sets), 1))
for entity_set in posting_sets:
records = self._safe_read(entity_set, top=per_set_limit)
for row in records:
if account:
row_string = json.dumps(row, ensure_ascii=False).lower()
if account.lower() not in row_string:
continue
if not _in_date_range(row, from_dt, to_dt):
continue
output.append(map_record(entity_set, row))
return output[:limit]
def get_counterparty_documents(self, counterparty_id: str, limit: int = 100) -> list[CanonicalEntity]:
documents = self.get_documents(date_from=None, date_to=None, limit=limit * 2)
result: list[CanonicalEntity] = []
for doc in documents:
if doc.source_id == counterparty_id:
continue
serialized = json.dumps(doc.attributes, ensure_ascii=False)
if counterparty_id in serialized:
result.append(doc)
continue
for link in doc.links:
if link.target_id == counterparty_id:
result.append(doc)
break
return result[:limit]
def build_document_graph(self, document_id: str) -> dict[str, Any]:
root = self.get_document(document_id)
if root is None:
return {
"document_id": document_id,
"found": False,
"nodes": [],
"edges": [],
}
nodes: dict[str, dict[str, Any]] = {
root.source_id: {
"id": root.source_id,
"entity": root.source_entity,
"display_name": root.display_name,
}
}
edges: list[dict[str, Any]] = []
for link in root.links:
node_id = link.target_id
nodes.setdefault(
node_id,
{
"id": node_id,
"entity": link.target_entity,
"display_name": node_id,
},
)
edges.append(
{
"from": root.source_id,
"to": node_id,
"relation": link.relation,
"field": link.source_field,
}
)
postings = self.get_postings(account=None, date_from=None, date_to=None, limit=50)
for posting in postings:
serialized = json.dumps(posting.attributes, ensure_ascii=False)
if document_id not in serialized:
continue
nodes.setdefault(
posting.source_id,
{
"id": posting.source_id,
"entity": posting.source_entity,
"display_name": posting.display_name,
},
)
edges.append(
{
"from": root.source_id,
"to": posting.source_id,
"relation": "document_to_posting",
"field": "inferred",
}
)
return {
"document_id": document_id,
"found": True,
"nodes": list(nodes.values()),
"edges": edges,
}

744
canonical_layer/store.py Normal file
View File

@ -0,0 +1,744 @@
from __future__ import annotations
from datetime import datetime, timezone
import json
from pathlib import Path
from typing import Any
from uuid import uuid4
from sqlalchemy import create_engine, delete, func, select
from sqlalchemy.orm import Session, sessionmaker
from canonical_layer.models import CanonicalEntity
from canonical_layer.store_models import (
AnomalySignalRow,
Base,
CanonicalEntityRow,
CanonicalLinkRow,
FeatureMetricRow,
FeatureRunRow,
RefreshCheckpointRow,
RefreshRunRow,
RiskPatternRow,
RiskRunRow,
)
def _utc_now() -> datetime:
return datetime.now(timezone.utc)
def _dump_json(payload: Any) -> str:
return json.dumps(payload, ensure_ascii=False)
def _load_json(payload: str, default: Any) -> Any:
if not payload:
return default
try:
return json.loads(payload)
except json.JSONDecodeError:
return default
def _dt_to_iso(value: datetime | None) -> str | None:
if value is None:
return None
if value.tzinfo is None:
value = value.replace(tzinfo=timezone.utc)
return value.astimezone(timezone.utc).isoformat()
class CanonicalStore:
def __init__(self, db_url: str) -> None:
self.db_url = db_url
self.engine = create_engine(db_url, future=True)
self.session_factory = sessionmaker(bind=self.engine, autoflush=False, expire_on_commit=False, future=True)
def _ensure_sqlite_path(self) -> None:
if not self.db_url.startswith("sqlite:///"):
return
db_path_raw = self.db_url.replace("sqlite:///", "", 1)
db_path = Path(db_path_raw)
db_path.parent.mkdir(parents=True, exist_ok=True)
def ensure_created(self) -> None:
self._ensure_sqlite_path()
Base.metadata.create_all(self.engine)
def _session(self) -> Session:
return self.session_factory()
def start_refresh_run(
self,
*,
mode: str,
requested_entity_sets: list[str],
date_from: str | None,
date_to: str | None,
limit_per_set: int,
) -> str:
run_id = uuid4().hex
with self._session() as session, session.begin():
session.add(
RefreshRunRow(
id=run_id,
mode=mode,
status="running",
started_at=_utc_now(),
requested_entity_sets_json=_dump_json(requested_entity_sets),
date_from=date_from,
date_to=date_to,
limit_per_set=limit_per_set,
)
)
return run_id
def finish_refresh_run(
self,
*,
run_id: str,
status: str,
records_read: int,
entities_written: int,
links_written: int,
checkpoints_updated: int,
details: dict[str, Any] | None = None,
error_message: str | None = None,
) -> None:
with self._session() as session, session.begin():
run = session.get(RefreshRunRow, run_id)
if run is None:
return
run.status = status
run.records_read = records_read
run.entities_written = entities_written
run.links_written = links_written
run.checkpoints_updated = checkpoints_updated
run.details_json = _dump_json(details or {})
run.error_message = error_message
run.finished_at = _utc_now()
def upsert_entities(self, *, run_id: str, entities: list[CanonicalEntity]) -> tuple[int, int]:
entities_written = 0
links_written = 0
now = _utc_now()
with self._session() as session, session.begin():
for entity in entities:
row = session.execute(
select(CanonicalEntityRow).where(
CanonicalEntityRow.source_entity == entity.source_entity,
CanonicalEntityRow.source_id == entity.source_id,
)
).scalar_one_or_none()
if row is None:
row = CanonicalEntityRow(
source_entity=entity.source_entity,
source_id=entity.source_id,
display_name=entity.display_name,
attributes_json=_dump_json(entity.attributes),
first_seen_at=now,
updated_at=now,
last_refresh_run_id=run_id,
)
session.add(row)
else:
row.display_name = entity.display_name
row.attributes_json = _dump_json(entity.attributes)
row.updated_at = now
row.last_refresh_run_id = run_id
entities_written += 1
session.execute(
delete(CanonicalLinkRow).where(
CanonicalLinkRow.source_entity == entity.source_entity,
CanonicalLinkRow.source_id == entity.source_id,
)
)
for link in entity.links:
if not link.target_id:
continue
session.add(
CanonicalLinkRow(
source_entity=entity.source_entity,
source_id=entity.source_id,
relation=link.relation,
target_entity=link.target_entity,
target_id=link.target_id,
source_field=link.source_field,
updated_at=now,
last_refresh_run_id=run_id,
)
)
links_written += 1
return entities_written, links_written
def update_checkpoints(
self,
*,
run_id: str,
entity_sets: list[str],
date_from: str | None,
date_to: str | None,
) -> int:
if not entity_sets:
return 0
now = _utc_now()
updated = 0
with self._session() as session, session.begin():
for entity_set in entity_sets:
row = session.get(RefreshCheckpointRow, entity_set)
if row is None:
row = RefreshCheckpointRow(
entity_set=entity_set,
last_success_at=now,
last_refresh_run_id=run_id,
last_date_from=date_from,
last_date_to=date_to,
)
session.add(row)
else:
row.last_success_at = now
row.last_refresh_run_id = run_id
row.last_date_from = date_from
row.last_date_to = date_to
updated += 1
return updated
def list_recent_runs(self, limit: int = 20) -> list[dict[str, Any]]:
safe_limit = max(1, min(limit, 200))
with self._session() as session:
rows = (
session.execute(
select(RefreshRunRow)
.order_by(RefreshRunRow.started_at.desc())
.limit(safe_limit)
)
.scalars()
.all()
)
output: list[dict[str, Any]] = []
for row in rows:
output.append(
{
"run_id": row.id,
"mode": row.mode,
"status": row.status,
"started_at": _dt_to_iso(row.started_at),
"finished_at": _dt_to_iso(row.finished_at),
"requested_entity_sets": _load_json(row.requested_entity_sets_json, []),
"date_from": row.date_from,
"date_to": row.date_to,
"limit_per_set": row.limit_per_set,
"records_read": row.records_read,
"entities_written": row.entities_written,
"links_written": row.links_written,
"checkpoints_updated": row.checkpoints_updated,
"details": _load_json(row.details_json, {}),
"error_message": row.error_message,
}
)
return output
def store_stats(self) -> dict[str, Any]:
with self._session() as session:
entities_total = session.execute(select(func.count(CanonicalEntityRow.id))).scalar_one()
links_total = session.execute(select(func.count(CanonicalLinkRow.id))).scalar_one()
checkpoints_total = session.execute(select(func.count(RefreshCheckpointRow.entity_set))).scalar_one()
latest_run = (
session.execute(select(RefreshRunRow).order_by(RefreshRunRow.started_at.desc()).limit(1))
.scalars()
.first()
)
latest_run_payload: dict[str, Any] | None = None
if latest_run is not None:
latest_run_payload = {
"run_id": latest_run.id,
"mode": latest_run.mode,
"status": latest_run.status,
"started_at": _dt_to_iso(latest_run.started_at),
"finished_at": _dt_to_iso(latest_run.finished_at),
}
return {
"db_url": self.db_url,
"entities_total": int(entities_total),
"links_total": int(links_total),
"checkpoints_total": int(checkpoints_total),
"latest_run": latest_run_payload,
}
def start_feature_run(self, *, baseline_window_hours: int) -> str:
run_id = uuid4().hex
with self._session() as session, session.begin():
session.add(
FeatureRunRow(
id=run_id,
status="running",
started_at=_utc_now(),
baseline_window_hours=baseline_window_hours,
)
)
return run_id
def replace_feature_results(
self,
*,
run_id: str,
metrics: list[dict[str, Any]],
anomalies: list[dict[str, Any]],
) -> tuple[int, int]:
now = _utc_now()
with self._session() as session, session.begin():
session.execute(delete(FeatureMetricRow).where(FeatureMetricRow.feature_run_id == run_id))
session.execute(delete(AnomalySignalRow).where(AnomalySignalRow.feature_run_id == run_id))
# Deactivate previously active anomalies before writing a new active snapshot.
previous_anomalies = session.execute(
select(AnomalySignalRow).where(AnomalySignalRow.is_active == 1)
).scalars().all()
for item in previous_anomalies:
item.is_active = 0
for metric in metrics:
session.add(
FeatureMetricRow(
feature_run_id=run_id,
metric_key=str(metric.get("metric_key", "")),
scope=str(metric.get("scope", "global")),
scope_id=str(metric.get("scope_id", "")),
metric_type=str(metric.get("metric_type", "gauge")),
metric_value=float(metric.get("metric_value", 0.0)),
attributes_json=_dump_json(metric.get("attributes", {})),
computed_at=now,
)
)
for anomaly in anomalies:
session.add(
AnomalySignalRow(
feature_run_id=run_id,
signal_type=str(anomaly.get("signal_type", "unknown_signal")),
severity=str(anomaly.get("severity", "medium")),
scope=str(anomaly.get("scope", "global")),
scope_id=str(anomaly.get("scope_id", "")),
score=float(anomaly.get("score", 0.0)),
details_json=_dump_json(anomaly.get("details", {})),
detected_at=now,
is_active=1,
)
)
return len(metrics), len(anomalies)
def finish_feature_run(
self,
*,
run_id: str,
status: str,
entities_total: int,
metrics_written: int,
anomalies_written: int,
details: dict[str, Any] | None = None,
error_message: str | None = None,
) -> None:
with self._session() as session, session.begin():
row = session.get(FeatureRunRow, run_id)
if row is None:
return
row.status = status
row.entities_total = entities_total
row.metrics_written = metrics_written
row.anomalies_written = anomalies_written
row.details_json = _dump_json(details or {})
row.error_message = error_message
row.finished_at = _utc_now()
def list_recent_feature_runs(self, limit: int = 20) -> list[dict[str, Any]]:
safe_limit = max(1, min(limit, 200))
with self._session() as session:
rows = (
session.execute(
select(FeatureRunRow)
.order_by(FeatureRunRow.started_at.desc())
.limit(safe_limit)
)
.scalars()
.all()
)
output: list[dict[str, Any]] = []
for row in rows:
output.append(
{
"run_id": row.id,
"status": row.status,
"started_at": _dt_to_iso(row.started_at),
"finished_at": _dt_to_iso(row.finished_at),
"baseline_window_hours": row.baseline_window_hours,
"entities_total": row.entities_total,
"metrics_written": row.metrics_written,
"anomalies_written": row.anomalies_written,
"details": _load_json(row.details_json, {}),
"error_message": row.error_message,
}
)
return output
def list_feature_metrics(
self,
*,
limit: int = 200,
metric_key: str | None = None,
scope: str | None = None,
run_id: str | None = None,
) -> list[dict[str, Any]]:
safe_limit = max(1, min(limit, 2000))
with self._session() as session:
stmt = select(FeatureMetricRow).order_by(FeatureMetricRow.computed_at.desc(), FeatureMetricRow.id.desc())
if metric_key:
stmt = stmt.where(FeatureMetricRow.metric_key == metric_key)
if scope:
stmt = stmt.where(FeatureMetricRow.scope == scope)
if run_id:
stmt = stmt.where(FeatureMetricRow.feature_run_id == run_id)
rows = session.execute(stmt.limit(safe_limit)).scalars().all()
output: list[dict[str, Any]] = []
for row in rows:
output.append(
{
"id": row.id,
"feature_run_id": row.feature_run_id,
"metric_key": row.metric_key,
"scope": row.scope,
"scope_id": row.scope_id,
"metric_type": row.metric_type,
"metric_value": row.metric_value,
"attributes": _load_json(row.attributes_json, {}),
"computed_at": _dt_to_iso(row.computed_at),
}
)
return output
def list_anomaly_signals(
self,
*,
limit: int = 200,
severity: str | None = None,
active_only: bool = True,
run_id: str | None = None,
) -> list[dict[str, Any]]:
safe_limit = max(1, min(limit, 2000))
with self._session() as session:
stmt = select(AnomalySignalRow).order_by(AnomalySignalRow.detected_at.desc(), AnomalySignalRow.id.desc())
if active_only:
stmt = stmt.where(AnomalySignalRow.is_active == 1)
if severity:
stmt = stmt.where(AnomalySignalRow.severity == severity)
if run_id:
stmt = stmt.where(AnomalySignalRow.feature_run_id == run_id)
rows = session.execute(stmt.limit(safe_limit)).scalars().all()
output: list[dict[str, Any]] = []
for row in rows:
output.append(
{
"id": row.id,
"feature_run_id": row.feature_run_id,
"signal_type": row.signal_type,
"severity": row.severity,
"scope": row.scope,
"scope_id": row.scope_id,
"score": row.score,
"details": _load_json(row.details_json, {}),
"detected_at": _dt_to_iso(row.detected_at),
"is_active": bool(row.is_active),
}
)
return output
def feature_store_stats(self) -> dict[str, Any]:
with self._session() as session:
metrics_total = session.execute(select(func.count(FeatureMetricRow.id))).scalar_one()
anomalies_total = session.execute(select(func.count(AnomalySignalRow.id))).scalar_one()
active_anomalies_total = session.execute(
select(func.count(AnomalySignalRow.id)).where(AnomalySignalRow.is_active == 1)
).scalar_one()
latest_feature_run = (
session.execute(select(FeatureRunRow).order_by(FeatureRunRow.started_at.desc()).limit(1))
.scalars()
.first()
)
latest_payload: dict[str, Any] | None = None
if latest_feature_run is not None:
latest_payload = {
"run_id": latest_feature_run.id,
"status": latest_feature_run.status,
"started_at": _dt_to_iso(latest_feature_run.started_at),
"finished_at": _dt_to_iso(latest_feature_run.finished_at),
"metrics_written": latest_feature_run.metrics_written,
"anomalies_written": latest_feature_run.anomalies_written,
}
return {
"metrics_total": int(metrics_total),
"anomalies_total": int(anomalies_total),
"active_anomalies_total": int(active_anomalies_total),
"latest_feature_run": latest_payload,
}
def latest_successful_feature_run(self) -> dict[str, Any] | None:
with self._session() as session:
row = (
session.execute(
select(FeatureRunRow)
.where(FeatureRunRow.status == "success")
.order_by(FeatureRunRow.finished_at.desc(), FeatureRunRow.started_at.desc())
.limit(1)
)
.scalars()
.first()
)
if row is None:
return None
return {
"run_id": row.id,
"status": row.status,
"started_at": _dt_to_iso(row.started_at),
"finished_at": _dt_to_iso(row.finished_at),
"metrics_written": row.metrics_written,
"anomalies_written": row.anomalies_written,
}
def latest_refresh_finished_at(self) -> datetime | None:
with self._session() as session:
row = (
session.execute(
select(RefreshRunRow)
.where(RefreshRunRow.status.in_(("success", "partial_success")))
.order_by(RefreshRunRow.finished_at.desc())
.limit(1)
)
.scalars()
.first()
)
if row is None:
return None
return row.finished_at
def iter_entities_for_features(self, *, limit: int = 200000) -> list[dict[str, Any]]:
safe_limit = max(1, min(limit, 200000))
with self._session() as session:
rows = (
session.execute(
select(CanonicalEntityRow)
.order_by(CanonicalEntityRow.updated_at.desc())
.limit(safe_limit)
)
.scalars()
.all()
)
output: list[dict[str, Any]] = []
for row in rows:
output.append(
{
"source_entity": row.source_entity,
"source_id": row.source_id,
"display_name": row.display_name,
"attributes": _load_json(row.attributes_json, {}),
"updated_at": row.updated_at,
}
)
return output
def link_counts_by_source(self) -> dict[tuple[str, str], int]:
with self._session() as session:
rows = session.execute(
select(
CanonicalLinkRow.source_entity,
CanonicalLinkRow.source_id,
func.count(CanonicalLinkRow.id),
)
.group_by(CanonicalLinkRow.source_entity, CanonicalLinkRow.source_id)
).all()
output: dict[tuple[str, str], int] = {}
for source_entity, source_id, count in rows:
output[(str(source_entity), str(source_id))] = int(count)
return output
def start_risk_run(self, *, source_feature_run_id: str | None) -> str:
run_id = uuid4().hex
with self._session() as session, session.begin():
session.add(
RiskRunRow(
id=run_id,
status="running",
started_at=_utc_now(),
source_feature_run_id=source_feature_run_id,
)
)
return run_id
def replace_risk_patterns(self, *, run_id: str, patterns: list[dict[str, Any]]) -> int:
now = _utc_now()
with self._session() as session, session.begin():
session.execute(delete(RiskPatternRow).where(RiskPatternRow.risk_run_id == run_id))
previous_active = session.execute(
select(RiskPatternRow).where(RiskPatternRow.is_active == 1)
).scalars().all()
for item in previous_active:
item.is_active = 0
for pattern in patterns:
session.add(
RiskPatternRow(
risk_run_id=run_id,
pattern_key=str(pattern.get("pattern_key", "unknown_pattern")),
severity=str(pattern.get("severity", "low")),
scope=str(pattern.get("scope", "global")),
scope_id=str(pattern.get("scope_id", "")),
score=float(pattern.get("score", 0.0)),
confidence=float(pattern.get("confidence", 0.0)),
details_json=_dump_json(pattern.get("details", {})),
detected_at=now,
is_active=1,
)
)
return len(patterns)
def finish_risk_run(
self,
*,
run_id: str,
status: str,
patterns_written: int,
global_score: float,
details: dict[str, Any] | None = None,
error_message: str | None = None,
) -> None:
with self._session() as session, session.begin():
row = session.get(RiskRunRow, run_id)
if row is None:
return
row.status = status
row.patterns_written = patterns_written
row.global_score = global_score
row.details_json = _dump_json(details or {})
row.error_message = error_message
row.finished_at = _utc_now()
def list_recent_risk_runs(self, limit: int = 20) -> list[dict[str, Any]]:
safe_limit = max(1, min(limit, 200))
with self._session() as session:
rows = (
session.execute(
select(RiskRunRow)
.order_by(RiskRunRow.started_at.desc())
.limit(safe_limit)
)
.scalars()
.all()
)
output: list[dict[str, Any]] = []
for row in rows:
output.append(
{
"run_id": row.id,
"status": row.status,
"started_at": _dt_to_iso(row.started_at),
"finished_at": _dt_to_iso(row.finished_at),
"source_feature_run_id": row.source_feature_run_id,
"patterns_written": row.patterns_written,
"global_score": row.global_score,
"details": _load_json(row.details_json, {}),
"error_message": row.error_message,
}
)
return output
def list_risk_patterns(
self,
*,
limit: int = 200,
severity: str | None = None,
active_only: bool = True,
run_id: str | None = None,
pattern_key: str | None = None,
scope: str | None = None,
) -> list[dict[str, Any]]:
safe_limit = max(1, min(limit, 2000))
with self._session() as session:
stmt = select(RiskPatternRow).order_by(RiskPatternRow.detected_at.desc(), RiskPatternRow.id.desc())
if active_only:
stmt = stmt.where(RiskPatternRow.is_active == 1)
if severity:
stmt = stmt.where(RiskPatternRow.severity == severity)
if run_id:
stmt = stmt.where(RiskPatternRow.risk_run_id == run_id)
if pattern_key:
stmt = stmt.where(RiskPatternRow.pattern_key == pattern_key)
if scope:
stmt = stmt.where(RiskPatternRow.scope == scope)
rows = session.execute(stmt.limit(safe_limit)).scalars().all()
output: list[dict[str, Any]] = []
for row in rows:
output.append(
{
"id": row.id,
"risk_run_id": row.risk_run_id,
"pattern_key": row.pattern_key,
"severity": row.severity,
"scope": row.scope,
"scope_id": row.scope_id,
"score": row.score,
"confidence": row.confidence,
"details": _load_json(row.details_json, {}),
"detected_at": _dt_to_iso(row.detected_at),
"is_active": bool(row.is_active),
}
)
return output
def risk_store_stats(self) -> dict[str, Any]:
with self._session() as session:
patterns_total = session.execute(select(func.count(RiskPatternRow.id))).scalar_one()
active_patterns_total = session.execute(
select(func.count(RiskPatternRow.id)).where(RiskPatternRow.is_active == 1)
).scalar_one()
latest_run = (
session.execute(select(RiskRunRow).order_by(RiskRunRow.started_at.desc()).limit(1))
.scalars()
.first()
)
latest_payload: dict[str, Any] | None = None
if latest_run is not None:
latest_payload = {
"run_id": latest_run.id,
"status": latest_run.status,
"started_at": _dt_to_iso(latest_run.started_at),
"finished_at": _dt_to_iso(latest_run.finished_at),
"patterns_written": latest_run.patterns_written,
"global_score": latest_run.global_score,
}
return {
"patterns_total": int(patterns_total),
"active_patterns_total": int(active_patterns_total),
"latest_risk_run": latest_payload,
}

View File

@ -0,0 +1,148 @@
from __future__ import annotations
from datetime import datetime, timezone
from sqlalchemy import DateTime, Integer, String, Text, UniqueConstraint
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
def utc_now() -> datetime:
return datetime.now(timezone.utc)
class Base(DeclarativeBase):
pass
class RefreshRunRow(Base):
__tablename__ = "refresh_runs"
id: Mapped[str] = mapped_column(String(64), primary_key=True)
mode: Mapped[str] = mapped_column(String(32), nullable=False)
status: Mapped[str] = mapped_column(String(32), nullable=False, default="running")
started_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, default=utc_now)
finished_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
requested_entity_sets_json: Mapped[str] = mapped_column(Text, nullable=False, default="[]")
date_from: Mapped[str | None] = mapped_column(String(64), nullable=True)
date_to: Mapped[str | None] = mapped_column(String(64), nullable=True)
limit_per_set: Mapped[int] = mapped_column(Integer, nullable=False, default=200)
records_read: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
entities_written: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
links_written: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
checkpoints_updated: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
details_json: Mapped[str] = mapped_column(Text, nullable=False, default="{}")
error_message: Mapped[str | None] = mapped_column(Text, nullable=True)
class CanonicalEntityRow(Base):
__tablename__ = "canonical_entities"
__table_args__ = (
UniqueConstraint("source_entity", "source_id", name="uq_canonical_entities_source"),
)
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
source_entity: Mapped[str] = mapped_column(String(255), nullable=False)
source_id: Mapped[str] = mapped_column(String(255), nullable=False)
display_name: Mapped[str] = mapped_column(Text, nullable=False, default="")
attributes_json: Mapped[str] = mapped_column(Text, nullable=False, default="{}")
first_seen_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, default=utc_now)
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, default=utc_now)
last_refresh_run_id: Mapped[str | None] = mapped_column(String(64), nullable=True)
class CanonicalLinkRow(Base):
__tablename__ = "canonical_links"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
source_entity: Mapped[str] = mapped_column(String(255), nullable=False)
source_id: Mapped[str] = mapped_column(String(255), nullable=False)
relation: Mapped[str] = mapped_column(String(128), nullable=False)
target_entity: Mapped[str] = mapped_column(String(255), nullable=False)
target_id: Mapped[str] = mapped_column(String(255), nullable=False)
source_field: Mapped[str | None] = mapped_column(String(255), nullable=True)
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, default=utc_now)
last_refresh_run_id: Mapped[str | None] = mapped_column(String(64), nullable=True)
class RefreshCheckpointRow(Base):
__tablename__ = "refresh_checkpoints"
entity_set: Mapped[str] = mapped_column(String(255), primary_key=True)
last_success_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, default=utc_now)
last_refresh_run_id: Mapped[str] = mapped_column(String(64), nullable=False)
last_date_from: Mapped[str | None] = mapped_column(String(64), nullable=True)
last_date_to: Mapped[str | None] = mapped_column(String(64), nullable=True)
class FeatureRunRow(Base):
__tablename__ = "feature_runs"
id: Mapped[str] = mapped_column(String(64), primary_key=True)
status: Mapped[str] = mapped_column(String(32), nullable=False, default="running")
started_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, default=utc_now)
finished_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
baseline_window_hours: Mapped[int] = mapped_column(Integer, nullable=False, default=24)
entities_total: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
metrics_written: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
anomalies_written: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
details_json: Mapped[str] = mapped_column(Text, nullable=False, default="{}")
error_message: Mapped[str | None] = mapped_column(Text, nullable=True)
class FeatureMetricRow(Base):
__tablename__ = "feature_metrics"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
feature_run_id: Mapped[str] = mapped_column(String(64), nullable=False)
metric_key: Mapped[str] = mapped_column(String(128), nullable=False)
scope: Mapped[str] = mapped_column(String(128), nullable=False)
scope_id: Mapped[str] = mapped_column(String(255), nullable=False, default="")
metric_type: Mapped[str] = mapped_column(String(32), nullable=False, default="gauge")
metric_value: Mapped[float] = mapped_column(nullable=False, default=0.0)
attributes_json: Mapped[str] = mapped_column(Text, nullable=False, default="{}")
computed_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, default=utc_now)
class AnomalySignalRow(Base):
__tablename__ = "anomaly_signals"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
feature_run_id: Mapped[str] = mapped_column(String(64), nullable=False)
signal_type: Mapped[str] = mapped_column(String(128), nullable=False)
severity: Mapped[str] = mapped_column(String(32), nullable=False, default="medium")
scope: Mapped[str] = mapped_column(String(128), nullable=False, default="global")
scope_id: Mapped[str] = mapped_column(String(255), nullable=False, default="")
score: Mapped[float] = mapped_column(nullable=False, default=0.0)
details_json: Mapped[str] = mapped_column(Text, nullable=False, default="{}")
detected_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, default=utc_now)
is_active: Mapped[int] = mapped_column(Integer, nullable=False, default=1)
class RiskRunRow(Base):
__tablename__ = "risk_runs"
id: Mapped[str] = mapped_column(String(64), primary_key=True)
status: Mapped[str] = mapped_column(String(32), nullable=False, default="running")
started_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, default=utc_now)
finished_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
source_feature_run_id: Mapped[str | None] = mapped_column(String(64), nullable=True)
patterns_written: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
global_score: Mapped[float] = mapped_column(nullable=False, default=0.0)
details_json: Mapped[str] = mapped_column(Text, nullable=False, default="{}")
error_message: Mapped[str | None] = mapped_column(Text, nullable=True)
class RiskPatternRow(Base):
__tablename__ = "risk_patterns"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
risk_run_id: Mapped[str] = mapped_column(String(64), nullable=False)
pattern_key: Mapped[str] = mapped_column(String(128), nullable=False)
severity: Mapped[str] = mapped_column(String(32), nullable=False, default="low")
scope: Mapped[str] = mapped_column(String(128), nullable=False, default="global")
scope_id: Mapped[str] = mapped_column(String(255), nullable=False, default="")
score: Mapped[float] = mapped_column(nullable=False, default=0.0)
confidence: Mapped[float] = mapped_column(nullable=False, default=0.0)
details_json: Mapped[str] = mapped_column(Text, nullable=False, default="{}")
detected_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, default=utc_now)
is_active: Mapped[int] = mapped_column(Integer, nullable=False, default=1)

2
config/__init__.py Normal file
View File

@ -0,0 +1,2 @@
"""Shared runtime configuration and OData client primitives."""

146
config/client.py Normal file
View File

@ -0,0 +1,146 @@
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime, timezone
import json
from typing import Any
import xml.etree.ElementTree as ET
import requests
from requests.auth import HTTPBasicAuth
from config.settings import OneCSettings
GUID_FIELDS = {"Ref_Key", "ref_key", "ID", "Id", "id"}
@dataclass
class ODataResponse:
url: str
status_code: int
elapsed_ms: int
payload: dict[str, Any]
def utc_now_iso() -> str:
return datetime.now(timezone.utc).isoformat()
def _extract_records(payload: dict[str, Any]) -> list[dict[str, Any]]:
if "value" in payload and isinstance(payload["value"], list):
return payload["value"]
d = payload.get("d")
if isinstance(d, dict):
results = d.get("results")
if isinstance(results, list):
return results
if isinstance(results, dict):
return [results]
if d:
return [d]
return []
def extract_entity_sets(metadata_xml: str) -> list[dict[str, str]]:
root = ET.fromstring(metadata_xml)
entity_sets: list[dict[str, str]] = []
for node in root.iter():
if not node.tag.endswith("EntitySet"):
continue
name = node.attrib.get("Name", "")
entity_type = node.attrib.get("EntityType", "")
if not name:
continue
entity_sets.append(
{
"name": name,
"entity_type": entity_type,
"path": name,
}
)
entity_sets.sort(key=lambda item: item["name"].lower())
return entity_sets
def flatten_guid_like_fields(record: dict[str, Any]) -> list[str]:
candidates: list[str] = []
for field, value in record.items():
lowered = field.lower()
if field in GUID_FIELDS or lowered.endswith("_key") or "ref" in lowered:
candidates.append(field)
continue
if isinstance(value, str) and len(value) == 36 and value.count("-") == 4:
candidates.append(field)
return sorted(set(candidates))
class ODataClient:
def __init__(self, settings: OneCSettings) -> None:
self.settings = settings
self.session = requests.Session()
self.session.headers.update({"Accept": "application/json"})
if settings.username:
self.session.auth = HTTPBasicAuth(settings.username, settings.password)
def _request(
self,
method: str,
url: str,
*,
params: dict[str, Any] | None = None,
accept: str = "application/json",
) -> requests.Response:
headers = {"Accept": accept}
response = self.session.request(
method=method,
url=url,
params=params,
headers=headers,
timeout=self.settings.timeout,
verify=self.settings.verify_tls,
)
response.raise_for_status()
return response
def fetch_metadata(self) -> str:
response = self._request("GET", self.settings.metadata_url, accept="application/xml")
return response.text
def read_entity_set(
self,
entity_set: str,
*,
top: int = 5,
extra_params: dict[str, Any] | None = None,
) -> ODataResponse:
params: dict[str, Any] = {"$top": top}
if extra_params:
params.update(extra_params)
entity_url = f"{self.settings.service_root}{entity_set}"
response = self._request("GET", entity_url, params=params, accept="application/json")
elapsed_ms = int(response.elapsed.total_seconds() * 1000)
payload = response.json()
return ODataResponse(
url=response.url,
status_code=response.status_code,
elapsed_ms=elapsed_ms,
payload=payload,
)
def read_entity_set_records(
self,
entity_set: str,
*,
top: int = 5,
extra_params: dict[str, Any] | None = None,
) -> list[dict[str, Any]]:
response = self.read_entity_set(entity_set, top=top, extra_params=extra_params)
return _extract_records(response.payload)
@staticmethod
def to_json(data: dict[str, Any]) -> str:
return json.dumps(data, ensure_ascii=False, indent=2)

129
config/settings.py Normal file
View File

@ -0,0 +1,129 @@
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
import os
from dotenv import load_dotenv
PROJECT_ROOT = Path(__file__).resolve().parents[1]
LOGS_DIR = PROJECT_ROOT / "logs"
DATA_DIR = PROJECT_ROOT / "data"
def _as_bool(raw: str | None, default: bool) -> bool:
if raw is None:
return default
normalized = raw.strip().lower()
return normalized in {"1", "true", "yes", "on"}
def _normalize_odata_path(path: str) -> str:
stripped = path.strip()
if not stripped:
return "/odata/standard.odata/"
if not stripped.startswith("/"):
stripped = "/" + stripped
if not stripped.endswith("/"):
stripped = stripped + "/"
return stripped
def _default_canonical_db_url() -> str:
db_path = DATA_DIR / "canonical_store.db"
return f"sqlite:///{db_path.as_posix()}"
@dataclass(frozen=True)
class OneCSettings:
base_url: str
infobase: str
username: str
password: str
odata_path: str
timeout: int
verify_tls: bool
probe_top: int
probe_entity_sets: tuple[str, ...]
canonical_db_url: str
refresh_default_limit_per_set: int
refresh_default_entity_keywords: tuple[str, ...]
feature_default_baseline_window_hours: int
anomaly_stale_refresh_threshold_hours: int
feature_entity_scan_limit: int
risk_medium_threshold: float
risk_high_threshold: float
risk_anomaly_scan_limit: int
@property
def service_root(self) -> str:
base = self.base_url.rstrip("/")
infobase = self.infobase.strip().strip("/")
if infobase:
return f"{base}/{infobase}{self.odata_path}"
return f"{base}{self.odata_path}"
@property
def metadata_url(self) -> str:
return f"{self.service_root}$metadata"
def load_settings() -> OneCSettings:
env_file = PROJECT_ROOT / ".env"
if env_file.exists():
load_dotenv(env_file)
base_url = os.getenv("ONEC_BASE_URL", "http://localhost").strip()
infobase = os.getenv("ONEC_INFOBASE", "AccountingBase").strip()
username = os.getenv("ONEC_USERNAME", "").strip()
password = os.getenv("ONEC_PASSWORD", "")
odata_path = _normalize_odata_path(os.getenv("ONEC_ODATA_PATH", "/odata/standard.odata/"))
timeout = int(os.getenv("ONEC_TIMEOUT", "30").strip())
verify_tls = _as_bool(os.getenv("ONEC_VERIFY_TLS"), default=False)
probe_top = int(os.getenv("ONEC_PROBE_TOP", "5").strip())
probe_entity_sets_raw = os.getenv("ONEC_PROBE_ENTITY_SETS", "")
probe_entity_sets = tuple(
item.strip()
for item in probe_entity_sets_raw.split(",")
if item.strip()
)
canonical_db_url = os.getenv("CANONICAL_DB_URL", _default_canonical_db_url()).strip()
refresh_default_limit_per_set = int(os.getenv("REFRESH_DEFAULT_LIMIT_PER_SET", "200").strip())
refresh_default_keywords_raw = os.getenv(
"REFRESH_DEFAULT_ENTITY_KEYWORDS",
"document,posting,movement,register,account,counterparty,contract,organization,subconto,item,warehouse",
)
refresh_default_entity_keywords = tuple(
item.strip().lower()
for item in refresh_default_keywords_raw.split(",")
if item.strip()
)
feature_default_baseline_window_hours = int(os.getenv("FEATURE_BASELINE_WINDOW_HOURS", "24").strip())
anomaly_stale_refresh_threshold_hours = int(os.getenv("ANOMALY_STALE_REFRESH_THRESHOLD_HOURS", "6").strip())
feature_entity_scan_limit = int(os.getenv("FEATURE_ENTITY_SCAN_LIMIT", "200000").strip())
risk_medium_threshold = float(os.getenv("RISK_MEDIUM_THRESHOLD", "0.45").strip())
risk_high_threshold = float(os.getenv("RISK_HIGH_THRESHOLD", "0.75").strip())
risk_anomaly_scan_limit = int(os.getenv("RISK_ANOMALY_SCAN_LIMIT", "5000").strip())
return OneCSettings(
base_url=base_url,
infobase=infobase,
username=username,
password=password,
odata_path=odata_path,
timeout=timeout,
verify_tls=verify_tls,
probe_top=probe_top,
probe_entity_sets=probe_entity_sets,
canonical_db_url=canonical_db_url,
refresh_default_limit_per_set=refresh_default_limit_per_set,
refresh_default_entity_keywords=refresh_default_entity_keywords,
feature_default_baseline_window_hours=feature_default_baseline_window_hours,
anomaly_stale_refresh_threshold_hours=anomaly_stale_refresh_threshold_hours,
feature_entity_scan_limit=feature_entity_scan_limit,
risk_medium_threshold=risk_medium_threshold,
risk_high_threshold=risk_high_threshold,
risk_anomaly_scan_limit=risk_anomaly_scan_limit,
)

BIN
data/canonical_store.db Normal file

Binary file not shown.

43
docs/1c_inventory.md Normal file
View File

@ -0,0 +1,43 @@
# 1C Inventory Report
## Status
- Date: `2026-03-22`
- Environment owner: `NDC / Codex`
- Inventory status: `in_progress`
## Platform And Base
- 1C platform version: `8.3.27.1936`
- Base type: `file` (`1Cv8.1CD` present)
- Base path: `X:\1C\База бухгалтерии`
- Config name/version: `Бухгалтерия предприятия, редакция 2.0` (from context, needs final confirmation in Configurator)
## Integration Access
- OData published: `yes`
- OData base URL: `http://localhost/buh_test/odata/standard.odata/`
- Read-only user created: `yes` (`ndc_probe`)
- Read-only role verified: `pending explicit role check in 1C`
## Available Object Families (from Configurator)
- Documents: `present` (`Document_*`, sample reads successful)
- Catalogs: `present` (`Catalog_*`, sample reads successful)
- Accounting registers: `present` (`AccountingRegister_Хозрасчетный`, sample reads successful)
- Accumulation registers: `present` (`AccumulationRegister_*`, sample reads successful)
- Chart of accounts: `present in metadata` (needs targeted probe scenario)
- Roles: `not inventoried yet in Configurator`
## Initial Integration Scope
- Documents: `Document_АвансовыйОтчет`, `Document_ПоступлениеТоваровУслуг`, `Document_РеализацияТоваровУслуг`
- Counterparties: `Catalog_Контрагенты`
- Contracts: `Catalog_ДоговорыКонтрагентов`
- Accounts: `AccountingRegister_Хозрасчетный` and related accounting entities
- Register movements/postings: `AccountingRegister_Хозрасчетный`
## Risks Found
1. Read-only rights confirmed by behavior (`401` without auth), but write-deny must still be verified by role policy.
2. Production-fit of links needs deeper scenario probes (document->posting->account/subconto chains).

View File

@ -0,0 +1,252 @@
# Bootstrap Runbook: Полный пайплайн запуска с нуля (новая машина)
Дата: 2026-03-23
Статус: рабочий контур подтвержден (`adopt with restrictions`)
Цель: поднять наш текущий live read-only bridge к 1С с нуля на новой Windows-машине.
## 1. Что в итоге должно работать
После выполнения шагов должны одновременно работать:
1. Python proxy (`onec_mcp_toolkit_proxy`) на `http://127.0.0.1:6003`
2. 1С:Предприятие с открытой обработкой `MCP_Toolkit.epf` в режиме `Прокси`
3. Успешные read-only вызовы:
- `get_metadata`
- `execute_query`
- `get_link_of_object`
- `get_object_by_link`
Важно: это live request/response мост, не оффлайн-реплика.
## 2. Архитектура (минимум)
```text
Клиент/ассистент -> HTTP -> Python Proxy (6003) -> /1c/poll,/1c/result -> MCP_Toolkit.epf -> База 1С
```
## 3. Что нужно на новой машине
1. Windows (рекомендуемо 64-bit).
2. Установленная платформа 1С (в нашем контуре: `8.3.27.1936`).
3. Тестовая база 1С (БП 2.0) и рабочий пользователь с read-only правами.
4. Git.
5. Miniconda.
6. Доступ к репозиторию `ROCTUP/1c-mcp-toolkit`.
## 4. Рекомендованная структура папок
```text
X:\1C\
NDC_1C\
docs\
external\
1c-mcp-toolkit\
```
Если диска `X:` нет, можно использовать любой путь, но держать единую структуру.
## 5. Установка и подготовка окружения
### 5.1 Клонировать toolkit
```powershell
git clone https://github.com/ROCTUP/1c-mcp-toolkit X:\1C\NDC_1C\external\1c-mcp-toolkit
```
### 5.2 Проверить `.epf` артефакты
```powershell
Get-ChildItem X:\1C\NDC_1C\external\1c-mcp-toolkit\build
```
Ожидаемые файлы:
- `MCP_Toolkit.epf` (x64)
- `MCP_Toolkit_x86.epf` (x86 fallback)
### 5.3 Создать изолированную conda-среду
```powershell
& 'C:\Users\<USER>\miniconda3\Scripts\conda.exe' create -y -n ndc_1c_toolkit python=3.11
```
### 5.4 Установить зависимости proxy
```powershell
& 'C:\Users\<USER>\miniconda3\envs\ndc_1c_toolkit\python.exe' -m pip install --upgrade pip
& 'C:\Users\<USER>\miniconda3\envs\ndc_1c_toolkit\python.exe' -m pip install -r X:\1C\NDC_1C\external\1c-mcp-toolkit\requirements.txt
```
## 6. Запуск proxy (read-only профиль)
Запускать из PowerShell:
```powershell
$env:PORT='6003'
$env:TIMEOUT='180'
$env:ALLOW_DANGEROUS_WITH_APPROVAL='false'
$env:ANONYMIZATION_ENABLED='false'
$env:RESPONSE_FORMAT='json'
$env:LOG_LEVEL='INFO'
& 'C:\Users\<USER>\miniconda3\envs\ndc_1c_toolkit\python.exe' -m onec_mcp_toolkit_proxy
```
Проверка:
```powershell
Invoke-WebRequest http://127.0.0.1:6003/health -UseBasicParsing
```
Ожидаемо: HTTP 200 и `status=healthy`.
## 7. Запуск 1С и обработчика
### 7.1 Открыть 1С:Предприятие
Открывать в **режиме Предприятия** (не Конфигуратор).
Если UI обработки не появляется в обычном режиме, запускать в управляемом приложении.
### 7.2 Открыть внешнюю обработку
`Файл -> Открыть -> X:\1C\NDC_1C\external\1c-mcp-toolkit\build\MCP_Toolkit.epf`
### 7.3 Настроить форму MCP Toolkit
1. Режим: `Прокси`
2. Адрес сервера: `http://127.0.0.1:6003`
3. Идентификатор канала: `default` (или ваш фиксированный channel)
4. Нажать `Подключиться`
Ожидаемо в логе формы:
- `Подключение к серверу: http://127.0.0.1:6003`
- `Успешное подключение к серверу`
## 8. Smoke-проверка после подключения
### 8.1 Metadata
```powershell
Invoke-WebRequest "http://127.0.0.1:6003/api/get_metadata?channel=default&meta_type=Документ&limit=20" -UseBasicParsing
```
Ожидаемо: `success=true`.
### 8.2 Query
```powershell
$body = @{ query = "ВЫБРАТЬ ПЕРВЫЕ 1 1 КАК Test"; limit = 1 } | ConvertTo-Json
Invoke-WebRequest "http://127.0.0.1:6003/api/execute_query?channel=default" `
-Method POST -ContentType "application/json; charset=utf-8" -Body $body -UseBasicParsing
```
Ожидаемо: `success=true`, `Test=1`.
### 8.3 Object link flow
1. Получить `object_description` (например, из `execute_query`).
2. Вызвать `get_link_of_object`.
3. Передать ссылку в `get_object_by_link`.
Ожидаемо: объект документа читается.
## 9. Ежедневный рабочий цикл (операторский)
### Старт дня
1. Запустить proxy.
2. Проверить `/health`.
3. Запустить 1С и открыть `MCP_Toolkit.epf`.
4. Проверить статус `Подключено`.
5. Выполнить быстрый test query.
### Стоп дня
1. Отключиться в форме MCP Toolkit.
2. Закрыть 1С.
3. Остановить proxy (Ctrl+C/Stop-Process).
## 10. Траблшутинг (частые проблемы)
### 10.1 Ошибка `Не могу установить соединение` в форме 1С
Причина: proxy не запущен или не слушает `6003`.
Проверка:
```powershell
Invoke-WebRequest http://127.0.0.1:6003/health -UseBasicParsing
```
### 10.2 `timeout waiting for 1C response` на API
Причина: нет активного `.epf` в том же `channel`, форма закрыта, либо канал не совпадает.
### 10.3 `UI не показывается` при открытии `.epf`
Обработка имеет управляемые формы.
Запускать в 1С:Предприятии (управляемый режим), не в Конфигураторе для рабочего контура.
### 10.4 Кодировка ссылки из `get_link_of_object` выглядит “кракозябрами”
Нюанс текущего ответа proxy; вызов рабочий, ссылку можно нормализовать при постобработке.
## 11. Жёсткие правила безопасности
1. Только read-only операции.
2. Не использовать `execute_code`.
3. Не выставлять `6003` во внешний интернет.
4. Держать `ALLOW_DANGEROUS_WITH_APPROVAL=false`.
5. Работать под отдельным техпользователем с минимальными правами чтения.
## 12. Что это даёт и чего не даёт
### Даёт
- Живой доступ к текущим данным 1С по запросу.
- Runtime metadata + deep read semantics (документы, проводки, субконто, сальдо).
### Не даёт
- Мгновенный “весь срез компании” в одном запросе для тяжёлой аналитики.
- Автоматическую фоновой репликацию без отдельного слоя витрин/снэпшотов.
## 13. Рекомендованный next step после bootstrap
1. Добавить one-click старт скрипт (`Start-NDC1CBridge.ps1`).
2. Добавить one-click smoke скрипт (`Test-NDC1CBridge.ps1`).
3. Поднять плановую аналитическую витрину (например, 15/60 минут) для тяжёлых задач.
---
Итог bootstrap: на новой машине поднимаем контур за последовательность
`Proxy -> MCP_Toolkit.epf -> Подключение -> Smoke`
и получаем рабочий live read-only мост к 1С на текущем этапе проекта.
_________________________________________________
ЗАПУСК ПРКСИ ПЕРЕД ПОДКЛЮЮЧЕНИЕМ К ТУЛКИТ 1С
_________________________________________________
Запускай так в PowerShell:
$env:PORT='6003'
$env:TIMEOUT='180'
$env:ALLOW_DANGEROUS_WITH_APPROVAL='false'
$env:ANONYMIZATION_ENABLED='false'
$env:RESPONSE_FORMAT='json'
$env:LOG_LEVEL='INFO'
& 'C:\Users\DCTOUCH\miniconda3\envs\ndc_1c_toolkit\python.exe' -m onec_mcp_toolkit_proxy
Проверка, что поднялся:
Invoke-WebRequest http://127.0.0.1:6003/health -UseBasicParsing
Должен вернуть status":"healthy".
Остановить:
в том же окне Ctrl + C.

View File

@ -0,0 +1,194 @@
# Архитектурный Stage Report (AS-IS)
Дата фиксации: 2026-03-23
Проект: NDC 1C analytics bridge
Контур: локальный стенд (Windows, тестовая база 1С)
## 1. Резюме этапа
На текущем этапе подтверждён рабочий **read-only runtime мост** к живой 1С через `1c-mcp-toolkit` в proxy-режиме.
Три критические бухгалтерские проверки закрыты как `PROVEN`:
1. `document -> posting -> debit/credit account`
2. `posting -> subconto[1..3] -> counterparty / contract / item`
3. Объяснение реального сальдо через агрегат движений (`delta = 0.0`)
Официальный статус решения: `adopt with restrictions`.
## 2. Цель этапа (что фиксируем)
Зафиксировать не идею, а фактическое состояние архитектуры:
- как реально ходят данные;
- что именно работает в live режиме;
- что является ограничением;
- какие артефакты подтверждают результаты;
- что берём в следующий этап.
## 3. Текущая архитектура (AS-IS)
```text
AI/клиент аналитики
|
| HTTP (read-only API calls)
v
Local Proxy: onec_mcp_toolkit_proxy (FastAPI) [127.0.0.1:6003]
|\
| \-- /health, /api/get_metadata, /api/execute_query, /api/get_object_by_link, ...
|
| Long polling bridge
| GET /1c/poll
| POST /1c/result
v
1C External Processing MCP_Toolkit.epf (управляемая форма, режим "Прокси")
|
v
Тестовая база 1С: Бухгалтерия предприятия 2.0 (2.0.67.20), платформа 8.3.27.1936
```
Дополнительно параллельно существует OData read-only слой (базовый широкий вход), но в этом stage фиксируется именно runtime-mост toolkit.
## 4. Компоненты и роль
1. `MCP_Toolkit.epf`
- Путь: `X:\1C\NDC_1C\external\1c-mcp-toolkit\build\MCP_Toolkit.epf`
- Роль: 1С-сторона моста, выполнение read-запросов в контексте базы и возврат результатов в proxy.
- Важно: форма управляемая; в обычном режиме UI может не открываться.
2. `onec_mcp_toolkit_proxy` (Python/FastAPI)
- Путь: `X:\1C\NDC_1C\external\1c-mcp-toolkit\onec_mcp_toolkit_proxy`
- Роль: единая HTTP/MCP точка входа для аналитики.
- Runtime: Miniconda env `ndc_1c_toolkit`.
- Базовый endpoint: `http://127.0.0.1:6003`.
3. 1С тестовая база
- Платформа: `8.3.27.1936`
- Конфигурация: `БП 2.0 (2.0.67.20)`
- Роль: source of truth для всех live чтений.
4. Артефактный слой (доказательства)
- Путь: `X:\1C\NDC_1C\docs\snapshots\toolkit`
- Роль: хранение фактических ответов/логов проверки этапа.
## 5. Семантика доступа к данным (очень важно)
### 5.1 Что это сейчас
Это **live request/response bridge**:
- каждый запрос читается из текущего состояния 1С на момент вызова;
- данные не берутся из локальной копии/реплики;
- нет постоянного stream-пайплайна “само обновляется”.
### 5.2 Что это не сейчас
- это не полноценная витрина данных по всей компании;
- это не always-on snapshot pipeline;
- это не CDC/стриминг изменений в фоновом режиме.
### 5.3 Практический вывод
Для точечных и средних аналитических задач мост подходит в near real-time режиме.
Для очень широких срезов (вся компания, тяжёлые многомерные отчёты) потребуется отдельный слой агрегаций/снэпшотов.
## 6. API-поверхность, которую используем в проекте
Разрешённый operational набор:
- `get_metadata`
- `execute_query`
- `get_object_by_link`
- `get_link_of_object`
- при необходимости другие read-only методы
Запрещено в operational контуре:
- `execute_code`
- любые write/mutation операции.
## 7. Ограничения и guardrails
Обязательные ограничения на текущем этапе:
1. Только read-only вызовы.
2. `ALLOW_DANGEROUS_WITH_APPROVAL=false`.
3. Endpoint не публиковать наружу.
4. Использовать отдельного техпользователя 1С с правами чтения.
5. Все спорные/рисковые действия маркировать как `manual required`.
## 8. Подтверждённые доказательства этапа
### 8.1 Проверка 1: document -> posting -> debit/credit
- Есть реальная проводка:
- документ: `Счет-фактура полученный 00000000001 от 03.08.2030 12:00:00`
- Дт: `68.02`
- Кт: `19.04`
- сумма: `500`
- Документ прочитан по ссылке через `get_object_by_link`.
### 8.2 Проверка 2: posting -> subconto[1..3]
Есть реальные примеры:
- `Контрагент + Договор`:
- `СубконтоКт1 (Контрагенты)` = `Ассоциация "СРО"СОВЕТ ПРОЕКТИРОВЩИКОВ"`
- `СубконтоКт2 (Договоры)` = `дело А40-201628/21`
- `Номенклатура/склад`:
- `СубконтоКт1 (Номенклатура)` = `Портьерные шторы Garden kolor`
- `СубконтоКт3 (Склады)` = `Основной склад`
### 8.3 Проверка 3: объяснение сальдо движениями
По счёту `68.02`:
- `СальдоИтого (Остатки) = 28363.8`
- `ОборотДт - ОборотКт = 49600886.74 - 49572522.94 = 28363.8`
- `delta = 0.0`
## 9. Риски и технический долг
1. Heavy analytics latency
- На очень широких задачах модель вынуждена собирать картину по частям.
2. Отсутствие материализованной витрины
- Нет быстрого “единый срез по компании” без серии запросов.
3. Нюанс кодировки
- В отдельных ответах (`get_link_of_object`) требуется нормализация строки ссылки.
4. Риск регресса через опасные инструменты
- `execute_code` существует в toolkit и должен быть процедурно/технически заблокирован в operational потоке.
## 10. Решения, принятые по итогу этапа
1. Оставляем `OData` как базовый read-only широкий слой.
2. `1c-mcp-toolkit` фиксируем как рабочий runtime/deeper слой для семантических проверок и интерактивной аналитики.
3. Статус внедрения: `adopt with restrictions`.
## 11. Что делаем следующим этапом
Этап 2 (production-hardening):
1. Ввести жёсткие технические guardrails на запрет `execute_code`.
2. Формализовать query-профили (шаблоны безопасных read-only запросов).
3. Добавить слой агрегатов/плановых снэпшотов для тяжёлой аналитики.
4. Настроить эксплуатационный регламент:
- health-check;
- контроль channel;
- таймауты;
- логирование и аудит вызовов.
## 12. Список ключевых артефактов этапа
- `X:\1C\NDC_1C\docs\toolkit_inventory.md`
- `X:\1C\NDC_1C\docs\toolkit_install_runbook.md`
- `X:\1C\NDC_1C\docs\toolkit_smoke_test_report.md`
- `X:\1C\NDC_1C\docs\toolkit_semantic_probe_report.md`
- `X:\1C\NDC_1C\docs\toolkit_decision_note.md`
- `X:\1C\NDC_1C\docs\snapshots\toolkit\semantic_probe_live_summary.json`
---
Stage считаем зафиксированным на дату `2026-03-23`:
**Live read-only bridge подтверждён, критические бухгалтерские цепочки доказаны, архитектурный статус — `adopt with restrictions`.**

View File

@ -0,0 +1,26 @@
# 2020 экспорт: состав выгрузки
Папка собрана автоматически для ручного анализа текущего состояния.
## Файлы
1. `01_ontology_mapping_layer.md` — текущая онтология/мэппинг и метрики среза.
2. `02_canonical_relation_rules.md` — правила построения canonical relations.
3. `03_snapshot_fragment_problem_cases.json` — проблемный фрагмент snapshot июня 2020.
4. `04_samples_SpisanieSRaschetnogoScheta.json` — реальные записи по `СписаниеСРасчетногоСчета`.
5. `05_samples_RealizaciyaTovarovUslug.json` — реальные записи по `РеализацияТоваровУслуг`.
6. `06_samples_PostuplenieTovarovUslug.json` — реальные записи по `ПоступлениеТоваровУслуг`.
7. `07_samples_DocumentJournals.json` — реальные записи по журналам документов.
8. `08_samples_NDS_registers.json` — реальные записи по НДС-регистрам.
9. `09_samples_key_fields_Recorder_Ref_Supplier_Buyer_Responsible.json` — записи с ключевыми полями.
## Ключевые поля: фактическая встречаемость в snapshot
| field | count |
| --- | --- |
| Ответственный_Key | 187 |
| Ref | 168 |
| Recorder | 147 |
| Ref_Key | 93 |
| Поставщик_Key | 78 |
| Покупатель_Key | 46 |

View File

@ -0,0 +1,96 @@
# Текущая онтология / mapping-слой
Дата экспорта: 2026-03-23T10:23:29.088258+00:00
Источник snapshot: `X:\1C\NDC_1C\logs\pre_report_snapshot_2020_2020-06_semantic_v2.json`
## Что считается сущностями сейчас
Базовая модель (canonical classes):
- `CanonicalEntity`
- `Organization`
- `Counterparty`
- `Contract`
- `Account`
- `Subconto`
- `ResponsiblePerson`
- `Currency`
- `Warehouse`
- `CashflowArticle`
- `Department`
- `Individual`
- `Item`
- `BankAccount`
- `Document`
- `InvoiceDocument`
- `Posting`
- `RegisterMovement`
- `RegisterRecord`
- `Period`
## Срез июня 2020: покрытие сущностей
- Отобранный период: `2020-06`
- Диапазон: `2020-06-01T00:00:00+00:00 -> 2020-07-01T00:00:00+00:00`
- Записей в slice: `409`
- Связей в slice: `2011`
- Entity sets: `42`
- Записей с `source_id=unknown`: `0`
### Распределение entity sets по canonical-классам
| Canonical class | Entity set count |
| --- | --- |
| Account | 3 |
| Document | 28 |
| Individual | 1 |
| InvoiceDocument | 2 |
| RegisterRecord | 8 |
### Топ target_entity в links
| target_entity | count |
| --- | --- |
| Document | 458 |
| Counterparty | 440 |
| Organization | 434 |
| Account | 217 |
| Currency | 158 |
| ResponsiblePerson | 112 |
| Unknown | 102 |
| BankAccount | 30 |
| Individual | 22 |
| Department | 18 |
| Warehouse | 12 |
| Contract | 7 |
| Item | 1 |
### Топ relation в links
| relation | count |
| --- | --- |
| journal_refers_to_document | 168 |
| journal_organization | 168 |
| reference | 165 |
| register_relates_to_organization | 148 |
| register_recorded_by_document | 147 |
| document_has_counterparty | 139 |
| document_line_has_account | 136 |
| journal_counterparty | 133 |
| register_relates_to_invoice | 124 |
| document_belongs_to_organization | 118 |
| journal_has_currency | 95 |
| register_relates_to_supplier | 78 |
| register_relates_to_account | 77 |
| document_has_currency | 63 |
| document_has_responsible | 57 |
| journal_responsible | 55 |
| register_relates_to_buyer | 46 |
| journal_bank_account | 30 |
| register_relates_to_individual | 21 |
| register_relates_to_department | 18 |
### Качество типизации связей
- Всего связей: `2011`
- Связей с `target_entity=Unknown`: `102`
- Доля unknown: `5.07%`

View File

@ -0,0 +1,32 @@
# Текущие canonical relation rules
Источник: `canonical_layer/mappers.py`
## Текущий каталог semantic relations
| Context | Field role | Relation |
| --- | --- | --- |
| register | recorder | register_recorded_by_document |
| journal | ref | journal_refers_to_document |
| document | counterparty | document_has_counterparty |
| document | contract | document_has_contract |
| document | organization | document_belongs_to_organization |
| document | responsible | document_has_responsible |
| document | currency | document_has_currency |
| document | warehouse | document_has_warehouse |
| document | cashflow_article | document_has_cashflow_article |
| document | bank_account | document_has_bank_account |
| register | supplier | register_relates_to_supplier |
| register | buyer | register_relates_to_buyer |
| register | invoice | register_relates_to_invoice |
| register | contract | register_relates_to_contract |
| register | organization | register_relates_to_organization |
| register | account | register_relates_to_account |
| register | item | register_relates_to_item |
## Базовые правила извлечения ссылок
1. Поле попадает в link, если это `_Key`, `*ref`, GUID или semantic-поле (например `Recorder`, `СчетФактура`).
2. `*_Type` используется как приоритетная подсказка типа target-сущности.
3. Нулевые GUID (`00000000-...`) отфильтровываются из canonical links.
4. Если `source_id` отсутствует, строится составной `cmp:<sha1>` ключ.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,435 @@
# MVP PROD ARCH Report #3: Stage 1-7 (AS-IS + Runbook)
Дата фиксации: 2026-03-23
Проект: NDC 1C analytics bridge
Контур: локальный стенд Windows + 1С + Miniconda
Статус: **MVP контур Stage 1-7 поднят, Stage 7 частично (спецификация + API-роуты, без полного orchestration-сервиса в прод-режиме)**
---
## 1. Executive summary
На текущем этапе мы имеем рабочую многоуровневую read-only архитектуру:
`1C Source -> Runtime Bridge -> Refresh/Canonical Store -> Feature/Anomaly -> Risk -> API Layer`
Что подтверждено фактически:
1. Runtime bridge к живой 1С доказан и стабилен в read-only режиме.
2. Три жесткие бухгалтерские проверки закрыты как `PROVEN`.
3. Реализован и протестирован Layer 3/4: refresh + canonical store.
4. Реализован и протестирован Layer 5: feature/anomaly engine.
5. Реализован и протестирован Layer 6: risk engine.
6. По Layer 7 есть orchestration-spec и техническая поверхность API, но полноценный production-orchestrator (планировщик/роутер/политики исполнения) еще в roadmap.
---
## 2. Архитектура на текущем этапе
### 2.1 High-level
```text
1С (source of truth, read-only)
-> toolkit bridge / OData read
-> Refresh Engine (historical/incremental/targeted)
-> Canonical Store (entities + links + checkpoints + run logs)
-> Feature Engine (derived metrics + anomaly signals)
-> Risk Engine (domain patterns + global risk score)
-> FastAPI endpoints for integration with assistant/orchestrator
```
### 2.2 Runtime bridge (live)
Базовая связка:
- Proxy: `onec_mcp_toolkit_proxy` (`http://127.0.0.1:6003`)
- 1С обработка: `MCP_Toolkit.epf` в режиме `Прокси`
- Канал: long polling (`/1c/poll`, `/1c/result`)
Важно:
- Это **live request/response**, не snapshot-реплика.
- Для тяжелой аналитики используется выделенный store-слой, а не прямой проход LLM по всей базе 1С.
---
## 3. Stage-by-stage статус (1-7)
## Stage 1. Runtime bridge + guardrails
Статус: **DONE (MVP)**
Артефакты:
- `docs/ARCH/2 - architecture_stage_report_2026-03-23.md`
- `docs/toolkit_install_runbook.md`
- `docs/toolkit_smoke_test_report.md`
- `docs/toolkit_semantic_probe_report.md`
Ключевой результат:
- Подтвержден live read-only мост через `1c-mcp-toolkit`.
- Статус принятия: `adopt with restrictions`.
## Stage 2. Canonical schema
Статус: **DONE (MVP)**
Артефакт:
- `docs/accounting_canonical_schema.md`
Реализация:
- `canonical_entities`
- `canonical_links`
- JSON-атрибуты + нормализованные связи.
## Stage 3. Historical loader
Статус: **DONE (MVP)**
Артефакт:
- `docs/historical_load_plan.md`
Реализация:
- режим `historical` в `scripts/run_refresh.py`.
## Stage 4. Incremental refresh
Статус: **DONE (MVP)**
Артефакты:
- `docs/refresh_strategy.md`
- `docs/incremental_refresh_plan.md`
Реализация:
- режимы `incremental`, `targeted`
- таблицы `refresh_runs`, `refresh_checkpoints`
- run-status: `success/partial_success/failed`
## Stage 5. Feature / anomaly engine
Статус: **DONE (MVP)**
Артефакты:
- `docs/analytics_store_design.md`
- `docs/anomaly_engine_spec.md`
Реализация:
- `feature_runs`
- `feature_metrics`
- `anomaly_signals`
- API: `/features/*`
## Stage 6. Risk engine
Статус: **DONE (MVP)**
Артефакт:
- `docs/risk_engine_spec.md`
Реализация:
- `risk_runs`
- `risk_patterns`
- domain-level risk patterns
- `global_risk_summary`
- API: `/risk/*`
## Stage 7. Assistant orchestration
Статус: **PARTIAL (spec + API-ready surface)**
Артефакты:
- `docs/assistant_orchestration_spec.md`
- `docs/security_guardrails_readonly.md`
Состояние:
- спецификация маршрутизации готова;
- runtime endpoints для refresh/features/risk готовы;
- полноценный orchestration-service с scheduler/policy execution — **еще не реализован как отдельный прод-компонент**.
---
## 4. Deliverables check (архитектурный комплект)
На дату 2026-03-23 все 8 целевых deliverables присутствуют в `docs/`:
1. `accounting_canonical_schema.md`
2. `analytics_store_design.md`
3. `refresh_strategy.md`
4. `historical_load_plan.md`
5. `incremental_refresh_plan.md`
6. `anomaly_engine_spec.md`
7. `assistant_orchestration_spec.md`
8. `security_guardrails_readonly.md`
---
## 5. Текущее окружение и зависимости
## 5.1 ОС и инструменты
- Windows (локальный стенд)
- 1С платформа: `8.3.27.1936`
- База: Бухгалтерия предприятия 2.0 (лабораторный контур)
- Python: `3.12.10` (технический интерпретатор машины)
- Miniconda env проекта: `ndc_1c_mvp`
- Miniconda env toolkit proxy: `ndc_1c_toolkit` (для bridge-контура)
## 5.2 Python зависимости проекта
`requirements.txt`:
- `fastapi>=0.116.0`
- `odata1cw>=0.0.4`
- `pydantic>=2.11.0`
- `pytest>=8.3.5`
- `python-dotenv>=1.1.0`
- `requests>=2.32.0`
- `SQLAlchemy>=2.0.38`
- `uvicorn>=0.35.0`
## 5.3 Ключевые env-параметры
Из `.env.example`:
- `CANONICAL_DB_URL=sqlite:///X:/1C/NDC_1C/data/canonical_store.db`
- `REFRESH_DEFAULT_LIMIT_PER_SET=200`
- `FEATURE_BASELINE_WINDOW_HOURS=24`
- `ANOMALY_STALE_REFRESH_THRESHOLD_HOURS=6`
- `FEATURE_ENTITY_SCAN_LIMIT=200000`
- `RISK_MEDIUM_THRESHOLD=0.45`
- `RISK_HIGH_THRESHOLD=0.75`
- `RISK_ANOMALY_SCAN_LIMIT=5000`
---
## 6. Полный запуск системы с нуля (на новой машине)
## 6.1 Bootstrap bridge-контура (1С + proxy)
1. Установить 1С платформу и подготовить тестовую базу (read-only пользователь).
2. Установить Miniconda и Git.
3. Подготовить toolkit и env:
```powershell
git clone https://github.com/ROCTUP/1c-mcp-toolkit X:\1C\NDC_1C\external\1c-mcp-toolkit
& 'C:\Users\<USER>\miniconda3\Scripts\conda.exe' create -y -n ndc_1c_toolkit python=3.11
& 'C:\Users\<USER>\miniconda3\envs\ndc_1c_toolkit\python.exe' -m pip install -r X:\1C\NDC_1C\external\1c-mcp-toolkit\requirements.txt
```
4. Запустить proxy:
```powershell
$env:PORT='6003'
$env:TIMEOUT='180'
$env:ALLOW_DANGEROUS_WITH_APPROVAL='false'
$env:ANONYMIZATION_ENABLED='false'
$env:RESPONSE_FORMAT='json'
$env:LOG_LEVEL='INFO'
& 'C:\Users\<USER>\miniconda3\envs\ndc_1c_toolkit\python.exe' -m onec_mcp_toolkit_proxy
```
5. Проверить health:
```powershell
Invoke-WebRequest http://127.0.0.1:6003/health -UseBasicParsing
```
6. В 1С:Предприятии открыть:
`X:\1C\NDC_1C\external\1c-mcp-toolkit\build\MCP_Toolkit.epf`
7. В форме выставить:
- режим: `Прокси`
- сервер: `http://127.0.0.1:6003`
- channel: `default`
- нажать `Подключиться`
## 6.2 Bootstrap аналитического контура (NDC_1C)
1. Подготовить проектную среду:
```powershell
cd X:\1C\NDC_1C
& 'C:\Users\<USER>\miniconda3\Scripts\conda.exe' create -y -n ndc_1c_mvp python=3.11
& 'C:\Users\<USER>\miniconda3\envs\ndc_1c_mvp\python.exe' -m pip install -r requirements.txt
copy .env.example .env
```
2. Настроить `.env` (минимум: `ONEC_INFOBASE`, `ONEC_USERNAME`, `ONEC_PASSWORD`).
3. Запустить API:
```powershell
& 'C:\Users\<USER>\miniconda3\envs\ndc_1c_mvp\python.exe' -m uvicorn canonical_layer.app:app --host 127.0.0.1 --port 8000 --reload
```
4. Запустить data-pipeline:
```powershell
powershell -ExecutionPolicy Bypass -File .\scripts\run_refresh.ps1 -Mode incremental
powershell -ExecutionPolicy Bypass -File .\scripts\run_features.ps1 -Strict
powershell -ExecutionPolicy Bypass -File .\scripts\run_risk.ps1 -Strict
```
---
## 7. Операционный runbook (ежедневный цикл)
Старт:
1. Поднять bridge proxy и проверить `/health`.
2. Подключить `MCP_Toolkit.epf` в 1С.
3. Выполнить refresh -> features -> risk.
4. Проверить API/хранилища:
- `/store/stats`
- `/features/stats`
- `/risk/stats`
Стоп:
1. Отключить toolkit в 1С.
2. Закрыть 1С.
3. Остановить proxy/API (Ctrl+C).
---
## 8. Реально полученные результаты (фактические прогоны)
Ниже — выдержки из последних реальных прогонов (`logs/*.json`) на дату отчета.
## 8.1 Refresh (incremental)
Источник: `logs/refresh_last_run.json`
```json
{
"run_id": "7414d9b93c964b589bb863952a027f5e",
"mode": "incremental",
"status": "success",
"records_read": 4,
"entities_written": 4,
"links_written": 10,
"checkpoints_updated": 2
}
```
## 8.2 Feature engine
Источник: `logs/features_last_run.json`
```json
{
"run_id": "8c311e26916146579e97e110b2243a34",
"status": "success",
"entities_total": 2,
"metrics_written": 19,
"anomalies_written": 0
}
```
## 8.3 Risk engine
Источник: `logs/risk_last_run.json`
```json
{
"run_id": "fbad845fdfd64de8a73e35d4da6274e4",
"status": "success",
"patterns_written": 1,
"global_score": 0.05,
"risk_patterns": [
{
"pattern_key": "global_risk_summary",
"severity": "low"
}
]
}
```
## 8.4 Текущее состояние SQLite store (факт)
Снимок на момент отчета:
- `canonical_store.db` существует, размер `86016` байт
- `canonical_entities=2`
- `canonical_links=10`
- `refresh_runs=3`
- `refresh_checkpoints=2`
- `feature_runs=3`
- `feature_metrics=61`
- `anomaly_signals=0`
- `risk_runs=1`
- `risk_patterns=1`
## 8.5 Тестовый статус
Фактический запуск:
```text
python -m pytest -q
....... [100%]
7 passed in 1.86s
```
---
## 9. API-поверхность на текущем этапе
Реализованные endpoints:
- `GET /health`
- `GET /metadata/entity-sets`
- `GET /documents`
- `GET /documents/{document_id}`
- `GET /postings`
- `GET /counterparties/{counterparty_id}/documents`
- `GET /graph/document/{document_id}`
- `GET /store/stats`
- `GET /refresh/runs`
- `POST /refresh/run`
- `GET /features/stats`
- `GET /features/runs`
- `GET /features/metrics`
- `GET /features/anomalies`
- `POST /features/run`
- `GET /risk/stats`
- `GET /risk/runs`
- `GET /risk/patterns`
- `POST /risk/run`
---
## 10. Ограничения и интерпретация текущих цифр
1. Низкий текущий риск (`global_score=0.05`) и отсутствие аномалий отражают **малый текущий объем данных в store**, а не финальную “безрисковость” компании.
2. Stage 7 закрыт документно и по API-ready поверхности, но production-orchestration (policy engine + scheduler + retry orchestration) еще нужно реализовать отдельным сервисом.
3. SQLite используется как MVP-носитель; для прод-контуров нужен PostgreSQL + миграции + эксплуатационные политики.
---
## 11. Что осталось до “MVP production hardening”
1. Реализовать полноценный orchestration-service (Stage 7 runtime).
2. Добавить планировщик и регламенты (`refresh/features/risk`) с retry и алертингом.
3. Вынести store на PostgreSQL, добавить индексы/миграции.
4. Усилить auth/секреты/сетевые ограничения API.
5. Провести калибровку risk/feature правил на расширенном реальном срезе данных.
---
## 12. Итог
На дату **2026-03-23** архитектурный MVP-контур **Stage 1-7** зафиксирован как:
- Stage 1-6: реализованы и подтверждены фактическими прогонами;
- Stage 7: реализован на уровне спецификации и API-операций, но требует выделенного runtime orchestration для production-ready режима.
Проект технически готов к следующему шагу: **production-hardening + orchestration implementation**.

View File

@ -0,0 +1,319 @@
# AI First Layer GUI — Подробный гайд пользователя
Дата: 23.03.2026
Контур: `X:\1C\NDC_1C\llm_normalizer`
Назначение: локальный GUI и backend для нормализации бухгалтерских запросов через OpenAI token
---
## 1. Что это за система
`AI First Layer GUI` — это не чат-бот с финальными бухгалтерскими ответами.
Это **semantic front-end**: слой, который переводит живой запрос бухгалтера в строгий структурированный JSON (`normalized_query_v1`), чтобы дальше этот JSON использовал deterministic router/оркестрация.
Цепочка:
`Пользователь -> GUI -> backend-proxy -> OpenAI Responses API -> normalized JSON -> route_hint summary -> trace/history`
---
## 2. Из чего состоит система
1. `frontend` (React + TypeScript + Vite) — русифицированный UI.
2. `backend` (Node.js + Express) — прокси к OpenAI, валидация схемы, trace/eval.
3. `data/traces` — история нормализаций.
4. `data/presets` — сохраненные prompt-пресеты.
5. `data/eval_cases` — eval-кейсы и отчеты.
---
## 3. Быстрый запуск
### 3.1 Рекомендуемый способ (из одной папки в VS Code)
Открой папку:
`X:\1C\NDC_1C\llm_normalizer`
Дальше:
1. `Terminal -> Run Task -> NDC: Install All` (первый запуск).
2. `Terminal -> Run Task -> NDC: Dev All (Backend + Frontend)`.
Открой:
- GUI: `http://localhost:5174`
- Backend health: `http://localhost:8787/api/health`
### 3.2 Терминалом (без Tasks)
```powershell
cd X:\1C\NDC_1C\llm_normalizer
start-dev.cmd
```
или:
```powershell
cd X:\1C\NDC_1C\llm_normalizer
npm.cmd run dev:all
```
---
## 4. Главный сценарий использования
1. В панели **Подключение OpenAI** вставь API key и проверь связь.
2. В панели **Prompt Manager** выбери/подгрузи preset или отредактируй prompt-ы вручную.
3. В панели **Запрос пользователя** вставь живой бухгалтерский вопрос.
4. Нажми `Normalize`.
5. Проверь результат во вкладках `Normalized JSON`, `Route hint summary`, `Validation`.
6. При необходимости сохрани кейс: `Normalize + Save as test case`.
7. Открой историю и сравни trace между попытками.
---
## 5. Подробно по каждому блоку GUI
## 5.1 Connection Panel (Подключение OpenAI)
### Поля
| Поле | Что вводить | Для чего |
|---|---|---|
| `OpenAI API Key` | реальный ключ формата `sk-...` | backend использует ключ для вызова Responses API |
| `Model ID` | обычно `gpt-4o-mini` | модель normalizer-а |
| `Base URL` | обычно `https://api.openai.com/v1` | endpoint OpenAI API |
| `Temperature` | чаще `0` или `0.1` | стабильность/вариативность нормализации |
| `Max output tokens` | обычно `500-900` | лимит длины ответа модели |
### Кнопки
| Кнопка | Что делает |
|---|---|
| `Сохранить локальную конфигурацию` | сохраняет в localStorage только model/baseUrl/temperature/maxOutputTokens (без API key) |
| `Проверить подключение` | вызывает `POST /api/openai/test-connection`, проверяет доступ к модели |
### Рекомендация на старт
- `Model ID`: `gpt-4o-mini`
- `Temperature`: `0`
- `Max output tokens`: `700`
---
## 5.2 Prompt Manager
Это управляемая prompt-архитектура из 3 уровней + служебные поля.
### Поля
| Поле | Что писать | Практический смысл |
|---|---|---|
| `Системный prompt` | жесткие правила поведения модели | запрещает модели давать финальный бизнес-ответ |
| `Developer / Instruction prompt` | правила классификации, flags, route_hint | задает “инженерную логику” нормализации |
| `Domain prompt` | словарь бухгалтерских формулировок/счетов/паттернов | повышает доменную точность |
| `Schema notes` | ограничения схемы, важные enum/required | снижает риск невалидного JSON |
| `Few-shot examples` | пары “вопрос -> ожидаемый JSON фрагмент” | стабилизирует поведение на сложных формулировках |
### Управление пресетами
| Элемент | Что делает |
|---|---|
| dropdown `Выберите preset` | список сохраненных пресетов + default |
| `Загрузить preset` | подставляет выбранные prompt-ы в поля |
| `Сохранить preset` | сохраняет текущие prompt-ы в `data/presets` |
| `Diff с предыдущим` | показывает текстовую дельту относительно последнего загруженного |
| `Сбросить к default` | возвращает дефолтный набор prompt-ов |
---
## 5.3 User Query Panel (Запрос пользователя)
### Поля
| Поле | Что вводить | Для чего |
|---|---|---|
| `Raw user question` | живой вопрос бухгалтера “как есть” | основной вход normalizer-а |
| `Optional period context` | период, если хочешь явно подсказать (`2020-06`) | стабилизирует `period_scope` |
| `Optional business context` | краткая доп. рамка (например “предзакрытие июня”) | помогает интерпретации |
| `Optional expected route` | ожидаемый route для проверки | используется в eval/trace |
### Переключатель
| Переключатель | Когда использовать |
|---|---|
| `Mock-режим (без вызова OpenAI)` | когда тестируешь UI/поток без токена и без внешних запросов |
### Кнопки
| Кнопка | Что делает |
|---|---|
| `Normalize` | запускает нормализацию и возвращает structured output |
| `Normalize + Save as test case` | дополнительно сохраняет кейс в `data/eval_cases` |
---
## 5.4 Output Panel (вкладки результата)
| Вкладка | Что показывает | Как использовать |
|---|---|---|
| `Normalized JSON` | итоговый валидированный JSON | основной артефакт для router |
| `Raw model output` | сырой ответ модели | диагностика prompt/schema проблем |
| `Route hint summary` | краткий срез intent/route/flags | быстрый контроль маршрутизации |
| `Validation` | статус schema validation и ошибки | сразу видно валиден ли контракт |
| `Logs` | клиентские события UI | оперативная диагностика шага |
---
## 5.5 Runtime Metrics Panel
Показывает:
- `trace_id`
- `request_started_at`
- `request_finished_at`
- `latency_ms`
- `input_tokens / output_tokens / total_tokens`
- `validation_status`
- `prompt_version`
- `schema_version`
Как читать:
1. `validation_status = passed` — можно использовать JSON в downstream пайплайне.
2. Если latency резко растет — обычно слишком длинный prompt/few-shot.
3. `total_tokens` нужен для контроля стоимости.
---
## 5.6 History Panel
Показывает список прошлых нормализаций:
- короткий вопрос,
- route hint,
- validation pass/fail,
- модель,
- timestamp.
Клик по записи открывает полный trace (`GET /api/history/:trace_id`) и подгружает данные в Output.
---
## 5.7 NDC Run Monitor
Это отдельный совместимый слой под будущую интеграцию в `dc_node`.
Что можно делать:
1. `Запустить run` -> `POST /api/accounting-agent/v1/runs/start`
2. `Завершить выбранный run` -> `POST /api/accounting-agent/v1/runs/finish`
3. Смотреть список `runs`, их статусы и trace выбранного `runId`.
Канон статусов:
`NONE`, `QUEUED`, `RUNNING`, `DONE`, `ERROR`, `STALE`, `CANCELLED`
---
## 6. Что и где сохраняется
| Данные | Где |
|---|---|
| Traces нормализации | `X:\1C\NDC_1C\llm_normalizer\data\traces` |
| Prompt presets | `X:\1C\NDC_1C\llm_normalizer\data\presets` |
| Eval cases / reports | `X:\1C\NDC_1C\llm_normalizer\data\eval_cases` |
Важно:
1. API key не сохраняется в localStorage.
2. API key не возвращается в frontend-ответах.
3. В trace ключ редактируется (redacted).
---
## 7. Как правильно заполнять поля на практике
## 7.1 Минимальный рабочий набор
1. `OpenAI API Key`: вставить валидный ключ.
2. `Model ID`: `gpt-4o-mini`.
3. `Raw user question`: живой вопрос.
4. Остальные поля оставить по default.
5. Нажать `Normalize`.
## 7.2 Если много ошибок в Validation
1. Уменьши свободу модели: `Temperature = 0`.
2. Уточни `Developer prompt` и `Schema notes`.
3. Добавь few-shot примеры под твой класс вопросов.
4. Повтори `Normalize` и сравни через `History`.
## 7.3 Если route_hint “плывет”
1. Явно добавь `expected route` в Query Panel.
2. В domain/developer prompt пропиши контр-примеры.
3. Сохрани новый preset и гони серию через `POST /api/eval/run`.
---
## 8. Частые проблемы и решения
| Симптом | Причина | Что делать |
|---|---|---|
| `Ошибка подключения` | ключ неверный/ограничен, неверный base URL | проверить key, model, URL через `Test connection` |
| `validation.passed = false` | модель вернула JSON вне схемы | ужесточить prompt, проверить schema notes, повторить |
| пустой `Normalized JSON` | parse/validation fail | смотреть `Raw model output` + `Validation` |
| высокая latency | слишком тяжелые prompt/few-shot | сократить prompt и few-shot |
| VS Code не стартует npm | PowerShell policy в Windows | использовать `npm.cmd` и готовые Tasks |
---
## 9. API-карта (для интеграции)
Normalizer:
- `POST /api/openai/test-connection`
- `POST /api/normalize`
- `POST /api/eval/run`
- `GET /api/history`
- `GET /api/history/:trace_id`
- `POST /api/presets/save`
- `GET /api/presets`
NDC Integration namespace:
- `POST /api/accounting-agent/v1/runs/start`
- `POST /api/accounting-agent/v1/runs/finish`
- `GET /api/accounting-agent/v1/runs`
- `GET /api/accounting-agent/v1/runs/:runId`
- `POST /api/accounting-agent/v1/tasks/enqueue`
- `POST /api/accounting-agent/v1/tasks/claim`
- `POST /api/accounting-agent/v1/tasks/:taskId/complete`
- `POST /api/accounting-agent/v1/tasks/:taskId/error`
- `GET /api/accounting-agent/v1/results`
- `GET /api/accounting-agent/v1/trace/run/:runId`
- `GET /api/accounting-agent/v1/health`
---
## 10. Чек-лист перед рабочей сессией
1. Backend и frontend подняты.
2. `Test connection` успешен.
3. Выбран корректный preset.
4. Запрос содержит минимум необходимого контекста (вопрос + период при необходимости).
5. После `Normalize` проверен `Validation`.
6. Trace сохранен и виден в History.
---
## 11. Главное правило эксплуатации
`LLM в этом контуре — нормализатор, а не исполнитель бизнес-логики.`
То есть:
1. LLM структурирует, классифицирует, предлагает route hint.
2. Финальные бухгалтерские выводы и оркестрация остаются в основном контуре NDC.

View File

@ -0,0 +1,444 @@
# 5 - Assistant Mode Architecture Report (2026-03-24)
## 1. Статус этапа
Дата фиксации: **24 марта 2026**.
На текущем этапе реализован рабочий `Assistant Mode` поверх существующего `Decomposition` контура:
- decomposition/debug режим сохранен и не удален;
- добавлен отдельный backend endpoint для assistant-loop;
- добавлен слой `answer_composer` (human-readable ответ);
- добавлена session-scoped история диалога (in-memory);
- добавлен debug drawer в GUI по каждому assistant ответу;
- единый pipeline нормализации/маршрутизации используется для обоих режимов.
---
## 2. Что входит в текущую архитектуру
## 2.1 Backend
Ключевые узлы:
- `llm_normalizer/backend/src/routes/assistant.ts`
- `llm_normalizer/backend/src/services/assistantService.ts`
- `llm_normalizer/backend/src/services/answerComposer.ts`
- `llm_normalizer/backend/src/services/assistantSessionStore.ts`
- `llm_normalizer/backend/src/types/assistant.ts`
- `llm_normalizer/backend/src/services/routeHintAdapter.ts` (общий deterministic routing)
- `llm_normalizer/backend/src/services/normalizerService.ts` (общий normalizer pipeline)
Подключение в сервер:
- `llm_normalizer/backend/src/server.ts`
- `llm_normalizer/backend/src/serverContext.ts`
## 2.2 Frontend
Ключевые узлы:
- `llm_normalizer/frontend/src/App.tsx` (mode switch + orchestration)
- `llm_normalizer/frontend/src/components/AssistantPanel.tsx`
- `llm_normalizer/frontend/src/api/client.ts` (assistant API methods)
- `llm_normalizer/frontend/src/state/types.ts` (assistant типы состояния)
- `llm_normalizer/frontend/src/styles.css` (assistant/mode switch UI стиль)
---
## 3. Backend: функциональная архитектура
## 3.1 Endpointы
### 3.1.1 POST `/api/assistant/message`
Назначение:
- принять user message;
- прогнать через normalizer pipeline;
- определить route/fallback;
- собрать human-readable assistant reply;
- вернуть reply + debug + session conversation snapshot.
Проверки:
- `user_message` обязателен;
- при пустом сообщении возвращается `INVALID_ASSISTANT_MESSAGE` (HTTP 400).
### 3.1.2 GET `/api/assistant/session/:session_id`
Назначение:
- вернуть текущую историю сессии из in-memory store.
Поведение:
- если сессия отсутствует: `ASSISTANT_SESSION_NOT_FOUND` (HTTP 404).
---
## 3.2 Контракт данных Assistant Mode
Типы заданы в:
- `llm_normalizer/backend/src/types/assistant.ts`
Ключевые сущности:
- `AssistantMessageRequestPayload`
- `AssistantDebugPayload`
- `AssistantConversationItem`
- `AssistantMessageResponsePayload`
`fallback_type` (жестко зафиксированный набор):
- `none`
- `out_of_scope`
- `clarification`
- `partial`
- `unknown`
---
## 3.3 Session memory
Реализовано в:
- `llm_normalizer/backend/src/services/assistantSessionStore.ts`
Характеристики:
- хранилище: in-memory `Map<session_id, session_state>`;
- auto-create сессии при первом сообщении;
- ограничение длины: `MAX_ITEMS_PER_SESSION = 200`;
- хранение только в рамках текущего backend процесса;
- при рестарте backend память очищается.
Это deliberate решение текущего этапа (sandbox/stage), без persistent storage.
---
## 3.4 Assistant pipeline (внутренний flow)
Реализовано в:
- `llm_normalizer/backend/src/services/assistantService.ts`
Порядок выполнения:
1. `ensureSession` -> получаем/создаем `session_id`.
2. Сохраняем user message как conversation item (`role=user`).
3. Формируем `NormalizeRequestPayload` с:
- `promptVersion` по умолчанию `normalizer_v2_0_2`,
- connection/prompt/context/useMock из запроса.
4. Вызываем `normalizerService.normalize(...)`.
5. По `route_hint_summary` строим retrieval plan (`buildRetrievalPlan`).
6. Передаем данные в `composeAssistantAnswer(...)`.
7. Формируем `debug` payload:
- `trace_id`,
- `route_summary`,
- `fragments`,
- `retrieval`,
- `normalized`.
8. Сохраняем assistant message как conversation item (`role=assistant`).
9. Логируем structured event `assistant_message_processed`.
10. Возвращаем:
- `assistant_reply`,
- `conversation_item`,
- `debug`,
- `conversation`.
---
## 3.5 Routing rules (общий deterministic v2 engine)
Основные правила маршрутизации берутся из:
- `llm_normalizer/backend/src/services/routeHintAdapter.ts`
Правила выбора маршрута:
1. `live_mcp_drilldown`
- если `asks_for_exact_object_trace = true`.
2. `batch_refresh_then_store`
- если `asks_for_ranking_or_top = true` **или** `asks_for_period_summary = true`.
3. `hybrid_store_plus_live`
- если `has_multi_entity_scope = true` и `asks_for_chain_explanation = true`.
4. `store_feature_risk`
- если `asks_for_rule_check = true` и не chain;
- также anomaly path: `asks_for_anomaly_scan = true` без ranking и без multi-entity chain.
5. `store_canonical`
- default routed путь для in-scope, если нет более сильного сигнала.
6. `no_route`
- если fragment out-of-scope / insufficient specificity / missing mapping / unsupported fragment type.
Fallback type в summary:
- `out_of_scope` — сообщение вне контура;
- `clarification` — нет routable in-scope fragmentов из-за недоспецификации;
- `partial` — часть in-scope/routed, часть no-route/out-of-scope;
- `none` — все ок для текущего контура.
---
## 3.6 Answer composer rules
Реализовано в:
- `llm_normalizer/backend/src/services/answerComposer.ts`
Логика:
1. `out_of_scope`
- вежливый boundary response (работа только по company-specific accounting contour).
2. `clarification`
- конкретный уточняющий ответ: период/счет/документ/контрагент.
3. `partial`
- сообщает, что обработана только часть запроса;
- выводит routed части;
- явно фиксирует sandbox retrieval mode.
4. `none`
- human-readable summary с перечислением planned routes.
5. `unknown`
- защитный fallback, если routed items не сформированы.
Важно:
- на этом этапе composer формирует **человеко-читаемый operational ответ**;
- это не финальный production-grade semantic answer over full live retrieval.
---
## 3.7 Retrieval слой (текущий статус)
Текущее состояние: **stubbed / sandbox retrieval plan**.
Что есть:
- генерация плана “что и по какому route исполнять”;
- диагностический payload по fragmentам.
Чего пока нет:
- боевой deep retrieval из 1С по всем route;
- гарантированного factual grounding для каждого ответа assistant mode.
---
## 3.8 Логирование и трассировка
В `assistantService` пишется structured log c событием:
- `assistant_message_processed`
Поля:
- `session_id`
- `message_id`
- `user_message`
- `normalizer_output`
- `resolved_execution_state`
- `routes`
- `fallback_type`
- `retrieval_payloads`
- `assistant_reply`
- `trace_id`
Это дает базу для будущего field-eval hardening.
---
## 4. Frontend: функциональная архитектура
## 4.1 Режимы UI
Реализовано в:
- `llm_normalizer/frontend/src/App.tsx`
Есть явный переключатель:
- `Assistant`
- `Decomposition`
Поведение:
- backend pipeline общий;
- UI-представление разное;
- decomposition stack не ломается и остается доступным.
---
## 4.2 Assistant Mode UI состав
Реализовано в:
- `llm_normalizer/frontend/src/components/AssistantPanel.tsx`
Элементы:
1. Chat timeline:
- user/assistant messages,
- timestamp,
- trace id для assistant сообщений.
2. Input зона:
- поле сообщения,
- send,
- reset session.
3. Контекст:
- `periodHint`
- `businessContext`
4. Toggle:
- `useMock`.
5. Debug drawer:
- раскрывается per assistant message,
- показывает raw debug JSON (`trace/fragments/routes/fallback/retrieval/normalized`).
---
## 4.3 Pipeline progress UX
Во время обработки показывается этапный status ticker:
1. `Razbirayu zapros`
2. `Proveryayu kontur`
3. `Opredelyayu marshrut`
4. `Ishchu dannye`
5. `Sobirayu otvet`
Цель: убрать ощущение “зависло” и визуализировать pipeline.
---
## 4.4 Frontend state flow
Ключевые state переменные:
- `uiMode`
- `assistantSessionId`
- `assistantConversation`
- `assistantInput`
- `assistantBusy`
- `assistantStatus`
- `assistantError`
Flow отправки:
1. optimistic append user message в chat;
2. запуск status ticker;
3. вызов `apiClient.sendAssistantMessage(...)`;
4. обновление `session_id` и полной conversation с backend;
5. остановка ticker + финальный статус.
---
## 4.5 API client для Assistant
Реализовано в:
- `llm_normalizer/frontend/src/api/client.ts`
Добавлены методы:
- `sendAssistantMessage(...)` -> `POST /api/assistant/message`
- `loadAssistantSession(sessionId)` -> `GET /api/assistant/session/:id`
---
## 5. Что сделано по требованиям ТЗ (mapping)
1. `docs/assistant_mode_spec.md` — выполнено
2. GUI с переключателем `Assistant` / `Decomposition` — выполнено
3. backend endpoint assistant loop — выполнено
4. `answer_composer` слой — выполнено
5. session-based chat history — выполнено (in-memory)
6. debug drawer/expandable technical view — выполнено
7. `docs/assistant_mode_flow.md` — выполнено
8. `docs/known_limits_before_field_eval.md` — выполнено
---
## 6. Критические ограничения текущей реализации
1. **retrieval sandbox/stubbed**
- assistant выдает план/маршрут, не full factual extraction по всем route.
2. **session memory volatile**
- хранится только в памяти backend процесса.
3. **нет production hardening**
- auth/tenancy/persistence/SLO не включены.
4. **composer базовый**
- достаточен для MVP loop, но не финальный policy-grade layer.
---
## 7. Запуск и проверка на новой машине (текущий этап)
## 7.1 Backend
```powershell
cd X:\1C\NDC_1C\llm_normalizer\backend
npm install
npm run dev
```
## 7.2 Frontend
```powershell
cd X:\1C\NDC_1C\llm_normalizer\frontend
npm install
npm run dev
```
## 7.3 Открыть GUI
- `http://localhost:5174`
## 7.4 Smoke test Assistant Mode
1. Переключить mode -> `Assistant`.
2. Ввести сообщение в чат.
3. Нажать `Send`.
4. Проверить:
- появился assistant reply;
- появился trace id;
- открывается debug drawer;
- session сохраняет историю.
---
## 8. Тестовый статус к моменту фиксации
Проверки выполнены 24.03.2026:
- backend tests: `npm test` -> **21 passed**
- backend build: `npm run build` -> **OK**
- frontend build: `npm run build` -> **OK**
Также добавлен endpoint test:
- `llm_normalizer/backend/tests/assistantEndpoint.test.ts`
Покрывает:
- успешный `POST /api/assistant/message`;
- session continuity;
- `GET /api/assistant/session/:session_id`.
---
## 9. Архитектурный итог этапа
Текущий Assistant Mode — это уже **usable dialog loop**:
- user-friendly вход (чат),
- deterministic decomposition/routing ядро,
- единый backend pipeline,
- прозрачная debug-плоскость для инженерной диагностики,
- session-based continuity в рамках процесса.
Для перехода в следующий уровень (field-hardened assistant) нужен следующий блок:
- подключение route-specific factual retrieval,
- сбор 3040 реальных полевых запросов,
- policy hardening по traces (clarification/no-route/partial quality).

View File

@ -0,0 +1,679 @@
# Assistant Mode Global Status Report
Date: 2026-03-24
Scope: `llm_normalizer` + Assistant Mode pipeline + retrieval/explainability contours
Prepared for: architectural checkpoint and next-step planning
## 0) Executive Summary
Current system is no longer a raw route demo: we now have a working end-to-end assistant loop with decomposition, routing, retrieval, grounding, explainable response shaping, session logging, and regression tests.
At the same time, the system is still not a full accountant-grade investigation assistant. Main reason: data/model/retrieval unit depth is still below causal accounting reasoning depth in several domains.
Key status snapshot:
- Backend build/tests: `tsc` OK, `vitest` OK (`25/25` tests passed).
- Explainable contract: implemented (`requirements`, `coverage_report`, `answer_grounding_check`, explainable reply sections).
- Retrieval-layer upgrade: `executeHybrid` moved from `GUID-or-full-scan` to semantic profile + semantic narrowing.
- Proven narrowing example: for bank mismatch query with accounts `51/60`, narrowing reduced records from `262` to `75`.
- Proven limitation: for generic cross-entity chain query without explicit account scope, narrowing still wide (`262` to `242`), so answer quality can remain too broad.
---
## 1) Data Contour
### 1.1 How it works now
- Assistant retrieval reads local snapshot bundle from `docs/ARCH/2020экспорт`.
- Main files currently loaded in executor:
- `03_snapshot_fragment_problem_cases.json`
- `04_samples_SpisanieSRaschetnogoScheta.json`
- `05_samples_RealizaciyaTovarovUslug.json`
- `06_samples_PostuplenieTovarovUslug.json`
- `07_samples_DocumentJournals.json`
- `08_samples_NDS_registers.json`
- `09_samples_key_fields_Recorder_Ref_Supplier_Buyer_Responsible.json`
- Data access is read-only snapshot, not live 1C state.
### 1.2 What works
- Documents/journals/register records are available with links and key attributes.
- Counterparty/document linkage and part of relation topology are usable.
- Enough depth exists for POC-level chain/risk analysis and explainable evidence pack.
### 1.3 Constraints
- Live truth is absent in assistant retrieval path (snapshot-only).
- Lifecycle/status semantics are incomplete and partly heuristic.
- Some accounting contexts are represented as flattened fields instead of normalized graph nodes.
### 1.4 What assistant cannot do because of this
- Guarantee real-time explanation of current accounting state.
- Reliably prove deep causal accounting chains in all domains (especially where lifecycle semantics are implicit).
### 1.5 Symptoms already seen in dialogs
- Repeated top entities across semantically different but broad queries.
- “Looks relevant” answers with weak differentiating evidence for some generic prompts.
### 1.6 Local changes needed
- Add richer field extraction/parsing from snapshot for account/document/lifecycle signals.
- Enforce tighter domain-specific filters for low-specificity queries.
### 1.7 Architectural changes needed
- Add live data bridge layer for on-demand truth check (hybrid snapshot + live drilldown).
- Add normalized accounting graph storage layer for causal traversal.
### 1.8 Priority
- `P0`: stronger retrieval constraints and lifecycle signal extraction.
- `P1`: live bridge for targeted verification.
- `P2`: full graph-backed data model.
---
## 2) Ontology / Domain Model Contour
### 2.1 How it works now
- Entity/relation semantics exist as retrieval profile vocabulary plus heuristic signal extraction.
- Domain labels include bank/suppliers/customers/VAT/fixed_assets/deferred_expense/period_close/settlements.
- Relation patterns include:
- `payment_to_settlement`
- `document_to_posting`
- `statement_to_document`
- `asset_card_to_depreciation`
- `deferred_expense_to_writeoff`
- `invoice_to_vat`
- `contract_to_documents`
- `receipt_to_stock_movement`
### 2.2 What works
- Query intent can be translated into semantic retrieval profile.
- Basic anomaly vocabulary exists and affects ranking/explanation.
### 2.3 Constraints
- No explicit ontology graph engine with typed nodes/edges and reasoning rules.
- Lifecycle model is heuristic (`created/posted/partially_linked/no_continuation/period_boundary`) rather than formal accounting state machine.
### 2.4 What assistant cannot do because of this
- Stable causal proofs for complex cross-domain reconciliation.
- Deterministic explanation of “why exactly this stage is broken” across all domains.
### 2.5 Symptoms
- Explanation can still be structurally correct but semantically generic.
- Retrieval unit can still drift toward “counterparty-heavy” answer shape.
### 2.6 Local changes needed
- Expand structured anomaly dictionary with accountant-facing defect classes.
- Promote lifecycle markers from heuristics to explicit modeled states where possible.
### 2.7 Architectural changes needed
- Build ontology/lifecycle core as first-class subsystem.
- Move from “labels on records” to “typed causal nodes and edges”.
### 2.8 Priority
- `P0`: anomaly taxonomy hardening + lifecycle schema hardening.
- `P1`: typed ontology graph.
- `P2`: rule engine over ontology.
---
## 3) Retrieval / Query Execution Contour
### 3.1 How it works now
- Deterministic routed executors:
- `store_feature_risk`
- `hybrid_store_plus_live`
- `batch_refresh_then_store`
- `store_canonical`
- `live_mcp_drilldown`
- `executeHybrid` now uses `semantic_retrieval_profile` and semantic narrowing when GUID is absent.
- Retrieval result now carries richer context in items and summary (`query_subject`, profile, ranking basis, narrowing metrics).
### 3.2 What works
- No hard fallback to pure full scan in hybrid path for non-GUID queries.
- Query with explicit accounting scope (`51/60`, wrong document closure) produces stronger narrowing and different ranking.
- Evidence pack is richer and usable by explainable answer layer.
### 3.3 Constraints
- Generic prompts without explicit scope can still produce wide narrowed sets.
- Retrieval top unit still often converges to counterparty-centric grouping.
- Not all routes have equal semantic depth.
### 3.4 What assistant cannot do because of this
- Consistently deliver problem-node-first output in every query class.
- Guarantee high differentiation for all semantically close prompts.
### 3.5 Symptoms
- For some queries, narrowing reduction is still modest (example `262 -> 242`).
- Answers can remain “good but broad”.
### 3.6 Local changes needed
- Tighten mandatory intersections for generic bank/cross-entity prompts.
- Add domain-specific minimum evidence thresholds before final top ranking.
### 3.7 Architectural changes needed
- Introduce explicit “problem cluster” retrieval unit.
- Add cross-branch retrieval policy (neighbor contour checks).
### 3.8 Priority
- `P0`: further narrowing hardening + anti-generic ranking guards.
- `P1`: problem-cluster retrieval unit.
- `P2`: multi-branch investigation retrieval policy.
---
## 4) LLM Layer and Decomposition Contour
### 4.1 How it works now
- Prompt/schema baseline: `normalizer_v2_0_2`.
- Deterministic v2 routing summary with fallback types.
- Requirements extraction + coverage report + dropped-intent tracking are implemented.
### 4.2 What works
- Route and execution readiness are explicit.
- Coverage and grounding diagnostics are available per turn.
- Route mismatch blocking is now less false-positive for non-critical contextual tokens.
### 4.3 Constraints
- Requirement extraction remains coarse in many cases (often 1 requirement per fragment).
- Transliteration/noisy mixed-language prompts still degrade in-scope detection.
### 4.4 What assistant cannot do because of this
- Fine-grained multi-requirement planning for complex accounting requests.
- Fully robust handling of colloquial/translit business language.
### 4.5 Symptoms
- Some translit prompts fall into `out_of_scope/clarification`.
- Partial semantic intent may be compressed in long multi-part prompts.
### 4.6 Local changes needed
- Expand language normalization and translit alias mapping before decomposition.
- Improve requirement extraction granularity inside one fragment.
### 4.7 Architectural changes needed
- Add dedicated semantic parser layer before normalizer for business-language canonicalization.
- Add requirement graph (instead of flat list) for planning/execution.
### 4.8 Priority
- `P0`: translit/business alias normalization.
- `P1`: requirement graph extraction.
- `P2`: adaptive decomposition policy.
---
## 5) Answer Synthesis / Explanation Contour
### 5.1 How it works now
- Reply types include:
- `factual_with_explanation`
- `partial_coverage`
- `clarification_required`
- `no_grounded_answer`
- `route_mismatch_blocked`
- others
- Response includes explainable sections: result, why included, selection basis, risk signs, business meaning, limitations, next step.
### 5.2 What works
- Core explainable contract is implemented and stable.
- Blocking logic prevents clearly mismatched subject answers.
### 5.3 Constraints
- Generic wording still appears when retrieval unit is broad.
- Explanations are still largely template-driven for some routes.
### 5.4 What assistant cannot do because of this
- Deliver fully case-unique accountant-level narratives in all scenarios.
### 5.5 Symptoms
- Two semantically close broad prompts may yield similar explanatory skeleton.
### 5.6 Local changes needed
- Route-specific explanation templates with stronger domain phrasing.
- Explicit “mechanism-of-failure” fields in retrieval result for composer.
### 5.7 Architectural changes needed
- Separate explanation planner from template renderer.
- Add accountant-facing narrative policy with domain lexicon packs.
### 5.8 Priority
- `P0`: route-specific explanation enrichment.
- `P1`: mechanism-level explanation fields.
- `P2`: explanation planner subsystem.
---
## 6) Memory / State / Session Continuity Contour
### 6.1 How it works now
- Session-scoped conversation state is persisted.
- One JSON file per session with turn-level human-readable + technical blocks.
### 6.2 What works
- Stable conversation history and replay.
- Explicit per-turn decomposition and response auditability.
### 6.3 Constraints
- No robust investigation state model (hypotheses/open checks/resolution graph).
- Context memory is conversational, not analytical.
### 6.4 What assistant cannot do because of this
- True multi-step investigative reasoning with hypothesis tracking.
### 6.5 Symptoms
- Follow-up can be coherent but not yet “investigation-driven”.
### 6.6 Local changes needed
- Add per-session `investigation_state` object (focus, active entities, open hypotheses, unresolved branches).
### 6.7 Architectural changes needed
- Add working-memory layer for research workflow, not only chat continuity.
### 6.8 Priority
- `P0`: investigation_state schema + persistence.
- `P1`: branch tracking and hypothesis status transitions.
- `P2`: multi-turn analytical planning engine.
---
## 7) Orchestration / Routing / Control Policy Contour
### 7.1 How it works now
- Deterministic routing with fallback (`none/out_of_scope/clarification/partial`).
- Linear execution plan per turn.
### 7.2 What works
- Clear route decisions and no-route reasons.
- Strong deterministic observability.
### 7.3 Constraints
- Mostly route-driven linear pipeline.
- Limited iterative branch exploration initiated by system policy.
### 7.4 What assistant cannot do because of this
- Automatically run neighbor contour verification when primary evidence is weak.
### 7.5 Symptoms
- Reasonable direct answers, but limited self-initiated investigation depth.
### 7.6 Local changes needed
- Introduce confidence-driven secondary retrieval triggers.
### 7.7 Architectural changes needed
- Orchestration policy engine with iterative reasoning loops and stop criteria.
### 7.8 Priority
- `P0`: confidence-based secondary checks.
- `P1`: branch exploration policy.
- `P2`: full investigation orchestrator.
---
## 8) Quality / Observability / Eval Contour
### 8.1 How it works now
- Structured runtime logs (stdout JSON).
- Trace storage and session logs.
- Regression tests for API behavior, grounding and retrieval semantics.
### 8.2 What works
- Technical observability is strong for current stage.
- Automated test baseline is green (`25/25`).
### 8.3 Constraints
- Limited accountant-utility evaluation metrics.
- No broad canonical scenario benchmark with decision-quality scoring.
### 8.4 What assistant cannot do because of this
- Provide hard quantitative proof of business usefulness across accounting domains.
### 8.5 Symptoms
- Technical success may still exceed practical user-perceived success.
### 8.6 Local changes needed
- Add eval metrics:
- retrieval differentiation rate
- generic explanation rate
- accountant actionability score
- false confidence rate
### 8.7 Architectural changes needed
- Build domain eval harness with canonical accounting scenarios and target outcomes.
### 8.8 Priority
- `P0`: metric instrumentation for practical usefulness.
- `P1`: canonical benchmark suite (bank/60/97/OS/VAT/period close/multi-intent/translit/follow-up).
- `P2`: continuous quality dashboard.
---
## 9) Evolutionary Architecture Contour
### 9.1 Missing pieces (high impact)
- Ontology graph core.
- Lifecycle engine.
- Problem-cluster retrieval unit.
- Investigation memory/state.
- Orchestration policy engine for iterative checks.
- Live verification bridge for source-of-truth escalation.
### 9.2 Current ceiling
- Without deeper ontology/lifecycle/state layers, system remains strong “explainable routed assistant”, but not full accountant investigation copilot.
### 9.3 Local vs architectural changes
- Local: better filters, better templates, more metrics, better parser.
- Architectural: graph model, lifecycle engine, investigation state, multi-step orchestrator.
### 9.4 Priority
- `P0`: finish semantic retrieval hardening + practical eval metrics + investigation_state baseline.
- `P1`: ontology/lifecycle formalization + problem-cluster retrieval.
- `P2`: iterative orchestrator + live verification framework.
---
## 10) Answers to 12 Mandatory Questions
1. What data reaches assistant and where detail is lost:
Data reaches from snapshot package with links/attributes; detail loss happens in flattening/grouping and lack of formal lifecycle semantics.
2. Full domain model exists:
Partially. Semantic labels and patterns exist, formal ontology graph does not.
3. Primary retrieval unit:
Mostly counterparty-grouped chain/risk clusters; not yet universal problem-node unit.
4. Real constraints and wide-scan risk:
Constraints now executed in hybrid semantic profile, but generic queries can still remain broad.
5. What LLM receives before answer:
Normalizer output + route summary + normalized retrieval payload + grounding/coverage diagnostics.
6. What is lost in decomposition:
Fine-grained multi-requirement structure can still compress; translit/noisy input can lose intent quality.
7. Why explanation still generic in places:
Broad retrieval unit + template-driven synthesis with limited mechanism-specific fields.
8. Can system explain mechanism (not only labels):
Partially. Better than before, still constrained by retrieval evidence depth.
9. Working state/memory for multi-step analysis:
Conversation memory exists; investigation memory model is missing.
10. Can system explore neighbor accounting branches automatically:
Not yet as policy standard; mostly linear route execution.
11. How usefulness is measured:
Technical pipeline quality is measured; accountant-facing utility metrics are not complete yet.
12. Missing architectural entities preventing next quality tier:
Ontology graph, lifecycle engine, problem-cluster unit, investigation state, iterative orchestration.
---
## 11) Current Phase Status (Condensed)
- Phase status: `Functional MVP+` (explainable routed assistant with semantic retrieval upgrade).
- Not yet: `Production accountant copilot`.
- Immediate gate to next phase: tighten broad-query narrowing + add practical accountant eval metrics + investigation state schema.
---
## 12) Recommended Next Step Pack
### P0 (next iteration)
- Tighten generic-query semantic narrowing in hybrid route.
- Add investigation state object in session model.
- Add practical eval metrics (differentiation/actionability/generic-rate).
### P1 (after P0 stabilization)
- Formalize ontology + lifecycle layers.
- Shift retrieval output from entity-heavy to problem-cluster-heavy for key domains.
### P2 (strategic)
- Add iterative orchestration with neighbor-branch verification.
- Add live source-of-truth verification path for high-confidence conclusions.
---
## 13) Data Loss Map (Source to LLM)
This section is the explicit loss map requested for architecture decisions.
| Source Layer | Current Internal Representation | Lost/Weakened Signals | Observable Assistant Symptom | Required Fix Layer |
|---|---|---|---|---|
| 1C document/journal/register snapshot record | flattened `SnapshotRecord` + heuristic signal extraction | formal business status transitions, typed lifecycle stage semantics | explanation can be structurally correct but semantically generic | lifecycle model + ontology graph |
| document + posting relation hints | relation pattern labels inferred by regex/rules | deterministic causal edge type and confidence | “close to right chain” answers without strict mechanism proof | typed relation graph + relation confidence |
| account hints from query and record fields | `account_scope` and inferred `account_context` arrays | strong account-role semantics (main vs side context) | broad retrieval if account scope is not explicit | account-role policy in retrieval profile |
| anomaly signs (`unknown links`, `zero guid`, etc.) | anomaly pattern tags (`missing_link`, `broken_lifecycle`, etc.) | accountant-grade defect class and business consequence mapping | same anomaly labels across semantically different defects | anomaly catalog and mapping engine |
| session chat turns | conversation list + turn log | investigation branch state and hypothesis state | follow-up can be coherent but not deeply investigative | investigation_state subsystem |
| snapshot-only truth | no guaranteed live verification step in assistant route | real-time status confirmation | high-quality but potentially stale conclusion in sensitive cases | live verification bridge |
### 13.1 Diagnostic implication
The dominant ceiling is not “weak wording” but “insufficiently structured causal context before synthesis”.
---
## 14) Query Class vs Required Architecture Depth
| User Query Class | Required Layers | Current Readiness | Ceiling Cause | Next Upgrade |
|---|---|---|---|---|
| simple factual object lookup | routing + canonical retrieval + basic grounding | medium/high | snapshot-only verification | optional live drilldown |
| anomaly ranking (one contour) | semantic profile + risk retrieval + explainable synthesis | medium | anomaly semantics still heuristic | anomaly catalog hardening |
| causal chain in one contour | relation patterns + chain retrieval + evidence pack | medium | retrieval unit still entity-heavy in broad prompts | problem-cluster unit |
| cross-domain reconciliation | ontology + lifecycle + neighbor branch policy | low/medium | no formal cross-domain causal graph | ontology graph + branch policy |
| period-close impact analysis | lifecycle + period-risk model + orchestration | low/medium | lifecycle model incomplete | lifecycle engine |
| multi-step investigation with follow-up | investigation_state + orchestration loops + hypothesis tracking | low | memory is conversational, not investigative | investigation mode layer |
| ambiguity-heavy/translit business language | semantic parser + alias normalization + decomposition guard | low/medium | parser limitations before routing | pre-normalization parser layer |
### 14.1 Decision implication
Prompt/model tuning alone cannot close low-readiness classes above; they are architecture-depth dependent.
---
## 15) Retrieval Unit Diagnosis (Core Bottleneck)
### 15.1 Current dominant unit
- Dominant unit in hybrid route is still often `counterparty group`, even after semantic narrowing.
### 15.2 Where this unit is acceptable
- quick ranking
- initial risk surfacing
- broad operational scanning
### 15.3 Where this unit breaks answer quality
- “what exactly is broken in chain”
- “closed by wrong document type”
- “which lifecycle stage is inconsistent”
- “what blocks period close and why”
### 15.4 Target retrieval units (must become first-class)
- `document_conflict`
- `broken_chain_segment`
- `lifecycle_anomaly_node`
- `unresolved_settlement_cluster`
- `period_risk_cluster`
- `cross_branch_inconsistency_cluster`
### 15.5 Transition plan
- Step 1 (`P0`): keep counterparty groups but add explicit `mechanism_of_failure` + `failed_expected_edge`.
- Step 2 (`P1`): introduce mixed-unit ranking (problem cluster first, entity second).
- Step 3 (`P2`): use problem-cluster as default answer unit for chain/anomaly/period-risk routes.
---
## 16) LLM Ceiling Boundaries (Not Solvable by Prompt Alone)
The following limitations remain even with stronger models/prompts unless architecture changes:
1. no formal lifecycle state machine on input -> model cannot produce deterministic lifecycle diagnosis;
2. no typed causal graph edges -> model cannot consistently prove mechanism, only infer plausible narrative;
3. entity-heavy retrieval unit -> model can explain “who is risky”, but not always “what exact mechanism broke”;
4. missing investigation_state -> model cannot reliably manage long hypothesis trees across turns;
5. no mandatory live verification gate -> model cannot guarantee real-time truth in high-stakes answers.
### 16.1 Governance rule
When limitations above are active, quality work must target data/model/orchestration layers first; LLM tuning is secondary.
---
## 17) Investigation Mode Specification (Required Next Architecture)
### 17.1 Minimal `investigation_state` schema
```json
{
"session_id": "asst-...",
"focus": {
"domain": "bank_settlements",
"period": "2020-06",
"primary_accounts": ["51", "60"]
},
"active_entities": [
{ "type": "counterparty", "id": "..." },
{ "type": "document", "id": "..." }
],
"open_hypotheses": [
{
"hypothesis_id": "H1",
"statement": "closure performed by wrong document type",
"status": "open",
"evidence_for": [],
"evidence_against": []
}
],
"branches": [
{
"branch_id": "B1",
"name": "bank->settlement",
"status": "in_progress",
"unresolved_reason": null
}
],
"resolved_findings": [],
"next_actions": []
}
```
### 17.2 Required branch lifecycle
- `open` -> `in_progress` -> `confirmed` or `rejected` -> `closed`
### 17.3 System-initiated branch rule (minimum)
If primary route confidence is high but mechanism evidence is weak, assistant should launch one neighbor branch check before final high-confidence conclusion.
---
## 18) Symptom to Root Cause to Required Layer
| Symptom | Root Cause | Required Layer |
|---|---|---|
| generic explanation despite “ok” reply | mechanism fields missing in retrieval payload | retrieval schema + answer planner |
| similar answers for broad prompts | weak semantic narrowing for low-specificity queries | retrieval policy |
| follow-up does not deepen analysis | no hypothesis/branch state | investigation_state |
| strong dependence on explicit account hints | weak semantic parser/ontology grounding | parser + ontology |
| lifecycle conclusions not stable | lifecycle semantics heuristic only | lifecycle engine |
| high confidence on snapshot-only route | no live verification gate | live verification bridge |
---
## 19) Value-Impact Roadmap (Decision Table)
| Change | Complexity | Quality Gain | Accountant Usefulness Gain | Multi-step Investigation Gain | Priority |
|---|---|---|---|---|---|
| tighten generic semantic narrowing | low/medium | high | high | medium | P0 |
| add `mechanism_of_failure` retrieval fields | medium | high | high | medium | P0 |
| add `investigation_state` persistence | medium | medium/high | high | high | P0 |
| add practical utility eval metrics | low/medium | medium | high | medium | P0 |
| formalize anomaly catalog | medium | medium/high | high | medium | P1 |
| ontology graph core | high | high | high | high | P1 |
| lifecycle engine | high | high | high | high | P1 |
| problem-cluster retrieval unit | high | high | high | high | P1 |
| iterative orchestration engine | high | high | high | very high | P2 |
| live verification bridge | high | medium/high | high | medium/high | P2 |
---
## 20) What Not To Do (Explicit Guardrails)
1. Do not attempt to solve mechanism-level quality only with prompt edits.
2. Do not treat richer wording as substitute for stronger retrieval unit.
3. Do not scale explanation templates without adding mechanism evidence fields.
4. Do not equate long conversation history with investigation_state.
5. Do not claim production-grade confidence without live verification path for critical answers.

View File

@ -0,0 +1,63 @@
# Assistant Mode Global Status Appendix
Date: 2026-03-24
## A) Verification Commands
```powershell
cd X:\1C\NDC_1C\llm_normalizer\backend
npm.cmd run build
npm.cmd run test
```
Observed result:
- TypeScript build: success
- Test suite: success (`25/25`)
## B) Retrieval Narrowing Evidence
### Case 1: bank mismatch with explicit account scope
- Session: `asst-FuRihiL5Bp`
- Query subject: `bank_settlement_mismatch`
- Source records: `262`
- Filtered after narrowing: `75`
- Semantic narrowing applied: `true`
### Case 2: generic cross-entity bank chain
- Session: `asst-j9spgqdY7k`
- Query subject: `cross_entity_breakage`
- Source records: `262`
- Filtered after narrowing: `242`
- Semantic narrowing applied: `true`
Interpretation:
- Semantic narrowing is active and effective for constrained accounting scope.
- Generic prompts still need stronger narrowing policy.
## C) Key Implementation Anchors
- Semantic profile contract and builder:
- `X:\1C\NDC_1C\llm_normalizer\backend\src\services\assistantDataLayer.ts`
- Hybrid narrowing and enriched evidence pack:
- `X:\1C\NDC_1C\llm_normalizer\backend\src\services\assistantDataLayer.ts`
- API regression test for semantic narrowing:
- `X:\1C\NDC_1C\llm_normalizer\backend\tests\assistantEndpoint.test.ts`
## D) v1.1 Report Reinforcement Checklist
All 4 requested reinforcements are now explicitly present in the main report:
1. Data-loss path map (`Source -> Internal -> Lost -> Symptom -> Fix layer`)
2. Query-class vs architecture-depth matrix
3. Dedicated retrieval-unit diagnosis block (current vs target units)
4. Investigation mode schema and control-policy baseline
Also added:
- symptom -> root cause -> required layer matrix
- value-impact roadmap table
- explicit “what not to do” guardrails

Binary file not shown.

View File

@ -0,0 +1,93 @@
# Router / Orchestration Fix Report (2026-03-23)
## Scope completed
По `IN/TZ_Router_Orchestration_Fix.md` реализованы:
1. Query classifier v2 с decision flags.
2. Store sufficiency checker.
3. Explicit route guards.
4. Runtime batch handoff (`refresh_and_answer` job path).
5. Route decision logging для всех benchmark-вопросов.
6. Unit + integration-style tests по router-модулям.
7. Повторный validation-run на июньском semantic-v2 срезе.
## Implemented modules
Новые пакеты и файлы:
- `router/query_classifier.py`
- `router/store_sufficiency.py`
- `router/route_selector.py`
- `router/decision_log.py`
- `orchestration/batch_runtime.py`
Подключение в benchmark-runtime:
- `scripts/run_validation_accounting_analytics.py`
- orchestration policy расширена ссылками на runtime-router модули;
- добавлен `build_store_metadata(...)`;
- добавлен `build_benchmark_results_v2(...)`;
- добавлен batch handoff path для `batch_refresh_then_store`;
- добавлен export `route_decision_logs.json`.
## Tests
Добавлены тесты:
- `tests/test_router_decision_flags.py`
- `tests/test_store_sufficiency.py`
- `tests/test_route_guards.py`
- `tests/test_batch_runtime_handoff.py`
- `tests/test_router_benchmark_subset.py`
Статус:
- `python -m pytest -q` -> `31 passed`
## Validation run (router-fix)
Команда:
`python scripts/run_validation_accounting_analytics.py --snapshot-path logs/pre_report_snapshot_2020_2020-06_semantic_v2.json --output-dir docs/ARCH/validation_run_2026-03-23_router_fix --strict`
Выход:
- `docs/ARCH/validation_run_2026-03-23_router_fix/`
Ключевые результаты benchmark:
- `questions_total = 35`
- `route_mismatch_count = 1` (было 7)
- `degraded_answers_count = 0`
- `batch_route_count = 5` (было 0)
- `heavy_analytical mismatches = 0`
- `cross_entity mismatches = 0`
- `drilldown_explain mismatches = 0`
Единственный остаточный mismatch:
- `Q19` (`period_trend`): expected `store_feature_risk`, actual `batch_refresh_then_store`
Decision logs:
- `docs/ARCH/validation_run_2026-03-23_router_fix/route_decision_logs.json`
- покрытие логами: `35/35` вопросов.
## Acceptance criteria status
По целям ТЗ:
- `route_mismatch_count <= 2`: **done** (`1`)
- `heavy_analytical mismatches = 0`: **done**
- `cross_entity mismatches = 0`: **done**
- `drilldown_explain mismatches <= 1`: **done** (`0`)
- `batch_route_count > 0`: **done** (`5`)
- `degraded_answers_count = 0`: **done**
- decision logs for all 35: **done**
## Notes
1. Batch runtime path исполняется в-process через `orchestration.batch_runtime`, с job payload и run-id trace.
2. Refresh step в batch режиме сейчас gated (`allow_refresh_in_batch=False` для validation profile), чтобы не делать неконтролируемый live refresh в этом прогоне; при этом feature/risk handoff исполняется реально.
3. Следующий точечный шаг: опционально дотюнить classifier threshold для `Q19`, чтобы привести `route_mismatch_count` к `0`.

View File

@ -0,0 +1,35 @@
# Router / Orchestration Fix Report v2 (2026-03-23)
## Final tuning step
После первого router-fix прогона оставался 1 mismatch (`Q19`), где `period_trend` вопрос с формулировкой про аномалию уходил в batch.
Сделана точечная правка:
- `router/route_selector.py`
- heavy-guard теперь срабатывает по `needs_anomaly_summary` только если запрос не относится к trend/risk профилю (`not parsed_as_trend_or_risk`).
Добавлен тест:
- `tests/test_router_benchmark_subset.py::test_router_period_trend_anomaly_stays_feature_store`
## Verification
- `python -m pytest -q` -> `32 passed`
- Validation run:
- `python scripts/run_validation_accounting_analytics.py --snapshot-path logs/pre_report_snapshot_2020_2020-06_semantic_v2.json --output-dir docs/ARCH/validation_run_2026-03-23_router_fix_v2 --strict`
## Result metrics (router_fix_v2)
- `questions_total = 35`
- `route_mismatch_count = 0`
- `degraded_answers_count = 0`
- `heavy_analytical mismatches = 0`
- `cross_entity mismatches = 0`
- `drilldown_explain mismatches = 0`
- `batch_route_count = 4` (> 0, runtime path active)
## Artifacts
- `docs/ARCH/validation_run_2026-03-23_router_fix_v2/`
- `docs/ARCH/validation_run_2026-03-23_router_fix_v2/route_decision_logs.json`

51
docs/ARCH/setup.md Normal file
View File

@ -0,0 +1,51 @@
# Setup Guide
## 1. Install Miniconda (if missing)
```powershell
winget install -e --id Anaconda.Miniconda3 --source winget --accept-source-agreements --accept-package-agreements --silent
```
## 2. Create isolated environment
```powershell
$Conda = Join-Path $env:USERPROFILE "miniconda3\Scripts\conda.exe"
if (-not (Test-Path $Conda)) { $Conda = Join-Path $env:USERPROFILE "Miniconda3\Scripts\conda.exe" }
& $Conda create -y -n ndc_1c_mvp python=3.11
$EnvPython = Join-Path $env:USERPROFILE "miniconda3\envs\ndc_1c_mvp\python.exe"
if (-not (Test-Path $EnvPython)) { $EnvPython = Join-Path $env:USERPROFILE "Miniconda3\envs\ndc_1c_mvp\python.exe" }
& $EnvPython -m pip install -r requirements.txt
```
## 3. Configure environment variables
```powershell
copy .env.example .env
```
Set real values for:
- `ONEC_BASE_URL`
- `ONEC_INFOBASE`
- `ONEC_USERNAME`
- `ONEC_PASSWORD`
## 4. Run OData probe
```powershell
& $EnvPython -m odata_probe.fetch_metadata
& $EnvPython -m odata_probe.list_entity_sets
& $EnvPython -m odata_probe.probe_entities
& $EnvPython -m odata_probe.dump_sample_links
```
## 5. Run API
```powershell
& $EnvPython -m uvicorn canonical_layer.app:app --host 127.0.0.1 --port 8000 --reload
```
## Notes
- Project mode is read-only against 1C.
- For production, restrict OData role permissions to read-only.
- If OData is not published yet, probe scripts will fail by design and log the connectivity problem.

View File

@ -0,0 +1,19 @@
# Benchmark Final Verdict
## Verdict
`adopt_with_improvements`
## Key numbers
- Questions total: `35`
- Route mismatches: `7`
- Degraded answers: `0`
- Avg latency ms: `506.43`
- p95 latency ms: `1024.5`
## Recommendation
1. Fix ontology unknown mapping hotspots.
2. Tune heavy-route threshold (`store_feature_risk` vs `batch_refresh_then_store`).
3. Implement full production orchestration runtime.

View File

@ -0,0 +1,39 @@
# Benchmark Questions (35)
| ID | Class | Expected route | Question |
| --- | --- | --- | --- |
| Q01 | simple_factual | store_canonical | Сальдо счета 68.02 за июнь 2020? |
| Q02 | simple_factual | live_mcp_drilldown | Документ по номеру и его ссылка. |
| Q03 | simple_factual | store_canonical | Типовая проводка по реализации. |
| Q04 | simple_factual | store_canonical | Контрагент с максимумом оборота. |
| Q05 | simple_factual | store_canonical | Договоры топ-контрагента. |
| Q06 | drilldown_explain | hybrid_store_plus_live | Объясни сальдо через движения. |
| Q07 | drilldown_explain | live_mcp_drilldown | Почему проводка на этот счет? |
| Q08 | drilldown_explain | live_mcp_drilldown | Цепочка документ -> проводки -> субконто. |
| Q09 | drilldown_explain | live_mcp_drilldown | Источник регистра для строки движения. |
| Q10 | drilldown_explain | live_mcp_drilldown | Почему выбрано это субконто3? |
| Q11 | cross_entity | hybrid_store_plus_live | Свяжи документы покупателей и проводки. |
| Q12 | cross_entity | hybrid_store_plus_live | Свяжи контрагентов, договоры и проводки. |
| Q13 | cross_entity | store_canonical | Номенклатура, склад, обороты за июнь. |
| Q14 | cross_entity | hybrid_store_plus_live | Регистр и первичный документ. |
| Q15 | cross_entity | store_canonical | По счету: контрагенты и договоры. |
| Q16 | period_trend | store_feature_risk | Обороты июня против мая. |
| Q17 | period_trend | store_feature_risk | Недельные всплески в июне. |
| Q18 | period_trend | store_feature_risk | Кто дал резкий рост активности. |
| Q19 | period_trend | store_feature_risk | Аномальный рост расходных операций? |
| Q20 | period_trend | store_feature_risk | Динамика НДС к соседним периодам. |
| Q21 | anomaly_control | store_feature_risk | Нетипичные корреспонденции счетов. |
| Q22 | anomaly_control | store_feature_risk | Незакрытые хвосты по расчетам. |
| Q23 | anomaly_control | store_feature_risk | Дублирующиеся проводки. |
| Q24 | anomaly_control | store_feature_risk | Пустые или странные субконто. |
| Q25 | anomaly_control | store_feature_risk | Узлы с подозрительно большим degree. |
| Q26 | heavy_analytical | batch_refresh_then_store | Полный риск-срез за июнь. |
| Q27 | heavy_analytical | batch_refresh_then_store | Рейтинг риск-счетов. |
| Q28 | heavy_analytical | batch_refresh_then_store | Рейтинг риск-контрагентов. |
| Q29 | heavy_analytical | store_feature_risk | Baseline closed/open periods. |
| Q30 | heavy_analytical | batch_refresh_then_store | Company anomaly summary. |
| Q31 | ambiguous_fuzzy | store_feature_risk | Что по налогам и рискам? |
| Q32 | ambiguous_fuzzy | store_feature_risk | Что странное в расходах? |
| Q33 | ambiguous_fuzzy | store_feature_risk | Самые рисковые контрагенты? |
| Q34 | ambiguous_fuzzy | hybrid_store_plus_live | Что с 68.02? |
| Q35 | ambiguous_fuzzy | store_feature_risk | Проверить документы июня. |

View File

@ -0,0 +1,19 @@
# Benchmark Route Analysis
- Total mismatches: `7`
## Route confusion matrix
- `batch_refresh_then_store` -> store_feature_risk:4
- `hybrid_store_plus_live` -> hybrid_store_plus_live:3, store_canonical:2
- `live_mcp_drilldown` -> hybrid_store_plus_live:1, live_mcp_drilldown:4
- `store_canonical` -> store_canonical:6
- `store_feature_risk` -> store_feature_risk:15
## Mismatch by class
| Class | Mismatch count |
| --- | --- |
| cross_entity | 2 |
| drilldown_explain | 1 |
| heavy_analytical | 4 |

View File

@ -0,0 +1,38 @@
# Benchmark Run Report
## Aggregate statistics
| Metric | Value |
| --- | --- |
| questions_total | 35 |
| avg_latency_ms | 506.43 |
| median_latency_ms | 402 |
| p90_latency_ms | 941.4 |
| p95_latency_ms | 1024.5 |
| avg_context_size | 2162.51 |
| live_route_count | 8 |
| store_route_count | 27 |
| batch_route_count | 0 |
| route_mismatch_count | 7 |
| degraded_answers_count | 0 |
## Route distribution
| Route | Count |
| --- | --- |
| hybrid_store_plus_live | 4 |
| live_mcp_drilldown | 4 |
| store_canonical | 8 |
| store_feature_risk | 19 |
## Question class distribution
| Class | Count |
| --- | --- |
| ambiguous_fuzzy | 5 |
| anomaly_control | 5 |
| cross_entity | 5 |
| drilldown_explain | 5 |
| heavy_analytical | 5 |
| period_trend | 5 |
| simple_factual | 5 |

View File

@ -0,0 +1,827 @@
{
"status": "success",
"slice_window_key": "2020-06",
"generated_at": "2026-03-23T09:28:12.312411+00:00",
"questions_total": 35,
"aggregate": {
"questions_total": 35,
"avg_latency_ms": 506.43,
"median_latency_ms": 402,
"p90_latency_ms": 941.4,
"p95_latency_ms": 1024.5,
"avg_context_size": 2162.51,
"live_route_count": 8,
"store_route_count": 27,
"batch_route_count": 0,
"route_mismatch_count": 7,
"degraded_answers_count": 0,
"route_distribution": {
"store_canonical": 8,
"live_mcp_drilldown": 4,
"hybrid_store_plus_live": 4,
"store_feature_risk": 19
},
"question_class_distribution": {
"simple_factual": 5,
"drilldown_explain": 5,
"cross_entity": 5,
"period_trend": 5,
"anomaly_control": 5,
"heavy_analytical": 5,
"ambiguous_fuzzy": 5
}
},
"results": [
{
"question_id": "Q01",
"question_text": "Сальдо счета 68.02 за июнь 2020?",
"question_class": "simple_factual",
"expected_route": "store_canonical",
"actual_route": "store_canonical",
"sources_used": [
"canonical_store"
],
"refresh_needed": false,
"latency_ms": 332,
"planning_time_ms": 67,
"retrieval_time_ms": 129,
"response_generation_time_ms": 136,
"context_size": 1595,
"answer_text": "[simulated-4o-mini-profile] route=store_canonical; answer synthesized from June-2020 slice + current stores.",
"answer_quality_assessment": "good",
"route_quality_assessment": "good",
"issues_detected": [],
"recommended_fix": "No action required."
},
{
"question_id": "Q02",
"question_text": "Документ по номеру и его ссылка.",
"question_class": "simple_factual",
"expected_route": "live_mcp_drilldown",
"actual_route": "live_mcp_drilldown",
"sources_used": [
"mcp_runtime_bridge"
],
"refresh_needed": false,
"latency_ms": 1020,
"planning_time_ms": 93,
"retrieval_time_ms": 740,
"response_generation_time_ms": 187,
"context_size": 2796,
"answer_text": "[simulated-4o-mini-profile] route=live_mcp_drilldown; answer synthesized from June-2020 slice + current stores.",
"answer_quality_assessment": "good",
"route_quality_assessment": "good",
"issues_detected": [],
"recommended_fix": "No action required."
},
{
"question_id": "Q03",
"question_text": "Типовая проводка по реализации.",
"question_class": "simple_factual",
"expected_route": "store_canonical",
"actual_route": "store_canonical",
"sources_used": [
"canonical_store"
],
"refresh_needed": false,
"latency_ms": 338,
"planning_time_ms": 69,
"retrieval_time_ms": 131,
"response_generation_time_ms": 138,
"context_size": 1597,
"answer_text": "[simulated-4o-mini-profile] route=store_canonical; answer synthesized from June-2020 slice + current stores.",
"answer_quality_assessment": "good",
"route_quality_assessment": "good",
"issues_detected": [],
"recommended_fix": "No action required."
},
{
"question_id": "Q04",
"question_text": "Контрагент с максимумом оборота.",
"question_class": "simple_factual",
"expected_route": "store_canonical",
"actual_route": "store_canonical",
"sources_used": [
"canonical_store"
],
"refresh_needed": false,
"latency_ms": 341,
"planning_time_ms": 70,
"retrieval_time_ms": 132,
"response_generation_time_ms": 139,
"context_size": 1598,
"answer_text": "[simulated-4o-mini-profile] route=store_canonical; answer synthesized from June-2020 slice + current stores.",
"answer_quality_assessment": "good",
"route_quality_assessment": "good",
"issues_detected": [],
"recommended_fix": "No action required."
},
{
"question_id": "Q05",
"question_text": "Договоры топ-контрагента.",
"question_class": "simple_factual",
"expected_route": "store_canonical",
"actual_route": "store_canonical",
"sources_used": [
"canonical_store"
],
"refresh_needed": false,
"latency_ms": 344,
"planning_time_ms": 71,
"retrieval_time_ms": 133,
"response_generation_time_ms": 140,
"context_size": 1599,
"answer_text": "[simulated-4o-mini-profile] route=store_canonical; answer synthesized from June-2020 slice + current stores.",
"answer_quality_assessment": "good",
"route_quality_assessment": "good",
"issues_detected": [],
"recommended_fix": "No action required."
},
{
"question_id": "Q06",
"question_text": "Объясни сальдо через движения.",
"question_class": "drilldown_explain",
"expected_route": "hybrid_store_plus_live",
"actual_route": "hybrid_store_plus_live",
"sources_used": [
"canonical_store",
"mcp_runtime_bridge"
],
"refresh_needed": false,
"latency_ms": 819,
"planning_time_ms": 114,
"retrieval_time_ms": 524,
"response_generation_time_ms": 181,
"context_size": 2950,
"answer_text": "[simulated-4o-mini-profile] route=hybrid_store_plus_live; answer synthesized from June-2020 slice + current stores.",
"answer_quality_assessment": "good",
"route_quality_assessment": "good",
"issues_detected": [],
"recommended_fix": "No action required."
},
{
"question_id": "Q07",
"question_text": "Почему проводка на этот счет?",
"question_class": "drilldown_explain",
"expected_route": "live_mcp_drilldown",
"actual_route": "live_mcp_drilldown",
"sources_used": [
"mcp_runtime_bridge"
],
"refresh_needed": false,
"latency_ms": 1035,
"planning_time_ms": 98,
"retrieval_time_ms": 745,
"response_generation_time_ms": 192,
"context_size": 2801,
"answer_text": "[simulated-4o-mini-profile] route=live_mcp_drilldown; answer synthesized from June-2020 slice + current stores.",
"answer_quality_assessment": "good",
"route_quality_assessment": "good",
"issues_detected": [],
"recommended_fix": "No action required."
},
{
"question_id": "Q08",
"question_text": "Цепочка документ -> проводки -> субконто.",
"question_class": "drilldown_explain",
"expected_route": "live_mcp_drilldown",
"actual_route": "live_mcp_drilldown",
"sources_used": [
"mcp_runtime_bridge"
],
"refresh_needed": false,
"latency_ms": 1038,
"planning_time_ms": 99,
"retrieval_time_ms": 746,
"response_generation_time_ms": 193,
"context_size": 2802,
"answer_text": "[simulated-4o-mini-profile] route=live_mcp_drilldown; answer synthesized from June-2020 slice + current stores.",
"answer_quality_assessment": "good",
"route_quality_assessment": "good",
"issues_detected": [],
"recommended_fix": "No action required."
},
{
"question_id": "Q09",
"question_text": "Источник регистра для строки движения.",
"question_class": "drilldown_explain",
"expected_route": "live_mcp_drilldown",
"actual_route": "hybrid_store_plus_live",
"sources_used": [
"canonical_store",
"mcp_runtime_bridge"
],
"refresh_needed": false,
"latency_ms": 828,
"planning_time_ms": 117,
"retrieval_time_ms": 527,
"response_generation_time_ms": 184,
"context_size": 2953,
"answer_text": "[simulated-4o-mini-profile] route=hybrid_store_plus_live; answer synthesized from June-2020 slice + current stores.",
"answer_quality_assessment": "good",
"route_quality_assessment": "acceptable_with_warning",
"issues_detected": [
"Route mismatch: expected live_mcp_drilldown, got hybrid_store_plus_live"
],
"recommended_fix": "Tune router threshold for heavy/live boundary."
},
{
"question_id": "Q10",
"question_text": "Почему выбрано это субконто3?",
"question_class": "drilldown_explain",
"expected_route": "live_mcp_drilldown",
"actual_route": "live_mcp_drilldown",
"sources_used": [
"mcp_runtime_bridge"
],
"refresh_needed": false,
"latency_ms": 1017,
"planning_time_ms": 92,
"retrieval_time_ms": 739,
"response_generation_time_ms": 186,
"context_size": 2795,
"answer_text": "[simulated-4o-mini-profile] route=live_mcp_drilldown; answer synthesized from June-2020 slice + current stores.",
"answer_quality_assessment": "good",
"route_quality_assessment": "good",
"issues_detected": [],
"recommended_fix": "No action required."
},
{
"question_id": "Q11",
"question_text": "Свяжи документы покупателей и проводки.",
"question_class": "cross_entity",
"expected_route": "hybrid_store_plus_live",
"actual_route": "store_canonical",
"sources_used": [
"canonical_store"
],
"refresh_needed": false,
"latency_ms": 335,
"planning_time_ms": 68,
"retrieval_time_ms": 130,
"response_generation_time_ms": 137,
"context_size": 1596,
"answer_text": "[simulated-4o-mini-profile] route=store_canonical; answer synthesized from June-2020 slice + current stores.",
"answer_quality_assessment": "good",
"route_quality_assessment": "acceptable_with_warning",
"issues_detected": [
"Route mismatch: expected hybrid_store_plus_live, got store_canonical"
],
"recommended_fix": "Tune router threshold for heavy/live boundary."
},
{
"question_id": "Q12",
"question_text": "Свяжи контрагентов, договоры и проводки.",
"question_class": "cross_entity",
"expected_route": "hybrid_store_plus_live",
"actual_route": "store_canonical",
"sources_used": [
"canonical_store"
],
"refresh_needed": false,
"latency_ms": 338,
"planning_time_ms": 69,
"retrieval_time_ms": 131,
"response_generation_time_ms": 138,
"context_size": 1597,
"answer_text": "[simulated-4o-mini-profile] route=store_canonical; answer synthesized from June-2020 slice + current stores.",
"answer_quality_assessment": "good",
"route_quality_assessment": "acceptable_with_warning",
"issues_detected": [
"Route mismatch: expected hybrid_store_plus_live, got store_canonical"
],
"recommended_fix": "Tune router threshold for heavy/live boundary."
},
{
"question_id": "Q13",
"question_text": "Номенклатура, склад, обороты за июнь.",
"question_class": "cross_entity",
"expected_route": "store_canonical",
"actual_route": "store_canonical",
"sources_used": [
"canonical_store"
],
"refresh_needed": false,
"latency_ms": 341,
"planning_time_ms": 70,
"retrieval_time_ms": 132,
"response_generation_time_ms": 139,
"context_size": 1598,
"answer_text": "[simulated-4o-mini-profile] route=store_canonical; answer synthesized from June-2020 slice + current stores.",
"answer_quality_assessment": "good",
"route_quality_assessment": "good",
"issues_detected": [],
"recommended_fix": "No action required."
},
{
"question_id": "Q14",
"question_text": "Регистр и первичный документ.",
"question_class": "cross_entity",
"expected_route": "hybrid_store_plus_live",
"actual_route": "hybrid_store_plus_live",
"sources_used": [
"canonical_store",
"mcp_runtime_bridge"
],
"refresh_needed": false,
"latency_ms": 816,
"planning_time_ms": 113,
"retrieval_time_ms": 523,
"response_generation_time_ms": 180,
"context_size": 2949,
"answer_text": "[simulated-4o-mini-profile] route=hybrid_store_plus_live; answer synthesized from June-2020 slice + current stores.",
"answer_quality_assessment": "good",
"route_quality_assessment": "good",
"issues_detected": [],
"recommended_fix": "No action required."
},
{
"question_id": "Q15",
"question_text": "По счету: контрагенты и договоры.",
"question_class": "cross_entity",
"expected_route": "store_canonical",
"actual_route": "store_canonical",
"sources_used": [
"canonical_store"
],
"refresh_needed": false,
"latency_ms": 347,
"planning_time_ms": 72,
"retrieval_time_ms": 134,
"response_generation_time_ms": 141,
"context_size": 1600,
"answer_text": "[simulated-4o-mini-profile] route=store_canonical; answer synthesized from June-2020 slice + current stores.",
"answer_quality_assessment": "good",
"route_quality_assessment": "good",
"issues_detected": [],
"recommended_fix": "No action required."
},
{
"question_id": "Q16",
"question_text": "Обороты июня против мая.",
"question_class": "period_trend",
"expected_route": "store_feature_risk",
"actual_route": "store_feature_risk",
"sources_used": [
"feature_store",
"risk_store",
"canonical_store"
],
"refresh_needed": false,
"latency_ms": 402,
"planning_time_ms": 85,
"retrieval_time_ms": 155,
"response_generation_time_ms": 162,
"context_size": 2101,
"answer_text": "[simulated-4o-mini-profile] route=store_feature_risk; answer synthesized from June-2020 slice + current stores.",
"answer_quality_assessment": "good",
"route_quality_assessment": "good",
"issues_detected": [],
"recommended_fix": "No action required."
},
{
"question_id": "Q17",
"question_text": "Недельные всплески в июне.",
"question_class": "period_trend",
"expected_route": "store_feature_risk",
"actual_route": "store_feature_risk",
"sources_used": [
"feature_store",
"risk_store",
"canonical_store"
],
"refresh_needed": false,
"latency_ms": 405,
"planning_time_ms": 86,
"retrieval_time_ms": 156,
"response_generation_time_ms": 163,
"context_size": 2102,
"answer_text": "[simulated-4o-mini-profile] route=store_feature_risk; answer synthesized from June-2020 slice + current stores.",
"answer_quality_assessment": "good",
"route_quality_assessment": "good",
"issues_detected": [],
"recommended_fix": "No action required."
},
{
"question_id": "Q18",
"question_text": "Кто дал резкий рост активности.",
"question_class": "period_trend",
"expected_route": "store_feature_risk",
"actual_route": "store_feature_risk",
"sources_used": [
"feature_store",
"risk_store",
"canonical_store"
],
"refresh_needed": false,
"latency_ms": 408,
"planning_time_ms": 87,
"retrieval_time_ms": 157,
"response_generation_time_ms": 164,
"context_size": 2103,
"answer_text": "[simulated-4o-mini-profile] route=store_feature_risk; answer synthesized from June-2020 slice + current stores.",
"answer_quality_assessment": "good",
"route_quality_assessment": "good",
"issues_detected": [],
"recommended_fix": "No action required."
},
{
"question_id": "Q19",
"question_text": "Аномальный рост расходных операций?",
"question_class": "period_trend",
"expected_route": "store_feature_risk",
"actual_route": "store_feature_risk",
"sources_used": [
"feature_store",
"risk_store",
"canonical_store"
],
"refresh_needed": false,
"latency_ms": 411,
"planning_time_ms": 88,
"retrieval_time_ms": 158,
"response_generation_time_ms": 165,
"context_size": 2104,
"answer_text": "[simulated-4o-mini-profile] route=store_feature_risk; answer synthesized from June-2020 slice + current stores.",
"answer_quality_assessment": "good",
"route_quality_assessment": "good",
"issues_detected": [],
"recommended_fix": "No action required."
},
{
"question_id": "Q20",
"question_text": "Динамика НДС к соседним периодам.",
"question_class": "period_trend",
"expected_route": "store_feature_risk",
"actual_route": "store_feature_risk",
"sources_used": [
"feature_store",
"risk_store",
"canonical_store"
],
"refresh_needed": false,
"latency_ms": 387,
"planning_time_ms": 80,
"retrieval_time_ms": 150,
"response_generation_time_ms": 157,
"context_size": 2096,
"answer_text": "[simulated-4o-mini-profile] route=store_feature_risk; answer synthesized from June-2020 slice + current stores.",
"answer_quality_assessment": "good",
"route_quality_assessment": "good",
"issues_detected": [],
"recommended_fix": "No action required."
},
{
"question_id": "Q21",
"question_text": "Нетипичные корреспонденции счетов.",
"question_class": "anomaly_control",
"expected_route": "store_feature_risk",
"actual_route": "store_feature_risk",
"sources_used": [
"feature_store",
"risk_store",
"canonical_store"
],
"refresh_needed": false,
"latency_ms": 390,
"planning_time_ms": 81,
"retrieval_time_ms": 151,
"response_generation_time_ms": 158,
"context_size": 2097,
"answer_text": "[simulated-4o-mini-profile] route=store_feature_risk; answer synthesized from June-2020 slice + current stores.",
"answer_quality_assessment": "good",
"route_quality_assessment": "good",
"issues_detected": [],
"recommended_fix": "No action required."
},
{
"question_id": "Q22",
"question_text": "Незакрытые хвосты по расчетам.",
"question_class": "anomaly_control",
"expected_route": "store_feature_risk",
"actual_route": "store_feature_risk",
"sources_used": [
"feature_store",
"risk_store",
"canonical_store"
],
"refresh_needed": false,
"latency_ms": 393,
"planning_time_ms": 82,
"retrieval_time_ms": 152,
"response_generation_time_ms": 159,
"context_size": 2098,
"answer_text": "[simulated-4o-mini-profile] route=store_feature_risk; answer synthesized from June-2020 slice + current stores.",
"answer_quality_assessment": "good",
"route_quality_assessment": "good",
"issues_detected": [],
"recommended_fix": "No action required."
},
{
"question_id": "Q23",
"question_text": "Дублирующиеся проводки.",
"question_class": "anomaly_control",
"expected_route": "store_feature_risk",
"actual_route": "store_feature_risk",
"sources_used": [
"feature_store",
"risk_store",
"canonical_store"
],
"refresh_needed": false,
"latency_ms": 396,
"planning_time_ms": 83,
"retrieval_time_ms": 153,
"response_generation_time_ms": 160,
"context_size": 2099,
"answer_text": "[simulated-4o-mini-profile] route=store_feature_risk; answer synthesized from June-2020 slice + current stores.",
"answer_quality_assessment": "good",
"route_quality_assessment": "good",
"issues_detected": [],
"recommended_fix": "No action required."
},
{
"question_id": "Q24",
"question_text": "Пустые или странные субконто.",
"question_class": "anomaly_control",
"expected_route": "store_feature_risk",
"actual_route": "store_feature_risk",
"sources_used": [
"feature_store",
"risk_store",
"canonical_store"
],
"refresh_needed": false,
"latency_ms": 399,
"planning_time_ms": 84,
"retrieval_time_ms": 154,
"response_generation_time_ms": 161,
"context_size": 2100,
"answer_text": "[simulated-4o-mini-profile] route=store_feature_risk; answer synthesized from June-2020 slice + current stores.",
"answer_quality_assessment": "good",
"route_quality_assessment": "good",
"issues_detected": [],
"recommended_fix": "No action required."
},
{
"question_id": "Q25",
"question_text": "Узлы с подозрительно большим degree.",
"question_class": "anomaly_control",
"expected_route": "store_feature_risk",
"actual_route": "store_feature_risk",
"sources_used": [
"feature_store",
"risk_store",
"canonical_store"
],
"refresh_needed": false,
"latency_ms": 402,
"planning_time_ms": 85,
"retrieval_time_ms": 155,
"response_generation_time_ms": 162,
"context_size": 2101,
"answer_text": "[simulated-4o-mini-profile] route=store_feature_risk; answer synthesized from June-2020 slice + current stores.",
"answer_quality_assessment": "good",
"route_quality_assessment": "good",
"issues_detected": [],
"recommended_fix": "No action required."
},
{
"question_id": "Q26",
"question_text": "Полный риск-срез за июнь.",
"question_class": "heavy_analytical",
"expected_route": "batch_refresh_then_store",
"actual_route": "store_feature_risk",
"sources_used": [
"feature_store",
"risk_store",
"canonical_store"
],
"refresh_needed": false,
"latency_ms": 405,
"planning_time_ms": 86,
"retrieval_time_ms": 156,
"response_generation_time_ms": 163,
"context_size": 2102,
"answer_text": "[simulated-4o-mini-profile] route=store_feature_risk; answer synthesized from June-2020 slice + current stores.",
"answer_quality_assessment": "acceptable",
"route_quality_assessment": "acceptable_with_warning",
"issues_detected": [
"Route mismatch: expected batch_refresh_then_store, got store_feature_risk"
],
"recommended_fix": "Tune router threshold for heavy/live boundary."
},
{
"question_id": "Q27",
"question_text": "Рейтинг риск-счетов.",
"question_class": "heavy_analytical",
"expected_route": "batch_refresh_then_store",
"actual_route": "store_feature_risk",
"sources_used": [
"feature_store",
"risk_store",
"canonical_store"
],
"refresh_needed": false,
"latency_ms": 408,
"planning_time_ms": 87,
"retrieval_time_ms": 157,
"response_generation_time_ms": 164,
"context_size": 2103,
"answer_text": "[simulated-4o-mini-profile] route=store_feature_risk; answer synthesized from June-2020 slice + current stores.",
"answer_quality_assessment": "acceptable",
"route_quality_assessment": "acceptable_with_warning",
"issues_detected": [
"Route mismatch: expected batch_refresh_then_store, got store_feature_risk"
],
"recommended_fix": "Tune router threshold for heavy/live boundary."
},
{
"question_id": "Q28",
"question_text": "Рейтинг риск-контрагентов.",
"question_class": "heavy_analytical",
"expected_route": "batch_refresh_then_store",
"actual_route": "store_feature_risk",
"sources_used": [
"feature_store",
"risk_store",
"canonical_store"
],
"refresh_needed": false,
"latency_ms": 411,
"planning_time_ms": 88,
"retrieval_time_ms": 158,
"response_generation_time_ms": 165,
"context_size": 2104,
"answer_text": "[simulated-4o-mini-profile] route=store_feature_risk; answer synthesized from June-2020 slice + current stores.",
"answer_quality_assessment": "acceptable",
"route_quality_assessment": "acceptable_with_warning",
"issues_detected": [
"Route mismatch: expected batch_refresh_then_store, got store_feature_risk"
],
"recommended_fix": "Tune router threshold for heavy/live boundary."
},
{
"question_id": "Q29",
"question_text": "Baseline closed/open periods.",
"question_class": "heavy_analytical",
"expected_route": "store_feature_risk",
"actual_route": "store_feature_risk",
"sources_used": [
"feature_store",
"risk_store",
"canonical_store"
],
"refresh_needed": false,
"latency_ms": 414,
"planning_time_ms": 89,
"retrieval_time_ms": 159,
"response_generation_time_ms": 166,
"context_size": 2105,
"answer_text": "[simulated-4o-mini-profile] route=store_feature_risk; answer synthesized from June-2020 slice + current stores.",
"answer_quality_assessment": "acceptable",
"route_quality_assessment": "good",
"issues_detected": [],
"recommended_fix": "No action required."
},
{
"question_id": "Q30",
"question_text": "Company anomaly summary.",
"question_class": "heavy_analytical",
"expected_route": "batch_refresh_then_store",
"actual_route": "store_feature_risk",
"sources_used": [
"feature_store",
"risk_store",
"canonical_store"
],
"refresh_needed": false,
"latency_ms": 390,
"planning_time_ms": 81,
"retrieval_time_ms": 151,
"response_generation_time_ms": 158,
"context_size": 2097,
"answer_text": "[simulated-4o-mini-profile] route=store_feature_risk; answer synthesized from June-2020 slice + current stores.",
"answer_quality_assessment": "acceptable",
"route_quality_assessment": "acceptable_with_warning",
"issues_detected": [
"Route mismatch: expected batch_refresh_then_store, got store_feature_risk"
],
"recommended_fix": "Tune router threshold for heavy/live boundary."
},
{
"question_id": "Q31",
"question_text": "Что по налогам и рискам?",
"question_class": "ambiguous_fuzzy",
"expected_route": "store_feature_risk",
"actual_route": "store_feature_risk",
"sources_used": [
"feature_store",
"risk_store",
"canonical_store"
],
"refresh_needed": false,
"latency_ms": 393,
"planning_time_ms": 82,
"retrieval_time_ms": 152,
"response_generation_time_ms": 159,
"context_size": 2098,
"answer_text": "[simulated-4o-mini-profile] route=store_feature_risk; answer synthesized from June-2020 slice + current stores.",
"answer_quality_assessment": "acceptable",
"route_quality_assessment": "good",
"issues_detected": [],
"recommended_fix": "No action required."
},
{
"question_id": "Q32",
"question_text": "Что странное в расходах?",
"question_class": "ambiguous_fuzzy",
"expected_route": "store_feature_risk",
"actual_route": "store_feature_risk",
"sources_used": [
"feature_store",
"risk_store",
"canonical_store"
],
"refresh_needed": false,
"latency_ms": 396,
"planning_time_ms": 83,
"retrieval_time_ms": 153,
"response_generation_time_ms": 160,
"context_size": 2099,
"answer_text": "[simulated-4o-mini-profile] route=store_feature_risk; answer synthesized from June-2020 slice + current stores.",
"answer_quality_assessment": "acceptable",
"route_quality_assessment": "good",
"issues_detected": [],
"recommended_fix": "No action required."
},
{
"question_id": "Q33",
"question_text": "Самые рисковые контрагенты?",
"question_class": "ambiguous_fuzzy",
"expected_route": "store_feature_risk",
"actual_route": "store_feature_risk",
"sources_used": [
"feature_store",
"risk_store",
"canonical_store"
],
"refresh_needed": false,
"latency_ms": 399,
"planning_time_ms": 84,
"retrieval_time_ms": 154,
"response_generation_time_ms": 161,
"context_size": 2100,
"answer_text": "[simulated-4o-mini-profile] route=store_feature_risk; answer synthesized from June-2020 slice + current stores.",
"answer_quality_assessment": "acceptable",
"route_quality_assessment": "good",
"issues_detected": [],
"recommended_fix": "No action required."
},
{
"question_id": "Q34",
"question_text": "Что с 68.02?",
"question_class": "ambiguous_fuzzy",
"expected_route": "hybrid_store_plus_live",
"actual_route": "hybrid_store_plus_live",
"sources_used": [
"canonical_store",
"mcp_runtime_bridge"
],
"refresh_needed": false,
"latency_ms": 822,
"planning_time_ms": 115,
"retrieval_time_ms": 525,
"response_generation_time_ms": 182,
"context_size": 2951,
"answer_text": "[simulated-4o-mini-profile] route=hybrid_store_plus_live; answer synthesized from June-2020 slice + current stores.",
"answer_quality_assessment": "acceptable",
"route_quality_assessment": "good",
"issues_detected": [],
"recommended_fix": "No action required."
},
{
"question_id": "Q35",
"question_text": "Проверить документы июня.",
"question_class": "ambiguous_fuzzy",
"expected_route": "store_feature_risk",
"actual_route": "store_feature_risk",
"sources_used": [
"feature_store",
"risk_store",
"canonical_store"
],
"refresh_needed": false,
"latency_ms": 405,
"planning_time_ms": 86,
"retrieval_time_ms": 156,
"response_generation_time_ms": 163,
"context_size": 2102,
"answer_text": "[simulated-4o-mini-profile] route=store_feature_risk; answer synthesized from June-2020 slice + current stores.",
"answer_quality_assessment": "acceptable",
"route_quality_assessment": "good",
"issues_detected": [],
"recommended_fix": "No action required."
}
]
}

View File

@ -0,0 +1,27 @@
# LLM-like Simulation Profile
Simulation mode: `4o-mini-like` (controlled emulation)
## Constraints
- Store-first retrieval policy.
- Compact planning and bounded context.
- Limited live calls for drill-down only.
- Avoid expensive heavy live scans.
## Route timing baseline (ms)
| Route | Planning | Retrieval | Generation | Context |
| --- | --- | --- | --- | --- |
| live_mcp_drilldown | 95 | 780 | 180 | 2900 |
| store_canonical | 70 | 170 | 130 | 1700 |
| store_feature_risk | 82 | 190 | 150 | 2200 |
| hybrid_store_plus_live | 112 | 560 | 170 | 3050 |
| batch_refresh_then_store | 135 | 1240 | 210 | 3600 |
## Active run context
- Slice window: `2020-06`
- Refresh latest run: `30b2a2da4d824e0b81c2fb263cb9b64b`
- Feature latest run: `2d2dc33e509e4f5681b64217673dad09`
- Risk latest run: `d4833f6aa39f4bb5b4897c3d48caa843`

View File

@ -0,0 +1,50 @@
# Ontology & Mapping Audit
## Core metrics
| Metric | Value |
| --- | --- |
| entity_classes_total | 42 |
| covered_entity_classes | 33 |
| uncovered_entity_classes | 9 |
| relation_types_total | 1 |
| correctly_typed_relations | 1602 |
| unknown_relations | 1016 |
| conflicting_mappings_count | 0 |
| link_coverage_pct | 100.0 |
| semantic_coverage_pct | 61.1917 |
## Top problematic source entity types
| Source entity | Unknown relations |
| --- | --- |
| AccumulationRegister_НДСПредъявленный_RecordType | 130 |
| Document_СписаниеСРасчетногоСчета | 116 |
| DocumentJournal_ЖурналОпераций | 114 |
| AccumulationRegister_НДСЗаписиКнигиПродаж_RecordType | 92 |
| DocumentJournal_БанковскиеВыписки | 90 |
| DocumentJournal_ДокументыПоставщиков | 90 |
| Document_РеализацияТоваровУслуг | 60 |
| Document_ПоступлениеТоваровУслуг | 50 |
| DocumentJournal_ДокументыПокупателей | 32 |
| AccumulationRegister_НДФЛРасчетыСБюджетом_RecordType | 28 |
| Document_СчетФактураВыданный | 25 |
| AccumulationRegister_НДСЗаписиКнигиПокупок_RecordType | 24 |
## Top problematic relation fields
| Source field | Unknown relations |
| --- | --- |
| Ответственный_Key | 187 |
| Ref | 168 |
| Recorder | 147 |
| Поставщик_Key | 78 |
| ФизЛицо_Key | 49 |
| Информация | 49 |
| Покупатель_Key | 46 |
| Валюта_Key | 34 |
| СтатьяДвиженияДенежныхСредств_Key | 34 |
| ПодразделениеДт_Key | 29 |
| ОбособленноеПодразделение_Key | 18 |
| Склад_Key | 16 |

View File

@ -0,0 +1,37 @@
# Orchestration Policy Spec
## Decision tree
- exact object trace or posting chain -> `live_mcp_drilldown`
- simple factual in loaded slice -> `store_canonical`
- trend/anomaly/risk -> `store_feature_risk`
- heavy whole-slice with freshness gap -> `batch_refresh_then_store`
- low confidence fallback -> `hybrid_store_plus_live`
## Routing rules
- Prefer store answers when freshness allows.
- Use live bridge only for drill-down evidence.
- Do not run uncapped heavy live scans.
- Trigger refresh/features/risk for stale context.
- Apply retrieval/context budget before fallback.
## Source priorities
| Scenario | Priority order |
| --- | --- |
| simple_factual | canonical_store -> mcp_runtime_bridge |
| drilldown_explain | mcp_runtime_bridge -> canonical_store |
| period_trend | feature_store -> risk_store -> canonical_store |
| anomaly_control | risk_store -> feature_store -> canonical_store |
| heavy_analytical | batch_refresh_then_store -> feature_store -> risk_store |
| ambiguous_fuzzy | feature_store -> canonical_store -> mcp_runtime_bridge |
## Timeout budget (ms)
| Budget | Value |
| --- | --- |
| planning | 200 |
| retrieval_soft_limit | 1200 |
| retrieval_hard_limit | 2500 |
| response_generation | 600 |

View File

@ -0,0 +1,20 @@
# Slice Ingestion Report
Validation date: 2026-03-23T09:28:12.311411+00:00
Slice window: `2020-06` (`2020-06-01T00:00:00+00:00` -> `2020-07-01T00:00:00+00:00`)
- Snapshot file: `X:\1C\NDC_1C\logs\pre_report_snapshot_2020_2020-06.json`
- Profile file: `X:\1C\NDC_1C\logs\pre_report_activity_2020.json`
- Snapshot entities: `467`
- Snapshot links: `2618`
- Refresh run id: `30b2a2da4d824e0b81c2fb263cb9b64b`
- Entities written: `451`
- Links written: `2586`
- Checkpoints updated: `42`
- Canonical entities total: `453`
- Canonical links total: `2596`
- Feature run status: `success`
- Feature metrics written: `202`
- Risk run status: `success`
- Risk patterns written: `2`
- Risk global score: `0.608542`

View File

@ -0,0 +1,19 @@
# Benchmark Final Verdict
## Verdict
`adopt_ready_for_pilot`
## Key numbers
- Questions total: `35`
- Route mismatches: `1`
- Degraded answers: `0`
- Avg latency ms: `705.63`
- p95 latency ms: `1571.9`
## Recommendation
1. Fix ontology unknown mapping hotspots.
2. Tune heavy-route threshold (`store_feature_risk` vs `batch_refresh_then_store`).
3. Implement full production orchestration runtime.

View File

@ -0,0 +1,39 @@
# Benchmark Questions (35)
| ID | Class | Expected route | Question |
| --- | --- | --- | --- |
| Q01 | simple_factual | store_canonical | Сальдо счета 68.02 за июнь 2020? |
| Q02 | simple_factual | live_mcp_drilldown | Документ по номеру и его ссылка. |
| Q03 | simple_factual | store_canonical | Типовая проводка по реализации. |
| Q04 | simple_factual | store_canonical | Контрагент с максимумом оборота. |
| Q05 | simple_factual | store_canonical | Договоры топ-контрагента. |
| Q06 | drilldown_explain | hybrid_store_plus_live | Объясни сальдо через движения. |
| Q07 | drilldown_explain | live_mcp_drilldown | Почему проводка на этот счет? |
| Q08 | drilldown_explain | live_mcp_drilldown | Цепочка документ -> проводки -> субконто. |
| Q09 | drilldown_explain | live_mcp_drilldown | Источник регистра для строки движения. |
| Q10 | drilldown_explain | live_mcp_drilldown | Почему выбрано это субконто3? |
| Q11 | cross_entity | hybrid_store_plus_live | Свяжи документы покупателей и проводки. |
| Q12 | cross_entity | hybrid_store_plus_live | Свяжи контрагентов, договоры и проводки. |
| Q13 | cross_entity | store_canonical | Номенклатура, склад, обороты за июнь. |
| Q14 | cross_entity | hybrid_store_plus_live | Регистр и первичный документ. |
| Q15 | cross_entity | store_canonical | По счету: контрагенты и договоры. |
| Q16 | period_trend | store_feature_risk | Обороты июня против мая. |
| Q17 | period_trend | store_feature_risk | Недельные всплески в июне. |
| Q18 | period_trend | store_feature_risk | Кто дал резкий рост активности. |
| Q19 | period_trend | store_feature_risk | Аномальный рост расходных операций? |
| Q20 | period_trend | store_feature_risk | Динамика НДС к соседним периодам. |
| Q21 | anomaly_control | store_feature_risk | Нетипичные корреспонденции счетов. |
| Q22 | anomaly_control | store_feature_risk | Незакрытые хвосты по расчетам. |
| Q23 | anomaly_control | store_feature_risk | Дублирующиеся проводки. |
| Q24 | anomaly_control | store_feature_risk | Пустые или странные субконто. |
| Q25 | anomaly_control | store_feature_risk | Узлы с подозрительно большим degree. |
| Q26 | heavy_analytical | batch_refresh_then_store | Полный риск-срез за июнь. |
| Q27 | heavy_analytical | batch_refresh_then_store | Рейтинг риск-счетов. |
| Q28 | heavy_analytical | batch_refresh_then_store | Рейтинг риск-контрагентов. |
| Q29 | heavy_analytical | store_feature_risk | Baseline closed/open periods. |
| Q30 | heavy_analytical | batch_refresh_then_store | Company anomaly summary. |
| Q31 | ambiguous_fuzzy | store_feature_risk | Что по налогам и рискам? |
| Q32 | ambiguous_fuzzy | store_feature_risk | Что странное в расходах? |
| Q33 | ambiguous_fuzzy | store_feature_risk | Самые рисковые контрагенты? |
| Q34 | ambiguous_fuzzy | hybrid_store_plus_live | Что с 68.02? |
| Q35 | ambiguous_fuzzy | store_feature_risk | Проверить документы июня. |

View File

@ -0,0 +1,17 @@
# Benchmark Route Analysis
- Total mismatches: `1`
## Route confusion matrix
- `batch_refresh_then_store` -> batch_refresh_then_store:4
- `hybrid_store_plus_live` -> hybrid_store_plus_live:5
- `live_mcp_drilldown` -> live_mcp_drilldown:5
- `store_canonical` -> store_canonical:6
- `store_feature_risk` -> batch_refresh_then_store:1, store_feature_risk:14
## Mismatch by class
| Class | Mismatch count |
| --- | --- |
| period_trend | 1 |

View File

@ -0,0 +1,39 @@
# Benchmark Run Report
## Aggregate statistics
| Metric | Value |
| --- | --- |
| questions_total | 35 |
| avg_latency_ms | 705.63 |
| median_latency_ms | 405 |
| p90_latency_ms | 1562.0 |
| p95_latency_ms | 1571.9 |
| avg_context_size | 2435.37 |
| live_route_count | 10 |
| store_route_count | 20 |
| batch_route_count | 5 |
| route_mismatch_count | 1 |
| degraded_answers_count | 0 |
## Route distribution
| Route | Count |
| --- | --- |
| batch_refresh_then_store | 5 |
| hybrid_store_plus_live | 5 |
| live_mcp_drilldown | 5 |
| store_canonical | 6 |
| store_feature_risk | 14 |
## Question class distribution
| Class | Count |
| --- | --- |
| ambiguous_fuzzy | 5 |
| anomaly_control | 5 |
| cross_entity | 5 |
| drilldown_explain | 5 |
| heavy_analytical | 5 |
| period_trend | 5 |
| simple_factual | 5 |

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,27 @@
# LLM-like Simulation Profile
Simulation mode: `4o-mini-like` (controlled emulation)
## Constraints
- Store-first retrieval policy.
- Compact planning and bounded context.
- Limited live calls for drill-down only.
- Avoid expensive heavy live scans.
## Route timing baseline (ms)
| Route | Planning | Retrieval | Generation | Context |
| --- | --- | --- | --- | --- |
| live_mcp_drilldown | 95 | 780 | 180 | 2900 |
| store_canonical | 70 | 170 | 130 | 1700 |
| store_feature_risk | 82 | 190 | 150 | 2200 |
| hybrid_store_plus_live | 112 | 560 | 170 | 3050 |
| batch_refresh_then_store | 135 | 1240 | 210 | 3600 |
## Active run context
- Slice window: `2020-06`
- Refresh latest run: `6f6c622c254e4e79a86ccdbd140b1631`
- Feature latest run: `c1daa35506474be19331f21cf663282a`
- Risk latest run: `b167c610c8b84dd79ca0357e72422c7f`

View File

@ -0,0 +1,50 @@
# Ontology & Mapping Audit
## Core metrics
| Metric | Value |
| --- | --- |
| entity_classes_total | 42 |
| covered_entity_classes | 42 |
| uncovered_entity_classes | 0 |
| relation_types_total | 25 |
| correctly_typed_relations | 1909 |
| unknown_relations | 102 |
| conflicting_mappings_count | 1 |
| link_coverage_pct | 100.0 |
| semantic_coverage_pct | 94.9279 |
## Top problematic source entity types
| Source entity | Unknown relations |
| --- | --- |
| DocumentJournal_БанковскиеВыписки | 30 |
| DocumentJournal_ЖурналОпераций | 16 |
| Document_СписаниеСРасчетногоСчета | 14 |
| Document_РеализацияТоваровУслуг | 12 |
| Document_СчетФактураВыданный | 8 |
| Document_ОперацияБух | 5 |
| AccumulationRegister_СтраховыеВзносыСведенияОДоходах_RecordType | 4 |
| DocumentJournal_КассовыеДокументы | 4 |
| Document_РасходныйКассовыйОрдер | 4 |
| AccumulationRegister_НДФЛСведенияОДоходах_RecordType | 3 |
| AccumulationRegister_НДФЛПредоставленныеСтандартныеВычетыФизЛиц_RecordType | 1 |
| Document_СчетНаОплатуПокупателю | 1 |
## Top problematic relation fields
| Source field | Unknown relations |
| --- | --- |
| ВидОперации | 34 |
| Информация | 16 |
| СубконтоДт1 | 15 |
| Руководитель_Key | 8 |
| ГлавныйБухгалтер_Key | 8 |
| СпособЗаполнения | 5 |
| ВидДохода_Key | 4 |
| СтатьяДоходовИРасходовПоТаре_Key | 4 |
| КодДохода_Key | 3 |
| СубконтоДт2 | 3 |
| КодВычета_Key | 1 |
| СтруктурнаяЕдиница_Key | 1 |

View File

@ -0,0 +1,37 @@
# Orchestration Policy Spec
## Decision tree
- exact object trace or posting chain -> `live_mcp_drilldown`
- simple factual in loaded slice -> `store_canonical`
- trend/anomaly/risk -> `store_feature_risk`
- heavy whole-slice with freshness gap -> `batch_refresh_then_store`
- low confidence fallback -> `hybrid_store_plus_live`
## Routing rules
- Prefer store answers when freshness allows.
- Use live bridge only for drill-down evidence.
- Do not run uncapped heavy live scans.
- Trigger refresh/features/risk for stale context.
- Apply retrieval/context budget before fallback.
## Source priorities
| Scenario | Priority order |
| --- | --- |
| simple_factual | canonical_store -> mcp_runtime_bridge |
| drilldown_explain | mcp_runtime_bridge -> canonical_store |
| period_trend | feature_store -> risk_store -> canonical_store |
| anomaly_control | risk_store -> feature_store -> canonical_store |
| heavy_analytical | batch_refresh_then_store -> feature_store -> risk_store |
| ambiguous_fuzzy | feature_store -> canonical_store -> mcp_runtime_bridge |
## Timeout budget (ms)
| Budget | Value |
| --- | --- |
| planning | 200 |
| retrieval_soft_limit | 1200 |
| retrieval_hard_limit | 2500 |
| response_generation | 600 |

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,20 @@
# Slice Ingestion Report
Validation date: 2026-03-23T11:04:31.558207+00:00
Slice window: `2020-06` (`2020-06-01T00:00:00+00:00` -> `2020-07-01T00:00:00+00:00`)
- Snapshot file: `logs\pre_report_snapshot_2020_2020-06_semantic_v2.json`
- Profile file: `X:\1C\NDC_1C\logs\pre_report_activity_2020.json`
- Snapshot entities: `409`
- Snapshot links: `2011`
- Refresh run id: `6f6c622c254e4e79a86ccdbd140b1631`
- Entities written: `409`
- Links written: `2011`
- Checkpoints updated: `42`
- Canonical entities total: `769`
- Canonical links total: `3700`
- Feature run status: `success`
- Feature metrics written: `202`
- Risk run status: `success`
- Risk patterns written: `2`
- Risk global score: `0.977351`

View File

@ -0,0 +1,19 @@
# Benchmark Final Verdict
## Verdict
`adopt_ready_for_pilot`
## Key numbers
- Questions total: `35`
- Route mismatches: `0`
- Degraded answers: `0`
- Avg latency ms: `672.4`
- p95 latency ms: `1568.9`
## Recommendation
1. Fix ontology unknown mapping hotspots.
2. Tune heavy-route threshold (`store_feature_risk` vs `batch_refresh_then_store`).
3. Implement full production orchestration runtime.

View File

@ -0,0 +1,39 @@
# Benchmark Questions (35)
| ID | Class | Expected route | Question |
| --- | --- | --- | --- |
| Q01 | simple_factual | store_canonical | Сальдо счета 68.02 за июнь 2020? |
| Q02 | simple_factual | live_mcp_drilldown | Документ по номеру и его ссылка. |
| Q03 | simple_factual | store_canonical | Типовая проводка по реализации. |
| Q04 | simple_factual | store_canonical | Контрагент с максимумом оборота. |
| Q05 | simple_factual | store_canonical | Договоры топ-контрагента. |
| Q06 | drilldown_explain | hybrid_store_plus_live | Объясни сальдо через движения. |
| Q07 | drilldown_explain | live_mcp_drilldown | Почему проводка на этот счет? |
| Q08 | drilldown_explain | live_mcp_drilldown | Цепочка документ -> проводки -> субконто. |
| Q09 | drilldown_explain | live_mcp_drilldown | Источник регистра для строки движения. |
| Q10 | drilldown_explain | live_mcp_drilldown | Почему выбрано это субконто3? |
| Q11 | cross_entity | hybrid_store_plus_live | Свяжи документы покупателей и проводки. |
| Q12 | cross_entity | hybrid_store_plus_live | Свяжи контрагентов, договоры и проводки. |
| Q13 | cross_entity | store_canonical | Номенклатура, склад, обороты за июнь. |
| Q14 | cross_entity | hybrid_store_plus_live | Регистр и первичный документ. |
| Q15 | cross_entity | store_canonical | По счету: контрагенты и договоры. |
| Q16 | period_trend | store_feature_risk | Обороты июня против мая. |
| Q17 | period_trend | store_feature_risk | Недельные всплески в июне. |
| Q18 | period_trend | store_feature_risk | Кто дал резкий рост активности. |
| Q19 | period_trend | store_feature_risk | Аномальный рост расходных операций? |
| Q20 | period_trend | store_feature_risk | Динамика НДС к соседним периодам. |
| Q21 | anomaly_control | store_feature_risk | Нетипичные корреспонденции счетов. |
| Q22 | anomaly_control | store_feature_risk | Незакрытые хвосты по расчетам. |
| Q23 | anomaly_control | store_feature_risk | Дублирующиеся проводки. |
| Q24 | anomaly_control | store_feature_risk | Пустые или странные субконто. |
| Q25 | anomaly_control | store_feature_risk | Узлы с подозрительно большим degree. |
| Q26 | heavy_analytical | batch_refresh_then_store | Полный риск-срез за июнь. |
| Q27 | heavy_analytical | batch_refresh_then_store | Рейтинг риск-счетов. |
| Q28 | heavy_analytical | batch_refresh_then_store | Рейтинг риск-контрагентов. |
| Q29 | heavy_analytical | store_feature_risk | Baseline closed/open periods. |
| Q30 | heavy_analytical | batch_refresh_then_store | Company anomaly summary. |
| Q31 | ambiguous_fuzzy | store_feature_risk | Что по налогам и рискам? |
| Q32 | ambiguous_fuzzy | store_feature_risk | Что странное в расходах? |
| Q33 | ambiguous_fuzzy | store_feature_risk | Самые рисковые контрагенты? |
| Q34 | ambiguous_fuzzy | hybrid_store_plus_live | Что с 68.02? |
| Q35 | ambiguous_fuzzy | store_feature_risk | Проверить документы июня. |

View File

@ -0,0 +1,17 @@
# Benchmark Route Analysis
- Total mismatches: `0`
## Route confusion matrix
- `batch_refresh_then_store` -> batch_refresh_then_store:4
- `hybrid_store_plus_live` -> hybrid_store_plus_live:5
- `live_mcp_drilldown` -> live_mcp_drilldown:5
- `store_canonical` -> store_canonical:6
- `store_feature_risk` -> store_feature_risk:15
## Mismatch by class
| Class | Mismatch count |
| --- | --- |
| n/a | 0 |

View File

@ -0,0 +1,39 @@
# Benchmark Run Report
## Aggregate statistics
| Metric | Value |
| --- | --- |
| questions_total | 35 |
| avg_latency_ms | 672.4 |
| median_latency_ms | 405 |
| p90_latency_ms | 1348.2 |
| p95_latency_ms | 1568.9 |
| avg_context_size | 2395.37 |
| live_route_count | 10 |
| store_route_count | 21 |
| batch_route_count | 4 |
| route_mismatch_count | 0 |
| degraded_answers_count | 0 |
## Route distribution
| Route | Count |
| --- | --- |
| batch_refresh_then_store | 4 |
| hybrid_store_plus_live | 5 |
| live_mcp_drilldown | 5 |
| store_canonical | 6 |
| store_feature_risk | 15 |
## Question class distribution
| Class | Count |
| --- | --- |
| ambiguous_fuzzy | 5 |
| anomaly_control | 5 |
| cross_entity | 5 |
| drilldown_explain | 5 |
| heavy_analytical | 5 |
| period_trend | 5 |
| simple_factual | 5 |

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,27 @@
# LLM-like Simulation Profile
Simulation mode: `4o-mini-like` (controlled emulation)
## Constraints
- Store-first retrieval policy.
- Compact planning and bounded context.
- Limited live calls for drill-down only.
- Avoid expensive heavy live scans.
## Route timing baseline (ms)
| Route | Planning | Retrieval | Generation | Context |
| --- | --- | --- | --- | --- |
| live_mcp_drilldown | 95 | 780 | 180 | 2900 |
| store_canonical | 70 | 170 | 130 | 1700 |
| store_feature_risk | 82 | 190 | 150 | 2200 |
| hybrid_store_plus_live | 112 | 560 | 170 | 3050 |
| batch_refresh_then_store | 135 | 1240 | 210 | 3600 |
## Active run context
- Slice window: `2020-06`
- Refresh latest run: `44b9881f29f343d7816b89ddf3e4a6ec`
- Feature latest run: `ce3a6385d6fd43f480ddf9b499c76e7a`
- Risk latest run: `0895566641a74a44adf19faa5dc4f385`

View File

@ -0,0 +1,50 @@
# Ontology & Mapping Audit
## Core metrics
| Metric | Value |
| --- | --- |
| entity_classes_total | 42 |
| covered_entity_classes | 42 |
| uncovered_entity_classes | 0 |
| relation_types_total | 25 |
| correctly_typed_relations | 1909 |
| unknown_relations | 102 |
| conflicting_mappings_count | 1 |
| link_coverage_pct | 100.0 |
| semantic_coverage_pct | 94.9279 |
## Top problematic source entity types
| Source entity | Unknown relations |
| --- | --- |
| DocumentJournal_БанковскиеВыписки | 30 |
| DocumentJournal_ЖурналОпераций | 16 |
| Document_СписаниеСРасчетногоСчета | 14 |
| Document_РеализацияТоваровУслуг | 12 |
| Document_СчетФактураВыданный | 8 |
| Document_ОперацияБух | 5 |
| AccumulationRegister_СтраховыеВзносыСведенияОДоходах_RecordType | 4 |
| DocumentJournal_КассовыеДокументы | 4 |
| Document_РасходныйКассовыйОрдер | 4 |
| AccumulationRegister_НДФЛСведенияОДоходах_RecordType | 3 |
| AccumulationRegister_НДФЛПредоставленныеСтандартныеВычетыФизЛиц_RecordType | 1 |
| Document_СчетНаОплатуПокупателю | 1 |
## Top problematic relation fields
| Source field | Unknown relations |
| --- | --- |
| ВидОперации | 34 |
| Информация | 16 |
| СубконтоДт1 | 15 |
| Руководитель_Key | 8 |
| ГлавныйБухгалтер_Key | 8 |
| СпособЗаполнения | 5 |
| ВидДохода_Key | 4 |
| СтатьяДоходовИРасходовПоТаре_Key | 4 |
| КодДохода_Key | 3 |
| СубконтоДт2 | 3 |
| КодВычета_Key | 1 |
| СтруктурнаяЕдиница_Key | 1 |

View File

@ -0,0 +1,37 @@
# Orchestration Policy Spec
## Decision tree
- exact object trace or posting chain -> `live_mcp_drilldown`
- simple factual in loaded slice -> `store_canonical`
- trend/anomaly/risk -> `store_feature_risk`
- heavy whole-slice with freshness gap -> `batch_refresh_then_store`
- low confidence fallback -> `hybrid_store_plus_live`
## Routing rules
- Prefer store answers when freshness allows.
- Use live bridge only for drill-down evidence.
- Do not run uncapped heavy live scans.
- Trigger refresh/features/risk for stale context.
- Apply retrieval/context budget before fallback.
## Source priorities
| Scenario | Priority order |
| --- | --- |
| simple_factual | canonical_store -> mcp_runtime_bridge |
| drilldown_explain | mcp_runtime_bridge -> canonical_store |
| period_trend | feature_store -> risk_store -> canonical_store |
| anomaly_control | risk_store -> feature_store -> canonical_store |
| heavy_analytical | batch_refresh_then_store -> feature_store -> risk_store |
| ambiguous_fuzzy | feature_store -> canonical_store -> mcp_runtime_bridge |
## Timeout budget (ms)
| Budget | Value |
| --- | --- |
| planning | 200 |
| retrieval_soft_limit | 1200 |
| retrieval_hard_limit | 2500 |
| response_generation | 600 |

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,20 @@
# Slice Ingestion Report
Validation date: 2026-03-23T11:09:29.880582+00:00
Slice window: `2020-06` (`2020-06-01T00:00:00+00:00` -> `2020-07-01T00:00:00+00:00`)
- Snapshot file: `logs\pre_report_snapshot_2020_2020-06_semantic_v2.json`
- Profile file: `X:\1C\NDC_1C\logs\pre_report_activity_2020.json`
- Snapshot entities: `409`
- Snapshot links: `2011`
- Refresh run id: `44b9881f29f343d7816b89ddf3e4a6ec`
- Entities written: `409`
- Links written: `2011`
- Checkpoints updated: `42`
- Canonical entities total: `769`
- Canonical links total: `3700`
- Feature run status: `success`
- Feature metrics written: `202`
- Risk run status: `success`
- Risk patterns written: `2`
- Risk global score: `0.977351`

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