""" Claude Chat API Routes Provides endpoints for AI-powered chat within the Atomizer dashboard. """ from fastapi import APIRouter, HTTPException, WebSocket, WebSocketDisconnect from fastapi.responses import StreamingResponse from pydantic import BaseModel from typing import Optional, List, Dict, Any import json import asyncio import os router = APIRouter() # Check for API key ANTHROPIC_API_KEY = os.environ.get("ANTHROPIC_API_KEY") class ChatMessage(BaseModel): role: str # "user" or "assistant" content: str class ChatRequest(BaseModel): message: str study_id: Optional[str] = None conversation_history: Optional[List[Dict[str, Any]]] = None class ChatResponse(BaseModel): response: str tool_calls: Optional[List[Dict[str, Any]]] = None study_id: Optional[str] = None # Store active conversations (in production, use Redis or database) _conversations: Dict[str, List[Dict[str, Any]]] = {} @router.get("/status") async def get_claude_status(): """ Check if Claude API is configured and available Returns: JSON with API status """ has_key = bool(ANTHROPIC_API_KEY) return { "available": has_key, "message": "Claude API is configured" if has_key else "ANTHROPIC_API_KEY not set" } @router.post("/chat", response_model=ChatResponse) async def chat_with_claude(request: ChatRequest): """ Send a message to Claude with Atomizer context Args: request: ChatRequest with message, optional study_id, and conversation history Returns: ChatResponse with Claude's response and any tool calls made """ if not ANTHROPIC_API_KEY: raise HTTPException( status_code=503, detail="Claude API not configured. Set ANTHROPIC_API_KEY environment variable." ) try: # Import here to avoid issues if anthropic not installed from api.services.claude_agent import AtomizerClaudeAgent # Create agent with study context agent = AtomizerClaudeAgent(study_id=request.study_id) # Convert conversation history format if needed history = [] if request.conversation_history: for msg in request.conversation_history: if isinstance(msg.get('content'), str): history.append(msg) # Skip complex message formats for simplicity # Get response result = await agent.chat(request.message, history) return ChatResponse( response=result["response"], tool_calls=result.get("tool_calls"), study_id=request.study_id ) except ImportError as e: raise HTTPException( status_code=503, detail=f"Anthropic SDK not installed: {str(e)}" ) except Exception as e: raise HTTPException( status_code=500, detail=f"Chat error: {str(e)}" ) @router.post("/chat/stream") async def chat_stream(request: ChatRequest): """ Stream a response from Claude token by token Args: request: ChatRequest with message and optional context Returns: StreamingResponse with text/event-stream """ if not ANTHROPIC_API_KEY: raise HTTPException( status_code=503, detail="Claude API not configured. Set ANTHROPIC_API_KEY environment variable." ) async def generate(): try: from api.services.claude_agent import AtomizerClaudeAgent agent = AtomizerClaudeAgent(study_id=request.study_id) # Convert history history = [] if request.conversation_history: for msg in request.conversation_history: if isinstance(msg.get('content'), str): history.append(msg) # Stream response async for token in agent.chat_stream(request.message, history): yield f"data: {json.dumps({'token': token})}\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( generate(), media_type="text/event-stream", headers={ "Cache-Control": "no-cache", "Connection": "keep-alive", } ) @router.websocket("/chat/ws") async def websocket_chat(websocket: WebSocket): """ WebSocket endpoint for real-time chat Message format (client -> server): {"type": "message", "content": "user message", "study_id": "optional"} Message format (server -> client): {"type": "token", "content": "..."} {"type": "done", "tool_calls": [...]} {"type": "error", "message": "..."} """ await websocket.accept() if not ANTHROPIC_API_KEY: await websocket.send_json({ "type": "error", "message": "Claude API not configured. Set ANTHROPIC_API_KEY environment variable." }) await websocket.close() return conversation_history = [] try: from api.services.claude_agent import AtomizerClaudeAgent while True: # Receive message from client data = await websocket.receive_json() if data.get("type") == "message": content = data.get("content", "") study_id = data.get("study_id") if not content: continue # Create agent agent = AtomizerClaudeAgent(study_id=study_id) try: # Use non-streaming chat for tool support result = await agent.chat(content, conversation_history) # Send response await websocket.send_json({ "type": "response", "content": result["response"], "tool_calls": result.get("tool_calls", []) }) # Update history (simplified - just user/assistant text) conversation_history.append({"role": "user", "content": content}) conversation_history.append({"role": "assistant", "content": result["response"]}) except Exception as e: await websocket.send_json({ "type": "error", "message": str(e) }) elif data.get("type") == "clear": # Clear conversation history conversation_history = [] await websocket.send_json({"type": "cleared"}) except WebSocketDisconnect: pass except Exception as e: try: await websocket.send_json({ "type": "error", "message": str(e) }) except: pass @router.get("/suggestions") async def get_chat_suggestions(study_id: Optional[str] = None): """ Get contextual chat suggestions based on current study Args: study_id: Optional study to get suggestions for Returns: List of suggested prompts """ base_suggestions = [ "What's the status of my optimization?", "Show me the best designs found", "Compare the top 3 trials", "What parameters have the most impact?", "Explain the convergence behavior" ] if study_id: # Add study-specific suggestions return { "suggestions": [ f"Summarize the {study_id} study", "What's the current best objective value?", "Are there any failed trials? Why?", "Show parameter sensitivity analysis", "What should I try next to improve results?" ] + base_suggestions[:3] } return { "suggestions": [ "List all available studies", "Help me create a new study", "What can you help me with?" ] + base_suggestions[:3] }