|
|
@@ -0,0 +1,1128 @@
|
|
|
+"""智保通 Agent 可以调用的受控业务工具。
|
|
|
+
|
|
|
+第三阶段新增退保进度、退保运营概览和合作机构查询。Agent 仍然只负责理解意图
|
|
|
+和选择工具:它可以读取流程状态,但不能绕过审批直接改变退款结果。
|
|
|
+合作机构数据通过只读 MCP 服务获取,MCP 不可用时必须明确降级,禁止编造机构。
|
|
|
+"""
|
|
|
+
|
|
|
+from typing import Any, Literal, Protocol
|
|
|
+
|
|
|
+from pydantic import BaseModel, ConfigDict, Field
|
|
|
+
|
|
|
+from zbt.core.errors import AppError
|
|
|
+from zbt.domains.catalog.service import ProductCatalogService
|
|
|
+from zbt.domains.enrollment.service import EnrollmentService
|
|
|
+from zbt.domains.partner_resources.gateway import (
|
|
|
+ PartnerResourceGateway,
|
|
|
+ PartnerResourceSearchResult,
|
|
|
+)
|
|
|
+from zbt.domains.surrender.service import SurrenderService
|
|
|
+from zbt.harness.policy import ToolPolicy
|
|
|
+from zbt.harness.schemas import (
|
|
|
+ AgentAction,
|
|
|
+ BusinessListBlock,
|
|
|
+ ChartBlock,
|
|
|
+ HarnessToolResult,
|
|
|
+ KnowledgeSourcesBlock,
|
|
|
+ MetricItem,
|
|
|
+ MetricsBlock,
|
|
|
+ QuoteBlock,
|
|
|
+ QuoteView,
|
|
|
+)
|
|
|
+from zbt.harness.tooling import ToolDefinition, ToolExecutionContext, ToolRegistry
|
|
|
+
|
|
|
+
|
|
|
+class AttributionMetricsProvider(Protocol):
|
|
|
+ def performance(self, admin_user_id: str | None = None) -> dict[str, Any]: ...
|
|
|
+
|
|
|
+ def order_ids(self, admin_user_id: str | None = None) -> set[str]: ...
|
|
|
+
|
|
|
+
|
|
|
+class KnowledgeSearchProvider(Protocol):
|
|
|
+ def search(
|
|
|
+ self,
|
|
|
+ query: str,
|
|
|
+ *,
|
|
|
+ limit: int = 5,
|
|
|
+ product_code: str | None = None,
|
|
|
+ document_types: tuple[str, ...] = (),
|
|
|
+ ) -> dict[str, Any]: ...
|
|
|
+
|
|
|
+
|
|
|
+class OperationalAnalyticsProvider(Protocol):
|
|
|
+ def query(
|
|
|
+ self,
|
|
|
+ *,
|
|
|
+ metric: str,
|
|
|
+ group_by: str,
|
|
|
+ days: int,
|
|
|
+ admin_user_id: str | None = None,
|
|
|
+ ) -> dict[str, Any]: ...
|
|
|
+
|
|
|
+
|
|
|
+class CustomerMemoryProvider(Protocol):
|
|
|
+ def save(
|
|
|
+ self,
|
|
|
+ *,
|
|
|
+ owner_id: str,
|
|
|
+ category: str,
|
|
|
+ content: str,
|
|
|
+ ) -> dict[str, object]: ...
|
|
|
+
|
|
|
+ def search(
|
|
|
+ self,
|
|
|
+ *,
|
|
|
+ owner_id: str,
|
|
|
+ query: str,
|
|
|
+ limit: int = 3,
|
|
|
+ ) -> dict[str, object]: ...
|
|
|
+
|
|
|
+ def delete(self, *, owner_id: str, memory_id: str) -> dict[str, object]: ...
|
|
|
+
|
|
|
+
|
|
|
+class EmptyAttributionMetrics:
|
|
|
+ def performance(self, admin_user_id: str | None = None) -> dict[str, Any]:
|
|
|
+ del admin_user_id
|
|
|
+ return {
|
|
|
+ "salesperson_count": 0,
|
|
|
+ "visit_count": 0,
|
|
|
+ "lead_count": 0,
|
|
|
+ "order_count": 0,
|
|
|
+ "premium_cents": 0,
|
|
|
+ "items": [],
|
|
|
+ }
|
|
|
+
|
|
|
+ def order_ids(self, admin_user_id: str | None = None) -> set[str]:
|
|
|
+ del admin_user_id
|
|
|
+ return set()
|
|
|
+
|
|
|
+
|
|
|
+class ToolArguments(BaseModel):
|
|
|
+ model_config = ConfigDict(extra="forbid")
|
|
|
+
|
|
|
+
|
|
|
+class ListProductsArgs(ToolArguments):
|
|
|
+ category: Literal["MEDICAL", "ACCIDENT"] | None = None
|
|
|
+
|
|
|
+
|
|
|
+class CalculateQuoteArgs(ToolArguments):
|
|
|
+ product_code: str = Field(min_length=2, max_length=32)
|
|
|
+ plan_code: str = Field(min_length=2, max_length=32)
|
|
|
+ age: int = Field(ge=0, le=120)
|
|
|
+ region_code: str = Field(default="510100", pattern=r"^\d{6}$")
|
|
|
+ occupation_code: str = Field(default="GENERAL", min_length=2, max_length=32)
|
|
|
+ relationship: Literal["SELF", "PARENT", "SPOUSE", "CHILD"]
|
|
|
+
|
|
|
+
|
|
|
+class PrepareEnrollmentArgs(ToolArguments):
|
|
|
+ product_code: str = Field(min_length=2, max_length=32)
|
|
|
+ plan_code: str = Field(min_length=2, max_length=32)
|
|
|
+ age: int = Field(ge=0, le=120)
|
|
|
+ relationship: Literal["SELF", "PARENT", "SPOUSE", "CHILD"]
|
|
|
+
|
|
|
+
|
|
|
+class EmptyArgs(ToolArguments):
|
|
|
+ pass
|
|
|
+
|
|
|
+
|
|
|
+class RecentItemsArgs(ToolArguments):
|
|
|
+ limit: int = Field(default=5, ge=1, le=20)
|
|
|
+
|
|
|
+
|
|
|
+class SearchKnowledgeArgs(ToolArguments):
|
|
|
+ query: str = Field(min_length=2, max_length=500)
|
|
|
+ alternate_queries: list[str] = Field(default_factory=list, max_length=2)
|
|
|
+ product_code: str | None = Field(default=None, min_length=2, max_length=64)
|
|
|
+ document_types: list[
|
|
|
+ Literal["INSURANCE_TERMS", "PRODUCT_GUIDE", "SERVICE_RULES", "FAQ"]
|
|
|
+ ] = Field(default_factory=list, max_length=4)
|
|
|
+ limit: int = Field(default=5, ge=1, le=8)
|
|
|
+
|
|
|
+
|
|
|
+class OperationAnalyticsArgs(ToolArguments):
|
|
|
+ metric: Literal[
|
|
|
+ "ORDER_COUNT",
|
|
|
+ "ORDER_PREMIUM_SUM",
|
|
|
+ "ACTIVE_POLICY_COUNT",
|
|
|
+ "ACTIVE_POLICY_PREMIUM_SUM",
|
|
|
+ ]
|
|
|
+ group_by: Literal["DAY", "PRODUCT"] = "DAY"
|
|
|
+ days: Literal[7, 30, 90] = 30
|
|
|
+
|
|
|
+
|
|
|
+class RememberCustomerArgs(ToolArguments):
|
|
|
+ category: Literal["PREFERENCE", "FAMILY_CONTEXT", "CONSTRAINT"]
|
|
|
+ content: str = Field(min_length=2, max_length=300)
|
|
|
+
|
|
|
+
|
|
|
+class RecallCustomerMemoryArgs(ToolArguments):
|
|
|
+ query: str = Field(min_length=2, max_length=300)
|
|
|
+ limit: int = Field(default=3, ge=1, le=5)
|
|
|
+
|
|
|
+
|
|
|
+class ForgetCustomerMemoryArgs(ToolArguments):
|
|
|
+ memory_id: str = Field(min_length=16, max_length=64)
|
|
|
+
|
|
|
+
|
|
|
+class SearchPartnerInstitutionsArgs(ToolArguments):
|
|
|
+ """合作机构 MCP 查询所需的结构化过滤条件。"""
|
|
|
+
|
|
|
+ city: str = Field(min_length=2, max_length=30)
|
|
|
+ district: str | None = Field(default=None, min_length=2, max_length=30)
|
|
|
+ service_type: str | None = Field(default=None, min_length=2, max_length=40)
|
|
|
+
|
|
|
+
|
|
|
+class UnavailablePartnerResources:
|
|
|
+ def search(
|
|
|
+ self,
|
|
|
+ *,
|
|
|
+ city: str,
|
|
|
+ district: str | None = None,
|
|
|
+ service_type: str | None = None,
|
|
|
+ ) -> PartnerResourceSearchResult:
|
|
|
+ del city, district, service_type
|
|
|
+ return PartnerResourceSearchResult(
|
|
|
+ available=False,
|
|
|
+ source="智保通合作医疗服务资源",
|
|
|
+ items=[],
|
|
|
+ total=0,
|
|
|
+ message="合作医疗服务资源暂时不可用,请稍后重试或联系人工服务。",
|
|
|
+ disclaimer=(
|
|
|
+ "本目录为本地功能验证数据,不构成真实就医推荐;"
|
|
|
+ "实际服务请以机构确认结果为准。"
|
|
|
+ ),
|
|
|
+ )
|
|
|
+
|
|
|
+
|
|
|
+def build_agent_tool_registry(
|
|
|
+ catalog: ProductCatalogService,
|
|
|
+ enrollment: EnrollmentService,
|
|
|
+ attribution: AttributionMetricsProvider | None = None,
|
|
|
+ knowledge: KnowledgeSearchProvider | None = None,
|
|
|
+ analytics: OperationalAnalyticsProvider | None = None,
|
|
|
+ memory: CustomerMemoryProvider | None = None,
|
|
|
+ surrender: SurrenderService | None = None,
|
|
|
+ partner_resources: PartnerResourceGateway | None = None,
|
|
|
+) -> ToolRegistry:
|
|
|
+ """注入第三阶段业务服务并构建客户、运营两类 Agent 的工具集合。"""
|
|
|
+
|
|
|
+ registry = ToolRegistry()
|
|
|
+ metrics = attribution or EmptyAttributionMetrics()
|
|
|
+ resources = partner_resources or UnavailablePartnerResources()
|
|
|
+
|
|
|
+ def list_products(
|
|
|
+ context: ToolExecutionContext,
|
|
|
+ arguments: BaseModel,
|
|
|
+ ) -> HarnessToolResult:
|
|
|
+ del context
|
|
|
+ args = _arguments(arguments, ListProductsArgs)
|
|
|
+ products = catalog.list_available(category=args.category)
|
|
|
+ return HarnessToolResult(
|
|
|
+ summary=f"当前共有{len(products)}款符合条件的在售产品",
|
|
|
+ data={"items": products, "total": len(products)},
|
|
|
+ )
|
|
|
+
|
|
|
+ def calculate_quote(
|
|
|
+ context: ToolExecutionContext,
|
|
|
+ arguments: BaseModel,
|
|
|
+ ) -> HarnessToolResult:
|
|
|
+ args = _arguments(arguments, CalculateQuoteArgs)
|
|
|
+ user = context.h5_user
|
|
|
+ if user is None:
|
|
|
+ raise AppError("AGENT_CUSTOMER_REQUIRED", "保费测算需要H5用户身份", 401)
|
|
|
+ product, plan = _resolve_product_plan(
|
|
|
+ catalog,
|
|
|
+ args.product_code,
|
|
|
+ args.plan_code,
|
|
|
+ )
|
|
|
+ try:
|
|
|
+ quote = enrollment.create_quote(
|
|
|
+ user,
|
|
|
+ product_id=str(product["product_id"]),
|
|
|
+ plan_id=str(plan["id"]),
|
|
|
+ age=args.age,
|
|
|
+ region_code=args.region_code,
|
|
|
+ occupation_code=args.occupation_code,
|
|
|
+ relationship=args.relationship,
|
|
|
+ )
|
|
|
+ except AppError as error:
|
|
|
+ if error.code != "ELIGIBILITY_REJECTED":
|
|
|
+ raise
|
|
|
+ block = QuoteBlock(
|
|
|
+ quote=QuoteView(
|
|
|
+ eligible=False,
|
|
|
+ product_name=str(product["name"]),
|
|
|
+ plan_name=str(plan["name"]),
|
|
|
+ reason=error.message,
|
|
|
+ )
|
|
|
+ )
|
|
|
+ return HarnessToolResult(
|
|
|
+ summary=f"资格校验未通过:{error.message}",
|
|
|
+ data={
|
|
|
+ "eligible": False,
|
|
|
+ "reason_code": (error.details or {}).get("reason_code"),
|
|
|
+ },
|
|
|
+ blocks=[block],
|
|
|
+ )
|
|
|
+ block = QuoteBlock(
|
|
|
+ quote=QuoteView(
|
|
|
+ eligible=True,
|
|
|
+ quote_id=str(quote["quote_id"]),
|
|
|
+ product_id=str(quote["product_id"]),
|
|
|
+ product_name=str(product["name"]),
|
|
|
+ plan_id=str(quote["plan_id"]),
|
|
|
+ plan_name=str(plan["name"]),
|
|
|
+ premium_cents=int(quote["premium_cents"]),
|
|
|
+ currency=str(quote["currency"]),
|
|
|
+ expires_at=str(quote["expires_at"]),
|
|
|
+ )
|
|
|
+ )
|
|
|
+ return HarnessToolResult(
|
|
|
+ summary=(
|
|
|
+ f"{product['name']}{plan['name']}测算成功,"
|
|
|
+ f"年保费{int(quote['premium_cents']) / 100:.2f}元"
|
|
|
+ ),
|
|
|
+ data=quote,
|
|
|
+ blocks=[block],
|
|
|
+ actions=[
|
|
|
+ AgentAction(
|
|
|
+ type="open_enrollment",
|
|
|
+ label="填写投保信息",
|
|
|
+ payload={
|
|
|
+ "product_code": args.product_code,
|
|
|
+ "plan_code": args.plan_code,
|
|
|
+ "quote_id": quote["quote_id"],
|
|
|
+ "age": args.age,
|
|
|
+ "relationship": args.relationship,
|
|
|
+ },
|
|
|
+ )
|
|
|
+ ],
|
|
|
+ )
|
|
|
+
|
|
|
+ def prepare_enrollment(
|
|
|
+ context: ToolExecutionContext,
|
|
|
+ arguments: BaseModel,
|
|
|
+ ) -> HarnessToolResult:
|
|
|
+ if context.h5_user is None:
|
|
|
+ raise AppError("AGENT_CUSTOMER_REQUIRED", "投保引导需要H5用户身份", 401)
|
|
|
+ args = _arguments(arguments, PrepareEnrollmentArgs)
|
|
|
+ product, plan = _resolve_product_plan(
|
|
|
+ catalog,
|
|
|
+ args.product_code,
|
|
|
+ args.plan_code,
|
|
|
+ )
|
|
|
+ action = AgentAction(
|
|
|
+ type="open_enrollment",
|
|
|
+ label="立即投保",
|
|
|
+ payload={
|
|
|
+ "product_id": product["product_id"],
|
|
|
+ "product_code": args.product_code,
|
|
|
+ "plan_id": plan["id"],
|
|
|
+ "plan_code": args.plan_code,
|
|
|
+ "age": args.age,
|
|
|
+ "relationship": args.relationship,
|
|
|
+ },
|
|
|
+ )
|
|
|
+ return HarnessToolResult(
|
|
|
+ summary="已准备安全投保表单入口",
|
|
|
+ data={"product": product, "plan": plan},
|
|
|
+ actions=[action],
|
|
|
+ )
|
|
|
+
|
|
|
+ def list_my_orders(
|
|
|
+ context: ToolExecutionContext,
|
|
|
+ arguments: BaseModel,
|
|
|
+ ) -> HarnessToolResult:
|
|
|
+ del arguments
|
|
|
+ user = context.h5_user
|
|
|
+ if user is None:
|
|
|
+ raise AppError("AGENT_CUSTOMER_REQUIRED", "订单查询需要H5用户身份", 401)
|
|
|
+ result = enrollment.list_orders(user.id)
|
|
|
+ items = [_safe_order_item(item) for item in result["items"]]
|
|
|
+ return HarnessToolResult(
|
|
|
+ summary=f"查到{len(items)}笔本人投保订单",
|
|
|
+ data={"items": items, "total": len(items)},
|
|
|
+ blocks=[
|
|
|
+ BusinessListBlock(
|
|
|
+ title="我的投保订单",
|
|
|
+ entity="order",
|
|
|
+ items=items[:5],
|
|
|
+ total=len(items),
|
|
|
+ )
|
|
|
+ ],
|
|
|
+ actions=[AgentAction(type="open_orders", label="查看我的订单")],
|
|
|
+ )
|
|
|
+
|
|
|
+ def list_my_policies(
|
|
|
+ context: ToolExecutionContext,
|
|
|
+ arguments: BaseModel,
|
|
|
+ ) -> HarnessToolResult:
|
|
|
+ del arguments
|
|
|
+ user = context.h5_user
|
|
|
+ if user is None:
|
|
|
+ raise AppError("AGENT_CUSTOMER_REQUIRED", "保单查询需要H5用户身份", 401)
|
|
|
+ result = enrollment.list_policies(user)
|
|
|
+ items = [_safe_policy_item(item) for item in result["items"]]
|
|
|
+ return HarnessToolResult(
|
|
|
+ summary=f"查到{len(items)}张本人保单",
|
|
|
+ data={"items": items, "total": len(items)},
|
|
|
+ blocks=[
|
|
|
+ BusinessListBlock(
|
|
|
+ title="我的保障",
|
|
|
+ entity="policy",
|
|
|
+ items=items[:5],
|
|
|
+ total=len(items),
|
|
|
+ )
|
|
|
+ ],
|
|
|
+ actions=[AgentAction(type="open_policies", label="查看我的保障")],
|
|
|
+ )
|
|
|
+
|
|
|
+ def list_my_surrenders(
|
|
|
+ context: ToolExecutionContext,
|
|
|
+ arguments: BaseModel,
|
|
|
+ ) -> HarnessToolResult:
|
|
|
+ """查询当前登录客户自己的退保状态,不允许模型指定其他用户。"""
|
|
|
+
|
|
|
+ del arguments
|
|
|
+ user = context.h5_user
|
|
|
+ if user is None:
|
|
|
+ raise AppError("AGENT_CUSTOMER_REQUIRED", "退保查询需要H5用户身份", 401)
|
|
|
+ if surrender is None:
|
|
|
+ raise AppError("SURRENDER_SERVICE_UNAVAILABLE", "退保服务尚未就绪", 503)
|
|
|
+ result = surrender.list_user_requests(user)
|
|
|
+ items = [
|
|
|
+ {
|
|
|
+ "request_no": item["request_no"],
|
|
|
+ "policy_id": item["policy_id"],
|
|
|
+ "reason": item["reason"],
|
|
|
+ "status": item["status"],
|
|
|
+ "refund_amount_cents": item["refund_amount_cents"],
|
|
|
+ "created_at": item["created_at"],
|
|
|
+ }
|
|
|
+ for item in result["items"]
|
|
|
+ ]
|
|
|
+ return HarnessToolResult(
|
|
|
+ summary=f"查到当前客户{len(items)}笔退保申请",
|
|
|
+ data={"items": items, "total": len(items)},
|
|
|
+ blocks=[
|
|
|
+ BusinessListBlock(
|
|
|
+ title="我的退保进度",
|
|
|
+ entity="surrender",
|
|
|
+ items=items[:5],
|
|
|
+ total=len(items),
|
|
|
+ )
|
|
|
+ ],
|
|
|
+ actions=[AgentAction(type="open_surrenders", label="查看退保进度")],
|
|
|
+ )
|
|
|
+
|
|
|
+ def surrender_operations(
|
|
|
+ context: ToolExecutionContext,
|
|
|
+ arguments: BaseModel,
|
|
|
+ ) -> HarnessToolResult:
|
|
|
+ """为运营 Agent 汇总退保数量和异常状态,只读且不执行审批。"""
|
|
|
+
|
|
|
+ del arguments
|
|
|
+ if context.admin_user is None:
|
|
|
+ raise AppError("AGENT_ADMIN_REQUIRED", "退保运营查询需要后台身份", 401)
|
|
|
+ if surrender is None:
|
|
|
+ raise AppError("SURRENDER_SERVICE_UNAVAILABLE", "退保服务尚未就绪", 503)
|
|
|
+ result = surrender.list_admin_requests()
|
|
|
+ source_items = list(result["items"])
|
|
|
+ items = [
|
|
|
+ {
|
|
|
+ "request_no": item["request_no"],
|
|
|
+ "status": item["status"],
|
|
|
+ "refund_amount_cents": item["refund_amount_cents"],
|
|
|
+ "created_at": item["created_at"],
|
|
|
+ }
|
|
|
+ for item in source_items
|
|
|
+ ]
|
|
|
+ waiting = sum(item["status"] == "WAITING_APPROVAL" for item in items)
|
|
|
+ exceptions = sum(
|
|
|
+ item["status"] in {"REFUND_RETRYING", "MANUAL_INTERVENTION"}
|
|
|
+ for item in items
|
|
|
+ )
|
|
|
+ refunded = sum(item["status"] == "REFUNDED" for item in items)
|
|
|
+ return HarnessToolResult(
|
|
|
+ summary=(
|
|
|
+ f"当前退保申请{len(items)}笔,等待审批{waiting}笔,"
|
|
|
+ f"退款异常{exceptions}笔,已完成{refunded}笔"
|
|
|
+ ),
|
|
|
+ data={
|
|
|
+ "total": len(items),
|
|
|
+ "waiting_approval": waiting,
|
|
|
+ "refund_exceptions": exceptions,
|
|
|
+ "refunded": refunded,
|
|
|
+ "items": items[:10],
|
|
|
+ },
|
|
|
+ blocks=[
|
|
|
+ MetricsBlock(
|
|
|
+ title="退保退款运行状态",
|
|
|
+ items=[
|
|
|
+ MetricItem(label="全部申请", value=len(items), unit="笔"),
|
|
|
+ MetricItem(label="等待审批", value=waiting, unit="笔"),
|
|
|
+ MetricItem(label="退款异常", value=exceptions, unit="笔"),
|
|
|
+ MetricItem(label="退款完成", value=refunded, unit="笔"),
|
|
|
+ ],
|
|
|
+ )
|
|
|
+ ],
|
|
|
+ actions=[
|
|
|
+ AgentAction(type="open_admin_surrenders", label="进入退保退款工作台")
|
|
|
+ ],
|
|
|
+ )
|
|
|
+
|
|
|
+ def search_partner_institutions(
|
|
|
+ context: ToolExecutionContext,
|
|
|
+ arguments: BaseModel,
|
|
|
+ ) -> HarnessToolResult:
|
|
|
+ """调用只读 MCP 资源目录,并把结果转换成稳定的业务卡片。"""
|
|
|
+
|
|
|
+ del context
|
|
|
+ args = _arguments(arguments, SearchPartnerInstitutionsArgs)
|
|
|
+ result = resources.search(
|
|
|
+ city=args.city,
|
|
|
+ district=args.district,
|
|
|
+ service_type=args.service_type,
|
|
|
+ )
|
|
|
+ items = [item.model_dump(mode="json") for item in result.items]
|
|
|
+ blocks = (
|
|
|
+ [
|
|
|
+ BusinessListBlock(
|
|
|
+ title="合作医疗服务资源",
|
|
|
+ entity="partner_institution",
|
|
|
+ items=items,
|
|
|
+ total=result.total,
|
|
|
+ )
|
|
|
+ ]
|
|
|
+ if items
|
|
|
+ else []
|
|
|
+ )
|
|
|
+ # MCP 不可用或没有可核验数据时,明确要求模型不得补写机构信息。
|
|
|
+ return HarnessToolResult(
|
|
|
+ summary=(
|
|
|
+ f"{result.message}。{result.disclaimer}"
|
|
|
+ if result.available
|
|
|
+ else (
|
|
|
+ f"{result.message} 当前没有可核验的机构数据,"
|
|
|
+ "不得补写机构名称、地址或联系方式。"
|
|
|
+ )
|
|
|
+ ),
|
|
|
+ data={
|
|
|
+ "available": result.available,
|
|
|
+ "source": result.source,
|
|
|
+ "items": items,
|
|
|
+ "total": result.total,
|
|
|
+ "message": result.message,
|
|
|
+ "disclaimer": result.disclaimer,
|
|
|
+ },
|
|
|
+ blocks=blocks,
|
|
|
+ )
|
|
|
+
|
|
|
+ def overview(
|
|
|
+ context: ToolExecutionContext,
|
|
|
+ arguments: BaseModel,
|
|
|
+ ) -> HarnessToolResult:
|
|
|
+ del arguments
|
|
|
+ orders = enrollment.list_orders()
|
|
|
+ policies = enrollment.list_all_policies()
|
|
|
+ if context.admin_user and context.admin_user.data_scope == "SELF":
|
|
|
+ allowed = metrics.order_ids(context.admin_user.id)
|
|
|
+ orders = _filter_business_result(orders, allowed, "order_id")
|
|
|
+ policies = _filter_business_result(policies, allowed, "order_id")
|
|
|
+ active = [item for item in policies["items"] if item["status"] == "ACTIVE"]
|
|
|
+ premium_cents = sum(int(item["premium_cents"]) for item in active)
|
|
|
+ items = [
|
|
|
+ MetricItem(label="订单总量", value=int(orders["total"]), unit="笔"),
|
|
|
+ MetricItem(label="有效保单", value=len(active), unit="张"),
|
|
|
+ MetricItem(label="累计保费", value=premium_cents / 100, unit="元"),
|
|
|
+ ]
|
|
|
+ return HarnessToolResult(
|
|
|
+ summary=(
|
|
|
+ f"当前订单{orders['total']}笔、有效保单{len(active)}张、"
|
|
|
+ f"累计保费{premium_cents / 100:.2f}元"
|
|
|
+ ),
|
|
|
+ data={
|
|
|
+ "order_count": orders["total"],
|
|
|
+ "active_policy_count": len(active),
|
|
|
+ "premium_cents": premium_cents,
|
|
|
+ },
|
|
|
+ blocks=[MetricsBlock(title="经营总览", items=items)],
|
|
|
+ )
|
|
|
+
|
|
|
+ def recent_orders(
|
|
|
+ context: ToolExecutionContext,
|
|
|
+ arguments: BaseModel,
|
|
|
+ ) -> HarnessToolResult:
|
|
|
+ args = _arguments(arguments, RecentItemsArgs)
|
|
|
+ result = enrollment.list_orders()
|
|
|
+ if context.admin_user and context.admin_user.data_scope == "SELF":
|
|
|
+ result = _filter_business_result(
|
|
|
+ result,
|
|
|
+ metrics.order_ids(context.admin_user.id),
|
|
|
+ "order_id",
|
|
|
+ )
|
|
|
+ items = [_safe_order_item(item) for item in result["items"][: args.limit]]
|
|
|
+ return HarnessToolResult(
|
|
|
+ summary=f"返回最近{len(items)}笔订单",
|
|
|
+ data={"items": items, "total": result["total"]},
|
|
|
+ blocks=[
|
|
|
+ BusinessListBlock(
|
|
|
+ title="近期订单",
|
|
|
+ entity="order",
|
|
|
+ items=items,
|
|
|
+ total=int(result["total"]),
|
|
|
+ )
|
|
|
+ ],
|
|
|
+ actions=[AgentAction(type="open_admin_orders", label="进入订单中心")],
|
|
|
+ )
|
|
|
+
|
|
|
+ def recent_policies(
|
|
|
+ context: ToolExecutionContext,
|
|
|
+ arguments: BaseModel,
|
|
|
+ ) -> HarnessToolResult:
|
|
|
+ args = _arguments(arguments, RecentItemsArgs)
|
|
|
+ result = enrollment.list_all_policies()
|
|
|
+ if context.admin_user and context.admin_user.data_scope == "SELF":
|
|
|
+ result = _filter_business_result(
|
|
|
+ result,
|
|
|
+ metrics.order_ids(context.admin_user.id),
|
|
|
+ "order_id",
|
|
|
+ )
|
|
|
+ items = [_safe_policy_item(item) for item in result["items"][: args.limit]]
|
|
|
+ return HarnessToolResult(
|
|
|
+ summary=f"返回最近{len(items)}张保单",
|
|
|
+ data={"items": items, "total": result["total"]},
|
|
|
+ blocks=[
|
|
|
+ BusinessListBlock(
|
|
|
+ title="近期保单",
|
|
|
+ entity="policy",
|
|
|
+ items=items,
|
|
|
+ total=int(result["total"]),
|
|
|
+ )
|
|
|
+ ],
|
|
|
+ actions=[AgentAction(type="open_admin_policies", label="进入保单中心")],
|
|
|
+ )
|
|
|
+
|
|
|
+ def attribution_performance(
|
|
|
+ context: ToolExecutionContext,
|
|
|
+ arguments: BaseModel,
|
|
|
+ ) -> HarnessToolResult:
|
|
|
+ del arguments
|
|
|
+ admin = context.admin_user
|
|
|
+ if admin is None:
|
|
|
+ raise AppError("AGENT_ADMIN_REQUIRED", "推广分析需要后台身份", 401)
|
|
|
+ owner_id = admin.id if admin.data_scope == "SELF" else None
|
|
|
+ result = metrics.performance(owner_id)
|
|
|
+ block = MetricsBlock(
|
|
|
+ title="推广业绩",
|
|
|
+ items=[
|
|
|
+ MetricItem(
|
|
|
+ label="推广员",
|
|
|
+ value=int(result["salesperson_count"]),
|
|
|
+ unit="人",
|
|
|
+ ),
|
|
|
+ MetricItem(label="访问", value=int(result["visit_count"]), unit="次"),
|
|
|
+ MetricItem(label="线索", value=int(result["lead_count"]), unit="人"),
|
|
|
+ MetricItem(label="归因订单", value=int(result["order_count"]), unit="笔"),
|
|
|
+ MetricItem(
|
|
|
+ label="归因保费",
|
|
|
+ value=int(result["premium_cents"]) / 100,
|
|
|
+ unit="元",
|
|
|
+ ),
|
|
|
+ ],
|
|
|
+ )
|
|
|
+ return HarnessToolResult(
|
|
|
+ summary=(
|
|
|
+ f"推广访问{result['visit_count']}次、线索{result['lead_count']}人、"
|
|
|
+ f"归因订单{result['order_count']}笔"
|
|
|
+ ),
|
|
|
+ data=result,
|
|
|
+ blocks=[block],
|
|
|
+ actions=[AgentAction(type="open_attribution", label="查看推广明细")],
|
|
|
+ )
|
|
|
+
|
|
|
+ def search_insurance_knowledge(
|
|
|
+ context: ToolExecutionContext,
|
|
|
+ arguments: BaseModel,
|
|
|
+ ) -> HarnessToolResult:
|
|
|
+ args = _arguments(arguments, SearchKnowledgeArgs)
|
|
|
+ queries = _normalize_knowledge_queries(args.query, args.alternate_queries)
|
|
|
+ attempt = (
|
|
|
+ sum(
|
|
|
+ execution.name == "search_insurance_knowledge"
|
|
|
+ for execution in context.executions
|
|
|
+ )
|
|
|
+ + 1
|
|
|
+ )
|
|
|
+ if attempt > 3:
|
|
|
+ raise AppError(
|
|
|
+ "AGENTIC_RETRIEVAL_LIMIT_REACHED",
|
|
|
+ "知识检索最多执行3轮,请基于现有证据回答或明确说明无法确定",
|
|
|
+ 409,
|
|
|
+ )
|
|
|
+ retrieval_plan = {
|
|
|
+ "original_query": args.query.strip(),
|
|
|
+ "executed_queries": queries,
|
|
|
+ "filters": {
|
|
|
+ "product_code": args.product_code,
|
|
|
+ "document_types": list(args.document_types),
|
|
|
+ },
|
|
|
+ "attempt": attempt,
|
|
|
+ }
|
|
|
+ if knowledge is None:
|
|
|
+ return HarnessToolResult(
|
|
|
+ summary="没有检索到已发布的可靠知识依据",
|
|
|
+ data={
|
|
|
+ "items": [],
|
|
|
+ "total": 0,
|
|
|
+ "agentic_retrieval": {
|
|
|
+ **retrieval_plan,
|
|
|
+ "next_step": "refine_query",
|
|
|
+ },
|
|
|
+ },
|
|
|
+ )
|
|
|
+ rankings = [
|
|
|
+ knowledge.search(
|
|
|
+ query,
|
|
|
+ limit=args.limit,
|
|
|
+ product_code=args.product_code,
|
|
|
+ document_types=tuple(args.document_types),
|
|
|
+ )["items"]
|
|
|
+ for query in queries
|
|
|
+ ]
|
|
|
+ items = _fuse_knowledge_rankings(rankings, limit=args.limit)
|
|
|
+ result = {"items": items, "total": len(items)}
|
|
|
+ if not items:
|
|
|
+ return HarnessToolResult(
|
|
|
+ summary="没有检索到已发布的可靠知识依据",
|
|
|
+ data={
|
|
|
+ **result,
|
|
|
+ "agentic_retrieval": {
|
|
|
+ **retrieval_plan,
|
|
|
+ "next_step": "refine_query",
|
|
|
+ },
|
|
|
+ },
|
|
|
+ )
|
|
|
+ best = items[0]
|
|
|
+ excerpt = str(best["content"])[:180]
|
|
|
+ evidence_data = {
|
|
|
+ **result,
|
|
|
+ "agentic_retrieval": {
|
|
|
+ **retrieval_plan,
|
|
|
+ "next_step": "evaluate_evidence",
|
|
|
+ },
|
|
|
+ "answer_constraints": {
|
|
|
+ "direct_evidence_only": True,
|
|
|
+ "no_scenario_expansion": True,
|
|
|
+ "no_numeric_inference": True,
|
|
|
+ "unsupported_answer": "当前知识库未明确,无法确定",
|
|
|
+ },
|
|
|
+ }
|
|
|
+ return HarnessToolResult(
|
|
|
+ summary=(
|
|
|
+ f"第{attempt}轮共执行{len(queries)}个检索问题,"
|
|
|
+ f"合并得到{result['total']}条已发布依据。最相关内容:{excerpt}"
|
|
|
+ f"(来源:{best['source_name']},版本v{best['document_version']})。"
|
|
|
+ "请先判断证据是否覆盖问题各子项;不足时缩小范围再次检索。"
|
|
|
+ "回答只能使用返回摘录中的直接事实;不得扩展具体场景或推断未出现的数字。"
|
|
|
+ ),
|
|
|
+ data=evidence_data,
|
|
|
+ blocks=[
|
|
|
+ KnowledgeSourcesBlock(
|
|
|
+ items=[
|
|
|
+ {
|
|
|
+ "document_id": str(item["document_id"]),
|
|
|
+ "document_version": int(item["document_version"]),
|
|
|
+ "title": str(item["title"]),
|
|
|
+ "source_name": str(item["source_name"]),
|
|
|
+ "excerpt": str(item["content"])[:180],
|
|
|
+ "score": float(item["score"]),
|
|
|
+ }
|
|
|
+ for item in items[:5]
|
|
|
+ ]
|
|
|
+ )
|
|
|
+ ],
|
|
|
+ )
|
|
|
+
|
|
|
+ def query_operation_analytics(
|
|
|
+ context: ToolExecutionContext,
|
|
|
+ arguments: BaseModel,
|
|
|
+ ) -> HarnessToolResult:
|
|
|
+ args = _arguments(arguments, OperationAnalyticsArgs)
|
|
|
+ admin = context.admin_user
|
|
|
+ if admin is None:
|
|
|
+ raise AppError("AGENT_ADMIN_REQUIRED", "运营分析需要后台身份", 401)
|
|
|
+ if analytics is None:
|
|
|
+ raise AppError(
|
|
|
+ "ANALYTICS_UNAVAILABLE",
|
|
|
+ "运营分析服务尚未就绪",
|
|
|
+ 503,
|
|
|
+ retryable=True,
|
|
|
+ )
|
|
|
+ result = analytics.query(
|
|
|
+ metric=args.metric,
|
|
|
+ group_by=args.group_by,
|
|
|
+ days=args.days,
|
|
|
+ admin_user_id=admin.id if admin.data_scope == "SELF" else None,
|
|
|
+ )
|
|
|
+ rows = list(result["rows"])
|
|
|
+ return HarnessToolResult(
|
|
|
+ summary=(
|
|
|
+ f"已按{args.days}天、{result['group_by']}维度查询"
|
|
|
+ f"{result['metric_name']},合计{result['total']}{result['unit']}。"
|
|
|
+ "查询由后端白名单语义层编译,只读取分析事实表。"
|
|
|
+ ),
|
|
|
+ data=result,
|
|
|
+ blocks=[
|
|
|
+ ChartBlock(
|
|
|
+ title=f"{args.days}天{result['metric_name']}趋势",
|
|
|
+ chart_type="line" if args.group_by == "DAY" else "bar",
|
|
|
+ unit=str(result["unit"]),
|
|
|
+ points=[
|
|
|
+ {
|
|
|
+ "label": str(item["label"]),
|
|
|
+ "value": float(item["value"]),
|
|
|
+ }
|
|
|
+ for item in rows
|
|
|
+ ],
|
|
|
+ )
|
|
|
+ ],
|
|
|
+ )
|
|
|
+
|
|
|
+ def remember_customer_preference(
|
|
|
+ context: ToolExecutionContext,
|
|
|
+ arguments: BaseModel,
|
|
|
+ ) -> HarnessToolResult:
|
|
|
+ args = _arguments(arguments, RememberCustomerArgs)
|
|
|
+ user = context.h5_user
|
|
|
+ if user is None:
|
|
|
+ raise AppError("AGENT_CUSTOMER_REQUIRED", "长期记忆需要H5用户身份", 401)
|
|
|
+ if memory is None:
|
|
|
+ raise AppError("CUSTOMER_MEMORY_UNAVAILABLE", "长期记忆服务尚未就绪", 503)
|
|
|
+ result = memory.save(
|
|
|
+ owner_id=user.id,
|
|
|
+ category=args.category,
|
|
|
+ content=args.content,
|
|
|
+ )
|
|
|
+ return HarnessToolResult(
|
|
|
+ summary=(
|
|
|
+ f"已按用户明确要求保存长期记忆:{result['content']}。"
|
|
|
+ f"记忆编号:{result['memory_id']}"
|
|
|
+ ),
|
|
|
+ data=dict(result),
|
|
|
+ )
|
|
|
+
|
|
|
+ def recall_customer_memories(
|
|
|
+ context: ToolExecutionContext,
|
|
|
+ arguments: BaseModel,
|
|
|
+ ) -> HarnessToolResult:
|
|
|
+ args = _arguments(arguments, RecallCustomerMemoryArgs)
|
|
|
+ user = context.h5_user
|
|
|
+ if user is None:
|
|
|
+ raise AppError("AGENT_CUSTOMER_REQUIRED", "长期记忆需要H5用户身份", 401)
|
|
|
+ if memory is None:
|
|
|
+ raise AppError("CUSTOMER_MEMORY_UNAVAILABLE", "长期记忆服务尚未就绪", 503)
|
|
|
+ result = memory.search(
|
|
|
+ owner_id=user.id,
|
|
|
+ query=args.query,
|
|
|
+ limit=args.limit,
|
|
|
+ )
|
|
|
+ return HarnessToolResult(
|
|
|
+ summary=(
|
|
|
+ f"检索到{result['total']}条当前用户的长期记忆。"
|
|
|
+ "记忆只用于个性化沟通,不能替代保险条款和业务数据。"
|
|
|
+ ),
|
|
|
+ data=dict(result),
|
|
|
+ )
|
|
|
+
|
|
|
+ def forget_customer_memory(
|
|
|
+ context: ToolExecutionContext,
|
|
|
+ arguments: BaseModel,
|
|
|
+ ) -> HarnessToolResult:
|
|
|
+ args = _arguments(arguments, ForgetCustomerMemoryArgs)
|
|
|
+ user = context.h5_user
|
|
|
+ if user is None:
|
|
|
+ raise AppError("AGENT_CUSTOMER_REQUIRED", "长期记忆需要H5用户身份", 401)
|
|
|
+ if memory is None:
|
|
|
+ raise AppError("CUSTOMER_MEMORY_UNAVAILABLE", "长期记忆服务尚未就绪", 503)
|
|
|
+ result = memory.delete(owner_id=user.id, memory_id=args.memory_id)
|
|
|
+ return HarnessToolResult(
|
|
|
+ summary="已删除指定长期记忆,后续对话不再使用该内容。",
|
|
|
+ data=dict(result),
|
|
|
+ )
|
|
|
+
|
|
|
+ definitions = (
|
|
|
+ ToolDefinition(
|
|
|
+ name="list_available_products",
|
|
|
+ description="查询当前真实在售保险产品和计划;推荐产品前必须调用。",
|
|
|
+ arguments=ListProductsArgs,
|
|
|
+ policy=ToolPolicy(personas=("customer", "operation"), effect="read"),
|
|
|
+ handler=list_products,
|
|
|
+ ),
|
|
|
+ ToolDefinition(
|
|
|
+ name="calculate_insurance_quote",
|
|
|
+ description="按真实产品、年龄、地区、职业和关系执行确定性资格校验与保费测算。",
|
|
|
+ arguments=CalculateQuoteArgs,
|
|
|
+ policy=ToolPolicy(personas=("customer",), effect="draft"),
|
|
|
+ handler=calculate_quote,
|
|
|
+ ),
|
|
|
+ ToolDefinition(
|
|
|
+ name="prepare_enrollment",
|
|
|
+ description="为已明确的产品和计划生成安全投保表单入口,不采集身份证号。",
|
|
|
+ arguments=PrepareEnrollmentArgs,
|
|
|
+ policy=ToolPolicy(personas=("customer",), effect="draft"),
|
|
|
+ handler=prepare_enrollment,
|
|
|
+ ),
|
|
|
+ ToolDefinition(
|
|
|
+ name="list_my_orders",
|
|
|
+ description="查询当前登录客户自己的投保订单。",
|
|
|
+ arguments=EmptyArgs,
|
|
|
+ policy=ToolPolicy(personas=("customer",), effect="read"),
|
|
|
+ handler=list_my_orders,
|
|
|
+ ),
|
|
|
+ ToolDefinition(
|
|
|
+ name="list_my_policies",
|
|
|
+ description="查询当前登录客户自己的电子保单。",
|
|
|
+ arguments=EmptyArgs,
|
|
|
+ policy=ToolPolicy(personas=("customer",), effect="read"),
|
|
|
+ handler=list_my_policies,
|
|
|
+ ),
|
|
|
+ ToolDefinition(
|
|
|
+ name="list_my_surrenders",
|
|
|
+ description="查询当前登录客户自己的退保申请、预计退款和处理进度。",
|
|
|
+ arguments=EmptyArgs,
|
|
|
+ policy=ToolPolicy(personas=("customer",), effect="read"),
|
|
|
+ handler=list_my_surrenders,
|
|
|
+ ),
|
|
|
+ ToolDefinition(
|
|
|
+ name="get_operation_overview",
|
|
|
+ description="查询订单、有效保单和累计保费经营指标。",
|
|
|
+ arguments=EmptyArgs,
|
|
|
+ policy=ToolPolicy(
|
|
|
+ personas=("operation",),
|
|
|
+ effect="read",
|
|
|
+ required_permissions=("dashboard:read",),
|
|
|
+ ),
|
|
|
+ handler=overview,
|
|
|
+ ),
|
|
|
+ ToolDefinition(
|
|
|
+ name="list_recent_orders",
|
|
|
+ description="按当前管理员数据范围查询近期订单,只返回脱敏字段。",
|
|
|
+ arguments=RecentItemsArgs,
|
|
|
+ policy=ToolPolicy(
|
|
|
+ personas=("operation",),
|
|
|
+ effect="read",
|
|
|
+ required_permissions=("order:read",),
|
|
|
+ ),
|
|
|
+ handler=recent_orders,
|
|
|
+ ),
|
|
|
+ ToolDefinition(
|
|
|
+ name="list_recent_policies",
|
|
|
+ description="按当前管理员数据范围查询近期保单,只返回脱敏字段。",
|
|
|
+ arguments=RecentItemsArgs,
|
|
|
+ policy=ToolPolicy(
|
|
|
+ personas=("operation",),
|
|
|
+ effect="read",
|
|
|
+ required_permissions=("policy:read",),
|
|
|
+ ),
|
|
|
+ handler=recent_policies,
|
|
|
+ ),
|
|
|
+ ToolDefinition(
|
|
|
+ name="get_surrender_operations",
|
|
|
+ description="查询退保申请、待审批、退款异常和退款完成数量,只读不执行审批。",
|
|
|
+ arguments=EmptyArgs,
|
|
|
+ policy=ToolPolicy(
|
|
|
+ personas=("operation",),
|
|
|
+ effect="read",
|
|
|
+ required_permissions=("surrender:read",),
|
|
|
+ ),
|
|
|
+ handler=surrender_operations,
|
|
|
+ ),
|
|
|
+ ToolDefinition(
|
|
|
+ name="get_attribution_performance",
|
|
|
+ description="查询当前管理员数据范围内的推广访问、线索、订单和保费。",
|
|
|
+ arguments=EmptyArgs,
|
|
|
+ policy=ToolPolicy(
|
|
|
+ personas=("operation",),
|
|
|
+ effect="read",
|
|
|
+ required_permissions=("attribution:read",),
|
|
|
+ ),
|
|
|
+ handler=attribution_performance,
|
|
|
+ ),
|
|
|
+ ToolDefinition(
|
|
|
+ name="search_partner_institutions",
|
|
|
+ description=(
|
|
|
+ "查询合作医疗服务机构、服务区域和服务时间的唯一入口。"
|
|
|
+ "用户提出此类查询时必须调用;工具通过只读MCP服务返回可核验结果,"
|
|
|
+ "只有工具明确返回不可用或空结果后才能说明没有可核验机构。"
|
|
|
+ ),
|
|
|
+ arguments=SearchPartnerInstitutionsArgs,
|
|
|
+ policy=ToolPolicy(
|
|
|
+ personas=("customer", "operation"),
|
|
|
+ effect="read",
|
|
|
+ ),
|
|
|
+ handler=search_partner_institutions,
|
|
|
+ ),
|
|
|
+ ToolDefinition(
|
|
|
+ name="search_insurance_knowledge",
|
|
|
+ description=(
|
|
|
+ "执行受控Agentic RAG:可提交原问题、最多2个改写问题及产品、文档类型过滤,"
|
|
|
+ "对已发布的保险条款、保障责任、免责范围和服务规则进行多路检索并合并证据。"
|
|
|
+ "调用后需判断证据充分性;不足时可缩小问题再次调用,"
|
|
|
+ "最终只引用直接支持结论的来源和版本。"
|
|
|
+ ),
|
|
|
+ arguments=SearchKnowledgeArgs,
|
|
|
+ policy=ToolPolicy(personas=("customer", "operation"), effect="read"),
|
|
|
+ handler=search_insurance_knowledge,
|
|
|
+ ),
|
|
|
+ ToolDefinition(
|
|
|
+ name="query_operation_analytics",
|
|
|
+ description=(
|
|
|
+ "按白名单指标、时间窗口和维度查询分析库,返回确定性聚合结果和图表。"
|
|
|
+ "需要趋势、分产品对比或一段时间内的经营分析时调用;"
|
|
|
+ "不得生成或提交原始SQL。"
|
|
|
+ ),
|
|
|
+ arguments=OperationAnalyticsArgs,
|
|
|
+ policy=ToolPolicy(
|
|
|
+ personas=("operation",),
|
|
|
+ effect="read",
|
|
|
+ required_permissions=("orders:read", "policies:read"),
|
|
|
+ ),
|
|
|
+ handler=query_operation_analytics,
|
|
|
+ ),
|
|
|
+ ToolDefinition(
|
|
|
+ name="remember_customer_preference",
|
|
|
+ description=(
|
|
|
+ "仅当客户明确说“记住、保存、以后按此偏好”时,"
|
|
|
+ "保存不含手机号、身份证号和银行卡号的偏好或家庭保障上下文。"
|
|
|
+ ),
|
|
|
+ arguments=RememberCustomerArgs,
|
|
|
+ policy=ToolPolicy(personas=("customer",), effect="draft"),
|
|
|
+ handler=remember_customer_preference,
|
|
|
+ ),
|
|
|
+ ToolDefinition(
|
|
|
+ name="recall_customer_memories",
|
|
|
+ description=(
|
|
|
+ "在需要跨会话个性化推荐时,语义检索当前登录客户自己的长期记忆。"
|
|
|
+ "不得把记忆当作条款或业务事实。"
|
|
|
+ ),
|
|
|
+ arguments=RecallCustomerMemoryArgs,
|
|
|
+ policy=ToolPolicy(personas=("customer",), effect="read"),
|
|
|
+ handler=recall_customer_memories,
|
|
|
+ ),
|
|
|
+ ToolDefinition(
|
|
|
+ name="forget_customer_memory",
|
|
|
+ description="按客户明确要求删除当前账号自己的指定长期记忆。",
|
|
|
+ arguments=ForgetCustomerMemoryArgs,
|
|
|
+ policy=ToolPolicy(personas=("customer",), effect="draft"),
|
|
|
+ handler=forget_customer_memory,
|
|
|
+ ),
|
|
|
+ )
|
|
|
+ for definition in definitions:
|
|
|
+ registry.register(definition)
|
|
|
+ return registry
|
|
|
+
|
|
|
+
|
|
|
+def _arguments(value: BaseModel, expected: type[ToolArguments]) -> Any:
|
|
|
+ if not isinstance(value, expected):
|
|
|
+ raise AppError("AGENT_TOOL_ARGUMENTS_INVALID", "工具参数类型错误", 500)
|
|
|
+ return value
|
|
|
+
|
|
|
+
|
|
|
+def _normalize_knowledge_queries(
|
|
|
+ query: str,
|
|
|
+ alternate_queries: list[str],
|
|
|
+) -> list[str]:
|
|
|
+ queries: list[str] = []
|
|
|
+ for candidate in [query, *alternate_queries]:
|
|
|
+ normalized = candidate.strip()
|
|
|
+ if len(normalized) >= 2 and normalized not in queries:
|
|
|
+ queries.append(normalized)
|
|
|
+ return queries[:3]
|
|
|
+
|
|
|
+
|
|
|
+def _fuse_knowledge_rankings(
|
|
|
+ rankings: list[list[dict[str, Any]]],
|
|
|
+ *,
|
|
|
+ limit: int,
|
|
|
+) -> list[dict[str, Any]]:
|
|
|
+ fused_scores: dict[str, float] = {}
|
|
|
+ items_by_chunk: dict[str, dict[str, Any]] = {}
|
|
|
+ for ranking in rankings:
|
|
|
+ for rank, item in enumerate(ranking, start=1):
|
|
|
+ chunk_id = str(item["chunk_id"])
|
|
|
+ items_by_chunk.setdefault(chunk_id, dict(item))
|
|
|
+ fused_scores[chunk_id] = fused_scores.get(chunk_id, 0.0) + (
|
|
|
+ 1.0 / (60 + rank)
|
|
|
+ )
|
|
|
+ ordered_chunk_ids = sorted(
|
|
|
+ fused_scores,
|
|
|
+ key=lambda chunk_id: (-fused_scores[chunk_id], chunk_id),
|
|
|
+ )
|
|
|
+ results: list[dict[str, Any]] = []
|
|
|
+ for chunk_id in ordered_chunk_ids[:limit]:
|
|
|
+ item = dict(items_by_chunk[chunk_id])
|
|
|
+ item["score"] = fused_scores[chunk_id]
|
|
|
+ results.append(item)
|
|
|
+ return results
|
|
|
+
|
|
|
+
|
|
|
+def _resolve_product_plan(
|
|
|
+ catalog: ProductCatalogService,
|
|
|
+ product_code: str,
|
|
|
+ plan_code: str,
|
|
|
+) -> tuple[dict[str, Any], dict[str, Any]]:
|
|
|
+ product = next(
|
|
|
+ (item for item in catalog.list_available() if item["product_code"] == product_code),
|
|
|
+ None,
|
|
|
+ )
|
|
|
+ if product is None:
|
|
|
+ raise AppError("PRODUCT_NOT_AVAILABLE", "产品当前不可投保", 404)
|
|
|
+ plan = next(
|
|
|
+ (item for item in product["plans"] if item["code"] == plan_code),
|
|
|
+ None,
|
|
|
+ )
|
|
|
+ if plan is None:
|
|
|
+ raise AppError("PLAN_NOT_AVAILABLE", "保障计划当前不可投保", 404)
|
|
|
+ return product, plan
|
|
|
+
|
|
|
+
|
|
|
+def _safe_order_item(item: dict[str, Any]) -> dict[str, Any]:
|
|
|
+ return {
|
|
|
+ "order_id": item["order_id"],
|
|
|
+ "order_no": item["order_no"],
|
|
|
+ "product_name": item["product_name"],
|
|
|
+ "plan_name": item["plan_name"],
|
|
|
+ "relationship": item["relationship"],
|
|
|
+ "status": item["status"],
|
|
|
+ "amount_cents": item["amount_cents"],
|
|
|
+ "currency": item["currency"],
|
|
|
+ "policy_no": item["policy_no"],
|
|
|
+ "created_at": str(item["created_at"]),
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+def _safe_policy_item(item: dict[str, Any]) -> dict[str, Any]:
|
|
|
+ return {
|
|
|
+ "policy_id": item["policy_id"],
|
|
|
+ "policy_no": item["policy_no"],
|
|
|
+ "order_no": item["order_no"],
|
|
|
+ "product_name": item["product_name"],
|
|
|
+ "plan_name": item["plan_name"],
|
|
|
+ "relationship": item["relationship"],
|
|
|
+ "status": item["status"],
|
|
|
+ "premium_cents": item["premium_cents"],
|
|
|
+ "currency": item["currency"],
|
|
|
+ "coverage_start": str(item["coverage_start"]),
|
|
|
+ "coverage_end": str(item["coverage_end"]),
|
|
|
+ "issued_at": str(item["issued_at"]),
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+def _filter_business_result(
|
|
|
+ result: dict[str, Any],
|
|
|
+ allowed_ids: set[str],
|
|
|
+ id_field: str,
|
|
|
+) -> dict[str, Any]:
|
|
|
+ items = [item for item in result["items"] if str(item.get(id_field, "")) in allowed_ids]
|
|
|
+ return {"items": items, "total": len(items)}
|