- 新增 steward_intent_registry,IntentDescriptor 统一描述意图的识别关键词、动作步骤构建、字段白名单与副作用集合,替代分散的 if/else - 新增 steward_intent_bootstrap 注册 expense_application 等意图;新增 steward_query_executors 提供差旅标准查询的无副作用执行与城市/席别标签化输出 - action_contracts/action_executor/graph_planner/intent_agent/model_plan_builder/planner_extraction/fallback 适配注册表,识别与执行分发自动从注册表取数 - 新增 intent_registry/query_executors 测试,更新 intent_agent 测试
609 lines
24 KiB
Python
609 lines
24 KiB
Python
from __future__ import annotations
|
|
|
|
from datetime import UTC, datetime
|
|
from typing import Any
|
|
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.api.deps import CurrentUserContext
|
|
from app.schemas.ontology import OntologyParseResult, OntologyPermission
|
|
from app.schemas.steward import (
|
|
StewardActionExecuteRequest,
|
|
StewardActionExecuteResponse,
|
|
StewardTask,
|
|
)
|
|
from app.schemas.user_agent import UserAgentRequest
|
|
from app.services.attachment_association_jobs import AttachmentAssociationJobRunner
|
|
from app.services.expense_claims import ExpenseClaimService
|
|
from app.services.steward_intent_registry import (
|
|
all_noop_actions,
|
|
all_side_effect_actions,
|
|
resolve_intent_by_action,
|
|
)
|
|
from app.services.user_agent import UserAgentService
|
|
from app.services.user_agent_application_dates import resolve_application_days_from_time_range
|
|
|
|
SUPPORTED_ACTIONS = {
|
|
"fill_application_fields",
|
|
"build_application_preview",
|
|
"fill_reimbursement_fields",
|
|
"build_reimbursement_preview",
|
|
"validate_required_fields",
|
|
"run_duplicate_precheck",
|
|
"save_application_draft",
|
|
"submit_application",
|
|
"create_reimbursement_draft",
|
|
"link_existing_application",
|
|
"associate_attachments",
|
|
}
|
|
APPLICATION_SIDE_EFFECT_ACTIONS = {"save_application_draft", "submit_application", "run_duplicate_precheck"}
|
|
REIMBURSEMENT_SIDE_EFFECT_ACTIONS = {"create_reimbursement_draft", "link_existing_application", "associate_attachments"}
|
|
NOOP_ACTIONS = {
|
|
"fill_application_fields",
|
|
"build_application_preview",
|
|
"fill_reimbursement_fields",
|
|
"build_reimbursement_preview",
|
|
"validate_required_fields",
|
|
}
|
|
TRANSPORT_MODE_LABELS = {
|
|
"train": "火车",
|
|
"rail": "火车",
|
|
"high_speed_rail": "火车",
|
|
"flight": "飞机",
|
|
"airplane": "飞机",
|
|
"plane": "飞机",
|
|
"ship": "轮船",
|
|
"boat": "轮船",
|
|
"taxi": "出租车",
|
|
}
|
|
EXPENSE_TYPE_LABELS = {
|
|
"travel": "差旅费",
|
|
"transport": "交通费",
|
|
"hotel": "住宿费",
|
|
"meal": "业务招待费",
|
|
"meeting": "会务费",
|
|
"office": "办公用品费",
|
|
"other": "其他费用",
|
|
}
|
|
APPLICATION_TYPE_LABELS = {
|
|
"travel": "差旅费用申请",
|
|
"travel_application": "差旅费用申请",
|
|
"transport": "交通费用申请",
|
|
"hotel": "住宿费用申请",
|
|
"meeting": "会务费用申请",
|
|
"office": "办公费用申请",
|
|
}
|
|
|
|
|
|
class StewardActionExecutor:
|
|
"""执行 LangGraph 规划出的确定性白名单动作。"""
|
|
|
|
def __init__(self, db: Session) -> None:
|
|
self.db = db
|
|
|
|
def execute(
|
|
self,
|
|
request: StewardActionExecuteRequest,
|
|
current_user: CurrentUserContext,
|
|
) -> StewardActionExecuteResponse:
|
|
action_type = self._normalize_action_type(request.action_type)
|
|
trace = [self._trace("received", action_type=action_type, plan_id=request.plan_id)]
|
|
supported = SUPPORTED_ACTIONS | all_side_effect_actions() | all_noop_actions()
|
|
if action_type not in supported:
|
|
return self._blocked(
|
|
action_type,
|
|
f"不支持的小财管家动作:{action_type or '空动作'}。",
|
|
trace=[*trace, self._trace("blocked", reason="unsupported_action")],
|
|
)
|
|
|
|
task = request.task
|
|
noop_actions = NOOP_ACTIONS | all_noop_actions()
|
|
if task is None and action_type not in noop_actions:
|
|
return self._blocked(
|
|
action_type,
|
|
"动作缺少任务快照,无法安全执行。",
|
|
trace=[*trace, self._trace("blocked", reason="missing_task")],
|
|
)
|
|
|
|
blocked_reasons = self._resolve_task_blockers(task)
|
|
if blocked_reasons:
|
|
return self._blocked(
|
|
action_type,
|
|
"当前任务仍有必填信息缺失,已停止执行动作。",
|
|
blocked_reasons=blocked_reasons,
|
|
trace=[*trace, self._trace("blocked", reason="missing_fields")],
|
|
)
|
|
|
|
if action_type in noop_actions:
|
|
return StewardActionExecuteResponse(
|
|
action_type=action_type,
|
|
status="succeeded",
|
|
message="动作已确认,无需调用外部业务服务。",
|
|
result_payload={
|
|
"task_id": task.task_id if task is not None else "",
|
|
"action_type": action_type,
|
|
},
|
|
trace=[*trace, self._trace("completed", mode="noop")],
|
|
)
|
|
|
|
# 优先走注册表:查到 action 所属意图的 executor 即委托执行
|
|
intent = resolve_intent_by_action(action_type)
|
|
if intent is not None and intent.executor is not None:
|
|
return intent.executor(self, request, current_user, trace)
|
|
|
|
# 兼容回退:注册表未命中时按旧逻辑分发
|
|
if action_type == "run_duplicate_precheck":
|
|
return self._run_duplicate_precheck(request, current_user, trace)
|
|
if action_type in APPLICATION_SIDE_EFFECT_ACTIONS:
|
|
return self._execute_application_action(request, current_user, action_type, trace)
|
|
if action_type in REIMBURSEMENT_SIDE_EFFECT_ACTIONS:
|
|
return self._execute_reimbursement_action(request, current_user, action_type, trace)
|
|
if action_type == "associate_attachments":
|
|
return self._execute_associate_attachments_action(request, current_user, trace)
|
|
|
|
return self._blocked(
|
|
action_type,
|
|
f"动作 {action_type} 暂未接入执行器。",
|
|
trace=[*trace, self._trace("blocked", reason="unwired_action")],
|
|
)
|
|
|
|
def _dispatch_application_action(
|
|
self,
|
|
request: StewardActionExecuteRequest,
|
|
current_user: CurrentUserContext,
|
|
trace: list[dict[str, Any]],
|
|
) -> StewardActionExecuteResponse:
|
|
"""registry 入口:分发申请类副作用动作。"""
|
|
action_type = self._normalize_action_type(request.action_type)
|
|
if action_type == "run_duplicate_precheck":
|
|
return self._run_duplicate_precheck(request, current_user, trace)
|
|
return self._execute_application_action(request, current_user, action_type, trace)
|
|
|
|
def _dispatch_reimbursement_action(
|
|
self,
|
|
request: StewardActionExecuteRequest,
|
|
current_user: CurrentUserContext,
|
|
trace: list[dict[str, Any]],
|
|
) -> StewardActionExecuteResponse:
|
|
"""registry 入口:分发报销类副作用动作。"""
|
|
action_type = self._normalize_action_type(request.action_type)
|
|
if action_type == "associate_attachments":
|
|
return self._execute_associate_attachments_action(request, current_user, trace)
|
|
return self._execute_reimbursement_action(request, current_user, action_type, trace)
|
|
|
|
def _run_duplicate_precheck(
|
|
self,
|
|
request: StewardActionExecuteRequest,
|
|
current_user: CurrentUserContext,
|
|
trace: list[dict[str, Any]],
|
|
) -> StewardActionExecuteResponse:
|
|
payload = self._build_application_user_agent_request(
|
|
request,
|
|
current_user,
|
|
action_type="submit_application",
|
|
force_submit_message=False,
|
|
)
|
|
service = UserAgentService(self.db)
|
|
facts = service._resolve_expense_application_facts(payload)
|
|
duplicate = service._find_duplicate_expense_application_record(payload, facts)
|
|
if duplicate is not None:
|
|
result_payload = {
|
|
"status": "blocked",
|
|
"blocking": True,
|
|
"duplicate_claim_id": str(duplicate.id or ""),
|
|
"duplicate_claim_no": str(duplicate.claim_no or ""),
|
|
"duplicate_stage": str(duplicate.approval_stage or ""),
|
|
}
|
|
return StewardActionExecuteResponse(
|
|
action_type="run_duplicate_precheck",
|
|
status="blocked",
|
|
message="检测到同一申请人、同一申请类型、同一时间段已有申请单,已停止直接提交。",
|
|
blocked_reasons=["duplicate_application"],
|
|
result_payload=result_payload,
|
|
trace=[*trace, self._trace("blocked", reason="duplicate_application")],
|
|
)
|
|
result_payload = {"status": "ok", "blocking": False}
|
|
return StewardActionExecuteResponse(
|
|
action_type="run_duplicate_precheck",
|
|
status="succeeded",
|
|
message="未发现重复或冲突申请,可以继续提交。",
|
|
result_payload=result_payload,
|
|
trace=[*trace, self._trace("completed", mode="duplicate_precheck")],
|
|
)
|
|
|
|
def _execute_application_action(
|
|
self,
|
|
request: StewardActionExecuteRequest,
|
|
current_user: CurrentUserContext,
|
|
action_type: str,
|
|
trace: list[dict[str, Any]],
|
|
) -> StewardActionExecuteResponse:
|
|
if action_type == "submit_application" and not request.confirmed:
|
|
return StewardActionExecuteResponse(
|
|
action_type=action_type,
|
|
status="needs_confirmation",
|
|
requires_confirmation=True,
|
|
message="提交申请前需要用户确认。请确认申请核对表无误后再提交。",
|
|
trace=[*trace, self._trace("needs_confirmation")],
|
|
)
|
|
if action_type == "submit_application":
|
|
precheck_blocker = self._resolve_submit_precheck_blocker(request.context_json)
|
|
if precheck_blocker:
|
|
return self._blocked(
|
|
action_type,
|
|
precheck_blocker,
|
|
blocked_reasons=["precheck_not_passed"],
|
|
trace=[*trace, self._trace("blocked", reason="precheck_not_passed")],
|
|
)
|
|
|
|
payload = self._build_application_user_agent_request(
|
|
request,
|
|
current_user,
|
|
action_type=action_type,
|
|
force_submit_message=action_type == "submit_application",
|
|
)
|
|
try:
|
|
user_agent_response = UserAgentService(self.db)._build_expense_application_response(
|
|
payload,
|
|
risk_flags=[],
|
|
)
|
|
except ValueError as exc:
|
|
return self._failed(action_type, str(exc), trace)
|
|
|
|
draft_payload = (
|
|
user_agent_response.draft_payload.model_dump(mode="json")
|
|
if user_agent_response.draft_payload is not None
|
|
else None
|
|
)
|
|
result_payload = {
|
|
"answer": user_agent_response.answer,
|
|
"suggested_actions": [
|
|
action.model_dump(mode="json")
|
|
for action in user_agent_response.suggested_actions
|
|
],
|
|
"requires_confirmation": user_agent_response.requires_confirmation,
|
|
"draft_payload": draft_payload,
|
|
}
|
|
status = "succeeded" if draft_payload is not None else "blocked"
|
|
blocked_reasons = [] if draft_payload is not None else ["application_not_persisted"]
|
|
return StewardActionExecuteResponse(
|
|
action_type=action_type,
|
|
status=status,
|
|
message=user_agent_response.answer,
|
|
blocked_reasons=blocked_reasons,
|
|
result_payload=result_payload,
|
|
trace=[*trace, self._trace("completed", service="UserAgentService")],
|
|
)
|
|
|
|
def _execute_reimbursement_action(
|
|
self,
|
|
request: StewardActionExecuteRequest,
|
|
current_user: CurrentUserContext,
|
|
action_type: str,
|
|
trace: list[dict[str, Any]],
|
|
) -> StewardActionExecuteResponse:
|
|
context_json = self._build_reimbursement_context_json(request, current_user)
|
|
run_id = self._build_run_id(request, action_type)
|
|
ontology = OntologyParseResult(
|
|
scenario="expense",
|
|
intent="draft",
|
|
permission=OntologyPermission(
|
|
level="draft_write",
|
|
allowed=True,
|
|
reason="小财管家白名单动作创建报销草稿。",
|
|
),
|
|
confidence=1.0,
|
|
run_id=run_id,
|
|
)
|
|
try:
|
|
result = ExpenseClaimService(self.db).save_or_submit_from_ontology(
|
|
run_id=run_id,
|
|
user_id=current_user.username,
|
|
message=self._resolve_message(request),
|
|
ontology=ontology,
|
|
context_json=context_json,
|
|
)
|
|
except ValueError as exc:
|
|
return self._failed(action_type, str(exc), trace)
|
|
|
|
persisted = str(result.get("claim_id") or "").strip() and str(result.get("status") or "").strip() == "draft"
|
|
return StewardActionExecuteResponse(
|
|
action_type=action_type,
|
|
status="succeeded" if persisted else "blocked",
|
|
message=str(result.get("message") or "报销草稿已生成。").strip(),
|
|
blocked_reasons=[] if persisted else ["reimbursement_not_persisted"],
|
|
result_payload=result,
|
|
trace=[*trace, self._trace("completed", service="ExpenseClaimService")],
|
|
)
|
|
|
|
def _execute_associate_attachments_action(
|
|
self,
|
|
request: StewardActionExecuteRequest,
|
|
current_user: CurrentUserContext,
|
|
trace: list[dict[str, Any]],
|
|
) -> StewardActionExecuteResponse:
|
|
receipt_ids = self._resolve_receipt_ids(request)
|
|
if not receipt_ids:
|
|
return self._blocked(
|
|
"associate_attachments",
|
|
"关联附件前需要先提供票据夹 receipt_ids。",
|
|
blocked_reasons=["missing_receipt_ids"],
|
|
trace=[*trace, self._trace("blocked", reason="missing_receipt_ids")],
|
|
)
|
|
try:
|
|
result = AttachmentAssociationJobRunner(self.db).run(
|
|
receipt_ids=receipt_ids,
|
|
current_user=current_user,
|
|
)
|
|
except ValueError as exc:
|
|
return self._blocked(
|
|
"associate_attachments",
|
|
str(exc),
|
|
blocked_reasons=["attachment_association_blocked"],
|
|
trace=[*trace, self._trace("blocked", reason="attachment_association_blocked")],
|
|
)
|
|
except Exception as exc:
|
|
return self._failed("associate_attachments", str(exc), trace)
|
|
|
|
return StewardActionExecuteResponse(
|
|
action_type="associate_attachments",
|
|
status="succeeded",
|
|
message=str(
|
|
result.get("message")
|
|
or f"已自动关联到 {result.get('claim_no') or '报销草稿'}。"
|
|
).strip(),
|
|
result_payload={
|
|
**dict(result or {}),
|
|
"receipt_ids": receipt_ids,
|
|
},
|
|
trace=[*trace, self._trace("completed", service="AttachmentAssociationJobRunner")],
|
|
)
|
|
|
|
def _build_application_user_agent_request(
|
|
self,
|
|
request: StewardActionExecuteRequest,
|
|
current_user: CurrentUserContext,
|
|
*,
|
|
action_type: str,
|
|
force_submit_message: bool,
|
|
) -> UserAgentRequest:
|
|
run_id = self._build_run_id(request, action_type)
|
|
context_json = self._build_application_context_json(request, current_user, action_type)
|
|
message = self._resolve_message(request)
|
|
if force_submit_message and "确认提交" not in message and "直接提交" not in message:
|
|
message = "\n".join([message, "确认提交"]).strip()
|
|
ontology = OntologyParseResult(
|
|
scenario="expense",
|
|
intent="operate",
|
|
permission=OntologyPermission(
|
|
level="approval_required",
|
|
allowed=True,
|
|
reason="小财管家白名单动作执行申请操作。",
|
|
),
|
|
confidence=1.0,
|
|
run_id=run_id,
|
|
)
|
|
return UserAgentRequest(
|
|
run_id=run_id,
|
|
user_id=current_user.username,
|
|
message=message,
|
|
ontology=ontology,
|
|
context_json=context_json,
|
|
tool_payload={},
|
|
selected_capability_codes=[],
|
|
degraded=False,
|
|
requires_confirmation=False,
|
|
)
|
|
|
|
def _build_application_context_json(
|
|
self,
|
|
request: StewardActionExecuteRequest,
|
|
current_user: CurrentUserContext,
|
|
action_type: str,
|
|
) -> dict[str, Any]:
|
|
fields = self._resolve_ontology_fields(request.task)
|
|
preview_fields = {
|
|
"applicationType": self._resolve_application_type_label(fields.get("expense_type")),
|
|
"time": str(fields.get("time_range") or ""),
|
|
"location": str(fields.get("location") or ""),
|
|
"reason": str(fields.get("reason") or ""),
|
|
"days": self._resolve_days_text(fields.get("time_range")),
|
|
"transportMode": self._resolve_transport_label(fields.get("transport_mode")),
|
|
"amount": str(fields.get("amount") or ""),
|
|
"applicant": current_user.name,
|
|
"department": current_user.department_name,
|
|
"position": current_user.position,
|
|
"grade": current_user.grade,
|
|
"managerName": current_user.manager_name,
|
|
}
|
|
context_json = {
|
|
**dict(request.context_json or {}),
|
|
"session_type": "application",
|
|
"entry_source": "steward_action_executor",
|
|
"document_type": "expense_application",
|
|
"application_stage": "expense_application",
|
|
"role_codes": current_user.role_codes,
|
|
"is_admin": current_user.is_admin,
|
|
"username": current_user.username,
|
|
"name": current_user.name,
|
|
"department_name": current_user.department_name,
|
|
"position": current_user.position,
|
|
"grade": current_user.grade,
|
|
"employee_no": current_user.employee_no,
|
|
"manager_name": current_user.manager_name,
|
|
"application_preview": {"fields": preview_fields},
|
|
}
|
|
if action_type == "save_application_draft":
|
|
context_json["application_action"] = "save_draft"
|
|
context_json["application_save_mode"] = True
|
|
return context_json
|
|
|
|
def _build_reimbursement_context_json(
|
|
self,
|
|
request: StewardActionExecuteRequest,
|
|
current_user: CurrentUserContext,
|
|
) -> dict[str, Any]:
|
|
fields = self._resolve_ontology_fields(request.task)
|
|
review_form_values = {
|
|
"expense_type": self._resolve_expense_type_label(fields.get("expense_type")),
|
|
"time_range": str(fields.get("time_range") or ""),
|
|
"occurred_date": str(fields.get("time_range") or ""),
|
|
"location": str(fields.get("location") or ""),
|
|
"reason": str(fields.get("reason") or ""),
|
|
"amount": str(fields.get("amount") or ""),
|
|
"transport_mode": self._resolve_transport_label(fields.get("transport_mode")),
|
|
"attachments": str(fields.get("attachments") or ""),
|
|
}
|
|
return {
|
|
**dict(request.context_json or {}),
|
|
"session_type": "expense",
|
|
"entry_source": "steward_action_executor",
|
|
"review_action": "save_draft",
|
|
"review_form_values": review_form_values,
|
|
"user_input_text": self._resolve_message(request),
|
|
"role_codes": current_user.role_codes,
|
|
"is_admin": current_user.is_admin,
|
|
"username": current_user.username,
|
|
"name": current_user.name,
|
|
"department_name": current_user.department_name,
|
|
"position": current_user.position,
|
|
"grade": current_user.grade,
|
|
"employee_no": current_user.employee_no,
|
|
"manager_name": current_user.manager_name,
|
|
}
|
|
|
|
@staticmethod
|
|
def _resolve_receipt_ids(request: StewardActionExecuteRequest) -> list[str]:
|
|
context_json = dict(request.context_json or {})
|
|
raw_values = context_json.get("receipt_ids") or context_json.get("receiptIds") or []
|
|
if isinstance(raw_values, str):
|
|
raw_values = [item.strip() for item in raw_values.split(",")]
|
|
if not isinstance(raw_values, list):
|
|
raw_values = []
|
|
receipt_ids = [
|
|
str(item or "").strip()
|
|
for item in raw_values
|
|
if str(item or "").strip()
|
|
]
|
|
if receipt_ids:
|
|
return list(dict.fromkeys(receipt_ids))
|
|
fields = StewardActionExecutor._resolve_ontology_fields(request.task)
|
|
raw_receipt = str(fields.get("receipt_ids") or fields.get("receiptIds") or "").strip()
|
|
if not raw_receipt:
|
|
return []
|
|
return list(dict.fromkeys(item.strip() for item in raw_receipt.split(",") if item.strip()))
|
|
|
|
@staticmethod
|
|
def _resolve_submit_precheck_blocker(context_json: dict[str, Any]) -> str:
|
|
precheck = context_json.get("precheck_result") or context_json.get("precheckResult")
|
|
if not isinstance(precheck, dict):
|
|
return "提交申请前需要先完成重复/冲突预检查。"
|
|
if bool(precheck.get("blocking")):
|
|
return str(precheck.get("summary") or "重复/冲突预检查未通过,已停止提交。").strip()
|
|
status = str(precheck.get("status") or "").strip().lower()
|
|
if status not in {"ok", "passed", "succeeded"}:
|
|
return "重复/冲突预检查未通过,已停止提交。"
|
|
return ""
|
|
|
|
@staticmethod
|
|
def _resolve_task_blockers(task: StewardTask | None) -> list[str]:
|
|
if task is None:
|
|
return []
|
|
return [
|
|
str(field or "").strip()
|
|
for field in list(task.missing_fields or [])
|
|
if str(field or "").strip()
|
|
]
|
|
|
|
@staticmethod
|
|
def _resolve_ontology_fields(task: StewardTask | None) -> dict[str, str]:
|
|
if task is None or not isinstance(task.ontology_fields, dict):
|
|
return {}
|
|
return {
|
|
str(key or "").strip(): str(value or "").strip()
|
|
for key, value in task.ontology_fields.items()
|
|
if str(key or "").strip() and str(value or "").strip()
|
|
}
|
|
|
|
@staticmethod
|
|
def _resolve_message(request: StewardActionExecuteRequest) -> str:
|
|
message = str(request.message or "").strip()
|
|
if message:
|
|
return message
|
|
if request.task is not None:
|
|
return str(request.task.summary or request.task.title or "").strip()
|
|
return "小财管家动作执行"
|
|
|
|
@staticmethod
|
|
def _resolve_transport_label(value: Any) -> str:
|
|
text = str(value or "").strip()
|
|
return TRANSPORT_MODE_LABELS.get(text.lower(), text)
|
|
|
|
@staticmethod
|
|
def _resolve_application_type_label(value: Any) -> str:
|
|
text = str(value or "").strip()
|
|
return APPLICATION_TYPE_LABELS.get(text.lower(), text or "费用申请")
|
|
|
|
@staticmethod
|
|
def _resolve_expense_type_label(value: Any) -> str:
|
|
text = str(value or "").strip()
|
|
return EXPENSE_TYPE_LABELS.get(text.lower(), text or "其他费用")
|
|
|
|
@staticmethod
|
|
def _resolve_days_text(value: Any) -> str:
|
|
days = resolve_application_days_from_time_range(str(value or ""))
|
|
return f"{days}天" if days else ""
|
|
|
|
@staticmethod
|
|
def _normalize_action_type(value: str) -> str:
|
|
return str(value or "").strip()
|
|
|
|
@staticmethod
|
|
def _build_run_id(request: StewardActionExecuteRequest, action_type: str) -> str:
|
|
trace_id = str(request.client_trace_id or "").strip()
|
|
if trace_id:
|
|
return f"steward-action:{trace_id}"
|
|
task_id = str(request.task.task_id if request.task is not None else "").strip()
|
|
suffix = task_id or datetime.now(UTC).strftime("%Y%m%d%H%M%S%f")
|
|
return f"steward-action:{action_type}:{suffix}"
|
|
|
|
@staticmethod
|
|
def _trace(stage: str, **extra: Any) -> dict[str, Any]:
|
|
return {
|
|
"stage": stage,
|
|
"at": datetime.now(UTC).isoformat(),
|
|
**extra,
|
|
}
|
|
|
|
@staticmethod
|
|
def _blocked(
|
|
action_type: str,
|
|
message: str,
|
|
*,
|
|
blocked_reasons: list[str] | None = None,
|
|
trace: list[dict[str, Any]] | None = None,
|
|
) -> StewardActionExecuteResponse:
|
|
return StewardActionExecuteResponse(
|
|
action_type=action_type,
|
|
status="blocked",
|
|
execution_source="rule_fallback",
|
|
fallback_used=True,
|
|
message=message,
|
|
blocked_reasons=blocked_reasons or [message],
|
|
trace=trace or [],
|
|
)
|
|
|
|
def _failed(
|
|
self,
|
|
action_type: str,
|
|
message: str,
|
|
trace: list[dict[str, Any]],
|
|
) -> StewardActionExecuteResponse:
|
|
return StewardActionExecuteResponse(
|
|
action_type=action_type,
|
|
status="failed",
|
|
message=message or "动作执行失败。",
|
|
blocked_reasons=[message] if message else [],
|
|
trace=[*trace, self._trace("failed", reason=message)],
|
|
)
|