落地报价模板 AI 解析与对象存储,并增加回复超时系统设置页。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -19,7 +19,7 @@ from agent.jobs.archive import archive_stream, process_archive_stub
|
||||
from agent.jobs.libreoffice import LibreOfficeConverter
|
||||
from agent.jobs.pdf_job import pdf_stream, process_pdf_job
|
||||
from agent.jobs.quote_fill import fill_quote_workbook
|
||||
from agent.jobs.recognition import process_recognition_stub
|
||||
from agent.jobs.recognition import process_recognition
|
||||
from agent.jobs.wake_consumer import claim_and_apply_wake, wake_stream
|
||||
from agent.llm import LlmGateway, LlmMode
|
||||
from agent.redis_coord.runtime import RedisRuntime
|
||||
@@ -211,7 +211,10 @@ class WorkerRuntime:
|
||||
return
|
||||
entry_id, data = claimed[0]
|
||||
try:
|
||||
process_recognition_stub(data)
|
||||
process_recognition(
|
||||
data,
|
||||
allow_network=bool(self.settings.llm_allow_network),
|
||||
)
|
||||
finally:
|
||||
stream.ack(entry_id)
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
识别任务壳:入队 Redis Stream,Worker HTTP 槽认领后跑占位处理。
|
||||
识别任务:入队 Redis Stream,Worker HTTP 槽认领后处理。
|
||||
|
||||
本文件职责:enqueue / process_stub;正式识别(附件/OCR/字段)后续按 payload.kind 分支。
|
||||
本文件职责:enqueue;process_recognition 按 kind 分支(含报价模板 AI 映射)。
|
||||
禁止:回调线程同步等识别完成;禁止本模块写主账六态。
|
||||
"""
|
||||
|
||||
@@ -41,7 +41,7 @@ def enqueue_recognition(
|
||||
"""
|
||||
识别任务入队(快回)。
|
||||
|
||||
kind 例:attachment_text / vision / sheet_map。
|
||||
kind 例:attachment_text / vision / quote_template_parse。
|
||||
"""
|
||||
rt = redis or get_redis_runtime()
|
||||
if idempotency_key:
|
||||
@@ -66,14 +66,109 @@ def process_recognition_stub(data: dict[str, Any]) -> dict[str, Any]:
|
||||
"""
|
||||
识别占位:回显 kind,不读附件、不调模型。
|
||||
|
||||
Worker 槽调用;正式实现替换为本函数旁的真实处理器。
|
||||
兼容旧调用名;正式路径请用 process_recognition。
|
||||
"""
|
||||
return process_recognition(data, allow_network=False)
|
||||
|
||||
|
||||
def process_recognition(data: dict[str, Any], *, allow_network: bool = True) -> dict[str, Any]:
|
||||
"""
|
||||
Worker 槽:按 kind 处理识别/映射任务。
|
||||
|
||||
quote_template_parse:调千问 sheet_map,再 PUT 主账 parseResult。
|
||||
"""
|
||||
kind = str(data.get("kind") or "unknown")
|
||||
payload = data.get("payload") or {}
|
||||
logger.info("recognition.stub kind=%s keys=%s", kind, sorted(payload.keys()) if isinstance(payload, dict) else [])
|
||||
if not isinstance(payload, dict):
|
||||
payload = {}
|
||||
if kind == "quote_template_parse":
|
||||
return _process_quote_template_parse(payload, allow_network=allow_network)
|
||||
logger.info(
|
||||
"recognition.stub kind=%s keys=%s",
|
||||
kind,
|
||||
sorted(payload.keys()),
|
||||
)
|
||||
return {
|
||||
"ok": True,
|
||||
"stub": True,
|
||||
"kind": kind,
|
||||
"note": "识别壳:未跑真实抽取",
|
||||
}
|
||||
|
||||
|
||||
def _process_quote_template_parse(payload: dict[str, Any], *, allow_network: bool) -> dict[str, Any]:
|
||||
"""跑 sheet_map 并回写主账;失败也必须回写 failed+原因。"""
|
||||
from agent.config import get_settings
|
||||
from agent.llm.mode_sheet_map import invoke_sheet_map
|
||||
|
||||
version_id = str(payload.get("versionId") or "").strip()
|
||||
settings = get_settings()
|
||||
net = bool(allow_network and settings.llm_allow_network)
|
||||
result = invoke_sheet_map(payload, allow_network=net)
|
||||
body: dict[str, Any]
|
||||
if result.get("ok") and isinstance(result.get("mapping"), dict):
|
||||
mapping = dict(result["mapping"])
|
||||
gaps = result.get("gaps_after")
|
||||
if not isinstance(gaps, list):
|
||||
gaps = mapping.get("mappingGaps") if isinstance(mapping.get("mappingGaps"), list) else []
|
||||
mapping["mappingGaps"] = [str(g) for g in gaps if g]
|
||||
# 运营可读摘要:点数之外固定带上未解析项
|
||||
n_fields = len(mapping.get("fields") or {})
|
||||
n_fee = len(mapping.get("feeRegions") or []) if isinstance(mapping.get("feeRegions"), list) else 0
|
||||
n_route = len(mapping.get("routeRegions") or []) if isinstance(mapping.get("routeRegions"), list) else 0
|
||||
points = n_fields + n_fee + n_route
|
||||
parse_message = f"已解析 {points} 个映射点"
|
||||
if mapping["mappingGaps"]:
|
||||
preview = ";".join(mapping["mappingGaps"][:8])
|
||||
more = len(mapping["mappingGaps"]) - 8
|
||||
parse_message += f";未解析到 {len(mapping['mappingGaps'])} 项:{preview}"
|
||||
if more > 0:
|
||||
parse_message += f" 等共{len(mapping['mappingGaps'])}项"
|
||||
body = {
|
||||
"versionId": version_id,
|
||||
"ok": True,
|
||||
"mappingJson": mapping,
|
||||
"parseStatus": "parsed",
|
||||
"parseMessage": parse_message,
|
||||
}
|
||||
else:
|
||||
err = str(result.get("error") or "AI 解析失败")
|
||||
body = {
|
||||
"versionId": version_id,
|
||||
"ok": False,
|
||||
"parseStatus": "failed",
|
||||
"parseMessage": err,
|
||||
}
|
||||
posted = _post_parse_result(body)
|
||||
logger.info(
|
||||
"quote_template_parse done versionId=%s ok=%s posted=%s",
|
||||
version_id,
|
||||
body.get("ok"),
|
||||
posted.get("ok"),
|
||||
)
|
||||
return {"ok": bool(body.get("ok")), "posted": posted, "kind": "quote_template_parse"}
|
||||
|
||||
|
||||
def _post_parse_result(body: dict[str, Any]) -> dict[str, Any]:
|
||||
"""PUT /inquiry/agent/quoteTemplate/parseResult。"""
|
||||
try:
|
||||
from agent.ledger import LedgerClient
|
||||
|
||||
client = LedgerClient.from_settings()
|
||||
resp = client.request_json(
|
||||
"PUT",
|
||||
"/inquiry/agent/quoteTemplate/parseResult",
|
||||
json_body=body,
|
||||
idempotency_key=f"parseResult:{body.get('versionId')}",
|
||||
)
|
||||
if not resp.ok:
|
||||
logger.warning(
|
||||
"parseResult 回写失败 status=%s err=%s",
|
||||
resp.status_code,
|
||||
resp.error,
|
||||
)
|
||||
return {"ok": False, "error": resp.error or f"http_{resp.status_code}"}
|
||||
return {"ok": True, "data": resp.data}
|
||||
except Exception as exc:
|
||||
logger.exception("parseResult 回写异常")
|
||||
return {"ok": False, "error": str(exc)}
|
||||
|
||||
Reference in New Issue
Block a user