Files
apipilot/README.md
T
2026-09-24 05:19:06 -04:00

279 lines
9.8 KiB
Markdown

# 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.