refactor(server): user_agent/steward/ocr 等服务重构并适配关联任务

- user_agent 拆分 application/locations/knowledge/response/review 四个子模块,接入申请位置语义与关联草稿分支
- steward planner/runtime/slot/plan_builder 决策链路重构,travel_reimbursement_calculator/orchestrator_expense_query 适配
- ocr/document_preview/document_intelligence/receipt_folder 复用预览与资产缓存,expense_claim_draft_flow/application_handoff 适配
- pyproject.toml 新增依赖,paddleocr bootstrap 脚本与 server_start.sh 调整
- 更新差旅/交通/通信等财务规则表,同步 document_intelligence/ocr/receipt_folder/user_agent 等测试
This commit is contained in:
caoxiaozhu
2026-06-24 10:42:24 +08:00
parent 332f77389d
commit 0264a4b5b4
41 changed files with 1273 additions and 182 deletions

View File

@@ -8,20 +8,25 @@ from sqlalchemy import or_, select
from app.api.deps import CurrentUserContext
from app.models.financial_record import ExpenseClaim
from app.schemas.reimbursement import TravelReimbursementCalculatorRequest
from app.schemas.user_agent import (
UserAgentDraftPayload,
UserAgentRequest,
UserAgentResponse,
UserAgentSuggestedAction,
)
from app.schemas.reimbursement import TravelReimbursementCalculatorRequest
from app.services.expense_claim_access_policy import ExpenseClaimAccessPolicy
from app.services.expense_claim_risk_stage import with_risk_business_stage
from app.services.travel_reimbursement_calculator import TravelReimbursementCalculatorService
from app.services.application_location_semantics import (
strip_route_location_prefix_with_jieba,
validate_application_location_text,
)
from app.services.application_system_estimate import apply_application_system_estimate_to_facts
from app.services.document_numbering import (
build_document_number,
generate_unique_expense_claim_no,
)
from app.services.expense_claim_access_policy import ExpenseClaimAccessPolicy
from app.services.expense_claim_risk_stage import with_risk_business_stage
from app.services.travel_reimbursement_calculator import TravelReimbursementCalculatorService
from app.services.user_agent_application_dates import (
expand_application_time_with_days,
resolve_application_date_range,
@@ -33,7 +38,6 @@ from app.services.user_agent_application_summary import (
build_application_summary_table,
resolve_application_time_label,
)
from app.services.application_system_estimate import apply_application_system_estimate_to_facts
APPLICATION_CONTEXT_VALUES = {
"application",
@@ -182,6 +186,17 @@ class UserAgentApplicationSlotMixin:
if not str(facts.get(field) or "").strip()
]
@staticmethod
def _resolve_application_validation_issues(facts: dict[str, str]) -> list[dict[str, str]]:
issues: list[dict[str, str]] = []
location_error = validate_application_location_text(facts.get("location", ""))
if location_error:
issues.append({
"field": "location",
"message": location_error,
})
return issues
def _resolve_application_missing_fields(self, facts: dict[str, str]) -> list[str]:
return [
*self._resolve_application_missing_base_fields(facts),
@@ -391,7 +406,11 @@ class UserAgentApplicationSlotMixin:
if re.fullmatch(r"(?:去|到|前往)?[\u4e00-\u9fa5]{1,8}出差(?P<days>\d+|[一二两三四五六七八九十]{1,3})?天?", text):
return ""
text = re.sub(r"^.*?(?:出差|前往|去|到|赴)[\u4e00-\u9fa5]{1,8}(?:出差)?(?P<days>\d+|[一二两三四五六七八九十]{1,3})?天?[,\s]*", "", text)
tokenized = strip_route_location_prefix_with_jieba(text)
if tokenized != text:
text = tokenized
else:
text = re.sub(r"^.*?(?:出差|前往|去|到|赴)[\u4e00-\u9fa5]{1,8}(?:出差)?(?P<days>\d+|[一二两三四五六七八九十]{1,3})?天?[,\s]*", "", text)
text = re.sub(r"^(?:出差|申请|费用申请|业务|本次|去|到|前往)\s*", "", text)
text = text.strip(" :,。;;")
if not text:
@@ -537,8 +556,16 @@ class UserAgentApplicationSlotMixin:
step: str,
facts: dict[str, str],
) -> list[UserAgentSuggestedAction]:
if step == "ask_missing":
missing_fields = self._resolve_application_missing_fields(facts)
if step in {"ask_missing", "ask_invalid"}:
missing_fields = (
self._resolve_application_missing_fields(facts)
if step == "ask_missing"
else [
issue.get("field", "")
for issue in self._resolve_application_validation_issues(facts)
if issue.get("field")
]
)
return [
UserAgentSuggestedAction(
label="一次性补充申请信息",
@@ -1209,7 +1236,22 @@ class UserAgentApplicationMixin(UserAgentApplicationSlotMixin, UserAgentApplicat
"我已按「费用申请 / 事前审批」来处理这条内容。",
"已识别信息:\n" + recognized_table,
f"当前还需要补充:{missing_text}",
"请一次性补齐上述字段,我会继续生成申请核对结果并让你确认是否提交。",
"请一次性补齐上述字段,我会继续生成申请核对结果,并请您确认是否提交。",
]
)
if step == "ask_invalid":
issue_messages = [
item["message"]
for item in self._resolve_application_validation_issues(facts)
if str(item.get("message") or "").strip()
]
return "\n\n".join(
[
"我已识别到申请信息里有需要先修正的字段。",
"已识别信息:\n" + recognized_table,
*issue_messages,
"请把地点改为真实出差地点,业务事项放在事由中;修正后我再帮您提交申请。",
]
)
@@ -1473,7 +1515,7 @@ class UserAgentApplicationMixin(UserAgentApplicationSlotMixin, UserAgentApplicat
pick("applicationType", "application_type")
),
"time": pick("time", "timeRange", "time_range"),
"location": pick("location"),
"location": normalize_application_location(pick("location")),
"reason": reason,
"days": pick("days"),
"transport_mode": pick("transportMode", "transport_mode"),
@@ -1507,6 +1549,8 @@ class UserAgentApplicationMixin(UserAgentApplicationSlotMixin, UserAgentApplicat
payload: UserAgentRequest,
facts: dict[str, str],
) -> str:
if self._resolve_application_validation_issues(facts):
return "ask_invalid"
if self._is_application_save_draft_action(payload):
return "draft"
if self._resolve_application_missing_base_fields(facts):
@@ -1516,4 +1560,3 @@ class UserAgentApplicationMixin(UserAgentApplicationSlotMixin, UserAgentApplicat
if self._is_application_submit_confirmation(payload):
return "submitted"
return "preview"