文件
csp/scripts/analyze_learning_note.py
cryptocommuniums-afk cfbe9a0363 feat: problems local stats, user status, admin panel enhancements, rating text
- Problems page: replace Luogu pass rate with local submission stats
  (local_submit_count, local_ac_count)
- Problems page: add user AC/fail status column (user_ac, user_fail_count)
- Admin users: add total_submissions and total_ac columns
- Admin users: add detail panel with submissions/rating/redeem tabs
- Admin: new endpoint GET /api/v1/admin/users/{id}/rating-history
- Rating history: note field includes problem title via JOIN
- Me page: translate task codes to friendly labels with icons
- Me page: problem links in rating history are clickable
- Wrong book service, learning note scoring, note image controller
- Backend SQL uses batch queries for performance

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-02-16 17:35:22 +08:00

152 行
5.0 KiB
Python
可执行文件
原始文件 Blame 文件历史

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
#!/usr/bin/env python3
"""Score a learning note (0-100) and map to rating (1-10) via LLM with fallback."""
from __future__ import annotations
import argparse
import json
import os
import time
from typing import Any, Dict, Optional
import requests
def env(name: str, default: str = "") -> str:
v = os.getenv(name, "").strip()
return v if v else default
def load_input(path: str) -> Dict[str, Any]:
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
if not isinstance(data, dict):
raise ValueError("input json must be object")
return data
def fallback(note: str) -> Dict[str, Any]:
n = note.strip()
score = 40
if len(n) >= 300:
score += 15
if len(n) >= 800:
score += 10
if "```" in n:
score += 15
if "踩坑" in n or "错误" in n or "debug" in n.lower():
score += 10
if "总结" in n or "注意" in n:
score += 10
score = min(100, score)
rating = max(1, min(10, round(score / 10)))
feedback_md = (
"### 笔记评分(规则兜底)\n"
f"- 评分:**{score}/100**,评级:**{rating}/10**\n"
"\n### 你做得好的地方\n"
"- 记录了学习过程(已检测到一定的笔记内容)。\n"
"\n### 建议补充\n"
"- 写清:本节课**学习目标**、**关键概念**、**代码模板**。\n"
"- 至少写 1 个你遇到的坑(如输入输出格式、编译报错)以及解决方案。\n"
"- 最后用 3-5 行做总结,方便复习。\n"
)
return {"score": score, "rating": rating, "feedback_md": feedback_md, "model_name": "fallback-rules"}
def call_llm(payload: Dict[str, Any]) -> Dict[str, Any]:
api_url = env("OI_LLM_API_URL") or env("CSP_LLM_API_URL")
api_key = env("OI_LLM_API_KEY") or env("CSP_LLM_API_KEY")
model = env("OI_LLM_MODEL", "qwen3-max")
if not api_url:
raise RuntimeError("missing OI_LLM_API_URL")
system = (
"你是一位面向小学生的C++竞赛教练,请对学习笔记打分。"
"评分满分100分,并给出10分制评级rating=round(score/10),范围1-10"
"评分维度覆盖度30、正确性30、可操作性20、反思总结20。"
"输出必须是JSON,不要输出其他任何文字。"
)
user = {
"task": "对学习笔记评分并给出改进建议",
"problem": {
"id": payload.get("problem_id"),
"title": payload.get("problem_title"),
},
"note": payload.get("note", ""),
"output_json_schema": {
"score": "integer 0-100",
"rating": "integer 1-10",
"feedback_md": "markdown string",
"model_name": "string",
},
}
headers = {"Content-Type": "application/json"}
if api_key:
headers["Authorization"] = f"Bearer {api_key}"
body = {
"model": model,
"stream": False,
"temperature": 0.2,
"messages": [
{"role": "system", "content": system},
{"role": "user", "content": json.dumps(user, ensure_ascii=False)},
],
}
last: Optional[Exception] = None
for attempt in range(4):
try:
resp = requests.post(api_url, headers=headers, json=body, timeout=50)
if resp.status_code < 500:
resp.raise_for_status()
else:
raise RuntimeError(f"HTTP {resp.status_code}")
data = resp.json()
content = data["choices"][0]["message"]["content"]
parsed = json.loads(content)
if not isinstance(parsed, dict):
raise ValueError("model output not object")
score = int(parsed.get("score", 0))
score = max(0, min(100, score))
rating = int(parsed.get("rating", round(score / 10)))
rating = max(1, min(10, rating))
feedback_md = str(parsed.get("feedback_md", "")).strip() or "### 笔记评分\n- 请补充更多内容(学习目标/代码/总结)。"
model_name = str(parsed.get("model_name", model)).strip() or model
return {"score": score, "rating": rating, "feedback_md": feedback_md, "model_name": model_name}
except Exception as e: # noqa: BLE001
last = e
time.sleep(0.6 * (attempt + 1))
raise RuntimeError(str(last) if last else "llm failed")
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--input-file", required=True)
args = ap.parse_args()
payload = load_input(args.input_file)
note = str(payload.get("note", ""))
if not note.strip():
print(
json.dumps(
{"score": 0, "rating": 1, "feedback_md": "### 笔记为空\n请先写笔记再评分。", "model_name": "validator"},
ensure_ascii=False,
)
)
return 0
try:
out = call_llm(payload)
except Exception:
out = fallback(note)
print(json.dumps(out, ensure_ascii=False))
return 0
if __name__ == "__main__":
raise SystemExit(main())