Files
X-Financial/server/src/app/services/account_behavior_profile.py

177 lines
6.6 KiB
Python
Raw Normal View History

from __future__ import annotations
from datetime import UTC, datetime, timedelta
from decimal import Decimal
from typing import Any
from sqlalchemy import select
from sqlalchemy.orm import Session, selectinload
from app.algorithem.employee_behavior_profile import (
LEVEL_LABELS,
PROFILE_LABELS,
ProfileComponent,
evaluate_weighted_profile,
score_by_bands,
)
from app.algorithem.employee_behavior_profile_tags import build_profile_radar, build_profile_tags
from app.models.agent_run import AgentRun
from app.schemas.employee_profile import EmployeeProfileLatestRead, EmployeeProfileRead
from app.services.employee_behavior_profile_helpers import EmployeeBehaviorProfileMetricHelpers
class AccountBehaviorProfileService(EmployeeBehaviorProfileMetricHelpers):
def __init__(self, db: Session) -> None:
self.db = db
def get_latest_account_profile(
self,
*,
account_id: str,
account_name: str,
identifiers: set[str],
scene: str,
window_days: int,
expense_type_scope: str,
) -> EmployeeProfileLatestRead:
if scene != "operations":
return EmployeeProfileLatestRead(
employee_id=account_id,
employee_name=account_name,
scene=scene,
window_days=window_days,
expense_type_scope=expense_type_scope,
empty_reason="当前账号未匹配员工目录,无法形成审批场景员工画像。",
)
runs = self._fetch_account_runs(identifiers, datetime.now(UTC) - timedelta(days=window_days))
if not runs:
return EmployeeProfileLatestRead(
employee_id=account_id,
employee_name=account_name,
scene=scene,
window_days=window_days,
expense_type_scope=expense_type_scope,
empty_reason="当前账号暂无可统计的智能体运行记录。",
)
result = self._calculate_account_ai_usage_profile(
runs=runs,
window_days=window_days,
expense_type_scope=expense_type_scope,
)
payload = {
"profile_type": result.profile_type,
"profile_label": result.profile_label,
"score": result.profile_score,
"level": result.profile_level,
"metrics": result.metrics,
"top_contributors": result.top_contributors(),
}
tags = build_profile_tags([payload], scene=scene)
radar = build_profile_radar([payload], tags, scene=scene)
return EmployeeProfileLatestRead(
employee_id=account_id,
employee_name=account_name,
scene=scene,
window_days=window_days,
expense_type_scope=expense_type_scope,
calculated_at=datetime.now(UTC),
review_priority_score=0,
review_priority_level="normal",
review_priority_label=LEVEL_LABELS["normal"],
profiles=[
EmployeeProfileRead(
profile_type=payload["profile_type"],
profile_label=PROFILE_LABELS.get(payload["profile_type"], payload["profile_type"]),
score=payload["score"],
level=payload["level"],
level_label=LEVEL_LABELS.get(payload["level"], payload["level"]),
metrics=payload["metrics"],
top_contributors=payload["top_contributors"],
)
],
profile_tags=tags,
radar=radar,
)
def _calculate_account_ai_usage_profile(
self,
*,
runs: list[AgentRun],
window_days: int,
expense_type_scope: str,
):
tool_calls = [tool for run in runs for tool in run.tool_calls]
failed_calls = [
tool for tool in tool_calls if str(tool.status or "").lower() not in {"success", "ok"}
]
estimated_tokens = self._estimate_tokens(runs)
duration_ms = self._sum_agent_run_duration_ms(runs)
token_mode = "estimated_token_count" if estimated_tokens else "unavailable"
return evaluate_weighted_profile(
"ai_usage",
[
ProfileComponent(
"ai_call_count_score",
"AI 调用次数",
score_by_bands(len(runs), [(0, 0), (3, 25), (10, 65), (20, 100)]),
len(runs),
"",
Decimal("0.25"),
),
ProfileComponent(
"token_cost_score",
"Token 使用强度",
score_by_bands(
estimated_tokens, [(0, 0), (2000, 25), (8000, 65), (20000, 100)]
),
estimated_tokens,
"tokens",
Decimal("0.25"),
),
ProfileComponent(
"ai_generated_claim_ratio_score",
"AI 生成申请比例",
score_by_bands(len(runs), [(0, 0), (2, 20), (8, 60), (16, 90)]),
len(runs),
"",
Decimal("0.20"),
),
ProfileComponent(
"failed_ai_call_score",
"AI 调用失败",
score_by_bands(len(failed_calls), [(0, 0), (1, 35), (3, 80)]),
len(failed_calls),
"",
Decimal("0.10"),
),
],
metrics={
"window_days": window_days,
"expense_type_scope": expense_type_scope,
"peer_sample_size": 0,
"ai_run_count": len(runs),
"tool_call_count": len(tool_calls),
"failed_tool_call_count": len(failed_calls),
"token_count_mode": token_mode,
"estimated_token_count": estimated_tokens,
"exact_token_count": None,
"ai_run_duration_ms": duration_ms,
"ai_run_duration_mode": "elapsed_or_tool_call_fallback",
},
)
def _fetch_account_runs(self, identifiers: set[str], cutoff: datetime) -> list[AgentRun]:
normalized = {item for item in identifiers if str(item or "").strip()}
if not normalized:
return []
stmt = (
select(AgentRun)
.options(selectinload(AgentRun.tool_calls))
.where(AgentRun.started_at >= cutoff, AgentRun.user_id.in_(normalized))
)
return list(self.db.scalars(stmt).all())