fix: improve import, evidence indexing and material-scoped grading

Preserve document revisions and evidence, harden indexing and export behavior, add explicit material selection and direct-folder import, validate model JSON schemas with safe error messages, and retain student selection while loading scoring data.
This commit is contained in:
2026-10-14 13:52:56 +08:00
parent 29c95ce5ad
commit ebe33392ca
42 changed files with 2332 additions and 304 deletions
+17 -11
View File
@@ -11,7 +11,7 @@ from .actions import prepare, require_scope
from .evidence import CONTENT_ALLOWED, STRUCTURE_ALLOWED
from .exports import STATUS_LABELS, batch_projects, page_type_label, project_result
from .knowledge_pages import page_links
from .models import (Conversation, Course, CourseVersion, IndexTask, KnowledgeChunk, KnowledgeDocument,
from .models import (Conversation, Course, CourseVersion, IndexTask, KnowledgeChunk, KnowledgeDocument, KnowledgePage,
Project, RubricVersion, ScoringBatch, Student, SubmissionFile)
from .pages import toc_entries
from .rag import retrieve
@@ -233,7 +233,8 @@ def _search(db, version_id, project_id, query, *, public_only=False, scopes=None
"source_role": "structure" if "structure" in scopes else "content",
# Stable identity so an answer can cite exactly what it used.
"citation_id": f"chunk:{chunk['chunk_id']}",
**links})
"answer_url": f"{links['url']}#chunk-{chunk['chunk_id']}",
"url": links["url"], "image_url": links["image_url"]})
return {"citations": citations, "found": bool(citations), "scopes": list(scopes)}
@@ -271,9 +272,10 @@ def search_structure(db, convo, args, _user):
"section": chunk.get("section") or "", "block_type": chunk.get("block_type") or "body",
"source_role": "structure",
"citation_id": f"chunk:{chunk['chunk_id']}",
"answer_url": f"{links['url']}#chunk-{chunk['chunk_id']}",
"toc_entries": toc_entries([{"kind": "toc", "text": chunk["text"]}])[:40]
if page_type == "toc" else [],
**links})
"url": links["url"], "image_url": links["image_url"]})
return {"citations": citations, "found": bool(citations), "scopes": ["structure"],
"note": "这些是结构或目录信息,只能说明章节安排,不能证明章节内容。"}
@@ -281,16 +283,18 @@ def search_structure(db, convo, args, _user):
def get_document_pages(db, convo, args, _user):
"""List a document's pages with page type, so a caller can navigate precisely."""
_document(db, convo, args.document_id)
rows = db.scalars(select(KnowledgeChunk).where(KnowledgeChunk.document_id == args.document_id)
.order_by(KnowledgeChunk.page, KnowledgeChunk.id)).all()
rows = db.scalars(select(KnowledgePage).where(KnowledgePage.document_id == args.document_id)
.order_by(KnowledgePage.page)).all()
pages: dict[int, dict] = {}
for chunk in rows:
entry = pages.setdefault(chunk.page, {"page": chunk.page, "page_type": chunk.page_type,
"page_type_label": page_type_label(chunk.page_type),
"chapter": chunk.chapter, "section": chunk.section,
"block_types": []})
if chunk.block_type not in entry["block_types"]:
entry["block_types"].append(chunk.block_type)
for block in (chunk.classification or {}).get("blocks", []):
kind = block.get("kind")
if kind and kind not in entry["block_types"]:
entry["block_types"].append(kind)
if chunk.section:
entry["section"] = chunk.section
return {"document_id": args.document_id, "count": len(pages),
@@ -300,9 +304,9 @@ def get_document_pages(db, convo, args, _user):
def search_document(db, convo, args, _user):
"""Content search restricted to one document, under the same page-type policy."""
document = _document(db, convo, args.document_id)
chunks = retrieve(db, course_version_id=document.course_version_id, project_id=document.project_id or 0,
chunks = retrieve(db, course_version_id=document.course_version_id, project_id=document.project_id,
query=args.query, limit=6, include_public=False,
allowed_page_types=sorted(CONTENT_ALLOWED), substantive_only=True)
allowed_page_types=sorted(CONTENT_ALLOWED), substantive_only=True, document_id=document.id)
citations = []
for chunk in chunks:
if chunk["document_id"] != document.id:
@@ -313,7 +317,9 @@ def search_document(db, convo, args, _user):
"project_id": chunk["project_id"], "page_type": chunk.get("page_type"),
"page_type_label": page_type_label(chunk.get("page_type")),
"section": chunk.get("section") or "", "source_role": "content",
"citation_id": f"chunk:{chunk['chunk_id']}", **links})
"citation_id": f"chunk:{chunk['chunk_id']}",
"answer_url": f"{links['url']}#chunk-{chunk['chunk_id']}",
"url": links["url"], "image_url": links["image_url"]})
return {"citations": citations, "found": bool(citations), "scopes": ["content"]}
@@ -402,7 +408,7 @@ def scores(db, convo, args, _user):
"target_label": str(evidence.get("target_label") or "")[:120],
"historical": bool(evidence.get("historical")),
"source": "historical_score_evidence" if evidence.get("historical") else "score_evidence",
**links, "score_url": url})
"url": links["url"], "image_url": links["image_url"], "score_url": url})
if citations:
# Scoring quotations are never presented as report body evidence in an answer.
for citation in citations: