Enhanced API Swagger documentation and improved web interface navigation with dropdown menus and better organization. API Changes (api/main.py): ========================== - Enhanced FastAPI app description with architecture diagram - Added detailed rate limiting information - Added server configurations (production + local) - Added contact and license information - Enhanced all endpoint descriptions with: * Detailed parameter descriptions * Response descriptions * Error responses * Rate limit information * Usage examples - Added Field descriptions to all Pydantic models - Added schema examples for better Swagger UI - Enhanced LLM endpoints with AI rate limiting details - Added status codes (201, 404, 429, 500) to endpoints - Improved startup message with docs URLs Swagger UI Improvements: - Better organized endpoint groups (Root, Health, Items, Users, LLM) - Detailed request/response schemas - Interactive examples for all endpoints - Rate limiting documentation - Architecture overview in description Web Changes (web/templates/base.html): ====================================== - Added dropdown menu for API documentation with: * API Root (/) * Swagger UI (/docs) * ReDoc (/redoc) * OpenAPI JSON (/openapi.json) - Added emoji icons to all menu items for better UX - Added tooltips (title attributes) to all links - Renamed "API Config" to "Settings" for clarity - Added CSS for dropdown menu functionality - Improved footer text - Better visual hierarchy with icons Navigation Menu: - 🏠 Home - 📦 Items - 👥 Users - 🤖 LLM Chat - 📚 API Docs (dropdown with 4 options) - ⚙️ Settings All endpoints now have comprehensive documentation visible in Swagger UI at https://commandware.it/api/docs 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
504 lines
15 KiB
Python
504 lines
15 KiB
Python
from typing import List, Optional
|
|
from pydantic import BaseModel, Field
|
|
import uvicorn
|
|
from datetime import datetime
|
|
from fastapi import FastAPI, HTTPException, status
|
|
from fastapi.responses import JSONResponse
|
|
from pydantic import BaseModel
|
|
from typing import Optional, List
|
|
import os
|
|
import httpx
|
|
|
|
# OpenAI API configuration
|
|
OPENAI_API_BASE = os.getenv("OPENAI_API_BASE", "http://localhost/api")
|
|
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "your-api-key")
|
|
DEFAULT_MODEL = os.getenv("DEFAULT_LLM_MODEL", "your-model-id")
|
|
|
|
app = FastAPI(
|
|
title="API7 Enterprise Demo API",
|
|
description="""
|
|
## API7 Enterprise Demo Application
|
|
|
|
This API demonstrates the capabilities of API7 Enterprise Gateway with:
|
|
|
|
* **CRUD Operations** - Items and Users management
|
|
* **LLM Integration** - AI-powered chat with rate limiting
|
|
* **Health Checks** - Kubernetes-ready endpoints
|
|
* **Rate Limiting** - Standard (100 req/min) and AI-based (100 tokens/min)
|
|
* **CORS** - Cross-origin resource sharing enabled
|
|
* **Proxy Rewrite** - Automatic /api prefix removal by API7 Gateway
|
|
|
|
### Architecture
|
|
|
|
\`\`\`
|
|
Client → Ingress (NGINX) → API7 Gateway → Backend API
|
|
↓
|
|
• Rate Limiting
|
|
• CORS
|
|
• Proxy Rewrite (/api → /)
|
|
• Service Discovery
|
|
\`\`\`
|
|
|
|
### Rate Limiting
|
|
|
|
- **Standard API** (\`/items\`, \`/users\`): 100 requests per 60 seconds per IP
|
|
- **LLM API** (\`/llm/*\`): 100 tokens per 60 seconds (AI-based rate limiting)
|
|
|
|
### Documentation
|
|
|
|
- **Swagger UI**: [/docs](/docs)
|
|
- **ReDoc**: [/redoc](/redoc)
|
|
- **OpenAPI JSON**: [/openapi.json](/openapi.json)
|
|
""",
|
|
version="1.0.0",
|
|
contact={
|
|
"name": "CommandWare",
|
|
"url": "https://commandware.it",
|
|
},
|
|
license_info={
|
|
"name": "MIT",
|
|
},
|
|
servers=[
|
|
{
|
|
"url": "https://commandware.it/api",
|
|
"description": "Production server (via API7 Gateway)"
|
|
},
|
|
{
|
|
"url": "http://localhost:8080",
|
|
"description": "Local development server"
|
|
}
|
|
]
|
|
)
|
|
|
|
# Models
|
|
class Item(BaseModel):
|
|
id: Optional[int] = Field(None, description="Item ID (auto-generated)")
|
|
name: str = Field(..., description="Item name", example="Laptop")
|
|
description: Optional[str] = Field(None, description="Item description", example="High-performance laptop")
|
|
price: float = Field(..., description="Item price", example=999.99, gt=0)
|
|
in_stock: bool = Field(True, description="Stock availability")
|
|
|
|
class Config:
|
|
schema_extra = {
|
|
"example": {
|
|
"name": "Laptop",
|
|
"description": "High-performance laptop",
|
|
"price": 999.99,
|
|
"in_stock": True
|
|
}
|
|
}
|
|
|
|
class User(BaseModel):
|
|
id: Optional[int] = Field(None, description="User ID (auto-generated)")
|
|
username: str = Field(..., description="Username", example="john_doe", min_length=3)
|
|
email: str = Field(..., description="Email address", example="john@example.com")
|
|
active: bool = Field(True, description="User active status")
|
|
|
|
class Config:
|
|
schema_extra = {
|
|
"example": {
|
|
"username": "john_doe",
|
|
"email": "john@example.com",
|
|
"active": True
|
|
}
|
|
}
|
|
|
|
# In-memory storage
|
|
items_db = [
|
|
{"id": 1, "name": "Laptop", "description": "High-performance laptop", "price": 999.99, "in_stock": True},
|
|
{"id": 2, "name": "Mouse", "description": "Wireless mouse", "price": 29.99, "in_stock": True},
|
|
{"id": 3, "name": "Keyboard", "description": "Mechanical keyboard", "price": 79.99, "in_stock": False},
|
|
]
|
|
|
|
users_db = [
|
|
{"id": 1, "username": "john_doe", "email": "john@example.com", "active": True},
|
|
{"id": 2, "username": "jane_smith", "email": "jane@example.com", "active": True},
|
|
]
|
|
|
|
# Root endpoint
|
|
@app.get(
|
|
"/",
|
|
tags=["Root"],
|
|
summary="API Information",
|
|
response_description="API metadata and links"
|
|
)
|
|
async def root():
|
|
"""
|
|
**Root endpoint** with API information and navigation links.
|
|
|
|
Returns API version, documentation links, and current timestamp.
|
|
"""
|
|
return {
|
|
"message": "Welcome to API7 Enterprise Demo API",
|
|
"version": "1.0.0",
|
|
"documentation": {
|
|
"swagger": "/docs",
|
|
"redoc": "/redoc",
|
|
"openapi": "/openapi.json"
|
|
},
|
|
"endpoints": {
|
|
"items": "/items",
|
|
"users": "/users",
|
|
"llm": "/llm"
|
|
},
|
|
"timestamp": datetime.now().isoformat()
|
|
}
|
|
|
|
# Health check
|
|
@app.get(
|
|
"/health",
|
|
tags=["Health"],
|
|
summary="Health Check",
|
|
response_description="Service health status"
|
|
)
|
|
async def health():
|
|
"""
|
|
**Health check endpoint** for Kubernetes liveness probe.
|
|
|
|
Returns service health status and timestamp.
|
|
"""
|
|
return {
|
|
"status": "healthy",
|
|
"service": "api",
|
|
"timestamp": datetime.now().isoformat()
|
|
}
|
|
|
|
# Readiness check
|
|
@app.get(
|
|
"/ready",
|
|
tags=["Health"],
|
|
summary="Readiness Check",
|
|
response_description="Service readiness status"
|
|
)
|
|
async def ready():
|
|
"""
|
|
**Readiness check endpoint** for Kubernetes readiness probe.
|
|
|
|
Returns service readiness status and timestamp.
|
|
"""
|
|
return {
|
|
"status": "ready",
|
|
"service": "api",
|
|
"timestamp": datetime.now().isoformat()
|
|
}
|
|
|
|
# Items endpoints
|
|
@app.get(
|
|
"/items",
|
|
response_model=List[Item],
|
|
tags=["Items"],
|
|
summary="List all items",
|
|
response_description="List of all items in inventory"
|
|
)
|
|
async def get_items():
|
|
"""
|
|
**Get all items** from the inventory.
|
|
|
|
Returns a list of all available items with their details.
|
|
|
|
**Rate Limit**: 100 requests per 60 seconds per IP (via API7 Gateway)
|
|
"""
|
|
return items_db
|
|
|
|
@app.get(
|
|
"/items/{item_id}",
|
|
response_model=Item,
|
|
tags=["Items"],
|
|
summary="Get item by ID",
|
|
response_description="Item details",
|
|
responses={404: {"description": "Item not found"}}
|
|
)
|
|
async def get_item(item_id: int):
|
|
"""
|
|
**Get a specific item** by ID.
|
|
|
|
- **item_id**: The ID of the item to retrieve
|
|
|
|
Returns item details if found, otherwise 404 error.
|
|
"""
|
|
item = next((item for item in items_db if item["id"] == item_id), None)
|
|
if item is None:
|
|
raise HTTPException(status_code=404, detail="Item not found")
|
|
return item
|
|
|
|
@app.post(
|
|
"/items",
|
|
response_model=Item,
|
|
tags=["Items"],
|
|
summary="Create new item",
|
|
status_code=status.HTTP_201_CREATED,
|
|
response_description="Created item with auto-generated ID"
|
|
)
|
|
async def create_item(item: Item):
|
|
"""
|
|
**Create a new item** in the inventory.
|
|
|
|
The ID will be auto-generated. Provide:
|
|
- **name**: Item name (required)
|
|
- **description**: Item description (optional)
|
|
- **price**: Item price (required, must be > 0)
|
|
- **in_stock**: Stock availability (default: true)
|
|
"""
|
|
new_id = max([i["id"] for i in items_db]) + 1 if items_db else 1
|
|
item_dict = item.dict()
|
|
item_dict["id"] = new_id
|
|
items_db.append(item_dict)
|
|
return item_dict
|
|
|
|
@app.put(
|
|
"/items/{item_id}",
|
|
response_model=Item,
|
|
tags=["Items"],
|
|
summary="Update item",
|
|
response_description="Updated item",
|
|
responses={404: {"description": "Item not found"}}
|
|
)
|
|
async def update_item(item_id: int, item: Item):
|
|
"""
|
|
**Update an existing item** by ID.
|
|
|
|
- **item_id**: The ID of the item to update
|
|
- Provide updated item data (name, description, price, in_stock)
|
|
"""
|
|
for idx, existing_item in enumerate(items_db):
|
|
if existing_item["id"] == item_id:
|
|
item_dict = item.dict()
|
|
item_dict["id"] = item_id
|
|
items_db[idx] = item_dict
|
|
return item_dict
|
|
raise HTTPException(status_code=404, detail="Item not found")
|
|
|
|
@app.delete(
|
|
"/items/{item_id}",
|
|
tags=["Items"],
|
|
summary="Delete item",
|
|
status_code=status.HTTP_200_OK,
|
|
response_description="Deletion confirmation",
|
|
responses={404: {"description": "Item not found"}}
|
|
)
|
|
async def delete_item(item_id: int):
|
|
"""
|
|
**Delete an item** from the inventory.
|
|
|
|
- **item_id**: The ID of the item to delete
|
|
|
|
Returns confirmation message if successful.
|
|
"""
|
|
for idx, item in enumerate(items_db):
|
|
if item["id"] == item_id:
|
|
items_db.pop(idx)
|
|
return {"message": "Item deleted successfully", "id": item_id}
|
|
raise HTTPException(status_code=404, detail="Item not found")
|
|
|
|
# Users endpoints
|
|
@app.get(
|
|
"/users",
|
|
response_model=List[User],
|
|
tags=["Users"],
|
|
summary="List all users",
|
|
response_description="List of all users"
|
|
)
|
|
async def get_users():
|
|
"""
|
|
**Get all users** from the system.
|
|
|
|
Returns a list of all registered users.
|
|
|
|
**Rate Limit**: 100 requests per 60 seconds per IP (via API7 Gateway)
|
|
"""
|
|
return users_db
|
|
|
|
@app.get(
|
|
"/users/{user_id}",
|
|
response_model=User,
|
|
tags=["Users"],
|
|
summary="Get user by ID",
|
|
response_description="User details",
|
|
responses={404: {"description": "User not found"}}
|
|
)
|
|
async def get_user(user_id: int):
|
|
"""
|
|
**Get a specific user** by ID.
|
|
|
|
- **user_id**: The ID of the user to retrieve
|
|
|
|
Returns user details if found, otherwise 404 error.
|
|
"""
|
|
user = next((user for user in users_db if user["id"] == user_id), None)
|
|
if user is None:
|
|
raise HTTPException(status_code=404, detail="User not found")
|
|
return user
|
|
|
|
@app.post(
|
|
"/users",
|
|
response_model=User,
|
|
tags=["Users"],
|
|
summary="Create new user",
|
|
status_code=status.HTTP_201_CREATED,
|
|
response_description="Created user with auto-generated ID"
|
|
)
|
|
async def create_user(user: User):
|
|
"""
|
|
**Create a new user** in the system.
|
|
|
|
The ID will be auto-generated. Provide:
|
|
- **username**: Username (required, min 3 characters)
|
|
- **email**: Email address (required)
|
|
- **active**: User active status (default: true)
|
|
"""
|
|
new_id = max([u["id"] for u in users_db]) + 1 if users_db else 1
|
|
user_dict = user.dict()
|
|
user_dict["id"] = new_id
|
|
users_db.append(user_dict)
|
|
return user_dict
|
|
|
|
# LLM endpoints
|
|
class LLMRequest(BaseModel):
|
|
prompt: str = Field(..., description="The prompt to send to the LLM", example="What is API7 Enterprise?")
|
|
max_tokens: Optional[int] = Field(150, description="Maximum tokens in response", example=150, ge=1, le=4096)
|
|
temperature: Optional[float] = Field(0.7, description="Sampling temperature (0-2)", example=0.7, ge=0, le=2)
|
|
model: Optional[str] = Field(DEFAULT_MODEL, description="Model to use", example="videogame-expert")
|
|
|
|
class Config:
|
|
schema_extra = {
|
|
"example": {
|
|
"prompt": "What is API7 Enterprise?",
|
|
"max_tokens": 150,
|
|
"temperature": 0.7,
|
|
"model": "videogame-expert"
|
|
}
|
|
}
|
|
|
|
class LLMResponse(BaseModel):
|
|
response: str = Field(..., description="LLM generated response")
|
|
tokens_used: int = Field(..., description="Total tokens used")
|
|
model: str = Field(..., description="Model used for generation")
|
|
timestamp: str = Field(..., description="Response timestamp")
|
|
|
|
@app.post(
|
|
"/llm/chat",
|
|
response_model=LLMResponse,
|
|
tags=["LLM"],
|
|
summary="LLM Chat Completion",
|
|
response_description="LLM generated response",
|
|
responses={
|
|
429: {"description": "Rate limit exceeded (100 tokens per 60 seconds)"},
|
|
500: {"description": "LLM service error"}
|
|
}
|
|
)
|
|
async def llm_chat(request: LLMRequest):
|
|
"""
|
|
**LLM Chat endpoint** - connects to OpenAI-compatible API (Open WebUI).
|
|
|
|
This endpoint uses **AI-based rate limiting** via API7 Gateway:
|
|
- **Limit**: 100 tokens per 60 seconds
|
|
- **Strategy**: total_tokens (input + output)
|
|
- **Error**: HTTP 429 when limit exceeded
|
|
|
|
Provide:
|
|
- **prompt**: Your question or prompt
|
|
- **max_tokens**: Maximum response length (default: 150)
|
|
- **temperature**: Randomness level 0-2 (default: 0.7)
|
|
- **model**: Model to use (default: configured model)
|
|
"""
|
|
try:
|
|
async with httpx.AsyncClient() as client:
|
|
response = await client.post(
|
|
f"{OPENAI_API_BASE}/chat/completions",
|
|
headers={
|
|
"Authorization": f"Bearer {OPENAI_API_KEY}",
|
|
"Content-Type": "application/json"
|
|
},
|
|
json={
|
|
"model": request.model,
|
|
"messages": [
|
|
{"role": "user", "content": request.prompt}
|
|
],
|
|
"max_tokens": request.max_tokens,
|
|
"temperature": request.temperature
|
|
},
|
|
timeout=30.0
|
|
)
|
|
response.raise_for_status()
|
|
data = response.json()
|
|
|
|
# Extract response and token usage
|
|
llm_response = data["choices"][0]["message"]["content"]
|
|
tokens_used = data.get("usage", {}).get("total_tokens", 0)
|
|
|
|
return LLMResponse(
|
|
response=llm_response,
|
|
tokens_used=tokens_used,
|
|
model=request.model,
|
|
timestamp=datetime.now().isoformat()
|
|
)
|
|
except httpx.HTTPStatusError as e:
|
|
raise HTTPException(status_code=e.response.status_code, detail=f"OpenAI API error: {e.response.text}")
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=f"LLM service error: {str(e)}")
|
|
|
|
@app.get(
|
|
"/llm/models",
|
|
tags=["LLM"],
|
|
summary="List available LLM models",
|
|
response_description="List of available models"
|
|
)
|
|
async def list_llm_models():
|
|
"""
|
|
**List available LLM models**.
|
|
|
|
Returns the list of models available through the configured LLM provider (Open WebUI).
|
|
"""
|
|
return {
|
|
"models": [
|
|
{
|
|
"id": "videogame-expert",
|
|
"name": "Videogame Expert",
|
|
"max_tokens": 4096,
|
|
"provider": "Open WebUI",
|
|
"description": "Specialized model for videogame-related questions"
|
|
}
|
|
],
|
|
"default_model": DEFAULT_MODEL,
|
|
"provider": "Open WebUI",
|
|
"timestamp": datetime.now().isoformat()
|
|
}
|
|
|
|
@app.get(
|
|
"/llm/health",
|
|
tags=["LLM"],
|
|
summary="LLM service health check",
|
|
response_description="LLM service health status"
|
|
)
|
|
async def llm_health():
|
|
"""
|
|
**LLM service health check**.
|
|
|
|
Returns the health status of the LLM integration, including:
|
|
- Provider information
|
|
- Endpoint configuration
|
|
- Rate limit settings
|
|
"""
|
|
return {
|
|
"status": "healthy",
|
|
"service": "llm-api",
|
|
"provider": "Open WebUI",
|
|
"endpoint": OPENAI_API_BASE,
|
|
"default_model": DEFAULT_MODEL,
|
|
"rate_limit": {
|
|
"enabled": True,
|
|
"limit": 100,
|
|
"window": "60 seconds",
|
|
"strategy": "total_tokens",
|
|
"managed_by": "API7 Gateway (ai-rate-limiting plugin)"
|
|
},
|
|
"timestamp": datetime.now().isoformat()
|
|
}
|
|
|
|
if __name__ == "__main__":
|
|
port = int(os.getenv("PORT", 8080))
|
|
print(f"Starting API server on port {port}")
|
|
print(f"Swagger UI: http://localhost:{port}/docs")
|
|
print(f"ReDoc: http://localhost:{port}/redoc")
|
|
uvicorn.run(app, host="0.0.0.0", port=port)
|