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:
Sibilla committed 2026-07-18 10:05:46 +00:00
1 parent f3fe13c60e
commit 76bc667adf
20 files changed
+3385 -245

No files matched your search

+315
View File
@@ -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.
+444
View File
@@ -0,0 +1,444 @@
# 🏗️ Sibilla — Architettura di Sistema
> **Autore:** Sibilla Team
> **Versione:** 0.2.0
> **Ultimo aggiornamento:** 18 Luglio 2026
---
## Indice
1. [Visione d'insieme](#visione-dinsieme)
2. [Stack tecnologico](#stack-tecnologico)
3. [Componenti principali](#componenti-principali)
4. [Struttura del progetto](#struttura-del-progetto)
5. [Pipeline di ricerca](#pipeline-di-ricerca)
6. [Modello dati](#modello-dati)
7. [Flusso delle richieste](#flusso-delle-richieste)
8. [Strategia di fallback](#strategia-di-fallback)
9. [Sicurezza e sanitizzazione](#sicurezza-e-sanitizzazione)
10. [Estendibilità](#estendibilità)
---
## Visione d'insieme
**Sibilla** è un motore di ricerca AI self-hosted che emula l'esperienza di Perplexity, combinando:
- **Ricerca web multi-engine** con fallback a cascata
- **Sintesi LLM** tramite Google Gemini
- **Query expansion** per migliorare il recall
- **Fetching full-content** delle pagine per una sintesi accurata
- **Interfaccia web** reattiva e mobile-first
- **API REST** per consumo programmatico e skill vocali (Alexa)
- **Server MCP** per integrazione con agent AI esterni
```
┌──────────────────────────────────────────────────────────────────┐
│ SIBILLA SYSTEM │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────────────────────┐ │
│ │ Browser │ │ curl / │ │ AI Agent (MCP Client) │ │
│ │ (Web UI) │ │ Alexa │ │ Claude/Gemini Desktop │ │
│ └────┬─────┘ └────┬─────┘ └───────────┬──────────────┘ │
│ │ │ │ │
│ │ HTTP/HTML │ REST/JSON │ stdio JSON-RPC │
│ ▼ ▼ ▼ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Flask Application │ │
│ │ │ │
│ │ GET / → Web UI (index.html) │ │
│ │ POST /api/ask → Search pipeline (Web) │ │
│ │ GET/POST /api/v1/query → Search pipeline (API) │ │
│ │ GET /api/health → Health check │ │
│ │ │ │
│ │ ┌──────────────────────────────────────────────────┐ │ │
│ │ │ _run_search_pipeline() ← shared │ │ │
│ │ └──────────────────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ MCP Server (stdlib) │ │
│ │ │ │
│ │ tools: web_search(query) → Degoog → SearXNG → DDG │ │
│ │ tools: fetch_page(url) → PageFetcher │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────┐ ┌───────────────┐ ┌──────────────┐ ┌────────┐ │
│ │ Degoog │ │ SearXNG │ │ DuckDuckGo │ │ Gemini │ │
│ │ (primary) │ │ (secondary) │ │ (fallback) │ │ LLM │ │
│ └───────────┘ └───────────────┘ └──────────────┘ └────────┘ │
└──────────────────────────────────────────────────────────────────┘
```
---
## Stack tecnologico
| Livello | Tecnologia | Ruolo |
|---------|-----------|-------|
| **Web framework** | Flask 3.x | Application server, routing, API |
| **LLM** | Google Gemini (gemini-2.5-flash) | Sintesi, query expansion |
| **SDK LLM** | google-genai ≥ 1.0 | Client Python per Gemini |
| **Search engines** | Degoog, SearXNG, DuckDuckGo | Ricerca web multi-source |
| **HTML parsing** | BeautifulSoup 4 | Scraping e estrazione contenuti |
| **Markdown** | Python-Markdown 3.5+ | Rendering Markdown → HTML |
| **Frontend** | Vanilla HTML/CSS/JS | Interfaccia utente mobile-first |
| **Testing** | Pytest | Unit e integration test |
| **Linting** | Ruff | Code quality e formattazione |
| **Protocol** | MCP (Model Context Protocol) | Integrazione agent AI esterni |
---
## Componenti principali
### 1. `app.py` — Flask Application Factory
Entry point dell'app web. Utilizza il pattern **Application Factory** per creare un'istanza Flask configurabile.
```python
def create_app(testing: bool = False) -> Flask:
```
**Responsabilità:**
- Inizializzazione lazy dei servizi (Degoog, SearXNG, DuckDuckGo, PageFetcher, Gemini)
- Routing degli endpoint (`/`, `/api/ask`, `/api/v1/query`, `/api/health`)
- Pipeline di ricerca condivisa (`_run_search_pipeline`)
- Gestione CORS per accesso cross-origin
- Configurazione via variabili d'ambiente (`.env`)
### 2. `search.py` — Client di Ricerca
Implementa tre backend di ricerca con interfaccia unificata:
| Classe | Backend | Tipo |
|--------|---------|------|
| `DegoogClient` | Degoog (self-hosted) | Primario |
| `SearXNGClient` | SearXNG (self-hosted) | Secondario |
| `DuckDuckGoClient` | DuckDuckGo HTML | Fallback |
| `PageFetcher` | HTTP + BeautifulSoup | Content extraction |
Ogni client restituisce `SearchResponse` contenente `list[SearchResult]`.
### 3. `ai.py` — Gemini LLM Client
Wrapper intorno all'SDK `google-genai` con due funzionalità:
- `synthesize()` — Sintesi di risposte basate su fonti
- `rewrite_query()` — Generazione di varianti di query per migliorare il recall
Gestione robusta degli errori con `_clean_error()`.
### 4. `markdown.py` — Conversione Markdown
Due funzioni principali:
- `render()` — Converte Markdown → HTML con sanitizzazione XSS
- `strip_markdown()` — Converte Markdown → plain text per TTS (Alexa, Google Home)
### 5. `prompts.py` — Template dei Prompt
Contiene i system prompt che guidano il comportamento di Gemini:
- `SYNTHESIS_SYSTEM_PROMPT` — Prompt principale per la sintesi con citazioni
- `FALLBACK_PROMPT` — Prompt quando non ci sono risultati di ricerca
- `QUERY_REWRITE_PROMPT` — Prompt per la query expansion
- `build_synthesis_prompt()` — Assembla il prompt completo con fonti
### 6. `mcp.py` — MCP Server
Server Model Context Protocol su stdio che espone due tool:
- `web_search(query)` — Ricerca web con fallback a cascata
- `fetch_page(url)` — Estrazione contenuto pagina web
### 7. `cli.py` / `__main__.py` — Entry Point
Avvio del server web Flask con configurazione via variabili d'ambiente.
---
## Struttura del progetto
```
sibilla/
├── docs/ # 📚 Documentazione
│ ├── ARCHITECTURE.md # Architettura di sistema (questo file)
│ ├── FLOW.md # Diagramma di flusso della pipeline
│ ├── API.md # Documentazione API REST
│ ├── WIKI.md # Wiki generale
│ ├── DEPLOYMENT.md # Guida al deployment
│ └── CONTRIBUTING.md # Guida alla contribuzione
│
├── src/sibilla/ # 📦 Codice sorgente
│ ├── __init__.py # Package init
│ ├── __main__.py # python -m sibilla
│ ├── app.py # Flask app factory + pipeline
│ ├── cli.py # CLI entry point
│ ├── search.py # Degoog, SearXNG, DDG, PageFetcher
│ ├── ai.py # GeminiClient (sintesi + query rewriting)
│ ├── markdown.py # Markdown → HTML + Markdown → plain text
│ ├── prompts.py # System prompt templates
│ └── mcp.py # MCP server stdio
│
├── templates/ # 🖼️ Template HTML (Jinja2)
│ └── index.html # Interfaccia di ricerca
│
├── static/ # 🎨 Asset statici
│ ├── style.css # Stili CSS (mobile-first, dark theme)
│ ├── app.js # Logica frontend (fetch, render, UX)
│ └── favicon.svg # Icona del sito
│
├── tests/ # 🧪 Test
│ ├── __init__.py
│ ├── test_app.py # Test endpoint Flask
│ ├── test_ai.py # Test GeminiClient
│ ├── test_search.py # Test client di ricerca
│ ├── test_prompts.py # Test template prompt
│ └── test_markdown.py # Test conversione Markdown
│
├── pyproject.toml # Configurazione progetto Python
├── README.md # README principale
└── .gitignore
```
---
## Pipeline di ricerca
La pipeline di ricerca è il cuore di Sibilla. Eseguita dalla funzione condivisa `_run_search_pipeline()`:
```
1. INPUT: query utente + num_sources
│
2. QUERY EXPANSION
┌─ Gemini genera 3 varianti della query originale
└─ Output: [query_originale, variante1, variante2, variante3]
│
3. MULTI-ENGINE SEARCH (cascading fallback)
┌─ Degoog: cerca con le prime 2 varianti
│ └─ Successo? → risultati aggregati
│ └─ Fallimento? → passa a SearXNG
├─ SearXNG: cerca con le prime 2 varianti
│ └─ Successo? → risultati aggregati
│ └─ Fallimento? → passa a DuckDuckGo
└─ DuckDuckGo: cerca con la query originale
└─ Successo? → risultati aggregati
└─ Fallimento? → fallback pure LLM
│
4. DEDUPLICAZIONE
┌─ Elimina URL duplicati (set di seen_urls)
└─ Ordine di priorità mantenuto
│
5. FETCH CONTENT
┌─ PageFetcher scarica fino a num_sources pagine
├─ Estrazione testo con BeautifulSoup
├─ Rimozione script, nav, ads, sidebar
└─ Rate limiting: 300ms tra una fetch e l'altra
│
6. PROMPT ASSEMBLY
┌─ build_synthesis_prompt() assembla:
│ ├─ Contesto temporale (data di oggi)
│ ├─ Domanda dell'utente
│ └─ Fonti numerate con titolo, URL, contenuto
└─ Istruzioni per citazioni inline [1], [2], ...
│
7. LLM SYNTHESIS
┌─ Gemini genera risposta con:
│ ├─ Sistema: SYNTHESIS_SYSTEM_PROMPT
│ ├─ Utente: prompt assemblato
│ ├─ Temperatura: 0.4 (bassa per fedeltà alle fonti)
│ └─ Max token: 4096
└─ Output: SynthesisResult (answer, model, tokens_used, error)
│
8. OUTPUT: JSON con answer, sources, metadata
```
---
## Modello dati
### SearchResult
```python
@dataclass
class SearchResult:
title: str # Titolo del risultato
url: str # URL della pagina
snippet: str # Snippet dal motore di ricerca
full_content: str # Contenuto completo (dopo fetch)
fetched: bool # Flag: contenuto scaricato?
```
### SearchResponse
```python
@dataclass
class SearchResponse:
query: str # Query originale
results: list[SearchResult] # Lista risultati
total_results: int # Totale risultati trovati
engine: str # Motore utilizzato
error: Optional[str] # Eventuale errore
```
### SynthesisResult
```python
@dataclass
class SynthesisResult:
answer: str # Testo della risposta
model: str # Modello LLM usato
tokens_used: int # Token consumati
error: Optional[str] # Eventuale errore
```
### Response JSON (`/api/ask`)
```json
{
"answer": "Testo in Markdown della risposta...",
"html_answer": "<p>Versione HTML della risposta...</p>",
"speakable_text": "Testo pulito per TTS...",
"sources": [
{
"index": 1,
"title": "Titolo della fonte",
"url": "https://example.com",
"snippet": "Estratto della pagina..."
}
],
"model": "gemini-2.5-flash",
"search_engine": "degoog",
"tokens_used": 1234,
"error": null
}
```
### Response JSON (`/api/v1/query`) — formato semplificato per API
```json
{
"query": "domanda originale",
"answer": "Risposta in Markdown...",
"speakable_text": "Risposta pulita per TTS...",
"sources": [
{"index": 1, "title": "...", "url": "..."}
],
"model": "gemini-2.5-flash",
"search_engine": "degoog",
"tokens_used": 1234
}
```
---
## Flusso delle richieste
### Web UI (`GET /`)
```
Browser → GET / → Flask → render_template("index.html")
└─ Jinja2 template HTML
└─ Static assets (CSS, JS, favicon)
Utente scrive query → POST /api/ask {query: "..."}
→ app.js fetch → render risposta
```
### API REST (`GET/POST /api/v1/query`)
```
curl / Lambda / Alexa Skill
│
├─ GET /api/v1/query?q=domanda
└─ POST /api/v1/query {"query": "domanda"}
│
▼
_run_search_pipeline(query, num_sources=5)
│
▼
JSON response con speakable_text
```
### MCP Server (`sibilla-mcp`)
```
AI Agent (Claude/Gemini Desktop)
│
├─ stdio JSON-RPC
│
▼
MCPServer.run()
├─ tools/list → [{web_search}, {fetch_page}]
└─ tools/call → esecuzione tool → risultato text
```
---
## Strategia di fallback
Sibilla implementa una **strategia di fallback a cascata** su tre livelli:
```
Livello 1: Degoog (primary)
│
├─ Disponibile? → Usa Degoog
└─ Non disponibile? ↓
Livello 2: SearXNG (secondary)
│
├─ Disponibile? → Usa SearXNG
└─ Non disponibile? ↓
Livello 3: DuckDuckGo (fallback)
│
├─ Disponibile? → Usa DuckDuckGo (HTML scraping)
└─ Non disponibile? ↓
Livello 4: Pure LLM (degraded mode)
└─ Gemini risponde senza fonti, spiegando i limiti
```
**Vantaggi:**
- Nessun single point of failure
- DuckDuckGo non richiede server esterni (scraping HTML)
- Pure LLM garantisce sempre una risposta (in degraded mode)
---
## Sicurezza e sanitizzazione
### Sanitizzazione HTML
Il Markdown renderizzato viene sanitizzato per prevenire XSS:
- **Allowlist di tag HTML:** solo tag sicuri (`h1-h6`, `p`, `strong`, `a`, ecc.)
- **Allowlist di attributi:** `href`, `title`, `alt`, `src`, `class`, `id`
- **Blocco `javascript:` URL** su `href` e `src`
- **Nessun event handler** (`onclick`, `onerror`, ecc.)
- **Strip di `<script>`, `<iframe>`, `<object>`**
### CORS
- `Access-Control-Allow-Origin: *` — permissivo per API pubbliche
- `Access-Control-Allow-Methods: GET, POST, OPTIONS`
- `Access-Control-Allow-Headers: Content-Type, Authorization`
### Rate Limiting
- PageFetcher: 300ms delay tra richieste consecutive
- Non implementato rate limiting a livello API (da aggiungere in futuro)
---
## Estendibilità
### Aggiungere un nuovo search engine
1. Creare una classe con interfaccia `search(query, num_results) → SearchResponse`
2. Aggiungere il client in `_run_search_pipeline()` nella catena di fallback
3. Aggiungere test in `tests/test_search.py`
### Aggiungere un nuovo endpoint API
1. Aggiungere una route in `app.py`
2. Riutilizzare `_run_search_pipeline()` per la logica condivisa
3. Aggiungere test in `tests/test_app.py`
### Aggiungere un nuovo tool MCP
1. Aggiungere il tool in `MCPServer._build_tools()`
2. Implementare il metodo handler
3. Registrare in `_handle_request()` sotto `tools/call`
### Aggiungere un nuovo modello LLM
1. Creare un client con interfaccia `synthesize(system_prompt, user_prompt, ...) → SynthesisResult`
2. Iniettare il client in `_run_search_pipeline()`
3. Aggiungere test in `tests/test_ai.py`
+217
View File
@@ -0,0 +1,217 @@
# 🤝 Sibilla — Guida alla Contribuzione
> **Versione:** 0.2.0
> **Ultimo aggiornamento:** 18 Luglio 2026
---
Grazie per il tuo interesse a contribuire a Sibilla! 🎉
## Codice di Condotta
Sii rispettoso, costruttivo e inclusivo. Tratta gli altri come vorresti essere trattato.
---
## Come Contribuire
### 1. Segnala un Bug
Apri una **issue** su GitHub con:
- **Titolo descrittivo** (es. "Errore 502 quando Degoog è lento")
- **Passi per riprodurre il bug**
- **Comportamento atteso** vs **comportamento osservato**
- **Log di errore** (se disponibili)
- **Ambiente:** versione Python, OS, versione Sibilla
### 2. Proponi una Feature
Apri una issue con il tag `enhancement` descrivendo:
- **Problema che risolve**
- **Soluzione proposta**
- **Alternative considerate**
- **Impatto sul codice esistente**
### 3. Invia una Pull Request
#### Setup sviluppo
```bash
# 1. Forka il repository
# 2. Clona il tuo fork
git clone https://github.com/tuo-user/sibilla.git
cd sibilla
# 3. Aggiungi l'upstream
git remote add upstream https://github.com/original/sibilla.git
# 4. Crea ambiente virtuale
python3 -m venv .venv
source .venv/bin/activate
# 5. Installa dipendenze di sviluppo
pip install -e ".[dev]"
# 6. Configura .env
cp .env.example .env
nano .env # Inserisci GOOGLE_API_KEY
```
#### Flusso di lavoro
```bash
# 1. Sincronizza con upstream
git fetch upstream
git checkout master
git merge upstream/master
# 2. Crea un branch
git checkout -b feature/nome-feature
# oppure
git checkout -b fix/nome-bug
# 3. Sviluppa
# ... scrivi codice ...
# 4. Testa
pytest tests/ -v
# 5. Linta
ruff check src tests
# 6. Committa con messaggi chiari
git add .
git commit -m "feat: aggiungi caching risposte con Redis"
# 7. Pusha
git push origin feature/nome-feature
# 8. Apri Pull Request su GitHub
```
---
## Convenzioni del Codice
### Stile
- **Python 3.10+** con type hints
- **Ruff** per linting e formattazione (`ruff check src tests`)
- **Line length:** 100 caratteri
- **Docstring:** stile Google (`"""Descrizione breve.` con `Args:`, `Returns:`, `Raises:`)
### Commit Messages
Usiamo [Conventional Commits](https://www.conventionalcommits.org/):
```
feat: aggiungi caching risposte con Redis
fix: correggi race condition in PageFetcher
docs: aggiorna README con esempi API v1
refactor: estrai pipeline condivisa in helper
test: aggiungi test per strip_markdown
chore: aggiorna dipendenze
```
### Struttura dei Branch
| Prefisso | Uso |
|----------|-----|
| `feature/` | Nuove funzionalità |
| `fix/` | Bug fix |
| `docs/` | Documentazione |
| `refactor/` | Refactoring |
| `test/` | Test |
| `chore/` | Manutenzione |
---
## Test
### Esecuzione
```bash
# Tutti i test
pytest tests/ -v
# Test specifico
pytest tests/test_app.py -v
# Con coverage
pip install pytest-cov
pytest tests/ --cov=src/sibilla --cov-report=html
```
### Scrivere Test
- **Nuove feature = nuovi test.** Ogni endpoint, funzione pubblica e classe deve essere testata.
- **Naming:** `test_<funzione>_<scenario>()` (es. `test_strip_markdown_with_images`)
- **Fixtures:** usa `create_app(testing=True)` per testare senza `.env`
Esempio:
```python
def test_api_v1_query_returns_speakable_text(client):
"""Verifica che /api/v1/query includa speakable_text."""
# Dato che Gemini non è configurato, verifica la risposta di errore
resp = client.get("/api/v1/query?q=test")
data = resp.get_json()
assert "speakable_text" in data
```
---
## Pipeline di Ricerca
Per aggiungere un nuovo passaggio alla pipeline:
1. **Identifica il punto di inserimento** in `_run_search_pipeline()` in `app.py`
2. **Implementa la logica** in una funzione o classe separata
3. **Aggiungi test** per il nuovo comportamento
4. **Documenta** in `docs/ARCHITECTURE.md`
### Aggiungere un Search Engine
1. Crea una classe in `search.py` che implementi `search(query, num_results) → SearchResponse`
2. Aggiungi il client in `_run_search_pipeline()` nella catena di fallback
3. Aggiungi test in `tests/test_search.py`
```python
class MySearchClient:
def search(self, query: str, num_results: int = 8) -> SearchResponse:
# ... implementazione ...
return SearchResponse(query=query, results=results, engine="myengine")
```
### Aggiungere un Endpoint API
1. Aggiungi una route in `app.py`
2. Riutilizza `_run_search_pipeline()` per la logica condivisa
3. Aggiungi test in `tests/test_app.py`
```python
@app.route("/api/v2/custom", methods=["POST"])
def api_v2_custom():
# ... usa _run_search_pipeline() ...
```
---
## Documentazione
La documentazione vive in `docs/`. Quando modifichi una funzionalità, aggiorna:
- `docs/API.md` — se modifichi endpoint o formati di risposta
- `docs/ARCHITECTURE.md` — se modifichi l'architettura
- `docs/FLOW.md` — se modifichi la pipeline
- `docs/WIKI.md` — per FAQ e roadmap
---
## Domande?
Apri una issue su GitHub o contatta i maintainer.
Grazie per contribuire a Sibilla! 🔮
+402
View File
@@ -0,0 +1,402 @@
# 🚀 Sibilla — Guida al Deployment
> **Versione:** 0.2.0
> **Ultimo aggiornamento:** 18 Luglio 2026
---
## Indice
1. [Prerequisiti](#prerequisiti)
2. [Installazione rapida](#installazione-rapida)
3. [Configurazione](#configurazione)
4. [Avvio](#avvio)
5. [Deployment in produzione](#deployment-in-produzione)
6. [Docker Compose](#docker-compose)
7. [Reverse Proxy](#reverse-proxy)
8. [Systemd Service](#systemd-service)
9. [Monitoraggio](#monitoraggio)
10. [Troubleshooting](#troubleshooting)
---
## Prerequisiti
### Software
| Componente | Versione minima | Note |
|-----------|----------------|------|
| Python | 3.10+ | |
| pip | 21.0+ | |
| Git | — | Opzionale, per clonare il repo |
### Servizi esterni
| Servizio | Obbligatorio? | Come ottenerlo |
|----------|:---:|---------------|
| Google API Key | ✅ Sì | [Google AI Studio](https://aistudio.google.com/apikey) |
| Degoog | Consigliato | `docker run -d -p 4444:4444 ghcr.io/degoog-org/degoog:latest` |
| SearXNG | No (fallback) | `docker run -d -p 8080:8080 searxng/searxng` |
| DuckDuckGo | No (automatico) | Nessuna configurazione necessaria |
---
## Installazione rapida
```bash
# 1. Clona il repository
git clone https://github.com/tuo-user/sibilla.git
cd sibilla
# 2. Crea ambiente virtuale
python3 -m venv .venv
source .venv/bin/activate # Linux/Mac
# .venv\Scripts\activate # Windows
# 3. Installa dipendenze
pip install -e ".[dev]"
# 4. Configura
cp .env.example .env # se esiste, altrimenti crealo
nano .env # Inserisci GOOGLE_API_KEY
# 5. Avvia (sviluppo)
sibilla
# → http://127.0.0.1:5000
```
---
## Configurazione
Crea un file `.env` nella root del progetto:
```env
# ═══════════════════════════════════════════════════════════════
# Sibilla Configuration
# ═══════════════════════════════════════════════════════════════
# Google Gemini API key (obbligatoria)
GOOGLE_API_KEY=AIzaSyD-xxxxxxxxxxxxxxxxxxxxxxxxxxxx
# Flask secret key (cambiala in produzione!)
SECRET_KEY=una-chiave-segreta-molto-lunga-e-casuale
# ── Server ───────────────────────────────────────────────────
# Host e porta del server Flask
SIBILLA_HOST=127.0.0.1
SIBILLA_PORT=5000
# Debug mode (0 = off, 1 = on)
# NON usare in produzione!
SIBILLA_DEBUG=1
# ── Search Engines ────────────────────────────────────────────
# Degoog (primary)
DEGOOG_URL=http://localhost:4444
# SearXNG (secondary)
SEARXNG_URL=http://localhost:8080
# DuckDuckGo fallback (1 = on, 0 = off)
SIBILLA_DDG_FALLBACK=1
```
### Variabili d'ambiente complete
| Variabile | Default | Descrizione |
|-----------|---------|-------------|
| `GOOGLE_API_KEY` | `""` | **Obbligatoria.** API key di Google AI |
| `SECRET_KEY` | `"dev-secret-key-change-me"` | Secret key Flask |
| `SIBILLA_HOST` | `"127.0.0.1"` | Host su cui ascoltare |
| `SIBILLA_PORT` | `"5000"` | Porta del server |
| `SIBILLA_DEBUG` | `"1"` | Debug mode (0/1) |
| `DEGOOG_URL` | `"http://localhost:4444"` | URL dell'istanza Degoog |
| `SEARXNG_URL` | `"http://localhost:8080"` | URL dell'istanza SearXNG |
| `SIBILLA_DDG_FALLBACK` | `"1"` | Abilita fallback DuckDuckGo (0/1) |
---
## Avvio
### Sviluppo
```bash
# Modo 1: CLI
sibilla
# Modo 2: Python module
python -m sibilla
# Modo 3: Flask diretto (con hot reload)
python -c "from sibilla.app import create_app; create_app().run(debug=True)"
```
### Produzione (Gunicorn)
```bash
# Installa Gunicorn
pip install gunicorn
# Avvia
gunicorn -w 4 -b 0.0.0.0:5000 "sibilla.app:create_app()"
# Con opzioni aggiuntive
gunicorn \
-w 4 \
-b 0.0.0.0:5000 \
--timeout 120 \
--access-logfile /var/log/sibilla/access.log \
--error-logfile /var/log/sibilla/error.log \
"sibilla.app:create_app()"
```
---
## Deployment in produzione
### Checklist pre-deployment
- [ ] `SIBILLA_DEBUG=0` (MAI in debug in produzione)
- [ ] `SECRET_KEY` cambiata con una chiave casuale lunga
- [ ] `GOOGLE_API_KEY` configurata
- [ ] Reverse proxy (nginx/Caddy) configurato con HTTPS
- [ ] Firewall: solo porta 80/443 esposta
- [ ] Rate limiting configurato (nginx o middleware)
- [ ] Logging configurato
- [ ] Health check monitorato
### Architettura di produzione consigliata
```
Internet
│
▼
┌──────────────────┐
│ Nginx / Caddy │ ← Reverse proxy + HTTPS (Let's Encrypt)
│ Porta 80/443 │ + Rate limiting
└────────┬─────────┘
│
▼
┌──────────────────┐
│ Gunicorn │ ← WSGI server
│ 4 workers │ Porta 5000 (internal)
└────────┬─────────┘
│
▼
┌──────────────────┐
│ Sibilla Flask │ ← Application
└────────┬─────────┘
│
┌────┴────┐
▼ ▼
┌────────┐ ┌────────┐
│ Degoog │ │ Gemini │
│ :4444 │ │ API │
└────────┘ └────────┘
```
---
## Docker Compose
Ecco un esempio di `docker-compose.yml` per avviare Sibilla + Degoog + SearXNG:
```yaml
version: "3.8"
services:
sibilla:
build: .
ports:
- "5000:5000"
environment:
- GOOGLE_API_KEY=${GOOGLE_API_KEY}
- SECRET_KEY=${SECRET_KEY:-changeme} # ⚠️ Cambiare in produzione!
- SIBILLA_HOST=0.0.0.0
- SIBILLA_PORT=5000
- SIBILLA_DEBUG=0
- DEGOOG_URL=http://degoog:4444
- SEARXNG_URL=http://searxng:8080
- SIBILLA_DDG_FALLBACK=1
depends_on:
- degoog
- searxng
restart: unless-stopped
degoog:
image: ghcr.io/degoog-org/degoog:latest
ports:
- "4444:4444"
restart: unless-stopped
searxng:
image: searxng/searxng:latest
ports:
- "8080:8080"
environment:
- SEARXNG_BASE_URL=http://localhost:8080/
volumes:
- ./searxng:/etc/searxng:rw
restart: unless-stopped
```
Avvia con:
```bash
docker compose up -d
```
---
## Reverse Proxy
### Nginx
```nginx
server {
listen 80;
server_name sibilla.tuo-dominio.com;
# Rate limiting
limit_req_zone $binary_remote_addr zone=sibilla:10m rate=10r/m;
limit_req zone=sibilla burst=5 nodelay;
location / {
proxy_pass http://127.0.0.1:5000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 120s;
}
# API endpoint with higher rate limit
location /api/ {
proxy_pass http://127.0.0.1:5000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_read_timeout 120s;
}
}
```
### Caddy (più semplice, auto-HTTPS)
```
sibilla.tuo-dominio.com {
reverse_proxy 127.0.0.1:5000
}
```
---
## Systemd Service
Crea `/etc/systemd/system/sibilla.service`:
```ini
[Unit]
Description=Sibilla AI Search Engine
After=network.target
[Service]
Type=simple
User=sibilla
Group=sibilla
WorkingDirectory=/opt/sibilla
EnvironmentFile=/opt/sibilla/.env
ExecStart=/opt/sibilla/.venv/bin/gunicorn -w 4 -b 127.0.0.1:5000 "sibilla.app:create_app()"
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
```
Attiva:
```bash
sudo systemctl daemon-reload
sudo systemctl enable --now sibilla
sudo systemctl status sibilla
```
---
## Monitoraggio
### Health check endpoint
```bash
curl http://localhost:5000/api/health
```
Monitora con:
- **Uptime Kuma** — monitoraggio self-hosted
- **Healthchecks.io** — cron monitoring
- **Prometheus + Grafana** — metriche avanzate (da aggiungere)
### Log
Con Gunicorn:
```bash
# Access log
tail -f /var/log/sibilla/access.log
# Error log
tail -f /var/log/sibilla/error.log
```
---
## Troubleshooting
### "GOOGLE_API_KEY is not set"
Il file `.env` non è stato caricato o manca la variabile:
```bash
# Verifica che il file .env esista
ls -la .env
# Verifica il contenuto
cat .env | grep GOOGLE_API_KEY
# Se usi systemd, assicurati che EnvironmentFile punti al percorso corretto
```
### "Connection refused" per Degoog/SearXNG
Il motore di ricerca non è in esecuzione:
```bash
# Verifica se Degoog è attivo
curl http://localhost:4444/api/search?q=test
# Verifica se SearXNG è attivo
curl http://localhost:8080/search?q=test&format=json
# Se non rispondono, Sibilla userà automaticamente DuckDuckGo
```
### "502 Bad Gateway" — Tutti i motori down
Verifica che DuckDuckGo fallback sia abilitato:
```env
SIBILLA_DDG_FALLBACK=1
```
Se anche DuckDuckGo non funziona, Gemini risponderà in modalità "pure LLM" (senza fonti).
### Timeout su richieste lente
Aumenta il timeout in Gunicorn:
```bash
gunicorn --timeout 180 ...
```
E nel reverse proxy:
```nginx
proxy_read_timeout 180s;
```
+268
View File
@@ -0,0 +1,268 @@
# 🔄 Sibilla — Diagramma di Flusso
> **Versione:** 0.2.0
> **Ultimo aggiornamento:** 18 Luglio 2026
---
## Pipeline di Ricerca Completa (Mermaid)
```mermaid
flowchart TD
A["👤 Utente invia query"] --> B["🌐 Web UI (POST /api/ask)<br/>o<br/>🔗 API (GET/POST /api/v1/query)"]
B --> C["🔮 Query Expansion<br/>Gemini genera 3 varianti"]
C --> D{"🔍 Degoog<br/>disponibile?"}
D -->|✅ Sì| E["Cerca con Degoog<br/>prime 2 varianti"]
D -->|❌ No| F{"🔍 SearXNG<br/>disponibile?"}
E --> G{"Risultati<br/>trovati?"}
G -->|✅ Sì| H["Aggrega risultati<br/>Dedup URL"]
G -->|❌ No| F
F -->|✅ Sì| I["Cerca con SearXNG<br/>prime 2 varianti"]
F -->|❌ No| J["🔍 DuckDuckGo<br/>fallback"]
I --> K{"Risultati<br/>trovati?"}
K -->|✅ Sì| H
K -->|❌ No| J
J --> L{"Risultati<br/>trovati?"}
L -->|✅ Sì| H
L -->|❌ No| M{"Tutti i backend<br/>falliti?"}
M -->|✅ Sì| N["⚠️ Fallback Pure LLM<br/>Gemini risponde senza fonti"]
M -->|❌ No| O["❌ Errore: 502<br/>Restituisci errore"]
H --> P["📥 PageFetcher<br/>Scarica full content<br/>max 5 pagine, 300ms delay"]
P --> Q["📝 Build Prompt<br/>Fonti numerate + data"]
Q --> R["🧠 Gemini Synthesis<br/>SYNTHESIS_SYSTEM_PROMPT<br/>temp=0.4, max_tokens=4096"]
R --> S["✅ JSON Response<br/>answer + html_answer<br/>+ speakable_text + sources"]
N --> T["✅ JSON Response<br/>solo LLM, nessuna fonte"]
style A fill:#a78bfa,color:#fff
style S fill:#34d399,color:#000
style T fill:#fbbf24,color:#000
style O fill:#f87171,color:#fff
style R fill:#8b5cf6,color:#fff
```
---
## Flusso Richiesta Web (Frontend)
```mermaid
sequenceDiagram
actor U as 👤 Utente
participant B as Browser (app.js)
participant F as Flask /api/ask
participant P as Pipeline
participant G as Gemini
participant S as Search Engines
U->>B: Scrivi query e clicca "Interroga"
B->>B: Disabilita form, mostra spinner
B->>F: POST /api/ask {query}
F->>P: _run_search_pipeline(query, num_sources)
P->>G: rewrite_query(query, 3 varianti)
G-->>P: [originale, var1, var2, var3]
P->>S: Degoog.search(var1) + Degoog.search(var2)
S-->>P: SearchResponse {results[]}
P->>S: PageFetcher.fetch_results(top5)
S-->>P: SearchResult[] con full_content
P->>G: synthesize(system_prompt, user_prompt)
G-->>P: SynthesisResult {answer, tokens}
P-->>F: enriched[], response_data
F-->>B: JSON {answer, html_answer, speakable_text, sources[]}
B->>B: Render HTML answer, mostra fonti
B->>U: Mostra risultato
```
---
## Flusso API v1
```mermaid
sequenceDiagram
actor C as 🔗 Client (curl/Lambda)
participant F as Flask /api/v1/query
participant P as Pipeline
participant G as Gemini
participant S as Search Engines
C->>F: GET /api/v1/query?q=domanda
F->>F: Estrai query da query string
F->>P: _run_search_pipeline(query, num_sources=5)
P->>G: rewrite_query(query, 3 varianti)
G-->>P: [originale, var1, var2, var3]
alt Degoog OK
P->>S: Degoog.search(varianti)
S-->>P: Risultati
else SearXNG fallback
P->>S: SearXNG.search(varianti)
S-->>P: Risultati
else DuckDuckGo fallback
P->>S: DDG.search(query)
S-->>P: Risultati
else Pure LLM
P->>G: synthesize(FALLBACK_PROMPT, query)
G-->>P: Risposta senza fonti
end
P->>S: PageFetcher.fetch_results(top5)
S-->>P: Contenuti completi
P->>G: synthesize(SYNTHESIS_PROMPT, fonti)
G-->>P: SynthesisResult
P-->>F: enriched[], response_data
F-->>C: JSON {query, answer, speakable_text, sources[]}
```
---
## Flusso MCP Server
```mermaid
sequenceDiagram
actor A as 🤖 AI Agent (Claude/Gemini)
participant M as MCP Server (stdio)
participant D as DegoogClient
participant S as SearXNGClient
participant G as DuckDuckGoClient
participant F as PageFetcher
A->>M: JSON-RPC: initialize
M-->>A: {protocolVersion, capabilities, serverInfo}
A->>M: JSON-RPC: tools/list
M-->>A: {tools: [web_search, fetch_page]}
A->>M: JSON-RPC: tools/call {name: "web_search", arguments: {query: "..."}}
M->>D: search(query)
alt Degoog ha risultati
D-->>M: SearchResponse
else Degoog vuoto
M->>S: search(query)
alt SearXNG ha risultati
S-->>M: SearchResponse
else Entrambi vuoti
M->>G: search(query)
G-->>M: SearchResponse
end
end
M-->>A: {content: [{text: "Risultati formattati..."}]}
A->>M: JSON-RPC: tools/call {name: "fetch_page", arguments: {url: "..."}}
M->>F: fetch(url)
F-->>M: Testo estratto (max 8000 chars)
M-->>A: {content: [{text: "Contenuto pagina..."}]}
```
---
## Macchina a Stati: Pipeline di Ricerca
```mermaid
stateDiagram-v2
[*] --> ReceivingQuery: Utente invia query
ReceivingQuery --> QueryExpansion: Query valida
ReceivingQuery --> Error400: Query vuota
QueryExpansion --> DegoogSearch: Gemini espande query
DegoogSearch --> AggregatingResults: Risultati trovati
DegoogSearch --> SearXNGFallback: Nessun risultato
SearXNGFallback --> AggregatingResults: Risultati trovati
SearXNGFallback --> DDGFallback: Nessun risultato
DDGFallback --> AggregatingResults: Risultati trovati
DDGFallback --> PureLLMFallback: Nessun risultato
AggregatingResults --> FetchingContent: Dedup URL completato
FetchingContent --> BuildingPrompt: Full content scaricato
BuildingPrompt --> Synthesizing: Prompt assemblato
Synthesizing --> ReturningResponse: Sintesi completata
Synthesizing --> ReturningErrorResponse: Errore Gemini
PureLLMFallback --> SynthesizingFallback: Usa FALLBACK_PROMPT
SynthesizingFallback --> ReturningFallback: Risposta senza fonti
Error400 --> [*]
ReturningResponse --> [*]
ReturningErrorResponse --> [*]
ReturningFallback --> [*]
```
---
## Diagramma dei Componenti
```mermaid
graph TB
subgraph "Interfacce Utente"
WEB["🌐 Web UI<br/>templates/index.html<br/>static/app.js"]
API["🔗 REST API<br/>/api/ask<br/>/api/v1/query"]
MCP["🤖 MCP Server<br/>stdio JSON-RPC"]
end
subgraph "Flask Application"
APP["app.py<br/>Flask App Factory"]
PIPELINE["_run_search_pipeline()<br/>Pipeline condivisa"]
end
subgraph "Servizi Core"
AI["ai.py<br/>GeminiClient"]
SEARCH["search.py"]
MD["markdown.py<br/>Render + Strip"]
PROMPTS["prompts.py<br/>System Prompts"]
end
subgraph "Backend Esterni"
GEMINI["🧠 Google Gemini API<br/>gemini-2.5-flash"]
DEGOOG["🔍 Degoog<br/>self-hosted"]
SEARXNG["🔍 SearXNG<br/>self-hosted"]
DDG["🔍 DuckDuckGo<br/>HTML scraping"]
end
WEB --> APP
API --> APP
MCP --> SEARCH
APP --> PIPELINE
PIPELINE --> AI
PIPELINE --> SEARCH
PIPELINE --> PROMPTS
PIPELINE --> MD
AI --> GEMINI
SEARCH --> DEGOOG
SEARCH --> SEARXNG
SEARCH --> DDG
```
---
## Strategia di Query Expansion
```mermaid
flowchart LR
Q["Query originale:<br/>'ultimo film sull Odissea'"] --> GE["Gemini rewrite_query()"]
GE --> V1["Variante 1:<br/>film Odissea 2026 recensione"]
GE --> V2["Variante 2:<br/>ultimo adattamento cinematografico Odissea"]
GE --> V3["Variante 3:<br/>The Odyssey movie latest release"]
V1 --> D1["Degoog search"]
V2 --> D2["Degoog search"]
D1 --> AGG["Aggregazione<br/>+ Deduplicazione"]
D2 --> AGG
```
---
## Legenda
| Simbolo | Significato |
|---------|------------|
| 🔍 | Motore di ricerca |
| 🧠 | LLM / AI |
| 📥 | Fetch / Download |
| 📝 | Costruzione prompt |
| ✅ | Successo |
| ❌ | Errore / Fallimento |
| ⚠️ | Degraded mode |
| 👤 | Interazione utente |
| 🔗 | API / Programmatico |
| 🤖 | Agente AI esterno |
+143
View File
@@ -0,0 +1,143 @@
# 📚 Sibilla — Wiki
> Wiki tecnica del progetto Sibilla — motore di ricerca AI self-hosted stile Perplexity.
---
## Cosa è Sibilla?
**Sibilla** è un'applicazione Python/Flask che implementa un motore di ricerca AI self-hosted, simile a Perplexity AI. Ti permette di:
- Fare domande in linguaggio naturale
- Ottenere risposte sintetizzate da Google Gemini basate su fonti web reali
- Visualizzare le fonti con link e citazioni inline
- Usare l'API REST per integrare Sibilla in altri servizi (skill Alexa, bot, app mobile)
- Esporre la ricerca web come tool via MCP (Model Context Protocol) per agent AI
---
## Perché si chiama Sibilla?
Le **Sibille** erano figure della mitologia greco-romana: profetesse che pronunciavano oracoli. Come l'oracolo, Sibilla "vede" nel web e "profetizza" risposte basate sulle fonti che consulta.
---
## Funzionalità
### ✅ Implementate
| Funzionalità | Descrizione |
|-------------|-------------|
| 🔍 **Multi-engine search** | Degoog → SearXNG → DuckDuckGo con fallback a cascata |
| 🧠 **Sintesi AI** | Google Gemini sintetizza risposte con citazioni [1], [2], ... |
| 📝 **Query expansion** | Gemini genera varianti di query per migliorare il recall |
| 📄 **Full content fetching** | Scarica e analizza il contenuto completo delle pagine |
| 🌐 **Interfaccia web** | UI responsive, dark theme, mobile-first |
| 📡 **API REST** | Endpoint `/api/ask` (web) e `/api/v1/query` (API) |
| 🗣️ **TTS-ready** | Campo `speakable_text` ottimizzato per sintesi vocale |
| 🔧 **MCP server** | Server Model Context Protocol per agent AI esterni |
| 🛡️ **XSS protection** | Sanitizzazione HTML con allowlist |
| 🔄 **CORS** | Header cross-origin per accesso da qualsiasi dominio |
| 📊 **Metadati** | Token count, modello usato, search engine usato |
### 🚧 In programma
| Funzionalità | Priorità |
|-------------|----------|
| Rate limiting API | Alta |
| Caching risposte (Redis) | Media |
| Multi-lingua (oltre IT/EN) | Media |
| Autenticazione API key | Media |
| Streaming risposta (SSE) | Bassa |
| Supporto immagini nei risultati | Bassa |
| Docker Compose (Sibilla + Degoog + SearXNG) | Bassa |
---
## Glossario
| Termine | Definizione |
|---------|------------|
| **Sintesi** | Processo con cui Gemini genera una risposta basata sulle fonti |
| **Query Expansion** | Generazione di varianti di una query per migliorare la ricerca |
| **Cascading Fallback** | Strategia di fallback a catena: prova A, poi B, poi C |
| **Full Content** | Contenuto testuale completo di una pagina web (dopo scraping) |
| **Sanitizzazione HTML** | Rimozione di tag/attributi pericolosi per prevenire XSS |
| **MCP** | Model Context Protocol — standard per esporre tool a LLM |
| **TTS** | Text-to-Speech — sintesi vocale (es. Alexa, Google Home) |
| **Degoog** | Metasearch engine self-hosted, alternativa leggera a SearXNG |
---
## Domande Frequenti (FAQ)
### Cosa serve per far funzionare Sibilla?
Come minimo:
1. **Python 3.10+**
2. **Google API Key** (per Gemini)
3. Almeno un motore di ricerca:
- **Degoog** (consigliato) — `docker run -d -p 4444:4444 ghcr.io/degoog-org/degoog:latest`
- **SearXNG** — `docker run -d -p 8080:8080 searxng/searxng`
- **DuckDuckGo** — funziona senza server (fallback automatico)
### Posso usare Sibilla senza Degoog/SearXNG?
Sì! Sibilla userà automaticamente DuckDuckGo come fallback (scraping HTML, nessuna API key necessaria). Puoi disabilitarlo con `SIBILLA_DDG_FALLBACK=0`.
### Quanto costa?
- **Sibilla:** gratuito e open-source (MIT)
- **Degoog:** gratuito, self-hosted
- **SearXNG:** gratuito, self-hosted
- **Gemini API:** ha un tier gratuito (vedi [Google AI Studio](https://aistudio.google.com/apikey))
### Posso cambiare il modello LLM?
Sì! Modifica il parametro `model` quando crei `GeminiClient` o cambia il default in `ai.py`. Supporta qualsiasi modello Gemini (es. `gemini-2.5-pro`, `gemini-1.5-flash`).
### Sibilla supporta altre lingue?
Sì! I prompt sono in italiano ma Gemini risponde nella lingua della domanda. Puoi modificare i prompt in `prompts.py` per altre lingue.
### Come integro Sibilla con la mia app?
Usa l'endpoint `/api/v1/query`:
```bash
curl "http://localhost:5000/api/v1/query?q=tua+domanda"
```
Vedi `docs/API.md` per la documentazione completa.
### Sibilla funziona dietro un reverse proxy (nginx, Caddy)?
Sì! Sibilla è una normale app Flask/WSGI. Puoi usare `gunicorn` come server WSGI e nginx/Caddy come reverse proxy. Vedi `docs/DEPLOYMENT.md`.
---
## Confronto con Perplexity
| Caratteristica | Perplexity | Sibilla |
|---------------|-----------|---------|
| **Hosting** | Cloud (perplexity.ai) | Self-hosted |
| **Costo** | $20/mese (Pro) | Gratuito (+ costi API Gemini) |
| **Privacy** | Dati su server Perplexity | Dati sul tuo server |
| **Motori di ricerca** | Interni di Perplexity | Degoog, SearXNG, DuckDuckGo |
| **LLM** | Modelli proprietari | Google Gemini |
| **API** | Via perplexity.ai | Locale, REST/MCP |
| **Personalizzazione** | Limitata | Completa (open source) |
| **MCP** | No | Sì |
| **TTS support** | No | Sì (via `speakable_text`) |
---
## Roadmap
```
v0.1.0 ✅ — MVP: ricerca + sintesi con Degoog/SearXNG/DDG + Gemini
v0.2.0 ✅ — API v1, TTS, CORS, documentazione completa
v0.3.0 📋 — Rate limiting, caching, autenticazione API key
v0.4.0 📋 — Docker Compose, streaming SSE, multi-lingua
v1.0.0 📋 — Stable release, test coverage 90%+
```