fix(recognize): party blocks belong before signatures, not at top

- Prompt: explicit rule that party blocks always go at the end, before signatures;
  preamble in the beginning is a paragraph, not party
- Postprocess: deterministic reorder pulls party blocks to the position right
  before the last signatures block (or to the end if no signatures)

Existing imported templates can be re-imported, or party blocks moved manually
in the editor (↑↓ buttons). New imports come out in correct order.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
admin
2026-05-01 12:55:04 +03:00
parent 2a03c1f450
commit 973d85c1dd
+41 -10
View File
@@ -57,15 +57,20 @@ const SYSTEM_PROMPT = `Ты — парсер юридических докуме
}
Важные правила:
1. Если в документе встречается реквизитный блок одной из сторон (ИНН/КПП/адрес/банк) — выдай ОДИН блок "party" с правильным role, не добавляй параграф с текстовым перечислением реквизитов.
2. Услуги/работы в табличной форме — НЕ переписывай построчно, ставь блок "services_table" (lines пустой массив, строки пользователь добавит вручную).
3. Если есть итоговые суммы (Итого, в т.ч. НДС, сумма прописью) — ставь блок "totals" со включенными нужными опциями.
4. Подписи в конце — блок "signatures" с указанием каких сторон видно.
5. Нумерованные пункты «1. Предмет договора», «2. Цена и порядок расчётов» и т.п. — каждый раздел как блок "terms" (заголовок раздела можно положить отдельным "heading" level 2).
6. Не выдумывай блоки, которых нет в документе.
7. text внутри блоков — обычная строка (не TipTap JSON), может содержать \\n для новых параграфов внутри блока.
8. Заполняй "title" коротким названием документа.
9. Если не уверен в типе документа — ставь "contract".`;
1. Блоки "party" (реквизиты сторон) ВСЕГДА ставь в конце документа — НЕПОСРЕДСТВЕННО ПЕРЕД блоком "signatures". Это адреса/банковские реквизиты, они идут в самом низу.
2. Преамбула в начале документа («ООО Х в лице ..., далее «Исполнитель», и ИП Y, заключили договор...») — это блок "paragraph", а не "party". Конкретные реквизиты не дублируй, имя стороны заменяй плейсхолдерами.
3. Если в документе встречается развёрнутый блок реквизитов стороны (ИНН/КПП/адрес/банк/р/счёт перечислены явно) — выдай ОДИН блок "party" с правильным role, БЕЗ дублирования текстом.
4. Услуги/работы в табличной форме — НЕ переписывай построчно, ставь блок "services_table" (lines пустой массив, строки пользователь добавит вручную).
5. Если есть итоговые суммы (Итого, в т.ч. НДС, сумма прописью) — ставь блок "totals" со включенными нужными опциями.
6. Подписи в конце — блок "signatures" с указанием каких сторон видно.
7. Нумерованные пункты «1. Предмет договора», «2. Цена и порядок расчётов» и т.п. — каждый раздел как блок "terms" (заголовок раздела можно положить отдельным "heading" level 2).
8. Не выдумывай блоки, которых нет в документе.
9. text внутри блоков — обычная строка (не TipTap JSON), может содержать \\n для новых параграфов внутри блока.
10. Заполняй "title" коротким названием документа.
11. Если не уверен в типе документа — ставь "contract".
Типичный порядок блоков:
heading → (опц.) paragraph-преамбула → terms (1. Предмет) → terms (2. Цена) → services_table → totals → terms (остальные пункты) → heading "Адреса и реквизиты сторон" → party (executor) → party (customer) → signatures.`;
function uid(): string {
return Math.random().toString(36).slice(2, 11);
@@ -82,10 +87,36 @@ function plainToRich(text: string): unknown {
};
}
/**
* Переставляет блоки в правильный порядок: party-блоки уходят в конец, перед signatures.
* Гарантия даже если LLM проигнорировала промпт.
*/
function reorderForFinalLayout<T extends { type: string }>(blocks: T[]): T[] {
const parties: T[] = [];
const rest: T[] = [];
for (const b of blocks) {
if (b.type === 'party') parties.push(b);
else rest.push(b);
}
if (parties.length === 0) return blocks;
// Найти ПОСЛЕДНИЙ signatures — party вставляется перед ним
let sigIdx = -1;
for (let i = rest.length - 1; i >= 0; i--) {
if (rest[i]!.type === 'signatures') {
sigIdx = i;
break;
}
}
if (sigIdx === -1) return [...rest, ...parties];
return [...rest.slice(0, sigIdx), ...parties, ...rest.slice(sigIdx)];
}
function llmToDocBody(result: LlmResult): { docBody: unknown; docType: DocType; title: string } {
const docType: DocType = (result.docType as DocType) ?? 'contract';
const title = result.title ?? 'Документ';
const blocks = (result.blocks ?? []).map((b) => {
const orderedRaw = reorderForFinalLayout(result.blocks ?? []);
const blocks = orderedRaw.map((b) => {
const id = uid();
switch (b.type) {
case 'heading':