v1.0.166: classify prompt v2 (LLM-suggested) + safe JSON parse with auto-repair
This commit is contained in:
@@ -46,7 +46,7 @@ def _ensure_classify_prompt():
|
|||||||
execute("""
|
execute("""
|
||||||
INSERT INTO prompts (role, name, body, is_active, notes) VALUES (
|
INSERT INTO prompts (role, name, body, is_active, notes) VALUES (
|
||||||
'classify', 'default-v1',
|
'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---',
|
'Ты — система классификации договорных документов. Проанализируй текст и верни СТРОГО ВАЛИДНЫЙ 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'
|
true, 'Авто-создан: classify prompt v2 — LLM сам предложил'
|
||||||
)
|
)
|
||||||
""")
|
""")
|
||||||
|
|||||||
@@ -50,13 +50,7 @@ def _call_llm_classify(header_text):
|
|||||||
data = resp.json()
|
data = resp.json()
|
||||||
|
|
||||||
raw = data.get("choices", [{}])[0].get("message", {}).get("content", "")
|
raw = data.get("choices", [{}])[0].get("message", {}).get("content", "")
|
||||||
# LLM может обернуть JSON в markdown-блок ```json ... ```
|
return _safe_json_parse(raw)
|
||||||
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())
|
|
||||||
|
|
||||||
|
|
||||||
def classify_batch(batch_id):
|
def classify_batch(batch_id):
|
||||||
@@ -103,7 +97,34 @@ def classify_batch(batch_id):
|
|||||||
return {"ok": True, "total": total, "done": done, "failed": failed}
|
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).
|
Умная выжимка текста для классификации (решение Q3 от Opus).
|
||||||
|
|
||||||
|
|||||||
@@ -75,7 +75,7 @@
|
|||||||
<body>
|
<body>
|
||||||
<div class="topbar">
|
<div class="topbar">
|
||||||
<img src="/nubes-logo.svg" alt="Nubes">
|
<img src="/nubes-logo.svg" alt="Nubes">
|
||||||
<span class="title">Сверка договоров — LLM AI-driven Event Sourcing <span style="font-weight:400;color:var(--muted);font-size:12px;">v1.0.165 — Lucee</span></span>
|
<span class="title">Сверка договоров — LLM AI-driven Event Sourcing <span style="font-weight:400;color:var(--muted);font-size:12px;">v1.0.166 — Lucee</span></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="content">
|
<div class="content">
|
||||||
|
|||||||
Reference in New Issue
Block a user