Files
WiseGrade/backend/wisegrade/api.py
T

595 lines
28 KiB
Python

import hashlib
import json
import mimetypes
from pathlib import Path
from fastapi import APIRouter, Depends, HTTPException
from fastapi.responses import FileResponse
from pydantic import BaseModel, Field
from redis import Redis
from rq import Queue, Retry
from sqlalchemy import delete, func, select
from sqlalchemy.orm import Session
from .config import get_settings
from .db import get_db
from .exports import export_excel, export_project_pdf, project_result
from .models import (AppSetting, AuditLog, Course, CourseVersion, CriterionScore, ExportTask,
KnowledgeChunk, KnowledgeDocument, Project, RubricVersion, ScoringBatch,
Student, Submission, SubmissionFile, TeacherRevision)
from .materialized import sync_course_entities, sync_rubric_entities
from .parsing import parse_document
from .rag import ingest
from .rules import RubricData
from .scoring import run_batch
from .settings_store import key_configured, model_config, save_model_config
from .storage import CATEGORIES, SUPPORTED, directories, scan, secure_path, sha256
router = APIRouter(prefix="/api")
def found(db, cls, entity_id):
item = db.get(cls, entity_id)
if not item:
raise HTTPException(404, f"{cls.__name__}不存在")
return item
def audit(db, action, entity_type, entity_id, details=None):
db.add(AuditLog(action=action, entity_type=entity_type, entity_id=entity_id, details=details or {}))
def remove_file_index(db, file):
for document in db.scalars(select(KnowledgeDocument).where(KnowledgeDocument.submission_file_id == file.id)).all():
db.execute(delete(KnowledgeChunk).where(KnowledgeChunk.document_id == document.id))
db.delete(document)
file.parsed, file.parse_status = {}, "pending"
def version_dict(item):
return {key: getattr(item, key) for key in ("id", "course_id", "version", "title", "objectives", "indicators", "directions", "prompts", "templates", "published", "created_at")}
def rubric_dict(item):
return {key: getattr(item, key) for key in ("id", "course_version_id", "version", "criteria", "weights", "published", "created_at")}
class CourseInput(BaseModel):
name: str = Field(min_length=1)
code: str = Field(min_length=1)
class VersionInput(BaseModel):
title: str = Field(min_length=1)
source_version_id: int | None = None
class VersionEdit(BaseModel):
title: str = Field(min_length=1)
objectives: list[dict]
indicators: list[dict]
directions: list[dict]
prompts: dict[str, str]
templates: list[str]
class ImportScan(BaseModel):
relative_dir: str
class FileConfirm(BaseModel):
relative_path: str
category: str
sha256: str
status: str = "recognized"
class ProjectConfirm(BaseModel):
relative_dir: str
student_no: str = Field(pattern=r"^\d{5,20}$")
name: str = Field(min_length=1)
topic: str = Field(min_length=1)
direction: str = ""
files: list[FileConfirm]
class ImportConfirm(BaseModel):
course_version_id: int
projects: list[ProjectConfirm]
class BatchInput(BaseModel):
course_version_id: int
rubric_version_id: int
class ReviewInput(BaseModel):
score: float = Field(ge=0)
comment: str = Field(min_length=1)
reason: str = ""
class ModelSettingsInput(BaseModel):
base_url: str | None = None
model: str | None = None
timeout_seconds: int | None = Field(default=None, ge=10, le=600)
concurrency: int | None = Field(default=None, ge=1, le=16)
retries: int | None = Field(default=None, ge=0, le=5)
api_key: str | None = None
@router.get("/dashboard")
def dashboard(db: Session = Depends(get_db)):
return {"courses": db.scalar(select(func.count(Course.id))), "projects": db.scalar(select(func.count(Project.id))),
"batches": db.scalar(select(func.count(ScoringBatch.id))),
"needs_attention": db.scalar(select(func.count(CriterionScore.id)).where(CriterionScore.status.in_(["failed", "unscorable"]))) }
@router.get("/courses")
def list_courses(db: Session = Depends(get_db)):
return [{"id": c.id, "name": c.name, "code": c.code} for c in db.scalars(select(Course).order_by(Course.id))]
@router.post("/courses")
def create_course(data: CourseInput, db: Session = Depends(get_db)):
if db.scalar(select(Course.id).where((Course.code == data.code) | (Course.name == data.name))):
raise HTTPException(409, "课程名称或代码已存在")
course = Course(**data.model_dump())
db.add(course)
db.flush()
version = CourseVersion(course_id=course.id, version=1, title="初始草稿", objectives=[], indicators=[], directions=[], prompts={}, templates=[])
db.add(version)
db.flush()
sync_course_entities(db, version)
audit(db, "create", "course", course.id)
db.commit()
return {"id": course.id}
@router.get("/courses/{course_id}/versions")
def list_versions(course_id: int, db: Session = Depends(get_db)):
found(db, Course, course_id)
return [version_dict(v) for v in db.scalars(select(CourseVersion).where(CourseVersion.course_id == course_id).order_by(CourseVersion.version.desc()))]
@router.post("/courses/{course_id}/versions")
def copy_version(course_id: int, data: VersionInput, db: Session = Depends(get_db)):
found(db, Course, course_id)
source = found(db, CourseVersion, data.source_version_id) if data.source_version_id else db.scalar(select(CourseVersion).where(CourseVersion.course_id == course_id).order_by(CourseVersion.version.desc()))
if source and source.course_id != course_id:
raise HTTPException(400, "源版本不属于课程")
next_version = (db.scalar(select(func.max(CourseVersion.version)).where(CourseVersion.course_id == course_id)) or 0) + 1
fields = {key: json.loads(json.dumps(getattr(source, key))) if source else ([] if key != "prompts" else {}) for key in ("objectives", "indicators", "directions", "prompts", "templates")}
version = CourseVersion(course_id=course_id, version=next_version, title=data.title, published=False, **fields)
db.add(version)
db.flush()
sync_course_entities(db, version)
if source:
rubric = db.scalar(select(RubricVersion).where(RubricVersion.course_version_id == source.id).order_by(RubricVersion.version.desc()))
if rubric:
copied = RubricVersion(course_version_id=version.id, version=1, criteria=rubric.criteria, weights=rubric.weights, published=False)
db.add(copied)
db.flush()
sync_rubric_entities(db, copied)
audit(db, "copy", "course_version", version.id, {"source_id": source.id if source else None})
db.commit()
return version_dict(version)
@router.put("/course-versions/{version_id}")
def edit_version(version_id: int, data: VersionEdit, db: Session = Depends(get_db)):
version = found(db, CourseVersion, version_id)
if version.published:
raise HTTPException(409, "已发布版本不可修改,请复制新版本")
for key, value in data.model_dump().items():
setattr(version, key, value)
sync_course_entities(db, version)
audit(db, "edit", "course_version", version.id)
db.commit()
return version_dict(version)
@router.post("/course-versions/{version_id}/publish")
def publish_version(version_id: int, db: Session = Depends(get_db)):
version = found(db, CourseVersion, version_id)
if not version.objectives or not version.indicators or not all(k in version.prompts for k in ("facts", "score", "consistency")):
raise HTTPException(400, "课程目标、指标点和三个Prompt不能为空")
version.published = True
audit(db, "publish", "course_version", version.id)
db.commit()
return version_dict(version)
@router.get("/course-versions/{version_id}/rubrics")
def list_rubrics(version_id: int, db: Session = Depends(get_db)):
found(db, CourseVersion, version_id)
return [rubric_dict(r) for r in db.scalars(select(RubricVersion).where(RubricVersion.course_version_id == version_id).order_by(RubricVersion.version.desc()))]
@router.post("/course-versions/{version_id}/rubrics")
def create_rubric(version_id: int, data: RubricData, db: Session = Depends(get_db)):
found(db, CourseVersion, version_id)
next_version = (db.scalar(select(func.max(RubricVersion.version)).where(RubricVersion.course_version_id == version_id)) or 0) + 1
rubric = RubricVersion(course_version_id=version_id, version=next_version, criteria=[c.model_dump() for c in data.criteria], weights=data.weights)
db.add(rubric)
db.flush()
sync_rubric_entities(db, rubric)
db.commit()
return rubric_dict(rubric)
@router.post("/course-versions/{version_id}/rubrics/copy-from/{source_id}")
def create_rubric_from_source(version_id: int, source_id: int, db: Session = Depends(get_db)):
found(db, CourseVersion, version_id)
source = found(db, RubricVersion, source_id)
next_version = (db.scalar(select(func.max(RubricVersion.version)).where(RubricVersion.course_version_id == version_id)) or 0) + 1
rubric = RubricVersion(course_version_id=version_id, version=next_version,
criteria=json.loads(json.dumps(source.criteria)), weights=dict(source.weights), published=False)
db.add(rubric)
db.flush()
sync_rubric_entities(db, rubric)
audit(db, "copy", "rubric", rubric.id, {"source_id": source.id})
db.commit()
return rubric_dict(rubric)
@router.get("/rubrics/catalog")
def rubric_catalog(db: Session = Depends(get_db)):
return [{"id": r.id, "course_version_id": r.course_version_id, "version": r.version,
"criteria_count": len(r.criteria)} for r in db.scalars(select(RubricVersion).where(RubricVersion.published.is_(True)).order_by(RubricVersion.id))]
@router.get("/rubrics/{rubric_id}")
def get_rubric(rubric_id: int, db: Session = Depends(get_db)):
return rubric_dict(found(db, RubricVersion, rubric_id))
@router.post("/rubrics/{rubric_id}/copy")
def copy_rubric(rubric_id: int, db: Session = Depends(get_db)):
source = found(db, RubricVersion, rubric_id)
next_version = (db.scalar(select(func.max(RubricVersion.version)).where(RubricVersion.course_version_id == source.course_version_id)) or 0) + 1
rubric = RubricVersion(course_version_id=source.course_version_id, version=next_version,
criteria=json.loads(json.dumps(source.criteria)), weights=dict(source.weights), published=False)
db.add(rubric)
db.flush()
sync_rubric_entities(db, rubric)
audit(db, "copy", "rubric", rubric.id, {"source_id": source.id})
db.commit()
return rubric_dict(rubric)
@router.put("/rubrics/{rubric_id}")
def edit_rubric(rubric_id: int, data: RubricData, db: Session = Depends(get_db)):
rubric = found(db, RubricVersion, rubric_id)
if rubric.published:
raise HTTPException(409, "已发布规则不可修改,请复制新版本")
rubric.criteria, rubric.weights = [c.model_dump() for c in data.criteria], data.weights
sync_rubric_entities(db, rubric)
audit(db, "edit", "rubric", rubric.id)
db.commit()
return rubric_dict(rubric)
@router.post("/rubrics/{rubric_id}/publish")
def publish_rubric(rubric_id: int, db: Session = Depends(get_db)):
rubric = found(db, RubricVersion, rubric_id)
data = RubricData(criteria=rubric.criteria, weights=rubric.weights)
version = found(db, CourseVersion, rubric.course_version_id)
objective_codes = {str(item.get("code")) for item in version.objectives}
indicator_codes = {str(item.get("code")) for item in version.indicators}
for criterion in data.criteria:
if not set(criterion.linked_objectives) <= objective_codes or not set(criterion.linked_outcomes) <= indicator_codes:
raise HTTPException(400, f"{criterion.criterion_id}关联了课程版本不存在的目标或指标点")
rubric.published = True
audit(db, "publish", "rubric", rubric.id)
db.commit()
return rubric_dict(rubric)
@router.get("/storage/directories")
def list_directories(relative: str = "."):
return directories(relative)
@router.post("/import/scan")
def scan_import(data: ImportScan):
return scan(data.relative_dir)
@router.post("/import/confirm")
def confirm_import(data: ImportConfirm, db: Session = Depends(get_db)):
version = found(db, CourseVersion, data.course_version_id)
if not version.published:
raise HTTPException(400, "请先发布课程版本")
imported = []
for item in data.projects:
folder = secure_path(item.relative_dir)
if not folder.is_dir():
raise ValueError("学生目录不存在")
student = db.scalar(select(Student).where(Student.student_no == item.student_no))
if not student:
student = Student(student_no=item.student_no, name=item.name)
db.add(student)
db.flush()
else:
student.name = item.name
project = db.scalar(select(Project).where(Project.course_version_id == version.id, Project.source_dir == item.relative_dir))
if not project:
project = Project(course_version_id=version.id, student_id=student.id, source_dir=item.relative_dir,
topic=item.topic, direction=item.direction)
db.add(project)
db.flush()
else:
project.student_id, project.topic, project.direction = student.id, item.topic, item.direction
selected = set()
selected_paths = set()
for file in item.files:
path = secure_path(file.relative_path)
if path.parent != folder or not path.is_file() or path.suffix.lower() not in SUPPORTED:
raise ValueError("材料必须是学生目录内的PDF、DOCX或PPTX文件")
if sha256(path) != file.sha256:
raise ValueError("文件已变化,请重新扫描")
if file.status == "ignored":
continue
if file.category not in CATEGORIES or file.category in selected or file.relative_path in selected_paths:
raise ValueError("请为每类材料只确认一个文件")
selected.add(file.category)
selected_paths.add(file.relative_path)
stat = path.stat()
record = db.scalar(select(SubmissionFile).where(SubmissionFile.project_id == project.id, SubmissionFile.relative_path == file.relative_path))
if not record:
record = SubmissionFile(project_id=project.id, relative_path=file.relative_path, category=file.category,
mime_type=mimetypes.guess_type(path.name)[0] or "application/octet-stream",
size=stat.st_size, mtime=stat.st_mtime, sha256=file.sha256, recognition_status="recognized")
db.add(record)
else:
if record.sha256 != file.sha256 or record.category != file.category:
remove_file_index(db, record)
record.category, record.sha256, record.size, record.mtime, record.recognition_status = file.category, file.sha256, stat.st_size, stat.st_mtime, "recognized"
for old in db.scalars(select(SubmissionFile).where(SubmissionFile.project_id == project.id)).all():
if old.relative_path not in selected_paths:
old.recognition_status = "ignored"
if old.parse_status == "ready":
remove_file_index(db, old)
for category in CATEGORIES:
submission = db.scalar(select(Submission).where(Submission.project_id == project.id, Submission.category == category))
if not submission:
submission = Submission(project_id=project.id, category=category, status="missing")
db.add(submission)
db.flush()
submission.status = "submitted" if category in selected else "missing"
for file in db.scalars(select(SubmissionFile).where(SubmissionFile.project_id == project.id,
SubmissionFile.category == category, SubmissionFile.recognition_status == "recognized")).all():
file.submission_id = submission.id
audit(db, "import", "project", project.id, {"files": len(selected)})
imported.append(project.id)
db.commit()
return {"project_ids": imported}
@router.get("/projects")
def list_projects(course_version_id: int | None = None, db: Session = Depends(get_db)):
query = select(Project).order_by(Project.id.desc())
if course_version_id:
query = query.where(Project.course_version_id == course_version_id)
result = []
for project in db.scalars(query):
student = db.get(Student, project.student_id)
files = db.scalars(select(SubmissionFile).where(SubmissionFile.project_id == project.id)).all()
result.append({"id": project.id, "student_no": student.student_no, "name": student.name, "topic": project.topic,
"course_version_id": project.course_version_id, "status": project.status,
"missing": [category for category in CATEGORIES if not any(file.category == category and file.recognition_status == "recognized" for file in files)],
"parse_errors": [file.parse_error for file in files if file.parse_error]})
return result
@router.get("/projects/{project_id}")
def get_project(project_id: int, db: Session = Depends(get_db)):
project = found(db, Project, project_id)
student = db.get(Student, project.student_id)
files = db.scalars(select(SubmissionFile).where(SubmissionFile.project_id == project.id)).all()
return {"id": project.id, "student_no": student.student_no, "name": student.name, "topic": project.topic,
"course_version_id": project.course_version_id, "direction": project.direction, "facts": project.facts,
"files": [{key: getattr(file, key) for key in ("id", "category", "relative_path", "sha256", "parse_status", "parse_error", "recognition_status")} for file in files]}
@router.get("/knowledge")
def knowledge(course_version_id: int, project_id: int | None = None, db: Session = Depends(get_db)):
query = select(KnowledgeDocument).where(KnowledgeDocument.course_version_id == course_version_id,
KnowledgeDocument.project_id == project_id)
return [{"id": doc.id, "title": doc.title, "type": doc.document_type, "status": doc.status,
"chunks": db.scalar(select(func.count(KnowledgeChunk.id)).where(KnowledgeChunk.document_id == doc.id))} for doc in db.scalars(query)]
@router.post("/knowledge/ingest-course/{course_version_id}")
def ingest_course(course_version_id: int, db: Session = Depends(get_db)):
version = found(db, CourseVersion, course_version_id)
root = Path("/app/course-docs")
processed = get_settings().processed_path / f"course-{course_version_id}"
completed, errors = [], {}
for filename in version.templates:
if Path(filename).name != filename:
errors[filename] = "非法课程资料路径"
continue
path = root / filename
if not path.is_file():
errors[filename] = "课程资料文件不存在"
continue
try:
category = "stage1" if "阶段报告1" in filename else "stage2" if "阶段报告2" in filename else "stage3" if "阶段报告3" in filename else "course"
parsed = parse_document(path, category, processed / hashlib.sha256(filename.encode()).hexdigest()[:16])
ingest(db, course_version_id=version.id, project_id=None, submission_file_id=None,
title=filename, category=category, source_hash=sha256(path), pages=parsed["pages"])
completed.append(filename)
except Exception as exc:
errors[filename] = str(exc)[:500]
rubric = db.scalar(select(RubricVersion).where(RubricVersion.course_version_id == version.id,
RubricVersion.published.is_(True)).order_by(RubricVersion.version.desc()))
virtual = [
("课程目标、毕业要求与选题方向", {"objectives": version.objectives, "indicators": version.indicators,
"directions": version.directions}),
("已发布评分规则", {"criteria": rubric.criteria, "weights": rubric.weights} if rubric else {}),
]
for title, payload in virtual:
if not payload:
continue
try:
content = json.dumps(payload, ensure_ascii=False, indent=2)
ingest(db, course_version_id=version.id, project_id=None, submission_file_id=None, title=title,
category="course", source_hash=hashlib.sha256(content.encode()).hexdigest(),
pages=[{"page_number": 1, "section": title, "text": content, "tables": []}])
completed.append(title)
except Exception as exc:
errors[title] = str(exc)[:500]
return {"completed": completed, "errors": errors}
@router.post("/scoring-batches")
def create_batch(data: BatchInput, db: Session = Depends(get_db)):
version = found(db, CourseVersion, data.course_version_id)
rubric = found(db, RubricVersion, data.rubric_version_id)
if not version.published or not rubric.published or rubric.course_version_id != version.id:
raise HTTPException(400, "请选择同一课程版本下已发布的规则")
settings = get_settings()
batch = ScoringBatch(course_version_id=version.id, rubric_version_id=rubric.id,
course_snapshot={key: getattr(version, key) for key in ("version", "title", "objectives", "indicators", "directions", "prompts", "templates")},
rubric_snapshot={"version": rubric.version, "criteria": rubric.criteria, "weights": rubric.weights},
model_config_snapshot=model_config(db), embedding_model_name=settings.embedding_model_name,
embedding_model_version=settings.embedding_model_version)
db.add(batch)
db.flush()
audit(db, "create", "scoring_batch", batch.id)
db.commit()
return {"id": batch.id, "status": batch.status}
@router.get("/scoring-batches")
def list_batches(db: Session = Depends(get_db)):
return [{key: getattr(batch, key) for key in ("id", "course_version_id", "rubric_version_id", "status", "progress_done", "progress_total", "error", "created_at", "finished_at")}
for batch in db.scalars(select(ScoringBatch).order_by(ScoringBatch.id.desc()))]
@router.get("/scoring-batches/{batch_id}/progress")
def batch_progress(batch_id: int, db: Session = Depends(get_db)):
batch = found(db, ScoringBatch, batch_id)
return {key: getattr(batch, key) for key in ("id", "status", "progress_done", "progress_total", "error", "created_at", "finished_at")}
@router.post("/scoring-batches/{batch_id}/start")
def start_batch(batch_id: int, db: Session = Depends(get_db)):
batch = found(db, ScoringBatch, batch_id)
if batch.status == "running":
raise HTTPException(409, "批次正在评分")
try:
Queue("wisegrade", connection=Redis.from_url(get_settings().redis_url)).enqueue(run_batch, batch_id,
job_timeout="12h", retry=Retry(max=1, interval=[60]))
except Exception as exc:
raise HTTPException(503, f"无法提交后台任务: {type(exc).__name__}") from exc
batch.status, batch.error = "queued", ""
db.commit()
return {"id": batch.id, "status": batch.status}
@router.get("/scoring-batches/{batch_id}/scores")
def batch_scores(batch_id: int, db: Session = Depends(get_db)):
batch = found(db, ScoringBatch, batch_id)
projects = db.scalars(select(Project).where(Project.course_version_id == batch.course_version_id).order_by(Project.id)).all()
return [project_result(db, batch, project) for project in projects]
@router.get("/projects/{project_id}/scores")
def project_scores(project_id: int, batch_id: int, db: Session = Depends(get_db)):
batch, project = found(db, ScoringBatch, batch_id), found(db, Project, project_id)
if project.course_version_id != batch.course_version_id:
raise HTTPException(400, "项目不属于评分批次课程版本")
return project_result(db, batch, project)
@router.put("/criterion-scores/{score_id}/review")
def review_score(score_id: int, data: ReviewInput, db: Session = Depends(get_db)):
score = found(db, CriterionScore, score_id)
batch = found(db, ScoringBatch, score.batch_id)
criterion = next((item for item in batch.rubric_snapshot["criteria"] if item["criterion_id"] == score.criterion_id), None)
if not criterion or data.score > criterion["max_score"]:
raise HTTPException(400, "教师分数超出规则满分")
db.add(TeacherRevision(score_id=score.id, old_score=score.teacher_score, new_score=data.score,
old_comment=score.teacher_comment, new_comment=data.comment, reason=data.reason))
score.teacher_score, score.teacher_comment, score.status = data.score, data.comment, "reviewed"
audit(db, "review", "criterion_score", score.id, {"reason": data.reason})
db.commit()
return {"id": score.id, "model_score": score.model_score, "teacher_score": score.teacher_score,
"final_score": score.final_score, "teacher_comment": score.teacher_comment}
@router.get("/criterion-scores/{score_id}/revisions")
def revisions(score_id: int, db: Session = Depends(get_db)):
found(db, CriterionScore, score_id)
return [{key: getattr(item, key) for key in ("id", "old_score", "new_score", "old_comment", "new_comment", "reason", "created_at")}
for item in db.scalars(select(TeacherRevision).where(TeacherRevision.score_id == score_id).order_by(TeacherRevision.id))]
@router.post("/exports/excel")
def create_excel(data: dict, db: Session = Depends(get_db)):
task = export_excel(db, int(data["batch_id"]))
audit(db, "export", "scoring_batch", task.batch_id, {"kind": "excel", "task_id": task.id})
db.commit()
return {"id": task.id}
@router.post("/exports/project-pdf")
def create_pdf(data: dict, db: Session = Depends(get_db)):
task = export_project_pdf(db, int(data["batch_id"]), int(data["project_id"]))
audit(db, "export", "project", task.project_id, {"kind": "pdf", "task_id": task.id})
db.commit()
return {"id": task.id}
@router.get("/exports")
def list_exports(db: Session = Depends(get_db)):
return [{key: getattr(task, key) for key in ("id", "batch_id", "project_id", "kind", "status", "error", "created_at")}
for task in db.scalars(select(ExportTask).order_by(ExportTask.id.desc()))]
@router.get("/exports/{task_id}")
def get_export(task_id: int, db: Session = Depends(get_db)):
task = found(db, ExportTask, task_id)
return {key: getattr(task, key) for key in ("id", "batch_id", "project_id", "kind", "status", "error", "created_at")}
@router.get("/exports/{task_id}/download")
def download_export(task_id: int, db: Session = Depends(get_db)):
task = found(db, ExportTask, task_id)
root = get_settings().exports_path.resolve()
path = Path(task.path or "").resolve()
if task.status != "completed" or not path.is_file() or not path.is_relative_to(root):
raise HTTPException(404, "导出文件不存在")
return FileResponse(path, filename=path.name)
@router.get("/assets/{file_id}/{page}")
def page_asset(file_id: int, page: int, db: Session = Depends(get_db)):
file = found(db, SubmissionFile, file_id)
pages = file.parsed.get("pages", []) if file.parsed else []
item = next((item for item in pages if item["page_number"] == page), None)
path = Path(item["rendered_image_path"]).resolve() if item and item.get("rendered_image_path") else None
root = get_settings().processed_path.resolve()
if not path or not path.is_file() or not path.is_relative_to(root):
raise HTTPException(404, "页面图像不存在")
return FileResponse(path, media_type="image/png")
@router.get("/settings/model")
def get_model_settings(db: Session = Depends(get_db)):
return {**model_config(db), "api_key_configured": key_configured(db),
"embedding_model_name": get_settings().embedding_model_name,
"embedding_model_path": str(get_settings().embedding_model_path),
"embedding_model_version": get_settings().embedding_model_version}
@router.put("/settings/model")
def put_model_settings(data: ModelSettingsInput, db: Session = Depends(get_db)):
result = save_model_config(db, data.model_dump(exclude_none=True))
audit(db, "edit", "model_settings", None, {"fields": [key for key in data.model_fields_set if key != "api_key"]})
db.commit()
return result