refactor(backend): update service layers
- services/ontology.py: update ontology service logic - services/orchestrator.py: update orchestrator service logic - services/user_agent.py: update user agent service logic
This commit is contained in:
@@ -155,13 +155,20 @@ OPERATE_KEYWORDS = (
|
|||||||
|
|
||||||
EXPENSE_TYPE_KEYWORDS = {
|
EXPENSE_TYPE_KEYWORDS = {
|
||||||
"差旅": "travel",
|
"差旅": "travel",
|
||||||
|
"出差": "travel",
|
||||||
"住宿": "hotel",
|
"住宿": "hotel",
|
||||||
"酒店": "hotel",
|
"酒店": "hotel",
|
||||||
"交通": "transport",
|
"交通": "transport",
|
||||||
|
"打车": "transport",
|
||||||
|
"网约车": "transport",
|
||||||
|
"出租车": "transport",
|
||||||
|
"停车费": "transport",
|
||||||
"餐费": "meal",
|
"餐费": "meal",
|
||||||
|
"用餐": "meal",
|
||||||
"会务": "meeting",
|
"会务": "meeting",
|
||||||
"招待费": "entertainment",
|
"招待费": "entertainment",
|
||||||
"招待": "entertainment",
|
"招待": "entertainment",
|
||||||
|
"宴请": "entertainment",
|
||||||
}
|
}
|
||||||
|
|
||||||
EXPENSE_NARRATIVE_KEYWORDS = (
|
EXPENSE_NARRATIVE_KEYWORDS = (
|
||||||
@@ -176,6 +183,10 @@ EXPENSE_NARRATIVE_KEYWORDS = (
|
|||||||
"打车",
|
"打车",
|
||||||
"车费",
|
"车费",
|
||||||
"餐费",
|
"餐费",
|
||||||
|
"吃饭",
|
||||||
|
"用餐",
|
||||||
|
"宴请",
|
||||||
|
"请客",
|
||||||
"住宿",
|
"住宿",
|
||||||
"发票",
|
"发票",
|
||||||
"票据",
|
"票据",
|
||||||
@@ -416,6 +427,11 @@ class SemanticOntologyService:
|
|||||||
permission=permission,
|
permission=permission,
|
||||||
missing_slots=missing_slots,
|
missing_slots=missing_slots,
|
||||||
ambiguity=ambiguity,
|
ambiguity=ambiguity,
|
||||||
|
allow_incomplete_draft=self._allow_incomplete_draft(
|
||||||
|
context_json,
|
||||||
|
scenario=scenario,
|
||||||
|
intent=intent,
|
||||||
|
),
|
||||||
model_clarification_required=bool(
|
model_clarification_required=bool(
|
||||||
model_parse is not None and model_parse.clarification_required
|
model_parse is not None and model_parse.clarification_required
|
||||||
),
|
),
|
||||||
@@ -778,6 +794,8 @@ class SemanticOntologyService:
|
|||||||
"conversation_scenario": payload.context_json.get("conversation_scenario"),
|
"conversation_scenario": payload.context_json.get("conversation_scenario"),
|
||||||
"conversation_intent": payload.context_json.get("conversation_intent"),
|
"conversation_intent": payload.context_json.get("conversation_intent"),
|
||||||
"draft_claim_id": payload.context_json.get("draft_claim_id"),
|
"draft_claim_id": payload.context_json.get("draft_claim_id"),
|
||||||
|
"review_action": payload.context_json.get("review_action"),
|
||||||
|
"review_form_values": payload.context_json.get("review_form_values"),
|
||||||
"conversation_history": payload.context_json.get("conversation_history", []),
|
"conversation_history": payload.context_json.get("conversation_history", []),
|
||||||
},
|
},
|
||||||
"rule_candidates": {
|
"rule_candidates": {
|
||||||
@@ -1004,6 +1022,10 @@ class SemanticOntologyService:
|
|||||||
suffix = match.group(1).strip()
|
suffix = match.group(1).strip()
|
||||||
normalized = f"客户{suffix}".replace(" ", "")
|
normalized = f"客户{suffix}".replace(" ", "")
|
||||||
upsert(self._make_entity("customer", match.group(0).strip(), normalized, role="filter"))
|
upsert(self._make_entity("customer", match.group(0).strip(), normalized, role="filter"))
|
||||||
|
labeled_customer_match = re.search(r"客户名称[::]\s*(?P<name>[^\n,。;]+)", query)
|
||||||
|
if labeled_customer_match:
|
||||||
|
customer_name = labeled_customer_match.group("name").strip()
|
||||||
|
upsert(self._make_entity("customer", customer_name, customer_name, role="filter"))
|
||||||
|
|
||||||
for match in re.finditer(r"供应商\s*([A-Za-z0-9一二三四五六七八九十]+)", query):
|
for match in re.finditer(r"供应商\s*([A-Za-z0-9一二三四五六七八九十]+)", query):
|
||||||
suffix = match.group(1).strip()
|
suffix = match.group(1).strip()
|
||||||
@@ -1062,6 +1084,35 @@ class SemanticOntologyService:
|
|||||||
if label in query:
|
if label in query:
|
||||||
upsert(self._make_entity("expense_type", label, normalized, role="filter"))
|
upsert(self._make_entity("expense_type", label, normalized, role="filter"))
|
||||||
|
|
||||||
|
has_customer_entertainment_signal = "客户" in query and any(
|
||||||
|
keyword in query for keyword in ("吃饭", "用餐", "餐饮", "宴请", "请客", "招待")
|
||||||
|
)
|
||||||
|
if has_customer_entertainment_signal:
|
||||||
|
upsert(
|
||||||
|
self._make_entity(
|
||||||
|
"expense_type",
|
||||||
|
"客户招待",
|
||||||
|
"entertainment",
|
||||||
|
role="filter",
|
||||||
|
confidence=0.96,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if any(keyword in query for keyword in ("打车", "网约车", "出租车", "车费", "停车费", "过路费")):
|
||||||
|
upsert(self._make_entity("expense_type", "交通", "transport", role="filter", confidence=0.9))
|
||||||
|
|
||||||
|
if any(keyword in query for keyword in ("出差", "机票", "火车", "高铁", "行程单")):
|
||||||
|
upsert(self._make_entity("expense_type", "差旅", "travel", role="filter", confidence=0.88))
|
||||||
|
|
||||||
|
if any(keyword in query for keyword in ("酒店", "住宿", "宾馆")):
|
||||||
|
upsert(self._make_entity("expense_type", "住宿", "hotel", role="filter", confidence=0.86))
|
||||||
|
|
||||||
|
if (
|
||||||
|
not has_customer_entertainment_signal
|
||||||
|
and any(keyword in query for keyword in ("餐费", "用餐", "午餐", "晚餐", "早餐", "餐饮"))
|
||||||
|
):
|
||||||
|
upsert(self._make_entity("expense_type", "餐费", "meal", role="filter", confidence=0.84))
|
||||||
|
|
||||||
for amount in self._extract_amount_entities(query):
|
for amount in self._extract_amount_entities(query):
|
||||||
upsert(amount)
|
upsert(amount)
|
||||||
|
|
||||||
@@ -1475,6 +1526,7 @@ class SemanticOntologyService:
|
|||||||
permission: OntologyPermission,
|
permission: OntologyPermission,
|
||||||
missing_slots: list[str],
|
missing_slots: list[str],
|
||||||
ambiguity: list[str],
|
ambiguity: list[str],
|
||||||
|
allow_incomplete_draft: bool,
|
||||||
model_clarification_required: bool,
|
model_clarification_required: bool,
|
||||||
model_clarification_question: str | None,
|
model_clarification_question: str | None,
|
||||||
) -> tuple[bool, str | None]:
|
) -> tuple[bool, str | None]:
|
||||||
@@ -1492,12 +1544,25 @@ class SemanticOntologyService:
|
|||||||
return True, "请说明这是报销、应收、应付,还是制度知识问题?"
|
return True, "请说明这是报销、应收、应付,还是制度知识问题?"
|
||||||
if intent == "compare" and len([item for item in entities if item.type != "amount"]) < 2:
|
if intent == "compare" and len([item for item in entities if item.type != "amount"]) < 2:
|
||||||
return True, "请补充需要对比的两个对象,例如两个客户、两个供应商或两个员工。"
|
return True, "请补充需要对比的两个对象,例如两个客户、两个供应商或两个员工。"
|
||||||
|
if allow_incomplete_draft and scenario == "expense" and intent == "draft":
|
||||||
|
return False, None
|
||||||
if missing_slots:
|
if missing_slots:
|
||||||
return True, self._build_missing_slot_question(missing_slots)
|
return True, self._build_missing_slot_question(missing_slots)
|
||||||
if ambiguity:
|
if ambiguity:
|
||||||
return True, f"当前问题存在歧义,请进一步说明:{';'.join(ambiguity)}。"
|
return True, f"当前问题存在歧义,请进一步说明:{';'.join(ambiguity)}。"
|
||||||
return False, None
|
return False, None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _allow_incomplete_draft(
|
||||||
|
context_json: dict[str, Any],
|
||||||
|
*,
|
||||||
|
scenario: str,
|
||||||
|
intent: str,
|
||||||
|
) -> bool:
|
||||||
|
if scenario != "expense" or intent != "draft":
|
||||||
|
return False
|
||||||
|
return str(context_json.get("review_action") or "").strip() == "save_draft"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _display_slot_label(slot: str) -> str:
|
def _display_slot_label(slot: str) -> str:
|
||||||
return MISSING_SLOT_LABELS.get(slot, slot)
|
return MISSING_SLOT_LABELS.get(slot, slot)
|
||||||
|
|||||||
@@ -166,6 +166,40 @@ class OrchestratorService:
|
|||||||
route_json["stage"] = "blocked"
|
route_json["stage"] = "blocked"
|
||||||
route_json["route_reason"] = route_reason
|
route_json["route_reason"] = route_reason
|
||||||
elif ontology.clarification_required:
|
elif ontology.clarification_required:
|
||||||
|
if selected_agent == AgentName.USER_AGENT.value and ontology.scenario == "expense":
|
||||||
|
clarification_response = self.user_agent_service.respond(
|
||||||
|
UserAgentRequest(
|
||||||
|
run_id=run.run_id,
|
||||||
|
user_id=payload.user_id,
|
||||||
|
message=payload.message or "",
|
||||||
|
ontology=ontology,
|
||||||
|
context_json=context_json,
|
||||||
|
tool_payload={"clarification_required": True},
|
||||||
|
selected_capability_codes=selected_capability_codes,
|
||||||
|
degraded=False,
|
||||||
|
requires_confirmation=requires_confirmation,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
clarification_result = self._build_user_agent_result(
|
||||||
|
clarification_response,
|
||||||
|
degraded=False,
|
||||||
|
)
|
||||||
|
clarification_result.update(
|
||||||
|
{
|
||||||
|
"clarification_required": True,
|
||||||
|
"missing_slots": ontology.missing_slots,
|
||||||
|
"ambiguity": ontology.ambiguity,
|
||||||
|
"parse_strategy": ontology.parse_strategy,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
outcome = ExecutionOutcome(
|
||||||
|
status=AgentRunStatus.BLOCKED.value,
|
||||||
|
result=clarification_result,
|
||||||
|
degraded=False,
|
||||||
|
tool_count=0,
|
||||||
|
failed_tool_count=0,
|
||||||
|
)
|
||||||
|
else:
|
||||||
outcome = ExecutionOutcome(
|
outcome = ExecutionOutcome(
|
||||||
status=AgentRunStatus.BLOCKED.value,
|
status=AgentRunStatus.BLOCKED.value,
|
||||||
result={
|
result={
|
||||||
|
|||||||
@@ -59,13 +59,13 @@ GENERIC_EXPENSE_PROMPTS = {
|
|||||||
EXPLICIT_DRAFT_KEYWORDS = ("生成", "草稿", "起草", "创建", "发起", "准备")
|
EXPLICIT_DRAFT_KEYWORDS = ("生成", "草稿", "起草", "创建", "发起", "准备")
|
||||||
|
|
||||||
EXPENSE_TYPE_LABELS = {
|
EXPENSE_TYPE_LABELS = {
|
||||||
"travel": "差旅",
|
"travel": "差旅费",
|
||||||
"hotel": "住宿",
|
"hotel": "住宿费",
|
||||||
"transport": "交通",
|
"transport": "交通费",
|
||||||
"meal": "餐费",
|
"meal": "餐费",
|
||||||
"meeting": "会务",
|
"meeting": "会务费",
|
||||||
"entertainment": "招待",
|
"entertainment": "业务招待费",
|
||||||
"other": "其他",
|
"other": "其他费用",
|
||||||
}
|
}
|
||||||
|
|
||||||
GROUP_SCENE_LABELS = {
|
GROUP_SCENE_LABELS = {
|
||||||
@@ -84,6 +84,7 @@ SLOT_LABELS = {
|
|||||||
"location": "地点",
|
"location": "地点",
|
||||||
"merchant_name": "酒店/商户",
|
"merchant_name": "酒店/商户",
|
||||||
"amount": "金额",
|
"amount": "金额",
|
||||||
|
"reason": "事由说明",
|
||||||
"participants": "参与人员",
|
"participants": "参与人员",
|
||||||
"attachments": "票据附件",
|
"attachments": "票据附件",
|
||||||
}
|
}
|
||||||
@@ -549,12 +550,20 @@ class UserAgentService:
|
|||||||
if payload.ontology.intent not in {"draft", "operate"} and attachment_count <= 0 and not ocr_documents:
|
if payload.ontology.intent not in {"draft", "operate"} and attachment_count <= 0 and not ocr_documents:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
slot_cards = self._build_review_slot_cards(payload, ocr_documents=ocr_documents)
|
|
||||||
document_cards = self._build_review_document_cards(payload, ocr_documents=ocr_documents)
|
document_cards = self._build_review_document_cards(payload, ocr_documents=ocr_documents)
|
||||||
claim_groups = self._build_review_claim_groups(
|
claim_groups = self._build_review_claim_groups(
|
||||||
payload,
|
payload,
|
||||||
document_cards=document_cards,
|
document_cards=document_cards,
|
||||||
)
|
)
|
||||||
|
slot_cards = self._build_review_slot_cards(
|
||||||
|
payload,
|
||||||
|
ocr_documents=ocr_documents,
|
||||||
|
claim_groups=claim_groups,
|
||||||
|
)
|
||||||
|
missing_slot_keys = self._resolve_review_missing_slot_keys(
|
||||||
|
payload,
|
||||||
|
slot_cards=slot_cards,
|
||||||
|
)
|
||||||
risk_briefs = self._build_review_risk_briefs(
|
risk_briefs = self._build_review_risk_briefs(
|
||||||
payload,
|
payload,
|
||||||
citations=citations,
|
citations=citations,
|
||||||
@@ -563,6 +572,7 @@ class UserAgentService:
|
|||||||
)
|
)
|
||||||
can_proceed = self._can_proceed_review(
|
can_proceed = self._can_proceed_review(
|
||||||
payload,
|
payload,
|
||||||
|
missing_slot_keys=missing_slot_keys,
|
||||||
claim_groups=claim_groups,
|
claim_groups=claim_groups,
|
||||||
)
|
)
|
||||||
confirmation_actions = self._build_review_confirmation_actions(
|
confirmation_actions = self._build_review_confirmation_actions(
|
||||||
@@ -593,7 +603,7 @@ class UserAgentService:
|
|||||||
scenario=payload.ontology.scenario,
|
scenario=payload.ontology.scenario,
|
||||||
intent=payload.ontology.intent,
|
intent=payload.ontology.intent,
|
||||||
can_proceed=can_proceed,
|
can_proceed=can_proceed,
|
||||||
missing_slots=list(payload.ontology.missing_slots),
|
missing_slots=[SLOT_LABELS.get(key, key) for key in missing_slot_keys],
|
||||||
risk_briefs=risk_briefs,
|
risk_briefs=risk_briefs,
|
||||||
slot_cards=slot_cards,
|
slot_cards=slot_cards,
|
||||||
document_cards=document_cards,
|
document_cards=document_cards,
|
||||||
@@ -607,8 +617,8 @@ class UserAgentService:
|
|||||||
payload: UserAgentRequest,
|
payload: UserAgentRequest,
|
||||||
*,
|
*,
|
||||||
ocr_documents: list[dict[str, object]],
|
ocr_documents: list[dict[str, object]],
|
||||||
|
claim_groups: list[UserAgentReviewClaimGroup],
|
||||||
) -> list[UserAgentReviewSlotCard]:
|
) -> list[UserAgentReviewSlotCard]:
|
||||||
missing_slots = set(payload.ontology.missing_slots)
|
|
||||||
entity_map = self._collect_entity_values(payload)
|
entity_map = self._collect_entity_values(payload)
|
||||||
time_slot = self._build_time_slot(payload)
|
time_slot = self._build_time_slot(payload)
|
||||||
location_slot = self._build_location_slot(payload)
|
location_slot = self._build_location_slot(payload)
|
||||||
@@ -621,7 +631,13 @@ class UserAgentService:
|
|||||||
ocr_documents=ocr_documents,
|
ocr_documents=ocr_documents,
|
||||||
)
|
)
|
||||||
merchant_slot = self._build_merchant_slot(payload, ocr_documents=ocr_documents)
|
merchant_slot = self._build_merchant_slot(payload, ocr_documents=ocr_documents)
|
||||||
|
reason_slot = self._build_reason_slot(payload)
|
||||||
attachment_slot = self._build_attachment_slot(payload)
|
attachment_slot = self._build_attachment_slot(payload)
|
||||||
|
required_keys = self._resolve_required_review_keys(
|
||||||
|
payload,
|
||||||
|
primary_expense_type=str(expense_type_slot["normalized_value"] or ""),
|
||||||
|
claim_groups=claim_groups,
|
||||||
|
)
|
||||||
|
|
||||||
cards = [
|
cards = [
|
||||||
self._make_slot_card(
|
self._make_slot_card(
|
||||||
@@ -632,7 +648,7 @@ class UserAgentService:
|
|||||||
source=expense_type_slot["source"],
|
source=expense_type_slot["source"],
|
||||||
confidence=expense_type_slot["confidence"],
|
confidence=expense_type_slot["confidence"],
|
||||||
evidence=expense_type_slot["evidence"],
|
evidence=expense_type_slot["evidence"],
|
||||||
missing_slots=missing_slots,
|
required="expense_type" in required_keys,
|
||||||
),
|
),
|
||||||
self._make_slot_card(
|
self._make_slot_card(
|
||||||
key="customer_name",
|
key="customer_name",
|
||||||
@@ -642,7 +658,7 @@ class UserAgentService:
|
|||||||
source=customer_slot["source"],
|
source=customer_slot["source"],
|
||||||
confidence=customer_slot["confidence"],
|
confidence=customer_slot["confidence"],
|
||||||
evidence=customer_slot["evidence"],
|
evidence=customer_slot["evidence"],
|
||||||
missing_slots=missing_slots,
|
required="customer_name" in required_keys,
|
||||||
),
|
),
|
||||||
self._make_slot_card(
|
self._make_slot_card(
|
||||||
key="time_range",
|
key="time_range",
|
||||||
@@ -652,7 +668,7 @@ class UserAgentService:
|
|||||||
source=time_slot["source"],
|
source=time_slot["source"],
|
||||||
confidence=time_slot["confidence"],
|
confidence=time_slot["confidence"],
|
||||||
evidence=time_slot["evidence"],
|
evidence=time_slot["evidence"],
|
||||||
missing_slots=missing_slots,
|
required="time_range" in required_keys,
|
||||||
),
|
),
|
||||||
self._make_slot_card(
|
self._make_slot_card(
|
||||||
key="location",
|
key="location",
|
||||||
@@ -662,8 +678,7 @@ class UserAgentService:
|
|||||||
source=location_slot["source"],
|
source=location_slot["source"],
|
||||||
confidence=location_slot["confidence"],
|
confidence=location_slot["confidence"],
|
||||||
evidence=location_slot["evidence"],
|
evidence=location_slot["evidence"],
|
||||||
required=False,
|
required="location" in required_keys,
|
||||||
missing_slots=missing_slots,
|
|
||||||
),
|
),
|
||||||
self._make_slot_card(
|
self._make_slot_card(
|
||||||
key="merchant_name",
|
key="merchant_name",
|
||||||
@@ -673,8 +688,7 @@ class UserAgentService:
|
|||||||
source=merchant_slot["source"],
|
source=merchant_slot["source"],
|
||||||
confidence=merchant_slot["confidence"],
|
confidence=merchant_slot["confidence"],
|
||||||
evidence=merchant_slot["evidence"],
|
evidence=merchant_slot["evidence"],
|
||||||
required=False,
|
required="merchant_name" in required_keys,
|
||||||
missing_slots=missing_slots,
|
|
||||||
),
|
),
|
||||||
self._make_slot_card(
|
self._make_slot_card(
|
||||||
key="amount",
|
key="amount",
|
||||||
@@ -684,7 +698,17 @@ class UserAgentService:
|
|||||||
source=amount_slot["source"],
|
source=amount_slot["source"],
|
||||||
confidence=amount_slot["confidence"],
|
confidence=amount_slot["confidence"],
|
||||||
evidence=amount_slot["evidence"],
|
evidence=amount_slot["evidence"],
|
||||||
missing_slots=missing_slots,
|
required="amount" in required_keys,
|
||||||
|
),
|
||||||
|
self._make_slot_card(
|
||||||
|
key="reason",
|
||||||
|
value=reason_slot["value"],
|
||||||
|
raw_value=reason_slot["raw_value"],
|
||||||
|
normalized_value=reason_slot["normalized_value"],
|
||||||
|
source=reason_slot["source"],
|
||||||
|
confidence=reason_slot["confidence"],
|
||||||
|
evidence=reason_slot["evidence"],
|
||||||
|
required="reason" in required_keys,
|
||||||
),
|
),
|
||||||
self._make_slot_card(
|
self._make_slot_card(
|
||||||
key="participants",
|
key="participants",
|
||||||
@@ -694,7 +718,7 @@ class UserAgentService:
|
|||||||
source=participants_slot["source"],
|
source=participants_slot["source"],
|
||||||
confidence=participants_slot["confidence"],
|
confidence=participants_slot["confidence"],
|
||||||
evidence=participants_slot["evidence"],
|
evidence=participants_slot["evidence"],
|
||||||
missing_slots=missing_slots,
|
required="participants" in required_keys,
|
||||||
),
|
),
|
||||||
self._make_slot_card(
|
self._make_slot_card(
|
||||||
key="attachments",
|
key="attachments",
|
||||||
@@ -704,7 +728,7 @@ class UserAgentService:
|
|||||||
source=attachment_slot["source"],
|
source=attachment_slot["source"],
|
||||||
confidence=attachment_slot["confidence"],
|
confidence=attachment_slot["confidence"],
|
||||||
evidence=attachment_slot["evidence"],
|
evidence=attachment_slot["evidence"],
|
||||||
missing_slots=missing_slots,
|
required="attachments" in required_keys,
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
return cards
|
return cards
|
||||||
@@ -994,11 +1018,12 @@ class UserAgentService:
|
|||||||
def _can_proceed_review(
|
def _can_proceed_review(
|
||||||
payload: UserAgentRequest,
|
payload: UserAgentRequest,
|
||||||
*,
|
*,
|
||||||
|
missing_slot_keys: list[str],
|
||||||
claim_groups: list[UserAgentReviewClaimGroup],
|
claim_groups: list[UserAgentReviewClaimGroup],
|
||||||
) -> bool:
|
) -> bool:
|
||||||
if payload.ontology.ambiguity:
|
if payload.ontology.ambiguity:
|
||||||
return False
|
return False
|
||||||
if payload.ontology.missing_slots:
|
if missing_slot_keys:
|
||||||
return False
|
return False
|
||||||
if not claim_groups:
|
if not claim_groups:
|
||||||
return False
|
return False
|
||||||
@@ -1019,7 +1044,7 @@ class UserAgentService:
|
|||||||
else str(payload.context_json.get("name") or "").strip()
|
else str(payload.context_json.get("name") or "").strip()
|
||||||
)
|
)
|
||||||
manager_name = self._resolve_manager_name(employee)
|
manager_name = self._resolve_manager_name(employee)
|
||||||
reason = self._extract_message_reason(payload.message)
|
reason = slot_map.get("reason").value if slot_map.get("reason") else ""
|
||||||
attachments = "、".join(self._resolve_attachment_names(payload))
|
attachments = "、".join(self._resolve_attachment_names(payload))
|
||||||
|
|
||||||
fields = [
|
fields = [
|
||||||
@@ -1161,6 +1186,38 @@ class UserAgentService:
|
|||||||
return cleaned[:300]
|
return cleaned[:300]
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _resolve_reason_text(cls, message: str) -> str:
|
||||||
|
reason = cls._extract_message_reason(message)
|
||||||
|
if not reason:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
compact = re.sub(r"\s+", "", reason)
|
||||||
|
if compact in GENERIC_EXPENSE_PROMPTS:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
instruction_prefixes = (
|
||||||
|
"帮我生成",
|
||||||
|
"请帮我生成",
|
||||||
|
"生成",
|
||||||
|
"起草",
|
||||||
|
"创建",
|
||||||
|
"发起",
|
||||||
|
"准备",
|
||||||
|
"帮我报销",
|
||||||
|
"我要报销",
|
||||||
|
"我想报销",
|
||||||
|
)
|
||||||
|
if compact.startswith(instruction_prefixes):
|
||||||
|
for separator in (",", ",", "。", ";", ";", ":", ":"):
|
||||||
|
if separator in reason:
|
||||||
|
trailing = reason.split(separator, 1)[1].strip()
|
||||||
|
if trailing:
|
||||||
|
return trailing[:300]
|
||||||
|
return ""
|
||||||
|
|
||||||
|
return reason
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _should_skip_model_answer(
|
def _should_skip_model_answer(
|
||||||
payload: UserAgentRequest,
|
payload: UserAgentRequest,
|
||||||
@@ -1561,6 +1618,31 @@ class UserAgentService:
|
|||||||
)
|
)
|
||||||
return self._build_slot_value()
|
return self._build_slot_value()
|
||||||
|
|
||||||
|
def _build_reason_slot(self, payload: UserAgentRequest) -> dict[str, str | float]:
|
||||||
|
review_form_values = self._resolve_review_form_values(payload)
|
||||||
|
edited_value = str(review_form_values.get("reason") or "").strip()
|
||||||
|
if edited_value:
|
||||||
|
return self._build_slot_value(
|
||||||
|
value=edited_value,
|
||||||
|
raw_value=edited_value,
|
||||||
|
normalized_value=edited_value,
|
||||||
|
source="user_form",
|
||||||
|
confidence=1.0,
|
||||||
|
evidence="来源于用户修改后的结构化表单。",
|
||||||
|
)
|
||||||
|
|
||||||
|
reason_value = self._resolve_reason_text(payload.message)
|
||||||
|
if reason_value:
|
||||||
|
return self._build_slot_value(
|
||||||
|
value=reason_value,
|
||||||
|
raw_value=reason_value,
|
||||||
|
normalized_value=reason_value,
|
||||||
|
source="user_text",
|
||||||
|
confidence=0.76,
|
||||||
|
evidence="系统从用户原始描述中提取了本次费用事由,建议继续核对。",
|
||||||
|
)
|
||||||
|
return self._build_slot_value()
|
||||||
|
|
||||||
def _build_amount_slot(
|
def _build_amount_slot(
|
||||||
self,
|
self,
|
||||||
payload: UserAgentRequest,
|
payload: UserAgentRequest,
|
||||||
@@ -1719,18 +1801,65 @@ class UserAgentService:
|
|||||||
def _normalize_expense_type_input(value: str) -> tuple[str, str]:
|
def _normalize_expense_type_input(value: str) -> tuple[str, str]:
|
||||||
compact = str(value or "").replace(" ", "")
|
compact = str(value or "").replace(" ", "")
|
||||||
if "招待" in compact or ("客户" in compact and any(keyword in compact for keyword in ("吃饭", "用餐", "宴请", "请客"))):
|
if "招待" in compact or ("客户" in compact and any(keyword in compact for keyword in ("吃饭", "用餐", "宴请", "请客"))):
|
||||||
return "entertainment", "招待"
|
return "entertainment", "业务招待费"
|
||||||
if any(keyword in compact for keyword in ("差旅", "出差", "机票", "行程")):
|
if any(keyword in compact for keyword in ("差旅", "出差", "机票", "行程")):
|
||||||
return "travel", "差旅"
|
return "travel", "差旅费"
|
||||||
if any(keyword in compact for keyword in ("住宿", "酒店", "宾馆")):
|
if any(keyword in compact for keyword in ("住宿", "酒店", "宾馆")):
|
||||||
return "hotel", "住宿"
|
return "hotel", "住宿费"
|
||||||
if any(keyword in compact for keyword in ("交通", "打车", "网约车", "出租车", "车费", "停车")):
|
if any(keyword in compact for keyword in ("交通", "打车", "网约车", "出租车", "车费", "停车")):
|
||||||
return "transport", "交通"
|
return "transport", "交通费"
|
||||||
if any(keyword in compact for keyword in ("餐费", "用餐", "午餐", "晚餐", "早餐", "伙食")):
|
if any(keyword in compact for keyword in ("餐费", "用餐", "午餐", "晚餐", "早餐", "伙食")):
|
||||||
return "meal", "餐费"
|
return "meal", "餐费"
|
||||||
if "会务" in compact:
|
if "会务" in compact:
|
||||||
return "meeting", "会务"
|
return "meeting", "会务费"
|
||||||
return "other", str(value or "").strip() or "其他"
|
return "other", str(value or "").strip() or "其他费用"
|
||||||
|
|
||||||
|
def _resolve_required_review_keys(
|
||||||
|
self,
|
||||||
|
payload: UserAgentRequest,
|
||||||
|
*,
|
||||||
|
primary_expense_type: str,
|
||||||
|
claim_groups: list[UserAgentReviewClaimGroup],
|
||||||
|
) -> set[str]:
|
||||||
|
required = {"expense_type", "time_range", "amount", "reason", "attachments"}
|
||||||
|
scene_codes = {
|
||||||
|
str(item.group_code or "").strip()
|
||||||
|
for item in claim_groups
|
||||||
|
if str(item.group_code or "").strip()
|
||||||
|
}
|
||||||
|
if primary_expense_type:
|
||||||
|
scene_codes.add(primary_expense_type)
|
||||||
|
|
||||||
|
compact_message = re.sub(r"\s+", "", payload.message)
|
||||||
|
if "entertainment" in scene_codes or (
|
||||||
|
"客户" in compact_message and any(keyword in compact_message for keyword in ("招待", "吃饭", "用餐", "宴请", "请客"))
|
||||||
|
):
|
||||||
|
required.update({"customer_name", "participants"})
|
||||||
|
|
||||||
|
return required
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _resolve_review_missing_slot_keys(
|
||||||
|
payload: UserAgentRequest,
|
||||||
|
*,
|
||||||
|
slot_cards: list[UserAgentReviewSlotCard],
|
||||||
|
) -> list[str]:
|
||||||
|
required_keys = {item.key for item in slot_cards if item.required}
|
||||||
|
missing_keys = {
|
||||||
|
item.key
|
||||||
|
for item in slot_cards
|
||||||
|
if item.required and (item.status == "missing" or not str(item.value).strip())
|
||||||
|
}
|
||||||
|
for key in payload.ontology.missing_slots:
|
||||||
|
normalized_key = str(key or "").strip()
|
||||||
|
if normalized_key and normalized_key in required_keys:
|
||||||
|
missing_keys.add(normalized_key)
|
||||||
|
|
||||||
|
ordered_keys: list[str] = []
|
||||||
|
for item in slot_cards:
|
||||||
|
if item.required and item.key in missing_keys and item.key not in ordered_keys:
|
||||||
|
ordered_keys.append(item.key)
|
||||||
|
return ordered_keys
|
||||||
|
|
||||||
def _make_slot_card(
|
def _make_slot_card(
|
||||||
self,
|
self,
|
||||||
@@ -1742,10 +1871,9 @@ class UserAgentService:
|
|||||||
source: str,
|
source: str,
|
||||||
confidence: float,
|
confidence: float,
|
||||||
evidence: str,
|
evidence: str,
|
||||||
missing_slots: set[str],
|
|
||||||
required: bool = True,
|
required: bool = True,
|
||||||
) -> UserAgentReviewSlotCard:
|
) -> UserAgentReviewSlotCard:
|
||||||
is_missing = key in missing_slots or not str(value).strip()
|
is_missing = required and not str(value).strip()
|
||||||
source_key = source if source in SOURCE_LABELS else "system"
|
source_key = source if source in SOURCE_LABELS else "system"
|
||||||
return UserAgentReviewSlotCard(
|
return UserAgentReviewSlotCard(
|
||||||
key=key,
|
key=key,
|
||||||
@@ -1764,43 +1892,6 @@ class UserAgentService:
|
|||||||
else ("该字段来自系统辅助上下文,建议你再核对一次。" if source in {"detail_context", "ocr"} else ""),
|
else ("该字段来自系统辅助上下文,建议你再核对一次。" if source in {"detail_context", "ocr"} else ""),
|
||||||
evidence=evidence,
|
evidence=evidence,
|
||||||
)
|
)
|
||||||
request_context = payload.context_json.get("request_context")
|
|
||||||
if isinstance(request_context, dict):
|
|
||||||
for key in ("city", "location"):
|
|
||||||
value = str(request_context.get(key) or "").strip()
|
|
||||||
if value:
|
|
||||||
return value
|
|
||||||
city_match = re.search(r"去(?P<city>[\u4e00-\u9fa5]{2,8})(?:出差|拜访|参会|见客户|客户现场)", payload.message)
|
|
||||||
if city_match:
|
|
||||||
return city_match.group("city").strip()
|
|
||||||
if "客户现场" in payload.message.replace(" ", ""):
|
|
||||||
return "客户现场"
|
|
||||||
return ""
|
|
||||||
|
|
||||||
def _make_slot_card(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
key: str,
|
|
||||||
value: str,
|
|
||||||
source: str,
|
|
||||||
confidence: float,
|
|
||||||
missing_slots: set[str],
|
|
||||||
required: bool = True,
|
|
||||||
) -> UserAgentReviewSlotCard:
|
|
||||||
is_missing = key in missing_slots or not str(value).strip()
|
|
||||||
return UserAgentReviewSlotCard(
|
|
||||||
key=key,
|
|
||||||
label=SLOT_LABELS.get(key, key),
|
|
||||||
value=str(value or "").strip(),
|
|
||||||
source=source,
|
|
||||||
confidence=confidence,
|
|
||||||
required=required,
|
|
||||||
confirmed=not is_missing and source in {"user_text", "page_context", "upload"},
|
|
||||||
status="missing" if is_missing else "identified" if source == "user_text" else "inferred",
|
|
||||||
hint=f"建议补充 {SLOT_LABELS.get(key, key)}。"
|
|
||||||
if is_missing and required
|
|
||||||
else "",
|
|
||||||
)
|
|
||||||
|
|
||||||
def _classify_document(
|
def _classify_document(
|
||||||
self,
|
self,
|
||||||
|
|||||||
Reference in New Issue
Block a user