feat: v0.2.0 — API REST, TTS support, documentazione completa e UI mobile-first
✨ Nuove funzionalità: - Nuovo endpoint API REST /api/v1/query (GET+POST) per consumo programmatico - Campo speakable_text ottimizzato per sintesi vocale (Alexa, Google Home) - Funzione strip_markdown() per conversione Markdown → plain text TTS - Supporto CORS per accesso cross-origin - UI responsive mobile-first con micro-interazioni e stati - Nuovo pulsante 'Nuova ricerca' e metadata bar 📚 Documentazione completa: - docs/ARCHITECTURE.md — architettura di sistema, stack, componenti - docs/FLOW.md — diagrammi Mermaid della pipeline e flussi - docs/API.md — documentazione endpoint REST con esempi curl - docs/WIKI.md — FAQ, glossario, roadmap - docs/DEPLOYMENT.md — guida deployment (Gunicorn, Nginx, Docker, systemd) - docs/CONTRIBUTING.md — guida alla contribuzione 🔧 Migliorie tecniche: - Pipeline di ricerca condivisa in _run_search_pipeline() - Gestione errori Gemini migliorata con _clean_error() - Sanitizzazione HTML con allowlist per prevenzione XSS - .env.example completo con commenti 🧪 Test: 109/109 passati
This commit is contained in:
1 parent
f3fe13c60e
commit
76bc667adf
20 files changed
+3385
-245
No files matched your search
+315
@@ -0,0 +1,315 @@
|
||||
# 📡 Sibilla — Documentazione API REST
|
||||
|
||||
> **Versione:** 0.2.0
|
||||
> **Ultimo aggiornamento:** 18 Luglio 2026
|
||||
|
||||
---
|
||||
|
||||
## Indice
|
||||
|
||||
1. [Panoramica](#panoramica)
|
||||
2. [Health Check](#health-check)
|
||||
3. [API Ask (Web)](#api-ask)
|
||||
4. [API v1 Query (Programmatica)](#api-v1-query)
|
||||
5. [Codici di errore](#codici-di-errore)
|
||||
6. [Esempi curl](#esempi-curl)
|
||||
7. [Integrazione Alexa Skill](#integrazione-alexa-skill)
|
||||
|
||||
---
|
||||
|
||||
## Panoramica
|
||||
|
||||
Tutti gli endpoint sono accessibili su `http://localhost:5000` (o l'host/porta configurati).
|
||||
|
||||
| Metodo | Endpoint | Descrizione | Formato |
|
||||
|--------|----------|-------------|---------|
|
||||
| `GET` | `/` | Interfaccia web HTML | `text/html` |
|
||||
| `GET` | `/api/health` | Health check | `application/json` |
|
||||
| `POST` | `/api/ask` | Ricerca + sintesi (web UI) | `application/json` |
|
||||
| `GET` | `/api/v1/query` | Ricerca + sintesi (API, query string) | `application/json` |
|
||||
| `POST` | `/api/v1/query` | Ricerca + sintesi (API, JSON body) | `application/json` |
|
||||
|
||||
**CORS:** Tutti gli endpoint includono header CORS permissivi (`Access-Control-Allow-Origin: *`).
|
||||
|
||||
---
|
||||
|
||||
## Health Check
|
||||
|
||||
### `GET /api/health`
|
||||
|
||||
Verifica lo stato del server e la configurazione.
|
||||
|
||||
**Request:**
|
||||
```bash
|
||||
curl http://localhost:5000/api/health
|
||||
```
|
||||
|
||||
**Response `200 OK`:**
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"version": "0.2.0",
|
||||
"gemini_configured": true,
|
||||
"degoog_url": "http://localhost:4444",
|
||||
"searxng_url": "http://localhost:8080"
|
||||
}
|
||||
```
|
||||
|
||||
| Campo | Tipo | Descrizione |
|
||||
|-------|------|-------------|
|
||||
| `status` | `string` | Sempre `"ok"` se il server risponde |
|
||||
| `version` | `string` | Versione di Sibilla |
|
||||
| `gemini_configured` | `boolean` | `true` se `GOOGLE_API_KEY` è configurata |
|
||||
| `degoog_url` | `string` | URL dell'istanza Degoog |
|
||||
| `searxng_url` | `string` | URL dell'istanza SearXNG |
|
||||
|
||||
---
|
||||
|
||||
## API Ask
|
||||
|
||||
### `POST /api/ask`
|
||||
|
||||
Endpoint principale per la ricerca con sintesi AI. Utilizzato dall'interfaccia web.
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"query": "Qual è stato l'ultimo film sull'Odissea?",
|
||||
"num_sources": 5
|
||||
}
|
||||
```
|
||||
|
||||
| Parametro | Tipo | Default | Range | Descrizione |
|
||||
|-----------|------|---------|-------|-------------|
|
||||
| `query` | `string` | *richiesto* | — | La domanda da cercare |
|
||||
| `num_sources` | `integer` | `5` | `1–10` | Numero massimo di fonti da usare |
|
||||
|
||||
**Response `200 OK`:**
|
||||
```json
|
||||
{
|
||||
"answer": "L'ultimo adattamento cinematografico dell'**Odissea** è *The Return* (2024)...",
|
||||
"html_answer": "<p>L'ultimo adattamento cinematografico dell'<strong>Odissea</strong> è <em>The Return</em> (2024)...</p>",
|
||||
"speakable_text": "L'ultimo adattamento cinematografico dell'Odissea è The Return del 2024...",
|
||||
"sources": [
|
||||
{
|
||||
"index": 1,
|
||||
"title": "The Return - Wikipedia",
|
||||
"url": "https://it.wikipedia.org/wiki/The_Return_(film_2024)",
|
||||
"snippet": "The Return è un film del 2024 diretto da Uberto Pasolini..."
|
||||
}
|
||||
],
|
||||
"model": "gemini-2.5-flash",
|
||||
"search_engine": "degoog",
|
||||
"tokens_used": 1847,
|
||||
"error": null
|
||||
}
|
||||
```
|
||||
|
||||
| Campo | Tipo | Descrizione |
|
||||
|-------|------|-------------|
|
||||
| `answer` | `string` | Risposta in formato **Markdown** |
|
||||
| `html_answer` | `string` | Risposta in **HTML** sicuro (sanitizzato) |
|
||||
| `speakable_text` | `string` | Risposta in **plain text**, ottimizzata per TTS |
|
||||
| `sources` | `array` | Lista delle fonti utilizzate |
|
||||
| `sources[].index` | `integer` | Numero della fonte (1-based) |
|
||||
| `sources[].title` | `string` | Titolo della pagina |
|
||||
| `sources[].url` | `string` | URL della fonte |
|
||||
| `sources[].snippet` | `string` | Estratto della pagina (max 200 caratteri) |
|
||||
| `model` | `string` | Modello LLM utilizzato |
|
||||
| `search_engine` | `string` | Motore di ricerca usato (`degoog`, `searxng`, `duckduckgo`, `none`) |
|
||||
| `tokens_used` | `integer` | Token totali consumati |
|
||||
| `error` | `string` or `null` | Eventuale messaggio di errore |
|
||||
|
||||
**Response `400 Bad Request`:**
|
||||
```json
|
||||
{
|
||||
"error": "La query non può essere vuota."
|
||||
}
|
||||
```
|
||||
|
||||
**Response `502 Bad Gateway`:**
|
||||
```json
|
||||
{
|
||||
"error": "Ricerca web fallita: Connection refused",
|
||||
"answer": "",
|
||||
"sources": []
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API v1 Query
|
||||
|
||||
### `GET /api/v1/query`
|
||||
|
||||
Endpoint semplificato per consumo programmatico. Accetta la query via query string.
|
||||
|
||||
**Request:**
|
||||
```bash
|
||||
curl "http://localhost:5000/api/v1/query?q=ultimo+film+sull%27Odissea"
|
||||
```
|
||||
|
||||
### `POST /api/v1/query`
|
||||
|
||||
Stesso endpoint, con query nel body JSON.
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"query": "Qual è stato l'ultimo film sull'Odissea?"
|
||||
}
|
||||
```
|
||||
|
||||
| Parametro | Tipo | Default | Descrizione |
|
||||
|-----------|------|---------|-------------|
|
||||
| `q` (GET) | `string` | *richiesto* | La query di ricerca (URL-encoded) |
|
||||
| `query` (POST) | `string` | *richiesto* | La query di ricerca |
|
||||
|
||||
**Response `200 OK`:**
|
||||
```json
|
||||
{
|
||||
"query": "Qual è stato l'ultimo film sull'Odissea?",
|
||||
"answer": "L'ultimo adattamento è **The Return** (2024), diretto da Uberto Pasolini...",
|
||||
"speakable_text": "L'ultimo adattamento è The Return del 2024, diretto da Uberto Pasolini...",
|
||||
"sources": [
|
||||
{
|
||||
"index": 1,
|
||||
"title": "The Return - Wikipedia",
|
||||
"url": "https://it.wikipedia.org/wiki/The_Return_(film_2024)"
|
||||
}
|
||||
],
|
||||
"model": "gemini-2.5-flash",
|
||||
"search_engine": "degoog",
|
||||
"tokens_used": 1847
|
||||
}
|
||||
```
|
||||
|
||||
| Campo | Tipo | Descrizione |
|
||||
|-------|------|-------------|
|
||||
| `query` | `string` | Echo della query originale |
|
||||
| `answer` | `string` | Risposta in formato **Markdown** |
|
||||
| `speakable_text` | `string` | **Plain text** ottimizzato per sintesi vocale (TTS) |
|
||||
| `sources` | `array` | Fonti semplificate (solo `index`, `title`, `url`) |
|
||||
| `model` | `string` | Modello LLM utilizzato |
|
||||
| `search_engine` | `string` | Motore di ricerca usato |
|
||||
| `tokens_used` | `integer` | Token totali consumati |
|
||||
|
||||
> **Nota:** Il formato è più compatto di `/api/ask` — pensato per API consumer come skill Alexa, Lambda function, o mobile app.
|
||||
|
||||
**Response `400 Bad Request`:**
|
||||
```json
|
||||
{
|
||||
"error": "Missing query parameter 'q'."
|
||||
}
|
||||
```
|
||||
|
||||
**Response `502 Bad Gateway`:**
|
||||
```json
|
||||
{
|
||||
"error": "Ricerca web fallita: Connection refused",
|
||||
"answer": "",
|
||||
"speakable_text": ""
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Codici di errore
|
||||
|
||||
| HTTP Status | Significato | Quando |
|
||||
|-------------|-------------|--------|
|
||||
| `200` | OK | Richiesta completata con successo |
|
||||
| `400` | Bad Request | Query mancante o vuota |
|
||||
| `502` | Bad Gateway | Tutti i motori di ricerca sono down |
|
||||
|
||||
**Errori interni gestiti:**
|
||||
- Se tutti i motori falliscono ma Gemini è disponibile → fallback a risposta solo LLM (200 OK)
|
||||
- Se Gemini non è configurato → `answer` vuoto con `error` nel JSON
|
||||
- Se Gemini restituisce errore → messaggio di errore inline in `answer`
|
||||
|
||||
---
|
||||
|
||||
## Esempi curl
|
||||
|
||||
### Health check
|
||||
```bash
|
||||
curl -s http://localhost:5000/api/health | python -m json.tool
|
||||
```
|
||||
|
||||
### Ricerca via API v1 (GET)
|
||||
```bash
|
||||
curl -s "http://localhost:5000/api/v1/query?q=cosa+sono+i+raggi+cosmici" | python -m json.tool
|
||||
```
|
||||
|
||||
### Ricerca via API v1 (POST)
|
||||
```bash
|
||||
curl -s -X POST http://localhost:5000/api/v1/query \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"query": "cosa sono i raggi cosmici"}' | python -m json.tool
|
||||
```
|
||||
|
||||
### Estrai solo la risposta speakable (per TTS)
|
||||
```bash
|
||||
curl -s "http://localhost:5000/api/v1/query?q=raggi+cosmici" | python -c "import sys,json; print(json.load(sys.stdin)['speakable_text'])"
|
||||
```
|
||||
|
||||
### Ricerca via web UI (POST /api/ask)
|
||||
```bash
|
||||
curl -s -X POST http://localhost:5000/api/ask \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"query": "raggi cosmici", "num_sources": 3}' | python -m json.tool
|
||||
```
|
||||
|
||||
### Verifica errori
|
||||
```bash
|
||||
# Query vuota → 400
|
||||
curl -i "http://localhost:5000/api/v1/query?q="
|
||||
|
||||
# Motori down → 502 (se tutti falliscono)
|
||||
curl -i "http://localhost:5000/api/v1/query?q=test"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Integrazione Alexa Skill
|
||||
|
||||
Ecco un esempio di come usare l'endpoint `/api/v1/query` da una Lambda Python per una skill Alexa:
|
||||
|
||||
```python
|
||||
import json
|
||||
import urllib.request
|
||||
|
||||
SIBILLA_URL = "https://tuo-server.com/api/v1/query"
|
||||
|
||||
def call_sibilla(query: str) -> dict:
|
||||
"""Chiama Sibilla e restituisce la risposta parlata."""
|
||||
params = urllib.parse.urlencode({"q": query})
|
||||
url = f"{SIBILLA_URL}?{params}"
|
||||
|
||||
with urllib.request.urlopen(url, timeout=30) as resp:
|
||||
data = json.loads(resp.read().decode())
|
||||
|
||||
return {
|
||||
"answer": data["answer"],
|
||||
"speakable_text": data["speakable_text"],
|
||||
"sources": data["sources"],
|
||||
}
|
||||
|
||||
# In un handler Alexa:
|
||||
def lambda_handler(event, context):
|
||||
query = event["request"]["intent"]["slots"]["query"]["value"]
|
||||
result = call_sibilla(query)
|
||||
|
||||
return {
|
||||
"version": "1.0",
|
||||
"response": {
|
||||
"outputSpeech": {
|
||||
"type": "SSML",
|
||||
"ssml": f"<speak>{result['speakable_text']}</speak>"
|
||||
},
|
||||
"shouldEndSession": True
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> **Nota:** Il campo `speakable_text` è già pulito da markup Markdown, URL, grassetti e codice — pronto per essere letto da un motore TTS.
|
||||
Reference in new issue
Block a user