Download app.py from roundb/RAG: direct link, hf CLI and curl.
- Browser
- Download file 11 kB
-
https://huggingface.co/spaces/roundb/RAG/resolve/main/app.py
- Command line
-
hf download hf://spaces/roundb/RAG/app.py
-
curl -L -o app.py https://huggingface.co/spaces/roundb/RAG/resolve/main/app.py
11 kB
| # ---------- app.py ---------- | |
| # Dependências: | |
| # pip install gradio faiss-cpu sentence-transformers openai | |
| import os | |
| from pathlib import Path | |
| import pickle | |
| from typing import List, Dict, Any | |
| import gradio as gr | |
| import faiss | |
| from sentence_transformers import SentenceTransformer | |
| from openai import OpenAI | |
| # ========= NVIDIA API ========= | |
| NV_API_KEY = os.environ.get("NVIDIA_API_KEY") or os.environ.get("NV_API_KEY") | |
| if not NV_API_KEY: | |
| raise RuntimeError( | |
| "A chave da NVIDIA não foi encontrada.\n" | |
| "Defina um secret chamado 'NVIDIA_API_KEY' (ou NV_API_KEY) com a tua chave da NVIDIA.\n" | |
| "• Localmente: export NVIDIA_API_KEY='SUA_CHAVE'\n" | |
| "• Hugging Face Spaces: Settings -> Repository secrets -> Add secret." | |
| ) | |
| client = OpenAI( | |
| base_url="https://integrate.api.nvidia.com/v1", | |
| api_key=NV_API_KEY, | |
| ) | |
| CHAT_MODEL = "meta/llama3-8b-instruct" | |
| # ========= Configuração do App ========= | |
| APP_TITLE = "EcoLexIA – Assistente Inteligente de Leis Ambientais de Portugal" | |
| INTRO = ( | |
| "👋 Bem-vindo ao **EcoLexIA**, o teu assistente jurídico especializado em **direito do ambiente em Portugal**.\n\n" | |
| "Este sistema utiliza **RAG (Retrieval-Augmented Generation)** para consultar automaticamente os documentos legais " | |
| "carregados (leis, decretos, regulamentos, pareceres, etc.) e responder às tuas perguntas com base nesses textos." | |
| ) | |
| SUGGESTION_QUESTIONS = [ | |
| "Resuma os principais princípios da Lei de Bases do Ambiente.", | |
| "Quais são as obrigações do Estado em matéria de proteção ambiental?", | |
| "Explique como funciona a Avaliação de Impacte Ambiental em Portugal.", | |
| "Que legislação regula a gestão de resíduos urbanos?", | |
| "Existe enquadramento legal para participação pública em decisões ambientais?", | |
| "Quais são as regras sobre emissões poluentes na indústria?", | |
| ] | |
| SUGGESTIONS_THEMES = { | |
| "Lei de Bases do Ambiente": [ | |
| "Quais são os princípios fundamentais da Lei de Bases do Ambiente?", | |
| "Como a Lei de Bases do Ambiente enquadra o desenvolvimento sustentável?", | |
| ], | |
| "Avaliação de Impacte Ambiental (AIA)": [ | |
| "Explique o que é Avaliação de Impacte Ambiental e quando é obrigatória.", | |
| "Que entidades estão envolvidas no processo de AIA?", | |
| ], | |
| "Resíduos & Poluição": [ | |
| "Que legislação trata da gestão de resíduos em Portugal?", | |
| "Que obrigações têm as empresas relativamente ao controlo de emissões poluentes?", | |
| ], | |
| "Ordenamento do Território & Conservação": [ | |
| "Como o ordenamento do território se articula com a proteção ambiental?", | |
| "Que diplomas legais regulam áreas protegidas e conservação da natureza?", | |
| ], | |
| } | |
| # ========= Caminhos do índice ========= | |
| INDEX_FILE = "faiss_index.faiss" | |
| EMBEDDINGS_FILE = "embeddings.pkl" | |
| if not Path(INDEX_FILE).exists() or not Path(EMBEDDINGS_FILE).exists(): | |
| raise FileNotFoundError( | |
| "❌ Índice não encontrado.\n" | |
| "Certifique-se de que 'faiss_index.faiss' e 'embeddings.pkl' " | |
| "foram gerados pelo build_index.py na mesma pasta deste app." | |
| ) | |
| index = faiss.read_index(INDEX_FILE) | |
| with open(EMBEDDINGS_FILE, "rb") as f: | |
| emb_data = pickle.load(f) | |
| texts = emb_data["texts"] | |
| metadatas = emb_data["metadatas"] | |
| # Mesmo modelo de embeddings usado no build_index.py | |
| embedding_model = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2") | |
| # ✅ Gradio atual espera "messages format": | |
| # lista de dicts: {"role": "user"/"assistant", "content": "..."} | |
| dialog_history: List[Dict[str, str]] = [] | |
| # ========= Recuperação de contexto ========= | |
| def retrieve_context(query: str, k: int = 4) -> str: | |
| if not query or not query.strip(): | |
| return "" | |
| q_emb = embedding_model.encode([query], convert_to_numpy=True) | |
| _, indices = index.search(q_emb, k) | |
| parts = [] | |
| for idx in indices[0]: | |
| if idx < 0 or idx >= len(texts): | |
| continue | |
| chunk = texts[idx] | |
| meta = metadatas[idx] if idx < len(metadatas) else {} | |
| src = meta.get("source", "documento desconhecido") | |
| parts.append(f"[Documento: {src}]\n{chunk}") | |
| return "\n\n---\n\n".join(parts) | |
| # ========= Streaming da NVIDIA ========= | |
| def nv_stream(messages: List[Dict[str, str]], temperature: float, top_p: float, max_tokens: int): | |
| reply = "" | |
| stream = client.chat.completions.create( | |
| model=CHAT_MODEL, | |
| messages=messages, | |
| temperature=temperature, | |
| top_p=top_p, | |
| max_tokens=max_tokens, | |
| stream=True, | |
| ) | |
| for chunk in stream: | |
| delta = chunk.choices[0].delta | |
| if getattr(delta, "content", None): | |
| reply += delta.content | |
| yield reply | |
| # ========= Lógica do chat ========= | |
| def chatbot(user_input: str, temperature: float, top_p: float, max_tokens: int): | |
| global dialog_history | |
| if not user_input or not user_input.strip(): | |
| return dialog_history, "" | |
| context = retrieve_context(user_input, k=6) | |
| system_msg = { | |
| "role": "system", | |
| "content": ( | |
| "És um assistente jurídico especializado em direito do ambiente em Portugal. " | |
| "Responde SEMPRE em português europeu, de forma clara e estruturada.\n\n" | |
| "Regras:\n" | |
| "1. Usa apenas o contexto abaixo para responder.\n" | |
| "2. Se não houver informação suficiente, diz que não encontras base nos documentos e " | |
| "sugere consultar a legislação oficial.\n" | |
| "3. Indica o nome do documento (PDF) sempre que fizer sentido.\n\n" | |
| f"=== CONTEXTO RECUPERADO ===\n{context}\n\n" | |
| ), | |
| } | |
| # Mensagens que vão para o modelo = system + histórico + user atual | |
| messages: List[Dict[str, str]] = [system_msg] + dialog_history + [{"role": "user", "content": user_input}] | |
| reply_full = "" | |
| try: | |
| for partial in nv_stream(messages, temperature, top_p, max_tokens): | |
| reply_full = partial | |
| # Atualiza histórico no formato messages (compatível com Gradio) | |
| dialog_history = dialog_history + [ | |
| {"role": "user", "content": user_input}, | |
| {"role": "assistant", "content": reply_full}, | |
| ] | |
| except Exception as e: | |
| reply_full = f"⚠️ Erro na API NVIDIA: {type(e).__name__}: {e}" | |
| dialog_history = dialog_history + [ | |
| {"role": "user", "content": user_input}, | |
| {"role": "assistant", "content": reply_full}, | |
| ] | |
| return dialog_history, "" | |
| def clear_history(): | |
| global dialog_history | |
| dialog_history = [] | |
| return [], "" | |
| # ========= CSS ========= | |
| custom_css = r""" | |
| body, .gradio-container { | |
| background: #ffffff; | |
| font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; | |
| } | |
| /* Header azul */ | |
| #header-box { | |
| max-width: 1100px; | |
| margin: 1.5rem auto 1rem auto; | |
| } | |
| .header-card { | |
| background: linear-gradient(135deg, #0b3c91 0%, #1565c0 40%, #1e88e5 100%); | |
| border-radius: 16px; | |
| padding: 1.4rem 1.8rem; | |
| color: #ffffff; | |
| box-shadow: 0 14px 30px rgba(15, 23, 42, 0.18); | |
| } | |
| .header-title { | |
| font-size: 1.6rem; | |
| font-weight: 700; | |
| margin: 0; | |
| color: #ffffff !important; | |
| } | |
| .header-subtitle { | |
| margin: 0.35rem 0 0 0; | |
| font-size: 0.96rem; | |
| opacity: 0.95; | |
| color: #ffffff !important; | |
| } | |
| /* Cartões principais */ | |
| .card { | |
| background: #ffffff; | |
| border-radius: 16px; | |
| border: 1px solid #e0e0e0; | |
| box-shadow: 0 8px 18px rgba(15, 23, 42, 0.06); | |
| padding: 1rem 1.1rem; | |
| } | |
| /* Chat */ | |
| #chat-window { | |
| height: 60vh; | |
| } | |
| /* Botões */ | |
| .gr-button-primary { | |
| background: #1565c0 !important; | |
| color: #ffffff !important; | |
| border: none !important; | |
| } | |
| .gr-button-secondary { | |
| background: #f5f5f5 !important; | |
| color: #333333 !important; | |
| border: 1px solid #e0e0e0 !important; | |
| } | |
| /* Sugestões */ | |
| .suggestion-btn { | |
| width: 100%; | |
| justify-content: flex-start; | |
| font-size: 0.88rem; | |
| } | |
| /* Rodapé */ | |
| .app-footer { | |
| margin-top: 1rem; | |
| font-size: 0.8rem; | |
| text-align: center; | |
| color: #555555; | |
| } | |
| """ | |
| # ========= Layout Gradio ========= | |
| with gr.Blocks(title=APP_TITLE) as demo: | |
| with gr.Group(elem_id="header-box"): | |
| gr.HTML( | |
| f""" | |
| <div class="header-card"> | |
| <div class="header-title">{APP_TITLE}</div> | |
| <div class="header-subtitle"> | |
| Consultor jurídico inteligente com RAG sobre legislação ambiental portuguesa. | |
| </div> | |
| </div> | |
| """ | |
| ) | |
| gr.Markdown(INTRO) | |
| with gr.Row(): | |
| with gr.Column(scale=3): | |
| with gr.Group(elem_classes="card"): | |
| gr.Markdown("### 💬 Conversa Jurídica") | |
| # ✅ Agora o valor/retorno é messages-format (dicts role/content) | |
| chatbot_ui = gr.Chatbot( | |
| elem_id="chat-window", | |
| label="Chatbot", | |
| ) | |
| txt = gr.Textbox( | |
| placeholder="Escreve aqui a tua pergunta sobre leis do ambiente em Portugal…", | |
| lines=3, | |
| show_label=False, | |
| ) | |
| with gr.Row(): | |
| btn_send = gr.Button("Enviar", variant="primary") | |
| btn_clear = gr.Button("Limpar", variant="secondary") | |
| with gr.Accordion("Parâmetros avançados", open=False): | |
| temperature = gr.Slider(0, 1, value=0.5, label="Temperature") | |
| top_p = gr.Slider(0, 1, value=0.9, label="Top-p") | |
| max_tokens = gr.Slider(64, 2048, value=512, step=64, label="Max Tokens") | |
| btn_send.click(chatbot, [txt, temperature, top_p, max_tokens], [chatbot_ui, txt]) | |
| txt.submit(chatbot, [txt, temperature, top_p, max_tokens], [chatbot_ui, txt]) | |
| btn_clear.click(clear_history, [], [chatbot_ui, txt]) | |
| with gr.Column(scale=2): | |
| with gr.Group(elem_classes="card"): | |
| gr.Markdown("### 💡 Sugestões rápidas") | |
| for q in SUGGESTION_QUESTIONS: | |
| gr.Button(q, elem_classes="suggestion-btn").click(lambda s=q: s, outputs=[txt]) | |
| gr.Markdown("---") | |
| gr.Markdown("### 📚 Explorar por tema") | |
| for theme, qs in SUGGESTIONS_THEMES.items(): | |
| with gr.Accordion(theme, open=False): | |
| for q in qs: | |
| gr.Button(q, elem_classes="suggestion-btn").click(lambda s=q: s, outputs=[txt]) | |
| gr.Markdown('<div class="app-footer">EcoLexIA · Sistema RAG para legislação ambiental em Portugal</div>') | |
| if __name__ == "__main__": | |
| demo.queue() # recomendado para streaming/requests no Spaces | |
| demo.launch( | |
| theme=gr.themes.Soft(), | |
| css=custom_css, | |
| ssr_mode=False, # DESLIGA SSR experimental (evita esse erro do asyncio) | |
| ) | |