1. Home
  2. iThink
  3. AI Locale & Agenti
  4. Interfaccia grafica per un agente AI con Gradio
AI Locale & Agenti Il Mio Primo Agente Digitale · Capitolo 7

Interfaccia grafica per un agente AI con Gradio

Robot bianco, simbolo di un assistente digitale
Foto: StockSnap · Pixabay · Licenza dei contenuti Pixabay

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.

  1. Installa le versioni indicate. Crea un ambiente virtuale e installa requirements.txt con i limiti gradio<5. Con un pip install gradio senza vincoli ottieni una versione più recente, che non accetta alcuni parametri usati in questo capitolo (per esempio bubble_full_width in gr.Chatbot).
  2. Controlla il backend senza interfaccia. Da Python importa gui_backend e chiama stream_llm_response con una sessione di prova. Se qui non funziona, il problema non è Gradio.
  3. Avvia la GUI in locale. Lascia GUI_SERVER_NAME=127.0.0.1 e lancia python gui_app.py. Apri http://127.0.0.1:7860 dal tuo computer.
  4. Prova la chat diretta. Disattiva "Usa Agente" e scrivi un messaggio: devi vedere la risposta arrivare a pezzi.
  5. Prova l'agente. Riattiva "Usa Agente" e chiedi un calcolo semplice: la risposta passa dal tool.
  6. Prova esporta e pulisci. Esporta la chat in Markdown, poi pulisci la conversazione.
  7. Apri alla rete solo dopo aver impostato l'accesso. Se vuoi usare la GUI da un altro dispositivo, imposta GUI_AUTH_USER e GUI_AUTH_PASSWORD nel file .env, poi cambia GUI_SERVER_NAME in 0.0.0.0.
  8. 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.0 la raggiunge chiunque sia nella tua rete: imposta sempre utente e password, o metti la GUI dietro un proxy che autentica.
  • share=True crea 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_size impostato, 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 a 127.0.0.1.
  • Usare share=True con un agente che ha accesso ai file.
  • Installare gradio senza 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_ctx ai 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.

Condividi LinkedIn Facebook X WhatsApp
Continua a leggere

Tutti gli articoli del blog

Strumenti gratuiti