后端新增风险图谱算法模块、风险观察与反馈服务、规则 DSL 校验器和可解释性引擎,完善系统仪表盘和财务仪表盘统计, 优化 agent 运行和编排执行链路,清理旧开发文档,前端新增 系统趋势、负载热力图等多种仪表盘图表组件,完善操作反馈 对话框和工作台日期选择器,优化报销创建和审批详情交互, 补充单元测试覆盖。
179 lines
6.7 KiB
Python
179 lines
6.7 KiB
Python
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())
|