ClassLens / chatkit /backend /app /file_processor.py
Yu Chen
custom csv upload logic
0641d27
Raw
History Blame
4.28 kB
"""File processing orchestrator.
Delegates extraction to a pluggable parser chosen by name or auto-picked.
Saves the parsed structured data to the database, along with metadata about
which parser produced it.
"""
from __future__ import annotations
import json
from fastapi import UploadFile
from .database import delete_parsed_data, save_parsed_data
from .parsers import AUTO, get_parser, pick_auto
from .parsers.base import ParserFile, ParserResult
async def process_uploaded_files(
files: list[UploadFile],
data_type: str,
session_id: int,
description: str = "",
model: str = "gpt-5.4",
parser: str = AUTO,
) -> dict:
"""Parse uploaded files and persist the structured result.
`data_type='answers'` triggers a dual extraction: the same files are parsed
twice (once for student_answers, once for teacher_answers) and both rows are
stored. The response merges both shapes under their respective keys.
"""
parser_files: list[ParserFile] = []
for file in files:
content = await file.read()
if not content:
raise ValueError(f"File '{file.filename}' is empty.")
parser_files.append(
ParserFile(filename=file.filename or "unknown", content=content)
)
if not parser_files:
raise ValueError("No files uploaded")
if data_type == "answers":
return await _process_combined_answers(
parser_files, session_id, description, model, parser
)
result = await _run_with_fallback(parser_files, data_type, description, model, parser)
await _persist(session_id, data_type, parser_files, result)
return _response_payload(result)
async def _process_combined_answers(
parser_files: list[ParserFile],
session_id: int,
description: str,
model: str,
parser: str,
) -> dict:
student_result = await _run_with_fallback(
parser_files, "student_answers", description, model, parser
)
teacher_result = await _run_with_fallback(
parser_files, "teacher_answers", description, model, parser
)
await _persist(session_id, "student_answers", parser_files, student_result)
await _persist(session_id, "teacher_answers", parser_files, teacher_result)
return {
"student_answers": _response_payload(student_result),
"teacher_answers": _response_payload(teacher_result),
}
async def _persist(
session_id: int,
data_type: str,
parser_files: list[ParserFile],
result: ParserResult,
) -> None:
await delete_parsed_data(session_id, data_type)
raw_text = json.dumps(
{
"source_files": [f.filename for f in parser_files],
"parser": result.parser_name,
"notes": list(result.notes),
},
ensure_ascii=False,
)
await save_parsed_data(
session_id,
data_type,
parser_files[0].filename,
raw_text,
result.data,
)
def _response_payload(result: ParserResult) -> dict:
return {
**result.data,
"_meta": {
"parser": result.parser_name,
"notes": list(result.notes),
},
}
async def _run_with_fallback(
parser_files: list[ParserFile],
data_type: str,
description: str,
model: str,
parser_name: str,
) -> ParserResult:
"""Run the selected parser; in auto mode, fall back to LLM on failure."""
if parser_name == AUTO:
first_file = parser_files[0]
chosen = pick_auto(first_file.content, first_file.filename, data_type)
try:
return await chosen.parse(parser_files, data_type, description, model)
except Exception as primary_err:
if chosen.name == "llm_vision":
raise
# Fallback to LLM vision on any native-parser failure
llm = get_parser("llm_vision")
result = await llm.parse(parser_files, data_type, description, model)
return ParserResult(
data=result.data,
parser_name=result.parser_name,
notes=(f"auto-fallback from {chosen.name}: {primary_err}", *result.notes),
)
chosen = get_parser(parser_name)
return await chosen.parse(parser_files, data_type, description, model)