oraculo / app.py
victorafarias's picture
Versão Estável 13082025
f3597fc
raw
history blame
34.6 kB
# app.py --
import sys
import os
import subprocess
import importlib.util
'''
print("--- INÍCIO DO DEBUG ---", flush=True)
# 1. Onde o Python está procurando por pacotes?
print("\n[1] Sys Path (Caminhos de Busca):", flush=True)
for path in sys.path:
print(f" - {path}", flush=True)
# 2. Quais pacotes estão realmente instalados (usando pip)?
print("\n[2] Pacotes Instalados (pip list):", flush=True)
try:
result = subprocess.run(
[sys.executable, '-m', 'pip', 'list'],
capture_output=True,
text=True,
check=True
)
print(result.stdout, flush=True)
except Exception as e:
print(f" ERRO ao rodar 'pip list': {e}", flush=True)
# 3. Vamos verificar especificamente o langchain-openai
print("\n[3] Verificando 'langchain_openai':", flush=True)
try:
spec = importlib.util.find_spec('langchain_openai')
if spec:
print(f" Pacote encontrado: {spec.name}", flush=True)
print(f" Localização: {spec.origin}", flush=True)
if spec.submodule_search_locations:
package_dir = spec.submodule_search_locations[0]
print(f" Pasta do pacote: {package_dir}", flush=True)
print(" Conteúdo da pasta do pacote:", flush=True)
try:
for item in os.listdir(package_dir):
print(f" - {item}", flush=True)
except Exception as e:
print(f" ERRO ao listar diretório: {e}", flush=True)
else:
print(" PACOTE 'langchain_openai' NÃO FOI ENCONTRADO PELO IMPORTLIB!", flush=True)
except Exception as e:
print(f" ERRO ao tentar encontrar 'langchain-openai': {e}", flush=True)
print("\n--- FIM DO DEBUG ---", flush=True)
'''
from flask import Flask, render_template, request, Response, jsonify
import json
import time
import uuid
import threading
import concurrent.futures
from html import escape, unescape
import re
from markdown_it import MarkdownIt
from markdown2 import markdown as markdown2_render
# Força o flush dos prints para aparecer nos logs do container
sys.stdout.reconfigure(line_buffering=True)
sys.stderr.reconfigure(line_buffering=True)
# Importações do LangChain
from langchain.prompts import PromptTemplate
from langchain_core.output_parsers import StrOutputParser
# Importa os LLMs
from llms import claude_llm, gemini_llm, openai_llm#, grok_llm
# Importa os prompts
from config import *
# Importa nosso processador RAG
from rag_processor import get_relevant_context
app = Flask(__name__)
# Garante que o diretório de uploads exista
if not os.path.exists('uploads'):
os.makedirs('uploads')
app.config['MAX_CONTENT_LENGTH'] = 100 * 1024 * 1024
# Instancia o conversor de Markdown
md = MarkdownIt()
# Variável global para controle de interrupção
processing_cancelled = False
def log_print(message):
"""Função para garantir que os logs apareçam no container"""
print(f"[DEBUG] {message}", flush=True)
sys.stdout.flush()
def safe_json_dumps(data):
"""Função para criar JSON de forma segura, com tratamento de strings muito grandes"""
try:
json_str = json.dumps(data, ensure_ascii=False)
# Se o JSON for muito grande (> 50MB), trunca o conteúdo
if len(json_str.encode('utf-8')) > 50 * 1024 * 1024:
log_print(f"JSON muito grande ({len(json_str)} chars), truncando...")
if 'content' in str(data):
# Cria uma versão truncada
truncated_data = data.copy() if isinstance(data, dict) else {}
if 'final_result' in truncated_data and 'content' in truncated_data['final_result']:
original_content = truncated_data['final_result']['content']
truncated_content = original_content[:10000] + "\n\n[CONTEÚDO TRUNCADO DEVIDO AO TAMANHO - Use o botão 'Copiar' para obter o texto completo]"
truncated_data['final_result']['content'] = truncated_content
elif 'partial_result' in truncated_data and 'content' in truncated_data['partial_result']:
original_content = truncated_data['partial_result']['content']
truncated_content = original_content[:10000] + "\n\n[CONTEÚDO TRUNCADO DEVIDO AO TAMANHO - Use o botão 'Copiar' para obter o texto completo]"
truncated_data['partial_result']['content'] = truncated_content
return json.dumps(truncated_data, ensure_ascii=False)
return json_str
except Exception as e:
log_print(f"Erro ao criar JSON: {e}")
return json.dumps({'error': f'Erro na serialização JSON: {str(e)}'})
# Variável global para armazenar o conteúdo completo do merge
merge_full_content = ""
# Função para renderização com fallback: tenta MarkdownIt, depois markdown2
def render_markdown_cascata(texto: str) -> str:
try:
html_1 = md.render(texto)
if not is_html_empty(html_1):
return html_1
except Exception as e:
log_print(f"MarkdownIt falhou: {e}")
try:
html_2 = markdown2_render(texto, extras=["fenced-code-blocks", "tables"])
if not is_html_empty(html_2):
return html_2
except Exception as e:
log_print(f"markdown2 falhou: {e}")
return f"<pre>{escape(texto)}</pre>"
def is_html_empty(html: str) -> bool:
"""
Verifica de forma robusta se uma string HTML não contém texto visível,
lidando com entidades HTML e múltiplos tipos de espaços em branco.
"""
if not html:
return True
# 1. Remove todas as tags HTML
text_only = re.sub('<[^<]+?>', '', html)
# 2. Decodifica entidades HTML (ex: &nbsp; para ' ')
decoded_text = unescape(text_only)
# 3. Substitui qualquer sequência de caracteres de espaço em branco por um único espaço
normalized_space = re.sub(r'\s+', ' ', decoded_text)
# 4. Verifica se o texto restante (após remover espaços nas pontas) está de fato vazio
return not normalized_space.strip()
@app.route('/')
def index():
"""Renderiza a página inicial da aplicação."""
return render_template('index.html')
# ROTA ATUALIZADA: Para converter texto em Markdown sob demanda
@app.route('/convert', methods=['POST'])
def convert():
data = request.get_json()
if not data or 'text' not in data:
return jsonify({'error': 'Nenhum texto fornecido'}), 400
text_to_convert = data['text']
# USA A FUNÇÃO DE CASCATA PARA MAIOR ROBUSTEZ
converted_html = render_markdown_cascata(text_to_convert)
return jsonify({'html': converted_html})
# NOVA ROTA: Para cancelar processamento
@app.route('/cancel', methods=['POST'])
def cancel():
global processing_cancelled
processing_cancelled = True
log_print("=== PROCESSAMENTO CANCELADO PELO USUÁRIO ===")
return jsonify({'status': 'cancelled'})
# NOVA ROTA: Para obter o conteúdo completo do merge
@app.route('/get-full-content', methods=['POST'])
def get_full_content():
global merge_full_content
data = request.get_json()
content_type = data.get('type', 'merge')
if content_type == 'merge' and merge_full_content:
return jsonify({'content': merge_full_content})
else:
return jsonify({'error': 'Conteúdo não encontrado'}), 404
@app.route('/process', methods=['POST'])
def process():
"""Processa a solicitação do usuário nos modos Hierárquico ou Atômico."""
global processing_cancelled
processing_cancelled = False # Reset do flag de cancelamento
log_print("=== ROTA PROCESS ACESSADA ===")
form_data = request.form
files = request.files.getlist('files')
contexto = form_data.get('contexto', '').strip()
mode = form_data.get('mode', 'real')
processing_mode = form_data.get('processing_mode', 'hierarchical')
# NOVOS PARÂMETROS: Tamanho do texto
min_chars = int(form_data.get('min_chars', 24000))
max_chars = int(form_data.get('max_chars', 30000))
log_print(f"Mode: {mode}, Processing: {processing_mode}")
log_print(f"Tamanho solicitado: {min_chars} - {max_chars} caracteres")
temp_file_paths = []
if mode == 'real':
for file in files:
if file and file.filename:
unique_filename = str(uuid.uuid4()) + "_" + os.path.basename(file.filename)
file_path = os.path.join('uploads', unique_filename)
file.save(file_path)
temp_file_paths.append(file_path)
def generate_stream(current_mode, form_data, file_paths):
"""Gera a resposta em streaming para o front-end."""
global processing_cancelled
log_print(f"=== GENERATE_STREAM INICIADO - Mode: {current_mode} ===")
solicitacao_usuario = form_data.get('solicitacao', '')
contexto = form_data.get('contexto', '')
if current_mode == 'test':
log_print("=== MODO TESTE EXECUTADO ===")
mock_text = form_data.get('mock_text', 'Este é um **texto** de `simulação`.')
# json_data = safe_json_dumps({'progress': 100, 'message': 'Simulação concluída!', 'partial_result': {'id': 'grok-output', 'content': mock_text}, 'done': True, 'mode': 'atomic' if processing_mode == 'atomic' else 'hierarchical'})
json_data = safe_json_dumps({'progress': 100, 'message': 'Simulação concluída!', 'partial_result': {'id': 'openai-output', 'content': mock_text}, 'done': True, 'mode': 'atomic' if processing_mode == 'atomic' else 'hierarchical'})
yield f"data: {json_data}\n\n"
if processing_mode == 'atomic':
json_data = safe_json_dumps({'partial_result': {'id': 'sonnet-output', 'content': mock_text}})
yield f"data: {json_data}\n\n"
json_data = safe_json_dumps({'partial_result': {'id': 'gemini-output', 'content': mock_text}})
yield f"data: {json_data}\n\n"
else:
if not solicitacao_usuario:
log_print("=== ERRO: SOLICITAÇÃO VAZIA ===")
json_data = safe_json_dumps({'error': 'Solicitação não fornecida.'})
yield f"data: {json_data}\n\n"
return
try:
log_print("=== INICIANDO PROCESSAMENTO REAL ===")
json_data = safe_json_dumps({'progress': 0, 'message': 'Processando arquivos e extraindo contexto...'})
yield f"data: {json_data}\n\n"
rag_context = get_relevant_context(file_paths, solicitacao_usuario)
log_print(f"=== RAG CONTEXT OBTIDO: {len(rag_context)} chars ===")
output_parser = StrOutputParser()
if processing_mode == 'atomic':
log_print("=== MODO ATÔMICO SELECIONADO ===")
# --- LÓGICA ATÔMICA (PARALELA) ---
results = {}
threads = []
def run_chain_with_timeout(chain, inputs, key, timeout=300):
def task():
if processing_cancelled:
return "CANCELLED"
return chain.invoke(inputs)
with concurrent.futures.ThreadPoolExecutor() as executor:
future = executor.submit(task)
try:
result = future.result(timeout=timeout)
if result == "CANCELLED":
results[key] = "CANCELLED"
elif not result or not result.strip():
results[key] = "Error:EmptyResponse"
else:
results[key] = result
except concurrent.futures.TimeoutError:
results[key] = f"Erro ao processar {key.upper()}: Tempo limite excedido."
except Exception as e:
results[key] = f"Erro ao processar {key.upper()}: {e}"
claude_atomic_llm = claude_llm.bind(max_tokens=60000)
#models = {'grok': grok_llm, 'sonnet': claude_atomic_llm, 'gemini': gemini_llm, 'openai': openai_llm}
# Melhoria 05/08/2025 - Multi-modelo
models = {}
if form_data.get('modelo-openai') == 'on':
models['openai'] = openai_llm
if form_data.get('modelo-sonnet') == 'on':
models['sonnet'] = claude_atomic_llm
if form_data.get('modelo-gemini') == 'on':
models['gemini'] = gemini_llm
# Verificação se pelo menos um modelo foi selecionado
if not models:
json_data = safe_json_dumps({'error': 'Você deve selecionar pelo menos um modelo para processamento.'})
yield f"data: {json_data}\n\n"
return
# Substituir os placeholders no template
updated_prompt_template = PROMPT_ATOMICO_INICIAL.replace(
"MIN_CHARS_PLACEHOLDER", str(min_chars)
).replace(
"MAX_CHARS_PLACEHOLDER", str(max_chars)
)
# --- renderiza e loga o prompt final Atomico ---
# ——— log do prompt atômico já formatado ———
log_print(f"[DEBUG] PROMPT ATÔMICO RENDERED:\n"
f"{updated_prompt_template.format(contexto=contexto, solicitacao_usuario=solicitacao_usuario, rag_context=rag_context)}\n""-"*80)
prompt = PromptTemplate(template=updated_prompt_template, input_variables=["contexto", "solicitacao_usuario", "rag_context"])
json_data = safe_json_dumps({'progress': 15, 'message': 'Iniciando processamento paralelo...'})
yield f"data: {json_data}\n\n"
for name, llm in models.items():
if processing_cancelled:
json_data = safe_json_dumps({'error': 'Processamento cancelado pelo usuário.'})
yield f"data: {json_data}\n\n"
return
chain = prompt | llm | output_parser
thread = threading.Thread(target=run_chain_with_timeout, args=(chain, {"contexto": contexto, "solicitacao_usuario": solicitacao_usuario, "rag_context": rag_context}, name))
threads.append(thread)
thread.start()
for thread in threads:
thread.join()
# Verificar se foi cancelado
if processing_cancelled or any(result == "CANCELLED" for result in results.values()):
json_data = safe_json_dumps({'error': 'Processamento cancelado pelo usuário.'})
yield f"data: {json_data}\n\n"
return
for key, result in results.items():
if result == "Error:EmptyResponse" or "Erro ao processar" in result:
error_msg = result if "Erro ao processar" in result else f"Falha no serviço {key.upper()}: Sem resposta."
json_data = safe_json_dumps({'error': error_msg})
yield f"data: {json_data}\n\n"
return
json_data = safe_json_dumps({'progress': 80, 'message': 'Todos os modelos responderam. Formatando saídas...'})
yield f"data: {json_data}\n\n"
# Envia o texto bruto para cada modelo
##grok_text = results.get('grok', '')
##log_print(f"--- Resposta Bruta do GROK (Atômico) ---\n{grok_text[:200]}...\n--------------------------------------")
##json_data = safe_json_dumps({'partial_result': {'id': 'grok-output', 'content': grok_text}})
##yield f"data: {json_data}\n\n"
openai_text = results.get('openai', '')
log_print(f"--- Resposta Bruta do OPEN AI (Atômico) ---\n{openai_text[:200]}...\n--------------------------------------")
json_data = safe_json_dumps({'partial_result': {'id': 'openai-output', 'content': openai_text}})
yield f"data: {json_data}\n\n"
sonnet_text = results.get('sonnet', '')
log_print(f"--- Resposta Bruta do Sonnet (Atômico) ---\n{sonnet_text[:200]}...\n----------------------------------------")
json_data = safe_json_dumps({'partial_result': {'id': 'sonnet-output', 'content': sonnet_text}})
yield f"data: {json_data}\n\n"
gemini_text = results.get('gemini', '')
log_print(f"--- Resposta Bruta do Gemini (Atômico) ---\n{gemini_text[:200]}...\n----------------------------------------")
json_data = safe_json_dumps({'partial_result': {'id': 'gemini-output', 'content': gemini_text}})
yield f"data: {json_data}\n\n"
json_data = safe_json_dumps({'progress': 100, 'message': 'Processamento Atômico concluído!', 'done': True, 'mode': 'atomic'})
yield f"data: {json_data}\n\n"
else:
log_print("=== MODO HIERÁRQUICO SELECIONADO ===")
# --- LÓGICA HIERÁRQUICA (SEQUENCIAL) ---
# Atualizar prompts hierárquicos com parâmetros de tamanho
##updated_grok_template = PROMPT_HIERARQUICO_GROK.replace(
updated_openai_template = PROMPT_HIERARQUICO_OPENAI.replace(
"MIN_CHARS_PLACEHOLDER", str(min_chars)
).replace(
"MAX_CHARS_PLACEHOLDER", str(max_chars)
)
# --- renderiza e loga o prompt final Hierárquico Grok ---
##log_print(f"[DEBUG] PROMPT HIERÁRQUICO GROK RENDERED:\n"
## f"{updated_grok_template.format(contexto=contexto, solicitacao_usuario=solicitacao_usuario, rag_context=rag_context)}\n""-"*80)
# --- renderiza e loga o prompt final Hierárquico OpenAI ---
log_print(f"[DEBUG] PROMPT HIERÁRQUICO OPEN AI RENDERED:\n"
f"{updated_openai_template.format(contexto=contexto, solicitacao_usuario=solicitacao_usuario, rag_context=rag_context)}\n""-"*80)
updated_sonnet_template = PROMPT_HIERARQUICO_SONNET.replace(
"MIN_CHARS_PLACEHOLDER", str(min_chars)
).replace(
"MAX_CHARS_PLACEHOLDER", str(max_chars)
)
# --- renderiza e loga o prompt final Hierárquico Sonnet ---
##log_print(f"[DEBUG] PROMPT HIERÁRQUICO SONNET RENDERED:\n"
## f"{updated_sonnet_template.format(contexto=contexto, solicitacao_usuario=solicitacao_usuario, texto_para_analise=resposta_grok)}\n""-"*80)
log_print(f"[DEBUG] PROMPT HIERÁRQUICO SONNET RENDERED:\n"
f"{updated_sonnet_template.format(contexto=contexto, solicitacao_usuario=solicitacao_usuario, texto_para_analise=resposta_openai)}\n""-"*80)
updated_gemini_template = PROMPT_HIERARQUICO_GEMINI.replace(
"MIN_CHARS_PLACEHOLDER", str(min_chars)
).replace(
"MAX_CHARS_PLACEHOLDER", str(max_chars)
)
# --- renderiza e loga o prompt final Hierárquico Gemini ---
log_print(f"[DEBUG] PROMPT HIERÁRQUICO GEMINI RENDERED:\n"
f"{updated_gemini_template.format(contexto=contexto, solicitacao_usuario=solicitacao_usuario, texto_para_analise=resposta_sonnet)}\n""-"*80)
##json_data = safe_json_dumps({'progress': 15, 'message': 'O GROK está processando sua solicitação...'})
json_data = safe_json_dumps({'progress': 15, 'message': 'A OPEN AI está processando sua solicitação...'})
yield f"data: {json_data}\n\n"
if processing_cancelled:
json_data = safe_json_dumps({'error': 'Processamento cancelado pelo usuário.'})
yield f"data: {json_data}\n\n"
return
#log_print("=== PROCESSANDO GROK ===")
#prompt_grok = PromptTemplate(template=updated_grok_template, input_variables=["contexto", "solicitacao_usuario", "rag_context"])
#chain_grok = prompt_grok | grok_llm | output_parser
#resposta_grok = chain_grok.invoke({"contexto": contexto, "solicitacao_usuario": solicitacao_usuario, "rag_context": rag_context})
#log_print(f"=== GROK TERMINOU: {len(resposta_grok)} chars ===")
log_print("=== PROCESSANDO OPEN AI ===")
openai_with_max_tokens = openai_llm.bind(max_completion_tokens=100000)
prompt_openai = PromptTemplate(template=updated_openai_template, input_variables=["contexto", "solicitacao_usuario", "rag_context"])
chain_openai = prompt_openai | openai_with_max_tokens | output_parser
resposta_openai = chain_openai.invoke({"contexto": contexto, "solicitacao_usuario": solicitacao_usuario, "rag_context": rag_context})
log_print(f"=== OPEN AI TERMINOU: {len(resposta_openai)} chars ===")
if processing_cancelled:
json_data = safe_json_dumps({'error': 'Processamento cancelado pelo usuário.'})
yield f"data: {json_data}\n\n"
return
##if not resposta_grok or not resposta_grok.strip():
## log_print("=== ERRO: GROK VAZIO ===")
## json_data = safe_json_dumps({'error': 'Falha no serviço GROK: Sem resposta.'})
## yield f"data: {json_data}\n\n"
## return
##
##log_print("=== ENVIANDO RESPOSTA GROK PARA FRONTEND ===")
##json_data = safe_json_dumps({'progress': 33, 'message': 'Claude Sonnet está processando...', 'partial_result': {'id': 'grok-output', 'content': resposta_grok}})
##yield f"data: {json_data}\n\n"
if not resposta_openai or not resposta_openai.strip():
log_print("=== ERRO: OPEN AI VAZIO ===")
json_data = safe_json_dumps({'error': 'Falha no serviço OPEN AI: Sem resposta.'})
yield f"data: {json_data}\n\n"
return
log_print("=== ENVIANDO RESPOSTA OPEN AI PARA FRONTEND ===")
json_data = safe_json_dumps({'progress': 33, 'message': 'Claude Sonnet está processando...', 'partial_result': {'id': 'openai-output', 'content': resposta_openai}})
yield f"data: {json_data}\n\n"
if processing_cancelled:
json_data = safe_json_dumps({'error': 'Processamento cancelado pelo usuário.'})
yield f"data: {json_data}\n\n"
return
log_print("=== PROCESSANDO SONNET ===")
prompt_sonnet = PromptTemplate(template=updated_sonnet_template, input_variables=["contexto", "solicitacao_usuario", "texto_para_analise"])
claude_with_max_tokens = claude_llm.bind(max_tokens=60000)
chain_sonnet = prompt_sonnet | claude_with_max_tokens | output_parser
resposta_sonnet = chain_sonnet.invoke({"contexto": contexto, "solicitacao_usuario": solicitacao_usuario, "texto_para_analise": resposta_openai})
log_print(f"=== SONNET TERMINOU: {len(resposta_sonnet)} chars ===")
if processing_cancelled:
json_data = safe_json_dumps({'error': 'Processamento cancelado pelo usuário.'})
yield f"data: {json_data}\n\n"
return
if not resposta_sonnet or not resposta_sonnet.strip():
log_print("=== ERRO: SONNET VAZIO ===")
json_data = safe_json_dumps({'error': 'Falha no serviço Claude Sonnet: Sem resposta.'})
yield f"data: {json_data}\n\n"
return
log_print("=== ENVIANDO RESPOSTA SONNET ===")
json_data = safe_json_dumps({'progress': 66, 'message': 'Gemini está processando...', 'partial_result': {'id': 'sonnet-output', 'content': resposta_sonnet}})
yield f"data: {json_data}\n\n"
if processing_cancelled:
json_data = safe_json_dumps({'error': 'Processamento cancelado pelo usuário.'})
yield f"data: {json_data}\n\n"
return
log_print("=== PROCESSANDO GEMINI ===")
prompt_gemini = PromptTemplate(template=updated_gemini_template, input_variables=["contexto", "solicitacao_usuario", "texto_para_analise"])
chain_gemini = prompt_gemini | gemini_llm | output_parser
resposta_gemini = chain_gemini.invoke({"contexto": contexto, "solicitacao_usuario": solicitacao_usuario, "texto_para_analise": resposta_sonnet})
log_print(f"=== GEMINI TERMINOU: {len(resposta_gemini)} chars ===")
if processing_cancelled:
json_data = safe_json_dumps({'error': 'Processamento cancelado pelo usuário.'})
yield f"data: {json_data}\n\n"
return
if not resposta_gemini or not resposta_gemini.strip():
log_print("=== ERRO: GEMINI VAZIO ===")
json_data = safe_json_dumps({'error': 'Falha no serviço Gemini: Sem resposta.'})
yield f"data: {json_data}\n\n"
return
log_print("=== ENVIANDO RESPOSTA GEMINI ===")
json_data = safe_json_dumps({'progress': 100, 'message': 'Processamento concluído!', 'partial_result': {'id': 'gemini-output', 'content': resposta_gemini}, 'done': True, 'mode': 'hierarchical'})
yield f"data: {json_data}\n\n"
log_print("=== PROCESSAMENTO COMPLETO ===")
except Exception as e:
log_print(f"Ocorreu um erro durante o processamento: {e}")
import traceback
log_print(f"Traceback: {traceback.format_exc()}")
json_data = safe_json_dumps({'error': f'Ocorreu um erro inesperado na aplicação: {e}'})
yield f"data: {json_data}\n\n"
return Response(generate_stream(mode, form_data, temp_file_paths), mimetype='text/event-stream')
@app.route('/merge', methods=['POST'])
def merge():
"""Recebe os textos do modo Atômico e os consolida usando Claude Sonnet."""
global merge_full_content, processing_cancelled
processing_cancelled = False # Reset do flag
data = request.get_json()
# Extrair parâmetros que faltavam no scope do merge
contexto = data.get('contexto', '')
min_chars = int(data.get('min_chars', 24000))
max_chars = int(data.get('max_chars', 30000))
log_print("=== ROTA MERGE ACESSADA ===")
log_print("=== USANDO CLAUDE SONNET PARA MERGE ===")
def generate_merge_stream():
"""Gera a resposta do merge em streaming."""
global merge_full_content, processing_cancelled
try:
log_print("=== INICIANDO MERGE STREAM ===")
json_data = safe_json_dumps({'progress': 0, 'message': 'Iniciando o processo de merge...'})
yield f"data: {json_data}\n\n"
if processing_cancelled:
json_data = safe_json_dumps({'error': 'Processamento cancelado pelo usuário.'})
yield f"data: {json_data}\n\n"
return
# Monta lista só com os textos que vieram no payload
available = []
for campo in ('openai_text', 'sonnet_text', 'gemini_text'):
txt = data.get(campo)
if txt and txt.strip():
available.append(txt)
if len(available) < 2:
json_data = safe_json_dumps({
'error': 'Para processar o merge, deve haver pelo menos dois textos gerados.'
})
yield f"data: {json_data}\n\n"
return
output_parser = StrOutputParser()
# Atualizar o template de merge com os parâmetros de tamanho padrão
updated_merge_template = PROMPT_ATOMICO_MERGE.replace(
"MIN_CHARS_PLACEHOLDER", str(min_chars)
).replace(
"MAX_CHARS_PLACEHOLDER", str(max_chars)
)
# --- renderiza e loga o prompt final Atomico Merge --
##log_print(f"[DEBUG] PROMPT MERGE RENDERED:\n"
## f"{updated_merge_template.format(contexto=contexto, solicitacao_usuario=data.get('solicitacao_usuario'), texto_para_analise_grok=data.get('grok_text'), texto_para_analise_sonnet=data.get('sonnet_text'), texto_para_analise_gemini=data.get('gemini_text'))}\n""-"*80)
##
##prompt_merge = PromptTemplate(template=updated_merge_template, input_variables=["contexto", "solicitacao_usuario", "texto_para_analise_grok", "texto_para_analise_sonnet", "texto_para_analise_gemini"])
log_print(f"[DEBUG] PROMPT MERGE RENDERED:\n"
f"{updated_merge_template.format(contexto=contexto, solicitacao_usuario=data.get('solicitacao_usuario'), texto_para_analise_openai=data.get('openai_text'), texto_para_analise_sonnet=data.get('sonnet_text'), texto_para_analise_gemini=data.get('gemini_text'))}\n""-"*80)
prompt_merge = PromptTemplate(template=updated_merge_template, input_variables=["contexto", "solicitacao_usuario", "texto_para_analise_openai", "texto_para_analise_sonnet", "texto_para_analise_gemini"])
# MUDANÇA: Usar Claude Sonnet para o merge
claude_with_max_tokens = claude_llm.bind(max_tokens=64000)
chain_merge = prompt_merge | claude_with_max_tokens | output_parser
json_data = safe_json_dumps({'progress': 50, 'message': 'Enviando textos para o Claude Sonnet para consolidação...'})
yield f"data: {json_data}\n\n"
log_print("=== INVOCANDO CLAUDE SONNET PARA MERGE ===")
if processing_cancelled:
json_data = safe_json_dumps({'error': 'Processamento cancelado pelo usuário.'})
yield f"data: {json_data}\n\n"
return
resposta_merge = chain_merge.invoke({
"contexto": data.get('contexto'),
"solicitacao_usuario": data.get('solicitacao_usuario'),
##"texto_para_analise_grok": data.get('grok_text'),
"texto_para_analise_openai": data.get('openai_text'),
"texto_para_analise_sonnet": data.get('sonnet_text'),
"texto_para_analise_gemini": data.get('gemini_text')
})
log_print(f"=== MERGE CLAUDE SONNET CONCLUÍDO: {len(resposta_merge)} chars ===")
if processing_cancelled:
json_data = safe_json_dumps({'error': 'Processamento cancelado pelo usuário.'})
yield f"data: {json_data}\n\n"
return
if not resposta_merge or not resposta_merge.strip():
json_data = safe_json_dumps({'error': 'Falha no serviço de Merge (Claude Sonnet): Sem resposta.'})
yield f"data: {json_data}\n\n"
return
# Armazena o conteúdo completo na variável global
merge_full_content = resposta_merge
word_count = len(resposta_merge.split())
log_print(f"=== CRIANDO JSON DE RESPOSTA DO MERGE ===")
# Usa a função safe para evitar problemas com JSON muito grande
json_data = safe_json_dumps({
'progress': 100,
'message': 'Merge concluído!',
'final_result': {
'content': resposta_merge,
'word_count': word_count
},
'done': True
})
log_print(f"=== JSON CRIADO: {len(json_data)} chars ===")
yield f"data: {json_data}\n\n"
log_print("=== MERGE STREAM FINALIZADO ===")
except Exception as e:
log_print(f"Erro no processo de merge: {e}")
import traceback
log_print(f"Traceback: {traceback.format_exc()}")
json_data = safe_json_dumps({'error': str(e)})
yield f"data: {json_data}\n\n"
return Response(generate_merge_stream(), mimetype='text/event-stream')
if __name__ == '__main__':
log_print("=== SERVIDOR FLASK INICIADO ===")
app.run(debug=True)