template
This commit is contained in:
10
backend/routers/__init__.py
Normal file
10
backend/routers/__init__.py
Normal 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
171
backend/routers/ai.py
Normal 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
62
backend/routers/health.py
Normal 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
191
backend/routers/projects.py
Normal 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))
|
||||
Reference in New Issue
Block a user