fix: restore chat citations and tool details
This commit is contained in:
@@ -64,11 +64,16 @@ class BatchId(Args):
|
||||
batch_id: int = Field(gt=0)
|
||||
|
||||
|
||||
class ScoresArgs(BatchId):
|
||||
class ScoresFilterArgs(BatchId):
|
||||
project_id: int | None = Field(default=None, gt=0)
|
||||
status: Literal["pending", "scored", "reviewed", "failed", "unscorable"] | None = None
|
||||
|
||||
|
||||
class ScoresArgs(ScoresFilterArgs):
|
||||
project_offset: int = Field(default=0, ge=0)
|
||||
criterion_offset: int = Field(default=0, ge=0)
|
||||
|
||||
|
||||
class CreateBatch(VersionId):
|
||||
rubric_version_id: int = Field(gt=0)
|
||||
|
||||
@@ -254,30 +259,39 @@ def scores(db, convo, args, _user):
|
||||
if convo.project_id:
|
||||
selected = [p for p in selected if p.id == convo.project_id]
|
||||
results = []
|
||||
citations = []
|
||||
for item in selected:
|
||||
result = project_result(db, batch, item)
|
||||
if args.status:
|
||||
result["rows"] = [row for row in result["rows"] if row["status"] == args.status]
|
||||
if result["rows"]:
|
||||
result["url"] = f"/?page=scores&batch_id={batch.id}&project_id={item.id}"
|
||||
results.append({"project_id": item.id, "student_no": result["student_no"], "name": result["name"],
|
||||
"totals": result["totals"], "url": result["url"],
|
||||
"rows": [{"criterion_id": row["criterion"]["criterion_id"],
|
||||
"criterion_name": row["criterion"]["name"],
|
||||
"max_score": row["criterion"]["max_score"], "score_id": row["score_id"],
|
||||
"model_score": row["model_score"], "teacher_score": row["teacher_score"],
|
||||
"final_score": row["final_score"], "status": row["status"],
|
||||
"error": row["error"], "evidence": [{"page": ev["page"],
|
||||
"quote": ev["quote"][:240], "document": ev["document"]}
|
||||
for ev in row["evidence"][:2]]} for row in result["rows"]]})
|
||||
for row in result["rows"]:
|
||||
for evidence in row["evidence"][:2]:
|
||||
citations.append({"title": evidence["document"], "page": evidence["page"],
|
||||
"quote": evidence["quote"][:500],
|
||||
"url": evidence["image_url"] or result["url"]})
|
||||
results.append((item, result))
|
||||
page = results[args.project_offset:args.project_offset + 1]
|
||||
output = []
|
||||
citations = []
|
||||
for item, result in page:
|
||||
url = f"/?page=scores&batch_id={batch.id}&project_id={item.id}"
|
||||
rows = result["rows"][args.criterion_offset:args.criterion_offset + 6]
|
||||
output.append({"project_id": item.id, "student_no": str(result["student_no"])[:50], "name": str(result["name"])[:100],
|
||||
"totals": result["totals"], "url": url, "total_rows": len(result["rows"]),
|
||||
"rows": [{"criterion_id": str(row["criterion"]["criterion_id"])[:100],
|
||||
"criterion_name": str(row["criterion"]["name"])[:150],
|
||||
"max_score": row["criterion"]["max_score"], "score_id": row["score_id"],
|
||||
"model_score": row["model_score"], "teacher_score": row["teacher_score"],
|
||||
"final_score": row["final_score"], "status": row["status"],
|
||||
"error": str(row["error"] or "")[:300], "evidence": [{"page": ev["page"],
|
||||
"quote": ev["quote"][:160], "document": str(ev["document"])[:200]}
|
||||
for ev in row["evidence"][:2]]} for row in rows]})
|
||||
for row in rows:
|
||||
for evidence in row["evidence"][:2]:
|
||||
citations.append({"title": str(evidence["document"])[:200], "page": evidence["page"],
|
||||
"quote": evidence["quote"][:320],
|
||||
"url": evidence["image_url"] or url})
|
||||
next_criterion_offset = args.criterion_offset + 6 if page and args.criterion_offset + 6 < len(page[0][1]["rows"]) else None
|
||||
next_project_offset = args.project_offset + 1 if args.project_offset + 1 < len(results) else None
|
||||
return {"batch_id": batch.id, "status_filter": args.status, "count": len(results),
|
||||
"results": results[:2], "citations": citations[:12], "truncated": len(results) > 2}
|
||||
"project_offset": args.project_offset, "criterion_offset": args.criterion_offset,
|
||||
"next_project_offset": next_project_offset, "next_criterion_offset": next_criterion_offset,
|
||||
"results": output, "citations": citations}
|
||||
|
||||
|
||||
def summary(db, convo, args, _user):
|
||||
@@ -411,8 +425,8 @@ TOOLS: dict[str, Tool] = {
|
||||
"search_project_material": Tool("只检索指定学生项目报告,返回原文片段和引用。", SearchProject, search_project),
|
||||
"list_batches": Tool("列出可访问的评分批次。", Empty, batch_list),
|
||||
"get_batch_progress": Tool("查询评分批次进度、状态和固定项目范围。", BatchId, progress),
|
||||
"get_scores": Tool("查询单学生或单批次评分详情,可按评分项状态筛选。", ScoresArgs, scores),
|
||||
"summarize_batch": Tool("用数据库计算批次或单学生完成数、未完成数和最终分布,可按评分项状态筛选,不补零。", ScoresArgs, summary),
|
||||
"get_scores": Tool("分页查询单学生或单批次评分详情,可按状态筛选。每次返回一个项目、最多六项评分;如需其余评分项,用 next_criterion_offset 作为 criterion_offset 再查;如需下个项目,用 next_project_offset 作为 project_offset,并将 criterion_offset 设为 0。", ScoresArgs, scores),
|
||||
"summarize_batch": Tool("用数据库计算批次或单学生完成数、未完成数和最终分布,可按评分项状态筛选,不补零。", ScoresFilterArgs, summary),
|
||||
"list_index_tasks": Tool("查询课程版本的材料解析与索引任务进度。", VersionId, index_tasks),
|
||||
"prepare_import": Tool("从只读扫描结果准备导入确认卡片;每页最多20个项目,文件有歧义时要求教师在卡片中修正。", PrepareImport, prepare_import),
|
||||
"prepare_course_ingest": Tool("准备课程资料入库确认卡片。", VersionId, prepare_kind("course_ingest")),
|
||||
|
||||
@@ -194,16 +194,70 @@ def _result_summary(result):
|
||||
return f"待确认操作 #{action.get('id', '—')}" if isinstance(action, dict) else "待确认操作已准备"
|
||||
if "found" in result:
|
||||
return f"找到 {len(result.get('citations') or [])} 条引用" if result["found"] else "未找到相关资料"
|
||||
if isinstance(result.get("results"), list) and "count" in result:
|
||||
rows = sum(len(item.get("rows") or []) for item in result["results"] if isinstance(item, dict))
|
||||
return f"返回 {rows} 项评分(共 {result['count']} 个项目)"
|
||||
for key in ("courses", "versions", "projects", "scores", "batches", "items"):
|
||||
if isinstance(result.get(key), list):
|
||||
return f"返回 {len(result[key])} 条{key}记录"
|
||||
return "工具已返回结果"
|
||||
|
||||
|
||||
def _safe_result(result):
|
||||
"""Persist a small structural preview; original student text stays in citations or business records."""
|
||||
def _safe_citation(item):
|
||||
if not isinstance(item, dict):
|
||||
return None
|
||||
return {key: value[:limit] if isinstance(value, str) else value for key, limit in
|
||||
(("title", 200), ("quote", 500), ("url", 500), ("image_url", 500))
|
||||
if isinstance((value := item.get(key)), str)} | {
|
||||
key: item[key] for key in ("chunk_id", "document_id", "page", "project_id")
|
||||
if isinstance(item.get(key), int)}
|
||||
|
||||
|
||||
def _answer_citations(items):
|
||||
"""Keep each distinct source passage, including later tool calls and pages cited by the answer."""
|
||||
seen = set()
|
||||
citations = []
|
||||
for item in items:
|
||||
safe = _safe_citation(item)
|
||||
if not safe:
|
||||
continue
|
||||
key = (safe.get("document_id"), safe.get("chunk_id"), safe.get("title"),
|
||||
safe.get("page"), safe.get("quote"), safe.get("url"))
|
||||
if key not in seen:
|
||||
seen.add(key)
|
||||
citations.append(safe)
|
||||
return citations
|
||||
|
||||
|
||||
def _safe_result(result, name=""):
|
||||
"""Persist bounded useful details without credentials or unrelated source text."""
|
||||
if not isinstance(result, dict):
|
||||
return {"summary": "工具已返回结果"}
|
||||
if name in {"search_course_material", "search_project_material"}:
|
||||
return {"found": bool(result.get("found")),
|
||||
"citations": [citation for item in (result.get("citations") or [])[:6]
|
||||
if (citation := _safe_citation(item))]}
|
||||
if name == "get_scores":
|
||||
preview = {key: result.get(key) for key in ("batch_id", "status_filter", "count", "project_offset",
|
||||
"criterion_offset", "next_project_offset", "next_criterion_offset")}
|
||||
preview["results"] = []
|
||||
for project in (result.get("results") or [])[:1]:
|
||||
if not isinstance(project, dict):
|
||||
continue
|
||||
row = {key: project.get(key) for key in ("project_id", "student_no", "name", "totals", "url", "total_rows")}
|
||||
row["rows"] = []
|
||||
for score in (project.get("rows") or [])[:6]:
|
||||
if not isinstance(score, dict):
|
||||
continue
|
||||
entry = {key: score.get(key) for key in ("criterion_id", "criterion_name", "max_score", "score_id",
|
||||
"model_score", "teacher_score", "final_score", "status")}
|
||||
entry["error"] = str(score.get("error") or "")[:300]
|
||||
entry["evidence"] = [{"document": str(ev.get("document") or "")[:200],
|
||||
"page": ev.get("page"), "quote": str(ev.get("quote") or "")[:160]}
|
||||
for ev in (score.get("evidence") or [])[:2] if isinstance(ev, dict)]
|
||||
row["rows"].append(entry)
|
||||
preview["results"].append(row)
|
||||
return preview
|
||||
allowed_text = {"status", "source", "format", "kind", "category"}
|
||||
allowed_number = {"id", "course_version_id", "batch_id", "project_id", "score_id",
|
||||
"task_id", "page", "count", "total", "progress_done", "progress_total"}
|
||||
@@ -258,11 +312,12 @@ def _run_agent(factory, conversation_id, run_id, user_text):
|
||||
content = reply.get("content")
|
||||
if not isinstance(content, str) or not content.strip():
|
||||
raise ModelError("会话模型未返回文字或工具调用")
|
||||
answer_citations = _answer_citations(citations)
|
||||
db.add(ChatMessage(conversation_id=conversation_id, role="assistant", content=content,
|
||||
citations=citations[:20], tool_calls=used))
|
||||
citations=answer_citations, tool_calls=used))
|
||||
run.status, run.finished_at = "completed", now()
|
||||
conversation.updated_at = now()
|
||||
answer = {"content": content, "citations": citations[:20], "tool_calls": used}
|
||||
answer = {"content": content, "citations": answer_citations, "tool_calls": used}
|
||||
done = {"run_id": run.id}
|
||||
run.events = [*(run.events or []), {"event": "message", "data": answer},
|
||||
{"event": "done", "data": done}][-140:]
|
||||
@@ -293,12 +348,12 @@ def _run_agent(factory, conversation_id, run_id, user_text):
|
||||
if isinstance(result, dict):
|
||||
citations.extend(result.get("citations", []))
|
||||
output = json.dumps(result, ensure_ascii=False, default=str)
|
||||
truncated = len(output) > 12000
|
||||
if truncated:
|
||||
output = json.dumps({"truncated": True, "message": "结果过长,请缩小查询范围"}, ensure_ascii=False)
|
||||
if len(output) > 12000:
|
||||
output = json.dumps({"notice": "结果已按结构压缩;可使用工具分页参数继续查询",
|
||||
**_safe_result(result, name)}, ensure_ascii=False, default=str)
|
||||
messages.append({"role": "tool", "tool_call_id": call_id, "content": output})
|
||||
entry = {**entry, "status": "completed", "summary": "结果过长,已截断" if truncated else _result_summary(result),
|
||||
"result": {"truncated": True} if truncated else _safe_result(result)}
|
||||
entry = {**entry, "status": "completed", "summary": _result_summary(result),
|
||||
"result": _safe_result(result, name)}
|
||||
except Exception as exc:
|
||||
# Tool errors are model-visible, but do not grant broader access or leak provider responses.
|
||||
db.rollback()
|
||||
|
||||
Reference in New Issue
Block a user