feat(cra): Datenblatt-Extraktion auf lokales 35B + llm_status-Fix
llm_cascade additiv modell-faehig (optionaler model-Param, Cache-Key kennt model_hint → keine Kollision; Default unveraendert für alle anderen Nutzer). Datenblatt-Extraktor nutzt jetzt qwen3.5:35b-a3b (CRA_DATASHEET_MODEL, gleiches Modell wie der Compliance Advisor) für bessere semantische Zuordnung. Plus llm_status (ok|empty|unavailable) + Logging statt stillem except; Frontend zeigt bei 'unavailable' einen Hinweis statt leerer Felder (wichtig auf prod ohne lokales Ollama → Cascade-Fallback bzw. Hinweis). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -10,8 +10,15 @@ Pure + testable: detect_signals / parse_grenzen_json / compute_followups. The
|
||||
async extract_grenzen() wraps the LLM call (llm_cascade, same as vendor extractor).
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from typing import Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Datasheet extraction uses the local 35B (same model as the Compliance Advisor) —
|
||||
# higher-quality semantic mapping than the default cascade model. Env-overridable.
|
||||
_DATASHEET_MODEL = os.getenv("CRA_DATASHEET_MODEL", "qwen3.5:35b-a3b")
|
||||
|
||||
# IACE Grenzen field keys (must match admin LimitsFormData). label + whether it
|
||||
# is essential for a usable risk assessment (=> asked as follow-up if empty).
|
||||
@@ -150,6 +157,7 @@ async def extract_grenzen(text: str, max_chars: int = 20000) -> dict:
|
||||
signals = detect_signals(text or "")
|
||||
limits: dict = {}
|
||||
provenance: dict = {}
|
||||
llm_status = "skipped" # skipped | ok | empty | unavailable
|
||||
excerpt = (text or "")[:max_chars]
|
||||
if len(excerpt) >= 200:
|
||||
try:
|
||||
@@ -157,20 +165,25 @@ async def extract_grenzen(text: str, max_chars: int = 20000) -> dict:
|
||||
res = await call_with_cascade(
|
||||
system=_system_prompt(),
|
||||
user=f"Datenblatt-Text:\n\n{excerpt}",
|
||||
min_confidence=0.5, max_tokens=4000,
|
||||
min_confidence=0.5, max_tokens=4000, model=_DATASHEET_MODEL,
|
||||
)
|
||||
parsed = parse_grenzen_json(res.get("text", "") if isinstance(res, dict) else "")
|
||||
for key, entry in parsed.items():
|
||||
limits[key] = entry["value"]
|
||||
provenance[key] = entry.get("source", "")
|
||||
except Exception:
|
||||
pass # extraction is best-effort; fall back to detector + follow-ups
|
||||
llm_status = "ok" if parsed else "empty"
|
||||
except Exception as e:
|
||||
# best-effort: keep the deterministic facts, but surface the failure so
|
||||
# a cold-start/timeout doesn't masquerade as "nothing on the datasheet".
|
||||
logger.warning("datasheet LLM extraction failed: %s (%s)", e, type(e).__name__)
|
||||
llm_status = "unavailable"
|
||||
|
||||
_merge_detected(limits, provenance, signals)
|
||||
return {
|
||||
"limits": limits,
|
||||
"provenance": provenance,
|
||||
"detected": signals,
|
||||
"llm_status": llm_status,
|
||||
"filled": sorted(limits.keys()),
|
||||
"missing": [k for k in _FIELD_KEYS if not (limits.get(k) or "").strip()],
|
||||
"followup": compute_followups(limits),
|
||||
|
||||
Reference in New Issue
Block a user