sinal-de-alerta / app.py
fabianonbfilho's picture
Upload app.py with huggingface_hub
e7505f8 verified
Raw
History Blame Contribute Delete
9.78 kB
#!/usr/bin/env python3
import sys
import base64
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent))
from dotenv import load_dotenv
load_dotenv(Path(__file__).parent / ".env", override=True)
import gradio as gr
from src.labdaps.ingestion.embedder import Embedder
from src.labdaps.chat.session import ChatSession
from src.labdaps.retrieval.vector_store import collection_count
ASSETS_DIR = Path(__file__).parent / "assets"
with open(ASSETS_DIR / "logo.png", "rb") as f:
LOGO_B64 = base64.b64encode(f.read()).decode()
LOGO_SRC = f"data:image/png;base64,{LOGO_B64}"
with open(ASSETS_DIR / "icon.png", "rb") as f:
ICON_B64 = base64.b64encode(f.read()).decode()
ICON_SRC = f"data:image/png;base64,{ICON_B64}"
print("[INFO] Inicializando modelo de embeddings...")
embedder = Embedder()
session = ChatSession(embedder)
_last_sources_md = "As fontes aparecerão aqui após cada resposta."
def format_sources(chunks) -> str:
if not chunks:
return "_Nenhuma fonte consultada._"
seen = set()
lines = []
for c in chunks:
key = (c.source_file, c.page_number)
if key not in seen:
seen.add(key)
lines.append(f"- **Sinal de Alerta** -- p. {c.page_number}")
return "**Fontes consultadas:**\n" + "\n".join(lines)
def respond(message: str, history: list):
global _last_sources_md
if not message.strip():
yield history, _last_sources_md, gr.update()
return
# Gradio 5: history e lista de dicts {"role": "user"|"assistant", "content": str}
new_history = history + [
{"role": "user", "content": message},
{"role": "assistant", "content": ""},
]
yield new_history, _last_sources_md, ""
session.history = [
{"role": msg["role"], "content": msg["content"]}
for msg in history
]
partial = ""
final_chunks = []
for text_delta, chunks in session.ask(message):
partial += text_delta
final_chunks = chunks
new_history[-1]["content"] = partial
yield new_history, format_sources(final_chunks), gr.update()
_last_sources_md = format_sources(final_chunks)
yield new_history, _last_sources_md, gr.update()
def reset():
global _last_sources_md
session.reset()
_last_sources_md = "As fontes aparecerão aqui após cada resposta."
return [], _last_sources_md, ""
# Paleta LABDAPS: azul marinho #1C2D6E, verde-limão #8DC63F, fundo escuro #080E1E
LABDAPS_CSS = f"""
/* Loading splash */
#labdaps-splash {{
position: fixed;
inset: 0;
background: #1C2D6E;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 99999;
transition: opacity 0.5s ease;
}}
#labdaps-splash img {{
width: 260px;
margin-bottom: 28px;
filter: brightness(0) invert(1);
}}
#labdaps-splash .spinner {{
width: 40px;
height: 40px;
border: 4px solid rgba(255,255,255,0.25);
border-top-color: #8DC63F;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}}
@keyframes spin {{ to {{ transform: rotate(360deg); }} }}
/* Global */
body, .gradio-container {{
background: #080E1E !important;
color: #E8EBF5 !important;
font-family: 'Inter', 'Segoe UI', sans-serif !important;
}}
/* Header */
#labdaps-header {{
background: linear-gradient(135deg, #1C2D6E 0%, #0F1A45 100%);
padding: 8px 16px;
border-radius: 10px;
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 4px;
border-bottom: 3px solid #8DC63F;
flex-shrink: 0;
}}
#labdaps-header img {{
height: 34px;
filter: brightness(0) invert(1);
}}
#labdaps-header .tagline {{
color: rgba(255,255,255,0.8);
font-size: 0.88em;
margin: 0;
}}
#labdaps-header .book-title {{
color: #8DC63F;
font-weight: 700;
font-size: 0.95em;
margin: 2px 0 0 0;
}}
/* Status */
#labdaps-status p, #labdaps-status em, #labdaps-status span,
#labdaps-status * {{
color: #a8b4d0 !important;
font-size: 13px !important;
margin: 4px 0 12px 0;
}}
/* Chatbot */
.gradio-chatbot {{
background: #0D1530 !important;
border: 1px solid #2D3F7A !important;
border-radius: 12px !important;
}}
.gradio-chatbot .message.user {{
background: #ffffff !important;
color: #111111 !important;
border-radius: 12px 12px 4px 12px !important;
border: 1px solid #c8d0e8 !important;
}}
.gradio-chatbot .message.bot {{
background: #ffffff !important;
color: #111111 !important;
border-radius: 12px 12px 12px 4px !important;
border: 1px solid #c8d0e8 !important;
}}
.gradio-chatbot .message.user *,
.gradio-chatbot .message.bot * {{
color: #111111 !important;
}}
/* Input */
textarea, input[type=text] {{
background: #ffffff !important;
color: #111111 !important;
border: 1px solid #2D3F7A !important;
border-radius: 8px !important;
}}
textarea:focus, input[type=text]:focus {{
border-color: #8DC63F !important;
box-shadow: 0 0 0 2px rgba(141, 198, 63, 0.25) !important;
}}
/* Buttons */
button.primary {{
background: #1C2D6E !important;
border: none !important;
color: #fff !important;
border-radius: 8px !important;
font-weight: 600 !important;
transition: background 0.2s;
}}
button.primary:hover {{
background: #8DC63F !important;
color: #0D1530 !important;
}}
button.secondary {{
background: transparent !important;
border: 1px solid #2D3F7A !important;
color: #8DC63F !important;
border-radius: 8px !important;
}}
button.secondary:hover {{
background: rgba(141, 198, 63, 0.1) !important;
}}
/* Sources panel */
#sources-panel {{
background: #0D1530;
border: 1px solid #2D3F7A;
border-radius: 12px;
padding: 16px;
}}
#sources-panel * {{
color: #ffffff !important;
}}
#sources-panel p, #sources-panel li,
#sources-panel span, #sources-panel strong,
#sources-panel em {{
color: #ffffff !important;
font-size: 13px !important;
}}
/* Labels */
label span {{
color: #8DC63F !important;
font-weight: 600 !important;
}}
/* Footer */
#labdaps-footer p {{
color: rgba(168, 180, 208, 0.7) !important;
font-size: 12px !important;
text-align: center;
margin-top: 8px;
margin-bottom: 0;
max-width: 65%;
margin-left: auto;
margin-right: auto;
}}
"""
HEAD_HTML = f"""
<link rel="icon" type="image/png" href="{ICON_SRC}">
<div id="labdaps-splash">
<img src="{LOGO_SRC}" alt="LABDAPS">
<div class="spinner"></div>
</div>
<script>
window.addEventListener('load', function() {{
setTimeout(function() {{
var splash = document.getElementById('labdaps-splash');
if (splash) {{
splash.style.opacity = '0';
setTimeout(function() {{ splash.style.display = 'none'; }}, 500);
}}
}}, 900);
setTimeout(function() {{
document.addEventListener('keydown', function(e) {{
if (e.ctrlKey && e.shiftKey && (e.key === 'L' || e.key === 'l')) {{
e.preventDefault();
var btns = document.querySelectorAll('button');
for (var i = 0; i < btns.length; i++) {{
if (btns[i].textContent.trim().startsWith('Nova conversa')) {{
btns[i].click();
break;
}}
}}
}}
}}, true);
}}, 2000);
}});
</script>
"""
collection_count()
n_chunks = collection_count()
status_msg = (
f"Base indexada com {n_chunks} trechos do livro. Respostas baseadas exclusivamente no conteúdo do Sinal de Alerta."
if n_chunks > 0
else "Execute `python ingest.py` para indexar o livro antes de usar o assistente."
)
with gr.Blocks(
title="Assistente LABDAPS - Sinal de Alerta",
css=LABDAPS_CSS,
head=HEAD_HTML,
theme=gr.themes.Base(
primary_hue="blue",
neutral_hue="slate",
),
) as demo:
gr.HTML(f"""
<div id="labdaps-header">
<img src="{LOGO_SRC}" alt="LABDAPS">
<div>
<p class="book-title">Sinal de Alerta</p>
<p class="tagline">Assistente de pesquisa baseado no livro do LABDAPS / FSP-USP</p>
</div>
</div>
""")
gr.Markdown(status_msg, elem_id="labdaps-status")
with gr.Row():
with gr.Column(scale=3):
chatbot = gr.Chatbot(label="Chat", type="messages", elem_classes=["gradio-chatbot"], height=420)
msg_box = gr.Textbox(
placeholder="Pergunte sobre ML em saúde, predição, modelos... (Enter para enviar)",
label="Sua pergunta",
lines=1,
)
with gr.Row():
send_btn = gr.Button("Enviar (Enter)", variant="primary")
clear_btn = gr.Button("Nova conversa (Ctrl+Shift+L)", variant="secondary")
gr.HTML("""
<div id="labdaps-footer">
<p>Este assistente responde <strong style="color:#8DC63F !important;">exclusivamente</strong> com base no livro
"Sinal de Alerta" (LABDAPS/FSP-USP). Para informações não cobertas, consulte os pesquisadores do laboratório.</p>
</div>
""")
with gr.Column(scale=1):
gr.Markdown("### <span style='color:#8DC63F'>Fontes consultadas</span>")
sources_md = gr.Markdown(
value=_last_sources_md,
elem_id="sources-panel",
)
send_btn.click(fn=respond, inputs=[msg_box, chatbot], outputs=[chatbot, sources_md, msg_box], queue=True)
msg_box.submit(fn=respond, inputs=[msg_box, chatbot], outputs=[chatbot, sources_md, msg_box], queue=True)
clear_btn.click(fn=reset, outputs=[chatbot, sources_md, msg_box])
demo.queue()
if __name__ == "__main__":
demo.launch(server_port=7861, share=False, show_error=True, show_api=False)