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="当前账号未匹配员工目录,无法形成审批场景员工画像。", ) cutoff = datetime.now(UTC) - timedelta(days=window_days) runs = self._fetch_account_runs(identifiers, cutoff) usage_duration_metrics = self._resolve_usage_duration_metrics(identifiers, cutoff, runs) if not runs and not usage_duration_metrics["online_duration_ms"]: 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, usage_duration_metrics=usage_duration_metrics, 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], usage_duration_metrics: dict[str, Any], 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) 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, **usage_duration_metrics, }, ) 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())