Su.9 / app.py
NiltonSouza's picture
Upload 12 files
f47e986 verified
# 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
@app.route('/')
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
@app.route('/dashboard')
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)
@app.route('/chat_history', methods=['POST'])
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
})
@app.route('/chat_new', methods=['POST'])
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)