#!/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"""
LABDAPS
""" 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"""
LABDAPS

Sinal de Alerta

Assistente de pesquisa baseado no livro do LABDAPS / FSP-USP

""") 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(""" """) with gr.Column(scale=1): gr.Markdown("### Fontes consultadas") 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)