|
|
import os |
|
|
import sys |
|
|
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) |
|
|
if ROOT not in sys.path: |
|
|
sys.path.insert(0, ROOT) |
|
|
import json |
|
|
import requests |
|
|
import streamlit as st |
|
|
from utils import base_utils as bu |
|
|
import re |
|
|
|
|
|
CONFIG = bu.load_config("configs/config.json") |
|
|
API_URL = CONFIG.get("ui", {}).get("api_url", "http://127.0.0.1:8000/query") |
|
|
|
|
|
|
|
|
def chamar_api(pergunta: str, mode: str, top_k: int, temperatura: float | None = None): |
|
|
"""Chama a API e retorna resposta e fragmentos recuperados.""" |
|
|
payload = {"question": pergunta, "top_k": top_k, "mode": mode} |
|
|
if temperatura is not None: |
|
|
payload["temperature"] = temperatura |
|
|
resp = requests.post(API_URL, json=payload, timeout=60) |
|
|
resp.raise_for_status() |
|
|
data = resp.json() |
|
|
return data["answer"], data.get("retrieved", []) |
|
|
|
|
|
|
|
|
def formatar_referencias(fragmentos): |
|
|
"""Formata referências numeradas coerentes com citation_id ([1], [2], ...).""" |
|
|
|
|
|
|
|
|
refs_por_id = {} |
|
|
for m in fragmentos: |
|
|
cit_id = m.get("citation_id") |
|
|
if cit_id is None: |
|
|
continue |
|
|
titulo = m.get("document_title") |
|
|
if titulo: |
|
|
titulo = re.sub(r"\[\d+\]", "", titulo).strip() |
|
|
else: |
|
|
titulo = "Documento" |
|
|
titulo_norm = titulo.replace("_", " ").replace("-", " ") |
|
|
refs_por_id[cit_id] = titulo_norm |
|
|
|
|
|
partes = [] |
|
|
for cit_id in sorted(refs_por_id.keys()): |
|
|
partes.append(f"[{cit_id}] {refs_por_id[cit_id]}") |
|
|
|
|
|
return " | ".join(partes) |
|
|
|
|
|
|
|
|
def listar_documentos_unicos(fragmentos): |
|
|
"""Lista documentos únicos recuperados.""" |
|
|
docs = set() |
|
|
for m in fragmentos: |
|
|
docs.add(m['document_id']) |
|
|
return sorted(list(docs)) |
|
|
|
|
|
|
|
|
def obter_lista_documentos(): |
|
|
"""Obtém a lista de documentos indexados no sistema.""" |
|
|
try: |
|
|
|
|
|
list_url = API_URL.replace("/query", "/list_documents") |
|
|
resp = requests.get(list_url, timeout=30) |
|
|
resp.raise_for_status() |
|
|
return resp.json().get("documents", []) |
|
|
except Exception as e: |
|
|
st.error(f"Erro ao obter lista de documentos: {e}") |
|
|
return None |
|
|
|
|
|
|
|
|
|
|
|
st.set_page_config( |
|
|
page_title="Chatbot NORM - Sistema de Consulta", |
|
|
page_icon="🤖", |
|
|
layout="wide", |
|
|
initial_sidebar_state="collapsed" |
|
|
) |
|
|
|
|
|
|
|
|
st.markdown(""" |
|
|
<style> |
|
|
/* Estilo geral */ |
|
|
.main { |
|
|
padding: 2rem; |
|
|
} |
|
|
|
|
|
/* Título principal */ |
|
|
.title-container { |
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
|
|
padding: 2rem; |
|
|
border-radius: 15px; |
|
|
margin-bottom: 2rem; |
|
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); |
|
|
} |
|
|
|
|
|
.title-text { |
|
|
color: white; |
|
|
font-size: 2.5rem; |
|
|
font-weight: bold; |
|
|
margin: 0; |
|
|
text-align: center; |
|
|
} |
|
|
|
|
|
.subtitle-text { |
|
|
color: rgba(255, 255, 255, 0.9); |
|
|
font-size: 1.1rem; |
|
|
margin-top: 0.5rem; |
|
|
text-align: center; |
|
|
} |
|
|
|
|
|
/* Cards de conteúdo */ |
|
|
.content-card { |
|
|
background: white; |
|
|
padding: 1.5rem; |
|
|
border-radius: 10px; |
|
|
border: 1px solid #e0e0e0; |
|
|
margin-bottom: 1rem; |
|
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); |
|
|
} |
|
|
|
|
|
/* Botões personalizados */ |
|
|
.stButton > button { |
|
|
width: 100%; |
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
|
|
color: white; |
|
|
border: none; |
|
|
padding: 0.75rem 1.5rem; |
|
|
border-radius: 8px; |
|
|
font-weight: bold; |
|
|
transition: all 0.3s ease; |
|
|
} |
|
|
|
|
|
.stButton > button:hover { |
|
|
transform: translateY(-2px); |
|
|
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4); |
|
|
} |
|
|
|
|
|
/* Tabs personalizadas */ |
|
|
.stTabs [data-baseweb="tab-list"] { |
|
|
gap: 2rem; |
|
|
background-color: #f8f9fa; |
|
|
padding: 1rem; |
|
|
border-radius: 10px; |
|
|
} |
|
|
|
|
|
.stTabs [data-baseweb="tab"] { |
|
|
padding: 1rem 2rem; |
|
|
background-color: white; |
|
|
border-radius: 8px; |
|
|
font-weight: 600; |
|
|
} |
|
|
|
|
|
.stTabs [aria-selected="true"] { |
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
|
|
color: white; |
|
|
} |
|
|
|
|
|
/* Expander personalizado */ |
|
|
.streamlit-expanderHeader { |
|
|
background-color: #f8f9fa; |
|
|
border-radius: 8px; |
|
|
font-weight: 600; |
|
|
} |
|
|
|
|
|
/* Cards de referência */ |
|
|
.reference-card { |
|
|
background: #f8f9fa; |
|
|
padding: 1rem; |
|
|
border-left: 4px solid #667eea; |
|
|
border-radius: 5px; |
|
|
margin: 0.5rem 0; |
|
|
} |
|
|
|
|
|
/* Badges */ |
|
|
.badge { |
|
|
display: inline-block; |
|
|
padding: 0.25rem 0.75rem; |
|
|
border-radius: 12px; |
|
|
font-size: 0.85rem; |
|
|
font-weight: 600; |
|
|
margin: 0.25rem; |
|
|
} |
|
|
|
|
|
.badge-primary { |
|
|
background-color: #667eea; |
|
|
color: white; |
|
|
} |
|
|
|
|
|
.badge-success { |
|
|
background-color: #4ade80; |
|
|
color: white; |
|
|
} |
|
|
|
|
|
/* Animação de loading */ |
|
|
@keyframes pulse { |
|
|
0%, 100% { opacity: 1; } |
|
|
50% { opacity: 0.5; } |
|
|
} |
|
|
|
|
|
.loading { |
|
|
animation: pulse 1.5s ease-in-out infinite; |
|
|
} |
|
|
</style> |
|
|
""", unsafe_allow_html=True) |
|
|
|
|
|
|
|
|
st.markdown(""" |
|
|
<div class="title-container"> |
|
|
<h1 class="title-text">🤖 Chatbot NORM - Sistema Inteligente de Consulta</h1> |
|
|
<p class="subtitle-text">Sistema Inteligente de Consulta e Resumo de Documentos</p> |
|
|
</div> |
|
|
""", unsafe_allow_html=True) |
|
|
|
|
|
|
|
|
aba_resumos, aba_chat = st.tabs(["📄 Resumos de Documentos", "💬 Chatbot Interativo"]) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
with aba_resumos: |
|
|
|
|
|
col1, col2 = st.columns([2, 1]) |
|
|
|
|
|
with col1: |
|
|
st.markdown("### 📋 Gerador de Resumos") |
|
|
st.markdown(""" |
|
|
<div class="content-card"> |
|
|
<p style='color: #666; margin-bottom: 1rem;'> |
|
|
Esta ferramenta gera resumos inteligentes a partir dos documentos indexados. |
|
|
O sistema utiliza RAG (Retrieval-Augmented Generation) para buscar os trechos |
|
|
mais relevantes e construir uma resposta contextualizada. |
|
|
</p> |
|
|
</div> |
|
|
""", unsafe_allow_html=True) |
|
|
|
|
|
|
|
|
if 'pergunta_doc' not in st.session_state: |
|
|
st.session_state.pergunta_doc = "Faça um resumo claro sobre o tema principal dos documentos." |
|
|
|
|
|
pergunta_doc = st.text_area( |
|
|
"📝 Digite sua pergunta ou solicite um resumo", |
|
|
value=st.session_state.pergunta_doc, |
|
|
height=100, |
|
|
help="Descreva o tipo de resumo que você precisa ou faça uma pergunta específica" |
|
|
) |
|
|
|
|
|
st.session_state.pergunta_doc = pergunta_doc |
|
|
|
|
|
with col2: |
|
|
st.markdown("### ⚙️ Configurações") |
|
|
top_k_resumo = st.slider( |
|
|
"Número de trechos para análise", |
|
|
min_value=3, |
|
|
max_value=15, |
|
|
value=8, |
|
|
help="Mais trechos = resumo mais completo, mas pode levar mais tempo" |
|
|
) |
|
|
|
|
|
temperatura_resumo = st.slider( |
|
|
"Temperatura do modelo", |
|
|
min_value=0.0, |
|
|
max_value=1.0, |
|
|
value=0.5, |
|
|
step=0.05, |
|
|
help="Valores baixos (0.0–0.3) deixam as respostas mais determinísticas; valores altos (0.7–1.0) geram respostas mais criativas e variadas." |
|
|
) |
|
|
|
|
|
st.markdown(f""" |
|
|
<div style='background: #f0f7ff; padding: 1rem; border-radius: 8px; margin-top: 1rem;'> |
|
|
<p style='margin: 0; color: #1e40af; font-size: 0.9rem;'> |
|
|
<strong>ℹ️ Dica (trechos):</strong> Use valores menores (3–5) para resumos mais diretos |
|
|
e valores maiores (10–15) para análises mais abrangentes. |
|
|
</p> |
|
|
<p style='margin: 0.5rem 0 0 0; color: #1e40af; font-size: 0.9rem;'> |
|
|
<strong>🔥 Dica (temperatura):</strong> Para respostas mais consistentes, mantenha a temperatura entre 0.0 e 0.3. |
|
|
Se quiser explorar diferentes formulações ou respostas mais criativas, aumente para 0.7–1.0. |
|
|
</p> |
|
|
</div> |
|
|
""", unsafe_allow_html=True) |
|
|
|
|
|
st.markdown("<br>", unsafe_allow_html=True) |
|
|
|
|
|
|
|
|
if 'mostrar_documentos' not in st.session_state: |
|
|
st.session_state.mostrar_documentos = False |
|
|
if 'docs_page' not in st.session_state: |
|
|
st.session_state.docs_page = 0 |
|
|
if 'docs_page_size' not in st.session_state: |
|
|
st.session_state.docs_page_size = 20 |
|
|
|
|
|
col_btn1, col_btn2, col_btn3 = st.columns([1, 1, 1]) |
|
|
with col_btn1: |
|
|
gerar_resumo = st.button("🚀 Gerar Resumo", use_container_width=True) |
|
|
with col_btn3: |
|
|
if st.button("📚 Listar Documentos", use_container_width=True): |
|
|
st.session_state.mostrar_documentos = not st.session_state.mostrar_documentos |
|
|
|
|
|
|
|
|
if st.session_state.mostrar_documentos: |
|
|
with st.spinner("🔍 Buscando documentos..."): |
|
|
documentos = obter_lista_documentos() |
|
|
if documentos: |
|
|
total_docs = len(documentos) |
|
|
page_size = st.session_state.docs_page_size |
|
|
total_pages = max((total_docs - 1) // page_size + 1, 1) |
|
|
|
|
|
|
|
|
if st.session_state.docs_page >= total_pages: |
|
|
st.session_state.docs_page = total_pages - 1 |
|
|
if st.session_state.docs_page < 0: |
|
|
st.session_state.docs_page = 0 |
|
|
|
|
|
current_page = st.session_state.docs_page |
|
|
start_idx = current_page * page_size |
|
|
end_idx = min(start_idx + page_size, total_docs) |
|
|
page_docs = documentos[start_idx:end_idx] |
|
|
|
|
|
st.success(f"✅ Total de documentos indexados: **{total_docs}**") |
|
|
st.markdown( |
|
|
f"Mostrando documentos {start_idx + 1}–{end_idx} de {total_docs} " |
|
|
) |
|
|
st.markdown("---") |
|
|
|
|
|
|
|
|
cols_per_row = 2 |
|
|
for i in range(0, len(page_docs), cols_per_row): |
|
|
cols = st.columns(cols_per_row) |
|
|
for j, col in enumerate(cols): |
|
|
idx_local = i + j |
|
|
if idx_local < len(page_docs): |
|
|
with col: |
|
|
global_idx = start_idx + idx_local |
|
|
doc_info = page_docs[idx_local] |
|
|
|
|
|
|
|
|
if isinstance(doc_info, str): |
|
|
doc_id = doc_info |
|
|
doc_title = doc_info |
|
|
else: |
|
|
doc_id = doc_info.get("id") or "" |
|
|
doc_title = doc_info.get("title") or doc_id |
|
|
|
|
|
doc_title_norm = doc_title.replace("_", " ").replace("-", " ") |
|
|
|
|
|
if doc_title_norm.isupper(): |
|
|
doc_title_norm = doc_title_norm.title() |
|
|
|
|
|
display_name = ( |
|
|
doc_title_norm if len(doc_title_norm) <= 60 else doc_title_norm[:57] + "..." |
|
|
) |
|
|
|
|
|
if st.button( |
|
|
f"#{global_idx+1} {display_name}", |
|
|
key=f"doc_btn_{global_idx}", |
|
|
use_container_width=True, |
|
|
): |
|
|
st.session_state.pergunta_doc = ( |
|
|
f"Faça um resumo claro do documento {doc_title_norm}." |
|
|
) |
|
|
|
|
|
|
|
|
col_prev, col_page_info, col_next = st.columns([1, 2, 1]) |
|
|
with col_prev: |
|
|
if st.button("⬅️ Anterior", disabled=current_page == 0): |
|
|
st.session_state.docs_page = max(current_page - 1, 0) |
|
|
st.rerun() |
|
|
with col_page_info: |
|
|
st.markdown( |
|
|
f"<div style='text-align:center; color:#555;'>Página <strong>{current_page+1}</strong> de <strong>{total_pages}</strong></div>", |
|
|
unsafe_allow_html=True, |
|
|
) |
|
|
with col_next: |
|
|
if st.button( |
|
|
"Próxima ➡️", disabled=current_page >= total_pages - 1 |
|
|
): |
|
|
st.session_state.docs_page = min(current_page + 1, total_pages - 1) |
|
|
st.rerun() |
|
|
elif documentos is not None: |
|
|
st.info("ℹ️ Nenhum documento encontrado no sistema.") |
|
|
|
|
|
if gerar_resumo: |
|
|
st.session_state.mostrar_documentos = False |
|
|
if not pergunta_doc.strip(): |
|
|
st.warning("⚠️ Por favor, digite uma pergunta ou solicitação de resumo.") |
|
|
else: |
|
|
with st.spinner("🔍 Analisando documentos e gerando resumo..."): |
|
|
try: |
|
|
resposta, fragmentos = chamar_api(pergunta=pergunta_doc, mode="summary", top_k=top_k_resumo, temperatura=temperatura_resumo) |
|
|
|
|
|
|
|
|
st.markdown("### ✨ Resumo Gerado") |
|
|
st.markdown(f""" |
|
|
<div class="content-card"> |
|
|
<p style='font-size: 1.05rem; line-height: 1.8; color: #333;'> |
|
|
{resposta} |
|
|
</p> |
|
|
</div> |
|
|
""", unsafe_allow_html=True) |
|
|
|
|
|
|
|
|
st.download_button( |
|
|
label="💾 Baixar resumo em .txt", |
|
|
data=resposta, |
|
|
file_name="resumo_chatbot_norm.txt", |
|
|
mime="text/plain", |
|
|
) |
|
|
|
|
|
|
|
|
if fragmentos: |
|
|
docs_unicos = listar_documentos_unicos(fragmentos) |
|
|
|
|
|
|
|
|
titulos_por_doc = {} |
|
|
for m in fragmentos: |
|
|
doc_id = m.get("document_id") |
|
|
if not doc_id: |
|
|
continue |
|
|
titulo = m.get("document_title") or doc_id |
|
|
titulos_por_doc[doc_id] = titulo |
|
|
|
|
|
col_stat1, col_stat2, col_stat3 = st.columns(3) |
|
|
with col_stat1: |
|
|
st.metric("📚 Documentos Consultados", len(docs_unicos)) |
|
|
with col_stat2: |
|
|
st.metric("📄 Trechos Analisados", len(fragmentos)) |
|
|
with col_stat3: |
|
|
st.metric("✅ Status", "Completo") |
|
|
|
|
|
|
|
|
st.markdown("### 📚 Documentos Consultados") |
|
|
for doc_id in docs_unicos: |
|
|
titulo = titulos_por_doc.get(doc_id, doc_id) |
|
|
st.markdown( |
|
|
f"<span class='badge badge-primary'>📄 {titulo}</span>", |
|
|
unsafe_allow_html=True, |
|
|
) |
|
|
|
|
|
|
|
|
st.markdown("### 🔗 Referências Utilizadas") |
|
|
st.info(f"**Citações:** {formatar_referencias(fragmentos)}") |
|
|
|
|
|
|
|
|
else: |
|
|
st.warning("Nenhum trecho foi recuperado pela busca. Tente reformular sua pergunta.") |
|
|
|
|
|
except Exception as e: |
|
|
st.error(f"Erro ao gerar resumo: {e}") |
|
|
st.info("Verifique se a API está rodando e acessível.") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
with aba_chat: |
|
|
col1, col2 = st.columns([2, 1]) |
|
|
|
|
|
with col1: |
|
|
st.markdown("### 💬 Chat com o Assistente") |
|
|
st.markdown(""" |
|
|
<div class="content-card"> |
|
|
<p style='color: #666; margin-bottom: 1rem;'> |
|
|
Faça perguntas sobre os documentos indexados. O chatbot responde |
|
|
utilizando <strong>apenas</strong> o conteúdo da base de dados e fornece |
|
|
referências precisas para cada resposta. |
|
|
</p> |
|
|
</div> |
|
|
""", unsafe_allow_html=True) |
|
|
|
|
|
pergunta = st.text_input( |
|
|
"❓ Digite sua pergunta", |
|
|
placeholder="Ex: Quais são os principais conceitos de química orgânica?", |
|
|
help="Faça perguntas específicas para obter melhores respostas" |
|
|
) |
|
|
|
|
|
with col2: |
|
|
st.markdown("### ⚙️ Configurações") |
|
|
top_k_chat = st.slider( |
|
|
"Número de trechos para consulta", |
|
|
min_value=1, |
|
|
max_value=10, |
|
|
value=4, |
|
|
help="Quantidade de trechos que o chatbot utilizará para responder" |
|
|
) |
|
|
|
|
|
temperatura_chat = st.slider( |
|
|
"Temperatura do modelo", |
|
|
min_value=0.0, |
|
|
max_value=1.0, |
|
|
value=0.5, |
|
|
step=0.05, |
|
|
help="Valores baixos (0.0–0.3) deixam as respostas mais objetivas; valores altos (0.7–1.0) deixam o chatbot mais criativo e variado." |
|
|
) |
|
|
|
|
|
st.markdown(""" |
|
|
<div style='background: #f0fdf4; padding: 1rem; border-radius: 8px; margin-top: 1rem;'> |
|
|
<p style='margin: 0; color: #15803d; font-size: 0.9rem;'> |
|
|
<strong>✨ Sugestão:</strong> Para perguntas objetivas, use 2-4 trechos. |
|
|
Para questões complexas, aumente para 6-10 trechos. |
|
|
</p> |
|
|
</div> |
|
|
""", unsafe_allow_html=True) |
|
|
|
|
|
st.markdown("<br>", unsafe_allow_html=True) |
|
|
|
|
|
col_btn1, col_btn2, col_btn3 = st.columns([1, 2, 1]) |
|
|
with col_btn2: |
|
|
enviar = st.button("📤 Enviar Pergunta", use_container_width=True) |
|
|
|
|
|
if enviar and pergunta.strip(): |
|
|
with st.spinner("Processando sua pergunta... 🤔 "): |
|
|
try: |
|
|
resposta, fragmentos = chamar_api(pergunta=pergunta, mode="chatbot", top_k=top_k_chat, temperatura=temperatura_chat) |
|
|
|
|
|
|
|
|
st.markdown("### 💡 Resposta do Chatbot") |
|
|
st.markdown(f""" |
|
|
<div class="content-card"> |
|
|
<p style='font-size: 1.05rem; line-height: 1.6; color: #333;'> |
|
|
{resposta} |
|
|
</p> |
|
|
</div> |
|
|
""", unsafe_allow_html=True) |
|
|
|
|
|
|
|
|
if fragmentos: |
|
|
docs_unicos = listar_documentos_unicos(fragmentos) |
|
|
|
|
|
|
|
|
titulos_por_doc = {} |
|
|
for m in fragmentos: |
|
|
doc_id = m.get("document_id") |
|
|
if not doc_id: |
|
|
continue |
|
|
titulo = m.get("document_title") or doc_id |
|
|
titulos_por_doc[doc_id] = titulo |
|
|
|
|
|
col_stat1, col_stat2 = st.columns(2) |
|
|
with col_stat1: |
|
|
st.metric("📚 Fontes Consultadas", len(docs_unicos)) |
|
|
with col_stat2: |
|
|
st.metric("📄 Trechos Utilizados", len(fragmentos)) |
|
|
|
|
|
|
|
|
st.markdown("### 📚 Documentos Fonte") |
|
|
for doc_id in docs_unicos: |
|
|
titulo = titulos_por_doc.get(doc_id, doc_id) |
|
|
st.markdown( |
|
|
f"<span class='badge badge-primary'>📄 {titulo}</span>", |
|
|
unsafe_allow_html=True, |
|
|
) |
|
|
|
|
|
|
|
|
st.markdown("### 🔗 Referências Citadas") |
|
|
st.success(f"**Citações completas:** {formatar_referencias(fragmentos)}") |
|
|
|
|
|
|
|
|
else: |
|
|
|
|
|
st.warning("Nenhum trecho relevante encontrado na base de dados.") |
|
|
st.info("Tente reformular sua pergunta ou usar termos diferentes.💡 ") |
|
|
|
|
|
except Exception as e: |
|
|
st.error(f"Erro ao processar pergunta: {e}") |
|
|
st.info("Verifique se a API está rodando corretamente.") |
|
|
|
|
|
elif enviar and not pergunta.strip(): |
|
|
st.warning("Por favor, digite uma pergunta antes de enviar.") |
|
|
|
|
|
|
|
|
st.markdown("<br><br>", unsafe_allow_html=True) |
|
|
st.markdown(""" |
|
|
<div style='text-align: center; color: #888; padding: 2rem; border-top: 1px solid #e0e0e0;'> |
|
|
<p style='margin: 0;'>🤖 <strong>Chatbot NORM</strong> - Sistema Inteligente de Consulta</p> |
|
|
<p style='margin: 0.5rem 0 0 0; font-size: 0.9rem;'> |
|
|
Laboratório de Inteligência Computacional Aplicada ICA da PUC-RIO |
|
|
</p> |
|
|
</div> |
|
|
""", unsafe_allow_html=True) |