Spaces:
Sleeping
Sleeping
| """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) | |