🚀

Создание API для AI-агента на FastAPI

Оборачиваем AI-агента в быстрый REST API. POST/GET эндпоинты, WebSocket для потоковой передачи ответов, авторизация по API-ключу, rate limiting и асинхронная обработка очередей задач через BackgroundTasks.

intermediate⏱ 22 мин
🌐 Архитектура FastAPI для AI-агента Клиент 🔑 Auth API-Key Header Rate Limiter Request Validator ⚡ FastAPI Core POST /agent/run GET /agent/status/:id WS /agent/stream GET /agent/history 🧠 AgentEngine LLM Client Tool Executor Memory Store Prompt Builder 🔧 ВСПОМОГАТЕЛЬНЫЕ КОМПОНЕНТЫ 📦 Redis Queue + Cache 🗄 PostgreSQL Sessions/History 📊 Prometheus Metrics 📝 Logs structlog

# 1. Каркас приложения: lifespan, CORS, middleware

Правильный старт — правильная архитектура. Используем 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")

# 2. POST /agent/run — синхронный запуск с BackgroundTasks

Основной эндпоинт запуска агента. Принимает задачу, валидирует входные данные, создаёт задачу в фоне и сразу возвращает 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")

# 3. WebSocket /agent/stream — стриминг ответов в реальном времени

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)})

# 4. Авторизация: API-ключи с хэшированием и скоупами

Безопасность на первом месте. Ключи хэшируются через 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

# 5. GET-эндпоинты: статус задачи, история, health

Вспомогательные эндпоинты для мониторинга и управления. Статус задачи с 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)
    }

# 6. OpenAPI-документация и авто-клиенты

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-проксированием.

📖 FastAPI Docs 📖 FastAPI WebSockets 📖 OpenAPI Client Gen 🔗 Каталог AI-агентов