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
+315
@@ -0,0 +1,315 @@
|
||||
# 📡 Sibilla — Documentazione API REST
|
||||
|
||||
> **Versione:** 0.2.0
|
||||
> **Ultimo aggiornamento:** 18 Luglio 2026
|
||||
|
||||
---
|
||||
|
||||
## Indice
|
||||
|
||||
1. [Panoramica](#panoramica)
|
||||
2. [Health Check](#health-check)
|
||||
3. [API Ask (Web)](#api-ask)
|
||||
4. [API v1 Query (Programmatica)](#api-v1-query)
|
||||
5. [Codici di errore](#codici-di-errore)
|
||||
6. [Esempi curl](#esempi-curl)
|
||||
7. [Integrazione Alexa Skill](#integrazione-alexa-skill)
|
||||
|
||||
---
|
||||
|
||||
## Panoramica
|
||||
|
||||
Tutti gli endpoint sono accessibili su `http://localhost:5000` (o l'host/porta configurati).
|
||||
|
||||
| Metodo | Endpoint | Descrizione | Formato |
|
||||
|--------|----------|-------------|---------|
|
||||
| `GET` | `/` | Interfaccia web HTML | `text/html` |
|
||||
| `GET` | `/api/health` | Health check | `application/json` |
|
||||
| `POST` | `/api/ask` | Ricerca + sintesi (web UI) | `application/json` |
|
||||
| `GET` | `/api/v1/query` | Ricerca + sintesi (API, query string) | `application/json` |
|
||||
| `POST` | `/api/v1/query` | Ricerca + sintesi (API, JSON body) | `application/json` |
|
||||
|
||||
**CORS:** Tutti gli endpoint includono header CORS permissivi (`Access-Control-Allow-Origin: *`).
|
||||
|
||||
---
|
||||
|
||||
## Health Check
|
||||
|
||||
### `GET /api/health`
|
||||
|
||||
Verifica lo stato del server e la configurazione.
|
||||
|
||||
**Request:**
|
||||
```bash
|
||||
curl http://localhost:5000/api/health
|
||||
```
|
||||
|
||||
**Response `200 OK`:**
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"version": "0.2.0",
|
||||
"gemini_configured": true,
|
||||
"degoog_url": "http://localhost:4444",
|
||||
"searxng_url": "http://localhost:8080"
|
||||
}
|
||||
```
|
||||
|
||||
| Campo | Tipo | Descrizione |
|
||||
|-------|------|-------------|
|
||||
| `status` | `string` | Sempre `"ok"` se il server risponde |
|
||||
| `version` | `string` | Versione di Sibilla |
|
||||
| `gemini_configured` | `boolean` | `true` se `GOOGLE_API_KEY` è configurata |
|
||||
| `degoog_url` | `string` | URL dell'istanza Degoog |
|
||||
| `searxng_url` | `string` | URL dell'istanza SearXNG |
|
||||
|
||||
---
|
||||
|
||||
## API Ask
|
||||
|
||||
### `POST /api/ask`
|
||||
|
||||
Endpoint principale per la ricerca con sintesi AI. Utilizzato dall'interfaccia web.
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"query": "Qual è stato l'ultimo film sull'Odissea?",
|
||||
"num_sources": 5
|
||||
}
|
||||
```
|
||||
|
||||
| Parametro | Tipo | Default | Range | Descrizione |
|
||||
|-----------|------|---------|-------|-------------|
|
||||
| `query` | `string` | *richiesto* | — | La domanda da cercare |
|
||||
| `num_sources` | `integer` | `5` | `1–10` | Numero massimo di fonti da usare |
|
||||
|
||||
**Response `200 OK`:**
|
||||
```json
|
||||
{
|
||||
"answer": "L'ultimo adattamento cinematografico dell'**Odissea** è *The Return* (2024)...",
|
||||
"html_answer": "<p>L'ultimo adattamento cinematografico dell'<strong>Odissea</strong> è <em>The Return</em> (2024)...</p>",
|
||||
"speakable_text": "L'ultimo adattamento cinematografico dell'Odissea è The Return del 2024...",
|
||||
"sources": [
|
||||
{
|
||||
"index": 1,
|
||||
"title": "The Return - Wikipedia",
|
||||
"url": "https://it.wikipedia.org/wiki/The_Return_(film_2024)",
|
||||
"snippet": "The Return è un film del 2024 diretto da Uberto Pasolini..."
|
||||
}
|
||||
],
|
||||
"model": "gemini-2.5-flash",
|
||||
"search_engine": "degoog",
|
||||
"tokens_used": 1847,
|
||||
"error": null
|
||||
}
|
||||
```
|
||||
|
||||
| Campo | Tipo | Descrizione |
|
||||
|-------|------|-------------|
|
||||
| `answer` | `string` | Risposta in formato **Markdown** |
|
||||
| `html_answer` | `string` | Risposta in **HTML** sicuro (sanitizzato) |
|
||||
| `speakable_text` | `string` | Risposta in **plain text**, ottimizzata per TTS |
|
||||
| `sources` | `array` | Lista delle fonti utilizzate |
|
||||
| `sources[].index` | `integer` | Numero della fonte (1-based) |
|
||||
| `sources[].title` | `string` | Titolo della pagina |
|
||||
| `sources[].url` | `string` | URL della fonte |
|
||||
| `sources[].snippet` | `string` | Estratto della pagina (max 200 caratteri) |
|
||||
| `model` | `string` | Modello LLM utilizzato |
|
||||
| `search_engine` | `string` | Motore di ricerca usato (`degoog`, `searxng`, `duckduckgo`, `none`) |
|
||||
| `tokens_used` | `integer` | Token totali consumati |
|
||||
| `error` | `string` or `null` | Eventuale messaggio di errore |
|
||||
|
||||
**Response `400 Bad Request`:**
|
||||
```json
|
||||
{
|
||||
"error": "La query non può essere vuota."
|
||||
}
|
||||
```
|
||||
|
||||
**Response `502 Bad Gateway`:**
|
||||
```json
|
||||
{
|
||||
"error": "Ricerca web fallita: Connection refused",
|
||||
"answer": "",
|
||||
"sources": []
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API v1 Query
|
||||
|
||||
### `GET /api/v1/query`
|
||||
|
||||
Endpoint semplificato per consumo programmatico. Accetta la query via query string.
|
||||
|
||||
**Request:**
|
||||
```bash
|
||||
curl "http://localhost:5000/api/v1/query?q=ultimo+film+sull%27Odissea"
|
||||
```
|
||||
|
||||
### `POST /api/v1/query`
|
||||
|
||||
Stesso endpoint, con query nel body JSON.
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"query": "Qual è stato l'ultimo film sull'Odissea?"
|
||||
}
|
||||
```
|
||||
|
||||
| Parametro | Tipo | Default | Descrizione |
|
||||
|-----------|------|---------|-------------|
|
||||
| `q` (GET) | `string` | *richiesto* | La query di ricerca (URL-encoded) |
|
||||
| `query` (POST) | `string` | *richiesto* | La query di ricerca |
|
||||
|
||||
**Response `200 OK`:**
|
||||
```json
|
||||
{
|
||||
"query": "Qual è stato l'ultimo film sull'Odissea?",
|
||||
"answer": "L'ultimo adattamento è **The Return** (2024), diretto da Uberto Pasolini...",
|
||||
"speakable_text": "L'ultimo adattamento è The Return del 2024, diretto da Uberto Pasolini...",
|
||||
"sources": [
|
||||
{
|
||||
"index": 1,
|
||||
"title": "The Return - Wikipedia",
|
||||
"url": "https://it.wikipedia.org/wiki/The_Return_(film_2024)"
|
||||
}
|
||||
],
|
||||
"model": "gemini-2.5-flash",
|
||||
"search_engine": "degoog",
|
||||
"tokens_used": 1847
|
||||
}
|
||||
```
|
||||
|
||||
| Campo | Tipo | Descrizione |
|
||||
|-------|------|-------------|
|
||||
| `query` | `string` | Echo della query originale |
|
||||
| `answer` | `string` | Risposta in formato **Markdown** |
|
||||
| `speakable_text` | `string` | **Plain text** ottimizzato per sintesi vocale (TTS) |
|
||||
| `sources` | `array` | Fonti semplificate (solo `index`, `title`, `url`) |
|
||||
| `model` | `string` | Modello LLM utilizzato |
|
||||
| `search_engine` | `string` | Motore di ricerca usato |
|
||||
| `tokens_used` | `integer` | Token totali consumati |
|
||||
|
||||
> **Nota:** Il formato è più compatto di `/api/ask` — pensato per API consumer come skill Alexa, Lambda function, o mobile app.
|
||||
|
||||
**Response `400 Bad Request`:**
|
||||
```json
|
||||
{
|
||||
"error": "Missing query parameter 'q'."
|
||||
}
|
||||
```
|
||||
|
||||
**Response `502 Bad Gateway`:**
|
||||
```json
|
||||
{
|
||||
"error": "Ricerca web fallita: Connection refused",
|
||||
"answer": "",
|
||||
"speakable_text": ""
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Codici di errore
|
||||
|
||||
| HTTP Status | Significato | Quando |
|
||||
|-------------|-------------|--------|
|
||||
| `200` | OK | Richiesta completata con successo |
|
||||
| `400` | Bad Request | Query mancante o vuota |
|
||||
| `502` | Bad Gateway | Tutti i motori di ricerca sono down |
|
||||
|
||||
**Errori interni gestiti:**
|
||||
- Se tutti i motori falliscono ma Gemini è disponibile → fallback a risposta solo LLM (200 OK)
|
||||
- Se Gemini non è configurato → `answer` vuoto con `error` nel JSON
|
||||
- Se Gemini restituisce errore → messaggio di errore inline in `answer`
|
||||
|
||||
---
|
||||
|
||||
## Esempi curl
|
||||
|
||||
### Health check
|
||||
```bash
|
||||
curl -s http://localhost:5000/api/health | python -m json.tool
|
||||
```
|
||||
|
||||
### Ricerca via API v1 (GET)
|
||||
```bash
|
||||
curl -s "http://localhost:5000/api/v1/query?q=cosa+sono+i+raggi+cosmici" | python -m json.tool
|
||||
```
|
||||
|
||||
### Ricerca via API v1 (POST)
|
||||
```bash
|
||||
curl -s -X POST http://localhost:5000/api/v1/query \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"query": "cosa sono i raggi cosmici"}' | python -m json.tool
|
||||
```
|
||||
|
||||
### Estrai solo la risposta speakable (per TTS)
|
||||
```bash
|
||||
curl -s "http://localhost:5000/api/v1/query?q=raggi+cosmici" | python -c "import sys,json; print(json.load(sys.stdin)['speakable_text'])"
|
||||
```
|
||||
|
||||
### Ricerca via web UI (POST /api/ask)
|
||||
```bash
|
||||
curl -s -X POST http://localhost:5000/api/ask \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"query": "raggi cosmici", "num_sources": 3}' | python -m json.tool
|
||||
```
|
||||
|
||||
### Verifica errori
|
||||
```bash
|
||||
# Query vuota → 400
|
||||
curl -i "http://localhost:5000/api/v1/query?q="
|
||||
|
||||
# Motori down → 502 (se tutti falliscono)
|
||||
curl -i "http://localhost:5000/api/v1/query?q=test"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Integrazione Alexa Skill
|
||||
|
||||
Ecco un esempio di come usare l'endpoint `/api/v1/query` da una Lambda Python per una skill Alexa:
|
||||
|
||||
```python
|
||||
import json
|
||||
import urllib.request
|
||||
|
||||
SIBILLA_URL = "https://tuo-server.com/api/v1/query"
|
||||
|
||||
def call_sibilla(query: str) -> dict:
|
||||
"""Chiama Sibilla e restituisce la risposta parlata."""
|
||||
params = urllib.parse.urlencode({"q": query})
|
||||
url = f"{SIBILLA_URL}?{params}"
|
||||
|
||||
with urllib.request.urlopen(url, timeout=30) as resp:
|
||||
data = json.loads(resp.read().decode())
|
||||
|
||||
return {
|
||||
"answer": data["answer"],
|
||||
"speakable_text": data["speakable_text"],
|
||||
"sources": data["sources"],
|
||||
}
|
||||
|
||||
# In un handler Alexa:
|
||||
def lambda_handler(event, context):
|
||||
query = event["request"]["intent"]["slots"]["query"]["value"]
|
||||
result = call_sibilla(query)
|
||||
|
||||
return {
|
||||
"version": "1.0",
|
||||
"response": {
|
||||
"outputSpeech": {
|
||||
"type": "SSML",
|
||||
"ssml": f"<speak>{result['speakable_text']}</speak>"
|
||||
},
|
||||
"shouldEndSession": True
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> **Nota:** Il campo `speakable_text` è già pulito da markup Markdown, URL, grassetti e codice — pronto per essere letto da un motore TTS.
|
||||
@@ -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`
|
||||
@@ -0,0 +1,217 @@
|
||||
# 🤝 Sibilla — Guida alla Contribuzione
|
||||
|
||||
> **Versione:** 0.2.0
|
||||
> **Ultimo aggiornamento:** 18 Luglio 2026
|
||||
|
||||
---
|
||||
|
||||
Grazie per il tuo interesse a contribuire a Sibilla! 🎉
|
||||
|
||||
## Codice di Condotta
|
||||
|
||||
Sii rispettoso, costruttivo e inclusivo. Tratta gli altri come vorresti essere trattato.
|
||||
|
||||
---
|
||||
|
||||
## Come Contribuire
|
||||
|
||||
### 1. Segnala un Bug
|
||||
|
||||
Apri una **issue** su GitHub con:
|
||||
|
||||
- **Titolo descrittivo** (es. "Errore 502 quando Degoog è lento")
|
||||
- **Passi per riprodurre il bug**
|
||||
- **Comportamento atteso** vs **comportamento osservato**
|
||||
- **Log di errore** (se disponibili)
|
||||
- **Ambiente:** versione Python, OS, versione Sibilla
|
||||
|
||||
### 2. Proponi una Feature
|
||||
|
||||
Apri una issue con il tag `enhancement` descrivendo:
|
||||
|
||||
- **Problema che risolve**
|
||||
- **Soluzione proposta**
|
||||
- **Alternative considerate**
|
||||
- **Impatto sul codice esistente**
|
||||
|
||||
### 3. Invia una Pull Request
|
||||
|
||||
#### Setup sviluppo
|
||||
|
||||
```bash
|
||||
# 1. Forka il repository
|
||||
# 2. Clona il tuo fork
|
||||
git clone https://github.com/tuo-user/sibilla.git
|
||||
cd sibilla
|
||||
|
||||
# 3. Aggiungi l'upstream
|
||||
git remote add upstream https://github.com/original/sibilla.git
|
||||
|
||||
# 4. Crea ambiente virtuale
|
||||
python3 -m venv .venv
|
||||
source .venv/bin/activate
|
||||
|
||||
# 5. Installa dipendenze di sviluppo
|
||||
pip install -e ".[dev]"
|
||||
|
||||
# 6. Configura .env
|
||||
cp .env.example .env
|
||||
nano .env # Inserisci GOOGLE_API_KEY
|
||||
```
|
||||
|
||||
#### Flusso di lavoro
|
||||
|
||||
```bash
|
||||
# 1. Sincronizza con upstream
|
||||
git fetch upstream
|
||||
git checkout master
|
||||
git merge upstream/master
|
||||
|
||||
# 2. Crea un branch
|
||||
git checkout -b feature/nome-feature
|
||||
# oppure
|
||||
git checkout -b fix/nome-bug
|
||||
|
||||
# 3. Sviluppa
|
||||
# ... scrivi codice ...
|
||||
|
||||
# 4. Testa
|
||||
pytest tests/ -v
|
||||
|
||||
# 5. Linta
|
||||
ruff check src tests
|
||||
|
||||
# 6. Committa con messaggi chiari
|
||||
git add .
|
||||
git commit -m "feat: aggiungi caching risposte con Redis"
|
||||
|
||||
# 7. Pusha
|
||||
git push origin feature/nome-feature
|
||||
|
||||
# 8. Apri Pull Request su GitHub
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Convenzioni del Codice
|
||||
|
||||
### Stile
|
||||
|
||||
- **Python 3.10+** con type hints
|
||||
- **Ruff** per linting e formattazione (`ruff check src tests`)
|
||||
- **Line length:** 100 caratteri
|
||||
- **Docstring:** stile Google (`"""Descrizione breve.` con `Args:`, `Returns:`, `Raises:`)
|
||||
|
||||
### Commit Messages
|
||||
|
||||
Usiamo [Conventional Commits](https://www.conventionalcommits.org/):
|
||||
|
||||
```
|
||||
feat: aggiungi caching risposte con Redis
|
||||
fix: correggi race condition in PageFetcher
|
||||
docs: aggiorna README con esempi API v1
|
||||
refactor: estrai pipeline condivisa in helper
|
||||
test: aggiungi test per strip_markdown
|
||||
chore: aggiorna dipendenze
|
||||
```
|
||||
|
||||
### Struttura dei Branch
|
||||
|
||||
| Prefisso | Uso |
|
||||
|----------|-----|
|
||||
| `feature/` | Nuove funzionalità |
|
||||
| `fix/` | Bug fix |
|
||||
| `docs/` | Documentazione |
|
||||
| `refactor/` | Refactoring |
|
||||
| `test/` | Test |
|
||||
| `chore/` | Manutenzione |
|
||||
|
||||
---
|
||||
|
||||
## Test
|
||||
|
||||
### Esecuzione
|
||||
|
||||
```bash
|
||||
# Tutti i test
|
||||
pytest tests/ -v
|
||||
|
||||
# Test specifico
|
||||
pytest tests/test_app.py -v
|
||||
|
||||
# Con coverage
|
||||
pip install pytest-cov
|
||||
pytest tests/ --cov=src/sibilla --cov-report=html
|
||||
```
|
||||
|
||||
### Scrivere Test
|
||||
|
||||
- **Nuove feature = nuovi test.** Ogni endpoint, funzione pubblica e classe deve essere testata.
|
||||
- **Naming:** `test_<funzione>_<scenario>()` (es. `test_strip_markdown_with_images`)
|
||||
- **Fixtures:** usa `create_app(testing=True)` per testare senza `.env`
|
||||
|
||||
Esempio:
|
||||
|
||||
```python
|
||||
def test_api_v1_query_returns_speakable_text(client):
|
||||
"""Verifica che /api/v1/query includa speakable_text."""
|
||||
# Dato che Gemini non è configurato, verifica la risposta di errore
|
||||
resp = client.get("/api/v1/query?q=test")
|
||||
data = resp.get_json()
|
||||
assert "speakable_text" in data
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pipeline di Ricerca
|
||||
|
||||
Per aggiungere un nuovo passaggio alla pipeline:
|
||||
|
||||
1. **Identifica il punto di inserimento** in `_run_search_pipeline()` in `app.py`
|
||||
2. **Implementa la logica** in una funzione o classe separata
|
||||
3. **Aggiungi test** per il nuovo comportamento
|
||||
4. **Documenta** in `docs/ARCHITECTURE.md`
|
||||
|
||||
### Aggiungere un Search Engine
|
||||
|
||||
1. Crea una classe in `search.py` che implementi `search(query, num_results) → SearchResponse`
|
||||
2. Aggiungi il client in `_run_search_pipeline()` nella catena di fallback
|
||||
3. Aggiungi test in `tests/test_search.py`
|
||||
|
||||
```python
|
||||
class MySearchClient:
|
||||
def search(self, query: str, num_results: int = 8) -> SearchResponse:
|
||||
# ... implementazione ...
|
||||
return SearchResponse(query=query, results=results, engine="myengine")
|
||||
```
|
||||
|
||||
### Aggiungere un Endpoint API
|
||||
|
||||
1. Aggiungi una route in `app.py`
|
||||
2. Riutilizza `_run_search_pipeline()` per la logica condivisa
|
||||
3. Aggiungi test in `tests/test_app.py`
|
||||
|
||||
```python
|
||||
@app.route("/api/v2/custom", methods=["POST"])
|
||||
def api_v2_custom():
|
||||
# ... usa _run_search_pipeline() ...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Documentazione
|
||||
|
||||
La documentazione vive in `docs/`. Quando modifichi una funzionalità, aggiorna:
|
||||
|
||||
- `docs/API.md` — se modifichi endpoint o formati di risposta
|
||||
- `docs/ARCHITECTURE.md` — se modifichi l'architettura
|
||||
- `docs/FLOW.md` — se modifichi la pipeline
|
||||
- `docs/WIKI.md` — per FAQ e roadmap
|
||||
|
||||
---
|
||||
|
||||
## Domande?
|
||||
|
||||
Apri una issue su GitHub o contatta i maintainer.
|
||||
|
||||
Grazie per contribuire a Sibilla! 🔮
|
||||
@@ -0,0 +1,402 @@
|
||||
# 🚀 Sibilla — Guida al Deployment
|
||||
|
||||
> **Versione:** 0.2.0
|
||||
> **Ultimo aggiornamento:** 18 Luglio 2026
|
||||
|
||||
---
|
||||
|
||||
## Indice
|
||||
|
||||
1. [Prerequisiti](#prerequisiti)
|
||||
2. [Installazione rapida](#installazione-rapida)
|
||||
3. [Configurazione](#configurazione)
|
||||
4. [Avvio](#avvio)
|
||||
5. [Deployment in produzione](#deployment-in-produzione)
|
||||
6. [Docker Compose](#docker-compose)
|
||||
7. [Reverse Proxy](#reverse-proxy)
|
||||
8. [Systemd Service](#systemd-service)
|
||||
9. [Monitoraggio](#monitoraggio)
|
||||
10. [Troubleshooting](#troubleshooting)
|
||||
|
||||
---
|
||||
|
||||
## Prerequisiti
|
||||
|
||||
### Software
|
||||
|
||||
| Componente | Versione minima | Note |
|
||||
|-----------|----------------|------|
|
||||
| Python | 3.10+ | |
|
||||
| pip | 21.0+ | |
|
||||
| Git | — | Opzionale, per clonare il repo |
|
||||
|
||||
### Servizi esterni
|
||||
|
||||
| Servizio | Obbligatorio? | Come ottenerlo |
|
||||
|----------|:---:|---------------|
|
||||
| Google API Key | ✅ Sì | [Google AI Studio](https://aistudio.google.com/apikey) |
|
||||
| Degoog | Consigliato | `docker run -d -p 4444:4444 ghcr.io/degoog-org/degoog:latest` |
|
||||
| SearXNG | No (fallback) | `docker run -d -p 8080:8080 searxng/searxng` |
|
||||
| DuckDuckGo | No (automatico) | Nessuna configurazione necessaria |
|
||||
|
||||
---
|
||||
|
||||
## Installazione rapida
|
||||
|
||||
```bash
|
||||
# 1. Clona il repository
|
||||
git clone https://github.com/tuo-user/sibilla.git
|
||||
cd sibilla
|
||||
|
||||
# 2. Crea ambiente virtuale
|
||||
python3 -m venv .venv
|
||||
source .venv/bin/activate # Linux/Mac
|
||||
# .venv\Scripts\activate # Windows
|
||||
|
||||
# 3. Installa dipendenze
|
||||
pip install -e ".[dev]"
|
||||
|
||||
# 4. Configura
|
||||
cp .env.example .env # se esiste, altrimenti crealo
|
||||
nano .env # Inserisci GOOGLE_API_KEY
|
||||
|
||||
# 5. Avvia (sviluppo)
|
||||
sibilla
|
||||
# → http://127.0.0.1:5000
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Configurazione
|
||||
|
||||
Crea un file `.env` nella root del progetto:
|
||||
|
||||
```env
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# Sibilla Configuration
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
# Google Gemini API key (obbligatoria)
|
||||
GOOGLE_API_KEY=AIzaSyD-xxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
|
||||
# Flask secret key (cambiala in produzione!)
|
||||
SECRET_KEY=una-chiave-segreta-molto-lunga-e-casuale
|
||||
|
||||
# ── Server ───────────────────────────────────────────────────
|
||||
|
||||
# Host e porta del server Flask
|
||||
SIBILLA_HOST=127.0.0.1
|
||||
SIBILLA_PORT=5000
|
||||
|
||||
# Debug mode (0 = off, 1 = on)
|
||||
# NON usare in produzione!
|
||||
SIBILLA_DEBUG=1
|
||||
|
||||
# ── Search Engines ────────────────────────────────────────────
|
||||
|
||||
# Degoog (primary)
|
||||
DEGOOG_URL=http://localhost:4444
|
||||
|
||||
# SearXNG (secondary)
|
||||
SEARXNG_URL=http://localhost:8080
|
||||
|
||||
# DuckDuckGo fallback (1 = on, 0 = off)
|
||||
SIBILLA_DDG_FALLBACK=1
|
||||
```
|
||||
|
||||
### Variabili d'ambiente complete
|
||||
|
||||
| Variabile | Default | Descrizione |
|
||||
|-----------|---------|-------------|
|
||||
| `GOOGLE_API_KEY` | `""` | **Obbligatoria.** API key di Google AI |
|
||||
| `SECRET_KEY` | `"dev-secret-key-change-me"` | Secret key Flask |
|
||||
| `SIBILLA_HOST` | `"127.0.0.1"` | Host su cui ascoltare |
|
||||
| `SIBILLA_PORT` | `"5000"` | Porta del server |
|
||||
| `SIBILLA_DEBUG` | `"1"` | Debug mode (0/1) |
|
||||
| `DEGOOG_URL` | `"http://localhost:4444"` | URL dell'istanza Degoog |
|
||||
| `SEARXNG_URL` | `"http://localhost:8080"` | URL dell'istanza SearXNG |
|
||||
| `SIBILLA_DDG_FALLBACK` | `"1"` | Abilita fallback DuckDuckGo (0/1) |
|
||||
|
||||
---
|
||||
|
||||
## Avvio
|
||||
|
||||
### Sviluppo
|
||||
|
||||
```bash
|
||||
# Modo 1: CLI
|
||||
sibilla
|
||||
|
||||
# Modo 2: Python module
|
||||
python -m sibilla
|
||||
|
||||
# Modo 3: Flask diretto (con hot reload)
|
||||
python -c "from sibilla.app import create_app; create_app().run(debug=True)"
|
||||
```
|
||||
|
||||
### Produzione (Gunicorn)
|
||||
|
||||
```bash
|
||||
# Installa Gunicorn
|
||||
pip install gunicorn
|
||||
|
||||
# Avvia
|
||||
gunicorn -w 4 -b 0.0.0.0:5000 "sibilla.app:create_app()"
|
||||
|
||||
# Con opzioni aggiuntive
|
||||
gunicorn \
|
||||
-w 4 \
|
||||
-b 0.0.0.0:5000 \
|
||||
--timeout 120 \
|
||||
--access-logfile /var/log/sibilla/access.log \
|
||||
--error-logfile /var/log/sibilla/error.log \
|
||||
"sibilla.app:create_app()"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Deployment in produzione
|
||||
|
||||
### Checklist pre-deployment
|
||||
|
||||
- [ ] `SIBILLA_DEBUG=0` (MAI in debug in produzione)
|
||||
- [ ] `SECRET_KEY` cambiata con una chiave casuale lunga
|
||||
- [ ] `GOOGLE_API_KEY` configurata
|
||||
- [ ] Reverse proxy (nginx/Caddy) configurato con HTTPS
|
||||
- [ ] Firewall: solo porta 80/443 esposta
|
||||
- [ ] Rate limiting configurato (nginx o middleware)
|
||||
- [ ] Logging configurato
|
||||
- [ ] Health check monitorato
|
||||
|
||||
### Architettura di produzione consigliata
|
||||
|
||||
```
|
||||
Internet
|
||||
│
|
||||
▼
|
||||
┌──────────────────┐
|
||||
│ Nginx / Caddy │ ← Reverse proxy + HTTPS (Let's Encrypt)
|
||||
│ Porta 80/443 │ + Rate limiting
|
||||
└────────┬─────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────┐
|
||||
│ Gunicorn │ ← WSGI server
|
||||
│ 4 workers │ Porta 5000 (internal)
|
||||
└────────┬─────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────┐
|
||||
│ Sibilla Flask │ ← Application
|
||||
└────────┬─────────┘
|
||||
│
|
||||
┌────┴────┐
|
||||
▼ ▼
|
||||
┌────────┐ ┌────────┐
|
||||
│ Degoog │ │ Gemini │
|
||||
│ :4444 │ │ API │
|
||||
└────────┘ └────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Docker Compose
|
||||
|
||||
Ecco un esempio di `docker-compose.yml` per avviare Sibilla + Degoog + SearXNG:
|
||||
|
||||
```yaml
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
sibilla:
|
||||
build: .
|
||||
ports:
|
||||
- "5000:5000"
|
||||
environment:
|
||||
- GOOGLE_API_KEY=${GOOGLE_API_KEY}
|
||||
- SECRET_KEY=${SECRET_KEY:-changeme} # ⚠️ Cambiare in produzione!
|
||||
- SIBILLA_HOST=0.0.0.0
|
||||
- SIBILLA_PORT=5000
|
||||
- SIBILLA_DEBUG=0
|
||||
- DEGOOG_URL=http://degoog:4444
|
||||
- SEARXNG_URL=http://searxng:8080
|
||||
- SIBILLA_DDG_FALLBACK=1
|
||||
depends_on:
|
||||
- degoog
|
||||
- searxng
|
||||
restart: unless-stopped
|
||||
|
||||
degoog:
|
||||
image: ghcr.io/degoog-org/degoog:latest
|
||||
ports:
|
||||
- "4444:4444"
|
||||
restart: unless-stopped
|
||||
|
||||
searxng:
|
||||
image: searxng/searxng:latest
|
||||
ports:
|
||||
- "8080:8080"
|
||||
environment:
|
||||
- SEARXNG_BASE_URL=http://localhost:8080/
|
||||
volumes:
|
||||
- ./searxng:/etc/searxng:rw
|
||||
restart: unless-stopped
|
||||
```
|
||||
|
||||
Avvia con:
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Reverse Proxy
|
||||
|
||||
### Nginx
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name sibilla.tuo-dominio.com;
|
||||
|
||||
# Rate limiting
|
||||
limit_req_zone $binary_remote_addr zone=sibilla:10m rate=10r/m;
|
||||
limit_req zone=sibilla burst=5 nodelay;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:5000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_read_timeout 120s;
|
||||
}
|
||||
|
||||
# API endpoint with higher rate limit
|
||||
location /api/ {
|
||||
proxy_pass http://127.0.0.1:5000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_read_timeout 120s;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Caddy (più semplice, auto-HTTPS)
|
||||
|
||||
```
|
||||
sibilla.tuo-dominio.com {
|
||||
reverse_proxy 127.0.0.1:5000
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Systemd Service
|
||||
|
||||
Crea `/etc/systemd/system/sibilla.service`:
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=Sibilla AI Search Engine
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=sibilla
|
||||
Group=sibilla
|
||||
WorkingDirectory=/opt/sibilla
|
||||
EnvironmentFile=/opt/sibilla/.env
|
||||
ExecStart=/opt/sibilla/.venv/bin/gunicorn -w 4 -b 127.0.0.1:5000 "sibilla.app:create_app()"
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
Attiva:
|
||||
```bash
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable --now sibilla
|
||||
sudo systemctl status sibilla
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Monitoraggio
|
||||
|
||||
### Health check endpoint
|
||||
|
||||
```bash
|
||||
curl http://localhost:5000/api/health
|
||||
```
|
||||
|
||||
Monitora con:
|
||||
- **Uptime Kuma** — monitoraggio self-hosted
|
||||
- **Healthchecks.io** — cron monitoring
|
||||
- **Prometheus + Grafana** — metriche avanzate (da aggiungere)
|
||||
|
||||
### Log
|
||||
|
||||
Con Gunicorn:
|
||||
```bash
|
||||
# Access log
|
||||
tail -f /var/log/sibilla/access.log
|
||||
|
||||
# Error log
|
||||
tail -f /var/log/sibilla/error.log
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "GOOGLE_API_KEY is not set"
|
||||
|
||||
Il file `.env` non è stato caricato o manca la variabile:
|
||||
|
||||
```bash
|
||||
# Verifica che il file .env esista
|
||||
ls -la .env
|
||||
|
||||
# Verifica il contenuto
|
||||
cat .env | grep GOOGLE_API_KEY
|
||||
|
||||
# Se usi systemd, assicurati che EnvironmentFile punti al percorso corretto
|
||||
```
|
||||
|
||||
### "Connection refused" per Degoog/SearXNG
|
||||
|
||||
Il motore di ricerca non è in esecuzione:
|
||||
|
||||
```bash
|
||||
# Verifica se Degoog è attivo
|
||||
curl http://localhost:4444/api/search?q=test
|
||||
|
||||
# Verifica se SearXNG è attivo
|
||||
curl http://localhost:8080/search?q=test&format=json
|
||||
|
||||
# Se non rispondono, Sibilla userà automaticamente DuckDuckGo
|
||||
```
|
||||
|
||||
### "502 Bad Gateway" — Tutti i motori down
|
||||
|
||||
Verifica che DuckDuckGo fallback sia abilitato:
|
||||
```env
|
||||
SIBILLA_DDG_FALLBACK=1
|
||||
```
|
||||
|
||||
Se anche DuckDuckGo non funziona, Gemini risponderà in modalità "pure LLM" (senza fonti).
|
||||
|
||||
### Timeout su richieste lente
|
||||
|
||||
Aumenta il timeout in Gunicorn:
|
||||
```bash
|
||||
gunicorn --timeout 180 ...
|
||||
```
|
||||
|
||||
E nel reverse proxy:
|
||||
```nginx
|
||||
proxy_read_timeout 180s;
|
||||
```
|
||||
+268
@@ -0,0 +1,268 @@
|
||||
# 🔄 Sibilla — Diagramma di Flusso
|
||||
|
||||
> **Versione:** 0.2.0
|
||||
> **Ultimo aggiornamento:** 18 Luglio 2026
|
||||
|
||||
---
|
||||
|
||||
## Pipeline di Ricerca Completa (Mermaid)
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["👤 Utente invia query"] --> B["🌐 Web UI (POST /api/ask)<br/>o<br/>🔗 API (GET/POST /api/v1/query)"]
|
||||
B --> C["🔮 Query Expansion<br/>Gemini genera 3 varianti"]
|
||||
C --> D{"🔍 Degoog<br/>disponibile?"}
|
||||
D -->|✅ Sì| E["Cerca con Degoog<br/>prime 2 varianti"]
|
||||
D -->|❌ No| F{"🔍 SearXNG<br/>disponibile?"}
|
||||
E --> G{"Risultati<br/>trovati?"}
|
||||
G -->|✅ Sì| H["Aggrega risultati<br/>Dedup URL"]
|
||||
G -->|❌ No| F
|
||||
F -->|✅ Sì| I["Cerca con SearXNG<br/>prime 2 varianti"]
|
||||
F -->|❌ No| J["🔍 DuckDuckGo<br/>fallback"]
|
||||
I --> K{"Risultati<br/>trovati?"}
|
||||
K -->|✅ Sì| H
|
||||
K -->|❌ No| J
|
||||
J --> L{"Risultati<br/>trovati?"}
|
||||
L -->|✅ Sì| H
|
||||
L -->|❌ No| M{"Tutti i backend<br/>falliti?"}
|
||||
M -->|✅ Sì| N["⚠️ Fallback Pure LLM<br/>Gemini risponde senza fonti"]
|
||||
M -->|❌ No| O["❌ Errore: 502<br/>Restituisci errore"]
|
||||
H --> P["📥 PageFetcher<br/>Scarica full content<br/>max 5 pagine, 300ms delay"]
|
||||
P --> Q["📝 Build Prompt<br/>Fonti numerate + data"]
|
||||
Q --> R["🧠 Gemini Synthesis<br/>SYNTHESIS_SYSTEM_PROMPT<br/>temp=0.4, max_tokens=4096"]
|
||||
R --> S["✅ JSON Response<br/>answer + html_answer<br/>+ speakable_text + sources"]
|
||||
|
||||
N --> T["✅ JSON Response<br/>solo LLM, nessuna fonte"]
|
||||
|
||||
style A fill:#a78bfa,color:#fff
|
||||
style S fill:#34d399,color:#000
|
||||
style T fill:#fbbf24,color:#000
|
||||
style O fill:#f87171,color:#fff
|
||||
style R fill:#8b5cf6,color:#fff
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Flusso Richiesta Web (Frontend)
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
actor U as 👤 Utente
|
||||
participant B as Browser (app.js)
|
||||
participant F as Flask /api/ask
|
||||
participant P as Pipeline
|
||||
participant G as Gemini
|
||||
participant S as Search Engines
|
||||
|
||||
U->>B: Scrivi query e clicca "Interroga"
|
||||
B->>B: Disabilita form, mostra spinner
|
||||
B->>F: POST /api/ask {query}
|
||||
F->>P: _run_search_pipeline(query, num_sources)
|
||||
P->>G: rewrite_query(query, 3 varianti)
|
||||
G-->>P: [originale, var1, var2, var3]
|
||||
P->>S: Degoog.search(var1) + Degoog.search(var2)
|
||||
S-->>P: SearchResponse {results[]}
|
||||
P->>S: PageFetcher.fetch_results(top5)
|
||||
S-->>P: SearchResult[] con full_content
|
||||
P->>G: synthesize(system_prompt, user_prompt)
|
||||
G-->>P: SynthesisResult {answer, tokens}
|
||||
P-->>F: enriched[], response_data
|
||||
F-->>B: JSON {answer, html_answer, speakable_text, sources[]}
|
||||
B->>B: Render HTML answer, mostra fonti
|
||||
B->>U: Mostra risultato
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Flusso API v1
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
actor C as 🔗 Client (curl/Lambda)
|
||||
participant F as Flask /api/v1/query
|
||||
participant P as Pipeline
|
||||
participant G as Gemini
|
||||
participant S as Search Engines
|
||||
|
||||
C->>F: GET /api/v1/query?q=domanda
|
||||
F->>F: Estrai query da query string
|
||||
F->>P: _run_search_pipeline(query, num_sources=5)
|
||||
P->>G: rewrite_query(query, 3 varianti)
|
||||
G-->>P: [originale, var1, var2, var3]
|
||||
|
||||
alt Degoog OK
|
||||
P->>S: Degoog.search(varianti)
|
||||
S-->>P: Risultati
|
||||
else SearXNG fallback
|
||||
P->>S: SearXNG.search(varianti)
|
||||
S-->>P: Risultati
|
||||
else DuckDuckGo fallback
|
||||
P->>S: DDG.search(query)
|
||||
S-->>P: Risultati
|
||||
else Pure LLM
|
||||
P->>G: synthesize(FALLBACK_PROMPT, query)
|
||||
G-->>P: Risposta senza fonti
|
||||
end
|
||||
|
||||
P->>S: PageFetcher.fetch_results(top5)
|
||||
S-->>P: Contenuti completi
|
||||
P->>G: synthesize(SYNTHESIS_PROMPT, fonti)
|
||||
G-->>P: SynthesisResult
|
||||
P-->>F: enriched[], response_data
|
||||
F-->>C: JSON {query, answer, speakable_text, sources[]}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Flusso MCP Server
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
actor A as 🤖 AI Agent (Claude/Gemini)
|
||||
participant M as MCP Server (stdio)
|
||||
participant D as DegoogClient
|
||||
participant S as SearXNGClient
|
||||
participant G as DuckDuckGoClient
|
||||
participant F as PageFetcher
|
||||
|
||||
A->>M: JSON-RPC: initialize
|
||||
M-->>A: {protocolVersion, capabilities, serverInfo}
|
||||
A->>M: JSON-RPC: tools/list
|
||||
M-->>A: {tools: [web_search, fetch_page]}
|
||||
|
||||
A->>M: JSON-RPC: tools/call {name: "web_search", arguments: {query: "..."}}
|
||||
|
||||
M->>D: search(query)
|
||||
alt Degoog ha risultati
|
||||
D-->>M: SearchResponse
|
||||
else Degoog vuoto
|
||||
M->>S: search(query)
|
||||
alt SearXNG ha risultati
|
||||
S-->>M: SearchResponse
|
||||
else Entrambi vuoti
|
||||
M->>G: search(query)
|
||||
G-->>M: SearchResponse
|
||||
end
|
||||
end
|
||||
|
||||
M-->>A: {content: [{text: "Risultati formattati..."}]}
|
||||
|
||||
A->>M: JSON-RPC: tools/call {name: "fetch_page", arguments: {url: "..."}}
|
||||
M->>F: fetch(url)
|
||||
F-->>M: Testo estratto (max 8000 chars)
|
||||
M-->>A: {content: [{text: "Contenuto pagina..."}]}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Macchina a Stati: Pipeline di Ricerca
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> ReceivingQuery: Utente invia query
|
||||
ReceivingQuery --> QueryExpansion: Query valida
|
||||
ReceivingQuery --> Error400: Query vuota
|
||||
|
||||
QueryExpansion --> DegoogSearch: Gemini espande query
|
||||
|
||||
DegoogSearch --> AggregatingResults: Risultati trovati
|
||||
DegoogSearch --> SearXNGFallback: Nessun risultato
|
||||
|
||||
SearXNGFallback --> AggregatingResults: Risultati trovati
|
||||
SearXNGFallback --> DDGFallback: Nessun risultato
|
||||
|
||||
DDGFallback --> AggregatingResults: Risultati trovati
|
||||
DDGFallback --> PureLLMFallback: Nessun risultato
|
||||
|
||||
AggregatingResults --> FetchingContent: Dedup URL completato
|
||||
FetchingContent --> BuildingPrompt: Full content scaricato
|
||||
BuildingPrompt --> Synthesizing: Prompt assemblato
|
||||
Synthesizing --> ReturningResponse: Sintesi completata
|
||||
Synthesizing --> ReturningErrorResponse: Errore Gemini
|
||||
|
||||
PureLLMFallback --> SynthesizingFallback: Usa FALLBACK_PROMPT
|
||||
SynthesizingFallback --> ReturningFallback: Risposta senza fonti
|
||||
|
||||
Error400 --> [*]
|
||||
ReturningResponse --> [*]
|
||||
ReturningErrorResponse --> [*]
|
||||
ReturningFallback --> [*]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Diagramma dei Componenti
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "Interfacce Utente"
|
||||
WEB["🌐 Web UI<br/>templates/index.html<br/>static/app.js"]
|
||||
API["🔗 REST API<br/>/api/ask<br/>/api/v1/query"]
|
||||
MCP["🤖 MCP Server<br/>stdio JSON-RPC"]
|
||||
end
|
||||
|
||||
subgraph "Flask Application"
|
||||
APP["app.py<br/>Flask App Factory"]
|
||||
PIPELINE["_run_search_pipeline()<br/>Pipeline condivisa"]
|
||||
end
|
||||
|
||||
subgraph "Servizi Core"
|
||||
AI["ai.py<br/>GeminiClient"]
|
||||
SEARCH["search.py"]
|
||||
MD["markdown.py<br/>Render + Strip"]
|
||||
PROMPTS["prompts.py<br/>System Prompts"]
|
||||
end
|
||||
|
||||
subgraph "Backend Esterni"
|
||||
GEMINI["🧠 Google Gemini API<br/>gemini-2.5-flash"]
|
||||
DEGOOG["🔍 Degoog<br/>self-hosted"]
|
||||
SEARXNG["🔍 SearXNG<br/>self-hosted"]
|
||||
DDG["🔍 DuckDuckGo<br/>HTML scraping"]
|
||||
end
|
||||
|
||||
WEB --> APP
|
||||
API --> APP
|
||||
MCP --> SEARCH
|
||||
APP --> PIPELINE
|
||||
PIPELINE --> AI
|
||||
PIPELINE --> SEARCH
|
||||
PIPELINE --> PROMPTS
|
||||
PIPELINE --> MD
|
||||
AI --> GEMINI
|
||||
SEARCH --> DEGOOG
|
||||
SEARCH --> SEARXNG
|
||||
SEARCH --> DDG
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Strategia di Query Expansion
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Q["Query originale:<br/>'ultimo film sull Odissea'"] --> GE["Gemini rewrite_query()"]
|
||||
GE --> V1["Variante 1:<br/>film Odissea 2026 recensione"]
|
||||
GE --> V2["Variante 2:<br/>ultimo adattamento cinematografico Odissea"]
|
||||
GE --> V3["Variante 3:<br/>The Odyssey movie latest release"]
|
||||
V1 --> D1["Degoog search"]
|
||||
V2 --> D2["Degoog search"]
|
||||
D1 --> AGG["Aggregazione<br/>+ Deduplicazione"]
|
||||
D2 --> AGG
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Legenda
|
||||
|
||||
| Simbolo | Significato |
|
||||
|---------|------------|
|
||||
| 🔍 | Motore di ricerca |
|
||||
| 🧠 | LLM / AI |
|
||||
| 📥 | Fetch / Download |
|
||||
| 📝 | Costruzione prompt |
|
||||
| ✅ | Successo |
|
||||
| ❌ | Errore / Fallimento |
|
||||
| ⚠️ | Degraded mode |
|
||||
| 👤 | Interazione utente |
|
||||
| 🔗 | API / Programmatico |
|
||||
| 🤖 | Agente AI esterno |
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
# 📚 Sibilla — Wiki
|
||||
|
||||
> Wiki tecnica del progetto Sibilla — motore di ricerca AI self-hosted stile Perplexity.
|
||||
|
||||
---
|
||||
|
||||
## Cosa è Sibilla?
|
||||
|
||||
**Sibilla** è un'applicazione Python/Flask che implementa un motore di ricerca AI self-hosted, simile a Perplexity AI. Ti permette di:
|
||||
|
||||
- Fare domande in linguaggio naturale
|
||||
- Ottenere risposte sintetizzate da Google Gemini basate su fonti web reali
|
||||
- Visualizzare le fonti con link e citazioni inline
|
||||
- Usare l'API REST per integrare Sibilla in altri servizi (skill Alexa, bot, app mobile)
|
||||
- Esporre la ricerca web come tool via MCP (Model Context Protocol) per agent AI
|
||||
|
||||
---
|
||||
|
||||
## Perché si chiama Sibilla?
|
||||
|
||||
Le **Sibille** erano figure della mitologia greco-romana: profetesse che pronunciavano oracoli. Come l'oracolo, Sibilla "vede" nel web e "profetizza" risposte basate sulle fonti che consulta.
|
||||
|
||||
---
|
||||
|
||||
## Funzionalità
|
||||
|
||||
### ✅ Implementate
|
||||
|
||||
| Funzionalità | Descrizione |
|
||||
|-------------|-------------|
|
||||
| 🔍 **Multi-engine search** | Degoog → SearXNG → DuckDuckGo con fallback a cascata |
|
||||
| 🧠 **Sintesi AI** | Google Gemini sintetizza risposte con citazioni [1], [2], ... |
|
||||
| 📝 **Query expansion** | Gemini genera varianti di query per migliorare il recall |
|
||||
| 📄 **Full content fetching** | Scarica e analizza il contenuto completo delle pagine |
|
||||
| 🌐 **Interfaccia web** | UI responsive, dark theme, mobile-first |
|
||||
| 📡 **API REST** | Endpoint `/api/ask` (web) e `/api/v1/query` (API) |
|
||||
| 🗣️ **TTS-ready** | Campo `speakable_text` ottimizzato per sintesi vocale |
|
||||
| 🔧 **MCP server** | Server Model Context Protocol per agent AI esterni |
|
||||
| 🛡️ **XSS protection** | Sanitizzazione HTML con allowlist |
|
||||
| 🔄 **CORS** | Header cross-origin per accesso da qualsiasi dominio |
|
||||
| 📊 **Metadati** | Token count, modello usato, search engine usato |
|
||||
|
||||
### 🚧 In programma
|
||||
|
||||
| Funzionalità | Priorità |
|
||||
|-------------|----------|
|
||||
| Rate limiting API | Alta |
|
||||
| Caching risposte (Redis) | Media |
|
||||
| Multi-lingua (oltre IT/EN) | Media |
|
||||
| Autenticazione API key | Media |
|
||||
| Streaming risposta (SSE) | Bassa |
|
||||
| Supporto immagini nei risultati | Bassa |
|
||||
| Docker Compose (Sibilla + Degoog + SearXNG) | Bassa |
|
||||
|
||||
---
|
||||
|
||||
## Glossario
|
||||
|
||||
| Termine | Definizione |
|
||||
|---------|------------|
|
||||
| **Sintesi** | Processo con cui Gemini genera una risposta basata sulle fonti |
|
||||
| **Query Expansion** | Generazione di varianti di una query per migliorare la ricerca |
|
||||
| **Cascading Fallback** | Strategia di fallback a catena: prova A, poi B, poi C |
|
||||
| **Full Content** | Contenuto testuale completo di una pagina web (dopo scraping) |
|
||||
| **Sanitizzazione HTML** | Rimozione di tag/attributi pericolosi per prevenire XSS |
|
||||
| **MCP** | Model Context Protocol — standard per esporre tool a LLM |
|
||||
| **TTS** | Text-to-Speech — sintesi vocale (es. Alexa, Google Home) |
|
||||
| **Degoog** | Metasearch engine self-hosted, alternativa leggera a SearXNG |
|
||||
|
||||
---
|
||||
|
||||
## Domande Frequenti (FAQ)
|
||||
|
||||
### Cosa serve per far funzionare Sibilla?
|
||||
|
||||
Come minimo:
|
||||
1. **Python 3.10+**
|
||||
2. **Google API Key** (per Gemini)
|
||||
3. Almeno un motore di ricerca:
|
||||
- **Degoog** (consigliato) — `docker run -d -p 4444:4444 ghcr.io/degoog-org/degoog:latest`
|
||||
- **SearXNG** — `docker run -d -p 8080:8080 searxng/searxng`
|
||||
- **DuckDuckGo** — funziona senza server (fallback automatico)
|
||||
|
||||
### Posso usare Sibilla senza Degoog/SearXNG?
|
||||
|
||||
Sì! Sibilla userà automaticamente DuckDuckGo come fallback (scraping HTML, nessuna API key necessaria). Puoi disabilitarlo con `SIBILLA_DDG_FALLBACK=0`.
|
||||
|
||||
### Quanto costa?
|
||||
|
||||
- **Sibilla:** gratuito e open-source (MIT)
|
||||
- **Degoog:** gratuito, self-hosted
|
||||
- **SearXNG:** gratuito, self-hosted
|
||||
- **Gemini API:** ha un tier gratuito (vedi [Google AI Studio](https://aistudio.google.com/apikey))
|
||||
|
||||
### Posso cambiare il modello LLM?
|
||||
|
||||
Sì! Modifica il parametro `model` quando crei `GeminiClient` o cambia il default in `ai.py`. Supporta qualsiasi modello Gemini (es. `gemini-2.5-pro`, `gemini-1.5-flash`).
|
||||
|
||||
### Sibilla supporta altre lingue?
|
||||
|
||||
Sì! I prompt sono in italiano ma Gemini risponde nella lingua della domanda. Puoi modificare i prompt in `prompts.py` per altre lingue.
|
||||
|
||||
### Come integro Sibilla con la mia app?
|
||||
|
||||
Usa l'endpoint `/api/v1/query`:
|
||||
|
||||
```bash
|
||||
curl "http://localhost:5000/api/v1/query?q=tua+domanda"
|
||||
```
|
||||
|
||||
Vedi `docs/API.md` per la documentazione completa.
|
||||
|
||||
### Sibilla funziona dietro un reverse proxy (nginx, Caddy)?
|
||||
|
||||
Sì! Sibilla è una normale app Flask/WSGI. Puoi usare `gunicorn` come server WSGI e nginx/Caddy come reverse proxy. Vedi `docs/DEPLOYMENT.md`.
|
||||
|
||||
---
|
||||
|
||||
## Confronto con Perplexity
|
||||
|
||||
| Caratteristica | Perplexity | Sibilla |
|
||||
|---------------|-----------|---------|
|
||||
| **Hosting** | Cloud (perplexity.ai) | Self-hosted |
|
||||
| **Costo** | $20/mese (Pro) | Gratuito (+ costi API Gemini) |
|
||||
| **Privacy** | Dati su server Perplexity | Dati sul tuo server |
|
||||
| **Motori di ricerca** | Interni di Perplexity | Degoog, SearXNG, DuckDuckGo |
|
||||
| **LLM** | Modelli proprietari | Google Gemini |
|
||||
| **API** | Via perplexity.ai | Locale, REST/MCP |
|
||||
| **Personalizzazione** | Limitata | Completa (open source) |
|
||||
| **MCP** | No | Sì |
|
||||
| **TTS support** | No | Sì (via `speakable_text`) |
|
||||
|
||||
---
|
||||
|
||||
## Roadmap
|
||||
|
||||
```
|
||||
v0.1.0 ✅ — MVP: ricerca + sintesi con Degoog/SearXNG/DDG + Gemini
|
||||
v0.2.0 ✅ — API v1, TTS, CORS, documentazione completa
|
||||
v0.3.0 📋 — Rate limiting, caching, autenticazione API key
|
||||
v0.4.0 📋 — Docker Compose, streaming SSE, multi-lingua
|
||||
v1.0.0 📋 — Stable release, test coverage 90%+
|
||||
```
|
||||
Reference in new issue
Block a user