From 973d85c1dd5c39a7ba71b3cc657bc5e02da107b2 Mon Sep 17 00:00:00 2001 From: admin Date: Fri, 1 May 2026 12:55:04 +0300 Subject: [PATCH] fix(recognize): party blocks belong before signatures, not at top MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- apps/api/src/modules/templates/recognize.ts | 51 +++++++++++++++++---- 1 file changed, 41 insertions(+), 10 deletions(-) diff --git a/apps/api/src/modules/templates/recognize.ts b/apps/api/src/modules/templates/recognize.ts index dc29367..fdb7e63 100644 --- a/apps/api/src/modules/templates/recognize.ts +++ b/apps/api/src/modules/templates/recognize.ts @@ -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(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':