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
+1 -9
View File
@@ -3,7 +3,6 @@
The ERP search endpoints are ilike-based; we fetch candidates with `q=` and
rank them with difflib so "میلگرد A3 14" still matches "میلگرد A3 سایز ۱۴".
"""
import difflib
import time
from dataclasses import dataclass, field
@@ -44,7 +43,7 @@ def _rank(query: str, items: list[Candidate], limit: int = 6) -> list[Candidate]
elif name in q and len(name) >= 3:
ratio = 0.85 + 0.15 * (len(name) / len(q))
else:
ratio = difflib.SequenceMatcher(None, q, name).ratio()
ratio = 0.0
if ratio >= 0.72:
scored.append((ratio, item))
scored.sort(key=lambda pair: (-pair[0], pair[1].name))
@@ -365,13 +364,6 @@ class Resolver:
exact = [c for c in candidates if normalize_text(c.name) == norm_name]
if len(exact) == 1:
return Resolution("one", exact)
top = candidates[0]
top_norm = normalize_text(top.name)
ratio = difflib.SequenceMatcher(None, norm_name, top_norm).ratio()
if (norm_name in top_norm or top_norm in norm_name) and len(norm_name) >= 3:
ratio = max(ratio, 0.90)
if ratio >= 0.88 and (len(candidates) == 1 or ratio > 0.95):
return Resolution("one", [top])
return Resolution("many", candidates)
async def unit_for_material(self, unit_name: str | None, material: dict | None) -> tuple[str | None, bool]: