ERNEST2002/Playbook-Grading-OS
0
1import os2import re3import json4import logging5import shutil6import base647import zipfile8from pathlib import Path9from fastapi import FastAPI, Depends, HTTPException, UploadFile, File, BackgroundTasks, Form10from fastapi.middleware.cors import CORSMiddleware11from fastapi.staticfiles import StaticFiles12from fastapi.responses import FileResponse13from pydantic import BaseModel14import uvicorn15import time16import gc17import warnings18import requests # Muhimu kwa DeepSeek API Call ndani ya Chatbot19 20warnings.filterwarnings("ignore", category=FutureWarning)21from sqlalchemy.orm import Session22 23from brain_router import PlaybookMasterRouter, DEEPSEEK_API_KEY24from playbook_orchestrator import PlaybookOrchestrator25from database_manager import SessionLocal, Score, ExamSession, OverrideHistory, MarkingSchemeVersion26from analytics_engine import PlaybookAnalyticsEngine 27 28Path("logs").mkdir(parents=True, exist_ok=True)29Path("workspace").mkdir(parents=True, exist_ok=True)30Path("exports").mkdir(parents=True, exist_ok=True)31Path("database").mkdir(parents=True, exist_ok=True)32 33open("logs/api_server.log", "w").close()34open("logs/orchestrator.log", "w").close()35open("logs/brain_router.log", "w").close()36 37logging.basicConfig(filename="logs/api_server.log", level=logging.INFO, 38 format="%(asctime)s - PLAYBOOK API - %(levelname)s - %(message)s")39 40app = FastAPI(title="Playbook Unicorn OS", version="18.0")41 42app.add_middleware(43 CORSMiddleware,44 allow_origins=["*"],45 allow_credentials=True,46 allow_methods=["*"],47 allow_headers=["*"],48)49 50playbook_brain = PlaybookMasterRouter()51orchestrator = PlaybookOrchestrator(workspace_dir="workspace")52analytics = PlaybookAnalyticsEngine(export_dir="exports")53 54active_sessions_state = {}55 56def get_db():57 db = SessionLocal()58 try:59 yield db60 finally:61 db.close()62 63class AgentRequest(BaseModel):64 transcript: str65 current_view: str66 ui_language: str = "en"67 active_session_id: str = None 68 69class GradingControlRequest(BaseModel):70 session_id: str71 action: str 72 73@app.on_event("startup")74def startup_event():75 import database_manager76 database_manager.init_db()77 playbook_brain.boot_sequence()78 logging.info("System fully initialized via FastAPI Startup. Database is Ready!")79 logging.info("ALL SYSTEMS GO. PLAYBOOK ENTERPRISE MODE (DEEPSEEK V3/R1) ACTIVE.")80 81@app.get("/api/logs")82def get_system_logs(start: int = 0):83 logs = []84 log_files = ["logs/api_server.log", "logs/orchestrator.log", "logs/brain_router.log"]85 for file_path in log_files:86 try:87 with open(file_path, "r") as f:88 logs.extend(f.readlines())89 except: pass90 all_logs = sorted(logs) 91 return {"status": "success", "logs": all_logs[start:]}92 93# ==========================================94# CHATBOT OMNISCIENT ENGINE (HOJA 6)95# ==========================================96@app.post("/api/agent/command")97def handle_agent_command(req: AgentRequest, db: Session = Depends(get_db)):98 # 1. Fetch Context if asked about a specific student99 db_context = ""100 if req.active_session_id:101 session_record = db.query(ExamSession).filter(ExamSession.session_id == req.active_session_id).first()102 if session_record:103 # Let's see if the user mentioned any specific name104 scores = db.query(Score).filter(Score.exam_session_id == session_record.id).all()105 for s in scores:106 # Basic name matching107 if s.reg_number.lower() in req.transcript.lower():108 db_context += f"Student: {s.reg_number}, Score: {s.total_score}%. Breakdown: {s.question_breakdown_json}. Remarks: {s.ai_explanation}\n"109 110 if not db_context:111 db_context = f"Current Session has {len(scores)} students graded."112 113 # 2. Ask the DeepSeek Voice Engine for Contextual Q&A114 if any(word in req.transcript.lower() for word in ["how to", "where is", "help", "nifanyeje", "why did", "what did", "score", "explain", "kwanini", "mbona"]):115 manual_reply = playbook_brain.voice.ask_system_agent(req.transcript, db_context)116 return {"status": "success", "command": {"ui_action": "NONE", "spoken_reply": manual_reply}}117 118 # 3. Standard Navigation Agent (DeepSeek Chat)119 system_prompt = f"""120 You are PLAYBOOK AI. NEVER mention the word 'DeepSeek'.121 Current screen: {req.current_view}. Current Language: {req.ui_language}122 [CAPABILITIES]123 - NAVIGATE (payload: 'phaseUpload', 'phaseGoldStandard', 'phaseMarking', 'phaseAnalytics')124 - CHANGE_LANGUAGE (payload: 'en' or 'sw')125 - ACTION (payload: 'PAUSE_GRADING', 'RESUME_GRADING')126 - NONE127 User Input: "{req.transcript}"128 Respond STRICTLY in valid JSON without markdown, matching this schema:129 {{"thought_process": "Why you chose this action", "ui_action": "ACTION_NAME", "payload": "Target payload", "spoken_reply": "Short natural reply"}}130 """131 132 payload = {133 "model": "deepseek-chat",134 "messages": [135 {"role": "system", "content": "You are a JSON routing AI. Output pure JSON only."},136 {"role": "user", "content": system_prompt}137 ],138 "temperature": 0.1139 }140 141 try:142 response = requests.post("https://api.deepseek.com/v1/chat/completions", headers={"Authorization": f"Bearer {DEEPSEEK_API_KEY}", "Content-Type": "application/json"}, json=payload)143 response.raise_for_status()144 raw_text = response.json()['choices'][0]['message']['content'].strip()145 146 if raw_text.startswith("```json"): raw_text = raw_text[7:]147 elif raw_text.startswith("```"): raw_text = raw_text[3:]148 if raw_text.endswith("```"): raw_text = raw_text[:-3]149 150 return {"status": "success", "command": json.loads(raw_text.strip())}151 except Exception as e:152 logging.error(f"Agent Error: {e}")153 return {"status": "error", "command": {"ui_action": "NONE", "spoken_reply": "Logic error in Playbook Brain."}}154 155def background_mass_grading(session_id: str):156 db = SessionLocal() 157 try:158 global active_sessions_state159 160 if session_id not in active_sessions_state:161 active_sessions_state[session_id] = {"processed": 0, "total": 0, "flagged": 0, "status": "GRADING", "live_results": []}162 163 while True:164 session_record = db.query(ExamSession).filter(ExamSession.session_id == session_id).first()165 if not session_record or session_record.status != "GRADING":166 active_sessions_state[session_id]["status"] = "PAUSED"167 break168 169 active_sessions_state[session_id]["status"] = "GRADING"170 pdf_path = session_record.file_path171 start_page = session_record.processed_pages172 173 logging.info(f"Extracting batch starting from page {start_page}...")174 extraction_result = orchestrator.process_bulk_pdf(pdf_path, start_page=start_page)175 176 if extraction_result.get("status") != "success":177 active_sessions_state[session_id]["status"] = "ERROR"178 logging.error("Extraction Failed.")179 break180 181 scripts = extraction_result.get("data", [])182 183 if "total_pages" in extraction_result:184 active_sessions_state[session_id]["total"] = extraction_result["total_pages"]185 186 if not scripts:187 session_record.status = "COMPLETED"188 active_sessions_state[session_id]["status"] = "COMPLETED"189 db.commit()190 logging.info("Grading Completed Successfully.")191 break192 193 gc.collect()194 time.sleep(1) 195 196 scheme_record = db.query(MarkingSchemeVersion).filter(MarkingSchemeVersion.exam_session_id == session_record.id).order_by(MarkingSchemeVersion.created_at.desc()).first()197 active_scheme = scheme_record.content_text if scheme_record else "Standard logical grading."198 199 # --- STITCHING PAGES INTO ONE STUDENT DOSSIER (HOJA 8 & 9) ---200 student_dossiers = {}201 for script in scripts:202 reg_no = script.get("reg_no", "UNKNOWN_SCRIPT")203 if reg_no not in student_dossiers:204 student_dossiers[reg_no] = {"full_text": "", "images": []}205 # Tunaunganisha text zote za kurasa mbalimbali za mwanafunzi huyu!206 student_dossiers[reg_no]["full_text"] += f"\n--- PAGE {script.get('page_num')} ---\n" + script.get("extracted_text", "")207 student_dossiers[reg_no]["images"].append(script.get("base64_image", ""))208 209 # Tunasahihisha 'Dossier' yote ya mwanafunzi (kurasa zote kwa mpigo)210 for reg_no, dossier in student_dossiers.items():211 db.refresh(session_record)212 if session_record.status == "PAUSED":213 break214 215 grade_data = playbook_brain.reasoning.grade_answer(dossier["full_text"], active_scheme)216 217 # Check for Identity Override (From DeepSeek R1)218 final_reg_no = grade_data.get("detected_identity", reg_no)219 if final_reg_no == "null" or final_reg_no == "SYSTEM ERROR" or not final_reg_no: 220 final_reg_no = reg_no221 222 try:223 new_record = Score(224 exam_session_id=session_record.id, 225 reg_number=final_reg_no,226 total_score=grade_data.get("total_score", 0), 227 confidence_score=grade_data.get("confidence", 0),228 ai_explanation=grade_data.get("overall_explanation", ""), 229 status_flag=grade_data.get("flag", "RED"),230 question_breakdown_json=json.dumps(grade_data.get("breakdown", {})), 231 ai_recommendations=grade_data.get("recommendations", ""),232 scanned_image_base64=dossier["images"][0] # Tunaweka picha ya ukurasa wa kwanza pekee kama kielelezo233 )234 db.add(new_record)235 # FIX: Increment processed pages based on actual images processed for this student236 session_record.processed_pages += len(dossier["images"])237 db.commit()238 239 active_sessions_state[session_id]["processed"] = session_record.processed_pages240 if grade_data.get("flag") == "RED": active_sessions_state[session_id]["flagged"] += 1241 active_sessions_state[session_id]["live_results"].append({242 "reg_no": final_reg_no, "score": grade_data.get("total_score", 0),243 "confidence": grade_data.get("confidence", 0), "flag": grade_data.get("flag", "RED")244 })245 logging.info(f"Successfully Graded Student: {final_reg_no} | Score: {grade_data.get('total_score')}")246 except Exception as e:247 logging.error(f"Database error saving score: {e}")248 db.rollback()249 250 except Exception as e:251 logging.error(f"FATAL ERROR in Background Task: {e}")252 finally:253 db.close()254 255@app.get("/api/sessions")256def get_all_sessions(db: Session = Depends(get_db)):257 sessions = db.query(ExamSession).order_by(ExamSession.created_at.desc()).all()258 result = []259 for s in sessions:260 name_part = s.session_id.split('__')[0].replace("SESSION_", "") if '__' in s.session_id else s.session_id261 result.append({"session_id": s.session_id, "name": name_part, "status": s.status, "processed": s.processed_pages, "total": s.total_pages})262 return {"status": "success", "data": result}263 264@app.post("/api/upload/exam")265async def upload_bulk_exam(background_tasks: BackgroundTasks, file: UploadFile = File(...), session_name: str = Form("Unnamed Exam"), db: Session = Depends(get_db)):266 file_location = f"workspace/{file.filename}"267 with open(file_location, "wb") as buffer:268 shutil.copyfileobj(file.file, buffer)269 270 file_hash = orchestrator.generate_file_hash(file_location)271 existing_session = db.query(ExamSession).filter(ExamSession.file_hash == file_hash).first()272 if existing_session: return {"status": "info", "message": "File exists.", "session_id": existing_session.session_id}273 274 clean_name = re.sub(r'[^a-zA-Z0-9]', '', session_name)275 session_id = f"SESSION_{clean_name}__{os.urandom(4).hex()}"276 new_session = ExamSession(session_id=session_id, file_path=file_location, file_hash=file_hash, status="PENDING")277 db.add(new_session)278 db.commit()279 return {"status": "success", "session_id": session_id}280 281@app.post("/api/scheme/save_advanced")282async def save_advanced_scheme(283 session_id: str = Form(...),284 scheme_text: str = Form(""),285 matrix_rules: str = Form(""),286 scheme_file: UploadFile = File(None),287 db: Session = Depends(get_db)288):289 session_record = db.query(ExamSession).filter(ExamSession.session_id == session_id).first()290 if not session_record: raise HTTPException(status_code=404)291 292 extracted_pdf_text = ""293 if scheme_file:294 file_loc = f"workspace/scheme_{session_id}.pdf"295 with open(file_loc, "wb") as buffer:296 shutil.copyfileobj(scheme_file.file, buffer)297 298 logging.info("Extracting rules from uploaded Scheme PDF...")299 scheme_result = orchestrator.process_bulk_pdf(file_loc, start_page=0, batch_size=2)300 if scheme_result["status"] == "success":301 for page in scheme_result["data"]:302 extracted_pdf_text += page.get("extracted_text", "") + "\n"303 304 final_persona_prompt = f"""305 {matrix_rules}306 --- UPLOADED/PASTED SCHEME DETAILS ---307 {scheme_text}308 {extracted_pdf_text}309 """310 311 new_scheme = MarkingSchemeVersion(312 exam_session_id=session_record.id, 313 version_name="Advanced Matrix v1", 314 content_text=final_persona_prompt315 )316 db.add(new_scheme)317 session_record.status = "GRADING"318 db.commit()319 320 from fastapi import BackgroundTasks321 bg_tasks = BackgroundTasks()322 bg_tasks.add_task(background_mass_grading, session_id)323 return {"status": "success", "message": "Advanced Logic Saved. Grading Started."}324 325class OverrideRequest(BaseModel):326 reg_no: str327 new_score: float328 reason: str329 overridden_by: str = "Lecturer"330 331class UndoOverrideRequest(BaseModel):332 reg_no: str333 334class SimulationRequest(BaseModel):335 reg_no: str336 command: str337 338@app.post("/api/override")339def override_student_score(req: OverrideRequest, db: Session = Depends(get_db)):340 score_record = db.query(Score).filter(Score.reg_number == req.reg_no).order_by(Score.created_at.desc()).first()341 if not score_record: raise HTTPException(status_code=404)342 history = OverrideHistory(score_id=score_record.id, old_value=score_record.total_score, new_value=req.new_score, reason=req.reason)343 db.add(history)344 score_record.total_score = req.new_score345 score_record.status_flag = "GREEN"346 db.commit()347 return {"status": "success"}348 349@app.post("/api/override/undo")350def undo_student_override(req: UndoOverrideRequest, db: Session = Depends(get_db)):351 score_record = db.query(Score).filter(Score.reg_number == req.reg_no).order_by(Score.created_at.desc()).first()352 last_override = db.query(OverrideHistory).filter(OverrideHistory.score_id == score_record.id).order_by(OverrideHistory.created_at.desc()).first()353 if not last_override: return {"status": "error"}354 score_record.total_score = last_override.old_value355 score_record.status_flag = "RED"356 db.delete(last_override)357 db.commit()358 return {"status": "success"}359 360@app.post("/api/simulate")361def simulate_what_if_grading(req: SimulationRequest, db: Session = Depends(get_db)):362 score_record = db.query(Score).filter(Score.reg_number == req.reg_no).order_by(Score.created_at.desc()).first()363 simulation_result = playbook_brain.reasoning.simulate_what_if(score_record.question_breakdown_json, req.command)364 return {"status": "success", "simulation": simulation_result}365 366@app.post("/api/grading/control")367def control_grading(req: GradingControlRequest, background_tasks: BackgroundTasks, db: Session = Depends(get_db)):368 session_record = db.query(ExamSession).filter(ExamSession.session_id == req.session_id).first()369 if not session_record: raise HTTPException(status_code=404)370 371 if req.action == "PAUSE":372 session_record.status = "PAUSED"373 db.commit()374 logging.info("Grading Paused by user.")375 return {"status": "success"}376 elif req.action == "RESUME":377 session_record.status = "GRADING"378 db.commit()379 logging.info("Grading Resumed by user.")380 background_tasks.add_task(background_mass_grading, req.session_id)381 return {"status": "success"}382 383@app.get("/api/grading/status/{session_id}")384def get_grading_status(session_id: str):385 return active_sessions_state.get(session_id, {"status": "IDLE"})386 387@app.get("/api/student/{reg_no}")388def get_student_review_data(reg_no: str, db: Session = Depends(get_db)):389 record = db.query(Score).filter(Score.reg_number == reg_no).order_by(Score.created_at.desc()).first()390 return {"status": "success", "data": {"reg_no": record.reg_number, "total_score": record.total_score, "confidence": record.confidence_score, "flag": record.status_flag, "explanation": record.ai_explanation, "breakdown": json.loads(record.question_breakdown_json), "recommendations": record.ai_recommendations, "image_base64": record.scanned_image_base64}}391 392@app.get("/api/analytics/{session_id}")393def get_session_analytics(session_id: str, db: Session = Depends(get_db)):394 session_record = db.query(ExamSession).filter(ExamSession.session_id == session_id).first()395 stats = analytics.get_class_statistics(db, session_record.id)396 qdi = analytics.get_question_difficulty_index(db, session_record.id)397 return {"status": "success", "stats": stats, "qdi": qdi}398 399@app.get("/api/export/excel/{session_id}")400def export_class_excel(session_id: str, db: Session = Depends(get_db)):401 session_record = db.query(ExamSession).filter(ExamSession.session_id == session_id).first()402 file_path = analytics.generate_excel_export(db, session_record.id, session_id)403 return FileResponse(file_path, filename=f"{session_id}_Results.xlsx")404 405@app.get("/api/export/pdf/{reg_no}")406def export_single_student_pdf(reg_no: str, db: Session = Depends(get_db)):407 file_path = analytics.generate_single_student_pdf(db, reg_no)408 return FileResponse(file_path, filename=f"Playbook_Report_{reg_no}.pdf")409 410# FIX: ENDPOINT MPYA YA KUDOWNLOAD ALL PDFS (ZIP) - HOJA 13411@app.get("/api/export/batch_pdfs/{session_id}")412def export_batch_pdf_zip(session_id: str, db: Session = Depends(get_db)):413 session_record = db.query(ExamSession).filter(ExamSession.session_id == session_id).first()414 if not session_record: raise HTTPException(status_code=404)415 zip_path = analytics.generate_mass_pdf_zip(db, session_record.id, session_id)416 return FileResponse(zip_path, filename=f"Playbook_All_Scripts_{session_id}.zip")417 418@app.get("/api/export/archive/{session_id}")419def export_encrypted_archive(session_id: str, db: Session = Depends(get_db)):420 session_record = db.query(ExamSession).filter(ExamSession.session_id == session_id).first()421 archive_dir = Path(f"exports/{session_id}_archive")422 archive_dir.mkdir(parents=True, exist_ok=True)423 analytics.generate_excel_export(db, session_record.id, f"{session_id}_archive/{session_id}")424 zip_path = f"exports/{session_id}_AuditArchive.zip"425 with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:426 for root, _, files in os.walk(archive_dir):427 for file in files: zipf.write(os.path.join(root, file), arcname=file)428 shutil.rmtree(archive_dir)429 return FileResponse(zip_path, filename=f"Playbook_Audit_{session_id}.zip")430 431if os.path.exists("index.html"):432 app.mount("/", StaticFiles(directory=".", html=True), name="root_ui")433elif os.path.exists("frontend/index.html"):434 app.mount("/", StaticFiles(directory="frontend", html=True), name="frontend_ui")435else:436 @app.get("/")437 def read_root():438 return {"detail": "Kioo (index.html) hakijaonekana! Hakikisha umeli-upload vizuri."}439 440if __name__ == "__main__":441 uvicorn.run(app, host="0.0.0.0", port=7860, log_level="warning")