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

+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`