From 76bc667adfb1ffd5c1bf3ba51856ddd2df11eee2 Mon Sep 17 00:00:00 2001 From: Sibilla Date: Sat, 18 Jul 2026 10:05:46 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20v0.2.0=20=E2=80=94=20API=20REST,=20TTS?= =?UTF-8?q?=20support,=20documentazione=20completa=20e=20UI=20mobile-first?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit โœจ 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 --- .env.example | 41 ++-- README.md | 268 ++++++++++++++++++------ docs/API.md | 315 ++++++++++++++++++++++++++++ docs/ARCHITECTURE.md | 444 ++++++++++++++++++++++++++++++++++++++++ docs/CONTRIBUTING.md | 217 ++++++++++++++++++++ docs/DEPLOYMENT.md | 402 ++++++++++++++++++++++++++++++++++++ docs/FLOW.md | 268 ++++++++++++++++++++++++ docs/WIKI.md | 143 +++++++++++++ pyproject.toml | 2 +- src/sibilla/__init__.py | 2 +- src/sibilla/ai.py | 33 ++- src/sibilla/app.py | 344 +++++++++++++++++++++---------- src/sibilla/markdown.py | 185 +++++++++++++++++ src/sibilla/mcp.py | 2 +- static/app.js | 59 +++++- static/favicon.svg | 53 +++++ static/style.css | 374 ++++++++++++++++++++++++++++----- templates/index.html | 13 +- tests/test_app.py | 201 ++++++++++++++++++ tests/test_markdown.py | 264 ++++++++++++++++++++++++ 20 files changed, 3385 insertions(+), 245 deletions(-) create mode 100644 docs/API.md create mode 100644 docs/ARCHITECTURE.md create mode 100644 docs/CONTRIBUTING.md create mode 100644 docs/DEPLOYMENT.md create mode 100644 docs/FLOW.md create mode 100644 docs/WIKI.md create mode 100644 src/sibilla/markdown.py create mode 100644 static/favicon.svg create mode 100644 tests/test_markdown.py diff --git a/.env.example b/.env.example index 92d991e..9a78489 100644 --- a/.env.example +++ b/.env.example @@ -1,21 +1,38 @@ -# Google AI API Key โ€” ottenila da https://aistudio.google.com/apikey -GOOGLE_API_KEY=your-api-key-here +# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• +# Sibilla Configuration Template +# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• +# +# Copia questo file in .env e modifica i valori: +# cp .env.example .env -# โ”€โ”€ Search backends (cascading: Degoog โ†’ SearXNG โ†’ DuckDuckGo) โ”€โ”€ +# Google Gemini API key (obbligatoria) +# Ottienila gratis: https://aistudio.google.com/apikey +GOOGLE_API_KEY=la-tua-api-key -# Degoog (primary) โ€” self-hosted metasearch, default port 4444 +# Flask secret key (cambiala in produzione!) +# Generane una con: python -c "import secrets; print(secrets.token_hex(32))" +SECRET_KEY=dev-secret-key-change-me + +# โ”€โ”€ Server โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +# Host e porta del server Flask +SIBILLA_HOST=127.0.0.1 +SIBILLA_PORT=5000 + +# Debug mode (0 = off, 1 = on) +# โš ๏ธ NON usare SIBILLA_DEBUG=1 in produzione! +SIBILLA_DEBUG=1 + +# โ”€โ”€ Search Engines โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +# Degoog (primary) โ€” motore di ricerca self-hosted # Avvia con: docker run -d --name degoog -p 4444:4444 ghcr.io/degoog-org/degoog:latest DEGOOG_URL=http://localhost:4444 -# SearXNG (fallback 1) โ€” self-hosted metasearch, default port 8080 +# SearXNG (secondary, fallback) โ€” motore di ricerca self-hosted # Avvia con: docker run -d -p 8080:8080 searxng/searxng SEARXNG_URL=http://localhost:8080 -# DuckDuckGo (fallback 2) โ€” nessun server richiesto +# DuckDuckGo fallback (1 = on, 0 = off) +# Funziona senza server โ€” scraping HTML, nessuna API key necessaria SIBILLA_DDG_FALLBACK=1 - -# โ”€โ”€ Flask โ”€โ”€ -SECRET_KEY=change-me-to-a-random-string -SIBILLA_HOST=127.0.0.1 -SIBILLA_PORT=5000 -SIBILLA_DEBUG=1 diff --git a/README.md b/README.md index 9a1fabf..82245d9 100644 --- a/README.md +++ b/README.md @@ -1,101 +1,241 @@ -# ๐Ÿ”ฎ Sibilla +# ๐Ÿ”ฎ Sibilla โ€” L'Oracolo AI -> *L'oracolo AI che cerca nel web e risponde alle tue domande.* -> Self-hosted Perplexity-like search engine. +> *Self-hosted Perplexity-like AI search engine.* +> *Chiedi, cerca, sintetizza โ€” con fonti verificabili.* + +[![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/) +[![Flask](https://img.shields.io/badge/flask-3.x-green.svg)](https://flask.palletsprojects.com/) +[![Gemini](https://img.shields.io/badge/LLM-Gemini%202.5%20Flash-purple.svg)](https://ai.google.dev/) +[![Version](https://img.shields.io/badge/version-0.2.0-blue.svg)](https://github.com/tuo-user/sibilla) +[![License](https://img.shields.io/badge/license-MIT-brightgreen.svg)](LICENSE) +[![MCP](https://img.shields.io/badge/MCP-compatible-orange.svg)](https://modelcontextprotocol.io/) + +--- **Sibilla** รจ un motore di ricerca AI self-hosted che combina: -- ๐Ÿง  **Google Gemini** โ€” LLM per sintesi e query expansion -- ๐Ÿ” **Degoog** โ€” metasearch engine self-hosted (primary) con MCP nativo -- ๐Ÿ”„ **SearXNG + DuckDuckGo** โ€” fallback a cascata se Degoog non risponde +- ๐Ÿง  **Google Gemini** โ€” LLM per sintesi con citazioni inline [1], [2], ... +- ๐Ÿ” **Degoog** โ€” metasearch engine self-hosted (primary) con API nativa +- ๐Ÿ”„ **SearXNG + DuckDuckGo** โ€” fallback a cascata se il primary non risponde +- ๐ŸŒ **Interfaccia web** โ€” UI responsive, dark theme, mobile-first +- ๐Ÿ“ก **API REST** โ€” endpoint per consumo programmatico e skill vocali (Alexa, Google Home) - ๐Ÿ”ง **MCP (Model Context Protocol)** โ€” server stdio per agent AI esterni -- ๐Ÿ–ฅ๏ธ **Flask web app** โ€” interfaccia pulita e reattiva +- ๐Ÿ—ฃ๏ธ **TTS-ready** โ€” campo `speakable_text` ottimizzato per sintesi vocale ``` -Tu chiedi โ†’ Query expansion โ†’ Degoog search โ†’ Fetch pagine โ†’ Gemini sintesi โ†’ Risposta con fonti +Tu chiedi โ†’ Query expansion โ†’ Ricerca multi-engine โ†’ Fetch pagine โ†’ Gemini sintesi โ†’ Risposta con fonti ``` -## Architettura +--- -``` -sibilla/ -โ”œโ”€โ”€ src/sibilla/ -โ”‚ โ”œโ”€โ”€ app.py # Flask app factory + pipeline /api/ask -โ”‚ โ”œโ”€โ”€ cli.py # Entry point server web -โ”‚ โ”œโ”€โ”€ search.py # DegoogClient, SearXNGClient, DuckDuckGoClient, PageFetcher -โ”‚ โ”œโ”€โ”€ ai.py # GeminiClient (sintesi + query rewriting) -โ”‚ โ”œโ”€โ”€ mcp.py # MCP server su stdio (per agent AI esterni) -โ”‚ โ””โ”€โ”€ prompts.py # System prompt stile Perplexity -โ”œโ”€โ”€ templates/ -โ”‚ โ””โ”€โ”€ index.html # Interfaccia di ricerca -โ”œโ”€โ”€ static/ -โ”‚ โ”œโ”€โ”€ style.css # Stili dark theme -โ”‚ โ””โ”€โ”€ app.js # Logica frontend -โ””โ”€โ”€ tests/ -``` +## ๐Ÿ“š Documentazione -## Pipeline di ricerca +| Documento | Descrizione | +|-----------|-------------| +| [Architettura](docs/ARCHITECTURE.md) | ๐Ÿ—๏ธ Visione d'insieme, stack tecnologico, componenti, modello dati | +| [Diagramma di Flusso](docs/FLOW.md) | ๐Ÿ”„ Pipeline di ricerca, flusso richieste, macchina a stati (Mermaid) | +| [API REST](docs/API.md) | ๐Ÿ“ก Documentazione completa degli endpoint `/api/ask` e `/api/v1/query` | +| [Wiki](docs/WIKI.md) | ๐Ÿ“š FAQ, glossario, confronto con Perplexity, roadmap | +| [Deployment](docs/DEPLOYMENT.md) | ๐Ÿš€ Guida al deployment in produzione (Gunicorn, Nginx, Docker, systemd) | +| [Contributing](docs/CONTRIBUTING.md) | ๐Ÿค Guida per contribuire, convenzioni, test, struttura branch | -``` -POST /api/ask {query: "..."} - โ”‚ - โ”œโ”€ 1. Gemini riscrive la query in 3 varianti (query expansion) - โ”œโ”€ 2. Multi-query search: Degoog โ†’ SearXNG โ†’ DuckDuckGo (cascading) - โ”œโ”€ 3. PageFetcher scarica il contenuto completo delle top pagine - โ”œโ”€ 4. Prompt assembly con fonti numerate - โ”œโ”€ 5. Gemini sintesi con citazioni [1], [2], ... - โ””โ”€ 6. JSON {answer, sources[], model, search_engine, tokens_used} -``` +--- -## Setup +## ๐Ÿš€ Quick Start ```bash +# 1. Clona e installa +git clone https://github.com/tuo-user/sibilla.git cd sibilla python3 -m venv .venv source .venv/bin/activate pip install -e ".[dev]" + +# 2. Avvia Degoog (motore di ricerca primary, opzionale ma consigliato) +docker run -d --name degoog -p 4444:4444 ghcr.io/degoog-org/degoog:latest + +# 3. Configura +# Crea un file .env con la tua Google API Key: +# GOOGLE_API_KEY=la-tua-api-key + +# 4. Avvia +sibilla +# โ†’ http://127.0.0.1:5000 ``` -### Prerequisiti +### Prerequisiti minimi -1. **Google API Key** โ€” [https://aistudio.google.com/apikey](https://aistudio.google.com/apikey) -2. **Degoog** (consigliato) โ€” motore di ricerca primary: - ```bash - docker run -d --name degoog -p 4444:4444 ghcr.io/degoog-org/degoog:latest - ``` -3. **SearXNG** (opzionale, fallback) โ€” se vuoi un secondo motore di backup: - ```bash - docker run -d -p 8080:8080 searxng/searxng - ``` +- **Python 3.10+** +- **Google API Key** โ€” [ottienila gratis](https://aistudio.google.com/apikey) +- Almeno un motore di ricerca (Degoog, SearXNG, o DuckDuckGo automatico) + +--- + +## ๐Ÿ”Œ API + +### Ricerca via API (per skill Alexa, bot, app) + +```bash +# GET โ€” il piรน semplice +curl "http://localhost:5000/api/v1/query?q=ultimo+film+sull%27Odissea" + +# POST โ€” per query lunghe +curl -X POST http://localhost:5000/api/v1/query \ + -H "Content-Type: application/json" \ + -d '{"query": "quali sono i migliori ristoranti a Roma?"}' + +# Estrai solo il testo per TTS +curl -s "http://localhost:5000/api/v1/query?q=raggi+cosmici" \ + | python -c "import sys,json; print(json.load(sys.stdin)['speakable_text'])" +``` + +**Risposta:** +```json +{ + "query": "ultimo film sull'Odissea", + "answer": "L'ultimo adattamento รจ **The Return** (2024)...", + "speakable_text": "L'ultimo adattamento รจ The Return del 2024...", + "sources": [{"index": 1, "title": "...", "url": "..."}], + "model": "gemini-2.5-flash", + "search_engine": "degoog", + "tokens_used": 1847 +} +``` + +> **Documentazione completa:** [`docs/API.md`](docs/API.md) + +--- + +## ๐Ÿ—๏ธ Architettura + +``` +sibilla/ +โ”œโ”€โ”€ docs/ # ๐Ÿ“š Documentazione +โ”‚ โ”œโ”€โ”€ ARCHITECTURE.md # Architettura di sistema +โ”‚ โ”œโ”€โ”€ FLOW.md # Diagramma di flusso (Mermaid) +โ”‚ โ”œโ”€โ”€ API.md # Documentazione API REST +โ”‚ โ”œโ”€โ”€ WIKI.md # Wiki + FAQ +โ”‚ โ”œโ”€โ”€ DEPLOYMENT.md # Guida al deployment +โ”‚ โ””โ”€โ”€ CONTRIBUTING.md # Guida alla contribuzione +โ”‚ +โ”œโ”€โ”€ src/sibilla/ # ๐Ÿ“ฆ Codice sorgente +โ”‚ โ”œโ”€โ”€ app.py # Flask app factory + pipeline condivisa +โ”‚ โ”œโ”€โ”€ search.py # Degoog, SearXNG, DuckDuckGo, PageFetcher +โ”‚ โ”œโ”€โ”€ ai.py # GeminiClient (sintesi + query rewriting) +โ”‚ โ”œโ”€โ”€ markdown.py # Markdown โ†’ HTML + strip per TTS +โ”‚ โ”œโ”€โ”€ prompts.py # System prompt stile Perplexity +โ”‚ โ”œโ”€โ”€ mcp.py # MCP server stdio (per agent AI) +โ”‚ โ””โ”€โ”€ cli.py # Entry point server +โ”‚ +โ”œโ”€โ”€ templates/index.html # Interfaccia web +โ”œโ”€โ”€ static/ # CSS, JS, favicon +โ”œโ”€โ”€ tests/ # Test (pytest, 109 test) +โ””โ”€โ”€ pyproject.toml # Configurazione progetto +``` + +--- + +## ๐Ÿ”„ Pipeline di Ricerca + +``` +POST /api/ask {query: "..."} + โ”‚ + โ”œโ”€ 1. ๐Ÿ”ฎ Gemini riscrive la query in 3 varianti (query expansion) + โ”œโ”€ 2. ๐Ÿ” Multi-engine search: Degoog โ†’ SearXNG โ†’ DuckDuckGo (cascading) + โ”œโ”€ 3. ๐Ÿ“ฅ PageFetcher scarica il contenuto completo delle top pagine + โ”œโ”€ 4. ๐Ÿ“ Prompt assembly con fonti numerate + data odierna + โ”œโ”€ 5. ๐Ÿง  Gemini sintesi con citazioni inline [1], [2], ... + โ””โ”€ 6. โœ… JSON {answer, html_answer, speakable_text, sources[], metadata} +``` + +--- + +## ๐Ÿ”ง MCP Server + +Sibilla espone le sue capacitร  di ricerca come tool MCP (Model Context Protocol) su stdio: + +```bash +sibilla-mcp +``` + +**Tool disponibili:** +- `web_search(query, num_results)` โ€” ricerca web con fallback Degoog โ†’ SearXNG โ†’ DuckDuckGo +- `fetch_page(url)` โ€” estrai contenuto testuale da una pagina web + +Compatibile con client MCP come Claude Desktop, Gemini Desktop e altri agent AI. + +--- + +## ๐Ÿงช Sviluppo + +```bash +# Installa dipendenze dev +pip install -e ".[dev]" + +# Lint +ruff check src tests + +# Test +pytest tests/ -v + +# Coverage +pip install pytest-cov +pytest tests/ --cov=src/sibilla --cov-report=html +``` + +--- + +## ๐Ÿš€ Deployment + +Per il deployment in produzione: + +```bash +# Con Gunicorn +pip install gunicorn +gunicorn -w 4 -b 0.0.0.0:5000 "sibilla.app:create_app()" + +# Con Docker Compose (Sibilla + Degoog + SearXNG) +docker compose up -d +``` + +> **Guida completa:** [`docs/DEPLOYMENT.md`](docs/DEPLOYMENT.md) + +--- + +## ๐Ÿ“‹ Configurazione Crea un file `.env`: ```env GOOGLE_API_KEY=la-tua-api-key +SECRET_KEY=una-chiave-segreta-lunga-e-casuale +SIBILLA_HOST=127.0.0.1 +SIBILLA_PORT=5000 +SIBILLA_DEBUG=1 DEGOOG_URL=http://localhost:4444 +SEARXNG_URL=http://localhost:8080 +SIBILLA_DDG_FALLBACK=1 ``` -Senza Degoog, Sibilla userร  automaticamente SearXNG e DuckDuckGo come fallback. +--- -## Uso +## ๐Ÿค Contribuire -```bash -# Avvia la web app -sibilla -# โ†’ http://127.0.0.1:5000 +Contributi benvenuti! Vedi [`docs/CONTRIBUTING.md`](docs/CONTRIBUTING.md) per: -# Avvia il server MCP (per agent AI esterni) -sibilla-mcp -``` +- Setup dell'ambiente di sviluppo +- Convenzioni di codice e commit +- Come aggiungere search engine, endpoint, o tool MCP +- Struttura dei test -## Sviluppo +--- -```bash -pip install -e ".[dev,mcp]" -ruff check src tests -pytest -``` +## ๐Ÿ“„ Licenza -## Licenza +MIT โ€” vedi il file [LICENSE](LICENSE) per i dettagli. -MIT +--- + +

+ Fatto con ๐Ÿ”ฎ da Sibilla Team ยท Architettura ยท API ยท Deployment +

diff --git a/docs/API.md b/docs/API.md new file mode 100644 index 0000000..2e4f93c --- /dev/null +++ b/docs/API.md @@ -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": "

L'ultimo adattamento cinematografico dell'Odissea รจ The Return (2024)...

", + "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"{result['speakable_text']}" + }, + "shouldEndSession": True + } + } +``` + +> **Nota:** Il campo `speakable_text` รจ giร  pulito da markup Markdown, URL, grassetti e codice โ€” pronto per essere letto da un motore TTS. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..bf47522 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -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": "

Versione HTML della risposta...

", + "speakable_text": "Testo pulito per TTS...", + "sources": [ + { + "index": 1, + "title": "Titolo della fonte", + "url": "https://example.com", + "snippet": "Estratto della pagina..." + } + ], + "model": "gemini-2.5-flash", + "search_engine": "degoog", + "tokens_used": 1234, + "error": null +} +``` + +### Response JSON (`/api/v1/query`) โ€” formato semplificato per API +```json +{ + "query": "domanda originale", + "answer": "Risposta in Markdown...", + "speakable_text": "Risposta pulita per TTS...", + "sources": [ + {"index": 1, "title": "...", "url": "..."} + ], + "model": "gemini-2.5-flash", + "search_engine": "degoog", + "tokens_used": 1234 +} +``` + +--- + +## Flusso delle richieste + +### Web UI (`GET /`) +``` +Browser โ†’ GET / โ†’ Flask โ†’ render_template("index.html") + โ””โ”€ Jinja2 template HTML + โ””โ”€ Static assets (CSS, JS, favicon) + +Utente scrive query โ†’ POST /api/ask {query: "..."} + โ†’ app.js fetch โ†’ render risposta +``` + +### API REST (`GET/POST /api/v1/query`) +``` +curl / Lambda / Alexa Skill + โ”‚ + โ”œโ”€ GET /api/v1/query?q=domanda + โ””โ”€ POST /api/v1/query {"query": "domanda"} + โ”‚ + โ–ผ + _run_search_pipeline(query, num_sources=5) + โ”‚ + โ–ผ + JSON response con speakable_text +``` + +### MCP Server (`sibilla-mcp`) +``` +AI Agent (Claude/Gemini Desktop) + โ”‚ + โ”œโ”€ stdio JSON-RPC + โ”‚ + โ–ผ +MCPServer.run() + โ”œโ”€ tools/list โ†’ [{web_search}, {fetch_page}] + โ””โ”€ tools/call โ†’ esecuzione tool โ†’ risultato text +``` + +--- + +## Strategia di fallback + +Sibilla implementa una **strategia di fallback a cascata** su tre livelli: + +``` +Livello 1: Degoog (primary) + โ”‚ + โ”œโ”€ Disponibile? โ†’ Usa Degoog + โ””โ”€ Non disponibile? โ†“ + +Livello 2: SearXNG (secondary) + โ”‚ + โ”œโ”€ Disponibile? โ†’ Usa SearXNG + โ””โ”€ Non disponibile? โ†“ + +Livello 3: DuckDuckGo (fallback) + โ”‚ + โ”œโ”€ Disponibile? โ†’ Usa DuckDuckGo (HTML scraping) + โ””โ”€ Non disponibile? โ†“ + +Livello 4: Pure LLM (degraded mode) + โ””โ”€ Gemini risponde senza fonti, spiegando i limiti +``` + +**Vantaggi:** +- Nessun single point of failure +- DuckDuckGo non richiede server esterni (scraping HTML) +- Pure LLM garantisce sempre una risposta (in degraded mode) + +--- + +## Sicurezza e sanitizzazione + +### Sanitizzazione HTML +Il Markdown renderizzato viene sanitizzato per prevenire XSS: + +- **Allowlist di tag HTML:** solo tag sicuri (`h1-h6`, `p`, `strong`, `a`, ecc.) +- **Allowlist di attributi:** `href`, `title`, `alt`, `src`, `class`, `id` +- **Blocco `javascript:` URL** su `href` e `src` +- **Nessun event handler** (`onclick`, `onerror`, ecc.) +- **Strip di `") + assert "" not in result + + def test_event_handlers_stripped(self): + """onerror/onclick handlers must be stripped even on allowed tags.""" + result = render('') + assert "onerror" not in result + assert 'src="x"' in result # legitimate attrs preserved + + def test_javascript_url_stripped(self): + result = render("[click](javascript:alert(1))") + # The link text might appear, but the javascript: href must not + assert "javascript:" not in result.lower() + assert "click" in result + + def test_regular_html_preserved_in_markdown(self): + """Sanitization should NOT strip tags that come from markdown rendering.""" + result = render("**bold** and *italic*") + assert "bold" in result + assert "italic" in result + + +class TestParagraphs: + def test_paragraph_wrapping(self): + result = render("Line 1.\n\nLine 2.") + assert result.count("

") >= 2 + + def test_single_line_break(self): + result = render("Line 1.\nLine 2.") + assert "Line 1." in result + assert "Line 2." in result + + def test_multiple_paragraphs(self): + text = "Primo paragrafo.\n\nSecondo paragrafo.\n\nTerzo." + result = render(text) + assert "Primo paragrafo" in result + assert "Secondo paragrafo" in result + assert "Terzo" in result + + +# โ”€โ”€ strip_markdown (TTS-friendly plain text) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +class TestStripMarkdown: + def test_empty_returns_empty(self): + assert strip_markdown("") == "" + assert strip_markdown(None) == "" + + def test_plain_text_passes_through(self): + text = "Ciao mondo, oggi รจ una bella giornata." + assert strip_markdown(text) == text + + def test_bold_double_asterisk(self): + result = strip_markdown("Questo รจ **importante** da sapere.") + assert result == "Questo รจ importante da sapere." + + def test_bold_double_underscore(self): + result = strip_markdown("Questo รจ __importante__ da sapere.") + assert result == "Questo รจ importante da sapere." + + def test_italic_single_asterisk(self): + result = strip_markdown("Una parola *italica* nel testo.") + assert result == "Una parola italica nel testo." + + def test_italic_single_underscore(self): + result = strip_markdown("Una parola _italica_ nel testo.") + assert result == "Una parola italica nel testo." + + def test_links_stripped(self): + result = strip_markdown( + "Visita [il nostro sito](https://example.com) per info." + ) + assert result == "Visita il nostro sito per info." + + def test_images_stripped(self): + result = strip_markdown( + "Ecco una foto: ![logo](https://a.com/logo.png)" + ) + assert result == "Ecco una foto: logo" + + def test_headings_stripped(self): + result = strip_markdown("# Titolo\n\nTesto del paragrafo.") + assert result == "Titolo\n\nTesto del paragrafo." + + def test_code_blocks_stripped(self): + result = strip_markdown("```python\nprint('hello')\n```\n\nFine.") + assert "```" not in result + assert "print('hello')" in result + assert "Fine." in result + + def test_inline_code_stripped(self): + result = strip_markdown("Usa il comando `pip install` per procedere.") + assert result == "Usa il comando pip install per procedere." + + def test_blockquotes_stripped(self): + result = strip_markdown("> Questa รจ una citazione.\n> Seconda riga.") + assert "Questa รจ una citazione." in result + assert ">" not in result + + def test_unordered_list_stripped(self): + result = strip_markdown("- Primo elemento\n- Secondo elemento\n- Terzo") + assert "Primo elemento" in result + assert "Secondo elemento" in result + assert "Terzo" in result + assert "-" not in result.split("Primo")[0] + + def test_ordered_list_stripped(self): + result = strip_markdown("1. Primo\n2. Secondo\n3. Terzo") + assert "Primo" in result + assert "Secondo" in result + assert "Terzo" in result + + def test_html_tags_stripped(self): + result = strip_markdown("Testo con HTML inline.") + assert result == "Testo con HTML inline." + + def test_horizontal_rules_stripped(self): + result = strip_markdown("Prima sezione\n---\nSeconda sezione") + assert "---" not in result + assert "Prima sezione" in result + assert "Seconda sezione" in result + + def test_multiple_blank_lines_collapsed(self): + result = strip_markdown("Riga 1\n\n\n\nRiga 2") + assert result == "Riga 1\n\nRiga 2" + + def test_complex_mixed_markdown(self): + """Realistic AI output with multiple markdown constructs.""" + text = """## Risposta + +**L'Odissea** รจ un poema epico attribuito a [Omero](https://it.wikipedia.org/wiki/Omero). + +Ecco alcuni punti chiave: +- Composto intorno all'*VIII secolo a.C.* +- Narra il viaggio di **Ulisse** +- รˆ diviso in `24 libri` + +Per approfondire, vedi [Wikipedia](https://it.wikipedia.org).""" + result = strip_markdown(text) + # No markdown artifacts + assert "**" not in result + assert "*" not in result.split("VIII")[0] # no italic markers + assert "[" not in result + assert "](" not in result + assert "`" not in result + assert "##" not in result + assert "-" not in result.split("Composto")[0] + # Content preserved + assert "Odissea" in result + assert "Omero" in result + assert "VIII secolo a.C." in result + assert "Ulisse" in result + assert "24 libri" in result + assert "Wikipedia" in result