# πŸ—οΈ 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": "

Versione HTML della risposta...

", "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 `