Files
inquiry_robot/inquiry-agent/agent/quote_templates/__init__.py
T

196 lines
5.8 KiB
Python

"""
报价模板访问。
真相在主账:后台上传/启用/停用 → 对象存储/本地上传路径 → 主账元数据 + mappingJson。
智能体生成前经 ledger 拉取已启用模板,再解析本地/下载字节后交 quote_fill。
禁止:把仓库 resources 当生产模板真相;禁止在本包发明价格或改 quoteVersion。
"""
from __future__ import annotations
import logging
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Optional
from agent.ledger import LedgerClient, LedgerResponse
from agent.ledger.api_shell import LedgerApiShell
logger = logging.getLogger(__name__)
@dataclass(frozen=True)
class TemplateEntry:
"""一条模板元数据(来自主账或联调缓存)。"""
code: str
filename: str = ""
object_key: str = ""
enabled: bool = False
source: str = "ledger"
template_id: str = ""
version_no: int = 0
version_id: str = ""
mapping_json: str = ""
biz_type: str = ""
keywords: tuple[str, ...] = ()
is_default: bool = False
def local_cache_dir() -> Path:
"""
可选本机缓存目录(非生产真相)。
正式模板由后台上传;此目录仅开发落盘缓存,可空。
"""
here = Path(__file__).resolve()
return here.parents[2] / "resources" / "quote_templates"
def list_templates_from_ledger(
client: Optional[LedgerClient] = None,
*,
enabled_only: bool = True,
) -> LedgerResponse:
"""
向主账查询已启用报价模板列表。
正式路径:GET /inquiry/quoteTemplate/agent/listEnabled
"""
shell = LedgerApiShell(client=client)
return shell.list_quote_templates(enabled_only=enabled_only)
def entries_from_ledger_response(resp: LedgerResponse) -> list[TemplateEntry]:
"""把主账 listEnabled 结果转成 TemplateEntry。"""
if not resp.ok or resp.data is None:
return []
raw = resp.data
# Jeecg Result 可能包在 result / data
if isinstance(raw, dict):
items = raw.get("result") or raw.get("data") or raw.get("records") or []
if isinstance(raw.get("result"), list):
items = raw["result"]
elif isinstance(raw, list):
items = raw
else:
items = []
out: list[TemplateEntry] = []
for it in items:
if not isinstance(it, dict):
continue
tid = str(it.get("templateId") or it.get("id") or "")
out.append(
TemplateEntry(
code=tid or str(it.get("name") or ""),
filename=str(it.get("fileName") or ""),
object_key=str(it.get("objectKey") or ""),
enabled=True,
source="ledger",
template_id=tid,
version_no=int(it.get("versionNo") or 0),
version_id=str(it.get("versionId") or ""),
mapping_json=str(it.get("mappingJson") or ""),
biz_type=str(it.get("bizType") or ""),
keywords=tuple(str(x) for x in (it.get("keywords") or []) if str(x).strip()),
is_default=bool(it.get("isDefault")),
)
)
return out
def list_local_cache_files() -> list[TemplateEntry]:
"""
列出本机缓存目录中的 xlsx(仅联调;可空)。
不等于后台已启用模板。
"""
root = local_cache_dir()
if not root.is_dir():
return []
out: list[TemplateEntry] = []
for path in sorted(root.iterdir()):
if not path.is_file():
continue
if path.suffix.lower() not in {".xlsx", ".xlsm", ".xltx"}:
continue
if path.name.startswith("."):
continue
out.append(
TemplateEntry(
code=path.stem,
filename=path.name,
object_key="",
enabled=False,
source="local_cache",
)
)
logger.info("quote_templates.local_cache count=%s", len(out))
return out
def list_template_registry() -> list[TemplateEntry]:
"""
优先主账;失败则回落本地缓存(可空)。
"""
resp = list_templates_from_ledger(enabled_only=True)
entries = entries_from_ledger_response(resp)
if entries:
return entries
return list_local_cache_files()
def registry_as_dicts() -> list[dict[str, Any]]:
"""便于 JSON/自检。"""
return [
{
"code": e.code,
"filename": e.filename,
"object_key": e.object_key,
"enabled": e.enabled,
"source": e.source,
"template_id": e.template_id,
"version_no": e.version_no,
"biz_type": e.biz_type,
}
for e in list_template_registry()
]
def pick_template_for_biz(
*,
biz_type: str,
facts: Optional[dict[str, Any]] = None,
quote: Optional[dict[str, Any]] = None,
keywords: Optional[list[str]] = None,
entries: Optional[list[TemplateEntry]] = None,
) -> Optional[TemplateEntry]:
"""
按询价需求匹配模板关键词;都未命中则同业务默认模板。
keywords 入参已废弃(兼容旧调用,忽略)。生产出单以主账 render 为准。
"""
_ = keywords
from agent.quote_templates.match import pick_quote_template
items = entries if entries is not None else list_template_registry()
rows = [
{
"templateId": e.template_id or e.code,
"name": e.filename or e.code,
"bizType": e.biz_type,
"keywords": list(e.keywords),
"isDefault": e.is_default,
"_entry": e,
}
for e in items
]
hit = pick_quote_template(
templates=rows, business_line=biz_type, facts=facts, quote=quote
)
if hit is None:
return None
entry = hit.raw.get("_entry")
return entry if isinstance(entry, TemplateEntry) else None