This commit is contained in:
2026-02-03 21:58:25 +01:00
parent 3019bcaf1a
commit 959dad3fec
14 changed files with 1477 additions and 1 deletions

View File

@@ -0,0 +1,10 @@
"""
Routers Package
This package contains all API route handlers organized by feature.
Each router is responsible for a specific domain of the application.
"""
from . import ai, projects, health
__all__ = ["ai", "projects", "health"]

171
backend/routers/ai.py Normal file
View File

@@ -0,0 +1,171 @@
"""
=============================================================================
AI ROUTER
=============================================================================
This router handles all AI-related endpoints.
Features:
- Text generation with OpenAI or Anthropic
- Streaming responses for real-time output
- Multiple AI models support
Architecture:
- Endpoints are thin - they just validate input and return output
- Business logic is in the services module
- This separation makes testing easier
"""
from fastapi import APIRouter, HTTPException
from fastapi.responses import StreamingResponse
from pydantic import BaseModel, Field
from typing import Optional
# Import our AI service
from services.ai_service import generate_text, stream_text, chat_completion
router = APIRouter()
# =============================================================================
# REQUEST/RESPONSE MODELS
# =============================================================================
class GenerateRequest(BaseModel):
"""Request body for text generation."""
prompt: str = Field(..., min_length=1, max_length=10000)
max_tokens: Optional[int] = Field(default=500, ge=1, le=4000)
temperature: Optional[float] = Field(default=0.7, ge=0, le=2)
class GenerateResponse(BaseModel):
"""Response from text generation."""
text: str
model: str
usage: Optional[dict] = None
class ChatMessage(BaseModel):
"""A single message in a chat conversation."""
role: str = Field(..., pattern="^(user|assistant|system)$")
content: str = Field(..., min_length=1)
class ChatRequest(BaseModel):
"""Request body for chat completion."""
messages: list[ChatMessage] = Field(..., min_items=1)
max_tokens: Optional[int] = Field(default=500, ge=1, le=4000)
temperature: Optional[float] = Field(default=0.7, ge=0, le=2)
class ChatResponse(BaseModel):
"""Response from chat completion."""
message: ChatMessage
model: str
# =============================================================================
# ENDPOINTS
# =============================================================================
@router.post("/generate", response_model=GenerateResponse)
async def generate_endpoint(request: GenerateRequest):
"""
Generate text from a prompt.
This is a simple completion endpoint - give it a prompt,
get back generated text.
Example:
POST /api/ai/generate
{"prompt": "Write a haiku about coding"}
"""
try:
result = await generate_text(
prompt=request.prompt,
max_tokens=request.max_tokens,
temperature=request.temperature,
)
return GenerateResponse(**result)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/chat", response_model=ChatResponse)
async def chat_endpoint(request: ChatRequest):
"""
Chat completion with message history.
Send a list of messages (conversation history) and get
the assistant's response.
Example:
POST /api/ai/chat
{
"messages": [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Hello!"}
]
}
"""
try:
# Convert Pydantic models to dicts
messages = [m.model_dump() for m in request.messages]
result = await chat_completion(
messages=messages,
max_tokens=request.max_tokens,
temperature=request.temperature,
)
return ChatResponse(
message=ChatMessage(**result["message"]),
model=result["model"],
)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/stream")
async def stream_endpoint(request: GenerateRequest):
"""
Stream generated text in real-time.
Uses Server-Sent Events (SSE) to stream the response
token by token. Great for chat interfaces!
The frontend can read this with:
const response = await fetch("/api/ai/stream", {...});
const reader = response.body.getReader();
"""
try:
return StreamingResponse(
stream_text(
prompt=request.prompt,
max_tokens=request.max_tokens,
temperature=request.temperature,
),
media_type="text/event-stream",
)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/models")
async def list_models():
"""
List available AI models.
Useful for letting users choose which model to use.
"""
return {
"openai": [
{"id": "gpt-4-turbo", "name": "GPT-4 Turbo", "description": "Most capable model"},
{"id": "gpt-4", "name": "GPT-4", "description": "High quality responses"},
{"id": "gpt-3.5-turbo", "name": "GPT-3.5 Turbo", "description": "Fast and efficient"},
],
"anthropic": [
{"id": "claude-3-opus-20240229", "name": "Claude 3 Opus", "description": "Most capable"},
{"id": "claude-3-sonnet-20240229", "name": "Claude 3 Sonnet", "description": "Balanced"},
{"id": "claude-3-haiku-20240307", "name": "Claude 3 Haiku", "description": "Fast"},
],
}

62
backend/routers/health.py Normal file
View File

@@ -0,0 +1,62 @@
"""
=============================================================================
HEALTH CHECK ROUTER
=============================================================================
Simple health check endpoints for monitoring and deployment.
These are useful for:
- Load balancer health checks
- Kubernetes readiness probes
- Monitoring systems
"""
from fastapi import APIRouter
from config import settings
router = APIRouter()
@router.get("/health")
async def health_check():
"""
Basic health check endpoint.
Returns 200 if the server is running.
"""
return {
"status": "healthy",
"environment": settings.ENVIRONMENT,
}
@router.get("/health/detailed")
async def detailed_health_check():
"""
Detailed health check with service status.
Useful for debugging connection issues.
"""
# Check AI provider configuration
ai_configured = bool(
settings.OPENAI_API_KEY or settings.ANTHROPIC_API_KEY
)
# Check Supabase configuration
supabase_configured = bool(
settings.SUPABASE_URL and settings.SUPABASE_SERVICE_KEY
)
return {
"status": "healthy",
"environment": settings.ENVIRONMENT,
"services": {
"ai": {
"configured": ai_configured,
"provider": settings.AI_PROVIDER,
"model": settings.AI_MODEL,
},
"database": {
"configured": supabase_configured,
"provider": "supabase",
},
},
}

191
backend/routers/projects.py Normal file
View File

@@ -0,0 +1,191 @@
"""
=============================================================================
PROJECTS ROUTER
=============================================================================
Example CRUD endpoints for managing projects.
This demonstrates:
- Supabase database integration
- CRUD operations (Create, Read, Update, Delete)
- Request validation with Pydantic
- Error handling patterns
You can adapt this pattern for any resource in your app.
"""
from fastapi import APIRouter, HTTPException, Header
from pydantic import BaseModel, Field
from typing import Optional
from datetime import datetime
# Import Supabase client
from services.database import get_supabase_client
router = APIRouter()
# =============================================================================
# REQUEST/RESPONSE MODELS
# =============================================================================
class ProjectCreate(BaseModel):
"""Data required to create a new project."""
name: str = Field(..., min_length=1, max_length=100)
description: Optional[str] = Field(default="", max_length=1000)
class ProjectUpdate(BaseModel):
"""Data that can be updated on a project."""
name: Optional[str] = Field(None, min_length=1, max_length=100)
description: Optional[str] = Field(None, max_length=1000)
class Project(BaseModel):
"""Full project model (database representation)."""
id: str
name: str
description: str
user_id: str
created_at: datetime
# =============================================================================
# ENDPOINTS
# =============================================================================
@router.get("")
async def list_projects(
authorization: str = Header(None, description="Bearer token from Supabase")
):
"""
List all projects for the authenticated user.
Requires authentication - pass the Supabase access token
in the Authorization header.
"""
if not authorization:
raise HTTPException(status_code=401, detail="Authorization required")
try:
# Get Supabase client
supabase = get_supabase_client()
# Extract user from token (Supabase handles this)
# In production, you'd verify the JWT properly
# Query projects
response = supabase.table("projects").select("*").execute()
return response.data
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("", response_model=Project)
async def create_project(
project: ProjectCreate,
authorization: str = Header(None),
):
"""
Create a new project.
Example:
POST /api/projects
{"name": "My AI App", "description": "An awesome AI project"}
"""
if not authorization:
raise HTTPException(status_code=401, detail="Authorization required")
try:
supabase = get_supabase_client()
# In production, extract user_id from JWT
# For demo, we'll use a placeholder
user_id = "demo-user-id"
response = supabase.table("projects").insert({
"name": project.name,
"description": project.description or "",
"user_id": user_id,
}).execute()
return response.data[0]
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/{project_id}", response_model=Project)
async def get_project(project_id: str):
"""
Get a specific project by ID.
"""
try:
supabase = get_supabase_client()
response = supabase.table("projects").select("*").eq("id", project_id).single().execute()
if not response.data:
raise HTTPException(status_code=404, detail="Project not found")
return response.data
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.patch("/{project_id}", response_model=Project)
async def update_project(
project_id: str,
project: ProjectUpdate,
authorization: str = Header(None),
):
"""
Update a project.
Only send the fields you want to update.
"""
if not authorization:
raise HTTPException(status_code=401, detail="Authorization required")
try:
supabase = get_supabase_client()
# Build update dict with only provided fields
update_data = project.model_dump(exclude_unset=True)
if not update_data:
raise HTTPException(status_code=400, detail="No fields to update")
response = supabase.table("projects").update(update_data).eq("id", project_id).execute()
if not response.data:
raise HTTPException(status_code=404, detail="Project not found")
return response.data[0]
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.delete("/{project_id}")
async def delete_project(
project_id: str,
authorization: str = Header(None),
):
"""
Delete a project.
"""
if not authorization:
raise HTTPException(status_code=401, detail="Authorization required")
try:
supabase = get_supabase_client()
response = supabase.table("projects").delete().eq("id", project_id).execute()
return {"message": "Project deleted", "id": project_id}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))