Spaces:
Sleeping
Sleeping
| from fastapi import FastAPI, File, UploadFile, HTTPException | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from fastapi.staticfiles import StaticFiles | |
| from fastapi.responses import FileResponse, StreamingResponse | |
| import os | |
| import tempfile | |
| from dotenv import load_dotenv | |
| from pydantic import BaseModel | |
| from typing import Optional, List | |
| import anthropic | |
| import json | |
| # Load environment variables | |
| load_dotenv() | |
| # Load prompts from files | |
| def load_prompt(filename): | |
| """Load prompt from text file in prompts directory""" | |
| try: | |
| prompts_dir = os.path.join(os.path.dirname(__file__), "prompts") | |
| file_path = os.path.join(prompts_dir, filename) | |
| with open(file_path, 'r', encoding='utf-8') as f: | |
| return f.read().strip() | |
| except FileNotFoundError: | |
| print(f"Warning: Prompt file {filename} not found. Using fallback.") | |
| return "" | |
| def load_document(filename): | |
| """Load document from text file in documents directory""" | |
| try: | |
| documents_dir = os.path.join(os.path.dirname(__file__), "documents") | |
| file_path = os.path.join(documents_dir, filename) | |
| with open(file_path, 'r', encoding='utf-8') as f: | |
| return f.read().strip() | |
| except FileNotFoundError: | |
| print(f"Warning: Document file {filename} not found. Using fallback.") | |
| return "" | |
| # Load prompts at startup | |
| SYSTEM_PROMPT_TEMPLATE = load_prompt("system_prompt.txt") | |
| TRANSITION_PROMPT_TEMPLATE = load_prompt("transition_prompt.txt") | |
| DOCUMENT = load_document("cian.txt") | |
| USER_GOAL = "More specifically, I want to understand the architecture of a transformer, why it works, and why it was designed that way. Pages 3, 4, 5, 6 are quite mysterious to me." | |
| app = FastAPI() | |
| # Enable CORS | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| def test(): | |
| return {"status": "OK", "app": "SokratesAI"} | |
| class ChatMessage(BaseModel): | |
| role: str | |
| content: str | |
| id: Optional[str] = None | |
| class ChatRequest(BaseModel): | |
| messages: List[ChatMessage] | |
| chunk: Optional[str] = None # Legacy support | |
| currentChunk: Optional[str] = None | |
| nextChunk: Optional[str] = None | |
| action: Optional[str] = None # 'skip', 'understood', or None | |
| document: Optional[str] = None | |
| user_goal: Optional[str] = None # User's goal for the chat, if applicable | |
| async def chat_endpoint(request: ChatRequest): | |
| print(f"💬 Received chat with {len(request.messages)} messages, action: {request.action}") | |
| # Use new format if available, otherwise fall back to legacy | |
| current_chunk = request.currentChunk or request.chunk or "No specific chunk provided" | |
| next_chunk = request.nextChunk or "" | |
| action = request.action | |
| user_goal = request.user_goal or USER_GOAL | |
| # Only include full document on first message or transitions to provide initial context | |
| include_document = len(request.messages) <= 1 or action in ['skip', 'understood'] | |
| document = DOCUMENT if include_document else "" | |
| # Create system prompt for research paper tutor with transition support | |
| is_transition = action in ['skip', 'understood'] | |
| if is_transition: | |
| system_prompt = TRANSITION_PROMPT_TEMPLATE.format( | |
| action=action, | |
| current_chunk=current_chunk, | |
| next_chunk=next_chunk, | |
| document=document, | |
| ) | |
| else: | |
| system_prompt = SYSTEM_PROMPT_TEMPLATE.format( | |
| current_chunk=current_chunk, | |
| document=document, | |
| user_goal=user_goal or "No specific goal provided" | |
| ) | |
| anthropic_api_key = os.environ.get("ANTHROPIC_API_KEY") | |
| if not anthropic_api_key: | |
| return {"role": "assistant", "content": "I'm sorry, but the chat service is not configured. Please check the API key configuration."} | |
| try: | |
| client = anthropic.Anthropic(api_key=anthropic_api_key) | |
| if not request.messages: | |
| # No conversation yet — assistant should speak first | |
| anthropic_messages = [ | |
| {"role": "user", "content": "Please start the conversation based on the provided context."} | |
| ] | |
| else: | |
| anthropic_messages = [ | |
| {"role": msg.role, "content": msg.content} | |
| for msg in request.messages | |
| if msg.role in ["user", "assistant"] | |
| ] | |
| # For transitions, add a dummy user message to trigger Claude response | |
| if not any(msg["role"] == "user" for msg in anthropic_messages): | |
| if is_transition: | |
| anthropic_messages.append({"role": "user", "content": "Please continue to the next section."}) | |
| else: | |
| return {"role": "assistant", "content": "I didn't receive your message. Could you please ask again?"} | |
| print("🤖 Calling Claude for chat response...") | |
| response = client.messages.create( | |
| model="claude-3-5-haiku-latest", | |
| max_tokens=10000, | |
| system=system_prompt, # system prompt here | |
| messages=anthropic_messages, | |
| ) | |
| response_text = response.content[0].text | |
| print(f"✅ Received response from Claude: {response_text[:100]}...") | |
| return {"role": "assistant", "content": response_text} | |
| except Exception as e: | |
| print(f"❌ Error in chat endpoint: {e}") | |
| return {"role": "assistant", "content": f"I'm sorry, I encountered an error: {str(e)}. Please try again."} | |
| async def upload_pdf(file: UploadFile = File(...)): | |
| """Simple PDF upload endpoint that saves the file locally""" | |
| print(f"📄 Uploading file: {file.filename}") | |
| try: | |
| # Read PDF bytes | |
| file_bytes = await file.read() | |
| print(f"📊 File size: {len(file_bytes)} bytes") | |
| # Create temporary file to save PDF | |
| with tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") as temp_file: | |
| temp_file.write(file_bytes) | |
| temp_file_path = temp_file.name | |
| print(f"✅ PDF saved to: {temp_file_path}") | |
| return { | |
| "message": "PDF uploaded successfully!", | |
| "file_path": temp_file_path, | |
| "filename": file.filename, | |
| "status": "uploaded", | |
| "size": len(file_bytes) | |
| } | |
| except Exception as e: | |
| print(f"❌ Error uploading PDF: {e}") | |
| raise HTTPException(status_code=500, detail=f"PDF upload error: {str(e)}") | |
| async def chat_stream(request: ChatRequest): | |
| """Streaming chat endpoint for continuous conversation""" | |
| print(f"💬 Received chat with {len(request.messages)} messages, action: {request.action}") | |
| # Use new format if available, otherwise fall back to legacy | |
| current_chunk = request.currentChunk or request.chunk or "No specific chunk provided" | |
| next_chunk = request.nextChunk or "" | |
| action = request.action | |
| user_goal = request.user_goal or USER_GOAL | |
| # Only include full document on first message or transitions to provide initial context | |
| # After that, the conversation history maintains context | |
| document = DOCUMENT | |
| # Create system prompt for research paper tutor with transition support | |
| is_transition = action in ['skip', 'understood'] | |
| print("🤖 Creating system prompt...") | |
| print(f"current_chunk: {current_chunk[:100] if current_chunk else 'None'}") | |
| print(f"next_chunk: {next_chunk[:100] if next_chunk else 'None'}") | |
| print(f"user_goal: {user_goal if user_goal else 'None'}") | |
| if is_transition: | |
| system_prompt = TRANSITION_PROMPT_TEMPLATE.format( | |
| action=action, | |
| current_chunk=current_chunk, | |
| next_chunk=next_chunk, | |
| ) | |
| print(f"Transition system prompt: {system_prompt[:200]}...") | |
| else: | |
| system_prompt = SYSTEM_PROMPT_TEMPLATE.format( | |
| current_chunk=current_chunk, | |
| document=document, | |
| user_goal=user_goal or "No specific goal provided" | |
| ) | |
| print(f"System prompt: {system_prompt[:200]}...") | |
| anthropic_api_key = os.environ.get("ANTHROPIC_API_KEY") | |
| if not anthropic_api_key: | |
| return {"role": "assistant", "content": "I'm sorry, but the chat service is not configured. Please check the API key configuration."} | |
| client = anthropic.Anthropic(api_key=anthropic_api_key) | |
| if not request.messages: | |
| # No conversation yet — assistant should speak first | |
| anthropic_messages = [ | |
| {"role": "user", "content": "Please start the conversation based on the provided context."} | |
| ] | |
| else: | |
| anthropic_messages = [ | |
| {"role": msg.role, "content": msg.content} | |
| for msg in request.messages | |
| if msg.role in ["user", "assistant"] | |
| ] | |
| print(anthropic_messages) | |
| # Ensure the conversation ends with a user message for thinking mode | |
| if anthropic_messages and anthropic_messages[-1]["role"] == "assistant": | |
| if is_transition: | |
| anthropic_messages.append({"role": "user", "content": "Please continue to the next section."}) | |
| else: | |
| anthropic_messages.append({"role": "user", "content": "Please continue."}) | |
| elif not any(msg["role"] == "user" for msg in anthropic_messages): | |
| if is_transition: | |
| anthropic_messages.append({"role": "user", "content": "Please continue to the next section."}) | |
| else: | |
| def generate_error(): | |
| yield f"data: {json.dumps({'error': 'I did not receive your message. Could you please ask again?'})}\n\n" | |
| return StreamingResponse( | |
| media_type="text/event-stream", | |
| content=generate_error(), | |
| headers={"Cache-Control": "no-cache", | |
| "Connection": "keep-alive", | |
| "Access-Control-Allow-Origin": "*"}, | |
| ) | |
| def generate(): | |
| try: | |
| with client.messages.stream( | |
| model="claude-sonnet-4-20250514", | |
| max_tokens=10000, | |
| system=system_prompt, # system prompt here | |
| thinking={ | |
| "type": "enabled", | |
| "budget_tokens": 1024, | |
| }, | |
| messages=anthropic_messages, | |
| ) as stream: | |
| for text in stream.text_stream: | |
| print(f"Raw text chunk: {repr(text)}") | |
| yield f"data: {json.dumps(text)}\n\n" | |
| yield f"data: {json.dumps({'done': True})}\n\n" | |
| except Exception as e: | |
| yield f"data: {json.dumps({'error': str(e)})}\n\n" | |
| return StreamingResponse( | |
| media_type="text/event_stream", | |
| content=generate(), | |
| headers={"Cache-Control": "no-cache", | |
| "Connection": "keep-alive", | |
| "Access-Control-Allow-Origin": "*"}, | |
| ) | |
| # Mount static files for production deployment | |
| frontend_path = os.path.join(os.path.dirname(__file__), "..", "frontend") | |
| assets_path = os.path.join(frontend_path, "assets") | |
| if os.path.exists(frontend_path): | |
| # Only mount assets if the directory exists (production build) | |
| if os.path.exists(assets_path): | |
| app.mount("/assets", StaticFiles(directory=assets_path), name="assets") | |
| # Serve other static files from frontend root (like pdf.worker.min.js) | |
| async def serve_pdf_worker(): | |
| pdf_worker_path = os.path.join(frontend_path, "pdf.worker.min.js") | |
| if os.path.exists(pdf_worker_path): | |
| return FileResponse(pdf_worker_path) | |
| raise HTTPException(status_code=404, detail="PDF worker not found") | |
| async def serve_frontend(): | |
| index_path = os.path.join(frontend_path, "index.html") | |
| if os.path.exists(index_path): | |
| return FileResponse(index_path) | |
| return {"message": "Backend is running - frontend not found"} | |
| else: | |
| def hello(): | |
| return {"message": "Backend is running!"} |