- Home
- iThink
- AI Locale & Agenti
- Interfaccia grafica per un agente AI con Gradio
Interfaccia grafica per un agente AI con Gradio
Una buona interfaccia grafica rende l'agente utilizzabile anche da chi non apre il terminale. In questo capitolo costruisci con Gradio Blocks una chat con risposte in streaming, un pannello per i parametri del modello, l'upload dei documenti per il RAG e l'esportazione delle conversazioni, e poi la pubblichi in locale, in modo privato.
Perché Gradio per agenti locali
| Alternativa | Pro | Contro | | --- | --- | --- | | Gradio | Python-native, streaming built-in, theme system, queue management, sharing tunnel | Meno flessibile di React/Vue per UI complesse | | Streamlit | Semplice, orientato alle data app, con elementi chat e streaming | Ricarica lo script a ogni interazione, stato legato alla sessione | | Chainlit | Purpose-built per LLM, auth, streaming | Più complesso, dipendenze extra | | FastAPI + React | Full control, production-grade | Mesi di dev frontend | | Textual (TUI) | Terminal, zero browser | Solo CLI, no file upload drag-drop |
Gradio vince per: setup in minuti, streaming SSE nativo, queue() per concorrenza, share=False per privacy, theme system CSS-like.
Dipendenze
# requirements.txt (aggiungi a Cap. 6)
gradio>=4.44.0,<5.0
huggingface_hub<1.0 # Gradio 4 importa HfFolder, rimosso dalla 1.0
pydantic-settings>=2.0
markdown>=3.5.0
pygments>=2.18.0 # code highlighting
pip install -r requirements.txt
Architettura GUI
┌─────────────────────────────────────────────────────────────────────────────┐
│ GRADIO BLOCKS APP │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Header: Title, Model Selector, Status Indicator │ │
│ ├─────────────────────────────────────────────────────────────────────┤ │
│ │ ┌─────────────────┐ ┌─────────────────────────────────────────┐ │ │
│ │ │ Sidebar │ │ Main Chat Area │ │ │
│ │ │ ┌───────────┐ │ │ ┌─────────────────────────────────┐ │ │ │
│ │ │ │ Parameters│ │ │ │ Message History (scroll) │ │ │ │
│ │ │ │ Temp: 0.7 │ │ │ │ [User] [Assistant] [User] ... │ │ │ │
│ │ │ │ Top-p:0.9 │ │ │ │ Streaming token-by-token │ │ │ │
│ │ │ │ Ctx: 8192 │ │ │ │ Markdown + Code Highlight │ │ │ │
│ │ │ │ GPU: 999 │ │ │ └─────────────────────────────────┘ │ │ │
│ │ │ ├───────────┤ │ │ ┌─────────────────────────────────┐ │ │ │
│ │ │ │ RAG Upload│ │ │ │ Input: Textbox + Send Button │ │ │ │
│ │ │ │ [Drag Drop]│ │ │ │ [📎] [🎤] [Send] │ │ │ │
│ │ │ │ Chunk: 1000│ │ │ └─────────────────────────────────┘ │ │ │
│ │ │ │ Overlap:200│ │ └─────────────────────────────────────────┘ │ │
│ │ │ ├───────────┤ │ │ │
│ │ │ │ Examples │ │ │ │
│ │ │ │ [Btn] [Btn]│ │ │ │
│ │ │ └───────────┘ │ │ │
│ │ └─────────────────┘ └─────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ Backend: │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────────┐ │
│ │ Session │ │ Agent │ │ RAG │ │ Config │ │
│ │ Manager │──│ Executor │──│ Pipeline │──│ (Pydantic) │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
1. Configurazione & Theme Custom
# gui_config.py
from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict
from typing import Literal
import gradio as gr
class GUIConfig(BaseSettings):
model_config = SettingsConfigDict(env_prefix="GUI_", env_file=".env")
# Server
server_name: str = "127.0.0.1" # "0.0.0.0" per LAN
server_port: int = 7860
share: bool = False # MAI True in produzione!
ssl_keyfile: str = ""
ssl_certfile: str = ""
auth_user: str = "" # obbligatori se server_name != 127.0.0.1
auth_password: str = ""
# Queue
queue_max_size: int = 20
queue_default_concurrency_limit: int = 4
# UI
theme: Literal["default", "soft", "monochrome", "glass", "custom"] = "custom"
title: str = "🤖 Il Mio Agente Digitale"
description: str = "Agente AI locale, privacy-first, powered by Ollama + LangChain"
favicon_path: str = ""
# Chat
max_history: int = 50
streaming: bool = True
show_token_count: bool = True
# RAG
rag_enabled: bool = True
rag_chunk_size: int = 1000
rag_chunk_overlap: int = 200
# Examples
examples: list[list[str]] = [
["Cos'è un Agente AI e come si differenzia da un chatbot?"],
["Spiegami la quantizzazione GGUF Q4_K_M vs Q8_0"],
["Come configurare Ollama per usare GPU su Linux?"],
["Scrivi una funzione Python per calcolare Fibonacci iterativo"],
["Analizza i documenti caricati: qual è l'architettura RAG?"],
]
config = GUIConfig()
# === Theme Custom ===
def get_custom_theme() -> gr.Theme:
"""Theme personalizzato: dark/light, colori brand, font."""
return gr.themes.Base(
primary_hue="blue",
secondary_hue="slate",
neutral_hue="slate",
radius_size="md",
font=["Inter", "system-ui", "sans-serif"],
font_mono=["JetBrains Mono", "Fira Code", "monospace"],
spacing_size="sm",
).set(
# Colors
body_background_fill="*neutral_50",
body_background_fill_dark="*neutral_950",
block_background_fill="white",
block_background_fill_dark="*neutral_900",
block_border_width="1px",
block_shadow="0 1px 3px rgba(0,0,0,0.1)",
block_shadow_dark="0 1px 3px rgba(0,0,0,0.3)",
button_primary_background_fill="*primary_600",
button_primary_background_fill_hover="*primary_700",
button_secondary_background_fill="*neutral_200",
button_secondary_background_fill_hover="*neutral_300",
button_secondary_background_fill_dark="*neutral_800",
button_secondary_background_fill_hover_dark="*neutral_700",
input_background_fill="white",
input_background_fill_dark="*neutral_800",
input_border_color="*neutral_300",
input_border_color_dark="*neutral_700",
# Codice nei messaggi
code_background_fill="*neutral_100",
code_background_fill_dark="*neutral_800",
)
# Theme selector per utente
THEMES = {
"default": gr.themes.Default(),
"soft": gr.themes.Soft(),
"monochrome": gr.themes.Monochrome(),
"glass": gr.themes.Glass(),
"custom": get_custom_theme(),
}
2. Session State Management
# gui_state.py
from dataclasses import dataclass, field
from typing import List, Dict, Any, Optional
from datetime import datetime
import uuid
@dataclass
class ChatMessage:
role: str # "user" | "assistant" | "system"
content: str
metadata: Dict[str, Any] = field(default_factory=dict)
timestamp: datetime = field(default_factory=datetime.now)
tokens: int = 0
@dataclass
class SessionState:
"""Stato per singola sessione utente (Gradio session)."""
session_id: str = field(default_factory=lambda: str(uuid.uuid4())[:8])
history: List[ChatMessage] = field(default_factory=list)
system_prompt: str = ""
model: str = "mistral:7b-instruct-q4_k_m"
temperature: float = 0.7
top_p: float = 0.9
num_ctx: int = 4096
num_predict: int = -1
num_gpu_layers: int = 999
rag_enabled: bool = False
rag_collection: str = "documents"
created_at: datetime = field(default_factory=datetime.now)
updated_at: datetime = field(default_factory=datetime.now)
def add_message(self, role: str, content: str, **metadata) -> ChatMessage:
msg = ChatMessage(role=role, content=content, metadata=metadata)
self.history.append(msg)
self.updated_at = datetime.now()
# Trim history
if len(self.history) > 100:
self.history = self.history[-100:]
return msg
def get_gradio_history(self) -> List[List[str]]:
"""Formato per gr.Chatbot: [[user, assistant], ...]"""
pairs = []
user_msg = None
for msg in self.history:
if msg.role == "user":
user_msg = msg.content
elif msg.role == "assistant" and user_msg is not None:
pairs.append([user_msg, msg.content])
user_msg = None
return pairs
def clear_history(self, keep_system: bool = True):
if keep_system and self.system_prompt:
sys = self.system_prompt
self.history = [ChatMessage(role="system", content=sys)]
else:
self.history = []
def to_dict(self) -> Dict[str, Any]:
return {
"session_id": self.session_id,
"model": self.model,
"temperature": self.temperature,
"top_p": self.top_p,
"num_ctx": self.num_ctx,
"num_predict": self.num_predict,
"num_gpu_layers": self.num_gpu_layers,
"rag_enabled": self.rag_enabled,
"rag_collection": self.rag_collection,
"history_length": len(self.history),
}
3. Backend Integration (Agent + RAG)
# gui_backend.py
from typing import AsyncGenerator, List, Dict, Any, Optional
from gui_state import SessionState, ChatMessage
from agent_setup import create_agent
from custom_tools import get_all_tools
from tool_descriptions import enhance_tool_descriptions
from llm_setup import get_llm
from rag_pipeline import create_conversational_qa_chain
import asyncio
import logging
logger = logging.getLogger(__name__)
class AgentBackend:
"""Backend condiviso per tutte le sessioni."""
def __init__(self):
self.llm = get_llm()
self.tools = enhance_tool_descriptions(get_all_tools())
self.agent = create_agent(llm=self.llm, tools=self.tools)
self.rag_chains: Dict[str, Any] = {} # cache per collection
def get_rag_chain(self, collection: str):
if collection not in self.rag_chains:
self.rag_chains[collection] = create_conversational_qa_chain(
collection_name=collection,
)
return self.rag_chains[collection]
backend = AgentBackend()
async def stream_agent_response(
session: SessionState,
user_input: str,
) -> AsyncGenerator[str, None]:
"""Stream risposta agente token-by-token."""
# Prepara input per agente
agent_input = {
"input": user_input,
}
# Se RAG abilitato, usa chain conversazionale
if session.rag_enabled:
rag_chain = backend.get_rag_chain(session.rag_collection)
# ConversationalRetrievalChain non streamma nativamente bene
# Fallback: invoke completo poi streamma simulato
result = await asyncio.to_thread(
rag_chain.invoke,
{"question": user_input, "chat_history": []},
)
answer = result["answer"]
# Simula streaming parola per parola
for word in answer.split():
yield word + " "
await asyncio.sleep(0.02)
return
# Altrimenti usa agente con tool
try:
# AgentExecutor non è async-native, wrap in thread
result = await asyncio.to_thread(
backend.agent.invoke,
agent_input,
)
answer = result.get("output", "Errore: nessuna risposta")
# Streamma risposta
for word in answer.split():
yield word + " "
await asyncio.sleep(0.01)
except Exception as e:
logger.error(f"Agent error: {e}")
yield f"Errore: {e}"
async def stream_llm_response(
session: SessionState,
user_input: str,
) -> AsyncGenerator[str, None]:
"""Stream diretto LLM (senza agent tools) - più veloce per chat semplice."""
from langchain_core.messages import HumanMessage, SystemMessage, AIMessage
messages = []
if session.system_prompt:
messages.append(SystemMessage(content=session.system_prompt))
# Esclude gli ultimi due: il messaggio utente corrente e il segnaposto vuoto dell'assistente
for msg in session.history[:-2]:
if msg.role == "user":
messages.append(HumanMessage(content=msg.content))
elif msg.role == "assistant":
messages.append(AIMessage(content=msg.content))
messages.append(HumanMessage(content=user_input))
# Stream da LLM
async for chunk in backend.llm.astream(messages):
if chunk.content:
yield chunk.content
4. Gradio Blocks: Interfaccia Completa
# gui_app.py
import gradio as gr
from gui_config import config, THEMES
from gui_state import SessionState
from gui_backend import stream_agent_response, stream_llm_response, backend
import asyncio
import logging
import tempfile
from typing import AsyncGenerator
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# === CSS Custom ===
CUSTOM_CSS = """
/* Chat container */
#chatbot { height: 60vh !important; min-height: 400px; }
/* Sidebar */
#sidebar { min-width: 280px; max-width: 350px; }
/* Code blocks */
.gradio-container pre { border-radius: 8px !important; }
/* Scrollbar */
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: #9ca3af; border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: #6b7280; }
/* Streaming cursor */
.streaming-cursor { animation: blink 1s infinite; }
@keyframes blink { 0%, 50% { opacity: 1; } 51%, 100% { opacity: 0; } }
"""
# === Event Handlers ===
def create_session_state() -> SessionState:
"""Crea nuovo stato per sessione Gradio."""
session = SessionState()
# System prompt default
session.system_prompt = """Sei un assistente AI locale, privacy-first.
Rispondi in italiano. Sii conciso, tecnico quando serve.
Se non sai qualcosa, ammettilo."""
return session
async def handle_user_message(
message: str,
history: list,
session: SessionState,
use_agent: bool,
) -> tuple[str, list, SessionState]:
"""Handler messaggio utente: aggiunge a history, prepara per streaming."""
if not message.strip():
return "", history, session
# Aggiungi messaggio utente
session.add_message("user", message)
# Aggiorna history per Gradio
new_history = session.get_gradio_history()
return "", new_history, session
async def stream_bot_response(
history: list,
session: SessionState,
use_agent: bool,
) -> AsyncGenerator[tuple[list, SessionState], None]:
"""Stream risposta bot (agente o LLM diretto)."""
if not session.history or session.history[-1].role != "user":
yield history, session
return
user_msg = session.history[-1].content
# Placeholder per messaggio assistant
session.add_message("assistant", "")
history = session.get_gradio_history()
yield history, session
# Stream
full_response = ""
stream_func = stream_agent_response if use_agent else stream_llm_response
async for chunk in stream_func(session, user_msg):
full_response += chunk
# Aggiorna ultimo messaggio
session.history[-1].content = full_response
history = session.get_gradio_history()
yield history, session
# Finalizza
session.history[-1].content = full_response.strip()
history = session.get_gradio_history()
yield history, session
def update_model_params(
session: SessionState,
model: str,
temperature: float,
top_p: float,
num_ctx: int,
num_predict: int,
num_gpu_layers: int,
) -> SessionState:
"""Aggiorna parametri modello."""
session.model = model
session.temperature = temperature
session.top_p = top_p
session.num_ctx = num_ctx
session.num_predict = num_predict
session.num_gpu_layers = num_gpu_layers
# Ricrea LLM con nuovi parametri
backend.llm = backend.llm.bind(
model=model,
temperature=temperature,
top_p=top_p,
num_ctx=num_ctx,
num_predict=num_predict,
)
return session
def toggle_rag(
session: SessionState,
enabled: bool,
collection: str,
) -> SessionState:
session.rag_enabled = enabled
session.rag_collection = collection
return session
def clear_chat(session: SessionState) -> tuple[list, SessionState]:
session.clear_history()
return [], session
def export_chat(session: SessionState) -> str:
"""Esporta chat in Markdown e restituisce il percorso del file (gr.File vuole un percorso)."""
lines = [f"# Chat Export - Session {session.session_id}", ""]
for msg in session.history:
if msg.role == "system":
continue
role_label = "👤 **Utente**" if msg.role == "user" else "🤖 **Assistente**"
lines.append(f"## {role_label}")
lines.append(msg.content)
lines.append("")
with tempfile.NamedTemporaryFile(
"w", suffix=".md", prefix=f"chat-{session.session_id}-", delete=False, encoding="utf-8"
) as fh:
fh.write("\n".join(lines))
return fh.name
# === Build UI ===
def build_ui() -> gr.Blocks:
theme = THEMES[config.theme]
with gr.Blocks(
theme=theme,
title=config.title,
css=CUSTOM_CSS,
analytics_enabled=False,
) as app:
# State
session_state = gr.State(create_session_state)
# Header
with gr.Row(equal_height=True):
gr.Markdown(f"# {config.title}")
with gr.Row():
model_dropdown = gr.Dropdown(
choices=[
"mistral:7b-instruct-q4_k_m",
"llama3.1:8b-instruct-q4_k_m",
"phi3.5:mini-q4_k_m",
"gemma2:9b-instruct-q4_k_m",
"qwen2.5:14b-instruct-q4_k_m",
],
value="mistral:7b-instruct-q4_k_m",
label="Modello",
scale=2,
)
status = gr.HTML("<span style='color:green'>●</span> Pronto")
gr.Markdown(config.description)
# Main Layout
with gr.Row():
# Sidebar
with gr.Column(scale=1, min_width=280, elem_id="sidebar"):
with gr.Group():
gr.Markdown("### ⚙️ Parametri Modello")
temperature = gr.Slider(
0.0, 2.0, value=0.7, step=0.1,
label="Temperature",
info="Creatività (0=deterministico, 1=creativo)"
)
top_p = gr.Slider(
0.0, 1.0, value=0.9, step=0.05,
label="Top-p",
info="Nucleus sampling"
)
num_ctx = gr.Slider(
512, 32768, value=4096, step=512,
label="Context Window",
info="Token max (input + output)"
)
num_predict = gr.Slider(
-1, 8192, value=-1, step=128,
label="Max Tokens Output",
info="-1 = illimitato"
)
num_gpu_layers = gr.Slider(
-1, 999, value=999, step=1,
label="GPU Layers",
info="-1=CPU, 999=tutti GPU"
)
apply_params = gr.Button("Applica Parametri", variant="secondary", size="sm")
with gr.Group():
gr.Markdown("### 📚 RAG (Documenti)")
rag_enabled = gr.Checkbox(
label="Abilita RAG",
value=False,
info="Usa documenti indicizzati per rispondere"
)
rag_collection = gr.Dropdown(
choices=["documents", "knowledge_base", "custom"],
value="documents",
label="Collection",
allow_custom_value=True,
)
rag_toggle = gr.Button(
"Attiva RAG" if not config.rag_enabled else "Disattiva RAG",
variant="secondary", size="sm"
)
with gr.Group():
gr.Markdown("### 🛠️ Modalità")
use_agent = gr.Checkbox(
label="Usa Agente (Tool Use)",
value=True,
info="Abilita web search, file I/O, shell, calculator"
)
gr.Markdown("*Agente = tool use. Disabilitato = chat diretta LLM*")
with gr.Group():
gr.Markdown("### 💾 Azioni")
clear_btn = gr.Button("🗑️ Pulisci Chat", variant="stop", size="sm")
export_btn = gr.Button("📥 Esporta Markdown", variant="secondary", size="sm")
export_file = gr.File(label="Download", visible=False)
# Main Chat Area
with gr.Column(scale=4):
chatbot = gr.Chatbot(
elem_id="chatbot",
show_label=False,
avatar_images=("👤", "🤖"),
bubble_full_width=False,
render_markdown=True,
sanitize_html=True,
show_copy_button=True,
likeable=True,
)
with gr.Row():
msg_input = gr.Textbox(
placeholder="Scrivi un messaggio... (Shift+Enter per nuova riga)",
show_label=False,
scale=8,
container=False,
autofocus=True,
)
send_btn = gr.Button("Invia", variant="primary", scale=1, size="lg")
# Examples
gr.Examples(
examples=config.examples,
inputs=msg_input,
label="Esempi rapidi",
)
# Footer
gr.Markdown("""
---
**Il Mio Primo Agente Digitale** — Serie Vibe Pro Vol.2 |
[Serie sul blog](https://www.roccobalzama.it/blog/) |
Privacy-first · Locale · Zero Cloud
""")
# === Event Bindings ===
# Parametri
apply_params.click(
fn=update_model_params,
inputs=[session_state, model_dropdown, temperature, top_p, num_ctx, num_predict, num_gpu_layers],
outputs=[session_state],
)
# RAG
rag_toggle.click(
fn=toggle_rag,
inputs=[session_state, rag_enabled, rag_collection],
outputs=[session_state],
).then(
fn=lambda e, c: "Disattiva RAG" if e else "Attiva RAG",
inputs=[rag_enabled, rag_collection],
outputs=[rag_toggle],
)
# Chat flow
msg_input.submit(
fn=handle_user_message,
inputs=[msg_input, chatbot, session_state, use_agent],
outputs=[msg_input, chatbot, session_state],
queue=True,
).then(
fn=stream_bot_response,
inputs=[chatbot, session_state, use_agent],
outputs=[chatbot, session_state],
queue=True,
)
send_btn.click(
fn=handle_user_message,
inputs=[msg_input, chatbot, session_state, use_agent],
outputs=[msg_input, chatbot, session_state],
queue=True,
).then(
fn=stream_bot_response,
inputs=[chatbot, session_state, use_agent],
outputs=[chatbot, session_state],
queue=True,
)
# Clear
clear_btn.click(
fn=clear_chat,
inputs=[session_state],
outputs=[chatbot, session_state],
queue=False,
)
# Export
export_btn.click(
fn=export_chat,
inputs=[session_state],
outputs=[export_file],
).then(
fn=lambda f: gr.File(visible=True),
inputs=[export_file],
outputs=[export_file],
)
return app
# === Launch ===
def launch_app():
app = build_ui()
app.queue(
max_size=config.queue_max_size,
default_concurrency_limit=config.queue_default_concurrency_limit,
)
launch_kwargs = {
"server_name": config.server_name,
"server_port": config.server_port,
"share": config.share,
"show_error": True,
"quiet": False,
}
if config.auth_user and config.auth_password:
launch_kwargs["auth"] = (config.auth_user, config.auth_password)
elif config.server_name != "127.0.0.1":
raise SystemExit("Imposta GUI_AUTH_USER e GUI_AUTH_PASSWORD per esporre la GUI in rete.")
if config.ssl_keyfile and config.ssl_certfile:
launch_kwargs["ssl_keyfile"] = config.ssl_keyfile
launch_kwargs["ssl_certfile"] = config.ssl_certfile
logger.info(f"Avvio su {config.server_name}:{config.server_port} (share={config.share})")
app.launch(**launch_kwargs)
if __name__ == "__main__":
launch_app()
5. Deploy Locale & HTTPS Tunnel
Locale (LAN only)
# .env
GUI_SERVER_NAME=0.0.0.0
GUI_SERVER_PORT=7860
GUI_SHARE=false
GUI_AUTH_USER=scegli-un-utente
GUI_AUTH_PASSWORD=scegli-una-password-lunga
# GUI_SSL_KEYFILE=./certs/key.pem
# GUI_SSL_CERTFILE=./certs/cert.pem
python gui_app.py
# Accessibile su http://TUA_IP_LAN:7860
HTTPS con Cloudflared (consigliato, gratuito)
# Installa cloudflared
# macOS: brew install cloudflared
# Linux: wget -q https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb && sudo dpkg -i cloudflared-linux-amd64.deb
# Tunnel temporaneo (quick tunnel, senza account): espone la GUI a Internet, usalo solo con auth attiva
cloudflared tunnel --url http://localhost:7860
# Output: https://random-name.trycloudflare.com
# Oppure tunnel permanente con DNS (richiede account Cloudflare e un dominio)
cloudflared tunnel login
cloudflared tunnel create agente-digitale
cloudflared tunnel route dns agente-digitale agente.tuodominio.com
cloudflared tunnel run --config ~/.cloudflared/config.yml agente-digitale
HTTPS con ngrok (alternativa)
# ngrok richiede un account e un authtoken: ngrok config add-authtoken <TOKEN>
ngrok http 7860
# Output: https://abc123.ngrok-free.app
Systemd Service (Produzione Linux)
# /etc/systemd/system/agente-digitale.service
[Unit]
Description=Agente Digitale Gradio UI
After=network.target ollama.service
[Service]
Type=simple
User=ubuntu
WorkingDirectory=/opt/agente-digitale
Environment=GUI_SERVER_NAME=0.0.0.0
Environment=GUI_SERVER_PORT=7860
Environment=GUI_SHARE=false
# Utente e password: file .env nella WorkingDirectory (GUI_AUTH_USER, GUI_AUTH_PASSWORD)
ExecStart=/opt/agente-digitale/venv/bin/python gui_app.py
Restart=on-failure
RestartSec=10
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now agente-digitale
sudo journalctl -u agente-digitale -f
6. Troubleshooting GUI
| Problema | Causa | Soluzione | | --- | --- | --- | | Address already in use | Porta 7860 occupata | Cambia GUI_SERVER_PORT, kill processo: lsof -ti:7860 \| xargs kill | | Streaming non funziona | Gradio version < 4.0 / queue non configurato | app.queue(), aggiorna Gradio pip install -U gradio | | File upload non appare | gr.File non in layout / type mismatch | Usa gr.UploadButton o gr.File(file_types=[".pdf", ".txt"]) | | Theme non si applica | Cache browser / CSS conflitto | Hard refresh (Ctrl+Shift+R), gr.Theme va passato a Blocks(theme=...) | | Agente lento / timeout | queue concurrency troppo bassa / LLM lento | Aumenta default_concurrency_limit, usa num_predict limitato | | Chat history persa su reload | Gradio state per-session, non persistente | Implementa salvataggio su file/DB in clear_chat/export | | Mobile layout rotto | CSS custom min-width sidebar | Usa gr.Row(equal_height=True) e scale responsive |
Come fare, passo per passo: dal codice alla GUI funzionante
Segui l'ordine: ogni passo si verifica prima di passare al successivo.
- Installa le versioni indicate. Crea un ambiente virtuale e installa
requirements.txtcon i limitigradio<5. Con unpip install gradiosenza vincoli ottieni una versione più recente, che non accetta alcuni parametri usati in questo capitolo (per esempiobubble_full_widthingr.Chatbot). - Controlla il backend senza interfaccia. Da Python importa
gui_backende chiamastream_llm_responsecon una sessione di prova. Se qui non funziona, il problema non è Gradio. - Avvia la GUI in locale. Lascia
GUI_SERVER_NAME=127.0.0.1e lanciapython gui_app.py. Aprihttp://127.0.0.1:7860dal tuo computer. - Prova la chat diretta. Disattiva "Usa Agente" e scrivi un messaggio: devi vedere la risposta arrivare a pezzi.
- Prova l'agente. Riattiva "Usa Agente" e chiedi un calcolo semplice: la risposta passa dal tool.
- Prova esporta e pulisci. Esporta la chat in Markdown, poi pulisci la conversazione.
- Apri alla rete solo dopo aver impostato l'accesso. Se vuoi usare la GUI da un altro dispositivo, imposta
GUI_AUTH_USEReGUI_AUTH_PASSWORDnel file.env, poi cambiaGUI_SERVER_NAMEin0.0.0.0. - Verifica dall'esterno. Dal telefono nella stessa rete apri l'indirizzo del computer: deve chiedere utente e password.
Sicurezza: chi può usare la tua interfaccia
L'interfaccia dà accesso a un agente che può leggere e scrivere file, fare richieste di rete e, a seconda dei tool attivi, lanciare comandi (vedi i limiti spiegati nel Cap. 6). Chi raggiunge la pagina usa quei tool con i tuoi permessi, quindi:
- Con
server_name="127.0.0.1"solo il tuo computer raggiunge la GUI. È la scelta predefinita. - Con
0.0.0.0la raggiunge chiunque sia nella tua rete: imposta sempre utente e password, o metti la GUI dietro un proxy che autentica. share=Truecrea un link pubblico temporaneo ospitato da Gradio (guida alla condivisione): con un agente che ha tool di sistema è una scelta da evitare.- I tunnel Cloudflare e ngrok espongono la porta a Internet: abilitali solo con autenticazione attiva e tool disattivati o ristretti.
- Nei log e nell'esportazione finiscono le conversazioni: se contengono dati personali, trattali come tali (conservazione limitata, accesso ristretto); per rimuovere i dati personali prima di darli a un modello vedi la pagina su anonimizzazione dei dati.
- Il carattere "privacy-first" riguarda il modello locale. Se l'agente usa la ricerca web o chiamate HTTP, quelle richieste escono dal tuo computer.
Checklist prima di aprire la GUI ad altre persone
- Autenticazione attiva (
auth) o proxy che autentica. share=False.- Tool di scrittura, shell e HTTP disattivati o ristretti per chi non è amministratore.
- HTTPS se esci dalla rete locale.
- Versioni di Gradio e dipendenze fissate.
- Coda con
max_sizeimpostato, per evitare sovraccarico. - Cartella di lavoro dell'agente separata dai tuoi dati.
- Conversazioni esportate e log con una durata di conservazione decisa.
Errori da evitare
- Mettere
0.0.0.0"per provare" e dimenticare di tornare a127.0.0.1. - Usare
share=Truecon un agente che ha accesso ai file. - Installare
gradiosenza vincoli di versione e ritrovarsi con parametri non più validi. - Confondere lo stato globale con quello per sessione: i parametri del modello cambiati da un utente non devono valere per tutti.
- Salvare password nel codice invece che in
.env. - Aspettarsi che lo streaming funzioni senza
queue()attivo.
Consigli pratici
- Prova prima l'interfaccia con la chat diretta, poi aggiungi RAG e agente: così sai quale parte rompe cosa.
- Tieni il tema in un file separato e prova modalità chiara e scura con il selettore del browser.
- Limita
num_ctxai valori che la tua macchina regge: un contesto troppo grande rallenta tutto o esaurisce la memoria. - Per chi usa la GUI da telefono, controlla che la barra laterale non occupi tutto lo schermo.
- Se la risposta dell'agente sembra "finta" streaming (parola per parola dopo l'attesa), è normale: l'agente completa il lavoro e poi la risposta viene riprodotta. Lo streaming reale è nella chat diretta.
- Per un assistente pubblico con molti utenti conviene un servizio costruito apposta, non questa GUI. Se hai bisogno di un assistente per clienti, vedi la pagina su chatbot e agenti o l'articolo sugli agenti AI in azienda.
Domande frequenti
Perché Gradio e non Streamlit? Gradio ha componenti pronti per chat, upload e streaming, e un sistema di code per più sessioni. Streamlit funziona bene per app di dati e supporta anche la chat, ma ricarica lo script a ogni interazione.
Posso usare la GUI da un altro computer? Sì: imposta 0.0.0.0 come indirizzo e abilita l'autenticazione. Meglio ancora, un proxy con HTTPS.
Dove sono salvate le conversazioni? Solo in memoria, per sessione. Se riavvii l'app o ricarichi la pagina si perdono; l'esportazione in Markdown le conserva su file.
Posso cambiare modello dalla GUI? L'esempio prevede un menu a tendina, ma il cambio vale per il backend condiviso. Per sessioni indipendenti serve una copia del modello per ciascuna.
Serve una GPU? No, ma senza GPU i modelli grandi sono lenti. Scegli la dimensione del modello in base alla tua macchina.
Prossimo capitolo
Cap. 8: il tuo agente specializzato → System prompt engineering, few-shot examples, fine-tuning base (LoRA/QLoRA su consumer GPU), valutazione della qualità, domain adaptation (legal, medical, code, business).
File completo
App Gradio production-ready: gui_app.py — 400+ righe, theme custom, session state, streaming, RAG toggle, agent toggle, export.
Serie "Il Mio Primo Agente Digitale" — Requisiti: Gradio 4.44.x (non 5/6), Python 3.11+, Ollama 0.3+, LangChain 0.3.x.