Files
sibilla/docs/ARCHITECTURE.md
Sibilla 76bc667adf 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
2026-07-18 10:05:46 +00:00

17 KiB

🏗️ Sibilla — Architettura di Sistema

Autore: Sibilla Team
Versione: 0.2.0
Ultimo aggiornamento: 18 Luglio 2026


Indice

  1. Visione d'insieme
  2. Stack tecnologico
  3. Componenti principali
  4. Struttura del progetto
  5. Pipeline di ricerca
  6. Modello dati
  7. Flusso delle richieste
  8. Strategia di fallback
  9. Sicurezza e sanitizzazione
  10. 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 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

@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 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