feat(bot): add DeepSeek crisis fallback engine for Jev failure and remove difflib heuristics

- Add app/fallback_matcher.py for intelligent crisis entity matching with token and latency observability
- Route Jev failure/timeouts in app/matching.py to DeepSeek fallback engine
- Handle MatchingFailureError defensively in handlers/__init__.py with honest error message
- Enforce strict 'new' entity status in handlers/quote.py and implement defensive get_or_create on commit
- Clean up app/resolve.py to eliminate difflib and character ratio heuristics
- Pass all smoke tests (59/59), fallback matching tests (15/15), and matching unit tests (13/13)
This commit is contained in:
2026-10-16 02:05:01 +03:30
parent 76b297f823
commit 9e848f535f
9 changed files with 870 additions and 124 deletions
+12 -9
View File
@@ -9,7 +9,7 @@ from app.drafts import (
KIND_MATERIAL, merge_material_patch, material_missing, new_material_payload,
)
from app.keyboards import pick_buttons, simple_review_keyboard
from app.resolve import safe_resolve
from app.nlu import normalize_text
from app.texts import ERR_GENERIC, esc, material_committed_report, material_review_text
logger = logging.getLogger("bot.material")
@@ -45,16 +45,19 @@ async def resolve_payload(ctx, payload: dict, trace_id: str | None = None,
elif hit["status"] == "ambiguity" and hit.get("candidates"):
options["unit"] = [{"id": c["id"], "name": c["name"]}
for c in hit["candidates"][:6]]
elif hit["status"] == "new":
options.pop("unit", None)
else:
res = await safe_resolve(ctx.resolver, "unit", payload["unit_name"])
await obs.log_resolution(trace_id, "unit", payload["unit_name"], res.status,
[{"id": c.id, "name": c.name} for c in (res.candidates or [])[:4]])
if res.resolved:
payload["canonical_unit_id"] = res.candidates[0].id
payload["unit_name"] = res.candidates[0].name
try:
units = await ctx.resolver.units()
except Exception:
units = []
norm_u = normalize_text(payload["unit_name"])
exact = [u for u in units if normalize_text(u.name) == norm_u or (u.extra or {}).get("code") == norm_u]
if len(exact) == 1:
payload["canonical_unit_id"] = exact[0].id
payload["unit_name"] = exact[0].name
options.pop("unit", None)
elif res.candidates:
options["unit"] = [{"id": c.id, "name": c.name} for c in res.candidates]
else:
options.pop("unit", None)