fix(bot): strip html tags for Bale, fallback to direct DB catalogs, align validation strictly with DB schema
This commit is contained in:
+145
-26
@@ -7,7 +7,9 @@ import difflib
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from sqlalchemy import text
|
||||
from app.api.client import ApiError, ErpClient
|
||||
from app.db import SessionLocal
|
||||
from app.nlu import normalize_text
|
||||
|
||||
|
||||
@@ -45,6 +47,108 @@ def _rank(query: str, items: list[Candidate], limit: int = 6) -> list[Candidate]
|
||||
return [item for ratio, item in scored[:limit] if ratio >= 0.40]
|
||||
|
||||
|
||||
async def _db_units() -> list[Candidate]:
|
||||
try:
|
||||
async with SessionLocal() as s:
|
||||
rows = (await s.execute(text("SELECT id, code, name_fa FROM units"))).mappings().all()
|
||||
return [
|
||||
Candidate(
|
||||
id=str(u["id"]),
|
||||
name=u.get("name_fa") or u.get("code") or str(u["id"]),
|
||||
extra={"code": u.get("code")},
|
||||
)
|
||||
for u in rows
|
||||
]
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
async def _db_categories() -> list[Candidate]:
|
||||
try:
|
||||
async with SessionLocal() as s:
|
||||
rows = (await s.execute(text("SELECT id, name, slug FROM material_categories"))).mappings().all()
|
||||
return [
|
||||
Candidate(
|
||||
id=str(c["id"]),
|
||||
name=c.get("name") or c.get("slug") or str(c["id"]),
|
||||
)
|
||||
for c in rows
|
||||
]
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
async def _db_projects() -> list[Candidate]:
|
||||
try:
|
||||
async with SessionLocal() as s:
|
||||
rows = (await s.execute(text("SELECT id, name FROM projects"))).mappings().all()
|
||||
return [
|
||||
Candidate(
|
||||
id=str(p["id"]),
|
||||
name=p.get("name") or str(p["id"]),
|
||||
)
|
||||
for p in rows
|
||||
]
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
async def _db_suppliers(q: str | None = None) -> list[Candidate]:
|
||||
try:
|
||||
async with SessionLocal() as s:
|
||||
if q:
|
||||
rows = (await s.execute(
|
||||
text("SELECT s.id, s.name, s.location, s.phone, s.contact_person, c.name as category_name "
|
||||
"FROM suppliers s LEFT JOIN material_categories c ON s.category_id = c.id "
|
||||
"WHERE s.name ILIKE :q LIMIT 20"),
|
||||
{"q": f"%{q}%"}
|
||||
)).mappings().all()
|
||||
else:
|
||||
rows = (await s.execute(
|
||||
text("SELECT s.id, s.name, s.location, s.phone, s.contact_person, c.name as category_name "
|
||||
"FROM suppliers s LEFT JOIN material_categories c ON s.category_id = c.id LIMIT 20")
|
||||
)).mappings().all()
|
||||
return [
|
||||
Candidate(
|
||||
id=str(r["id"]),
|
||||
name=r.get("name") or str(r["id"]),
|
||||
extra={
|
||||
"location": r.get("location"),
|
||||
"phone": r.get("phone"),
|
||||
"contact_person": r.get("contact_person"),
|
||||
"category_name": r.get("category_name"),
|
||||
},
|
||||
)
|
||||
for r in rows
|
||||
]
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
async def _db_materials(q: str | None = None) -> list[Candidate]:
|
||||
try:
|
||||
async with SessionLocal() as s:
|
||||
if q:
|
||||
rows = (await s.execute(
|
||||
text("SELECT id, name, topic, canonical_unit_id, status FROM materials WHERE name ILIKE :q LIMIT 20"),
|
||||
{"q": f"%{q}%"}
|
||||
)).mappings().all()
|
||||
else:
|
||||
rows = (await s.execute(
|
||||
text("SELECT id, name, topic, canonical_unit_id, status FROM materials LIMIT 20")
|
||||
)).mappings().all()
|
||||
return [
|
||||
Candidate(
|
||||
id=str(r["id"]),
|
||||
name=r.get("name") or str(r["id"]),
|
||||
extra=dict(r),
|
||||
)
|
||||
for r in rows
|
||||
]
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
class Resolver:
|
||||
"""Caches catalog lists (units/categories/projects are stable; suppliers/materials queried on demand)."""
|
||||
|
||||
@@ -72,45 +176,60 @@ class Resolver:
|
||||
|
||||
async def units(self) -> list[Candidate]:
|
||||
async def fetch():
|
||||
rows = await self._api.list_units()
|
||||
return [Candidate(id=str(u["id"]), name=u.get("name_fa") or u.get("code") or str(u["id"]),
|
||||
extra={"code": u.get("code")}) for u in rows]
|
||||
try:
|
||||
rows = await self._api.list_units()
|
||||
return [Candidate(id=str(u["id"]), name=u.get("name_fa") or u.get("code") or str(u["id"]),
|
||||
extra={"code": u.get("code")}) for u in rows]
|
||||
except Exception:
|
||||
return await _db_units()
|
||||
return await self._cached("units", fetch)
|
||||
|
||||
async def categories(self) -> list[Candidate]:
|
||||
async def fetch():
|
||||
rows = await self._api.list_categories()
|
||||
return [Candidate(id=str(c["id"]), name=c.get("name") or c.get("slug") or str(c["id"])) for c in rows]
|
||||
try:
|
||||
rows = await self._api.list_categories()
|
||||
return [Candidate(id=str(c["id"]), name=c.get("name") or c.get("slug") or str(c["id"])) for c in rows]
|
||||
except Exception:
|
||||
return await _db_categories()
|
||||
return await self._cached("categories", fetch)
|
||||
|
||||
async def projects(self) -> list[Candidate]:
|
||||
async def fetch():
|
||||
rows = await self._api.list_projects()
|
||||
return [Candidate(id=str(p["id"]), name=p.get("name") or str(p["id"])) for p in rows]
|
||||
try:
|
||||
rows = await self._api.list_projects()
|
||||
return [Candidate(id=str(p["id"]), name=p.get("name") or str(p["id"])) for p in rows]
|
||||
except Exception:
|
||||
return await _db_projects()
|
||||
return await self._cached("projects", fetch)
|
||||
|
||||
async def suppliers(self, q: str | None = None) -> list[Candidate]:
|
||||
rows = await self._api.list_suppliers(q=q, page_size=20)
|
||||
items = [
|
||||
Candidate(
|
||||
id=str(s["id"]),
|
||||
name=s.get("name") or str(s["id"]),
|
||||
extra={
|
||||
"location": s.get("location"),
|
||||
"phone": s.get("phone"),
|
||||
"contact_person": s.get("contact_person"),
|
||||
"category_name": (s.get("category") or {}).get("name") if isinstance(s.get("category"), dict) else None,
|
||||
},
|
||||
)
|
||||
for s in rows
|
||||
]
|
||||
try:
|
||||
rows = await self._api.list_suppliers(q=q, page_size=20)
|
||||
items = [
|
||||
Candidate(
|
||||
id=str(s["id"]),
|
||||
name=s.get("name") or str(s["id"]),
|
||||
extra={
|
||||
"location": s.get("location"),
|
||||
"phone": s.get("phone"),
|
||||
"contact_person": s.get("contact_person"),
|
||||
"category_name": (s.get("category") or {}).get("name") if isinstance(s.get("category"), dict) else None,
|
||||
},
|
||||
)
|
||||
for s in rows
|
||||
]
|
||||
except Exception:
|
||||
items = await _db_suppliers(q=q)
|
||||
return _rank(q, items) if q else items
|
||||
|
||||
async def materials(self, q: str | None = None) -> list[Candidate]:
|
||||
rows = await self._api.list_materials(q=q, page_size=20)
|
||||
extra = {str(m["id"]): m for m in rows}
|
||||
items = [Candidate(id=str(m["id"]), name=m.get("name") or str(m["id"]), extra=extra.get(str(m["id"]), {}))
|
||||
for m in rows]
|
||||
try:
|
||||
rows = await self._api.list_materials(q=q, page_size=20)
|
||||
extra = {str(m["id"]): m for m in rows}
|
||||
items = [Candidate(id=str(m["id"]), name=m.get("name") or str(m["id"]), extra=extra.get(str(m["id"]), {}))
|
||||
for m in rows]
|
||||
except Exception:
|
||||
items = await _db_materials(q=q)
|
||||
return _rank(q, items) if q else items
|
||||
|
||||
# ------------------------------------------------------------ resolutions
|
||||
@@ -159,5 +278,5 @@ class Resolver:
|
||||
async def safe_resolve(resolver: Resolver, kind: str, name: str | None) -> Resolution:
|
||||
try:
|
||||
return await resolver.resolve(kind, name)
|
||||
except ApiError:
|
||||
except Exception:
|
||||
return Resolution("none", [])
|
||||
|
||||
Reference in New Issue
Block a user