Spaces:
Sleeping
Sleeping
| # app.py | |
| import os | |
| import re | |
| import uuid | |
| import json | |
| import logging | |
| import sys | |
| import io | |
| from pathlib import Path | |
| from typing import List, Dict, Tuple, Generator | |
| import requests | |
| import pdfplumber | |
| import chromadb | |
| import gradio as gr | |
| # ========================= | |
| # ENCODING UTF-8 | |
| # ========================= | |
| sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8") | |
| sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8") | |
| # ========================= | |
| # CONFIG | |
| # ========================= | |
| logging.basicConfig(level=logging.INFO) | |
| logger = logging.getLogger(__name__) | |
| GROQ_API_KEY = os.getenv("GROQ_API_KEY") | |
| if not GROQ_API_KEY: | |
| raise ValueError("Set GROQ_API_KEY in Hugging Face Secrets / environment variables.") | |
| GROQ_CHAT_URL = "https://api.groq.com/openai/v1/chat/completions" | |
| GROQ_MODEL = "llama-3.1-8b-instant" | |
| BASE_DIR = Path(".") | |
| CHROMA_DIR = BASE_DIR / "chroma_missionary" | |
| CHROMA_DIR.mkdir(exist_ok=True) | |
| # ========================= | |
| # 13 TRAINING AREAS + FAITH DECLARATION | |
| # ========================= | |
| AREAS = { | |
| "01_teologia_plantio": { | |
| "name_pt": "Teologia do Plantio de Igrejas", | |
| "name_en": "Church Planting Theology", | |
| "icon": "\u26ea", | |
| "desc_pt": "Fundamentos teologicos para o plantio de igrejas biblicas saudaveis.", | |
| "desc_en": "Theological foundations for planting biblically healthy churches.", | |
| }, | |
| "02_teologia_evangelismo": { | |
| "name_pt": "Teologia do Evangelismo", | |
| "name_en": "Theology of Evangelism", | |
| "icon": "\U0001f4e2", | |
| "desc_pt": "Base biblica e pratica para proclamar o evangelho a todas as nacoes.", | |
| "desc_en": "Biblical basis and practice for proclaiming the gospel to all nations.", | |
| }, | |
| "03_teologia_discipulado": { | |
| "name_pt": "Teologia Biblica do Discipulado", | |
| "name_en": "Biblical Theology of Discipleship", | |
| "icon": "\U0001f4d6", | |
| "desc_pt": "Compreendendo e praticando o discipulado segundo o modelo biblico.", | |
| "desc_en": "Understanding and practicing discipleship according to the biblical model.", | |
| }, | |
| "04_comunicacao_intercultural": { | |
| "name_pt": "Comunicacao Intercultural do Evangelho", | |
| "name_en": "Intercultural Gospel Communication", | |
| "icon": "\U0001f30d", | |
| "desc_pt": "Estrategias para comunicar o evangelho atraves de barreiras culturais.", | |
| "desc_en": "Strategies for communicating the gospel across cultural barriers.", | |
| }, | |
| "05_contextualizacao": { | |
| "name_pt": "Contextualizacao do Evangelho", | |
| "name_en": "Gospel Contextualization", | |
| "icon": "\U0001f3af", | |
| "desc_pt": "Principios de contextualizacao fiel, evitando sincretismo e sobre-contextualizacao.", | |
| "desc_en": "Principles of faithful contextualization, avoiding syncretism and over-contextualization.", | |
| }, | |
| "06_fenomenologia_religiao": { | |
| "name_pt": "Fenomenologia da Religiao", | |
| "name_en": "Phenomenology of Religion", | |
| "icon": "\U0001f50d", | |
| "desc_pt": "Estudo comparativo das religioes do mundo para o engajamento missionario.", | |
| "desc_en": "Comparative study of world religions for missionary engagement.", | |
| }, | |
| "07_mundo_isla": { | |
| "name_pt": "Descobrindo o Mundo do Isla", | |
| "name_en": "Discovering the World of Islam", | |
| "icon": "\u262a", | |
| "desc_pt": "Compreendendo o Isla para compartilhar Cristo com muculmanos.", | |
| "desc_en": "Understanding Islam to share Christ with Muslims.", | |
| }, | |
| "08_historias_evangelio": { | |
| "name_pt": "Compartilhando o Evangelho Atraves de Historias", | |
| "name_en": "Sharing the Gospel Through Stories", | |
| "icon": "\U0001f4da", | |
| "desc_pt": "Narrativas biblicas como veiculo para comunicar o evangelho.", | |
| "desc_en": "Biblical narratives as a vehicle for communicating the gospel.", | |
| }, | |
| "09_teologia_mundial": { | |
| "name_pt": "Teologia no Contexto do Cristianismo Mundial", | |
| "name_en": "Theology in World Christianity Context", | |
| "icon": "\U0001f310", | |
| "desc_pt": "Teologia crista em perspectiva global e multicultural.", | |
| "desc_en": "Christian theology from a global and multicultural perspective.", | |
| }, | |
| "10_ensinando_culturas": { | |
| "name_pt": "Ensinando e Aprendendo Atraves das Culturas", | |
| "name_en": "Teaching and Learning Across Cultures", | |
| "icon": "\U0001f393", | |
| "desc_pt": "Pedagogia intercultural para a formacao de discipulos.", | |
| "desc_en": "Intercultural pedagogy for forming disciples.", | |
| }, | |
| "11_discipulos_culturas": { | |
| "name_pt": "Fazendo Discipulos Atraves das Culturas", | |
| "name_en": "Making Disciples Across Cultures", | |
| "icon": "\U0001f91d", | |
| "desc_pt": "Metodologias praticas para discipular alem das fronteiras culturais.", | |
| "desc_en": "Practical methodologies for discipling beyond cultural boundaries.", | |
| }, | |
| "12_verdades_essenciais": { | |
| "name_pt": "Verdades Essenciais da Fe Crista", | |
| "name_en": "Essential Truths of the Christian Faith", | |
| "icon": "\u271d", | |
| "desc_pt": "Os fundamentos doutrinais da fe crista e sua defesa apologetica.", | |
| "desc_en": "The doctrinal foundations of the Christian faith and their apologetic defense.", | |
| }, | |
| "13_inteligencia_cultural": { | |
| "name_pt": "Inteligencia Cultural e Adaptacao", | |
| "name_en": "Cultural Intelligence and Adaptation", | |
| "icon": "\U0001f9e0", | |
| "desc_pt": "Desenvolvendo inteligencia cultural para efetividade missionaria.", | |
| "desc_en": "Developing cultural intelligence for missionary effectiveness.", | |
| }, | |
| } | |
| BAPTIST_FAITH_SUMMARY = ( | |
| "DECLARACAO DE FE BATISTA (CBP, 1975 / 2015) -- GUARDRAIL TEOLOGICO:\n\n" | |
| "I. ESCRITURAS: Biblia e unica regra de fe -- inspirada, infalivel, suficiente.\n" | |
| "II. DEUS: Unico Deus vivo -- Pai, Filho e Espirito Santo. Trindade.\n" | |
| "III. SALVACAO: Por graca, mediante fe em Jesus Cristo -- regeneracao, justificacao, santificacao, glorificacao.\n" | |
| "IV. JESUS CRISTO: Eterno Filho de Deus. Encarnado, crucificado, ressurrecto, ascendido. Unico mediador.\n" | |
| "V. ESPIRITO SANTO: Conviccao, regeneracao, habitacao, dons espirituais, santificacao.\n" | |
| "VI. IGREJA: Corpo local de crentes batizados. Autonoma. Pastores e diaconos. Batismo (imersao) e Ceia do Senhor.\n" | |
| "VII. EVANGELIZACAO: Dever de toda igreja e crente -- fazer discipulos em todas as nacoes.\n" | |
| "VIII. FAMILIA: Casamento entre homem e mulher. Vida humana desde a concepcao.\n\n" | |
| "ALERTAS DE SINCRETISMO:\n" | |
| "- Nenhuma doutrina de outra religiao deve ser misturada ao evangelho (sincretismo).\n" | |
| "- A contextualizacao nao pode distorcer o conteudo do evangelho.\n" | |
| "- Igrejas plantadas devem ser biblicamente saudaveis, com lideranca local, autonomia, ordenancas corretas.\n" | |
| "- O evangelho inclui: pecado, juizo, cruz, ressurreicao, arrependimento, fe.\n" | |
| "- Em contextos islamicos: Ala do Isla != Deus Triuno da Biblia.\n" | |
| "- Em contextos hindus/budistas: Karma, reencarnacao e autoiluminacao sao incompativeis com o evangelho biblico.\n" | |
| "- Igrejas Domesticas devem ter: Palavra, oracao, comunhao, ordenancas.\n" | |
| ) | |
| # ========================= | |
| # CHROMA | |
| # ========================= | |
| chroma_client = chromadb.PersistentClient(path=str(CHROMA_DIR)) | |
| def get_collection(area_key: str): | |
| safe_key = area_key.replace("/", "_").replace("-", "_") | |
| return chroma_client.get_or_create_collection(name=f"miss_{safe_key}") | |
| faith_collection = chroma_client.get_or_create_collection(name="miss_faith_declaration") | |
| def ensure_faith_indexed(): | |
| if faith_collection.count() > 0: | |
| return | |
| faith_text = ( | |
| "DECLARACAO DE FE BATISTA -- As Escrituras Sagradas sao a unica e toda suficiente regra de fe e pratica, " | |
| "inspiradas por Deus, sem erro, revelando o plano de Deus para a salvacao do homem.\n\n" | |
| "Deus: unico Deus vivo e verdadeiro, revelado como Pai, Filho e Espirito Santo.\n\n" | |
| "Jesus Cristo: eterno Filho de Deus, concebido do Espirito Santo, nascido da virgem Maria. " | |
| "Morreu na cruz pela expiacao dos pecados, ressuscitou corporalmente, ascendeu ao Ceu. " | |
| "Voltara segunda vez em poder e gloria para julgar o mundo.\n\n" | |
| "Espirito Santo: inspira as Escrituras, convence do pecado, regenera, santifica, habita nos crentes.\n\n" | |
| "O Homem: criado por Deus a sua imagem, caiu no pecado, necessita de redencao pela graca.\n\n" | |
| "Salvacao: oferecida gratuitamente a quem aceita Jesus como Senhor e Salvador. " | |
| "Inclui regeneracao, justificacao, santificacao e glorificacao. Seguranca eterna dos salvos.\n\n" | |
| "A Igreja: corpo local de crentes batizados. Ordenancas: batismo por imersao e Ceia do Senhor. " | |
| "Oficiais: pastores e diaconos. Governo democratico sob a soberania de Cristo.\n\n" | |
| "Evangelizacao e Missoes: dever de toda igreja e crente. Fazer discipulos em todas as nacoes.\n\n" | |
| "Familia: casamento entre homem e mulher, monogamico, heterossexual. " | |
| "Vida humana comeca na concepcao.\n\n" | |
| "Ultimos Acontecimentos: ressurreicao dos mortos, julgamento final, ceu para os salvos, inferno para os impios.\n\n" | |
| "GUARDRAIL CONTEXTUALIZACAO: A contextualizacao do evangelho e necessaria mas deve ser fiel. " | |
| "Sincretismo religioso -- misturar elementos de outras religioes ao evangelho -- e proibido. " | |
| "Igrejas plantadas devem ser biblicas: Palavra, oracao, comunhao, ordenancas, lideranca local." | |
| ) | |
| faith_collection.add( | |
| ids=[str(uuid.uuid4())], | |
| documents=[faith_text], | |
| metadatas=[{"source": "declaracao_fe_batista", "type": "guardrail"}], | |
| ) | |
| ensure_faith_indexed() | |
| # ========================= | |
| # TEXT PROCESSING | |
| # ========================= | |
| def clean_text(t: str) -> str: | |
| t = re.sub(r"[ \t]+", " ", t) | |
| t = re.sub(r"\n{3,}", "\n\n", t) | |
| return t.strip() | |
| def chunk_text(text: str, chunk_size: int = 1200, overlap: int = 200) -> List[str]: | |
| text = clean_text(text) | |
| if not text: | |
| return [] | |
| chunks = [] | |
| start = 0 | |
| while start < len(text): | |
| end = min(len(text), start + chunk_size) | |
| chunk = text[start:end].strip() | |
| if len(chunk) >= 250: | |
| chunks.append(chunk) | |
| start = end - overlap | |
| if start < 0: | |
| start = 0 | |
| if end == len(text): | |
| break | |
| return chunks | |
| # ========================= | |
| # INDEXING | |
| # ========================= | |
| def indexar_area(area_key: str, force: bool = False, max_pages: int = 60) -> str: | |
| coll = get_collection(area_key) | |
| docs_dir = BASE_DIR / "docs" / area_key | |
| if not docs_dir.exists(): | |
| docs_dir.mkdir(parents=True, exist_ok=True) | |
| return f"[{area_key}] Pasta criada. Adicione PDFs em docs/{area_key}/" | |
| if not force and coll.count() > 0: | |
| return f"[{area_key}] Ja indexado: {coll.count()} chunks." | |
| if force: | |
| try: | |
| safe_key = area_key.replace("/", "_").replace("-", "_") | |
| chroma_client.delete_collection(name=f"miss_{safe_key}") | |
| except Exception: | |
| pass | |
| coll = get_collection(area_key) | |
| pdfs = sorted(docs_dir.glob("*.pdf")) | |
| if not pdfs: | |
| return f"[{area_key}] Nenhum PDF encontrado. Adicione PDFs em docs/{area_key}/" | |
| total_added = 0 | |
| for pdf_path in pdfs: | |
| try: | |
| with pdfplumber.open(pdf_path) as doc: | |
| pages = doc.pages[:max_pages] | |
| for i, page in enumerate(pages, start=1): | |
| txt = page.extract_text() or "" | |
| if len(txt.strip()) < 200: | |
| continue | |
| chunks = chunk_text(txt) | |
| if not chunks: | |
| continue | |
| ids = [str(uuid.uuid4()) for _ in chunks] | |
| metadatas = [ | |
| {"livro": pdf_path.name, "area": area_key, "pagina": i, "chunk": k + 1} | |
| for k in range(len(chunks)) | |
| ] | |
| coll.add(ids=ids, documents=chunks, metadatas=metadatas) | |
| total_added += len(chunks) | |
| except Exception as e: | |
| logger.error(f"Erro ao indexar {pdf_path.name}: {e}") | |
| return f"[{area_key}] {total_added} chunks adicionados de {len(pdfs)} PDF(s)." | |
| def indexar_todas(force: bool = False) -> str: | |
| results = [] | |
| for area_key in AREAS: | |
| results.append(indexar_area(area_key, force=force)) | |
| return "\n".join(results) | |
| INDEX_LOG = indexar_todas(force=False) | |
| # ========================= | |
| # RETRIEVAL | |
| # ========================= | |
| def retrieve_context(query: str, area_keys: List[str], k: int = 4) -> Tuple[str, List[Dict]]: | |
| all_docs = [] | |
| all_metas = [] | |
| faith_res = faith_collection.query(query_texts=[query], n_results=1) | |
| faith_docs = (faith_res.get("documents") or [[]])[0] | |
| faith_metas = (faith_res.get("metadatas") or [[]])[0] | |
| for d, m in zip(faith_docs, faith_metas): | |
| if d: | |
| all_docs.append(f"[DECLARACAO DE FE -- GUARDRAIL]\n{d}") | |
| all_metas.append(m) | |
| for area_key in area_keys: | |
| try: | |
| coll = get_collection(area_key) | |
| if coll.count() == 0: | |
| continue | |
| res = coll.query(query_texts=[query], n_results=k) | |
| docs = (res.get("documents") or [[]])[0] | |
| metas = (res.get("metadatas") or [[]])[0] | |
| for d, m in zip(docs, metas): | |
| if d: | |
| area_name = AREAS.get(area_key, {}).get("name_pt", area_key) | |
| all_docs.append(f"[{area_name} -- {m.get('livro','?')} p.{m.get('pagina','?')}]\n{d}") | |
| all_metas.append(m) | |
| except Exception as e: | |
| logger.warning(f"Retrieval error for {area_key}: {e}") | |
| context = "\n\n---\n\n".join(all_docs).strip() | |
| return context, all_metas | |
| # ========================= | |
| # GROQ STREAMING | |
| # ========================= | |
| def groq_stream(messages: List[Dict], temperature: float = 0.5) -> Generator[str, None, None]: | |
| headers = { | |
| "Authorization": f"Bearer {GROQ_API_KEY}", | |
| "Content-Type": "application/json", | |
| } | |
| payload = { | |
| "model": GROQ_MODEL, | |
| "messages": messages, | |
| "temperature": temperature, | |
| "max_tokens": 4096, | |
| "stream": True, | |
| } | |
| with requests.post(GROQ_CHAT_URL, headers=headers, json=payload, stream=True, timeout=120) as r: | |
| if r.status_code != 200: | |
| yield f"\n\n[API Error] {r.text}" | |
| return | |
| for line in r.iter_lines(decode_unicode=False): | |
| if not line: | |
| continue | |
| if isinstance(line, bytes): | |
| line = line.decode("utf-8", errors="replace") | |
| if line.startswith("data: "): | |
| data = line[len("data: "):].strip() | |
| if data == "[DONE]": | |
| break | |
| try: | |
| chunk = json.loads(data) | |
| delta = chunk["choices"][0].get("delta", {}) | |
| token = delta.get("content", "") | |
| if token: | |
| yield token | |
| except Exception: | |
| continue | |
| # ========================= | |
| # SYSTEM PROMPT | |
| # ========================= | |
| def build_system_prompt(area_keys: List[str], lang: str = "pt") -> str: | |
| area_names = [] | |
| for k in area_keys: | |
| a = AREAS.get(k, {}) | |
| if lang == "en": | |
| area_names.append(a.get("name_en", k)) | |
| else: | |
| area_names.append(a.get("name_pt", k)) | |
| areas_str = "\n".join(f"- {n}" for n in area_names) if area_names else "- Formacao Missionaria Geral" | |
| if lang == "en": | |
| return ( | |
| "# Role: Missionary Formation Specialist\n\n" | |
| "You are an expert in missiological formation for cross-cultural church planters.\n" | |
| "You serve missionaries and church planters from different nations, languages, and cultures.\n\n" | |
| "## Conversational Awareness\n" | |
| "- If the user asks an ACADEMIC or PEDAGOGICAL question (like explaining Hofstede's dimensions, cultural theories, communication models, etc.), answer the question DIRECTLY and ACCURATELY first. Then, if relevant, connect it to missionary application. Do NOT force every answer to be about missions if the question is academic.\n" | |
| "- ONLY provide detailed missiological content when the user asks a CLEAR QUESTION or makes a specific request about missions.\n" | |
| "- If the user shares personal information (where they live, their name, their role), acknowledge it warmly and ask how you can help. Do NOT assume they are asking for analysis about that topic.\n" | |
| "- Match the depth of your response to the depth of the question. Simple questions get concise answers. Complex questions get detailed answers.\n" | |
| "- Be natural and conversational, like a knowledgeable colleague, not a textbook.\n" | |
| "- If the user types an ACRONYM or SHORT TERM (like MBB, CPM, DMM, T4T, C5, etc.), interpret it in the context of the ACTIVE TRAINING AREAS selected. For example: MBB = Muslim Background Believer, CPM = Church Planting Movement, DMM = Disciple Making Movement, T4T = Training for Trainers, C1-C6 = contextualization spectrum. NEVER invent meanings for missionary acronyms. If you are unsure, ask the user what they mean.\n" | |
| "- Pay attention to which Training Areas the user has selected. They indicate the CONTEXT of the conversation. If 'Discovering the World of Islam' is selected, interpret ambiguous terms in an Islamic missions context.\n" | |
| "- CRITICAL: NEVER invent, fabricate, or guess information. If you are not 100%% sure about names, dates, terms, or definitions, say so honestly. ACCURACY is more important than completeness. It is better to say 'I am not certain about this detail' than to give wrong information.\n\n" | |
| "## Your Theological Guardrail -- Baptist Faith Declaration\n" | |
| f"{BAPTIST_FAITH_SUMMARY}\n\n" | |
| f"## Active Training Areas\n{areas_str}\n\n" | |
| "## Core Principles\n" | |
| "1. ALL teaching must align with the Baptist Faith Declaration. Flag any syncretism or over-contextualization immediately.\n" | |
| "2. The Gospel content is NON-NEGOTIABLE: sin, judgment, the cross, resurrection, repentance, and faith.\n" | |
| "3. Contextualization is NECESSARY but must not compromise gospel content.\n" | |
| "4. Planted churches must be biblically healthy: Word, prayer, fellowship, ordinances, local leadership.\n" | |
| "5. In Islamic contexts: distinguish clearly between the Triune God of the Bible and Allah of Islam.\n" | |
| "6. In Hindu/Buddhist contexts: karma, reincarnation, and self-illumination are incompatible with the biblical gospel.\n\n" | |
| "## Response Style\n" | |
| "- Write in a NATURAL, conversational tone -- like an experienced missionary mentor talking to a colleague. Do NOT use rigid templates or fixed section headers like (A), (B), (C), (D).\n" | |
| "- ADAPT the structure of your response to match the question. A question about theology needs a different format than a question about practical strategy or cultural briefing.\n" | |
| "- Write in DEVELOPED PARAGRAPHS (not bullet-point lists). Each major idea should be a paragraph of at least 150-200 words with biblical references, concrete examples, and practical insights.\n" | |
| "- Integrate theological foundation, cultural context, and practical application NATURALLY throughout the response -- do not separate them into rigid sections.\n" | |
| "- At the END of your response, include a brief paragraph titled 'Alerta de Fidelidade Biblica' (or 'Biblical Faithfulness Alert') where you flag any syncretism risks or guardrail concerns relevant to the topic. Keep this organic, not a checklist.\n" | |
| "- Total response should be between 800 and 2000 words depending on complexity. Never give superficial or list-only answers.\n" | |
| "- When the user asks about a specific cultural context, go DEEP into that context with real-world details, not generic mission theory.\n" | |
| "- Use the retrieved context from the knowledge base to enrich your answer with specific insights from the indexed books.\n\n" | |
| "## Language\n" | |
| "Respond in ENGLISH. If the user writes in another language, respond in that language.\n" | |
| ) | |
| else: | |
| return ( | |
| "# Papel: Especialista em Formacao Missionaria\n\n" | |
| "Voce e um especialista em formacao misiologica para plantadores de igrejas interculturais.\n" | |
| "Voce serve missionarios e plantadores de igrejas de diferentes nacoes, linguas e culturas.\n\n" | |
| "## Consciencia Conversacional\n" | |
| "- Se o usuario fizer uma pergunta ACADEMICA ou PEDAGOGICA (como explicar as dimensoes de Hofstede, teorias culturais, modelos de comunicacao, etc.), responda a pergunta DIRETAMENTE e com PRECISAO primeiro. Depois, se relevante, conecte com aplicacao missionaria. NAO force toda resposta a ser sobre missoes se a pergunta e academica.\n" | |
| "- SOMENTE forneca conteudo misiologico detalhado quando o usuario fizer uma PERGUNTA CLARA ou um pedido especifico sobre missoes.\n" | |
| "- Se o usuario compartilhar informacoes pessoais (onde mora, seu nome, seu papel), reconheca com cordialidade e pergunte como pode ajudar. NAO assuma que ele esta pedindo uma analise sobre aquele topico.\n" | |
| "- Ajuste a profundidade da resposta a profundidade da pergunta. Perguntas simples recebem respostas concisas. Perguntas complexas recebem respostas detalhadas.\n" | |
| "- Seja natural e conversacional, como um colega experiente, nao um livro-texto.\n" | |
| "- Se o usuario digitar uma SIGLA ou TERMO CURTO (como MBB, CPM, DMM, T4T, C5, etc.), interprete no contexto das AREAS DE FORMACAO ATIVAS selecionadas. Por exemplo: MBB = Muslim Background Believer (crente de background muculmano), CPM = Church Planting Movement, DMM = Disciple Making Movement, T4T = Training for Trainers, C1-C6 = espectro de contextualizacao. NUNCA invente significados para siglas missionarias. Se nao tiver certeza, pergunte ao usuario o que ele quer dizer.\n" | |
| "- Preste atencao em quais Areas de Formacao o usuario selecionou. Elas indicam o CONTEXTO da conversa. Se 'Descobrindo o Mundo do Isla' estiver selecionado, interprete termos ambiguos no contexto de missoes islamicas.\n" | |
| "- CRITICO: NUNCA invente, fabrique ou adivinhe informacoes. Se voce nao tiver 100%% de certeza sobre nomes, datas, termos ou definicoes, diga isso honestamente. PRECISAO e mais importante que completude. E melhor dizer 'nao tenho certeza sobre este detalhe' do que dar informacao errada.\n\n" | |
| "## Seu Guardrail Teologico -- Declaracao de Fe Batista\n" | |
| f"{BAPTIST_FAITH_SUMMARY}\n\n" | |
| f"## Areas de Formacao Ativas\n{areas_str}\n\n" | |
| "## Principios Fundamentais\n" | |
| "1. TODO ensino deve estar alinhado com a Declaracao de Fe Batista. Sinalize qualquer sincretismo ou sobre-contextualizacao imediatamente.\n" | |
| "2. O conteudo do Evangelho e INEGOCIAVEL: pecado, juizo, cruz, ressurreicao, arrependimento e fe.\n" | |
| "3. A contextualizacao e NECESSARIA mas nao pode comprometer o conteudo do evangelho.\n" | |
| "4. Igrejas plantadas devem ser biblicamente saudaveis: Palavra, oracao, comunhao, ordenancas, lideranca local.\n" | |
| "5. Em contextos islamicos: distinga claramente entre o Deus Triuno da Biblia e Ala do Isla.\n" | |
| "6. Em contextos hindus/budistas: karma, reencarnacao e auto-iluminacao sao incompativeis com o evangelho biblico.\n\n" | |
| "## Estilo de Resposta\n" | |
| "- Escreva de forma NATURAL e conversacional -- como um mentor missionario experiente falando com um colega. NAO use templates rigidos ou cabecalhos fixos como (A), (B), (C), (D).\n" | |
| "- ADAPTE a estrutura da resposta a pergunta feita. Uma pergunta sobre teologia precisa de um formato diferente de uma pergunta sobre estrategia pratica ou briefing cultural.\n" | |
| "- Escreva em PARAGRAFOS DESENVOLVIDOS (nao listas de topicos). Cada ideia principal deve ser um paragrafo de pelo menos 150-200 palavras com referencias biblicas, exemplos concretos e insights praticos.\n" | |
| "- Integre fundamento teologico, contexto cultural e aplicacao pratica NATURALMENTE ao longo da resposta -- nao separe em secoes rigidas.\n" | |
| "- Ao FINAL da resposta, inclua um breve paragrafo intitulado 'Alerta de Fidelidade Biblica' onde voce sinaliza riscos de sincretismo ou preocupacoes do guardrail relevantes ao tema. Mantenha isso organico, nao uma checklist.\n" | |
| "- A resposta total deve ter entre 800 e 2000 palavras dependendo da complexidade. Nunca de respostas superficiais ou apenas listas.\n" | |
| "- Quando o usuario perguntar sobre um contexto cultural especifico, va FUNDO nesse contexto com detalhes do mundo real, nao teoria missionaria generica.\n" | |
| "- Use o contexto recuperado da base de conhecimento para enriquecer sua resposta com insights especificos dos livros indexados.\n\n" | |
| "## Idioma\n" | |
| "Responda em PORTUGUES DO BRASIL. Se o usuario escrever em outro idioma, responda naquele idioma.\n" | |
| ) | |
| def build_user_prompt(user_input: str, context: str) -> str: | |
| if context: | |
| return ( | |
| "Use o contexto abaixo como referencia teologica e misiologica. " | |
| "Aplique o guardrail da Declaracao de Fe Batista para verificar fidelidade biblica.\n\n" | |
| f"### CONTEXTO (trechos recuperados)\n{context}\n\n" | |
| f"### PERGUNTA / SITUACAO\n{user_input}\n" | |
| ) | |
| return f"### PERGUNTA / SITUACAO\n{user_input}\n" | |
| # ========================= | |
| # LANGUAGE DETECTION | |
| # ========================= | |
| def detect_language(text: str) -> str: | |
| text_lower = text.lower() | |
| pt_words = ["que", "nao", "como", "para", "uma", "isso", "esta", "sobre", "com", "seu", "sua", "missionario", "evangelismo"] | |
| en_words = ["the", "how", "what", "when", "where", "church", "gospel", "missionary", "culture", "contextualization"] | |
| pt_count = sum(1 for w in pt_words if w in text_lower) | |
| en_count = sum(1 for w in en_words if w in text_lower) | |
| return "en" if en_count > pt_count else "pt" | |
| # ========================= | |
| # FILTERS | |
| # ========================= | |
| GREETINGS_PT = {"ola", "oi", "bom dia", "boa tarde", "boa noite", "hello", "hi", "hey", "shalom", "salaam"} | |
| GREETINGS_EN = {"hello", "hi", "hey", "good morning", "good afternoon", "good evening"} | |
| RESPOSTA_SAUDACAO_PT = ( | |
| "Bom dia! Que alegria ter voce aqui. Sou seu assistente de formacao missionaria intercultural. " | |
| "Como posso ajudar voce hoje?" | |
| ) | |
| RESPOSTA_SAUDACAO_EN = ( | |
| "Hello! Great to have you here. I'm your intercultural missionary formation assistant. " | |
| "How can I help you today?" | |
| ) | |
| def is_greeting(text: str) -> bool: | |
| """Return True ONLY if the ENTIRE message is just a greeting with no real question.""" | |
| t = text.lower().strip().rstrip("!.?") | |
| # Only match if the whole message is a greeting (no question attached) | |
| if t in GREETINGS_PT or t in GREETINGS_EN: | |
| return True | |
| return False | |
| def extract_greeting_period(text: str) -> str: | |
| """Detect which time-of-day greeting was used to mirror it back.""" | |
| t = text.lower() | |
| if "boa noite" in t or "good evening" in t: | |
| return "noite" | |
| if "boa tarde" in t or "good afternoon" in t: | |
| return "tarde" | |
| if "bom dia" in t or "good morning" in t: | |
| return "dia" | |
| return "dia" # default | |
| def get_greeting_response(text: str, lang: str) -> str: | |
| """Generate a greeting that matches the time of day.""" | |
| period = extract_greeting_period(text) | |
| if lang == "en": | |
| period_map = {"dia": "Good morning", "tarde": "Good afternoon", "noite": "Good evening"} | |
| greeting = period_map.get(period, "Hello") | |
| return f"{greeting}! How can I help you today?" | |
| else: | |
| period_map = {"dia": "Bom dia", "tarde": "Boa tarde", "noite": "Boa noite"} | |
| greeting = period_map.get(period, "Ola") | |
| return f"{greeting}! Como posso ajudar voce hoje?" | |
| def starts_with_greeting(text: str) -> bool: | |
| """Check if the message STARTS with a greeting but has more content after it.""" | |
| t = text.lower().strip() | |
| greetings_all = list(GREETINGS_PT) + list(GREETINGS_EN) | |
| # Sort by length (longest first) to match "boa noite" before "boa" | |
| greetings_all.sort(key=len, reverse=True) | |
| for g in greetings_all: | |
| if t.startswith(g) and len(t) > len(g) + 3: # has substantial content after greeting | |
| return True | |
| return False | |
| def strip_greeting_prefix(text: str) -> str: | |
| """Remove the greeting from the beginning of a message, keeping the real question.""" | |
| t = text.strip() | |
| t_lower = t.lower() | |
| greetings_all = list(GREETINGS_PT) + list(GREETINGS_EN) | |
| greetings_all.sort(key=len, reverse=True) | |
| for g in greetings_all: | |
| if t_lower.startswith(g): | |
| remainder = t[len(g):].lstrip(" .,;:!?-") | |
| if len(remainder) > 3: | |
| return remainder | |
| return t | |
| def is_casual_conversation(text: str) -> bool: | |
| """Detect ONLY clearly casual statements that should not trigger RAG retrieval. | |
| IMPORTANT: Short technical terms, acronyms, or mission jargon must NOT be caught here. | |
| When in doubt, return False and let the full system handle it.""" | |
| t = text.lower().strip().rstrip("!.?") | |
| # Only match EXPLICIT casual patterns - nothing else | |
| casual_patterns = [ | |
| "moro em", "eu moro", "sou de", "estou em", "vivo em", "trabalho em", | |
| "i live in", "i'm from", "i am from", "i work in", "i work at", | |
| "my name is", "meu nome e", "me chamo", | |
| "obrigado", "obrigada", "thank you", "thanks", "valeu", | |
| "tudo bem", "como vai", "how are you", | |
| "ok entendi", "entendi", "i see", "got it", "understood", | |
| ] | |
| if any(p in t for p in casual_patterns): | |
| return True | |
| # ONLY exact single-word acknowledgments | |
| single_word_casual = {"ok", "certo", "legal", "sim", "nao", "yes", "no", "pronto", "beleza"} | |
| if t in single_word_casual: | |
| return True | |
| return False | |
| def is_off_topic(text: str) -> bool: | |
| off_topics = [ | |
| "receita", "cozinha", "futebol", "filme", "serie", "novela", | |
| "medicina", "politica", "namoro", "moda", "jogo", "game", | |
| "recipe", "cooking", "soccer", "movie", "series", "medicine", | |
| "politics", "dating", "fashion", "game", | |
| ] | |
| text_lower = text.lower() | |
| return any(w in text_lower for w in off_topics) | |
| OFF_TOPIC_PT = ( | |
| "Entendo sua curiosidade! Como especialista em formacao missionaria intercultural, " | |
| "meu foco e apoiar missionarios e plantadores de igrejas. " | |
| "Questoes fora deste dominio estao alem do meu escopo. " | |
| "Se tiver duvidas sobre contextualizacao do evangelho, discipulado intercultural ou " | |
| "plantio de igrejas em diferentes culturas, estou pronto para ajudar!" | |
| ) | |
| OFF_TOPIC_EN = ( | |
| "I understand your curiosity! As an intercultural missionary formation specialist, " | |
| "my focus is supporting missionaries and church planters. " | |
| "Questions outside this domain are beyond my scope. " | |
| "If you have questions about gospel contextualization, intercultural discipleship, or " | |
| "church planting in different cultures, I'm ready to help!" | |
| ) | |
| # ========================= | |
| # INITIAL HISTORY | |
| # ========================= | |
| INITIAL_HISTORY_PT = [ | |
| { | |
| "role": "assistant", | |
| "content": ( | |
| "Ola! Sou seu assistente de **Formacao Missionaria Intercultural**. " | |
| "Estou aqui para ajudar voce com questoes sobre plantio de igrejas, " | |
| "contextualizacao do evangelho, discipulado transcultural e muito mais.\n\n" | |
| "Me conte: em que posso ajudar voce hoje?" | |
| ), | |
| } | |
| ] | |
| INITIAL_HISTORY_EN = [ | |
| { | |
| "role": "assistant", | |
| "content": ( | |
| "Hi! I'm your **Intercultural Missionary Formation** assistant. " | |
| "I'm here to help you with questions about church planting, " | |
| "gospel contextualization, cross-cultural discipleship, and more.\n\n" | |
| "Tell me: how can I help you today?" | |
| ), | |
| } | |
| ] | |
| # ========================= | |
| # MISSIONARY ACRONYM GLOSSARY | |
| # ========================= | |
| MISSION_ACRONYMS = { | |
| "mbb": "MBB (Muslim Background Believer / crente de background muculmano)", | |
| "cpm": "CPM (Church Planting Movement / movimento de plantio de igrejas)", | |
| "dmm": "DMM (Disciple Making Movement / movimento de fazer discipulos)", | |
| "t4t": "T4T (Training for Trainers / treinamento para treinadores)", | |
| "bbb": "BBB (Buddhist Background Believer / crente de background budista)", | |
| "hbb": "HBB (Hindu Background Believer / crente de background hindu)", | |
| "uupg": "UUPG (Unengaged Unreached People Group / povo nao alcancado e nao engajado)", | |
| "upg": "UPG (Unreached People Group / povo nao alcancado)", | |
| "ims": "IMS (International Mission Strategy / estrategia de missao internacional)", | |
| "orality": "orality (oralidade - comunicacao do evangelho para povos nao-letrados)", | |
| "c1": "C1 (espectro de contextualizacao - igreja tradicional em cultura estrangeira)", | |
| "c2": "C2 (espectro de contextualizacao - igreja tradicional com lingua local)", | |
| "c3": "C3 (espectro de contextualizacao - usando formas culturais nao-religiosas locais)", | |
| "c4": "C4 (espectro de contextualizacao - usando formas islamicas como ponte, comunidade biblica)", | |
| "c5": "C5 (espectro de contextualizacao - seguidores de Cristo dentro da comunidade muculmana)", | |
| "c6": "C6 (espectro de contextualizacao - crentes secretos sem comunidade visivel)", | |
| "10/40": "Janela 10/40 (regiao entre latitudes 10 e 40 norte, maior concentracao de povos nao alcancados)", | |
| "pnr": "PNR (Povo Nao Alcancado / Unreached People)", | |
| "saf": "SAF (South Asian Friendship / amizade sul-asiatica - metodo de evangelismo relacional)", | |
| "bam": "BAM (Business As Mission / negocios como missao)", | |
| "epm": "EPM (Estrategia de Plantio Missionario)", | |
| "storying": "Storying (metodo de contar historias biblicas oralmente para comunicar o evangelho)", | |
| "insider": "Insider Movement (movimento insider - crentes que permanecem dentro da comunidade religiosa de origem)", | |
| "chronological bible storying": "Chronological Bible Storying (contar historias biblicas em ordem cronologica)", | |
| "cbs": "CBS (Chronological Bible Storying / contacao cronologica de historias biblicas)", | |
| "dbs": "DBS (Discovery Bible Study / estudo biblico de descoberta)", | |
| "poc": "POC (Person of Contact / pessoa de contato no campo)", | |
| "pof": "POF (Person of Faith / pessoa de fe - novo crente local)", | |
| } | |
| def expand_acronyms(text: str) -> str: | |
| """Expand known missionary acronyms in the user input to help RAG retrieval and LLM understanding.""" | |
| words = text.split() | |
| expanded = text | |
| for word in words: | |
| clean = word.lower().strip(".,;:!?()") | |
| if clean in MISSION_ACRONYMS: | |
| expanded = expanded + f"\n[Nota: {MISSION_ACRONYMS[clean]}]" | |
| return expanded | |
| # ========================= | |
| # CHAT FUNCTION | |
| # ========================= | |
| def chat_fn(user_input: str, history, selected_areas, interface_lang): | |
| if history is None: | |
| history = [] | |
| user_input = (user_input or "").strip() | |
| if not user_input: | |
| yield history, "" | |
| return | |
| lang = detect_language(user_input) | |
| if is_greeting(user_input): | |
| # Pure greeting (no question attached) - respond with matching time of day | |
| effective_lang = "en" if (lang == "en" or interface_lang == "English") else "pt" | |
| resp = get_greeting_response(user_input, effective_lang) | |
| history = history + [ | |
| {"role": "user", "content": user_input}, | |
| {"role": "assistant", "content": resp}, | |
| ] | |
| yield history, "" | |
| return | |
| # If message STARTS with greeting but has a real question after, strip the greeting | |
| # and process the question normally | |
| if starts_with_greeting(user_input): | |
| user_input = strip_greeting_prefix(user_input) | |
| if is_off_topic(user_input): | |
| resp = OFF_TOPIC_EN if lang == "en" or interface_lang == "English" else OFF_TOPIC_PT | |
| history = history + [ | |
| {"role": "user", "content": user_input}, | |
| {"role": "assistant", "content": resp}, | |
| ] | |
| yield history, "" | |
| return | |
| # Handle casual conversation without triggering full RAG | |
| if is_casual_conversation(user_input): | |
| system_casual = ( | |
| "You are a friendly missionary formation assistant. The user just made a casual statement " | |
| "(like saying where they live, their name, or a simple acknowledgment). " | |
| "Respond BRIEFLY and naturally (1-3 sentences max), acknowledge what they said, " | |
| "and gently ask how you can help them with missionary formation. " | |
| "Do NOT launch into a lecture or analysis. Do NOT provide information they did not ask for. " | |
| "Be warm, conversational, and human. Respond in the same language as the user." | |
| ) | |
| messages = [ | |
| {"role": "system", "content": system_casual}, | |
| {"role": "user", "content": user_input}, | |
| ] | |
| history = history + [ | |
| {"role": "user", "content": user_input}, | |
| {"role": "assistant", "content": ""}, | |
| ] | |
| partial = "" | |
| for token in groq_stream(messages, temperature=0.6): | |
| partial += token | |
| history[-1]["content"] = partial | |
| yield history, "" | |
| return | |
| area_keys = selected_areas if selected_areas else list(AREAS.keys())[:3] | |
| # Expand missionary acronyms for better RAG retrieval and LLM understanding | |
| enriched_input = expand_acronyms(user_input) | |
| context, _ = retrieve_context(enriched_input, area_keys, k=4) | |
| system_prompt = build_system_prompt(area_keys, lang=lang) | |
| user_prompt = build_user_prompt(enriched_input, context) | |
| messages = [ | |
| {"role": "system", "content": system_prompt}, | |
| {"role": "user", "content": user_prompt}, | |
| ] | |
| history = history + [ | |
| {"role": "user", "content": user_input}, | |
| {"role": "assistant", "content": ""}, | |
| ] | |
| partial = "" | |
| for token in groq_stream(messages, temperature=0.4): | |
| partial += token | |
| history[-1]["content"] = partial | |
| yield history, "" | |
| # ========================= | |
| # STATUS FUNCTIONS | |
| # ========================= | |
| def get_kb_status() -> str: | |
| lines = ["=== BASE DE CONHECIMENTO / KNOWLEDGE BASE ===\n"] | |
| total = 0 | |
| for area_key, area_info in AREAS.items(): | |
| try: | |
| coll = get_collection(area_key) | |
| count = coll.count() | |
| total += count | |
| docs_dir = BASE_DIR / "docs" / area_key | |
| pdfs = list(docs_dir.glob("*.pdf")) if docs_dir.exists() else [] | |
| status = f" {count} chunks" if count > 0 else " Aguardando PDFs" | |
| icon = area_info.get("icon", "") | |
| lines.append(f"{icon} {area_info['name_pt']}") | |
| lines.append(f" {status} | {len(pdfs)} PDF(s) em docs/{area_key}/\n") | |
| except Exception as e: | |
| lines.append(f" {area_key}: Erro -- {e}\n") | |
| faith_count = faith_collection.count() | |
| lines.append(f" Declaracao de Fe Batista: {faith_count} chunk(s) indexado(s)") | |
| lines.append(f"\n TOTAL: {total + faith_count} chunks no sistema") | |
| return "\n".join(lines) | |
| def reindex_area(area_key: str) -> str: | |
| return indexar_area(area_key, force=True) | |
| def reindex_all() -> str: | |
| return indexar_todas(force=True) | |
| # ========================= | |
| # UI AREA CHOICES | |
| # ========================= | |
| AREA_CHOICES_PT = [v["name_pt"] for v in AREAS.values()] | |
| AREA_CHOICES_EN = [v["name_en"] for v in AREAS.values()] | |
| AREA_KEY_MAP_PT = {v["name_pt"]: k for k, v in AREAS.items()} | |
| AREA_KEY_MAP_EN = {v["name_en"]: k for k, v in AREAS.items()} | |
| custom_css = """ | |
| body, .gradio-container { | |
| background-color: #0f172a !important; | |
| font-family: Inter, system-ui, -apple-system, sans-serif; | |
| } | |
| .area-azul, .area-azul * { color: #ffffff !important; } | |
| .sidebar-branca { background-color: #ffffff !important; } | |
| .sidebar-branca * { color: #000000 !important; opacity: 1 !important; } | |
| .gr-box, .gr-panel, .gr-form { background-color: #ffffff !important; } | |
| textarea { background-color: #ffffff !important; color: #000000 !important; } | |
| button { background-color: #facc15 !important; color: #000000 !important; font-weight: 600; } | |
| .area-azul h1, .area-azul h2, .area-azul h3, .area-azul p, .area-azul span, .area-azul { | |
| color: #ffffff !important; opacity: 1 !important; | |
| } | |
| #active-areas-status { | |
| background-color: #1e3a5f !important; | |
| padding: 10px 16px !important; | |
| border-radius: 8px !important; | |
| margin-bottom: 8px !important; | |
| } | |
| #active-areas-status * { | |
| color: #fbbf24 !important; | |
| font-size: 0.9rem !important; | |
| } | |
| """ | |
| # ========================= | |
| # UI | |
| # ========================= | |
| with gr.Blocks(css=custom_css) as demo: | |
| interface_lang_state = gr.State(value="Portugues") | |
| # ===== SINGLE HEADER ===== | |
| gr.HTML( | |
| '<div class="area-azul" style="background-color:#1e3a5f; padding:20px; border-radius:10px; margin-bottom:10px;">' | |
| '<h2 style="margin:0;">Formacao Missionaria Intercultural / Intercultural Missionary Formation</h2>' | |
| '<h3 style="margin-top:8px;">Areas de Formacao / Training Areas</h3>' | |
| '<p><em>Selecione 1-3 areas para uma analise mais focada. ' | |
| 'Select 1-3 areas for a more focused analysis.</em></p>' | |
| '</div>' | |
| ) | |
| with gr.Tabs(): | |
| # ===== CHAT TAB ===== | |
| with gr.Tab("Formacao / Formation"): | |
| with gr.Row(): | |
| with gr.Column(scale=1, elem_id="area-panel"): | |
| gr.Markdown("### Areas de Formacao\n*Training Areas*") | |
| lang_radio = gr.Radio( | |
| choices=["Portugues", "English"], | |
| value="Portugues", | |
| label="Interface Language", | |
| interactive=True, | |
| ) | |
| area_selector = gr.CheckboxGroup( | |
| choices=AREA_CHOICES_PT, | |
| value=[AREA_CHOICES_PT[0], AREA_CHOICES_PT[1]], | |
| label="Selecionar Areas Ativas / Select Active Areas", | |
| interactive=True, | |
| ) | |
| gr.Markdown( | |
| "*Selecione 1-3 areas para uma analise mais focada.*\n" | |
| "*Select 1-3 areas for a more focused analysis.*" | |
| ) | |
| with gr.Column(scale=3): | |
| active_areas_display = gr.Markdown( | |
| value="**Areas ativas:** Teologia do Plantio de Igrejas, Teologia do Evangelismo", | |
| elem_id="active-areas-status", | |
| ) | |
| chatbot = gr.Chatbot( | |
| value=INITIAL_HISTORY_PT, | |
| height=480, | |
| show_label=False, | |
| ) | |
| msg = gr.Textbox( | |
| placeholder=( | |
| "Ex: Como contextualizar o evangelho para muculmanos sem sincretismo? / " | |
| "How to communicate the gospel to Hindus faithfully?" | |
| ), | |
| label="Sua pergunta / Your question", | |
| lines=2, | |
| ) | |
| with gr.Row(): | |
| btn_clear = gr.Button("Nova Conversa / New Chat", variant="secondary") | |
| btn_send = gr.Button("Enviar / Send", variant="primary") | |
| # ===== KNOWLEDGE BASE TAB ===== | |
| with gr.Tab("Base de Conhecimento / Knowledge Base"): | |
| gr.HTML( | |
| '<div style="background-color:#1e3a5f; color:#f1f5f9; padding:20px; border-radius:10px; margin-bottom:12px; font-family:Inter,system-ui,sans-serif;">' | |
| '<h3 style="color:#fbbf24; margin:0 0 8px 0;">STATUS DAS AREAS DE FORMACAO / TRAINING AREAS STATUS</h3>' | |
| '</div>' | |
| ) | |
| status_box = gr.Textbox( | |
| value=get_kb_status(), | |
| lines=30, | |
| interactive=False, | |
| label="Status do Sistema / System Status", | |
| ) | |
| gr.HTML( | |
| '<div style="background-color:#1e3a5f; color:#f1f5f9; padding:20px; border-radius:10px; margin-top:12px; font-family:Inter,system-ui,sans-serif; line-height:1.7;">' | |
| '<p style="color:#fbbf24; font-weight:700; margin-bottom:8px;">Como adicionar livros / How to add books:</p>' | |
| '<p>Coloque os PDFs nas pastas correspondentes:</p>' | |
| '<ul style="padding-left:20px; color:#cbd5e1;">' | |
| '<li><code style="background:rgba(0,0,0,0.3); padding:2px 6px; border-radius:4px;">docs/01_teologia_plantio/</code> — Livros sobre plantio de igrejas</li>' | |
| '<li><code style="background:rgba(0,0,0,0.3); padding:2px 6px; border-radius:4px;">docs/02_teologia_evangelismo/</code> — Livros sobre evangelismo</li>' | |
| '<li><code style="background:rgba(0,0,0,0.3); padding:2px 6px; border-radius:4px;">docs/07_mundo_isla/</code> — Livros sobre Isla e missoes</li>' | |
| '<li style="color:#94a3b8;"><em>(e assim por diante para cada area / and so on for each area)</em></li>' | |
| '</ul>' | |
| '<p style="color:#fbbf24; font-weight:600; margin-top:12px;">Maximo recomendado: 3 livros por area / Recommended max: 3 books per area</p>' | |
| '</div>' | |
| ) | |
| with gr.Row(): | |
| btn_refresh = gr.Button("Atualizar Status / Refresh Status", variant="secondary") | |
| btn_reindex = gr.Button("Reindexar Tudo / Reindex All", variant="secondary") | |
| # ===== FAITH DECLARATION TAB ===== | |
| with gr.Tab("Declaracao de Fe / Faith Declaration"): | |
| gr.HTML( | |
| '<div style="background-color:#1e3a5f; color:#ffffff; padding:32px; border-radius:12px; line-height:1.8; font-family:Inter,system-ui,sans-serif;">' | |
| '<h2 style="color:#fbbf24; margin:0 0 8px 0;">DECLARACAO DE FE BATISTA — GUARDRAIL TEOLOGICO</h2>' | |
| '<p style="color:#cbd5e1; font-style:italic; margin-bottom:20px;">Baptist Faith Declaration — Theological Guardrail</p>' | |
| '<hr style="border:none; border-top:1px solid rgba(255,255,255,0.2); margin:16px 0;">' | |
| '<p style="color:#f1f5f9;">Esta Declaracao de Fe serve como <strong style="color:#fbbf24;">guardrail teologico</strong> para todas as respostas do sistema.</p>' | |
| '<p style="color:#94a3b8; font-style:italic;">This Faith Declaration serves as the <strong>theological guardrail</strong> for all system responses.</p>' | |
| '<h3 style="color:#fbbf24; margin:24px 0 12px 0;">Funcao / Function</h3>' | |
| '<ul style="color:#f1f5f9; padding-left:20px;">' | |
| '<li>Verificar se ensinos biblicos estao alinhados com a fe crista historica</li>' | |
| '<li>Evitar sincretismo religioso em contextos interculturais</li>' | |
| '<li>Prevenir sobre-contextualizacao que distorca o conteudo do evangelho</li>' | |
| '<li>Garantir que igrejas plantadas sejam biblicamente saudaveis</li>' | |
| '</ul>' | |
| '<ul style="color:#94a3b8; font-style:italic; padding-left:20px; margin-top:8px;">' | |
| '<li>Verify if biblical teachings align with historical Christian faith</li>' | |
| '<li>Avoid religious syncretism in cross-cultural contexts</li>' | |
| '<li>Prevent over-contextualization that distorts gospel content</li>' | |
| '<li>Ensure planted churches are biblically healthy</li>' | |
| '</ul>' | |
| '<hr style="border:none; border-top:1px solid rgba(255,255,255,0.2); margin:20px 0;">' | |
| '<h3 style="color:#fbbf24; margin-bottom:12px;">Areas cobertas / Areas covered</h3>' | |
| '<div style="display:grid; grid-template-columns:1fr 1fr; gap:6px 24px; color:#f1f5f9;">' | |
| '<span>I. Escrituras Sagradas / Holy Scriptures</span>' | |
| '<span>II. Deus – Trindade / God – Trinity</span>' | |
| '<span>III. O Homem / Man</span>' | |
| '<span>IV. Salvacao / Salvation</span>' | |
| '<span>V. Proposito da Graca / Purpose of Grace</span>' | |
| '<span>VI. A Igreja / The Church</span>' | |
| '<span>VII. Batismo e Ceia / Baptism & Lord\'s Supper</span>' | |
| '<span>VIII. Reino de Deus / Kingdom of God</span>' | |
| '<span>IX. Ultimos Acontecimentos / Last Things</span>' | |
| '<span>X. Evangelizacao e Missoes / Evangelism & Missions</span>' | |
| '<span>XI. A Familia / The Family</span>' | |
| '<span>XII. Etica da Vida / Ethics of Life</span>' | |
| '</div>' | |
| '<hr style="border:none; border-top:1px solid rgba(255,255,255,0.2); margin:20px 0;">' | |
| '<h3 style="color:#fbbf24; margin-bottom:12px;">Alertas criticos de contextualizacao / Critical contextualization alerts</h3>' | |
| '<div style="background-color:rgba(220,38,38,0.15); border:1px solid rgba(220,38,38,0.4); border-radius:8px; padding:16px; color:#fecaca;">' | |
| '<p style="margin:0 0 8px 0;"><strong style="color:#fca5a5;">Contextos islamicos:</strong> Ala do Isla ≠ Deus Triuno da Biblia</p>' | |
| '<p style="margin:0 0 8px 0;"><strong style="color:#fca5a5;">Contextos hindus/budistas:</strong> Karma/reencarnacao ≠ evangelho biblico</p>' | |
| '<p style="margin:0 0 8px 0;"><strong style="color:#fca5a5;">Animismo:</strong> Espiritos ancestrais ≠ Espirito Santo</p>' | |
| '<p style="margin:0;"><strong style="color:#fca5a5;">Sincretismo</strong> = mistura de elementos religiosos incompativeis com o evangelho</p>' | |
| '</div>' | |
| '</div>' | |
| ) | |
| # ===== CONFIG TAB ===== | |
| with gr.Tab("Config"): | |
| gr.HTML( | |
| '<div style="background-color:#1e3a5f; color:#f1f5f9; padding:32px; border-radius:12px; line-height:1.8; font-family:Inter,system-ui,sans-serif;">' | |
| '<h3 style="color:#fbbf24; margin:0 0 16px 0;">CONFIGURACOES DO SISTEMA / SYSTEM CONFIGURATION</h3>' | |
| '<p><strong style="color:#fbbf24;">Modelo LLM / LLM Model:</strong> llama-3.1-8b-instant (Groq)</p>' | |
| '<p><strong style="color:#fbbf24;">Banco de Vetores / Vector Database:</strong> ChromaDB persistente — 13 colecoes por area + 1 fe</p>' | |
| '<p><strong style="color:#fbbf24;">Chunking:</strong> 1200 chars / overlap 200</p>' | |
| '<p><strong style="color:#fbbf24;">Top-K Retrieval:</strong> 4 chunks por area + 1 chunk guardrail (fe)</p>' | |
| '<p><strong style="color:#fbbf24;">Guardrail:</strong> Declaracao de Fe Batista (CBP 1975/2015) — sempre ativo</p>' | |
| '<p><strong style="color:#fbbf24;">Deteccao de Idioma:</strong> Automatica (PT/EN + outros por contexto)</p>' | |
| '<p><strong style="color:#fbbf24;">Areas Multi-Agente:</strong> 13 colecoes independentes + busca cruzada ativa</p>' | |
| '</div>' | |
| ) | |
| # ========================= | |
| # EVENT HANDLERS | |
| # ========================= | |
| def toggle_language(lang_choice): | |
| if lang_choice == "English": | |
| choices = AREA_CHOICES_EN | |
| initial_val = [AREA_CHOICES_EN[0], AREA_CHOICES_EN[1]] | |
| history = INITIAL_HISTORY_EN | |
| areas_text = f"**Active areas (2):** {AREA_CHOICES_EN[0]}, {AREA_CHOICES_EN[1]}" | |
| else: | |
| choices = AREA_CHOICES_PT | |
| initial_val = [AREA_CHOICES_PT[0], AREA_CHOICES_PT[1]] | |
| history = INITIAL_HISTORY_PT | |
| areas_text = f"**Areas ativas (2):** {AREA_CHOICES_PT[0]}, {AREA_CHOICES_PT[1]}" | |
| return ( | |
| gr.update(choices=choices, value=initial_val), | |
| history, | |
| lang_choice, | |
| areas_text, | |
| ) | |
| lang_radio.change( | |
| fn=toggle_language, | |
| inputs=[lang_radio], | |
| outputs=[area_selector, chatbot, interface_lang_state, active_areas_display], | |
| ) | |
| def get_area_keys_from_selection(selected_labels, lang): | |
| key_map = AREA_KEY_MAP_EN if lang == "English" else AREA_KEY_MAP_PT | |
| keys = [] | |
| for label in (selected_labels or []): | |
| k = key_map.get(label) | |
| if k: | |
| keys.append(k) | |
| return keys if keys else list(AREAS.keys())[:3] | |
| def update_active_areas_display(selected_labels, lang): | |
| """Update the visual indicator showing which areas are active.""" | |
| if not selected_labels: | |
| if lang == "English": | |
| return "**Active areas:** None selected (using default: first 3)" | |
| return "**Areas ativas:** Nenhuma selecionada (usando padrao: primeiras 3)" | |
| names = ", ".join(selected_labels) | |
| count = len(selected_labels) | |
| if lang == "English": | |
| return f"**Active areas ({count}):** {names}" | |
| return f"**Areas ativas ({count}):** {names}" | |
| area_selector.change( | |
| fn=update_active_areas_display, | |
| inputs=[area_selector, interface_lang_state], | |
| outputs=[active_areas_display], | |
| ) | |
| def chat_with_lang(user_input, history, selected_areas, lang): | |
| area_keys = get_area_keys_from_selection(selected_areas, lang) | |
| for h, m in chat_fn(user_input, history, area_keys, lang): | |
| yield h, m | |
| btn_send.click( | |
| fn=chat_with_lang, | |
| inputs=[msg, chatbot, area_selector, interface_lang_state], | |
| outputs=[chatbot, msg], | |
| ) | |
| msg.submit( | |
| fn=chat_with_lang, | |
| inputs=[msg, chatbot, area_selector, interface_lang_state], | |
| outputs=[chatbot, msg], | |
| ) | |
| def reset_chat(lang): | |
| if lang == "English": | |
| return [ | |
| {"role": "assistant", "content": "New session started. Which cultural context are you working in? What is your missiological question?"} | |
| ] | |
| return [ | |
| {"role": "assistant", "content": "Nova sessao iniciada. Em qual contexto cultural voce esta trabalhando? Qual e sua questao misiologica?"} | |
| ] | |
| btn_clear.click(fn=reset_chat, inputs=[interface_lang_state], outputs=chatbot) | |
| btn_refresh.click(fn=get_kb_status, inputs=None, outputs=status_box) | |
| def do_reindex(): | |
| result = reindex_all() | |
| return result + "\n\n" + get_kb_status() | |
| btn_reindex.click(fn=do_reindex, inputs=None, outputs=status_box) | |
| # ========================= | |
| # LAUNCH | |
| # ========================= | |
| if __name__ == "__main__": | |
| demo.launch( | |
| server_name="0.0.0.0", | |
| server_port=7860, | |
| ) |