# Pepper — API di ricezione e invio
Questo documento definisce **come si parla con Pepper**: il formato delle richieste in arrivo e delle risposte in uscita. È il contratto che ogni progetto chiamante deve rispettare. Per la panoramica del progetto vedi README.md.
> Esiste una **seconda modalità**, separata e aggiuntiva: l'**API agente** (`/agent`), con tool-use (function calling) e risposta in streaming, pensata per i progetti che devono far *agire* Pepper e non solo elaborare un testo. Si attiva per progetto dal pannello ed è documentata in **[AGENT.md](?f=agent)**. Tutto quello che segue in questa pagina **non cambia**.
## Endpoint
```
POST https://gateway.pepperai.it/api.php
Content-Type: application/json
```
Sempre in **POST**, sempre **JSON**. Niente GET: il contenuto può essere lungo e non deve finire nei log del server.
`/api.php` è l'endpoint principale, quello di questa pagina. Ce ne sono altri due, piccoli, che servono le funzioni aggiunte con la 6d:
| Endpoint | A cosa serve |
|---|---|
| `POST /api.php` | mandare un lavoro e ricevere la risposta |
| `POST /pratica.php` | ritirare una risposta [differita](#lavoro-differito) |
| `POST /capacita.php` | chiedere [cosa sa fare Pepper](#cosa-sa-fare-pepper-per-questo-progetto) per questo progetto |
| `POST /conoscenza.php` | caricare, elencare e togliere i documenti della [conoscenza](#la-conoscenza) del progetto |
C'è poi `GET /stato.php`, senza chiave, che dice soltanto se Pepper è vivo: serve al controllo esterno, non ai progetti.
## Autenticazione
Ogni progetto ha una propria chiave, che invia nell'header:
```
X-Pepper-Key: <chiave-del-progetto>
```
La chiave deve corrispondere al `progetto` dichiarato nel corpo della richiesta. Chiave mancante, sbagliata o non abbinata al progetto → errore `401`.
**Un progetto non vede mai niente di un altro.** È una promessa a chi chiama: con la
tua chiave non raggiungi le conversazioni, le pratiche, le varianti di ricetta, la
cache o la spesa di nessun altro progetto — e una conversazione o una pratica di un
altro progetto risponde esattamente come una che non esiste, con lo stesso errore.
## Richiesta
```json
{
"progetto": "dossiero",
"encode": "riassumi",
"contenuto": "Testo da elaborare..."
}
```
| Campo | Tipo | Obbligatorio | Note |
|-------------|--------|:---:|------|
| `progetto` | string | ✅ | Identificativo del chiamante, minuscolo, senza spazi (es. `dossiero`, `sito-x`). Deve essere registrato in Pepper. |
| `encode` | string | ✅ | Nome della ricetta da applicare (es. `riassumi`, `correggi`, `estrai`, `classifica`, `libero`). Deve esistere in Pepper. |
| `contenuto` | string | ✅ | Il testo su cui lavorare. Massimo 100.000 caratteri (oltre → errore `413`). |
| `conversazione` | string | ❌ | Per richieste con memoria. Ometti il campo → richiesta singola, senza memoria. `"nuova"` → Pepper apre una conversazione e restituisce il suo ID nella risposta. Un ID esistente (es. `cnv_a1b2c3`) → prosegue quella conversazione ricordando i messaggi precedenti. |
| `extra` | object | ❌ | Parametri opzionali della ricetta (es. `{"lingua": "en"}`). Ignorato se la ricetta non lo prevede. Due sono speciali: con `estrai` i `campi` e con `classifica` le `categorie` **obbligano** la forma della risposta (vedi sotto). |
| `documenti` | array | ❌ | PDF e foto da far leggere a Pepper insieme al `contenuto` (vedi [Documenti](#documenti-pdf) e [Foto](#foto)). Vanno abilitati per progetto. |
| `modo` | string | ❌ | `"subito"` (predefinito) oppure `"differito"`: il lavoro costa la metà e la risposta si ritira più tardi (vedi [Lavoro differito](#lavoro-differito)). |
| `contenuti` | array | ❌ | Più testi in una volta, **solo** in differito: è lì che il differito paga davvero. Al posto di `contenuto`. |
| `stream` | bool | ❌ | `true` → la risposta arriva **a pezzi**, mentre viene scritta (vedi [La risposta a pezzi](#la-risposta-a-pezzi)). Senza, il JSON di sempre. Non con `modo: "differito"`. |
## Ripetere una richiesta senza pagarla due volte
Se una richiesta va in timeout, il chiamante non sa se è arrivata: rimandandola
si paga (e si scrive in conversazione) due volte. Basta aggiungere un'intestazione:
```
Idempotency-Key: un-valore-unico-di-questa-richiesta
```
Con la stessa chiave, entro **un'ora**, Pepper restituisce **la stessa risposta**
di prima — identica, id di conversazione compreso — senza rifare niente e senza
consumare né limite orario né budget. Si riconosce da `meta.idempotente: true`.
La chiave la sceglie il chiamante e deve essere unica per richiesta (per esempio
l'id della pratica più un contatore, o un `uuid`).
## Risposta
### Successo — HTTP `200`
```json
{
"ok": true,
"risposta": "Il testo elaborato dall'AI...",
"conversazione": null,
"meta": {
"progetto": "dossiero",
"encode": "riassumi",
"provider": "claude",
"modello": "claude-opus-5",
"durata_ms": 1840,
"versione": "Pepper 6"
}
}
```
| Campo | Note |
|-------------|------|
| `ok` | Sempre `true` in caso di successo |
| `risposta` | Il risultato dell'elaborazione. Sempre una stringa; per le ricette che producono dati strutturati (es. `estrai`) è una stringa JSON da decodificare. |
| `conversazione` | `null` per le richieste singole. Se la richiesta ha aperto o proseguito una conversazione, contiene il suo ID (es. `cnv_a1b2c3`): il chiamante lo salva e lo rimanda nelle chiamate successive per continuare il discorso. |
| `meta` | Informazioni di servizio: utili per log e debug, il chiamante può ignorarle. `versione` è il **modello** che ha risposto — «Pepper 6», «Pepper 7» — cioè il cervello con cui il progetto sta parlando (vedi I modelli). Chi vuole mostrarlo lo prende da qui e non ne tiene una copia. |
In `meta` possono comparire anche questi campi, solo quando servono:
| Campo | Quando c'è | Cosa vuol dire |
|-------|-----------|----------------|
| `troncata` | la risposta ha raggiunto il limite di lunghezza | è incompleta: era troppo lunga |
| `cache` | la risposta arrivava dalla cache di Pepper | stessa richiesta già fatta di recente: non è stata pagata e non è stata rigenerata |
| `tentativi` | c'è voluta più di una prova | il provider aveva risposto male o non aveva risposto, Pepper ha riprovato da solo |
| `ripiego` | ha risposto la riserva | il motore di prima scelta non ce l'ha fatta (es. `modello:claude-haiku-4-5` = un altro motore, `pepper:Pepper 6 · OpenAI` = un altro modello Pepper). Se e come ripiegare lo decide il modello Pepper |
| `citazioni` | c'erano documenti allegati, o Pepper ha cercato sul web | da dove viene ogni pezzo della risposta: la pagina del PDF o l'indirizzo web (vedi [Documenti](#documenti-pdf)) |
| `citazioni_spente` | c'erano documenti **e** uno schema JSON obbligato | le citazioni non convivono con lo schema: hanno vinto i campi. Vale `"schema_json"` |
| `fonti` | la risposta ha usato la [conoscenza](#la-conoscenza) del progetto | da quali documenti del progetto (nome, numero del pezzo, somiglianza) |
| `conoscenza` | la conoscenza è accesa ma gli indici non hanno risposto | vale `"non_disponibile"`: la risposta è arrivata senza documenti |
| `ricerche` | Pepper ha cercato su internet | quante ricerche ha fatto: si pagano a parte, un centesimo l'una |
| `documenti_pagine` | c'erano documenti allegati | quante pagine sono state mandate (ogni pagina pesa 1.500-3.000 token) |
### Risposte a forma garantita
Per due ricette la forma della risposta non è più affidata alle istruzioni ma
imposta al modello, che non può uscirne:
- **`estrai`** con `extra.campi`: la risposta è un oggetto JSON con **esattamente**
quei campi, ognuno stringa oppure `null` se il dato non c'è.
- **`classifica`** con `extra.categorie`: la risposta è **una** di quelle categorie,
scritta da sola come prima (niente JSON attorno: il contratto non cambia).
Senza quei parametri le due ricette si comportano come prima.
### Errore — HTTP `4xx` / `5xx`
```json
{
"ok": false,
"errore": {
"codice": "encode_sconosciuto",
"messaggio": "La ricetta 'riasumi' non esiste. Disponibili: riassumi, correggi, estrai, classifica, libero."
}
}
```
| HTTP | `codice` | Quando |
|------|------------------------|--------|
| 400 | `richiesta_non_valida` | JSON malformato o campo obbligatorio mancante |
| 401 | `non_autorizzato` | Chiave assente, errata o non abbinata al `progetto` |
| 404 | `encode_sconosciuto` | La ricetta richiesta non esiste |
| 404 | `conversazione_sconosciuta` | L'ID di conversazione non esiste o appartiene a un altro progetto |
| 413 | `contenuto_troppo_grande` | `contenuto` oltre il limite |
| 429 | `troppe_richieste` | Il progetto ha superato il suo limite di chiamate — riprovare dopo qualche secondo |
| 429 | `budget_esaurito` | Il progetto ha finito il suo tetto di spesa giornaliero — riprovare il giorno dopo |
| 502 | `errore_ai` | Il provider AI ha risposto con un errore — riprovare più tardi |
| 503 | `ai_non_disponibile` | Provider AI momentaneamente irraggiungibile o sovraccarico |
| 400 | `documento_non_valido` | Un allegato non è un PDF leggibile (base64 storto, file protetto da password, tipo diverso) |
| 403 | `documenti_non_abilitati` | Il progetto non può allegare documenti — si accende dal pannello |
| 403 | `immagini_non_abilitate` | Il progetto non può allegare foto — si accende dal pannello (Progetti → Immagini) |
| 413 | `documento_troppo_grande` | Un allegato supera i limiti di peso o di pagine |
| 413 | `troppi_documenti` | Più di 5 allegati in una chiamata |
| 403 | `differito_non_abilitato` | Il progetto non può mandare lavoro in differito — si accende dal pannello |
| 429 | `troppe_pratiche` | Il progetto ha troppe pratiche differite ancora aperte: ritirane qualcuna |
| 404 | `pratica_sconosciuta` | Il numero di pratica non esiste o è di un altro progetto |
| 422 | `richiesta_rifiutata` | Il modello si è rifiutato di rispondere per motivi di sicurezza. Non è un guasto: riprovare uguale non serve; se la richiesta è legittima va riformulata |
Regola per i chiamanti: **controllare sempre `ok` prima di leggere `risposta`**. Gli errori `429`, `502` e `503` si possono ritentare (con una piccola attesa); gli errori `4xx` no — vanno corretti.
## La risposta a pezzi
Una risposta lunga può metterci un minuto, di più con un modello che ragiona. Con
`"stream": true` nella richiesta non si aspetta in silenzio: il testo arriva **mentre
viene scritto**, come eventi SSE (`Content-Type: text/event-stream`, HTTP `200`).
```
event: delta
data: {"text": "Il verbale "}
event: delta
data: {"text": "riguarda il rifacimento del tetto…"}
event: done
data: {"ok": true, "risposta": "Il verbale riguarda il rifacimento del tetto…",
"conversazione": null, "meta": {"versione": "Pepper 7", …}}
```
- **`delta`** — un pezzo di testo. Messi insieme danno la risposta.
- **`done`** — la fine, con dentro **la risposta intera nella forma di sempre**:
`ok`, `risposta`, `conversazione`, `meta`. Chi vuole può ignorare i `delta` e
leggere solo questo.
- **`error`** — `{"codice", "messaggio"}`, gli stessi della [tabella degli errori](#errore--http-4xx--5xx);
poi lo stream si chiude.
- **`tool_call` / `tool_done`** — solo quando Pepper cerca su internet: si accende e si
spegne il pallino della ricerca (il [kit della chat](?f=agent#il-kit-della-chat) li
sa già disegnare).
- Ogni ~15 secondi di silenzio arriva un commento `: ping`, che tiene viva la connessione.
Cosa sapere:
- **Gli errori prima dello stream escono come sempre** — JSON col loro codice HTTP
(chiave sbagliata, ricetta sconosciuta, limite orario…). Lo stream si apre solo
quando Pepper sta per chiamare il modello; da lì in poi il codice HTTP è già partito,
e un errore esce come evento `error`. Chi chiama gestisce tutti e due i casi.
- **Con la forma obbligata** (`estrai` con `campi`, `classifica` con `categorie`) il
testo non arriva a pezzi ma tutto insieme, subito prima del `done`: i pezzi
sarebbero JSON a metà.
- **Dalla cache** la risposta arriva tutta insieme, con `meta.cache: true`: non si finge
uno streaming che non c'è.
- **Con ChatGPT** la risposta per ora arriva in un pezzo solo: il contratto è lo stesso,
cambia solo il ritmo.
- **Se chi chiama chiude a metà**, Pepper si ferma, salva nella conversazione quello che
era arrivato e scrive il costo nel log (esito `interrotta`). Si paga quello che è
stato generato fino a lì.
- **`Idempotency-Key` vale anche qui**: chi riprova riceve la risposta di prima, a
pezzi o intera a seconda di come la chiede la seconda volta.
- Non con `modo: "differito"` (`400`): lì la risposta arriva ore dopo.
Col client PHP basta passare una funzione: `pepper('libero', $domanda, aPezzi: fn($pezzo) => print($pezzo))`.
## Documenti (PDF)
Al `contenuto` si possono allegare dei PDF: Pepper li legge — testo, tabelle,
grafici, timbri — e nella risposta dice **da quale pagina** ha preso ogni cosa.
Va abilitato per progetto dal pannello (**Progetti → Documenti**). Un progetto
che non ce l'ha riceve `403 documenti_non_abilitati`.
```json
{
"progetto": "dossiero",
"encode": "libero",
"contenuto": "Di cosa parla questo verbale? Chi erano i presenti?",
"documenti": [
{"nome": "verbale.pdf", "dati": "<il PDF in base64>"}
]
}
```
Ogni voce di `documenti` è `{"nome", "dati"}` (il PDF in base64) **oppure**
`{"nome", "url"}` con un indirizzo `https://` raggiungibile: in quel caso il file
non passa dentro la richiesta e lo scarica il provider.
### I limiti
| | Limite | Perché |
|---|---|---|
| Documenti per chiamata | 5 | |
| Peso di un documento | 10 MB | oltre, `413 documento_troppo_grande` |
| Peso di tutti insieme | 20 MB | il base64 gonfia di un terzo |
| Pagine | **600**, ma **100** sul modello economico | il limite scende sotto il milione di token di contesto |
Le pagine si contano prima di spedire: la risposta è `413` in mezzo secondo,
non dopo un minuto di caricamento.
### Le citazioni
Con un documento allegato, `meta.citazioni` contiene da dove viene ogni pezzo:
```json
"meta": {
"documenti_pagine": 12,
"citazioni": [
{"fonte": "documento", "documento": "verbale.pdf", "pagina": 3,
"testo": "il consiglio ha approvato all'unanimità"}
]
}
```
`risposta` resta **una stringa**, come sempre: chi non guarda le citazioni non si
accorge di niente.
> **Le citazioni non convivono con lo schema JSON obbligato.** Con `estrai` +
> `campi` o `classifica` + `categorie` vincono i campi, le citazioni si spengono
> e `meta.citazioni_spente` vale `"schema_json"`. È un limite del provider, non
> una scelta: insieme sono un errore.
## Foto
Nello stesso elenco `documenti` si possono mettere anche delle foto: Pepper le
guarda e dice cosa c'è, cosa c'è scritto, cosa non torna.
Hanno un permesso loro, separato da quello dei PDF (**Progetti → Immagini**).
Un progetto che non ce l'ha riceve `403 immagini_non_abilitate`; un progetto che
ha solo le foto può mandare foto e non PDF, e viceversa.
```json
{
"progetto": "dossiero",
"encode": "libero",
"contenuto": "Cosa c'è scritto su questo scontrino?",
"documenti": [
{"nome": "scontrino.jpg", "tipo": "image/jpeg", "dati": "<la foto in base64>"}
]
}
```
- **Formati**: JPEG, PNG, GIF, WebP. Le HEIC dell'iPhone vanno convertite prima.
- **Peso**: 5 MB a foto (è il tetto del provider). Valgono anche i limiti di
sopra: 5 allegati per chiamata in tutto, 20 MB insieme.
- **`tipo`** è consigliato; se manca, Pepper capisce da solo se è una foto
guardando i primi byte (o l'estensione, per una foto mandata con `url`).
- **Niente citazioni** sulle foto: si citano solo i PDF.
- Solo con Claude, come i PDF.
### Quanto costa
Un PDF non ha supplementi, ma **ogni pagina pesa 1.500-3.000 token**: cento
pagine con Opus sono più di un euro in una chiamata sola. È la ragione per cui il
permesso è per progetto e per cui conviene mettere un tetto di spesa prima di
accenderlo.
Due cose da sapere:
- **Il ripiego sull'altro provider si spegne** quando ci sono allegati: OpenAI non
ha lo stesso contratto per i PDF e risponderebbe ignorando il documento senza
dirlo. Meglio un errore onesto.
- **La cache delle risposte non vale** con gli allegati: la risposta salvata
sarebbe senza citazioni, e restituirla monca senza dirlo è peggio che rifarla.
## Lavoro differito
Per i lavori che possono aspettare. Costa **la metà**: Pepper consegna il lotto al
provider, che ci lavora quando ha spazio (entro 24 ore, di solito entro un'ora), e
restituisce subito un **numero di pratica**.
Va abilitato per progetto (**Progetti → Differito**). Senza, `403
differito_non_abilitato`.
### Consegnare
```json
{
"progetto": "dossiero",
"encode": "riassumi",
"modo": "differito",
"contenuti": ["primo verbale...", "secondo verbale...", "terzo..."]
}
```
Risposta **HTTP `202`** (presa in carico, non fatta):
```json
{
"ok": true,
"risposta": null,
"pratica": "prt_a1b2c3d4e5f6",
"stato": "in_corso",
"quanti": 3,
"meta": {"differito": true, "modello": "claude-opus-5", "versione": "Pepper 6"}
}
```
Il numero di pratica **è la ricevuta**: si salva e si usa per ritirare.
### Ritirare
```
POST https://<dominio-pepper>/pratica.php
X-Pepper-Key: <chiave del progetto>
{"progetto": "dossiero", "pratica": "prt_a1b2c3d4e5f6"}
```
```json
{
"ok": true,
"pratica": "prt_a1b2c3d4e5f6",
"stato": "pronta",
"quanti": 3,
"risposte": [
{"ok": true, "risposta": "Il primo verbale..."},
{"ok": true, "risposta": "Il secondo..."},
{"ok": false, "errore": "Il provider non ha fatto in tempo entro 24 ore."}
],
"meta": {"differito": true, "creato_il": "2026-09-05 22:10:03", "versione": "Pepper 6"}
}
```
`stato` vale `in_corso`, `pronta`, `fallita` o `scaduta`. Quando il testo era uno
solo, la risposta arriva **anche** come `risposta`, nella stessa forma di
`/api.php`. Le richieste che non sono andate a buon fine **non si pagano**.
Se la pratica aveva un documento allegato, ogni risposta si porta dietro le sue
`citazioni`, nella stessa forma di `meta.citazioni`: anche in differito si sa da
quale pagina viene ogni cosa.
Pepper chiede al provider «è pronto?» soltanto quando qualcuno passa a ritirare
(e non più di una volta ogni 20 secondi): non c'è nessun lavoro pianificato che
gira di notte, quindi non c'è niente che possa rompersi di notte.
### Cosa il differito non fa
- **Niente conversazioni**: la risposta arriverebbe ore dopo e si infilerebbe in
mezzo ad altri messaggi. Chi ci prova riceve `400`.
- **Niente risposta a pezzi** e niente cache delle risposte.
- **Niente `Idempotency-Key`**: il numero di pratica è già la ricevuta.
- **Niente allegati insieme a `contenuti`**: gli allegati si mettono su un
`contenuto` alla volta, altrimenti finirebbero appiccicati a ogni testo e il
conto si moltiplicherebbe senza che nessuno l'abbia chiesto.
- Un progetto può avere al massimo **20 pratiche aperte** e **100 testi** per
pratica.
## La conoscenza
I documenti che il progetto carica **una volta**, e in cui Pepper pesca **a ogni
domanda**: un regolamento, un listino, le procedure dell'ufficio. È diverso dai
[documenti allegati](#documenti-pdf): quelli sono «leggi questo adesso», questa è
«ricordati di questo sempre».
Come funziona: Pepper taglia ogni documento a pezzi e ne fa un indice. A ogni domanda
cerca i pezzi che le somigliano di più (al massimo 5) e li mette davanti alla domanda,
con l'istruzione di rispondere su quelli e di dire se la risposta non c'è.
**Si accende in due posti**, come internet: sul progetto (pannello → Progetti →
Conoscenza) e sulla ricetta (pannello → Ricette). Spenta di fabbrica.
```
POST https://<dominio-pepper>/conoscenza.php
X-Pepper-Key: <chiave del progetto>
{"progetto": "dossiero", "azione": "carica", "nome": "Regolamento 2026", "testo": "..."}
{"progetto": "dossiero", "azione": "elenco"}
{"progetto": "dossiero", "azione": "togli", "documento": 12}
```
- `carica` risponde `{"ok": true, "id": 12, "nome": "...", "pezzi": 31, "sostituito": false}`.
Un documento **con lo stesso nome prende il posto** di quello di prima: per aggiornare
il listino si rimanda. Il testo è testo: da un PDF, si estrae prima.
- `elenco` risponde `{"ok": true, "documenti": [{"id", "nome", "pezzi", "caratteri", "caricato_il"}], "pezzi": 31}`.
- `togli` toglie il documento **davvero**: i suoi pezzi, e le risposte in cache del progetto
che potevano contenerlo.
Nella risposta di `/api.php`, `meta.fonti` dice da dove viene quello che Pepper ha detto:
`[{"documento": "Regolamento 2026", "pezzo": 4, "somiglianza": 0.62}]`. Il testo dei pezzi
non si ripete: i documenti il progetto li ha già.
Da sapere:
- **Gli indici li fa OpenAI**, anche quando le risposte le dà Claude (Anthropic non fa
indici di ricerca). Serve la chiave OpenAI nelle Impostazioni di Pepper, e **i testi
caricati passano anche da OpenAI**. `/capacita.php` lo dice in `conoscenza.nota`.
- **Se OpenAI non risponde**, Pepper risponde lo stesso, senza documenti, e
`meta.conoscenza` vale `"non_disponibile"`.
- **Costa** qualche centinaio di token in più su ogni domanda (i pezzi entrano nel prompt),
più l'indice della domanda. Caricare costa l'indice, una volta sola.
- **Non impara dalle conversazioni**: ci finisce solo quello che qualcuno ci mette apposta.
- **Non nel differito**: lì i testi sono tanti e partono tutti insieme.
- Limiti: 200 documenti per progetto, 500.000 caratteri per documento.
- **Un progetto non vede mai i documenti di un altro**, nemmeno con la domanda copiata
parola per parola: la ricerca filtra per progetto *prima* di cercare. Un documento di un
altro progetto, chiesto con `togli`, risponde come uno che non esiste.
| HTTP | `codice` | Quando |
|---|---|---|
| 403 | `conoscenza_non_abilitata` | la conoscenza non è accesa sul progetto |
| 404 | `documento_sconosciuto` | `togli` di un documento che non c'è (o di un altro progetto) |
| 413 | `contenuto_troppo_grande` | documento oltre 500.000 caratteri |
| 429 | `troppi_documenti` | il progetto ha già 200 documenti |
| 503 | `conoscenza_senza_chiave` | manca la chiave OpenAI in Pepper |
Col client PHP: `pepper_conoscenza_carica($nome, $testo)`, `pepper_conoscenza_elenco()`,
`pepper_conoscenza_togli($id)`.
## Ricerca su internet
Pepper può cercare sul web mentre risponde. Servono **due permessi accesi**:
quello del progetto (**Progetti → Internet**) e quello della ricetta
(**Ricette → Può cercare su internet**). Due e non uno, perché `correggi` non ha
niente da cercare e una ricetta che cerca lo farebbe per tutti i progetti.
Non c'è niente da cambiare nella richiesta: se i permessi ci sono, Pepper cerca
quando serve. Nella risposta compaiono `meta.ricerche` (quante ne ha fatte) e le
fonti dentro `meta.citazioni`:
```json
"meta": {
"ricerche": 2,
"citazioni": [
{"fonte": "web", "url": "https://www.normattiva.it/...",
"titolo": "Decreto legislativo ...", "testo": "l'articolo 3 prevede che..."}
]
}
```
Tre cose da sapere:
- **Costa a parte**: un centesimo a ricerca, più i token delle pagine che entrano
in contesto — che sono la parte grossa. Massimo 3 ricerche per chiamata.
- **Si possono restringere le fonti**: nel pannello, per progetto, si mette una
lista di domini ammessi (per esempio `normattiva.it, gazzettaufficiale.it`) e
Pepper cerca solo lì. È il modo concreto di ridurre il rischio, perché una
pagina qualsiasi può contenere righe scritte apposta per il modello.
- **Nelle conversazioni le fonti non si riportano indietro**: la memoria di Pepper
è fatta di testo, e i risultati delle ricerche richiederebbero di rimandare
intatto un blocco cifrato del provider. Nei giri successivi resta il testo della
risposta, non le pagine.
## Cosa sa fare Pepper (per questo progetto)
Il modello viaggia in ogni risposta (`meta.versione`), ma da solo dice soltanto
*con chi* si sta parlando. Questo endpoint dice anche *cosa* può fare.
```
POST https://<dominio-pepper>/capacita.php
X-Pepper-Key: <chiave del progetto>
{"progetto": "dossiero"}
```
```json
{
"ok": true,
"progetto": "dossiero",
"versione": "Pepper 7",
"novita": "Adesso ragiona prima di rispondere: più attento sulle domande difficili.",
"pepper": {"nome": "Pepper 7", "descrizione": "...", "ragionamento": true,
"voce_nelle_app": false},
"ricette": [
{"nome": "riassumi", "descrizione": "...", "livello": "economico", "internet": false}
],
"puo": {"conversazioni": true, "documenti": true, "immagini": false, "differito": true,
"internet": false, "agente": false, "streaming": true, "conoscenza": false},
"conoscenza": null,
"limiti": {"contenuto_caratteri": 100000, "documenti_per_chiamata": 5,
"documento_mb": 10, "immagine_mb": 5, "documento_pagine": 600,
"immagini_tipi": ["image/jpeg", "image/png", "image/gif", "image/webp"],
"documento_pagine_economico": 100, "differito_testi": 100,
"differito_pratiche_aperte": 20, "chiamate_ora": 100,
"domini_internet": []},
"spesa": {"oggi_usd": 0.42, "tetto_usd": 5, "residuo_usd": 4.58},
"modelli": {"provider": "claude", "principale": "claude-opus-5-5",
"economico": "claude-haiku-4-5"}
}
```
Serve per **accendere le funzioni solo quando esistono davvero**: il pulsante
«allega PDF» si mostra se `puo.documenti` è vero, e il giorno che lo accendi dal
pannello compare da solo, senza toccare il codice del progetto.
Non fa magie: quello che il progetto fa con queste informazioni va comunque
scritto. Serve a rendere la differenza **visibile** invece che sepolta in un file.
## Client pronto per PHP
Per i progetti PHP non serve scrivere nulla: si copia **`client/pepper-client.php`** nel progetto, si definiscono tre costanti (`PEPPER_URL`, `PEPPER_PROGETTO`, `PEPPER_KEY`) e si chiama:
```php
require_once __DIR__ . '/pepper-client.php';
$riassunto = pepper('riassumi', $testoLungo)['risposta'];
// conversazione con memoria
$r = pepper('libero', 'Ti spiego la situazione...', conversazione: 'nuova');
$r2 = pepper('libero', 'E quindi?', conversazione: $r['conversazione']);
```
Dalla 6d il client ha anche il resto:
```php
// un PDF da leggere, con le citazioni di pagina
$r = pepper('libero', 'Di cosa parla?', documenti: ['/percorso/verbale.pdf']);
foreach ($r['meta']['citazioni'] ?? [] as $c) {
echo $c['documento'], ' pag. ', $c['pagina'], ': ', $c['testo'], "\n";
}
// lavoro differito: costa la metà
$pratica = pepper_differito('riassumi', $centoVerbali); // -> 'prt_a1b2c3d4e5f6'
$p = pepper_pratica($pratica); // più tardi
if ($p['stato'] === 'pronta') { foreach ($p['risposte'] as $x) { echo $x['risposta']; } }
// cosa può fare questo progetto adesso (tenuto da parte per un giorno)
if (pepper_capacita()['puo']['documenti']) { mostra_pulsante_allega(); }
```
Gli errori arrivano come `PepperException`: `$e->codice` è il codice della tabella qui sotto, `$e->riprovabile` dice se ha senso ritentare. Gli esempi che seguono servono per chi non usa PHP o vuole fare da sé.
## Esempi di chiamata
### curl
```bash
curl -s https://gateway.pepperai.it/api.php \
-H "Content-Type: application/json" \
-H "X-Pepper-Key: chiave-di-dossiero" \
-d '{
"progetto": "dossiero",
"encode": "riassumi",
"contenuto": "Lungo testo del verbale della riunione..."
}'
```
### PHP (dal progetto chiamante)
```php
function chiedi_a_pepper(string $encode, string $contenuto): string
{
$ch = curl_init('https://gateway.pepperai.it/api.php');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'X-Pepper-Key: ' . PEPPER_KEY, // definita nella config del progetto
],
CURLOPT_POSTFIELDS => json_encode([
'progetto' => 'dossiero',
'encode' => $encode,
'contenuto' => $contenuto,
]),
CURLOPT_TIMEOUT => 300, // l'AI può impiegare minuti, di più se il modello ragiona
]);
$risposta = json_decode(curl_exec($ch), true);
curl_close($ch);
if (!($risposta['ok'] ?? false)) {
throw new Exception('Pepper: ' . ($risposta['errore']['messaggio'] ?? 'errore sconosciuto'));
}
return $risposta['risposta'];
}
// Uso:
$riassunto = chiedi_a_pepper('riassumi', $testoLungo);
```
### Conversazione con memoria
```bash
# Prima chiamata: apre la conversazione
curl -s https://gateway.pepperai.it/api.php \
-H "Content-Type: application/json" -H "X-Pepper-Key: chiave-di-dossiero" \
-d '{"progetto":"dossiero","encode":"libero","conversazione":"nuova",
"contenuto":"Ti spiego la situazione del cliente Rossi..."}'
# → risposta con "conversazione": "cnv_a1b2c3"
# Chiamate successive: si rimanda lo stesso ID, Pepper ricorda tutto il discorso
curl -s https://gateway.pepperai.it/api.php \
-H "Content-Type: application/json" -H "X-Pepper-Key: chiave-di-dossiero" \
-d '{"progetto":"dossiero","encode":"libero","conversazione":"cnv_a1b2c3",
"contenuto":"E se invece il cliente rifiutasse la proposta?"}'
```
Le conversazioni sono salvate nel MySQL del VPS e **appartengono al progetto che le ha aperte**: un ID usato da un progetto diverso risponde `404 conversazione_sconosciuta`.
## Cosa succede dentro Pepper (lato invio verso l'AI)
Il chiamante non deve saperlo, ma per riferimento: Pepper prende le istruzioni della ricetta `encode`, le usa come *system prompt*, mette il `contenuto` come messaggio utente e chiama il provider attivo.
Con **Claude** (provider attivo di default) la chiamata sottostante è:
```
POST https://api.anthropic.com/v1/messages
x-api-key: <ANTHROPIC_API_KEY>
anthropic-version: 2023-06-01
Content-Type: application/json
{
"model": "claude-opus-4-8",
"max_tokens": 16000,
"system": "<istruzioni della ricetta>",
"messages": [
{"role": "user", "content": "<contenuto>"}
]
}
```
La risposta utile sta in `content[0].text`; controllare `stop_reason` (se è `max_tokens` la risposta è troncata). Con **OpenAI** l'adattatore farà l'equivalente su `POST /v1/chat/completions` con `Authorization: Bearer <OPENAI_API_KEY>`. In entrambi i casi l'adattatore traduce la risposta del provider nel formato Pepper descritto sopra: **i progetti chiamanti vedono sempre e solo il formato Pepper**, mai quello del provider.
## Regole di compatibilità
- I campi `progetto`, `encode`, `contenuto`, `ok`, `risposta`, `errore` **non cambieranno mai** di nome o significato.
- Tutto quello che la 6d ha aggiunto (`documenti`, `modo`, `contenuti`, i campi nuovi in `meta`, i due endpoint nuovi) è **facoltativo**: un progetto scritto per la 6b continua a funzionare senza cambiare una riga, e le capacità nuove sono comunque spente finché non le accendi dal pannello.
- Campi nuovi potranno essere **aggiunti** (in richiesta come opzionali, in risposta dentro `meta`): i chiamanti devono ignorare i campi che non conoscono.
- **Il passaggio a un modello che ragiona («Pepper 7») non chiede niente a chi usa questo endpoint**: la richiesta e la risposta restano le stesse, il ragionamento resta dentro Pepper e non torna indietro. Cambiano solo due cose da sapere: le risposte arrivano **più lente** (serve un timeout di almeno 300 secondi, come nel client pronto; Pepper stesso ne concede 300) e **costano di più**. Il lavoro in più per l'app c'è soltanto in modalità agente: vedi [AGENT.md → Il ragionamento e i `blocchi`](?f=agent#il-ragionamento-e-i-blocchi).
- Se in futuro servirà una modifica incompatibile, si aggiungerà un nuovo endpoint (es. `/api2.php`) lasciando vivo il vecchio.
- L'API agente (`/agent`, vedi [AGENT.md](?f=agent)) è un endpoint a sé: evolve per conto suo e non tocca in alcun modo questo contratto.