Оборачиваем AI-агента в быстрый REST API. POST/GET эндпоинты, WebSocket для потоковой передачи ответов, авторизация по API-ключу, rate limiting и асинхронная обработка очередей задач через BackgroundTasks.
Правильный старт — правильная архитектура. Используем lifespan для graceful startup/shutdown, CORS middleware для браузерных клиентов, и структурное логирование через structlog. Каждый компонент инициализируется в правильном порядке.
# === app.py === from contextlib import asynccontextmanager from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware import structlog from core.agent import AgentEngine from core.db import Database from api.routes import agent_router logger = structlog.get_logger() @asynccontextmanager async def lifespan(app: FastAPI): # STARTUP logger.info("starting_agent_api") app.state.db = Database("postgresql+asyncpg://...") await app.state.db.connect() app.state.agent = AgentEngine( llm_model="gpt-4o", tools=["search", "calculate", "db_query"] ) yield # SHUTDOWN logger.info("shutting_down_agent_api") await app.state.db.disconnect() await app.state.agent.close() app = FastAPI(title="QantCore Agent API", version="1.0.0", lifespan=lifespan) # CORS — разрешаем фронтенду доступ app.add_middleware( CORSMiddleware, allow_origins=["https://qantcore.space"], allow_methods=["GET", "POST", "OPTIONS"], allow_headers=["X-API-Key", "Content-Type"], ) app.include_router(agent_router, prefix="/api/v1")
Основной эндпоинт запуска агента. Принимает задачу, валидирует входные данные, создаёт задачу в фоне и сразу возвращает task_id. Клиент затем опрашивает статус. Используем Pydantic v2 с field_validator для строгой валидации.
# === api/routes.py === import uuid from datetime import datetime from fastapi import APIRouter, BackgroundTasks, HTTPException, Depends from pydantic import BaseModel, Field, field_validator from core.auth import verify_api_key router = APIRouter() class AgentRequest(BaseModel): task: str = Field(..., min_length=3, max_length=4096) model: str = Field(default="gpt-4o") temperature: float = Field(default=0.3, ge=0, le=2) max_steps: int = Field(default=10, ge=1, le=50) @field_validator("task") @classmethod def validate_task(cls, v: str) -> str: if not v.strip(): raise ValueError("task не может быть пустым") return v.strip() class TaskResponse(BaseModel): task_id: str status: str created_at: datetime estimated_time: int = 30 # секунд @router.post("/agent/run", response_model=TaskResponse) async def run_agent( req: AgentRequest, bg: BackgroundTasks, user=Depends(verify_api_key) ): task_id = f"tsk_{uuid.uuid4().hex[:12]}" # Сохраняем задачу в БД со статусом pending await router.app.state.db.insert_task( task_id=task_id, user_id=user.id, task=req.task, status="pending" ) # Запускаем агента в фоне bg.add_task(execute_agent_task, task_id, req) return TaskResponse(task_id=task_id, status="pending", created_at=datetime.utcnow()) async def execute_agent_task(task_id: str, req: AgentRequest): await router.app.state.db.update_status(task_id, "running") try: result = await router.app.state.agent.run( task=req.task, model=req.model, temperature=req.temperature, max_steps=req.max_steps ) await router.app.state.db.complete_task(task_id, result, "success") except Exception as e: await router.app.state.db.complete_task(task_id, {"error": str(e)}, "failed")
WebSocket эндпоинт для потоковой передачи ответов агента — пользователь видит каждый шаг рассуждения. Токены летят через async generator, WebSocket отправляет JSON-фреймы с типом: thinking, tool_call, tool_result, final_answer. Идеально для интерактивных чат-интерфейсов.
# === api/ws.py === import json from fastapi import WebSocket, WebSocketDisconnect @router.websocket("/agent/stream") async def agent_stream(ws: WebSocket): await ws.accept() # Первое сообщение — API-ключ и задача init_msg = await ws.receive_json() api_key = init_msg.get("api_key") task = init_msg.get("task") # Валидация ключа if not verify_api_key_sync(api_key): await ws.send_json({"type": "error", "message": "Invalid API key"}) await ws.close(code=4001) return try: # Запускаем агента со стримингом async for event in router.app.state.agent.stream_run(task): await ws.send_json({ "type": event.type, # thinking|tool_call|tool_result|answer "data": event.data, "timestamp": event.ts.isoformat() }) await ws.send_json({"type": "done"}) except WebSocketDisconnect: pass # Клиент отключился — останавливаем стриминг except Exception as e: await ws.send_json({"type": "error", "data": str(e)})
Безопасность на первом месте. Ключи хэшируются через bcrypt (в БД только хэш), префикс qnt_ для идентификации. Каждый ключ имеет scope: read (только чтение истории), run (запуск агента), admin (полный доступ). Реализован throttling: не более 60 запросов в минуту.
# === core/auth.py === import bcrypt, secrets, time from fastapi import HTTPException, Security from fastapi.security import APIKeyHeader api_key_header = APIKeyHeader(name="X-API-Key", auto_error=True) def generate_api_key(prefix="qnt") -> tuple[str, str]: """Генерирует ключ + хэш. В БД сохраняем ТОЛЬКО хэш.""" raw_key = f"{prefix}_{secrets.token_urlsafe(32)}" key_hash = bcrypt.hashpw(raw_key.encode(), bcrypt.gensalt()).decode() return raw_key, key_hash # Простейший in-memory rate limiter (для прода — Redis) _rate_limit_store: dict[str, list[float]] = {} def check_rate_limit(key: str, max_rpm=60) -> bool: now = time.monotonic() if key not in _rate_limit_store: _rate_limit_store[key] = [now] return True # Очищаем старые записи (>60 сек) _rate_limit_store[key] = [t for t in _rate_limit_store[key] if now - t < 60] if len(_rate_limit_store[key]) >= max_rpm: return False _rate_limit_store[key].append(now) return True async def verify_api_key(key: str = Security(api_key_header)): if not check_rate_limit(key): raise HTTPException(429, detail="Rate limit exceeded. Try again later.") # Ищем хэш ключа в БД user = await db.find_user_by_key_hash(key) if not user: raise HTTPException(401, detail="Invalid API key") return user
Вспомогательные эндпоинты для мониторинга и управления. Статус задачи с ETA, пагинированная история запусков с возможностью перезапуска, и health эндпоинт для оркестратора. Всё асинхронное, с правильными HTTP-статусами.
# === api/routes.py (продолжение) === from typing import Optional @router.get("/agent/status/{task_id}") async def task_status( task_id: str, user=Depends(verify_api_key) ): task = await router.app.state.db.get_task(task_id, user.id) if not task: raise HTTPException(404, "Task not found") return { "task_id": task.id, "status": task.status, "result": task.result if task.status == "success" else None, "created_at": task.created_at, "finished_at": task.finished_at, "duration_ms": task.duration_ms } @router.get("/agent/history") async def task_history( page: int = 1, size: int = 20, status: Optional[str] = None, user=Depends(verify_api_key) ): total, items = await router.app.state.db.list_tasks( user_id=user.id, page=page, size=size, status=status ) return { "items": items, "pagination": { "page": page, "size": size, "total": total, "pages": (total + size - 1) // size } } @router.get("/health") async def health(): return { "status": "ok", "version": "1.0.0", "uptime_seconds": int(time.monotonic() - _start_time) }
FastAPI генерирует OpenAPI 3.1 схему автоматически. Это значит что клиентский код для Python, TypeScript и других языков можно сгенерировать одной командой. Плюс интерактивная документация Swagger UI на /docs и ReDoc на /redoc.
# Генерация Python-клиента pip install openapi-python-client # Из корня проекта: openapi-python-client generate \ --url http://localhost:8000/openapi.json \ --output agent-client # Использование сгенерированного клиента: from agent_client import Client from agent_client.models import AgentRequest client = Client(base_url="https://agent.qantcore.space") client.set_api_key("qnt_your_api_key") task = await client.agent.run_agent( AgentRequest(task="Проанализируй продажи за Q2") ) print(f"Task: {task.task_id} — {task.status}") # Для TypeScript (с openapi-typescript-codegen): # npx openapi-typescript-codegen --input http://localhost:8000/openapi.json
Вы построили полноценный REST + WebSocket API для AI-агента на FastAPI. Реализованы: POST /agent/run с фоновым выполнением и отслеживанием статуса, WebSocket /agent/stream для потоковой передачи шагов рассуждения агента, авторизация по API-ключу с bcrypt-хэшированием и rate limiting (60 rpm), пагинированная история задач с фильтрацией, автоматическая OpenAPI-документация на /docs и генерация типизированных клиентов для Python/TypeScript. Продакшн-реди архитектура готова к развёртыванию за nginx с WebSocket-проксированием.