v1.0.11: differ.cfm — сравнение spec_rows между допниками

This commit is contained in:
2026-06-18 18:13:33 +04:00
parent c04b2a2fe3
commit c8605b3cb6
4 changed files with 323 additions and 1 deletions
+80
View File
@@ -0,0 +1,80 @@
# Техническое задание (ТЗ)
## Разработка ИИ-агента «Сверка договоров»
---
## 1. Общие сведения и цель проекта
**Цель проекта:** Автоматизация процесса анализа, структурирования и отслеживания хронологии изменений в цепочках договоров и дополнительных соглашений (ДС) с клиентами с использованием локальной языковой модели (LLM).
**Ключевое свойство системы:** ИИ-агент должен работать строго в режиме «Экстракция и сопоставление на основе фактов». Фантазирование (галлюцинации) недопустимо. Если подзадача не может быть решена из-за нехватки данных или двусмысленности, система должна явно маркировать её как **«нерешаемую для ИИ»** и передавать на ручную обработку.
---
## 2. Архитектурные ограничения и безопасность
- **Конфиденциальность данных:** Информация в документах является строго конфиденциальной. Применение внешних облачных API (OpenAI, Anthropic и др.) **категорически запрещено**.
- **Инфраструктура:** Решение должно быть развернуто локально / в закрытом контуре компании на базе собственной (on-premise) модели.
---
## 3. Функциональные требования
Система должна последовательно выполнять три основные бизнес-задачи.
### Этап 1. Парсинг и структурирование спецификаций
| Параметр | Описание |
|----------|----------|
| **Входные данные** | Файлы договоров и ДС в форматах Word (`.doc`, `.docx`) и PDF (сканы и текстовые слои) |
| **Действие агента** | Извлечение табличных данных и текстовых спецификаций. Преобразование неструктурированного текста в JSON / базу данных |
| **Требования к выходу** | Четко структурированный массив строк (услуги, объемы, цены, условия). Если скан нечитаем или структура таблицы нарушена так, что парсинг невозможен, этап маркируется как **«Ошибка парсинга / Требуется ручной ввод»** |
### Этап 2. Построение кумулятивного статуса договора во времени
| Параметр | Описание |
|----------|----------|
| **Входные данные** | Цепочка документов (Основной договор → ДС №1 → ДС №2 → …), отсортированная по хронологии |
| **Действие агента** | Реконструкция истории изменений |
Агент должен уметь обрабатывать **два типа ДС**:
1. **Полное обновление** — ДС утверждает новую редакцию спецификации (полная замена статуса).
2. **Точечные изменения** — ДС меняет только отдельные позиции. Агент должен идентифицировать конкретную измененную строку в основном договоре и применить изменения (изменение цены, добавление позиции, аннулирование строки).
| Требования к выходу | Кумулятивная (актуальная на выбранную дату) спецификация договора + лог изменений по каждой строке. Если агент не может однозначно связать строку из ДС со строкой из договора, строка помечается статусом **«Конфликт изменений / Невозможно сопоставить»** |
### Этап 3. Мэтчинг артикулов с каталогом услуг
| Параметр | Описание |
|----------|----------|
| **Входные данные** | Текстовое описание позиции из спецификации договора (коды и артикулы в договорах отсутствуют) и Эталонный каталог услуг компании (с артикулами) |
| **Действие агента** | Семантическое сопоставление (мэтчинг) описания из договора с позицией каталога для присвоения артикула |
| **Требования к выходу** | Присвоенный артикул с коэффициентом уверенности (confidence score) |
> **Важно:** Процент успешного сопоставления на этом этапе может быть небольшим. При любых сомнениях (метрика уверенности ниже заданного порога или наличие нескольких похожих услуг в каталоге) агент обязан присвоить статус **«Артикул не определен / Требуется ручная привязка»**, избегая ложных срабатываний.
---
## 4. Требования к обработке исключений («Не-фантазирование»)
Для обеспечения надежности агент на каждом шаге должен руководствоваться правилом:
> **«Лучше отказ от распознавания, чем выдуманный результат».**
Система должна поддерживать **ролевую модель уверенности ИИ**:
| Статус | Описание |
|--------|----------|
| **SUCCESS** | Задача решена с высокой степенью уверенности |
| **UNRESOLVED** | Подзадача признана нерешаемой (причины: разрыв логической цепочки в ДС, отсутствие похожих позиций в каталоге, противоречащие друг другу пункты). Потребуется интерфейс для разбора таких кейсов оператором-человеком |
---
## 5. Ожидаемый результат и формат поставки
| Компонент | Описание |
|-----------|----------|
| **Пайплайн обработки документов** | Модули OCR / парсинга, логический блок работы с локальной LLM, модуль сборки кумулятивного статуса |
| **База данных** | Хранилище для структурированных версий договоров и истории их изменений |
| **UI / Экран оператора** (опционально, для MVP) | Интерфейс, где выводятся результаты сверки и подсвечиваются «проблемные» зоны, которые ИИ-агент отметил как нерешаемые |
+74
View File
@@ -0,0 +1,74 @@
# ТЗ: Агент «Сверка договоров»
## Контекст
Есть договоры и дополнительные соглашения (допники) с клиентами. Файлы в форматах Word (.docx) и PDF. Данные строго конфиденциальны — обработка только на локальной модели (без внешних API).
---
## Задача 1: Разбор спецификации в структурированный вид
**Вход:** файл договора или допника (docx / pdf)
**Что делать:**
- Извлечь текст из файла
- Найти и распарсить спецификацию (таблица услуг/товаров)
- Вывести структуру: номер строки, наименование, количество, единица измерения, цена, сумма
- Если таблица не найдена или не поддаётся разбору — отметить задачу как **нерешаемую** с указанием причины (нет таблицы / нечитаемый PDF / нестандартный формат)
**Выход:** JSON или таблица со строками спецификации
---
## Задача 2: Кумулятивный статус договора по цепочке допников
**Вход:** базовый договор + список допников в хронологическом порядке
**Что делать:**
1. Взять спецификацию базового договора как начальное состояние
2. Для каждого допника определить тип изменений:
- **Полная замена** — допник содержит новую полную спецификацию → заменить текущее состояние целиком
- **Частичное изменение** — допник содержит только изменённые строки → идентифицировать строку (по наименованию / номеру позиции) и применить изменение
3. Строить историю состояний: дата допника → что изменилось → текущее состояние
4. Если строку из допника невозможно сопоставить с текущим состоянием — отметить эту строку как **нерешаемую** (нет совпадения), не применять изменение, продолжить обработку остальных
**Выход:**
- История изменений (timeline): дата / документ → изменения
- Итоговое состояние спецификации на последнюю дату
---
## Задача 3: Сопоставление позиций с каталогом услуг (опционально)
**Вход:** спецификация договора (из задачи 1) + каталог услуг (артикул + описание)
**Что делать:**
- По текстовому описанию строки спецификации найти ближайший артикул в каталоге
- Использовать семантическое сравнение (embedding / fuzzy match)
- Если уверенность совпадения ниже порога — отметить строку как **нерешаемую** (нет уверенного совпадения), не назначать артикул
**Выход:** спецификация с добавленным полем `артикул` (или пометкой `не определено`)
**Примечание:** задача реализуема только в части случаев. Приоритет — не ошибиться, а не охватить всё.
---
## Общие требования
| Требование | Описание |
|---|---|
| Модель | Только локальная (без внешних API) |
| Конфиденциальность | Файлы не покидают локальную среду |
| Обработка исключений | На каждом шаге — явная пометка нерешаемых подзадач с причиной. Не фантазировать, не домысливать |
| Форматы входа | .docx, .pdf |
| Формат выхода | JSON + человекочитаемый отчёт (md или html) |
| Логирование | Каждый шаг (файл → задача → результат / ошибка) фиксируется |
---
## Критерии успеха
- Структурированная спецификация получена или задача явно помечена как нерешаемая
- История изменений по допникам построена корректно
- Ни одно изменение не применено «наугад» без идентификации строки
- Итоговый отчёт читаем и проверяем вручную
+168
View File
@@ -0,0 +1,168 @@
<cfsetting showdebugoutput="no" enablecfoutputonly="true">
<cfheader name="Content-Type" value="application/json; charset=utf-8">
<cfparam name="url.contract_id" default="">
<cfset result = {ok: false, error: ""}>
<cftry>
<cfif NOT len(url.contract_id)>
<cfset result.error = "contract_id required">
<cfelse>
<!--- Берём два последних допника по дате --->
<cfquery name="sups" datasource="baza">
SELECT id, type, created_at
FROM supplements
WHERE contract_id = <cfqueryparam value="#url.contract_id#" cfsqltype="cf_sql_varchar">
ORDER BY created_at DESC
LIMIT 2
</cfquery>
<cfif sups.recordCount LT 2>
<cfset result.error = "need at least 2 supplements, found " & sups.recordCount>
<cfelse>
<cfset supNew = sups.id[1]>
<cfset supOld = sups.id[2]>
<!--- Читаем spec_rows для обоих --->
<cfquery name="rowsOld" datasource="baza">
SELECT row_num, name, price, qty, sum, date_start
FROM spec_rows
WHERE supplement_id = <cfqueryparam value="#supOld#" cfsqltype="cf_sql_varchar">
ORDER BY row_num
</cfquery>
<cfquery name="rowsNew" datasource="baza">
SELECT row_num, name, price, qty, sum, date_start
FROM spec_rows
WHERE supplement_id = <cfqueryparam value="#supNew#" cfsqltype="cf_sql_varchar">
ORDER BY row_num
</cfquery>
<!--- Индексация по row_num --->
<cfset oldByNum = {}>
<cfloop query="rowsOld">
<cfset oldByNum[rowsOld.row_num] = {
name: rowsOld.name,
price: rowsOld.price,
qty: rowsOld.qty,
sum: rowsOld.sum,
date_start: rowsOld.date_start
}>
</cfloop>
<cfset newByNum = {}>
<cfloop query="rowsNew">
<cfset newByNum[rowsNew.row_num] = {
name: rowsNew.name,
price: rowsNew.price,
qty: rowsNew.qty,
sum: rowsNew.sum,
date_start: rowsNew.date_start
}>
</cfloop>
<!--- Собираем все row_num --->
<cfset allNums = structKeyArray(oldByNum)>
<cfloop array="#structKeyArray(newByNum)#" index="n">
<cfif NOT structKeyExists(oldByNum, n)>
<cfset arrayAppend(allNums, n)>
</cfif>
</cfloop>
<cfset arraySort(allNums, "numeric")>
<!--- Сравниваем --->
<cfset changes = []>
<cfset fields = ["name", "price", "qty", "sum", "date_start"]>
<cfloop array="#allNums#" index="num">
<cfset inOld = structKeyExists(oldByNum, num)>
<cfset inNew = structKeyExists(newByNum, num)>
<cfif inOld AND NOT inNew>
<cfset arrayAppend(changes, {
row_num: num,
change_type: "deleted",
old_values: oldByNum[num],
new_values: {}
})>
<cfelseif NOT inOld AND inNew>
<cfset arrayAppend(changes, {
row_num: num,
change_type: "added",
old_values: {},
new_values: newByNum[num]
})>
<cfelse>
<cfset oldVals = oldByNum[num]>
<cfset newVals = newByNum[num]>
<cfset same = true>
<cfset changedFields = {}>
<cfloop array="#fields#" index="f">
<cfif structKeyExists(oldVals, f) AND structKeyExists(newVals, f)>
<cfset ov = oldVals[f]>
<cfset nv = newVals[f]>
<cfif toString(ov) NEQ toString(nv)>
<cfset same = false>
<cfset changedFields[f] = {old: ov, new: nv}>
</cfif>
<cfelseif structKeyExists(oldVals, f) OR structKeyExists(newVals, f)>
<cfset same = false>
<cfset changedFields[f] = {
old: structKeyExists(oldVals, f) ? oldVals[f] : "",
new: structKeyExists(newVals, f) ? newVals[f] : ""
}>
</cfif>
</cfloop>
<cfset arrayAppend(changes, {
row_num: num,
change_type: same ? "unchanged" : "changed",
old_values: oldVals,
new_values: newVals,
changed_fields: changedFields
})>
</cfif>
</cfloop>
<!--- Сохраняем в spec_history --->
<cfloop array="#changes#" index="ch">
<cfquery datasource="baza">
INSERT INTO spec_history (contract_id, supplement_id, row_num, change_type, old_values, new_values)
VALUES (
<cfqueryparam value="#url.contract_id#" cfsqltype="cf_sql_varchar">,
<cfqueryparam value="#supNew#" cfsqltype="cf_sql_varchar">,
<cfqueryparam value="#ch.row_num#" cfsqltype="cf_sql_integer">,
<cfqueryparam value="#ch.change_type#" cfsqltype="cf_sql_varchar">,
<cfqueryparam value="#serializeJSON(ch.old_values)#" cfsqltype="cf_sql_varchar">::jsonb,
<cfqueryparam value="#serializeJSON(ch.new_values)#" cfsqltype="cf_sql_varchar">::jsonb
)
</cfquery>
</cfloop>
<cfset result = {
ok: true,
contract_id: url.contract_id,
supplement_old: supOld,
supplement_new: supNew,
changes: changes,
summary: {
added: 0,
deleted: 0,
changed: 0,
unchanged: 0
}
}>
<cfloop array="#changes#" index="ch">
<cfset result.summary[ch.change_type] = result.summary[ch.change_type] + 1>
</cfloop>
</cfif>
</cfif>
<cfcatch>
<cfset result = {ok: false, error: cfcatch.message, detail: cfcatch.detail}>
</cfcatch>
</cftry>
<cfoutput>#serializeJSON(result)#</cfoutput>
+1 -1
View File
@@ -1 +1 @@
<cfsetting showdebugoutput=no enablecfoutputonly=yes><cfoutput>OK v1.0.10</cfoutput>
<cfsetting showdebugoutput=no enablecfoutputonly=yes><cfoutput>OK v1.0.11</cfoutput>