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:
1 parent
f3fe13c60e
commit
76bc667adf
20 files changed
+3385
-245
No files matched your search
@@ -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`
|
||||
Reference in new issue
Block a user