Files
X-Financial/server/src/app/services/user_agent_review_messages.py
2026-05-22 10:42:31 +08:00

674 lines
30 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
import json
import re
from datetime import UTC, datetime, timedelta
from decimal import Decimal, InvalidOperation
from typing import Any
from sqlalchemy import or_, select
from sqlalchemy.orm import selectinload
from app.api.deps import CurrentUserContext
from app.core.agent_enums import AgentAssetStatus, AgentAssetType
from app.models.employee import Employee
from app.models.financial_record import ExpenseClaim
from app.schemas.agent_asset import AgentAssetListItem
from app.schemas.reimbursement import TravelReimbursementCalculatorRequest
from app.schemas.user_agent import (
UserAgentCitation,
UserAgentDraftPayload,
UserAgentExpenseQueryRecord,
UserAgentQueryPayload,
UserAgentQueryStatusGroup,
UserAgentReviewAction,
UserAgentReviewClaimGroup,
UserAgentReviewDocumentCard,
UserAgentReviewDocumentField,
UserAgentReviewEditField,
UserAgentReviewPayload,
UserAgentReviewRiskBrief,
UserAgentReviewSlotCard,
UserAgentRequest,
UserAgentSuggestedAction,
)
from app.services.agent_assets import AgentAssetService
from app.services.expense_claims import ExpenseClaimService
from app.services.expense_rule_runtime import ExpenseRuleRuntimeService, RuntimeTravelPolicy, resolve_document_type_label
from app.services.risk_ontology_bridge import resolve_rule_codes_for_risk_check
from app.services.travel_reimbursement_calculator import TravelReimbursementCalculatorService
from app.services.user_agent_constants import *
class UserAgentReviewMessageMixin:
def _build_review_confirmation_actions(
self,
payload: UserAgentRequest,
*,
can_proceed: bool,
claim_groups: list[UserAgentReviewClaimGroup],
draft_payload: UserAgentDraftPayload | None,
missing_slot_keys: set[str] | None = None,
) -> list[UserAgentReviewAction]:
missing_slot_keys = set(missing_slot_keys or set())
if self._is_review_association_choice_pending(payload):
claim_no = str(payload.tool_payload.get("association_candidate_claim_no") or "").strip()
link_label = f"关联到草稿 {claim_no}" if claim_no else "关联到现有草稿"
return [
UserAgentReviewAction(
label=link_label,
action_type="link_to_existing_draft",
description=(
f"把本次上传票据并入现有草稿 {claim_no}"
if claim_no
else "把本次上传票据并入现有草稿。"
),
emphasis="primary",
),
UserAgentReviewAction(
label="单独建立报销单",
action_type="create_new_claim_from_documents",
description="基于当前上传的多张票据,新建一张独立的报销草稿。",
emphasis="secondary",
),
]
review_action = str(payload.context_json.get("review_action") or "").strip()
if "expense_type" in missing_slot_keys and not review_action:
return [
UserAgentReviewAction(
label="保存为草稿",
action_type="save_draft",
description="先暂存当前已识别信息,稍后仍可从个人报销继续补充或提交。",
emphasis="primary",
),
]
primary_action = UserAgentReviewAction(
label="继续下一步" if can_proceed else "保存为草稿",
action_type="next_step" if can_proceed else "save_draft",
description=(
"当前识别信息已满足继续处理条件,确认后进入下一步。"
if can_proceed
else "暂存当前识别结果,后续可以继续补充或修改。"
),
emphasis="primary",
)
if len(claim_groups) > 1 and can_proceed:
primary_action.description = f"系统建议拆分为 {len(claim_groups)} 张报销单,确认后继续下一步。"
if draft_payload is not None and draft_payload.claim_no and not can_proceed:
primary_action.description = f"保存后会生成草稿 {draft_payload.claim_no},后续仍可继续补充。"
actions = []
if can_proceed:
actions.append(
UserAgentReviewAction(
label="保存为草稿",
action_type="save_draft",
description="先暂存当前已识别信息,稍后仍可从个人报销继续补充或提交。",
emphasis="secondary",
)
)
actions.append(primary_action)
return actions
def _build_review_intent_summary(
self,
payload: UserAgentRequest,
*,
slot_cards: list[UserAgentReviewSlotCard],
claim_groups: list[UserAgentReviewClaimGroup],
) -> str:
slots = {item.key: item for item in slot_cards}
expense_type = slots.get("expense_type")
amount = slots.get("amount")
time_range = slots.get("time_range")
location = slots.get("location")
customer = slots.get("customer_name")
summary = "我先根据您当前提供的信息整理出一笔报销:"
if expense_type and expense_type.value:
summary = f"识别到您希望报销一笔“{expense_type.value}”费用:"
details: list[str] = []
if customer and customer.value:
details.append(f"客户:{customer.value}")
if time_range and time_range.value:
details.append(f"时间:{time_range.value}")
if location and location.value:
details.append(f"地点:{location.value}")
if amount and amount.value:
details.append(f"金额:{amount.value}")
reason = slots.get("reason")
if reason and reason.value:
details.append(f"事由:{reason.value}")
if details:
return "\n\n".join([summary, "基础信息识别结果:", "\n".join(details)])
return summary
def _build_review_body_answer(
self,
payload: UserAgentRequest,
*,
review_payload: UserAgentReviewPayload | None,
draft_payload: UserAgentDraftPayload | None,
) -> str | None:
if review_payload is None:
return None
if payload.ontology.scenario != "expense":
return None
if payload.ontology.intent not in {"draft", "operate"}:
return None
if payload.tool_payload.get("draft_limit_reached"):
return (
str(payload.tool_payload.get("message") or "").strip()
or "你当前已保存 3 个草稿,请先完成已保存的草稿,才能再次新建草稿。"
)
review_action = str(payload.context_json.get("review_action") or "").strip()
if payload.tool_payload.get("preview_only") and not review_action:
return review_payload.body_message or self._build_review_intent_summary(
payload,
slot_cards=review_payload.slot_cards,
claim_groups=review_payload.claim_groups,
)
if payload.tool_payload.get("duplicate_attachment_blocked") or payload.tool_payload.get("duplicate_invoice_blocked"):
return (
str(payload.tool_payload.get("message") or "").strip()
or "检测到本次上传票据与当前单据已有票据重复,请重新上传不同的票据后再归集。"
)
if review_action == "save_draft":
if draft_payload is not None and draft_payload.claim_no:
return (
f"已按您当前确认的信息保存为草稿 {draft_payload.claim_no}"
"后续您可以继续补充缺失项,或修改识别结果后再继续提交。"
)
return "已按您当前确认的信息保存为草稿。后续您可以继续补充缺失项,或修改识别结果后再继续提交。"
if review_action == "link_to_existing_draft":
document_count = self._resolve_review_document_count(payload)
followup_copy = self._build_review_action_followup_copy(review_payload)
if draft_payload is not None and draft_payload.claim_no:
return (
f"已将本次上传的 {document_count} 张票据关联到草稿 {draft_payload.claim_no}"
f"{followup_copy or '您可以继续补充识别字段,确认无误后再提交审批。'}"
)
return f"已将本次上传的票据关联到现有草稿。{followup_copy or '您可以继续补充识别字段,确认无误后再提交审批。'}"
if review_action == "create_new_claim_from_documents":
document_count = self._resolve_review_document_count(payload)
followup_copy = self._build_review_action_followup_copy(review_payload)
if draft_payload is not None and draft_payload.claim_no:
return (
f"已按当前上传的 {document_count} 张票据新建报销草稿 {draft_payload.claim_no}"
f"{followup_copy or '您可以继续补充识别字段,确认无误后再提交审批。'}"
)
return f"已按当前上传票据新建报销草稿。{followup_copy or '您可以继续补充识别字段,确认无误后再提交审批。'}"
if review_action == "next_step":
if draft_payload is not None and draft_payload.status == "submitted":
stage_text = draft_payload.approval_stage or "审批中"
return f"报销单 {draft_payload.claim_no or ''} 已提交,当前节点为 {stage_text}".strip()
if payload.tool_payload.get("submission_blocked"):
reasons = self._resolve_submission_blocked_reasons(payload)
if reasons:
reason_lines = "\n".join(
f"{index}. {reason}" for index, reason in enumerate(reasons, start=1)
)
return (
"AI预审暂未通过所以还没有提交到审批人。\n"
f"{reason_lines}\n"
"请先处理以上项目;处理完成后再点继续下一步。"
)
return str(payload.tool_payload.get("message") or "").strip() or "当前报销单暂时还不能提交审批。"
return (
f"{self._build_review_intent_summary(payload, slot_cards=review_payload.slot_cards, claim_groups=review_payload.claim_groups)}\n\n"
"当前关键信息已基本齐全,您确认无误后可以继续下一步。"
)
return review_payload.body_message or None
def _build_review_body_message(
self,
payload: UserAgentRequest,
*,
slot_cards: list[UserAgentReviewSlotCard],
risk_briefs: list[UserAgentReviewRiskBrief],
can_proceed: bool,
document_cards: list[UserAgentReviewDocumentCard],
travel_receipt_state: dict[str, Any] | None = None,
) -> str:
if self._is_review_association_choice_pending(payload):
claim_no = str(payload.tool_payload.get("association_candidate_claim_no") or "").strip()
document_count = len(document_cards) or self._resolve_review_document_count(payload)
if claim_no:
return (
f"已识别出本次上传的 {document_count} 张票据。"
f"系统检测到你已有草稿 {claim_no},请选择关联到该草稿,或单独建立一张新的报销单。"
)
return (
f"已识别出本次上传的 {document_count} 张票据。"
"系统检测到你已有可用草稿,请先选择关联到现有草稿,或单独建立一张新的报销单。"
)
blocked_reasons = self._resolve_submission_blocked_reasons(payload)
if blocked_reasons:
reason_text = "".join(dict.fromkeys(reason.strip("。;;") for reason in blocked_reasons if reason))
return (
f"AI预审未通过{reason_text}"
"请先根据风险提示补充原因、调整金额或更换附件,整改后再继续提交。"
)
travel_message = self._build_travel_receipt_guidance_message(
payload,
travel_receipt_state=travel_receipt_state or {},
can_proceed=can_proceed,
)
if travel_message:
return travel_message
missing_labels = self._resolve_review_missing_slot_labels(slot_cards)
if travel_receipt_state:
missing_labels.extend(
str(item)
for item in travel_receipt_state.get("required_missing_labels", [])
if str(item).strip()
)
missing_labels = list(dict.fromkeys(missing_labels))
expense_type_slot = next((item for item in slot_cards if item.key == "expense_type"), None)
if expense_type_slot is not None and not str(expense_type_slot.value or "").strip():
return (
f"{self._build_review_intent_summary(payload, slot_cards=slot_cards, claim_groups=[])}\n\n"
"我已经先保留了当前识别出的时间、地点和事由,但还不能确定这张单据应该走哪类报销流程。"
"请先点击“选择报销类型”,在差旅费、交通费、住宿费等选项中选定;"
"选定后,后续上传的票据都会作为这张单据的补充继续核对,不会重新改判报销类型。"
)
review_payload = UserAgentReviewPayload(
intent_summary="",
body_message="",
scenario=payload.ontology.scenario,
intent=payload.ontology.intent,
can_proceed=can_proceed,
missing_slots=missing_labels,
risk_briefs=risk_briefs,
slot_cards=slot_cards,
document_cards=[],
claim_groups=[],
confirmation_actions=[],
edit_fields=[],
)
return "\n\n".join(
item
for item in [
self._build_review_intent_summary(payload, slot_cards=slot_cards, claim_groups=[]),
self._build_review_standard_calculation_copy(payload, slot_cards),
self._build_review_guidance_copy(review_payload, mention_save_draft=not can_proceed),
]
if item
)
def _build_review_standard_calculation_copy(
self,
payload: UserAgentRequest,
slot_cards: list[UserAgentReviewSlotCard],
) -> str:
slots = {item.key: item for item in slot_cards}
expense_type = str(slots.get("expense_type").value if slots.get("expense_type") else "").strip()
if "差旅" in expense_type:
return self._build_review_travel_calculation_table(payload, slots)
if "交通" in expense_type:
return (
"报销测算参考:交通费通常以实际票据金额为基础,结合出行地点、业务事由和票据合规性复核;"
"如果它属于差旅行程的一部分,后续也会并入差旅费测算。"
)
if "住宿" in expense_type:
return (
"报销测算参考:住宿费通常按“实际住宿金额”和“目的地住宿标准 × 住宿天数”取合规口径;"
"补齐酒店票据后再核对是否超标。"
)
return (
"报销测算参考:先以用户填写金额或票据识别金额为基础,"
"再结合费用类型、发生地点、业务事由和规则中心限额进行复核。"
)
def _build_review_travel_calculation_table(
self,
payload: UserAgentRequest,
slots: dict[str, UserAgentReviewSlotCard],
) -> str:
destination = self._resolve_slot_text(slots, "location")
days = self._resolve_review_travel_days(payload, slots)
ticket_amount = self._resolve_slot_money(slots, "amount")
employee = self._resolve_employee_profile(payload)
grade = self._resolve_review_employee_grade(payload, employee=employee)
if not destination or not grade:
return "\n".join(
[
"报销测算参考:",
"",
"| 项目 | 当前信息 | 测算说明 |",
"| --- | --- | --- |",
f"| 出差地点 | {destination or '待确认'} | 用于匹配城市住宿标准和补贴区域 |",
f"| 出差天数 | {days} 天 | 来自业务发生时间或用户描述 |",
f"| 职级 | {grade or '待确认'} | 补齐后才能匹配住宿标准和补贴档位 |",
f"| 交通票据 | {self._format_decimal_money(ticket_amount)} 元 | 上传票据后会按真实金额重新复核 |",
]
)
current_user = CurrentUserContext(
username=str(payload.user_id or payload.context_json.get("name") or "anonymous").strip() or "anonymous",
name=str(payload.context_json.get("name") or payload.user_id or "anonymous").strip() or "anonymous",
role_codes=[
str(item).strip()
for item in list(payload.context_json.get("role_codes") or [])
if str(item).strip()
],
is_admin=bool(payload.context_json.get("is_admin")),
department_name=str(payload.context_json.get("department_name") or payload.context_json.get("department") or "").strip(),
)
try:
calculation = TravelReimbursementCalculatorService(self.db).calculate(
TravelReimbursementCalculatorRequest(days=days, location=destination, grade=grade),
current_user,
)
except Exception:
return "\n".join(
[
"报销测算参考:",
"",
"| 项目 | 当前信息 | 测算说明 |",
"| --- | --- | --- |",
f"| 出差地点 | {destination} | 暂时未能匹配规则中心地点 |",
f"| 出差天数 | {days} 天 | 来自业务发生时间或用户描述 |",
f"| 职级 | {grade} | 暂时无法自动匹配差旅标准 |",
f"| 交通票据 | {self._format_decimal_money(ticket_amount)} 元 | 上传票据后会按真实金额重新复核 |",
]
)
total_amount = (
ticket_amount
+ self._coerce_decimal_money(calculation.hotel_amount)
+ self._coerce_decimal_money(calculation.allowance_amount)
).quantize(Decimal("0.01"))
ticket_basis = "当前未上传交通票据,先按 0.00 元占位" if ticket_amount <= Decimal("0.00") else "已识别或填写的交通票据金额"
return "\n".join(
[
"报销测算参考:",
"",
(
f"职级 {calculation.grade},目的地 {destination},匹配城市 {calculation.matched_city}"
"补齐交通、酒店等票据后,我会按真实票据金额和规则中心标准重新复核。"
),
"",
"| 项目 | 测算口径 | 金额 |",
"| --- | --- | ---: |",
f"| 交通票据 | {ticket_basis} | {self._format_decimal_money(ticket_amount)} 元 |",
f"| 住宿标准 | {self._format_decimal_money(calculation.hotel_rate)} 元/天 × {calculation.days} 天 | {self._format_decimal_money(calculation.hotel_amount)} 元 |",
f"| 出差补贴 | {self._format_decimal_money(calculation.total_allowance_rate)} 元/天 × {calculation.days} 天 | {self._format_decimal_money(calculation.allowance_amount)} 元 |",
f"| 参考合计 | 交通票据 + 住宿标准 + 出差补贴 | {self._format_decimal_money(total_amount)} 元 |",
]
)
@staticmethod
def _resolve_slot_text(slots: dict[str, UserAgentReviewSlotCard], key: str) -> str:
item = slots.get(key)
return str(getattr(item, "value", "") or getattr(item, "raw_value", "") or "").strip()
def _resolve_review_travel_days(
self,
payload: UserAgentRequest,
slots: dict[str, UserAgentReviewSlotCard],
) -> int:
text = " ".join(
[
str(payload.message or ""),
str(payload.context_json.get("user_input_text") or ""),
self._resolve_slot_text(slots, "reason"),
self._resolve_slot_text(slots, "time_range"),
]
)
explicit_match = re.search(r"(?<!\d)(\d{1,2})\s*天", text)
if explicit_match:
return max(1, int(explicit_match.group(1)))
dates = self._extract_dates_from_text(self._resolve_slot_text(slots, "time_range"))
if len(dates) >= 2:
return max(1, (max(dates).date() - min(dates).date()).days)
return 1
def _resolve_slot_money(
self,
slots: dict[str, UserAgentReviewSlotCard],
key: str,
) -> Decimal:
text = self._resolve_slot_text(slots, key).replace(",", "")
match = re.search(r"([0-9]+(?:\.[0-9]{1,2})?)", text)
if not match:
return Decimal("0.00")
return self._coerce_decimal_money(match.group(1))
@staticmethod
def _build_review_action_followup_copy(review_payload: UserAgentReviewPayload) -> str:
missing_slots = [str(item).strip() for item in review_payload.missing_slots if str(item).strip()]
receipt_briefs = [
item
for item in review_payload.risk_briefs
if "差旅票据待补充" in str(item.title or "")
]
if missing_slots:
return f"当前仍有 {''.join(missing_slots)},暂时只能保存为草稿,补齐后再继续下一步。"
if receipt_briefs:
return "当前必需票据已具备;如还有市内交通、打车、地铁或停车等乘车票据,可以继续上传,也可以继续下一步或保存草稿。"
if review_payload.can_proceed:
return "当前信息已较完整,您可以继续下一步,也可以先保存为草稿。"
return ""
def _build_travel_receipt_guidance_message(
self,
payload: UserAgentRequest,
*,
travel_receipt_state: dict[str, Any],
can_proceed: bool,
) -> str:
review_action = str(payload.context_json.get("review_action") or "").strip()
if review_action or not travel_receipt_state.get("has_long_distance_ticket"):
return ""
employee = self._resolve_employee_profile(payload)
user_name = (
str(employee.name).strip()
if employee is not None and employee.name
else str(payload.context_json.get("name") or payload.user_id or "同事").strip()
)
destination = str(travel_receipt_state.get("destination") or "待确认").strip()
days = max(1, int(travel_receipt_state.get("days") or 1))
ticket_type_label = str(travel_receipt_state.get("ticket_type_label") or "交通").strip()
ticket_amount = self._coerce_decimal_money(travel_receipt_state.get("ticket_amount"))
required_labels = [
str(item).strip()
for item in travel_receipt_state.get("required_missing_labels", [])
if str(item).strip()
]
optional_labels = [
str(item).strip()
for item in travel_receipt_state.get("optional_missing_labels", [])
if str(item).strip()
]
provide_items: list[str] = []
if required_labels:
provide_items.append("1. 酒店住宿发票/住宿清单(必须,当前待上传)")
if optional_labels:
provide_items.append(f"{len(provide_items) + 1}. 市内交通/乘车票据(非必须,如打车、地铁、停车等)")
sections = [
f"您好,{user_name}。我先按票据信息做一次差旅预检。",
"\n".join(
[
"已识别信息:",
f"1. 出差地点:{destination}",
f"2. 预计天数:{days}",
f"3. 票据类型:{ticket_type_label}",
f"4. 票据金额:{self._format_decimal_money(ticket_amount)}",
]
),
]
if provide_items:
sections.append("还需补充:\n" + "\n".join(provide_items))
else:
sections.append("票据完整性:当前核心票据已较完整,无需继续上传票据。")
if required_labels:
sections.append(
"处理建议:酒店票据仍缺失,暂时不能继续下一步。"
"您可以先保存为草稿,补齐后再提交。"
)
elif can_proceed and optional_labels:
sections.append(
"处理建议:必需票据已具备。"
"如暂时没有乘车票据,也可以继续下一步,或先保存为草稿。"
)
elif can_proceed:
sections.append(
"处理建议:当前信息已较完整,确认无误后可以继续下一步;"
"暂时不提交时,也可以先保存为草稿。"
)
estimate_copy = self._build_travel_receipt_estimate_copy(
payload,
travel_receipt_state=travel_receipt_state,
)
if estimate_copy:
sections.append(estimate_copy)
return "\n\n".join(section for section in sections if section)
def _build_travel_receipt_estimate_copy(
self,
payload: UserAgentRequest,
*,
travel_receipt_state: dict[str, Any],
) -> str:
destination = str(travel_receipt_state.get("destination") or "").strip()
days = max(1, int(travel_receipt_state.get("days") or 1))
ticket_type_label = str(travel_receipt_state.get("ticket_type_label") or "交通").strip()
ticket_amount = self._coerce_decimal_money(travel_receipt_state.get("ticket_amount"))
employee = self._resolve_employee_profile(payload)
grade = self._resolve_review_employee_grade(payload, employee=employee)
if not destination or not grade:
return (
"差旅费测算:\n"
f"1. 职级:{grade or '待确认'}\n"
f"2. 目的地:{destination or '出差地点待确认'}\n"
f"3. 已提交{ticket_type_label}{self._format_decimal_money(ticket_amount)}\n"
"4. 住宿和补贴金额:需补齐职级或地点后再核算。"
)
current_user = CurrentUserContext(
username=str(payload.user_id or payload.context_json.get("name") or "anonymous").strip() or "anonymous",
name=str(payload.context_json.get("name") or payload.user_id or "anonymous").strip() or "anonymous",
role_codes=[
str(item).strip()
for item in list(payload.context_json.get("role_codes") or [])
if str(item).strip()
],
is_admin=bool(payload.context_json.get("is_admin")),
department_name=str(payload.context_json.get("department_name") or payload.context_json.get("department") or "").strip(),
)
try:
calculation = TravelReimbursementCalculatorService(self.db).calculate(
TravelReimbursementCalculatorRequest(days=days, location=destination, grade=grade),
current_user,
)
except Exception:
return (
"差旅费测算:\n"
f"1. 职级:{grade}\n"
f"2. 目的地:{destination}\n"
f"3. 已提交{ticket_type_label}{self._format_decimal_money(ticket_amount)}\n"
"4. 住宿和补贴标准:暂时无法自动测算,请以规则中心最新差旅标准为准。"
)
total_amount = (
ticket_amount
+ self._coerce_decimal_money(calculation.hotel_amount)
+ self._coerce_decimal_money(calculation.allowance_amount)
).quantize(Decimal("0.01"))
return (
"差旅费测算:\n"
f"1. 职级:{calculation.grade}\n"
f"2. 目的地:{calculation.matched_city or destination}\n"
f"3. 已提交{ticket_type_label}{self._format_decimal_money(ticket_amount)}\n"
f"4. 住宿标准:{self._format_decimal_money(calculation.hotel_rate)} 元/天 × {calculation.days}\n"
f"5. 出差补贴:{self._format_decimal_money(calculation.total_allowance_rate)} 元/天 × {calculation.days}\n"
f"6. 参考合计:{self._format_decimal_money(total_amount)}"
)
@staticmethod
def _coerce_decimal_money(value: Any) -> Decimal:
try:
return Decimal(str(value or "0")).quantize(Decimal("0.01"))
except (InvalidOperation, ValueError):
return Decimal("0.00")
@staticmethod
def _format_decimal_money(value: Any) -> str:
return f"{UserAgentReviewMessageMixin._coerce_decimal_money(value):.2f}"
@staticmethod
def _resolve_review_missing_slot_labels(
slot_cards: list[UserAgentReviewSlotCard],
) -> list[str]:
return [item.label for item in slot_cards if item.status == "missing"]
@staticmethod
def _build_review_guidance_copy(
review_payload: UserAgentReviewPayload,
*,
mention_save_draft: bool,
) -> str:
reminder_count = len(review_payload.risk_briefs)
if review_payload.can_proceed:
if reminder_count:
return (
f"当前关键信息已基本齐全,但还有 {reminder_count} 条提醒。"
"请核查对话中的文字说明,确认无误后继续下一步。"
)
return "当前关键信息已基本齐全,您确认无误后可以继续下一步。"
return ""
@staticmethod
def _can_proceed_review(
payload: UserAgentRequest,
*,
missing_slot_keys: list[str],
claim_groups: list[UserAgentReviewClaimGroup],
) -> bool:
if payload.ontology.ambiguity:
return False
if missing_slot_keys:
return False
if not claim_groups:
return False
return True