401 lines
15 KiB
Python
401 lines
15 KiB
Python
import streamlit as st
|
|
from pathlib import Path
|
|
import time
|
|
|
|
from config import settings
|
|
from core import rag
|
|
from agent import agent, AgentMode
|
|
from ollama_client import ollama, LLMClient, PROVIDER_BASE_URLS
|
|
from logger import get_logger
|
|
|
|
log = get_logger("app")
|
|
|
|
from document_processor import DocumentProcessor
|
|
|
|
SUPPORTED_EXTENSIONS = list(DocumentProcessor.SUPPORTED_EXTENSIONS.keys())
|
|
|
|
PROVIDER_MODELS = {
|
|
"ollama": ["gemma2:9b", "llama3.2", "llama3.1", "mistral"],
|
|
"opencode": ["claude-3.5-sonnet", "gpt-4o", "gemini-2.0-flash", "deepseek-r1", "llama-3.3-70b"],
|
|
"openrouter": ["openrouter/auto", "anthropic/claude-3.5-sonnet", "google/gemini-pro-1.5", "meta-llama/llama-3.1-70b-instruct"],
|
|
"groq": ["llama-3.1-70b-versatile", "llama-3.1-8b-instant", "mixtral-8x7b-32768"],
|
|
"openai": ["gpt-4o", "gpt-4o-mini", "gpt-4-turbo"]
|
|
}
|
|
|
|
st.set_page_config(
|
|
page_title="RAGGER",
|
|
page_icon="📚",
|
|
layout="wide"
|
|
)
|
|
|
|
|
|
def save_settings(provider, model, api_key, base_url, ollama_host, ollama_model):
|
|
env_path = Path(".env")
|
|
lines = []
|
|
|
|
if env_path.exists():
|
|
for line in env_path.read_text().splitlines():
|
|
if line.startswith("LLM_PROVIDER="):
|
|
lines.append(f"LLM_PROVIDER={provider}")
|
|
elif line.startswith("LLM_MODEL="):
|
|
lines.append(f"LLM_MODEL={model}")
|
|
elif line.startswith("LLM_API_KEY="):
|
|
lines.append(f"LLM_API_KEY={api_key}")
|
|
elif line.startswith("LLM_BASE_URL="):
|
|
lines.append(f"LLM_BASE_URL={base_url}")
|
|
elif line.startswith("OLLAMA_HOST="):
|
|
lines.append(f"OLLAMA_HOST={ollama_host}")
|
|
elif line.startswith("OLLAMA_MODEL="):
|
|
lines.append(f"OLLAMA_MODEL={ollama_model}")
|
|
else:
|
|
lines.append(line)
|
|
else:
|
|
lines = [
|
|
f"LLM_PROVIDER={provider}",
|
|
f"LLM_MODEL={model}",
|
|
f"LLM_API_KEY={api_key}",
|
|
f"LLM_BASE_URL={base_url}",
|
|
f"OLLAMA_HOST={ollama_host}",
|
|
f"OLLAMA_MODEL={ollama_model}",
|
|
]
|
|
|
|
env_path.write_text("\n".join(lines) + "\n")
|
|
log.info(f"Settings saved: provider={provider}, model={model}")
|
|
|
|
|
|
def check_health():
|
|
log.debug("Running startup health check")
|
|
issues = []
|
|
|
|
if not ollama.is_available():
|
|
issues.append("Ollama non raggiungibile")
|
|
log.warning("Ollama not available at startup")
|
|
|
|
return issues
|
|
|
|
|
|
startup_issues = check_health()
|
|
if startup_issues:
|
|
st.warning("⚠️ Problemi rilevati: " + ", ".join(startup_issues))
|
|
|
|
|
|
def show_logs():
|
|
"""Mostra i log dell'applicazione"""
|
|
log_file = Path("logs/ragger.log")
|
|
if log_file.exists():
|
|
lines = log_file.read_text().splitlines()
|
|
last_lines = lines[-50:] if len(lines) > 50 else lines
|
|
return "\n".join(last_lines)
|
|
return "Nessun log disponibile"
|
|
|
|
|
|
def check_ollama_status():
|
|
"""Verifica se Ollama sta elaborando qualcosa"""
|
|
try:
|
|
import httpx
|
|
resp = httpx.get(f"{settings.OLLAMA_HOST}/api/tags", timeout=5)
|
|
return resp.status_code == 200
|
|
except:
|
|
return False
|
|
|
|
|
|
st.title("📚 RAGGER")
|
|
st.caption("Sistema RAG con Agent Intelligente")
|
|
|
|
|
|
with st.sidebar:
|
|
st.header("⚙️ Modalità")
|
|
|
|
mode = st.selectbox(
|
|
"Seleziona modalità",
|
|
options=["chat", "lettera", "progetto", "impostazioni"],
|
|
index=0,
|
|
format_func=lambda x: {
|
|
"chat": "💬 Chat",
|
|
"lettera": "✉️ Lettera",
|
|
"progetto": "📋 Progetto",
|
|
"impostazioni": "⚙️ Impostazioni"
|
|
}[x]
|
|
)
|
|
|
|
st.divider()
|
|
|
|
with st.expander("📜 Log Applicazione", expanded=True):
|
|
log_content = show_logs()
|
|
st.text_area("", log_content, height=200, key="log_viewer", disabled=True)
|
|
|
|
col_log1, col_log2 = st.columns(2)
|
|
with col_log1:
|
|
if st.button("🔄 Aggiorna Log", use_container_width=True):
|
|
st.rerun()
|
|
with col_log2:
|
|
if st.button("📋 Copia Log", use_container_width=True):
|
|
st.write("Log copiato!")
|
|
|
|
ollama_status = check_ollama_status()
|
|
if ollama_status:
|
|
st.success("🟢 Ollama connesso")
|
|
else:
|
|
st.error("🔴 Ollama non risponde")
|
|
|
|
if mode == "impostazioni":
|
|
st.divider()
|
|
st.header("Configurazione")
|
|
st.write(f"**Ollama:** {settings.OLLAMA_HOST}")
|
|
st.write(f"**Modello:** {settings.OLLAMA_MODEL}")
|
|
st.write(f"**Embedding:** {settings.OLLAMA_EMBED_MODEL}")
|
|
|
|
if st.button("Test Connessione Ollama"):
|
|
with st.spinner("Testing..."):
|
|
if ollama.is_available():
|
|
st.success("✅ Ollama connesso!")
|
|
else:
|
|
st.error("❌ Ollama non raggiungibile")
|
|
|
|
stats = rag.get_stats()
|
|
st.metric("Documenti indicizzati", stats["total_chunks"])
|
|
|
|
st.divider()
|
|
|
|
tab_imp, _ = st.tabs(["⚙️ Impostazioni"])
|
|
with tab_imp:
|
|
st.header("⚙️ Impostazioni Provider LLM")
|
|
|
|
current_provider = settings.LLM_PROVIDER
|
|
all_providers = ["ollama", "opencode", "openrouter", "groq", "openai"]
|
|
provider = st.selectbox(
|
|
"Provider",
|
|
options=all_providers,
|
|
index=all_providers.index(current_provider) if current_provider in all_providers else 0
|
|
)
|
|
|
|
if provider == "ollama":
|
|
st.info("🏠 **Ollama** - Locale/On-premise")
|
|
ollama_host = st.text_input("Ollama Host", value=settings.OLLAMA_HOST)
|
|
ollama_models = ollama.list_models() or PROVIDER_MODELS["ollama"]
|
|
model = st.selectbox("Modello", options=ollama_models, index=0)
|
|
api_key = ""
|
|
base_url = ""
|
|
elif provider == "opencode":
|
|
st.info("🧘 **OpenCode Zen**")
|
|
api_key = st.text_input("API Key", value=settings.LLM_API_KEY or "", type="password")
|
|
base_url = st.text_input("Base URL", value=settings.LLM_BASE_URL or PROVIDER_BASE_URLS["opencode"])
|
|
models = PROVIDER_MODELS.get(provider, [])
|
|
model = st.selectbox("Modello", options=models, index=0)
|
|
ollama_host = settings.OLLAMA_HOST
|
|
else:
|
|
st.info(f"🌐 **{provider.capitalize()}**")
|
|
api_key = st.text_input("API Key", value=settings.LLM_API_KEY or "", type="password")
|
|
default_url = PROVIDER_BASE_URLS.get(provider, "")
|
|
base_url = st.text_input("Base URL", value=settings.LLM_BASE_URL or default_url)
|
|
models = PROVIDER_MODELS.get(provider, [])
|
|
model = st.selectbox("Modello", options=models, index=0)
|
|
ollama_host = settings.OLLAMA_HOST
|
|
|
|
col1, col2 = st.columns(2)
|
|
with col1:
|
|
if st.button("💾 Salva", type="primary"):
|
|
save_settings(provider, model, api_key, base_url, ollama_host, model if provider == "ollama" else settings.OLLAMA_MODEL)
|
|
st.success("✅ Salvato!")
|
|
with col2:
|
|
if st.button("🔄 Ricarica"):
|
|
st.rerun()
|
|
|
|
st.json({
|
|
"provider": settings.LLM_PROVIDER,
|
|
"model": settings.LLM_MODEL or settings.OLLAMA_MODEL,
|
|
})
|
|
|
|
|
|
if mode == "chat":
|
|
with st.sidebar:
|
|
st.divider()
|
|
st.header("📄 Documenti")
|
|
|
|
uploaded = st.file_uploader(
|
|
"Carica documento",
|
|
type=SUPPORTED_EXTENSIONS,
|
|
accept_multiple_files=True
|
|
)
|
|
|
|
if uploaded:
|
|
for file in uploaded:
|
|
if st.button(f"Indicizza: {file.name}", key=f"idx_{file.name}"):
|
|
save_path = Path(settings.UPLOAD_DIR) / file.name
|
|
save_path.write_bytes(file.getbuffer())
|
|
|
|
with st.spinner(f"Indicizzando {file.name}..."):
|
|
try:
|
|
result = rag.ingest_document(save_path)
|
|
st.success(f"✅ {result['chunks_created']} chunks")
|
|
except Exception as e:
|
|
st.error(f"❌ {e}")
|
|
|
|
stats = rag.get_stats()
|
|
st.metric("Chunks indicizzati", stats["total_chunks"])
|
|
|
|
st.header("💬 Chat con i tuoi documenti")
|
|
|
|
if "messages" not in st.session_state:
|
|
st.session_state.messages = []
|
|
|
|
for msg in st.session_state.messages:
|
|
with st.chat_message(msg["role"]):
|
|
st.write(msg["content"])
|
|
if "sources" in msg:
|
|
with st.expander("📚 Fonti"):
|
|
for src in msg["sources"]:
|
|
st.write(f"- **{src['source']}**" +
|
|
(f" (pag. {src['page']})" if src['page'] else ""))
|
|
|
|
if prompt := st.chat_input("Fai una domanda..."):
|
|
log.info(f"Chat prompt: {prompt[:50]}...")
|
|
st.session_state.messages.append({"role": "user", "content": prompt})
|
|
|
|
with st.chat_message("user"):
|
|
st.write(prompt)
|
|
|
|
with st.chat_message("assistant"):
|
|
with st.spinner("Elaborazione in corso..."):
|
|
result = agent.process(prompt, mode="chat")
|
|
|
|
st.write(result["answer"])
|
|
|
|
if result.get("sources"):
|
|
with st.expander("📚 Fonti"):
|
|
for src in result["sources"]:
|
|
st.write(f"- **{src['source']}**" +
|
|
(f" (pag. {src['page']})" if src['page'] else "") +
|
|
f" - Score: {src['score']:.2f}")
|
|
st.caption(src["excerpt"])
|
|
|
|
st.session_state.messages.append({
|
|
"role": "assistant",
|
|
"content": result["answer"],
|
|
"sources": result.get("sources", [])
|
|
})
|
|
|
|
|
|
elif mode == "lettera":
|
|
with st.sidebar:
|
|
st.divider()
|
|
st.header("📄 Documenti")
|
|
|
|
uploaded = st.file_uploader(
|
|
"Carica documento",
|
|
type=SUPPORTED_EXTENSIONS,
|
|
accept_multiple_files=True
|
|
)
|
|
|
|
if uploaded:
|
|
for file in uploaded:
|
|
if st.button(f"Indicizza: {file.name}", key=f"idx_{file.name}"):
|
|
save_path = Path(settings.UPLOAD_DIR) / file.name
|
|
save_path.write_bytes(file.getbuffer())
|
|
|
|
with st.spinner(f"Indicizzando {file.name}..."):
|
|
try:
|
|
result = rag.ingest_document(save_path)
|
|
st.success(f"✅ {result['chunks_created']} chunks")
|
|
except Exception as e:
|
|
st.error(f"❌ {e}")
|
|
|
|
stats = rag.get_stats()
|
|
st.metric("Chunks indicizzati", stats["total_chunks"])
|
|
|
|
st.header("✉️ Genera Lettera")
|
|
|
|
topic = st.text_area("Argomento della lettera", placeholder="Es: Richiesta di patrocinio per evento annuale...")
|
|
context = st.text_area("Contesto aggiuntivo (opzionale)", placeholder="Dettagli specifici, destinatario, tono...")
|
|
|
|
col1, col2 = st.columns([1, 1])
|
|
with col1:
|
|
if st.button("✉️ Genera Lettera", type="primary", use_container_width=True):
|
|
if not topic:
|
|
st.warning("Inserisci un argomento")
|
|
else:
|
|
with st.spinner("Generazione in corso..."):
|
|
result = agent.process(topic, mode="lettera", context=context)
|
|
|
|
st.session_state["last_letter"] = result["answer"]
|
|
|
|
with col2:
|
|
if st.button("🗑️ Pulisci", use_container_width=True):
|
|
if "last_letter" in st.session_state:
|
|
del st.session_state["last_letter"]
|
|
st.rerun()
|
|
|
|
if "last_letter" in st.session_state:
|
|
st.divider()
|
|
st.subheader("Lettera generata")
|
|
st.text_area("", st.session_state["last_letter"], height=400, key="letter_output")
|
|
|
|
c1, c2 = st.columns(2)
|
|
with c1:
|
|
st.button("📋 Copia", on_click=lambda: st.write("Copiato!"))
|
|
with c2:
|
|
st.button("💾 Salva")
|
|
|
|
|
|
elif mode == "progetto":
|
|
with st.sidebar:
|
|
st.divider()
|
|
st.header("📄 Documenti")
|
|
|
|
uploaded = st.file_uploader(
|
|
"Carica documento",
|
|
type=SUPPORTED_EXTENSIONS,
|
|
accept_multiple_files=True
|
|
)
|
|
|
|
if uploaded:
|
|
for file in uploaded:
|
|
if st.button(f"Indicizza: {file.name}", key=f"idx_{file.name}"):
|
|
save_path = Path(settings.UPLOAD_DIR) / file.name
|
|
save_path.write_bytes(file.getbuffer())
|
|
|
|
with st.spinner(f"Indicizzando {file.name}..."):
|
|
try:
|
|
result = rag.ingest_document(save_path)
|
|
st.success(f"✅ {result['chunks_created']} chunks")
|
|
except Exception as e:
|
|
st.error(f"❌ {e}")
|
|
|
|
stats = rag.get_stats()
|
|
st.metric("Chunks indicizzati", stats["total_chunks"])
|
|
|
|
st.header("📋 Genera Proposta Progetto")
|
|
|
|
title = st.text_input("Titolo del progetto", placeholder="Es: Insieme senza confini")
|
|
description = st.text_area("Descrizione", placeholder="Descrivi l'obiettivo, il target, le attività previste...")
|
|
|
|
col1, col2 = st.columns([1, 1])
|
|
with col1:
|
|
if st.button("📋 Genera Proposta", type="primary", use_container_width=True):
|
|
if not title or not description:
|
|
st.warning("Inserisci titolo e descrizione")
|
|
else:
|
|
with st.spinner("Generazione in corso..."):
|
|
result = agent.process(description, mode="progetto", context=title)
|
|
|
|
st.session_state["last_project"] = result["answer"]
|
|
|
|
with col2:
|
|
if st.button("🗑️ Pulisci", use_container_width=True):
|
|
if "last_project" in st.session_state:
|
|
del st.session_state["last_project"]
|
|
st.rerun()
|
|
|
|
if "last_project" in st.session_state:
|
|
st.divider()
|
|
st.subheader("Proposta generata")
|
|
st.text_area("", st.session_state["last_project"], height=500, key="project_output")
|
|
|
|
c1, c2, c3 = st.columns(3)
|
|
with c1:
|
|
st.button("📋 Copia")
|
|
with c2:
|
|
st.button("💾 Salva")
|
|
with c3:
|
|
st.button("📧 Email")
|