Spaces:
Sleeping
Sleeping
| from fastapi import FastAPI, HTTPException | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from pydantic import BaseModel | |
| from typing import List, Optional, Dict | |
| import os | |
| import json | |
| from datetime import datetime | |
| from langchain_core.messages import HumanMessage, AIMessage | |
| from socratic_graph import learner_app, get_resources, get_text_content | |
| from ingest import process_new_files | |
| import shutil | |
| from fastapi import UploadFile, File | |
| from fastapi.staticfiles import StaticFiles | |
| app = FastAPI() | |
| # Enable CORS for React development | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], # In production, restrict this to your frontend URL | |
| allow_credentials=True, | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| # --- MODELS --- | |
| class Message(BaseModel): | |
| role: str # "user" or "assistant" | |
| content: str | |
| class ChatRequest(BaseModel): | |
| messages: List[Message] | |
| grade: str | |
| subject: str | |
| topic: str | |
| hint_level: int | |
| status: str | |
| class ChatResponse(BaseModel): | |
| message: Message | |
| hint_level: int | |
| status: str | |
| context: str | |
| # --- UTILS --- | |
| def convert_to_langchain(messages: List[Message]): | |
| lc_messages = [] | |
| for m in messages: | |
| if m.role == "user": | |
| lc_messages.append(HumanMessage(content=m.content)) | |
| else: | |
| lc_messages.append(AIMessage(content=m.content)) | |
| return lc_messages | |
| # --- ENDPOINTS --- | |
| async def get_curriculum(): | |
| try: | |
| vector_store = get_resources() | |
| all_data = vector_store.get(include=['metadatas']) | |
| metadatas = all_data['metadatas'] | |
| options = {} | |
| for meta in metadatas: | |
| grade = meta.get('grade', 'Unknown') | |
| subject = meta.get('subject', 'Unknown') | |
| topics_str = meta.get('topics', 'General') | |
| if grade not in options: options[grade] = {} | |
| if subject not in options[grade]: options[grade][subject] = set() | |
| t_list = [t.strip().title() for t in topics_str.split(',') if t.strip()] | |
| for t in t_list: | |
| options[grade][subject].add(t) | |
| # Convert sets to sorted lists | |
| final_options = {} | |
| for g in options: | |
| final_options[g] = {} | |
| for s in options[g]: | |
| final_options[g][s] = sorted(list(options[g][s])) | |
| return final_options | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def chat(request: ChatRequest): | |
| try: | |
| lc_messages = convert_to_langchain(request.messages) | |
| initial_state = { | |
| "messages": lc_messages, | |
| "hint_level": request.hint_level, | |
| "context": "", | |
| "status": request.status, | |
| "safety_status": "PASS", | |
| "current_topic": request.topic, | |
| "grade": request.grade, | |
| "subject": request.subject, | |
| "selected_topic": request.topic | |
| } | |
| final_state = await learner_app.ainvoke(initial_state) | |
| response_msg = final_state["messages"][-1] | |
| response_text = get_text_content(response_msg.content) | |
| # Log for evaluation (consistent with app.py logic) | |
| log_entry = { | |
| "question": request.messages[-1].content if request.messages else "", | |
| "answer": response_text, | |
| "contexts": [final_state.get("context", "")], | |
| "timestamp": datetime.now().isoformat() | |
| } | |
| with open("eval_logs.jsonl", "a", encoding="utf-8") as f: | |
| f.write(json.dumps(log_entry) + "\n") | |
| return ChatResponse( | |
| message=Message(role="assistant", content=response_text), | |
| hint_level=final_state["hint_level"], | |
| status=final_state["status"], | |
| context=final_state.get("context", "") | |
| ) | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def ingest(): | |
| try: | |
| result = process_new_files() | |
| return {"status": "success", "message": result} | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def upload_file(grade: str, subject: str, file: UploadFile = File(...)): | |
| try: | |
| # Create directory structure: data/Grade/Subject/ | |
| target_dir = os.path.join("data", grade, subject) | |
| os.makedirs(target_dir, exist_ok=True) | |
| file_path = os.path.join(target_dir, file.filename) | |
| with open(file_path, "wb") as buffer: | |
| shutil.copyfileobj(file.file, buffer) | |
| return {"status": "success", "filename": file.filename} | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| # Mount the React production build output directory if it exists | |
| frontend_dist_dir = os.path.join(os.path.dirname(__file__), "frontend", "dist") | |
| if os.path.exists(frontend_dist_dir): | |
| app.mount("/", StaticFiles(directory=frontend_dist_dir, html=True), name="static") | |
| else: | |
| # Optional fallback or message | |
| print("Warning: frontend/dist directory not found. Static files will not be served.") | |
| if __name__ == "__main__": | |
| import uvicorn | |
| uvicorn.run(app, host="0.0.0.0", port=8000) | |