# ApiPilot _Libreria Python generica che trasforma endpoint di un'app FastAPI in tool eseguibili da agenti AI, senza riferimenti hardcoded a domini o progetti specifici._ ## Che cosa fa ApiPilot - **Scansiona** l'OpenAPI della tua app FastAPI e registra solo gli endpoint marcati con `x-apipilot.enabled: true` - **Genera** definizioni di tool nel formato function calling compatibile OpenAI (nome, descrizione, schema JSON parametri per path / query / body) - **Esegue** i tool validando gli argomenti e applicando l'autenticazione e i permessi della tua app ospite - **Gestisce** timeout, risposte troppo grandi, errori strutturati e risultati vuoti - **Permette** di registrare tool composti Python (multi-step) non legati a un singolo endpoint - **Separa** il nucleo dal codice dei provider LLM, con adapter iniziale per il formato OpenAI (incluso `tool_call_id`) - **Opzionalmente** espone catalogo ed esecuzione via HTTP con autenticazione della tua app ## Installazione ```bash # Crea e attiva l'ambiente virtuale (consigliato) python -m venv .venv source .venv/bin/activate # Linux/macOS # .\.venv\Scripts\activate # Windows PowerShell # Installa ApiPilot (modalità editabile + dipendenze test/dev pip install -e ".[test,dev]" ``` Senza venv (sconsigliato, a seconda della policy PEP 668): ```bash pip install -e ".[test]" --break-system-packages ``` Requisiti minimi: Python 3.10+, FastAPI 0.100+, Pydantic v2. ## Utilizzo base ### 1. Abilita un endpoint con `x-apipilot` Aggiungi `openapi_extra={"x-apipilot": {...}}` ai decorator delle rotte che vuoi esporre come tool: ```python from fastapi import APIRouter router = APIRouter() @router.get( "/items", summary="Elenca gli elementi", openapi_extra={ "x-apipilot": { "enabled": True, "name": "list_items", "description": ( "Cerca gli elementi disponibili. Usalo quando " "l'utente chiede un elenco o cerca elementi " "di una categoria." ), } }, ) async def list_items(category: str | None = None, limit: int = 10): ... ``` ### 2. Installa ApiPilot sulla tua app ```python from fastapi import FastAPI from apipilot.fastapi import install_apipilot app = FastAPI() def get_current_user(authorization: str | None = None): # La tua logica di autenticazione esistente ... pilot = install_apipilot( app, get_current_user=get_current_user, ) ``` ### 3. Ottieni i tool disponibili per l'utente ```python tools = await pilot.tools_for(user) # Ogni tool è già in formato: # {"name": "...", "description": "...", "parameters": {...}} ``` ### 4. Esegui un tool ```python result = await pilot.execute( user=user, name="list_items", arguments={"category": "active"}, ) # result: {"success": bool, "data": Any, "error": dict|None, "meta": dict} ``` ### 5. Integra con un agente AI (es. OpenAI function calling) ```python from apipilot.adapters.openai import OpenAIAdapter # conversione -> messaggio tools per OpenAI openai_tools = OpenAIAdapter.to_tools(tools) # ricevi la tool_call dal modello e parsala tool_call_id, name, args = OpenAIAdapter.parse_tool_call(assistant_msg.tool_calls[0]) # esegui resp = await pilot.execute(user=user, name=name, arguments=args) # costruisci il messaggio di risposta per il ciclo successivo tool_result = OpenAIAdapter.make_tool_result(tool_call_id, name, resp) ``` ## Formato di `x-apipilot` Ogni operazione abilitata dichiara in `openapi_extra["x-apipilot"]`: | Campo | Tipo | Obbligatorio | Default | Note | |----------------------|----------|--------------|----------------------|------| | `enabled` | `bool` | Sì | `false` | Deve essere `true` per essere preso in carico. | | `name` | `string` | Consigliato | da `operationId` o route | Nome univoco del tool, usato anche da `execute`. | | `description` | `string` | No | `summary` endpoint | Descrizione per l'agente AI (influisce sulla scelta). | | `timeout` | `number` | No | `default_timeout` (30s) | Secondi oltre i quali la chiamata viene interrotta. | | `max_response_size` | `number` | No | `1_000_000` (1 MB) | Limite byte risultato JSON oltre il quale viene troncato. | Nomi duplicati, configurazioni incomplete o schemi non supportati (`$ref` non risolti fuori standard) sollevano errori chiari in fase di installazione. ## Autorizzazione (controllo per-tool) ```python def allow_tool(user, record) -> bool: if user and user.get("role") != "admin" and record.name == "create_item": return False return True pilot = install_apipilot(app, get_current_user=get_current_user, allow_tool=allow_tool) ``` I tool non ammessi non compaiono in `tools_for` e `execute` solleva `ToolNotFoundError` se richiamati. ## Tool composti (multi-step) ```python from apipilot.tool import CompositeTool async def find_cheapest(args, user): res = await pilot.execute(user=user, name="list_items", arguments=args) if not res["success"]: return {"error": res["error"]} items = (res["data"] or {}).get("items", []) if not items: return None return {"cheapest": min(items, key=lambda it: it["price"])} pilot.add_tool( CompositeTool( name="find_cheapest_item", description="Trova l'elemento più economico nella categoria.", parameters_schema={ "type": "object", "properties": {"category": {"type": "string"}}, "required": [], }, run_fn=find_cheapest, ) ) ``` ## Endpoints HTTP opzionali Passa `expose_http_endpoints=True`: ```python pilot = install_apipilot(app, get_current_user=get_current_user, expose_http_endpoints=True, http_prefix="/apipilot") ``` - `GET /apipilot/tools` — catalogo tool per l'utente autenticato. - `POST /apipilot/execute` payload `{"name": "...", "arguments": {...}}`. Se `require_auth_for_http=True` (default) e `get_current_user` è fornito, gli endpoint HTTP usano la tua funzione di autenticazione. ## Struttura risposta `ExecuteResponse` ```python { "success": True | False, "data": Any | None, "error": { "code": "INVALID_ARGUMENTS" | "EXECUTION_ERROR" | "TIMEOUT" | "UNEXPECTED_EXECUTION_ERROR", "message": str, # altri campi specifici (status_code, endpoint, details, ...) } | None, "meta": { "tool": str, "truncated"?: True, "max_response_size"?: int, } } ``` ## Sicurezza — garanzie di ApiPilot - **Il modello non sceglie URL, metodo HTTP o credenziali**: ogni tool mappa in modo statico una rotta registrata. - **Header tecnici e credenziali non sono esposti** nello schema dei parametri del tool (i parametri `in: header` sono saltati). - **Autorizzazione a due livelli**: controllo `allow_tool(user, record)` prima ancora della validazione argomenti, più le dipendenze native FastAPI sull'endpoint target. - **Validazione schema argomenti** tramite `jsonschema` prima di invocare qualsiasi funzione endpoint. - **Timeout per esecuzione** e **capped size delle risposte** per evitare injection di contesto eccessivo. - **Nomi tool univoci**: duplicati intercettati in fase di installazione. ## Eseguire i test ```bash pip install -e ".[test]" pytest -q ``` Coverage di riferimento: selezione operazioni, generazione parametri, autorizzazione, rifiuto tool sconosciuti, argomenti invalidi, gestione errori, timeout, troncamento risposte grandi, tool composti, endpoints HTTP. ## Demo backend incluso **Servizio completo (start/stop/restart/status/logs):** ```bash ./demo.sh start # avvia in background, crea .venv se serve ./demo.sh status # verifica stato (PID e log path) ./demo.sh logs 50 # ultime 50 righe di log ./demo.sh restart # stop + start ./demo.sh stop # arresto graceful (con fallback SIGKILL) ./demo.sh help # elenco comandi # URL : http://127.0.0.1:9292/docs # Auth : Demo senza autenticazione obbligatoria. # Senza header: default user=demo-user, ruolo=admin # Con header Authorization: Bearer admin-token / Bearer user-token per testare altri utenti # Files: .demo.pid (PID), .demo.log (log), .venv/ (ambiente) ``` **Modo veloce, primo piano (con venv auto):** ```bash ./run_demo.sh # → crea .venv se manca, installa dipendenze, avvia su http://127.0.0.1:9292 ``` **Modo manuale con venv:** ```bash # Crea venv e installa (solo se non già fatto) python -m venv .venv source .venv/bin/activate pip install -e ".[test]" # Avvia il demo python -m examples.demo_backend # → http://127.0.0.1:9292/docs # Demo senza autenticazione obbligatoria. # Senza header Authorization usa default user=demo-user (ruolo admin) # Con Authorization: Bearer admin-token / Bearer user-token puoi testare altri utenti ``` Vedi [examples/demo_backend.py](examples/demo_backend.py) per uno scenario completo (catalogo prodotti, ordini, tool composto di raccomandazione, demo ciclo agente). ## Moduli - [apipilot/core.py](apipilot/core.py) — classe `ApiPilot`, autorizzazione, esecuzione, gestione errori/size/timeout. - [apipilot/fastapi.py](apipilot/fastapi.py) — `install_apipilot`, endpoints HTTP opzionali. - [apipilot/openapi_parser.py](apipilot/openapi_parser.py) — parsing OpenAPI, generazione schema JSON, deduplica/valida. - [apipilot/validation.py](apipilot/validation.py) — validazione argomenti via `jsonschema`. - [apipilot/tool.py](apipilot/tool.py) — `CompositeTool` e protocollo `PilotTool`. - [apipilot/errors.py](apipilot/errors.py) — gerarchia eccezioni. - [apipilot/types.py](apipilot/types.py) — TypedDict e dataclass condivisi. - [apipilot/adapters/openai.py](apipilot/adapters/openai.py) — adapter per function calling OpenAI (`to_tools`, `parse_tool_call`, `make_tool_result` con `tool_call_id`). ## Modifiche & versionamento Vedere [CHANGELOG.md](CHANGELOG.md) per il tracciamento per data delle modifiche.