279 lines
9.8 KiB
Markdown
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.
|