✨ 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
17 KiB
🏗️ Sibilla — Architettura di Sistema
Autore: Sibilla Team
Versione: 0.2.0
Ultimo aggiornamento: 18 Luglio 2026
Indice
- Visione d'insieme
- Stack tecnologico
- Componenti principali
- Struttura del progetto
- Pipeline di ricerca
- Modello dati
- Flusso delle richieste
- Strategia di fallback
- Sicurezza e sanitizzazione
- 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.
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 fontirewrite_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 XSSstrip_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 citazioniFALLBACK_PROMPT— Prompt quando non ci sono risultati di ricercaQUERY_REWRITE_PROMPT— Prompt per la query expansionbuild_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 cascatafetch_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
@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
@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
@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)
{
"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
{
"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 suhrefesrc - Nessun event handler (
onclick,onerror, ecc.) - Strip di
<script>,<iframe>,<object>
CORS
Access-Control-Allow-Origin: *— permissivo per API pubblicheAccess-Control-Allow-Methods: GET, POST, OPTIONSAccess-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
- Creare una classe con interfaccia
search(query, num_results) → SearchResponse - Aggiungere il client in
_run_search_pipeline()nella catena di fallback - Aggiungere test in
tests/test_search.py
Aggiungere un nuovo endpoint API
- Aggiungere una route in
app.py - Riutilizzare
_run_search_pipeline()per la logica condivisa - Aggiungere test in
tests/test_app.py
Aggiungere un nuovo tool MCP
- Aggiungere il tool in
MCPServer._build_tools() - Implementare il metodo handler
- Registrare in
_handle_request()sottotools/call
Aggiungere un nuovo modello LLM
- Creare un client con interfaccia
synthesize(system_prompt, user_prompt, ...) → SynthesisResult - Iniettare il client in
_run_search_pipeline() - Aggiungere test in
tests/test_ai.py