fix(ocr+safety): stop document-content hallucinations from empty OCR
Three independent bugs combined to make Shira fabricate the content of case-46-Friedman's appeal in production (CTS case rewritten as a knee injury, wrong dates, wrong court file number, wrong %-of-disability — written into save_memory/Task/Meeting on prod). * api/services/ocr.py — ocr_docx_images now reads word/document.xml first (text content), then OCRs embedded images. Previously it only OCR'd the word/media/* images, so a text-heavy DOCX with one signature image returned only "[signature]" to the LLM. Verified against the same Friedman appeal: 16633 chars / 80 paragraphs / 0 XML noise. * mcp_server/tools/document_tools.py — _ocr_fallback now refuses to wrap an empty/signature-only OCR result as success. If <80 useful chars or just "[signature]", returns an explicit failure that instructs the LLM not to describe / summarize the document. * api/services/prompt_builder.py — TOOL_RULES adds the absolute rule "DOCUMENT FAITHFULNESS": never quote a document not literally in the tool result for THIS turn; never infer content from a file name; treat dates and case numbers as especially dangerous to invent. Refs Task Master #5, #6, #7 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -97,6 +97,12 @@ def register_document_tools(
|
||||
except Exception as e:
|
||||
return fail(f"שגיאה בשמירת נתיב: {e}")
|
||||
|
||||
# Minimum length (in characters) of an extracted body that we consider
|
||||
# "real text". Below this, the OCR almost certainly returned only chrome
|
||||
# like "[signature]" or "תמונה ריקה" and the LLM has nothing to work with —
|
||||
# so we MUST report failure explicitly, never wrap it as success.
|
||||
MIN_USEFUL_TEXT_CHARS = 80
|
||||
|
||||
async def _ocr_fallback(filePath: str, reason: str) -> str:
|
||||
"""Fetch file bytes and OCR via Claude Vision."""
|
||||
from api.services.ocr import extract_text_from_bytes
|
||||
@@ -115,10 +121,28 @@ def register_document_tools(
|
||||
text = await extract_text_from_bytes(
|
||||
bytes_result["base64"], mime_type, file_name
|
||||
)
|
||||
return ok(f'=== {file_name} (OCR via Vision) ===\n\n{text}')
|
||||
except Exception as e:
|
||||
return fail(f'שגיאה ב-OCR של "{file_name}": {e}')
|
||||
|
||||
# Guard against the hallucination trap: if OCR returned only a signature
|
||||
# or a near-empty result, the LLM must NOT be allowed to invent content
|
||||
# to fill the gap. Return an explicit failure so it tells the user it
|
||||
# could not read the document.
|
||||
body = (text or "").strip()
|
||||
body_lower = body.lower()
|
||||
looks_empty = (
|
||||
len(body) < MIN_USEFUL_TEXT_CHARS
|
||||
or body_lower in {"[signature]", "signature", "חתימה", "[חתימה]"}
|
||||
or body.startswith("❌")
|
||||
)
|
||||
if looks_empty:
|
||||
return fail(
|
||||
f'לא הצלחתי לחלץ טקסט שמיש מ-"{file_name}" (התקבלו רק {len(body)} תווים, "{body[:60]}"). '
|
||||
'אל תנסי לתאר או לסכם את המסמך — הטקסט לא הגיע אלייך. '
|
||||
'הציעי למשתמש לפתוח את הקובץ ידנית או לנסות מסמך אחר.'
|
||||
)
|
||||
return ok(f'=== {file_name} (OCR via Vision) ===\n\n{body}')
|
||||
|
||||
async def read_document(filePath: str) -> str:
|
||||
try:
|
||||
result = await crm.post("SmartAssistant/action/readDocument", {"filePath": filePath})
|
||||
|
||||
Reference in New Issue
Block a user