From c2c92d0a3725a130fe1b67e67841b2b692192748 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Wed, 24 Jun 2026 09:04:31 +0400 Subject: [PATCH] v1.0.166: classify prompt v2 (LLM-suggested) + safe JSON parse with auto-repair --- deploy/db/prompts.py | 4 ++-- deploy/services/classify.py | 37 +++++++++++++++++++++++++++++-------- index.cfm | 2 +- 3 files changed, 32 insertions(+), 11 deletions(-) diff --git a/deploy/db/prompts.py b/deploy/db/prompts.py index f9fc220..5c9c77f 100644 --- a/deploy/db/prompts.py +++ b/deploy/db/prompts.py @@ -46,7 +46,7 @@ def _ensure_classify_prompt(): execute(""" INSERT INTO prompts (role, name, body, is_active, notes) VALUES ( 'classify', 'default-v1', - 'Ты — классификатор договорных документов. Ниже фрагмент текста документа.\n\nОпредели:\n1. doc_type: "contract" (договор), "supplement" (допсоглашение/допник), "specification" (спецификация/приложение), "other"\n2. own_number: номер ЭТОГО документа (например "МЭС-123-2024" или "ДС №2")\n3. parent_number: номер родительского договора (для допников/спецификаций — "к Договору №..."). Для самого договора — null.\n4. doc_date: дата документа в формате YYYY-MM-DD\n5. counterparty: название контрагента (из поля "Заказчик", "Арендатор" или аналогичного)\n6. confidence: "ok" если уверен, "low" если сомневаешься\n\nВерни СТРОГО JSON без пояснений:\n{\n "doc_type": "...",\n "own_number": "..." или null,\n "parent_number": "..." или null,\n "doc_date": "YYYY-MM-DD" или null,\n "counterparty": "..." или null,\n "confidence": "ok" или "low"\n}\n\nДОКУМЕНТ:\n---\n{header_text}\n---', - true, 'Авто-создан: classify prompt' + 'Ты — система классификации договорных документов. Проанализируй текст и верни СТРОГО ВАЛИДНЫЙ JSON ОДНОЙ СТРОКОЙ (без переносов строк, без markdown, без лишних пробелов в начале/конце).\n\nПоля:\n- doc_type: "contract" (договор) / "supplement" (допсоглашение) / "specification" (спецификация/приложение) / "other"\n- own_number: номер ЭТОГО документа, строка без лишних пробелов (или null)\n- parent_number: номер родительского договора из фраз «к Договору №...» (или null)\n- doc_date: дата в YYYY-MM-DD. «27 февраля 2026» → 2026-02-27 (или null)\n- counterparty: полное название контрагента (Заказчик/Арендатор), без сокращений (или null)\n\nПример вывода:\n{"doc_type":"supplement","own_number":"1","parent_number":"01300_2","doc_date":"2026-02-27","counterparty":"АО XXX003"}\n\nДОКУМЕНТ:\n---\n{header_text}\n---', + true, 'Авто-создан: classify prompt v2 — LLM сам предложил' ) """) diff --git a/deploy/services/classify.py b/deploy/services/classify.py index 68e41cc..531f32a 100644 --- a/deploy/services/classify.py +++ b/deploy/services/classify.py @@ -50,13 +50,7 @@ def _call_llm_classify(header_text): data = resp.json() raw = data.get("choices", [{}])[0].get("message", {}).get("content", "") - # LLM может обернуть JSON в markdown-блок ```json ... ``` - json_text = raw - if "```json" in json_text: - json_text = json_text.split("```json")[1].split("```")[0] - elif "```" in json_text: - json_text = json_text.split("```")[1].split("```")[0] - return json.loads(json_text.strip()) + return _safe_json_parse(raw) def classify_batch(batch_id): @@ -103,7 +97,34 @@ def classify_batch(batch_id): return {"ok": True, "total": total, "done": done, "failed": failed} -def _smart_extract(elements_json): +def _safe_json_parse(raw): + """Parse LLM response, fixing common JSON errors (multiline, markdown, unescaped chars).""" + if not raw: + raise ValueError("empty LLM response") + + # Strip markdown wrappers + text = raw.strip() + if "```json" in text: + text = text.split("```json")[1].split("```")[0].strip() + elif "```" in text: + text = text.split("```")[1].split("```")[0].strip() + + # Try strict parse first + try: + return json.loads(text) + except json.JSONDecodeError: + pass + + # Fix common issues: unescaped newlines inside strings, unterminated strings + # Replace literal newlines inside JSON values + import re as _re + # Collapse multiline to single line + text = _re.sub(r'\n\s*', ' ', text) + # Remove trailing commas before closing braces + text = _re.sub(r',\s*}', '}', text) + text = _re.sub(r',\s*]', ']', text) + + return json.loads(text) """ Умная выжимка текста для классификации (решение Q3 от Opus). diff --git a/index.cfm b/index.cfm index 55da409..bb7e629 100644 --- a/index.cfm +++ b/index.cfm @@ -75,7 +75,7 @@
Nubes - Сверка договоров — LLM AI-driven Event Sourcing v1.0.165 — Lucee + Сверка договоров — LLM AI-driven Event Sourcing v1.0.166 — Lucee