feat: add persistent chat agent and indexed batch workflows

This commit is contained in:
2026-09-26 19:43:23 +08:00
parent 8bebcdb470
commit d61987c86c
17 changed files with 1758 additions and 180 deletions
+30 -8
View File
@@ -1,4 +1,6 @@
from concurrent.futures import ThreadPoolExecutor, as_completed
from contextlib import contextmanager
import fcntl
from sqlalchemy import select, update
@@ -13,7 +15,25 @@ from .storage import secure_path, sha256
from .settings_store import client_for_batch
@contextmanager
def file_index_lock(file_id: int):
root = get_settings().processed_path
root.mkdir(parents=True, exist_ok=True)
with (root / f"file-{file_id}.lock").open("a+b") as handle:
fcntl.flock(handle, fcntl.LOCK_EX)
try:
yield
finally:
fcntl.flock(handle, fcntl.LOCK_UN)
def parse_submission(db, file: SubmissionFile, *, embedder=None):
with file_index_lock(file.id):
db.refresh(file)
return _parse_submission_locked(db, file, embedder=embedder)
def _parse_submission_locked(db, file: SubmissionFile, *, embedder=None):
path = secure_path(file.relative_path)
if sha256(path) != file.sha256:
file.parse_status, file.parse_error = "changed", "源文件已变化,请重新扫描并确认导入"
@@ -59,6 +79,8 @@ def _project_facts(db, project, course_snapshot, client, *, retriever):
category=category, query="项目名称 目标 指标 硬件 软件 实验 数据 成本 结论", limit=3, include_public=False))
if not snippets:
return {}
if client is None:
raise ModelError("未配置或无法连接评分模型")
facts = client.json_call(prompts["facts"], {"evidence": snippets}, FactResult).model_dump()
from .models import Student
student = db.get(Student, project.student_id)
@@ -102,6 +124,8 @@ def _score_criterion(db, batch, project, criterion, client, *, retriever):
score.status, score.error = "unscorable", "无可检索的材料证据"
db.commit()
return score
if client is None:
raise ModelError("未配置或无法连接评分模型")
page_paths = {page["page_number"]: page.get("rendered_image_path") for page in ready[0].parsed["pages"]}
images = [page_paths[page] for page in dict.fromkeys(chunk["page"] for chunk in chunks[:2]) if page_paths.get(page)]
public_context = retriever(db, course_version_id=batch.course_version_id, project_id=project.id,
@@ -140,7 +164,10 @@ def _run_project(batch_id, project_id, session_factory, client_factory, embedder
with session_factory() as db:
batch = db.get(ScoringBatch, batch_id)
project = db.get(Project, project_id)
client = client_factory(db, batch)
try:
client = client_factory(db, batch)
except Exception:
client = None
files = db.scalars(select(SubmissionFile).where(SubmissionFile.project_id == project.id)).all()
for file in files:
if file.recognition_status == "recognized":
@@ -178,16 +205,11 @@ def run_batch(batch_id: int, *, session_factory=SessionLocal, client_factory=cli
batch = db.get(ScoringBatch, batch_id)
if not batch:
raise ValueError("评分批次不存在")
project_ids = db.scalars(select(Project.id).where(Project.course_version_id == batch.course_version_id).order_by(Project.id)).all()
project_ids = batch.project_ids if batch.project_ids is not None else db.scalars(
select(Project.id).where(Project.course_version_id == batch.course_version_id).order_by(Project.id)).all()
criteria = batch.rubric_snapshot["criteria"]
batch.status, batch.progress_total, batch.progress_done, batch.finished_at = "running", len(project_ids) * len(criteria), 0, None
db.commit()
try:
client_factory(db, batch)
except Exception as exc:
batch.status, batch.error, batch.finished_at = "failed", str(exc), now()
db.commit()
return
failures = 0
with ThreadPoolExecutor(max_workers=max(1, int(batch.model_config_snapshot.get("concurrency", 1)))) as pool:
futures = [pool.submit(_run_project, batch_id, project_id, session_factory, client_factory, embedder, retriever) for project_id in project_ids]