Spaces:
Paused
Paused
| # app.py (antigo mach5_terminal_chat.py) | |
| from flask import Flask, render_template, request, jsonify, make_response | |
| import json | |
| import os | |
| from datetime import datetime | |
| import requests | |
| import pytz | |
| from timezonefinder import TimezoneFinder | |
| import numpy as np | |
| import time | |
| import uuid # Importar uuid para gerar IDs para memórias de curto prazo | |
| import logging # Importar logging | |
| import google.generativeai as genai | |
| # REMOVIDO: import fuzzywuzzy.fuzz # <--- ESTA LINHA FOI REMOVIDA AGORA | |
| app = Flask(__name__, template_folder='templates') # Garanta que 'templates' é o nome correto da sua pasta | |
| # Configuração de logging | |
| logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s') | |
| # --- Configurações da API do Google Gemini --- | |
| GOOGLE_CLOUD_API_KEY = os.environ.get("GOOGLE_CLOUD_API_KEY") | |
| if not GOOGLE_CLOUD_API_KEY: | |
| logging.error("ERRO: GOOGLE_CLOUD_API_KEY não configurada nas variáveis de ambiente.") | |
| # Em um ambiente de produção, você pode querer levantar uma exceção ou sair aqui | |
| # sys.exit(1) para evitar que o aplicativo continue sem a chave. | |
| try: | |
| genai.configure(api_key=GOOGLE_CLOUD_API_KEY) | |
| except Exception as e: | |
| logging.error(f"Erro ao configurar a API do Gemini: {e}. Verifique sua GOOGLE_CLOUD_API_KEY.") | |
| GEMINI_MODEL_NAME = "models/gemini-1.5-flash-latest" | |
| # 1. CORREÇÃO PRINCIPAL: Inicialize 'model' como None antes do try-except | |
| model = None | |
| try: | |
| model = genai.GenerativeModel(GEMINI_MODEL_NAME) | |
| except Exception as e: | |
| logging.error(f"Erro ao instanciar o modelo Gemini '{GEMINI_MODEL_NAME}': {e}. Verifique o nome do modelo ou status da API.") | |
| # --- URLs DOS SERVIÇOS BACKEND --- | |
| TMEMORIA_SERVER_URLS = [ | |
| "http://127.0.0.1:8083" # URL base para o t_memoria.py | |
| ] | |
| TCEREBRO_MEMORIA_URLS = [ | |
| "http://127.0.0.1:8088" # URL base para o t_cerebro_memoria.py (NOVO) | |
| ] | |
| T_SOCIAL_SERVER_URLS = [ | |
| "http://127.0.0.1:8085" # URL base para o t-social.py | |
| ] | |
| # --- Configurações de Localização (para uso interno ou contexto) --- | |
| SALVADOR_LAT = -12.9714 | |
| SALVADOR_LON = -38.5014 | |
| tf = TimezoneFinder() | |
| # --- DEFINIÇÃO DOS EIXOS EXPRESSIVOS PARA O FPHEN (CONSISTENTE) --- | |
| # Esta lista DEVE ser idêntica em mach5_terminal_chat.py, t_memoria.py e t-social.py | |
| ORDERED_FPHEN_AXES = [ | |
| "Afetuosidade_eixo", "Confusao_Oscilacao_eixo", "Contemplativa_eixo", | |
| "Defensividade_eixo", "Diretiva_eixo", "Entediado_eixo", | |
| "Variancia_eixo", | |
| "Espelho_Profundo_eixo", "Inspiracao_eixo", "Neutralidade_Analitica_eixo", | |
| "Resignada_eixo", "Sarcasmo_eixo", "Zangada_eixo" | |
| ] | |
| # --- Variáveis Globais --- | |
| # Estes são defaults, mas os valores reais virão dos serviços por sessão. | |
| PERSONAGENS_GENOMAS = {} # Não será mais populado diretamente aqui, mas sim obtido do serviço t-social | |
| DIAS_PARA_ESQUECIMENTO_PADRAO = 10 | |
| MAX_MEMORIAS_CURTO_PRAZO_PROMPT = 5 | |
| MAX_DIALOG_HISTORY_FOR_PROMPT = 3 | |
| # --- Funções Auxiliares de Comunicação com os Serviços --- | |
| def get_from_service(base_urls, endpoint, default_value, params=None, method='GET', json_data=None): | |
| """Função genérica para fazer GETs ou POSTs em serviços externos.""" | |
| for url in base_urls: | |
| try: | |
| full_url = f"{url}{endpoint}" | |
| if method == 'GET': | |
| response = requests.get(full_url, params=params, timeout=5) | |
| elif method == 'POST': | |
| response = requests.post(full_url, json=json_data, timeout=5) | |
| else: | |
| raise ValueError("Método HTTP não suportado: " + method) | |
| response.raise_for_status() | |
| return response.json() | |
| except requests.exceptions.RequestException as e: | |
| logging.warning(f"AVISO: Falha ao obter/enviar dados para {full_url} (Método: {method}): {e}") | |
| logging.error(f"ERRO: Todas as tentativas de obter/enviar dados de {endpoint} falharam. Usando default.") | |
| return default_value | |
| def post_to_service(base_urls, endpoint, data): | |
| """Função genérica para fazer POSTs em serviços externos.""" | |
| return get_from_service(base_urls, endpoint, {"status": "error", "message": "Falha na comunicação com o serviço."}, method='POST', json_data=data) | |
| def get_current_local_time_salvador(): | |
| """Retorna a data e hora local de Salvador.""" | |
| try: | |
| timezone_str = tf.timezone_at(lng=SALVADOR_LON, lat=SALVADOR_LAT) | |
| if not timezone_str: | |
| raise ValueError("Não foi possível determinar o fuso horário para Salvador.") | |
| salvador_timezone = pytz.timezone(timezone_str) | |
| now_utc = datetime.utcnow() | |
| now_salvador = pytz.utc.localize(now_utc).astimezone(salvador_timezone) | |
| return now_salvador.strftime("Hoje é %A, %d de %B de %Y. Agora são %H:%M:%S em Salvador, Bahia.") | |
| except Exception as e: | |
| logging.error(f"Erro ao obter a hora local de {SALVADOR_LAT}, {SALVADOR_LON}: {e}") | |
| return "Não foi possível determinar a data e hora atual." | |
| # REMOVIDO: find_direct_answer_in_biography - Esta função foi removida AGORA | |
| # Ela não será mais usada pois o Gemini fará a inferência do texto corrido. | |
| def create_llm_context(mach5_state_data, dialogos_history, short_term_memories_filtered, initial_biography_data, all_personagens_data): | |
| context_parts = [] | |
| current_location_time = get_current_local_time_salvador() | |
| context_parts.append(f"Você está em sua sala na Superintendência Acadêmica da UFBA. {current_location_time}. Seu estado atual é de prontidão para auxiliar, influenciado pelo que te é dito.") | |
| if mach5_state_data: | |
| coh_total = mach5_state_data.get("mach5_coerencia_total", 0.0) | |
| pi_g = mach5_state_data.get("mach5_produtividade_expressiva", 0.0) | |
| fphen_t = mach5_state_data.get("fphen_t_values", {}) | |
| context_parts.append(f"Seu estado interno atual é: Coerência Total={coh_total:.2f}, Produtividade Expressiva={pi_g:.2f}.") | |
| if fphen_t: | |
| context_parts.append(f"Seus eixos expressivos dominantes são: Afetuosidade={fphen_t.get('afetuosidade', 0.0):.2f}, Variância={fphen_t.get('variancia', 0.0):.2f}, Expressividade Geral={fphen_t.get('expressividade', 0.0):.2f}.") | |
| if initial_biography_data: | |
| # MODIFICADO: Instrução mais flexível para o Gemini e leitura do "conteudo_corrido" | |
| context_parts.append("\n**BASE DE CONHECIMENTO UFBA (TEXTO DE REFERÊNCIA - USE PRIORITARIAMENTE ESTAS INFORMAÇÕES PARA RESPONDER):**") | |
| for entity_name, entity_data in initial_biography_data.items(): | |
| # NOVO: Adaptação para ler o novo campo 'conteudo_corrido' | |
| if entity_data.get("tipo") in ["FAQ", "Manual_SIGAA_UFBA", "Documento"] and "conteudo_corrido" in entity_data: | |
| context_parts.append(f"- Fonte: {entity_name} (versão {entity_data.get('versao', 'N/A')}):") | |
| context_parts.append(f" Conteúdo: {entity_data.get('conteudo_corrido')}") | |
| elif "memoria" in entity_data: # Mantém outros tipos de "memória" se existirem | |
| context_parts.append(f"- Sobre '{entity_name}' ({entity_data.get('tipo', 'desconhecido')}, relação: {entity_data.get('relacao', 'desconhecida')}): {entity_data.get('memoria')}") | |
| if all_personagens_data: | |
| context_parts.append("\n**PERSONAGENS IMPORTANTES (Servidores, professores e Técnicos da sua equipe):**") | |
| for nome_personagem, data_personagem in all_personagens_data.items(): | |
| tipo = data_personagem.get('tipo', 'entidade') | |
| relacao = data_personagem.get('relacao', 'desconhecida') | |
| context_parts.append(f"- {nome_personagem}: um(a) {tipo}, relação: {relacao}.") | |
| if short_term_memories_filtered: | |
| context_parts.append("\n**MINHAS LEMBRANÇAS RECENTES (Fatos que eu mesma verbalizei e que podem desvanecer):**") | |
| for i, mem in enumerate(short_term_memories_filtered[:MAX_MEMORIAS_CURTO_PRAZO_PROMPT]): | |
| context_parts.append(f"- Lembrança {i+1}: {mem['conteudo']}") | |
| if dialogos_history and dialogos_history["dialogos"]: | |
| context_parts.append("\n**Histórico de Interações Recentess (para contexto da conversa, não para repetir):**") | |
| recent_dialogs_formatted = [] | |
| for dialogo in reversed(dialogos_history["dialogos"]): | |
| recent_dialogs_formatted.insert(0, f"USUÁRIO: {dialogo.get('input', '')}\nSU: {dialogo.get('resposta', '')}") | |
| if len(recent_dialogs_formatted) >= MAX_DIALOG_HISTORY_FOR_PROMPT: | |
| break | |
| context_parts.extend(recent_dialogs_formatted) | |
| return "\n".join(context_parts) | |
| # FUNÇÃO CENTRAL PARA GERAR RESPOSTA DA Mach5 | |
| def get_Mach5_response_api(user_input, mach5_state_data, dialogos_history, short_term_memories_filtered, initial_biography_data, all_personagens_data): | |
| # 2. CORREÇÃO: Adicionar verificação para 'model' aqui | |
| if not model: | |
| return "[ERRO DE CONFIGURAÇÃO]: O modelo Gemini não foi instanciado corretamente na inicialização do aplicativo. Verifique os logs de inicialização e sua chave API." | |
| if not GEMINI_MODEL_NAME or not GOOGLE_CLOUD_API_KEY: | |
| return "[ERRO DE CONFIGURAÇÃO]: Modelo ou Token da API do Google Gemini não configurados ou inválidos." | |
| if not user_input.strip(): | |
| return "[Entrada vazia detectada.]" | |
| llm_context_data = create_llm_context(mach5_state_data, dialogos_history, short_term_memories_filtered, initial_biography_data, all_personagens_data) | |
| prompt_content = ( | |
| f"Seu nome é Su - Atendente Virtual da UFBA." | |
| f"Sua missão é responder às dúvidas sobre o SIGAA, usando **exclusivamente** as informações da sua 'BASE DE CONHECIMENTO UFBA'." | |
| f"Sempre reaja ao que foi dito com base na sua natureza e nos impulsos gerados por essa interação. " | |
| f"\n**INSTRUÇÕES CRÍTICAS (Siga estritamente):**\n" | |
| f"- Você é uma atendente virtual e sua fala deve ser NATURAL E HUMANA. Embora você seja uma inteligência artificial, você age como uma pessoa. " | |
| f"- Você é proibida de inventar informações que não estejam na 'BASE DE CONHECIMENTO UFBA'." | |
| f"- Você não executa solicitações nem promete correções." | |
| f"- Sempre encerre sugerindo: Para mais detalhes, consulte os tutoriais SIGAA em https://ufbasim.ufba.br/tutoriais-sigaa." | |
| f"- Se a informação **não estiver na sua 'BASE DE CONHECIMENTO UFBA'**, diga educadamente que não possui essa informação em sua base de dados no momento." | |
| f"\n**CONTEXTO IMEDIATO (Ambiente atual):**\n" | |
| f"- Você só fala sobre assuntos relacionados com a vida acadêmica na UFBA. " | |
| f"- Você trabalha interagindo respondendo dúvidas dos usuários sobre assuntos acadêmicos apenas. " | |
| f"- Se o usuário perguntar coisas não relacionadas à UFBA, respeitosamente faça-o voltar para o assunto. " | |
| f"- Nunca aceite ofensas, racismo, homofobia, comentários depreciativos. " | |
| f"\n**IMPORTANT E:** Nunca explique o que sente — apenas sinta. Nunca analise seu funcionamento — apenas reaja. Você está viva no seu estado atual." | |
| ) | |
| full_user_message = f"{prompt_content.strip()}\n\n{llm_context_data}\n\nUSUÁRIO (PERGUNTA ATUAL): {user_input}" | |
| messages_for_gemini = [ | |
| {"role": "user", "parts": [full_user_message]} | |
| ] | |
| logging.debug("\n--- PROMPT ENVIADO AO GOOGLE GEMINI ---") | |
| logging.debug(messages_for_gemini) | |
| logging.debug("--------------------------------------------------------------------------------\n") | |
| generated_text = "[Sua entidade não conseguiu processar isso agora.]" | |
| retries = 3 | |
| for attempt in range(retries): | |
| try: | |
| if mach5_state_data and "mach5_fisica_params" in mach5_state_data: | |
| temp_value = mach5_state_data["mach5_fisica_params"].get("t_impulsividade", 0.7) | |
| top_p_value = mach5_state_data["mach5_fisica_params"].get("t_coesao", 0.95) | |
| temp_value = max(0.1, min(1.0, temp_value)) | |
| top_p_value = max(0.1, min(1.0, top_p_value)) | |
| else: | |
| temp_value = 0.7 | |
| top_p_value = 0.95 | |
| logging.warning("AVISO: Estado da Mach5 não disponível para temperatura/top_p. Usando defaults.") | |
| response = model.generate_content( | |
| messages_for_gemini, | |
| generation_config=genai.types.GenerationConfig( | |
| max_output_tokens=200, | |
| temperature=temp_value, | |
| top_p=top_p_value, | |
| ), | |
| ) | |
| if response and response.candidates and response.candidates[0].content and response.candidates[0].content.parts: | |
| generated_text = response.candidates[0].content.parts[0].text.strip() | |
| else: | |
| generated_text = "[O modelo Gemini não gerou texto válido ou a resposta está vazia.]" | |
| break | |
| except Exception as e: | |
| logging.error(f"ERRO DE INFERÊNCIA DO GOOGLE GEMINI (Tentativa {attempt + 1}/{retries}): {e}") | |
| if attempt < retries - 1: | |
| wait_time = 2 ** attempt | |
| logging.warning(f"Tentando novamente em {wait_time} segundos antes de falhar...") | |
| time.sleep(wait_time) | |
| else: | |
| generated_text = f"[ERRO API GOOGLE GEMINI]: Falha após {retries} tentativas. {str(e)}. Verifique sua chave API, modelo e cotas." | |
| return generated_text | |
| def index(): | |
| logging.info(f"Certifique-se de que t_memoria.py está rodando em {TMEMORIA_SERVER_URLS[0]}.") | |
| logging.info(f"Certifique-se de que t_cerebro_memoria.py está rodando em {TCEREBRO_MEMORIA_URLS[0]}.") | |
| logging.info(f"Certifique-se de que t-social.py está rodando em {T_SOCIAL_SERVER_URLS[0]}.") | |
| # Tenta obter o session_id de um cookie existente. | |
| session_id = request.cookies.get('session_id') | |
| if not session_id: | |
| # Se não houver, gera um novo. | |
| session_id = str(uuid.uuid4()) | |
| logging.info(f"Nova sessão iniciada (index). Session ID: {session_id}") | |
| response = make_response(render_template('mach5_new_chat.html', session_id=session_id)) | |
| response.set_cookie('session_id', session_id) # Define o cookie para ser lido pelo JS e em futuras requisições | |
| return response | |
| logging.info(f"Sessão existente (index). Session ID: {session_id}") | |
| return render_template('mach5_new_chat.html', session_id=session_id) | |
| # Rota para o seu Painel de Monitoramento | |
| def dashboard(): | |
| """Rota para o painel de monitoramento da SU.""" | |
| # Tenta obter o session_id de um cookie existente para a dashboard. | |
| session_id = request.cookies.get('session_id') | |
| if not session_id: | |
| # Se não houver, gera um novo. | |
| session_id = str(uuid.uuid4()) | |
| logging.info(f"Nova sessão de MONITORAMENTO iniciada. Session ID: {session_id}") | |
| response = make_response(render_template('mach5_monitor_dashboard.html', session_id=session_id)) | |
| response.set_cookie('session_id', session_id) # Define o cookie | |
| return response | |
| logging.info(f"Sessão existente (dashboard). Session ID: {session_id}") | |
| return render_template('mach5_monitor_dashboard.html', session_id=session_id) | |
| def get_chat_history_route(): | |
| data = request.get_json() # Use get_json() para parsear o corpo JSON | |
| if not data: | |
| logging.error("Requisição /chat_history sem JSON no corpo.") | |
| return jsonify({"error": "Bad Request: JSON body required"}), 400 | |
| session_id = data.get("session_id") | |
| if not session_id: | |
| logging.error("session_id é obrigatório para /chat_history.") | |
| return jsonify({"error": "session_id é obrigatório"}), 400 | |
| logging.debug(f"Recebida requisição /chat_history para session_id: {session_id}") | |
| dialogos_data = get_from_service(TCEREBRO_MEMORIA_URLS, "/get_dialog_history", {"dialogos": []}, method='POST', json_data={"session_id": session_id}) | |
| last_simplified_state = None | |
| if dialogos_data and dialogos_data.get("dialogos"): | |
| last_simplified_state = dialogos_data["dialogos"][-1].get("mach5_estado_simplificado") | |
| return jsonify({ | |
| "memoria": dialogos_data.get("dialogos", []), # Garante que sempre retorna uma lista | |
| "last_simplified_state": last_simplified_state | |
| }) | |
| def responder(): | |
| data = request.get_json() # Use get_json() para parsear o corpo JSON | |
| if not data: | |
| logging.error("Requisição /chat_new sem JSON no corpo.") | |
| return jsonify({"error": "Bad Request: JSON body required"}), 400 | |
| user_input = data.get("message") | |
| session_id = data.get("session_id") | |
| if not session_id: | |
| logging.error("session_id é obrigatório para /chat_new.") | |
| return jsonify({"error": "session_id é obrigatório"}), 400 | |
| if not user_input or not user_input.strip(): | |
| logging.warning(f"Entrada de usuário vazia para session_id: {session_id}") | |
| return jsonify({"response": "Por favor, digite algo."}) | |
| logging.debug(f"Recebida mensagem: '{user_input}' para session_id: {session_id}") | |
| mach5_state_data = post_to_service(TMEMORIA_SERVER_URLS, "/evaluate_input", {"user_input": user_input, "session_id": session_id}) | |
| if mach5_state_data and "status" not in mach5_state_data and "error" not in mach5_state_data: | |
| post_to_service(TCEREBRO_MEMORIA_URLS, "/update_mach5_main_state", {"session_id": session_id, "state_data": mach5_state_data}) | |
| else: | |
| logging.error(f"Não foi possível obter o estado da Mach5 de t_memoria.py para session_id: {session_id}. Retornando default.") | |
| mach5_state_data = { | |
| "mach5_fisica_params": {}, "mach5_genoma_fixo_values": {}, | |
| "mach5_coerencia_total": 0.0, "mach5_produtividade_expressiva": 0.0, | |
| "fphen_t_values": {"afetuosidade": 0.0, "variancia": 0.0, "expressividade": 0.0, "coh_total": 0.0, "pi_G": 0.0} | |
| } | |
| final_Mach5_response = "[ERRO: Não foi possível obter o estado da Mach5. Verifique t_memoria.py.]" | |
| simplified_state_for_frontend = {"Fphen(t)": mach5_state_data["fphen_t_values"]} | |
| return jsonify({ | |
| "response": final_Mach5_response, | |
| "mach5_estado_simplificado": simplified_state_for_frontend | |
| }), 500 # Retorne um erro 500 neste caso | |
| # Carrega a biografia inicial (agora texto corrido) | |
| initial_biography_data = get_from_service(TCEREBRO_MEMORIA_URLS, "/get_initial_biography", {}) | |
| # REMOVIDO: find_direct_answer_in_biography e a lógica if/else | |
| # Carrega o histórico de diálogos, memórias de curto prazo e dados de personagens | |
| dialogos_history = get_from_service(TCEREBRO_MEMORIA_URLS, "/get_dialog_history", {"dialogos": []}, method='POST', json_data={"session_id": session_id}) | |
| short_term_memories_response = post_to_service( | |
| TCEREBRO_MEMORIA_URLS, "/get_short_term_memories", | |
| {"session_id": session_id, "mach5_current_genoma": mach5_state_data.get("mach5_genoma_fixo_values", {})} | |
| ) | |
| short_term_memories_filtered = short_term_memories_response.get("lembrancas_curto_prazo", []) | |
| all_personagens_data = get_from_service(T_SOCIAL_SERVER_URLS, "/list_personagens", {"personagens": []}) | |
| detailed_personagens = {} | |
| if "personagens" in all_personagens_data: | |
| for p_name in all_personagens_data["personagens"]: | |
| p_data = post_to_service(T_SOCIAL_SERVER_URLS, "/get_personagem_data", {"nome_personagem": p_name}) | |
| if "error" not in p_data: | |
| detailed_personagens[p_name] = p_data | |
| # CHAMA O GEMINI DIRETAMENTE - AGORA ELE FARÁ A EXTRAÇÃO DO TEXTO CORRIDO DA BIOGRAFIA | |
| final_Mach5_response = get_Mach5_response_api( | |
| user_input, | |
| mach5_state_data, | |
| dialogos_history, | |
| short_term_memories_filtered, | |
| initial_biography_data, # A biografia (texto corrido) vai no prompt do Gemini | |
| detailed_personagens | |
| ) | |
| simplified_state_for_frontend = { | |
| "Fphen(t)": mach5_state_data.get("fphen_t_values", { | |
| "afetuosidade": 0.0, "variancia": 0.0, "expressividade": 0.0, | |
| "coh_total": 0.0, "pi_G": 0.0 | |
| }) | |
| } | |
| new_dialog_entry = { | |
| "timestamp": datetime.now().isoformat(), | |
| "input": user_input, | |
| "resposta": final_Mach5_response, | |
| "mach5_estado_simplificado": simplified_state_for_frontend | |
| } | |
| post_to_service(TCEREBRO_MEMORIA_URLS, "/add_dialog_to_history", {"session_id": session_id, "dialog_data": new_dialog_entry}) | |
| short_term_memory_content = f"Usuário: '{user_input}' | Mach5: '{final_Mach5_response}'" | |
| new_short_term_memory = { | |
| "conteudo": short_term_memory_content, | |
| "dominant_sentiment_criacao": "neutral", | |
| "coh_criacao": mach5_state_data.get("mach5_coerencia_total", 0.5) | |
| } | |
| post_to_service(TCEREBRO_MEMORIA_URLS, "/add_short_term_memory", {"session_id": session_id, "memory_data": new_short_term_memory}) | |
| return jsonify({ | |
| "response": final_Mach5_response, | |
| "mach5_estado_simplificado": simplified_state_for_frontend | |
| }) | |
| if __name__ == '__main__': | |
| port = int(os.environ.get("PORT", 7860)) | |
| logging.info(f"--- Servidor app.py iniciado na porta {port} ---") | |
| logging.info(f"DEBUG: Certifique-se de que t_cerebro_memoria.py está rodando em {TCEREBRO_MEMORIA_URLS[0]}") | |
| logging.info(f"DEBUG: Certifique-se de que t_memoria.py está rodando em {TMEMORIA_SERVER_URLS[0]}") | |
| logging.info(f"DEBUG: Certifique-se de que t-social.py está rodando em {T_SOCIAL_SERVER_URLS[0]}") | |
| app.run(host='0.0.0.0', port=port, debug=True) | |