新增批量人物蒸馏脚本,精简排行榜相关接口
- batch-distill: 独立单文件脚本,按时间窗调用线上服务批量蒸馏人物档案 - 精简 admin_users / public_leaderboard 路由及前端排行榜页面 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
3a04ba5726
commit
01696f75e9
8 files changed
+552
-418
No files matched your search
@@ -20,7 +20,6 @@ import os
|
||||
import re
|
||||
from collections.abc import AsyncIterator
|
||||
from contextvars import ContextVar
|
||||
from difflib import SequenceMatcher
|
||||
from hashlib import sha256
|
||||
from time import perf_counter
|
||||
from typing import Any
|
||||
@@ -269,15 +268,6 @@ class AdminLeaderboardDimension(BaseModel):
|
||||
last_seen_at: str = ""
|
||||
|
||||
|
||||
class AdminLeaderboardQuestion(BaseModel):
|
||||
text: str
|
||||
user_id: str = ""
|
||||
email: str = ""
|
||||
thread_id: str = ""
|
||||
run_id: str = ""
|
||||
asked_at: str = ""
|
||||
|
||||
|
||||
class AdminLeaderboardTrendPoint(BaseModel):
|
||||
date: str
|
||||
runs: int = 0
|
||||
@@ -314,8 +304,6 @@ class AdminLeaderboardResponse(BaseModel):
|
||||
models: list[AdminLeaderboardDimension] = Field(default_factory=list)
|
||||
agents: list[AdminLeaderboardDimension] = Field(default_factory=list)
|
||||
run_statuses: list[AdminLeaderboardDimension] = Field(default_factory=list)
|
||||
recent_questions: list[AdminLeaderboardQuestion] = Field(default_factory=list)
|
||||
frequent_questions: list[AdminLeaderboardQuestion] = Field(default_factory=list)
|
||||
trends: list[AdminLeaderboardTrendPoint] = Field(default_factory=list)
|
||||
|
||||
|
||||
@@ -330,7 +318,6 @@ class AdminUserAnalyticsResponse(BaseModel):
|
||||
models: list[AdminLeaderboardDimension] = Field(default_factory=list)
|
||||
agents: list[AdminLeaderboardDimension] = Field(default_factory=list)
|
||||
run_statuses: list[AdminLeaderboardDimension] = Field(default_factory=list)
|
||||
recent_questions: list[AdminLeaderboardQuestion] = Field(default_factory=list)
|
||||
trends: list[AdminLeaderboardTrendPoint] = Field(default_factory=list)
|
||||
|
||||
|
||||
@@ -1014,129 +1001,6 @@ async def _query_model_rankings(
|
||||
]
|
||||
|
||||
|
||||
async def _query_recent_questions(
|
||||
session,
|
||||
since: datetime,
|
||||
until: datetime,
|
||||
*,
|
||||
user_id: str | None,
|
||||
limit: int,
|
||||
exclude_user_ids: set[str] | None = None,
|
||||
include_scheduled: bool = True,
|
||||
include_admins: bool = True,
|
||||
include_failed: bool = True,
|
||||
cleanup_system_questions: bool = True,
|
||||
) -> list[AdminLeaderboardQuestion]:
|
||||
exclude_user_ids = exclude_user_ids or set()
|
||||
stmt = (
|
||||
select(RunRow.first_human_message, RunRow.user_id, UserRow.email, RunRow.thread_id, RunRow.run_id, RunRow.created_at)
|
||||
.outerjoin(UserRow, UserRow.id == RunRow.user_id)
|
||||
.where(_run_time_filter(since, until), RunRow.first_human_message.is_not(None), RunRow.first_human_message != "")
|
||||
.order_by(RunRow.created_at.desc())
|
||||
.limit(limit)
|
||||
)
|
||||
if user_id:
|
||||
stmt = stmt.where(RunRow.user_id == user_id)
|
||||
stmt = _apply_run_scope(
|
||||
stmt,
|
||||
user_column=RunRow.user_id,
|
||||
run_id_column=RunRow.run_id,
|
||||
exclude_user_ids=exclude_user_ids,
|
||||
include_admins=include_admins,
|
||||
include_failed=include_failed,
|
||||
include_scheduled=include_scheduled,
|
||||
)
|
||||
rows = (await _execute_leaderboard_query(session, "recent_questions", stmt)).all()
|
||||
questions: list[AdminLeaderboardQuestion] = []
|
||||
for r in rows:
|
||||
text = _clean_question_text(r[0]) if cleanup_system_questions else str(r[0] or "").strip()
|
||||
if cleanup_system_questions and not _is_displayable_question(text):
|
||||
continue
|
||||
questions.append(
|
||||
AdminLeaderboardQuestion(
|
||||
text=text[:500],
|
||||
user_id=r[1] or "",
|
||||
email=r[2] or "",
|
||||
thread_id=r[3] or "",
|
||||
run_id=r[4] or "",
|
||||
asked_at=_iso(r[5]),
|
||||
)
|
||||
)
|
||||
return questions
|
||||
|
||||
|
||||
async def _query_frequent_questions(
|
||||
session,
|
||||
since: datetime,
|
||||
until: datetime,
|
||||
limit: int,
|
||||
exclude_user_ids: set[str] | None = None,
|
||||
include_scheduled: bool = True,
|
||||
include_admins: bool = True,
|
||||
include_failed: bool = True,
|
||||
cleanup_system_questions: bool = True,
|
||||
) -> list[AdminLeaderboardQuestion]:
|
||||
exclude_user_ids = exclude_user_ids or set()
|
||||
stmt = (
|
||||
select(
|
||||
RunRow.first_human_message,
|
||||
RunRow.user_id,
|
||||
UserRow.email,
|
||||
RunRow.created_at,
|
||||
)
|
||||
.outerjoin(UserRow, UserRow.id == RunRow.user_id)
|
||||
.where(_run_time_filter(since, until), RunRow.first_human_message.is_not(None), RunRow.first_human_message != "")
|
||||
.order_by(RunRow.created_at.desc())
|
||||
.limit(min(max(limit * 20, 200), 1000))
|
||||
)
|
||||
stmt = _apply_run_scope(
|
||||
stmt,
|
||||
user_column=RunRow.user_id,
|
||||
run_id_column=RunRow.run_id,
|
||||
exclude_user_ids=exclude_user_ids,
|
||||
include_admins=include_admins,
|
||||
include_failed=include_failed,
|
||||
include_scheduled=include_scheduled,
|
||||
)
|
||||
rows = (await _execute_leaderboard_query(session, "frequent_questions", stmt)).all()
|
||||
groups: list[dict] = []
|
||||
for r in rows:
|
||||
text = _clean_question_text(r[0]) if cleanup_system_questions else str(r[0] or "").strip()
|
||||
if cleanup_system_questions and not _is_displayable_question(text):
|
||||
continue
|
||||
key = _question_similarity_key(text)
|
||||
if not key:
|
||||
continue
|
||||
target = None
|
||||
for group in groups:
|
||||
score = SequenceMatcher(None, key, group["key"]).ratio()
|
||||
if score >= 0.72 or key in group["key"] or group["key"] in key:
|
||||
target = group
|
||||
break
|
||||
if target is None:
|
||||
groups.append({"key": key, "text": text, "count": 1, "user_id": r[1] or "", "email": r[2] or "", "asked_at": r[3]})
|
||||
else:
|
||||
target["count"] += 1
|
||||
if r[3] and (target.get("asked_at") is None or r[3] > target["asked_at"]):
|
||||
target.update({"text": text, "user_id": r[1] or "", "email": r[2] or "", "asked_at": r[3]})
|
||||
|
||||
groups.sort(key=lambda item: (item["count"], _iso(item.get("asked_at"))), reverse=True)
|
||||
questions: list[AdminLeaderboardQuestion] = []
|
||||
for group in groups[:limit]:
|
||||
text = group["text"][:460]
|
||||
if group["count"] > 1:
|
||||
text = f"{text}(相似问题 {group['count']} 次)"
|
||||
questions.append(
|
||||
AdminLeaderboardQuestion(
|
||||
text=text,
|
||||
user_id=group["user_id"],
|
||||
email=group["email"],
|
||||
asked_at=_iso(group.get("asked_at")),
|
||||
)
|
||||
)
|
||||
return questions
|
||||
|
||||
|
||||
async def _query_trends(
|
||||
session,
|
||||
since: datetime,
|
||||
@@ -1306,10 +1170,6 @@ async def _build_user_leaderboard_response(
|
||||
"include_scheduled": include_scheduled,
|
||||
"include_admins": include_admins,
|
||||
}
|
||||
question_common = {
|
||||
**common,
|
||||
"cleanup_system_questions": settings.cleanup_system_questions,
|
||||
}
|
||||
overview = await _query_overview(session, since_dt, until_dt, **common)
|
||||
await _leaderboard_pause()
|
||||
users = await _query_user_leaderboard(session, since_dt, until_dt, **common)
|
||||
@@ -1326,10 +1186,6 @@ async def _build_user_leaderboard_response(
|
||||
await _leaderboard_pause()
|
||||
run_statuses = await _query_run_dimensions(session, since_dt, until_dt, user_id=None, field="status", limit=limit, **common)
|
||||
await _leaderboard_pause()
|
||||
recent_questions = await _query_recent_questions(session, since_dt, until_dt, user_id=None, limit=limit, **question_common)
|
||||
await _leaderboard_pause()
|
||||
frequent_questions = await _query_frequent_questions(session, since_dt, until_dt, limit=limit, **question_common)
|
||||
await _leaderboard_pause()
|
||||
trends = await _query_trends(session, since_dt, until_dt, **common)
|
||||
|
||||
return AdminLeaderboardResponse(
|
||||
@@ -1347,8 +1203,6 @@ async def _build_user_leaderboard_response(
|
||||
models=models,
|
||||
agents=agents,
|
||||
run_statuses=run_statuses,
|
||||
recent_questions=recent_questions,
|
||||
frequent_questions=frequent_questions,
|
||||
trends=trends,
|
||||
)
|
||||
|
||||
@@ -1572,95 +1426,6 @@ def _merge_dimensions_from_daily(
|
||||
return sorted(merged_items, key=lambda item: (item.count, _sort_datetime(item.last_seen_at)), reverse=True)[:limit]
|
||||
|
||||
|
||||
def _merge_recent_questions_from_daily(
|
||||
responses: list[AdminLeaderboardResponse],
|
||||
limit: int,
|
||||
) -> list[AdminLeaderboardQuestion]:
|
||||
questions: list[AdminLeaderboardQuestion] = []
|
||||
seen: set[str] = set()
|
||||
for response in responses:
|
||||
for question in response.recent_questions:
|
||||
key = question.run_id or f"{question.user_id}:{question.asked_at}:{question.text}"
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
questions.append(question)
|
||||
questions.sort(key=lambda item: _sort_datetime(item.asked_at), reverse=True)
|
||||
return questions[:limit]
|
||||
|
||||
|
||||
_FREQUENT_QUESTION_COUNT_RE = re.compile(r"(相似问题\s+(\d+)\s+次)$")
|
||||
|
||||
|
||||
def _split_frequent_question_count(text: str) -> tuple[str, int]:
|
||||
match = _FREQUENT_QUESTION_COUNT_RE.search(text)
|
||||
if not match:
|
||||
return text, 1
|
||||
return _FREQUENT_QUESTION_COUNT_RE.sub("", text).strip(), max(1, int(match.group(1)))
|
||||
|
||||
|
||||
def _merge_frequent_questions_from_daily(
|
||||
responses: list[AdminLeaderboardResponse],
|
||||
limit: int,
|
||||
) -> list[AdminLeaderboardQuestion]:
|
||||
groups: list[dict] = []
|
||||
for response in responses:
|
||||
for question in response.frequent_questions:
|
||||
base_text, count = _split_frequent_question_count(question.text)
|
||||
key = _question_similarity_key(base_text)
|
||||
if not key:
|
||||
continue
|
||||
target = None
|
||||
for group in groups:
|
||||
score = SequenceMatcher(None, key, group["key"]).ratio()
|
||||
if score >= 0.72 or key in group["key"] or group["key"] in key:
|
||||
target = group
|
||||
break
|
||||
if target is None:
|
||||
groups.append(
|
||||
{
|
||||
"key": key,
|
||||
"text": base_text,
|
||||
"count": count,
|
||||
"user_id": question.user_id,
|
||||
"email": question.email,
|
||||
"thread_id": question.thread_id,
|
||||
"run_id": question.run_id,
|
||||
"asked_at": question.asked_at,
|
||||
}
|
||||
)
|
||||
else:
|
||||
target["count"] += count
|
||||
if _sort_datetime(question.asked_at) > _sort_datetime(target.get("asked_at")):
|
||||
target.update(
|
||||
{
|
||||
"text": base_text,
|
||||
"user_id": question.user_id,
|
||||
"email": question.email,
|
||||
"thread_id": question.thread_id,
|
||||
"run_id": question.run_id,
|
||||
"asked_at": question.asked_at,
|
||||
}
|
||||
)
|
||||
groups.sort(key=lambda item: (item["count"], _sort_datetime(item.get("asked_at"))), reverse=True)
|
||||
questions: list[AdminLeaderboardQuestion] = []
|
||||
for group in groups[:limit]:
|
||||
text_value = group["text"][:460]
|
||||
if group["count"] > 1:
|
||||
text_value = f"{text_value}(相似问题 {group['count']} 次)"
|
||||
questions.append(
|
||||
AdminLeaderboardQuestion(
|
||||
text=text_value,
|
||||
user_id=group["user_id"],
|
||||
email=group["email"],
|
||||
thread_id=group["thread_id"],
|
||||
run_id=group["run_id"],
|
||||
asked_at=group["asked_at"],
|
||||
)
|
||||
)
|
||||
return questions
|
||||
|
||||
|
||||
def _merge_trends_from_daily(responses: list[AdminLeaderboardResponse]) -> list[AdminLeaderboardTrendPoint]:
|
||||
trends_by_date: dict[str, AdminLeaderboardTrendPoint] = {}
|
||||
for response in responses:
|
||||
@@ -1719,8 +1484,6 @@ def _merge_daily_leaderboard_payloads(
|
||||
models=_merge_dimensions_from_daily(responses, "models", limit),
|
||||
agents=_merge_dimensions_from_daily(responses, "agents", limit),
|
||||
run_statuses=_merge_dimensions_from_daily(responses, "run_statuses", limit),
|
||||
recent_questions=_merge_recent_questions_from_daily(responses, limit),
|
||||
frequent_questions=_merge_frequent_questions_from_daily(responses, limit),
|
||||
trends=_merge_trends_from_daily(responses),
|
||||
)
|
||||
|
||||
@@ -2267,8 +2030,6 @@ async def _build_leaderboard_report_prompt(
|
||||
models = snapshot.models
|
||||
agents = snapshot.agents
|
||||
run_statuses = snapshot.run_statuses
|
||||
recent_questions = snapshot.recent_questions
|
||||
frequent_questions = snapshot.frequent_questions
|
||||
trends = snapshot.trends
|
||||
top_users = snapshot.users_by_activity[:12]
|
||||
context = {
|
||||
@@ -2280,8 +2041,6 @@ async def _build_leaderboard_report_prompt(
|
||||
"top_models": [m.model_dump() for m in models[:12]],
|
||||
"agents": [a.model_dump() for a in agents[:12]],
|
||||
"run_statuses": [s.model_dump() for s in run_statuses[:12]],
|
||||
"frequent_questions": [q.model_dump() for q in frequent_questions[:30]],
|
||||
"recent_questions": [q.model_dump() for q in recent_questions[:40]],
|
||||
"trends": [t.model_dump() for t in trends[-21:]],
|
||||
}
|
||||
return (
|
||||
@@ -2289,12 +2048,10 @@ async def _build_leaderboard_report_prompt(
|
||||
"报告需要有结论,不要只罗列数字;请使用 Markdown 标题、列表和表格,让前端可以直接渲染。"
|
||||
"重点要求:"
|
||||
"1) 你是专门负责分析用户行为的专家,不要自称企业 AI 应用运营分析师;"
|
||||
"2) 优先突出用户最近常用问题、最近关注的实时问题、事件、主题和业务方向,必须引用 recent_questions 中的近期问题作为依据;"
|
||||
"3) 单独分析 frequent_questions,归纳高频关注簇,说明这些高频问题反映出的需求强度和重复频率;"
|
||||
"4) 结合 top_users、top_skills、top_tools、top_models 和 trends,说明使用频率、使用增长、常用能力和重点用户行为画像;"
|
||||
"5) 可以提及使用强度、关注变化和行为机会,但不要把报告重点放在系统本身的问题、故障、失败率或回答质量缺陷上;"
|
||||
"6) 给管理员 3-5 条可执行建议,建议应围绕用户关注主题、资源配置、内容供给、培训引导和高频能力优化。"
|
||||
"输出结构:摘要、最近关注、高频问题与频率、重点用户行为、常用能力、趋势观察、行动建议。"
|
||||
"2) 结合 top_users、top_skills、top_tools、top_models 和 trends,说明使用频率、使用增长、常用能力和重点用户行为画像;"
|
||||
"3) 可以提及使用强度、关注变化和行为机会,但不要把报告重点放在系统本身的问题、故障、失败率或回答质量缺陷上;"
|
||||
"4) 给管理员 3-5 条可执行建议,建议应围绕资源配置、内容供给、培训引导和高频能力优化。"
|
||||
"输出结构:摘要、重点用户行为、常用能力、趋势观察、行动建议。"
|
||||
"\n\n统计数据如下:\n"
|
||||
f"{json.dumps(context, ensure_ascii=False, default=str)}"
|
||||
)
|
||||
@@ -2452,10 +2209,6 @@ async def get_user_analytics(
|
||||
"include_scheduled": settings.include_scheduled,
|
||||
"include_admins": settings.include_admins,
|
||||
}
|
||||
question_common = {
|
||||
**common,
|
||||
"cleanup_system_questions": settings.cleanup_system_questions,
|
||||
}
|
||||
since_dt, until_dt, range_days = _resolve_range(days, since, until)
|
||||
async with session_factory() as session:
|
||||
await _set_analytics_read_timeout(session)
|
||||
@@ -2470,7 +2223,6 @@ async def get_user_analytics(
|
||||
models = await _query_run_dimensions(session, since_dt, until_dt, user_id=user_id, field="model", limit=limit, **common)
|
||||
agents = await _query_run_dimensions(session, since_dt, until_dt, user_id=user_id, field="agent", limit=limit, **common)
|
||||
run_statuses = await _query_run_dimensions(session, since_dt, until_dt, user_id=user_id, field="status", limit=limit, **common)
|
||||
recent_questions = await _query_recent_questions(session, since_dt, until_dt, user_id=user_id, limit=limit, **question_common)
|
||||
trends = await _query_trends(session, since_dt, until_dt, user_id=user_id, **common)
|
||||
|
||||
return AdminUserAnalyticsResponse(
|
||||
@@ -2484,7 +2236,6 @@ async def get_user_analytics(
|
||||
models=models,
|
||||
agents=agents,
|
||||
run_statuses=run_statuses,
|
||||
recent_questions=recent_questions,
|
||||
trends=trends,
|
||||
)
|
||||
|
||||
|
||||
@@ -62,13 +62,6 @@ def _anonymize(resp: AdminLeaderboardResponse) -> AdminLeaderboardResponse:
|
||||
user.email = _mask_email(user.email)
|
||||
user.user_id = hashed(user.user_id)
|
||||
|
||||
for bucket in (resp.recent_questions, resp.frequent_questions):
|
||||
for question in bucket:
|
||||
question.email = _mask_email(question.email)
|
||||
question.user_id = hashed(question.user_id)
|
||||
question.thread_id = ""
|
||||
question.run_id = ""
|
||||
|
||||
return resp
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user