refactor(backend): update and add service layers

- services/ontology.py: update ontology service
- services/orchestrator.py: update orchestrator service
- services/user_agent.py: update user agent service
- services/settings.py: update settings service
- services/expense_claims.py: update expense claims service
- services/agent_conversations.py: add new agent conversations service
This commit is contained in:
caoxiaozhu
2026-05-12 06:36:09 +00:00
parent a6a28ba865
commit 01df3452fd
6 changed files with 1442 additions and 80 deletions

View File

@@ -40,6 +40,7 @@ class ExpenseClaimService:
self._ensure_ready()
claim = self._find_target_claim(ontology=ontology, context_json=context_json)
is_new_claim = claim is None
before_json = self._serialize_claim(claim) if claim is not None else None
employee = self._resolve_employee(ontology=ontology, context_json=context_json)
@@ -47,12 +48,30 @@ class ExpenseClaimService:
occurred_at = self._resolve_occurred_at(ontology)
expense_type = self._resolve_expense_type(ontology.entities)
location = self._resolve_location(message=message, context_json=context_json)
reason = self._resolve_reason(message=message, context_json=context_json)
reason = self._resolve_reason(
message=message,
context_json=context_json,
allow_message_fallback=is_new_claim,
)
attachment_count = self._resolve_attachment_count(context_json)
final_amount = amount if amount is not None else (claim.amount if claim is not None else Decimal("0.00"))
final_occurred_at = (
occurred_at if occurred_at is not None else (claim.occurred_at if claim is not None else datetime.now(UTC))
)
final_expense_type = expense_type or (claim.expense_type if claim is not None else "other")
final_location = location or (claim.location if claim is not None else "待补充")
final_reason = reason or (claim.reason if claim is not None else "待补充")
final_attachment_count = (
attachment_count if attachment_count > 0 else int(claim.invoice_count or 0) if claim is not None else 0
)
final_risk_flags = list(ontology.risk_flags) or (
list(claim.risk_flags_json or []) if claim is not None else []
)
if claim is None:
claim = ExpenseClaim(
claim_no=self._generate_claim_no(occurred_at),
claim_no=self._generate_claim_no(final_occurred_at),
employee_id=employee.id if employee is not None else None,
employee_name=employee.name if employee is not None else self._resolve_employee_name(
ontology=ontology,
@@ -65,16 +84,16 @@ class ExpenseClaimService:
context_json=context_json,
),
project_code=self._resolve_project_code(ontology.entities),
expense_type=expense_type,
reason=reason,
location=location,
amount=amount,
expense_type=final_expense_type,
reason=final_reason,
location=final_location,
amount=final_amount,
currency="CNY",
invoice_count=attachment_count,
occurred_at=occurred_at,
invoice_count=final_attachment_count,
occurred_at=final_occurred_at,
status="draft",
approval_stage="待补充",
risk_flags_json=list(ontology.risk_flags),
risk_flags_json=final_risk_flags,
)
self.db.add(claim)
else:
@@ -86,6 +105,7 @@ class ExpenseClaimService:
ontology=ontology,
context_json=context_json,
user_id=user_id,
fallback=claim.employee_name,
)
)
claim.department_id = employee.organization_unit_id if employee is not None else claim.department_id
@@ -95,24 +115,24 @@ class ExpenseClaimService:
fallback=claim.department_name,
)
claim.project_code = self._resolve_project_code(ontology.entities) or claim.project_code
claim.expense_type = expense_type or claim.expense_type
claim.reason = reason
claim.location = location
claim.amount = amount
claim.invoice_count = attachment_count
claim.occurred_at = occurred_at
claim.expense_type = final_expense_type
claim.reason = final_reason
claim.location = final_location
claim.amount = final_amount
claim.invoice_count = final_attachment_count
claim.occurred_at = final_occurred_at
claim.status = "draft"
claim.approval_stage = "待补充"
claim.risk_flags_json = list(ontology.risk_flags)
claim.risk_flags_json = final_risk_flags
self.db.flush()
self._upsert_primary_item(
claim=claim,
occurred_at=occurred_at,
expense_type=expense_type,
amount=amount,
reason=reason,
location=location,
occurred_at=final_occurred_at,
expense_type=final_expense_type,
amount=final_amount,
reason=final_reason,
location=final_location,
attachment_names=self._resolve_attachment_names(context_json),
)
self.db.commit()
@@ -130,7 +150,7 @@ class ExpenseClaimService:
return {
"message": (
f"创建报销草稿 {claim.claim_no},当前状态为 draft。"
f"{'创建' if is_new_claim else '更新'}报销草稿 {claim.claim_no},当前状态为 draft。"
"你可以继续补充费用明细、客户单位和票据附件。"
),
"draft_only": True,
@@ -229,6 +249,7 @@ class ExpenseClaimService:
ontology: OntologyParseResult,
context_json: dict[str, Any],
user_id: str | None,
fallback: str = "待补充",
) -> str:
for item in ontology.entities:
if item.type == "employee" and item.value.strip():
@@ -237,7 +258,7 @@ class ExpenseClaimService:
value = str(context_json.get(key) or "").strip()
if value:
return value
return str(user_id or "待补充").strip() or "待补充"
return str(user_id or fallback).strip() or fallback
@staticmethod
def _resolve_department_name(
@@ -270,26 +291,33 @@ class ExpenseClaimService:
return None
@staticmethod
def _resolve_expense_type(entities: list[OntologyEntity]) -> str:
def _resolve_expense_type(entities: list[OntologyEntity]) -> str | None:
for item in entities:
if item.type == "expense_type":
normalized = item.normalized_value.strip()
if normalized:
return normalized
return "other"
return None
@staticmethod
def _resolve_reason(*, message: str, context_json: dict[str, Any]) -> str:
def _resolve_reason(
*,
message: str,
context_json: dict[str, Any],
allow_message_fallback: bool,
) -> str | None:
request_context = context_json.get("request_context")
if isinstance(request_context, dict):
for key in ("reason", "title"):
value = str(request_context.get(key) or "").strip()
if value:
return value
return str(message or "").strip()[:500] or "待补充"
if not allow_message_fallback:
return None
return str(message or "").strip()[:500] or None
@staticmethod
def _resolve_location(*, message: str, context_json: dict[str, Any]) -> str:
def _resolve_location(*, message: str, context_json: dict[str, Any]) -> str | None:
request_context = context_json.get("request_context")
if isinstance(request_context, dict):
for key in ("city", "location"):
@@ -299,10 +327,10 @@ class ExpenseClaimService:
compact = str(message or "").replace(" ", "")
if "客户现场" in compact:
return "客户现场"
return "待补充"
return None
@staticmethod
def _resolve_occurred_at(ontology: OntologyParseResult) -> datetime:
def _resolve_occurred_at(ontology: OntologyParseResult) -> datetime | None:
start_date = ontology.time_range.start_date
if start_date:
try:
@@ -310,10 +338,10 @@ class ExpenseClaimService:
return datetime(parsed.year, parsed.month, parsed.day, tzinfo=UTC)
except ValueError:
pass
return datetime.now(UTC)
return None
@staticmethod
def _resolve_amount(entities: list[OntologyEntity]) -> Decimal:
def _resolve_amount(entities: list[OntologyEntity]) -> Decimal | None:
for item in entities:
if item.type != "amount" or item.role == "threshold":
continue
@@ -321,7 +349,7 @@ class ExpenseClaimService:
return Decimal(item.normalized_value).quantize(Decimal("0.01"))
except (InvalidOperation, ValueError):
continue
return Decimal("0.00")
return None
@staticmethod
def _resolve_attachment_names(context_json: dict[str, Any]) -> list[str]: