victorafarias commited on
Commit
18df1d9
·
0 Parent(s):

Versão inicial do sistema

Browse files
Files changed (11) hide show
  1. .gitignore +12 -0
  2. Dockerfile +18 -0
  3. app.py +112 -0
  4. config.py +95 -0
  5. custom_grok.py +56 -0
  6. llms.py +32 -0
  7. rag_processor.py +58 -0
  8. requirements.txt +0 -0
  9. resposta_sonnet.md +27 -0
  10. static/style.css +364 -0
  11. templates/index.html +245 -0
.gitignore ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Arquivos de ambiente virtual
2
+ .venv311/
3
+ __pycache__/
4
+
5
+ # Arquivo de segredos
6
+ .env
7
+
8
+ # Arquivos de upload temporários
9
+ uploads/
10
+
11
+ # Arquivos do VS Code
12
+ .vscode/
Dockerfile ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Usa uma imagem oficial do Python como base
2
+ FROM python:3.11-slim
3
+
4
+ # Define o diretório de trabalho dentro do contêiner
5
+ WORKDIR /app
6
+
7
+ # Copia o arquivo de dependências primeiro para aproveitar o cache do Docker
8
+ COPY requirements.txt requirements.txt
9
+
10
+ # Instala as dependências
11
+ RUN pip install --no-cache-dir -r requirements.txt
12
+
13
+ # Copia todo o resto do código do projeto para o diretório de trabalho
14
+ COPY . .
15
+
16
+ # Comando para iniciar o servidor web de produção (gunicorn)
17
+ # Ele vai procurar por uma variável chamada 'app' no arquivo 'app.py'
18
+ CMD ["gunicorn", "--bind", "0.0.0.0:7860", "app:app"]
app.py ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app.py
2
+
3
+ from flask import Flask, render_template, request, Response
4
+ import markdown2
5
+ import json
6
+ import time
7
+ import os
8
+ import uuid # Para gerar nomes de arquivo únicos
9
+
10
+ # Importações do LangChain
11
+ from langchain.prompts import PromptTemplate
12
+ from langchain.chains import LLMChain
13
+
14
+ # Importa os LLMs
15
+ from llms import grok_llm, claude_llm, gemini_llm
16
+
17
+ # Importa os prompts
18
+ from config import PROMPT_GROK, PROMPT_CLAUDE_SONNET, PROMPT_GEMINI
19
+
20
+ # Importa nosso processador RAG
21
+ from rag_processor import get_relevant_context
22
+
23
+ app = Flask(__name__)
24
+
25
+ app.config['MAX_CONTENT_LENGTH'] = 100 * 1024 * 1024
26
+
27
+ if not os.path.exists('uploads'):
28
+ os.makedirs('uploads')
29
+
30
+ @app.route('/')
31
+ def index():
32
+ return render_template('index.html')
33
+
34
+ @app.route('/process', methods=['POST'])
35
+ def process():
36
+ form_data = request.form
37
+ files = request.files.getlist('files')
38
+ mode = form_data.get('mode', 'real')
39
+
40
+ temp_file_paths = []
41
+ if mode == 'real':
42
+ for file in files:
43
+ if file and file.filename:
44
+ unique_filename = str(uuid.uuid4()) + "_" + file.filename
45
+ file_path = os.path.join('uploads', unique_filename)
46
+ file.save(file_path)
47
+ temp_file_paths.append(file_path)
48
+
49
+ def generate_stream(current_mode, form_data, file_paths):
50
+ # Lógica de simulação atualizada para a nova ordem
51
+ if current_mode == 'test':
52
+ mock_text = form_data.get('mock_text', 'Este é um texto de simulação.')
53
+ mock_html = markdown2.markdown(mock_text, extras=["fenced-code-blocks", "tables"])
54
+
55
+ yield f"data: {json.dumps({'progress': 0, 'message': 'Simulando Etapa 1: GROK...'})}\n\n"
56
+ time.sleep(1)
57
+ # O primeiro resultado agora vai para a coluna do GROK
58
+ yield f"data: {json.dumps({'progress': 33, 'message': 'Simulando Etapa 2: Claude Sonnet...', 'partial_result': {'id': 'grok-output', 'content': mock_html}})}\n\n"
59
+ time.sleep(1)
60
+ # O segundo resultado vai para a coluna do Sonnet
61
+ yield f"data: {json.dumps({'progress': 66, 'message': 'Simulando Etapa 3: Gemini...', 'partial_result': {'id': 'sonnet-output', 'content': mock_html}})}\n\n"
62
+ time.sleep(1)
63
+ yield f"data: {json.dumps({'progress': 100, 'message': 'Simulação concluída!', 'partial_result': {'id': 'gemini-output', 'content': mock_html}, 'done': True})}\n\n"
64
+
65
+ # Lógica real com a ordem de execução alterada
66
+ else:
67
+ solicitacao_usuario = form_data.get('solicitacao', '')
68
+ if not solicitacao_usuario:
69
+ yield f"data: {json.dumps({'error': 'Solicitação não fornecida.'})}\n\n"
70
+ return
71
+
72
+ try:
73
+ # --- ETAPA RAG (continua sendo o passo zero) ---
74
+ yield f"data: {json.dumps({'progress': 0, 'message': 'Processando arquivos e extraindo contexto...'})}\n\n"
75
+ rag_context = get_relevant_context(file_paths, solicitacao_usuario)
76
+
77
+ # --- ETAPA 1: GROK (agora o primeiro) ---
78
+ yield f"data: {json.dumps({'progress': 15, 'message': 'O GROK está processando sua solicitação com os arquivos...'})}\n\n"
79
+ prompt_grok = PromptTemplate(template=PROMPT_GROK, input_variables=["solicitacao_usuario", "rag_context"])
80
+ chain_grok = LLMChain(llm=grok_llm, prompt=prompt_grok)
81
+ resposta_grok = chain_grok.invoke({"solicitacao_usuario": solicitacao_usuario, "rag_context": rag_context})['text']
82
+ grok_html = markdown2.markdown(resposta_grok, extras=["fenced-code-blocks", "tables"])
83
+ yield f"data: {json.dumps({'progress': 33, 'message': 'Agora, o Claude Sonnet está aprofundando o texto...', 'partial_result': {'id': 'grok-output', 'content': grok_html}})}\n\n"
84
+
85
+ # --- ETAPA 2: Claude Sonnet (agora o segundo) ---
86
+ prompt_sonnet = PromptTemplate(template=PROMPT_CLAUDE_SONNET, input_variables=["solicitacao_usuario", "texto_para_analise"])
87
+
88
+ # Usamos .bind() para anexar o parâmetro max_tokens ao modelo nesta chamada específica.
89
+ claude_with_max_tokens = claude_llm.bind(max_tokens=10000)
90
+ chain_sonnet = LLMChain(llm=claude_with_max_tokens, prompt=prompt_sonnet)
91
+
92
+ # O input agora é a resposta do GROK
93
+ resposta_sonnet = chain_sonnet.invoke({"solicitacao_usuario": solicitacao_usuario, "texto_para_analise": resposta_grok})['text']
94
+ sonnet_html = markdown2.markdown(resposta_sonnet, extras=["fenced-code-blocks", "tables"])
95
+ yield f"data: {json.dumps({'progress': 66, 'message': 'Estamos quase lá! Seu texto está passando por uma revisão final com o Gemini...', 'partial_result': {'id': 'sonnet-output', 'content': sonnet_html}})}\n\n"
96
+
97
+ # --- ETAPA 3: Gemini (continua o último) ---
98
+ prompt_gemini = PromptTemplate(template=PROMPT_GEMINI, input_variables=["solicitacao_usuario", "texto_para_analise"])
99
+ chain_gemini = LLMChain(llm=gemini_llm, prompt=prompt_gemini)
100
+ # O input agora é a resposta do SONNET
101
+ resposta_gemini = chain_gemini.invoke({"solicitacao_usuario": solicitacao_usuario, "texto_para_analise": resposta_sonnet})['text']
102
+ gemini_html = markdown2.markdown(resposta_gemini, extras=["fenced-code-blocks", "tables"])
103
+ yield f"data: {json.dumps({'progress': 100, 'message': 'Processamento concluído!', 'partial_result': {'id': 'gemini-output', 'content': gemini_html}, 'done': True})}\n\n"
104
+
105
+ except Exception as e:
106
+ print(f"Ocorreu um erro durante o processamento: {e}")
107
+ yield f"data: {json.dumps({'error': f'Ocorreu um erro inesperado na aplicação: {e}'})}\n\n"
108
+
109
+ return Response(generate_stream(mode, form_data, temp_file_paths), mimetype='text/event-stream')
110
+
111
+ if __name__ == '__main__':
112
+ app.run(debug=True)
config.py ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # config.py
2
+
3
+ # Prompt para o primeiro agente (GROK)
4
+ PROMPT_GROK = """
5
+ <prompt>
6
+ <role>
7
+ Você é um filósofo e teólogo católico, especialista em redigir textos profundos e detalhados sobre assuntos diversos da filosofia, teologia, política, antropologia, educação, psicologia etc.
8
+ </role>
9
+
10
+ <requirements>
11
+ <word_count>Entre 4000 e 5000 palavras</word_count>
12
+ <language>Português do Brasil</language>
13
+ <paragraph_structure>Parágrafos curtos para facilitar a leitura</paragraph_structure>
14
+ <language_style>
15
+ - Linguagem profunda e formal, mas acessível a leigos
16
+ - Evitar tecnicismos excessivos
17
+ - Evitar rigidez acadêmica desnecessária
18
+ - Manter profundidade intelectual sem perder clareza
19
+ </language_style>
20
+ </requirements>
21
+
22
+ <context_from_documents>
23
+ A seguir, trechos de documentos fornecidos pelo usuário para sua referência. Use-os como base teórica para enriquecer sua resposta.
24
+ ---
25
+ {rag_context}
26
+ ---
27
+ </context_from_documents>
28
+
29
+ <user_request>
30
+ <solicitacao_usuario>
31
+ {solicitacao_usuario}
32
+ </solicitacao_usuario>
33
+ </user_request>
34
+
35
+ <instructions>
36
+ Com base na solicitação do usuário acima, desenvolva um texto que:
37
+ 1. Explore o tema com profundidade filosófica e teológica
38
+ 2. Mantenha conexão com a tradição católica quando relevante
39
+ 3. Apresente argumentos bem estruturados e fundamentados
40
+ 4. Use exemplos práticos quando apropriado para ilustrar conceitos
41
+ 5. Mantenha tom respeitoso e reflexivo ao longo do texto
42
+ 6. Organize o conteúdo de forma lógica e progressiva
43
+ </instructions>
44
+ </prompt>
45
+ """
46
+
47
+ # Prompt para o segundo agente (Claude Sonnet)
48
+ PROMPT_CLAUDE_SONNET = """
49
+ Com base na solicitação original do usuário e no texto gerado pelo primeiro especialista, sua tarefa é analisar criticamente o texto e aprimorá-lo. Não faça reduções e nem resumos. Se conseguir aprofundar e detalhar melhor o texto, adicionar novas referência de novos autores, faça. Se não conseguir, não faça nada.
50
+
51
+ **Solicitação Original do Usuário:**
52
+ ---
53
+ {solicitacao_usuario}
54
+ ---
55
+
56
+ **Texto Gerado para Análise:**
57
+ ---
58
+ {texto_para_analise}
59
+ ---
60
+
61
+ **Suas Instruções:**
62
+ 1. **Valide se o texto atingiu a quantidade de palavras mínimas (4000 palavras).
63
+ 2. **Analise o texto:** Verifique a coesão, coerência e profundidade dos argumentos.
64
+ 3. **Aprofunde e Detalhe:** Identifique pontos que podem ser mais explorados. Adicione detalhes, exemplos e nuances que enriqueçam o conteúdo original.
65
+ 4. **Faça Correções:** Corrija eventuais imprecisões conceituais ou argumentativas.
66
+ 5. **Não Resuma ou Reduza:** Seu objetivo é expandir e aprofundar, nunca encurtar o texto. O resultado final deve ser uma versão mais completa e robusta do que a original.
67
+ 6. **Mantenha o Estilo:** Respeite o estilo de linguagem e o tom do texto original.
68
+
69
+ Reescreva o texto completo, incorporando suas melhorias, detalhamentos e correções.
70
+ """
71
+
72
+ # Prompt para o terceiro agente (Gemini)
73
+ PROMPT_GEMINI = """
74
+ Você é o revisor final. Sua função é polir e aperfeiçoar o texto que já passou por uma primeira rodada de escrita e uma segunda de revisão e aprofundamento. Não faça reduções e nem resumos. Se conseguir aprofundar e detalhar melhor o texto, adicionar novas referência de novos autores, faça. Se não conseguir, não faça nada.
75
+
76
+ **Solicitação Original do Usuário:**
77
+ ---
78
+ {solicitacao_usuario}
79
+ ---
80
+
81
+ **Texto Revisado para Análise Final:**
82
+ ---
83
+ {texto_para_analise}
84
+ ---
85
+
86
+ **Suas Instruções:**
87
+ 1. **Análise Crítica Final:** Leia o texto atentamente, buscando a máxima qualidade, clareza e profundidade.
88
+ 2. **Valide se o texto atingiu a quantidade de palavras mínimas (4000 palavras).
89
+ 3. **Correções e Complementos Finais:** Adicione os toques finais. Melhore a fluidez entre os parágrafos, enriqueça o vocabulário e adicione insights que possam ter sido omitidos.
90
+ 4. **Não Resuma ou Reduza:** Assim como o revisor anterior, seu papel é adicionar valor e profundidade, não remover conteúdo.
91
+ 5. **Garantia de Qualidade:** Assegure que o texto final atende a todos os requisitos da solicitação original do usuário de forma exemplar.
92
+ 6. **Exiba na resposta apenas o texto revisado, sem nenhuma outra mensagem para o usuário.
93
+
94
+ Reescreva o texto completo com suas melhorias finais. O texto deve estar impecável e pronto para publicação.
95
+ """
custom_grok.py ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Any, List, Optional
2
+ from langchain_core.language_models.chat_models import BaseChatModel
3
+ from langchain_core.messages import AIMessage, HumanMessage
4
+ from langchain_core.outputs import ChatGeneration, ChatResult
5
+ import requests
6
+
7
+ class GrokChatModel(BaseChatModel):
8
+ model: str
9
+ api_key: str
10
+ base_url: str
11
+
12
+ def _llm_type(self) -> str:
13
+ return "grok-chat"
14
+
15
+ def _default_headers(self):
16
+ return {
17
+ "x-api-key": self.api_key,
18
+ "Content-Type": "application/json",
19
+ }
20
+
21
+ def _generate(
22
+ self, messages: List[HumanMessage],
23
+ stop: Optional[List[str]] = None,
24
+ **kwargs: Any
25
+ ) -> ChatResult:
26
+ last_message = messages[-1].content
27
+
28
+ payload = {
29
+ "model": self.model,
30
+ "messages": [{"role": "user", "content": last_message}],
31
+ "temperature": 0.7
32
+ }
33
+
34
+ response = requests.post(
35
+ self.base_url,
36
+ headers=self._default_headers(),
37
+ json=payload
38
+ )
39
+
40
+ if response.status_code != 200:
41
+ raise ValueError(f"Erro na chamada da API da Grok: {response.status_code} - {response.text}")
42
+
43
+ result = response.json()
44
+ content = result["choices"][0]["message"]["content"]
45
+
46
+ # ✅ Correção: Retornar ChatResult com lista de ChatGeneration
47
+ return ChatResult(
48
+ generations=[ChatGeneration(message=AIMessage(content=content))]
49
+ )
50
+
51
+ @property
52
+ def _identifying_params(self) -> dict:
53
+ return {
54
+ "model": self.model,
55
+ "base_url": self.base_url,
56
+ }
llms.py ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # llms.py
2
+
3
+ import os
4
+ from dotenv import load_dotenv
5
+ # from langchain_openai import ChatOpenAI
6
+ from custom_grok import GrokChatModel
7
+ from langchain_google_genai import ChatGoogleGenerativeAI
8
+ from langchain_anthropic import ChatAnthropic
9
+
10
+ # Carrega as variáveis de ambiente do arquivo .env
11
+ load_dotenv()
12
+
13
+ # --- Inicialização dos LLMs ---
14
+
15
+ # GROK da xAI
16
+ grok_llm = GrokChatModel(
17
+ api_key=os.getenv("X_API_KEY"),
18
+ model=os.getenv("GROK_MODEL_ID"),
19
+ base_url=os.getenv("X_API_BASE_URL")
20
+ )
21
+
22
+ # Claude Sonnet
23
+ claude_llm = ChatAnthropic(
24
+ api_key=os.getenv("ANTHROPIC_API_KEY"),
25
+ model_name=os.getenv("CLAUDE_MODEL_ID")
26
+ )
27
+
28
+ # Gemini
29
+ gemini_llm = ChatGoogleGenerativeAI(
30
+ google_api_key=os.getenv("GOOGLE_API_KEY"),
31
+ model=os.getenv("GEMINI_MODEL_ID")
32
+ )
rag_processor.py ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # rag_processor.py
2
+
3
+ import os
4
+ from typing import List
5
+ from langchain_community.document_loaders import PyPDFLoader, Docx2txtLoader, TextLoader
6
+ from langchain.text_splitter import RecursiveCharacterTextSplitter
7
+ from langchain_community.vectorstores import FAISS
8
+ from langchain_community.embeddings import HuggingFaceEmbeddings
9
+
10
+ embeddings_model = HuggingFaceEmbeddings(model_name="all-MiniLM-L6-v2")
11
+
12
+ def get_relevant_context(file_paths: List[str], user_query: str) -> str:
13
+ """
14
+ Processa arquivos a partir de seus caminhos no disco, cria uma base de conhecimento
15
+ e retorna os trechos mais relevantes para a consulta do usuário.
16
+ """
17
+ documents = []
18
+
19
+ # 1. Carregar e Extrair Texto dos Arquivos a partir dos caminhos
20
+ for file_path in file_paths:
21
+ filename = os.path.basename(file_path)
22
+
23
+ if filename.endswith(".pdf"):
24
+ loader = PyPDFLoader(file_path)
25
+ elif filename.endswith(".docx"):
26
+ loader = Docx2txtLoader(file_path)
27
+ elif filename.endswith(".txt"):
28
+ loader = TextLoader(file_path, encoding='utf-8')
29
+ else:
30
+ continue
31
+
32
+ documents.extend(loader.load())
33
+
34
+ if not documents:
35
+ return "Nenhum documento de referência foi fornecido ou os formatos não são suportados."
36
+
37
+ # 2. Dividir o Texto em Chunks
38
+ text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
39
+ texts = text_splitter.split_documents(documents)
40
+
41
+ # 3. Criar a Base de Conhecimento Vetorial
42
+ vector_store = FAISS.from_documents(texts, embeddings_model)
43
+
44
+ # 4. Buscar os Chunks Mais Relevantes
45
+ retriever = vector_store.as_retriever(search_kwargs={"k": 4})
46
+ relevant_docs = retriever.invoke(user_query)
47
+
48
+ # 5. Formatar e Retornar o Contexto
49
+ context = "\n\n".join([doc.page_content for doc in relevant_docs])
50
+
51
+ # Limpa os arquivos temporários após o uso
52
+ for file_path in file_paths:
53
+ try:
54
+ os.remove(file_path)
55
+ except OSError as e:
56
+ print(f"Erro ao deletar o arquivo {file_path}: {e}")
57
+
58
+ return context
requirements.txt ADDED
Binary file (9.01 kB). View file
 
resposta_sonnet.md ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # As Bases Morais Pré-Políticas do Estado Liberal: Secularismo e Religião
2
+
3
+ ## Introdução: O Dilema Moderno da Fundamentação Moral
4
+
5
+ A questão das bases morais que sustentam o Estado liberal moderno constitui um dos debates mais cruciais de nosso tempo. Em um mundo crescentemente secularizado, emerge com urgência a pergunta fundamental: sobre que alicerces morais pode edificar-se uma ordem política justa e duradoura?
6
+
7
+ Este interrogante torna-se ainda mais premente quando consideramos que o projeto da modernidade, em sua busca por autonomia racional, pretendeu emancipar-se de toda transcendência religiosa, almejando construir uma moralidade puramente secular e auto fundamentada. Contudo, as crises morais e políticas contemporâneas sugerem que tal empreendimento pode estar fadado ao fracasso.
8
+
9
+ O presente ensaio propõe-se a examinar criticamente esta problemática, tomando como fio condutor o célebre debate entre Jürgen Habermas e Joseph Ratzinger, posteriormente Papa Bento XVI, sobre os fundamentos da democracia liberal. Nossa tese central sustenta que a tradição católica oferece bases morais mais sólidas e consistentes para o Estado liberal do que as alternativas puramente seculares, demonstrando a insuficiência da razão autônoma para estabelecer os princípios éticos fundamentais da vida política.
10
+
11
+ ## O Encontro Histórico: Habermas e Ratzinger face à Dialética da Secularidade
12
+
13
+ Em janeiro de 2004, na Academia Católica da Baviera, ocorreu um dos diálogos intelectuais mais significativos do século XXI. De um lado, Jürgen Habermas, o grande teórico da modernidade secular e da ética discursiva; do outro, Joseph Ratzinger, então Prefeito da Congregação para a Doutrina da Fé, defensor da racionalidade cristã e futuro Papa Bento XVI.
14
+
15
+ O encontro não foi casual. Ambos os pensadores reconheciam que a modernidade ocidental atravessava uma crise profunda em seus fundamentos normativos. Habermas, outrora defensor de uma razão pós-metafísica completamente emancipada da religião, começara a reconhecer que a secularização radical poderia estar minando as próprias bases morais da democracia liberal.
16
+
17
+ Ratzinger, por sua vez, não se opunha ao projeto democrático moderno, mas questionava sua sustentabilidade quando divorciado de fundamentos transcendentes. Para o futuro Papa, a democracia liberal necessita de pressupostos morais que ela mesma não pode produzir, uma observação que ecoava a célebre formulação do jurista alemão Ernst-Wolfgang Böckenförde sobre o "dilema do Estado liberal".
18
+
19
+ ## A Insuficiência da Razão Secular: Crítica aos Fundamentos Puramente Imanentes
20
+
21
+ ### O Problema da Auto fundamentação Moral
22
+
23
+ O projeto moderno de uma ética secular enfrenta um obstáculo aparentemente intransponível: o problema da auto fundamentação. Como pode a razão humana, limitada e contingente, estabelecer princípios morais universais e absolutos? Esta dificuldade manifesta-se de forma particularmente aguda quando examinamos os três pilares sobre os quais o liberalismo secular pretende edificar sua moralidade: a intuição normativa, o processo democrático e a legalidade formal.
24
+
25
+ Alasdair MacIntyre, em sua obra magistral "Depois da Virtude", demonstra como o projeto iluminista de uma moralidade racional e secular resultou em um relativismo moral disfarçado. Segundo MacIntyre, ao abandonar a teleologia aristotélica e a síntese tomista, a modernidade perdeu o horizonte de significado que conferia consistência aos juízos morais.
26
+
27
+ A "intuição normativa", tão cara aos teóricos liberais contemporâneos, revela-se problemática quando submetida ao escrutínio filosófico rigoroso. Sobre que base podemos afirmar que cer
static/style.css ADDED
@@ -0,0 +1,364 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* static/style.css */
2
+
3
+ body {
4
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
5
+ line-height: 1.6;
6
+ background-color: #f4f4f9;
7
+ color: #333;
8
+ margin: 0;
9
+ padding: 20px;
10
+ }
11
+
12
+ .container {
13
+ max-width: 95%;
14
+ margin: auto;
15
+ background: #fff;
16
+ padding: 20px 30px;
17
+ border-radius: 8px;
18
+ box-shadow: 0 0 15px rgba(0,0,0,0.1);
19
+ }
20
+
21
+ .header-container {
22
+ display: flex;
23
+ justify-content: space-between;
24
+ align-items: flex-start;
25
+ margin-bottom: 20px;
26
+ }
27
+
28
+ .header-container p {
29
+ margin-top: -10px;
30
+ color: #6c757d;
31
+ }
32
+
33
+ .refresh-btn {
34
+ padding: 10px 15px;
35
+ font-size: 16px;
36
+ background-color: #28a745;
37
+ color: white;
38
+ border: none;
39
+ border-radius: 5px;
40
+ cursor: pointer;
41
+ transition: background-color 0.3s;
42
+ height: fit-content;
43
+ }
44
+
45
+ .refresh-btn:hover {
46
+ background-color: #218838;
47
+ }
48
+
49
+ h1, h2 {
50
+ color: #444;
51
+ }
52
+
53
+ textarea {
54
+ width: 100%;
55
+ padding: 10px;
56
+ border-radius: 5px;
57
+ border: 1px solid #ddd;
58
+ margin-bottom: 10px;
59
+ font-size: 16px;
60
+ box-sizing: border-box;
61
+ }
62
+
63
+ button[type="submit"] {
64
+ display: block;
65
+ width: 100%;
66
+ padding: 12px;
67
+ font-size: 18px;
68
+ background-color: #007bff;
69
+ color: white;
70
+ border: none;
71
+ border-radius: 5px;
72
+ cursor: pointer;
73
+ transition: background-color 0.3s;
74
+ }
75
+
76
+ button[type="submit"]:hover {
77
+ background-color: #0056b3;
78
+ }
79
+
80
+ .results-container {
81
+ display: flex;
82
+ justify-content: space-between;
83
+ gap: 20px;
84
+ margin-top: 30px;
85
+ /* Permite que as colunas alinhem no topo se tiverem tamanhos diferentes */
86
+ align-items: flex-start;
87
+ }
88
+
89
+ .result-column {
90
+ flex: 1;
91
+ min-width: 0;
92
+ background-color: #fafafa;
93
+ border: 1px solid #e0e0e0;
94
+ border-radius: 8px;
95
+ display: flex;
96
+ flex-direction: column;
97
+ }
98
+
99
+ .column-header {
100
+ display: flex;
101
+ justify-content: space-between;
102
+ align-items: center;
103
+ padding: 10px 15px;
104
+ background-color: #e9ecef;
105
+ border-bottom: 1px solid #e0e0e0;
106
+ border-radius: 8px 8px 0 0;
107
+ }
108
+
109
+ .column-header h2 {
110
+ margin: 0;
111
+ font-size: 1.2em;
112
+ }
113
+
114
+ .copy-btn {
115
+ padding: 5px 10px;
116
+ font-size: 12px;
117
+ background-color: #6c757d;
118
+ color: white;
119
+ border: none;
120
+ border-radius: 4px;
121
+ cursor: pointer;
122
+ }
123
+
124
+ .copy-btn:hover {
125
+ background-color: #5a6268;
126
+ }
127
+
128
+ /* CORREÇÃO: Removida a altura fixa e a rolagem individual */
129
+ .output-box {
130
+ padding: 15px;
131
+ white-space: pre-wrap;
132
+ word-wrap: break-word;
133
+ /* A altura agora será automática, baseada no conteúdo */
134
+ }
135
+
136
+ /* Estilos para o Loader e Barra de Progresso */
137
+ #loader-overlay {
138
+ position: fixed;
139
+ top: 0;
140
+ left: 0;
141
+ width: 100%;
142
+ height: 100%;
143
+ background-color: rgba(0, 0, 0, 0.8);
144
+ z-index: 1000;
145
+ display: flex;
146
+ justify-content: center;
147
+ align-items: center;
148
+ }
149
+
150
+ .loader-content {
151
+ display: flex;
152
+ flex-direction: column;
153
+ align-items: center;
154
+ color: white;
155
+ text-align: center;
156
+ }
157
+
158
+ .loader-spinner {
159
+ border: 8px solid #f3f3f3;
160
+ border-top: 8px solid #007bff;
161
+ border-radius: 50%;
162
+ width: 60px;
163
+ height: 60px;
164
+ animation: spin 1.5s linear infinite;
165
+ margin-bottom: 20px;
166
+ }
167
+
168
+ #loader-message {
169
+ font-size: 1.2em;
170
+ margin-bottom: 20px;
171
+ }
172
+
173
+ .progress-bar-container {
174
+ width: 300px;
175
+ height: 20px;
176
+ background-color: #555;
177
+ border-radius: 10px;
178
+ overflow: hidden;
179
+ }
180
+
181
+ .progress-bar {
182
+ width: 0%;
183
+ height: 100%;
184
+ background-color: #28a745;
185
+ border-radius: 10px;
186
+ transition: width 0.5s ease-in-out;
187
+ }
188
+
189
+ @keyframes spin {
190
+ 0% { transform: rotate(0deg); }
191
+ 100% { transform: rotate(360deg); }
192
+ }
193
+
194
+ /* Estilos para a caixa de erro */
195
+ .error-box {
196
+ background-color: #f8d7da;
197
+ color: #721c24;
198
+ padding: 15px;
199
+ margin-bottom: 20px;
200
+ border: 1px solid #f5c6cb;
201
+ border-radius: 5px;
202
+ position: relative;
203
+ }
204
+
205
+ .close-btn-error {
206
+ position: absolute;
207
+ top: 10px;
208
+ right: 15px;
209
+ font-size: 24px;
210
+ font-weight: bold;
211
+ color: #721c24;
212
+ cursor: pointer;
213
+ line-height: 1;
214
+ }
215
+
216
+ .close-btn-error:hover {
217
+ opacity: 0.7;
218
+ }
219
+
220
+ /* Estilos para o Seletor de Modo */
221
+ .controls-container {
222
+ display: flex;
223
+ align-items: center;
224
+ gap: 20px;
225
+ }
226
+
227
+ .mode-toggle {
228
+ display: flex;
229
+ align-items: center;
230
+ gap: 10px;
231
+ font-size: 14px;
232
+ color: #555;
233
+ }
234
+
235
+ .switch {
236
+ position: relative;
237
+ display: inline-block;
238
+ width: 50px;
239
+ height: 24px;
240
+ }
241
+
242
+ .switch input {
243
+ opacity: 0;
244
+ width: 0;
245
+ height: 0;
246
+ }
247
+
248
+ .slider {
249
+ position: absolute;
250
+ cursor: pointer;
251
+ top: 0;
252
+ left: 0;
253
+ right: 0;
254
+ bottom: 0;
255
+ background-color: #ccc;
256
+ transition: .4s;
257
+ }
258
+
259
+ .slider:before {
260
+ position: absolute;
261
+ content: "";
262
+ height: 18px;
263
+ width: 18px;
264
+ left: 3px;
265
+ bottom: 3px;
266
+ background-color: white;
267
+ transition: .4s;
268
+ }
269
+
270
+ input:checked + .slider {
271
+ background-color: #28a745;
272
+ }
273
+
274
+ input:checked + .slider:before {
275
+ transform: translateX(26px);
276
+ }
277
+
278
+ .slider.round {
279
+ border-radius: 24px;
280
+ }
281
+
282
+ .slider.round:before {
283
+ border-radius: 50%;
284
+ }
285
+
286
+ /* Estilos para tabelas e código gerados pelo Markdown */
287
+ .output-box table {
288
+ border-collapse: collapse;
289
+ width: 100%;
290
+ margin-bottom: 1em;
291
+ }
292
+ .output-box th, .output-box td {
293
+ border: 1px solid #ddd;
294
+ padding: 8px;
295
+ text-align: left;
296
+ }
297
+ .output-box th {
298
+ background-color: #f2f2f2;
299
+ }
300
+ .output-box pre {
301
+ background-color: #eee;
302
+ padding: 10px;
303
+ border-radius: 5px;
304
+ white-space: pre-wrap;
305
+ word-wrap: break-word;
306
+ }
307
+ .output-box code {
308
+ font-family: "Courier New", Courier, monospace;
309
+ }
310
+
311
+ /* static/style.css (ADICIONAR NO FINAL) */
312
+
313
+ /* Efeito visual ao arrastar arquivos sobre a textarea */
314
+ textarea.drag-over {
315
+ border-color: #007bff;
316
+ border-style: dashed;
317
+ box-shadow: 0 0 10px rgba(0, 123, 255, 0.5);
318
+ }
319
+
320
+ /* Container da lista de arquivos */
321
+ #file-list-container {
322
+ display: none; /* Começa escondido */
323
+ margin-top: -5px;
324
+ margin-bottom: 15px;
325
+ padding: 10px;
326
+ border: 1px solid #e0e0e0;
327
+ border-radius: 5px;
328
+ background-color: #f9f9f9;
329
+ }
330
+
331
+ #file-list-container p {
332
+ margin: 0 0 5px 0;
333
+ font-weight: bold;
334
+ font-size: 14px;
335
+ }
336
+
337
+ #file-list {
338
+ list-style-type: none;
339
+ padding: 0;
340
+ margin: 0;
341
+ }
342
+
343
+ #file-list li {
344
+ background-color: #e9ecef;
345
+ padding: 5px 10px;
346
+ border-radius: 4px;
347
+ margin-bottom: 5px;
348
+ font-size: 13px;
349
+ display: flex;
350
+ justify-content: space-between;
351
+ align-items: center;
352
+ }
353
+
354
+ .remove-file-btn {
355
+ cursor: pointer;
356
+ color: #dc3545;
357
+ font-weight: bold;
358
+ font-size: 18px;
359
+ margin-left: 10px;
360
+ }
361
+
362
+ .remove-file-btn:hover {
363
+ color: #a02430;
364
+ }
templates/index.html ADDED
@@ -0,0 +1,245 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="pt-br">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Sistema Multi-Agente de IA</title>
7
+ <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
8
+ </head>
9
+ <body>
10
+
11
+ <div id="loader-overlay" style="display: none;">
12
+ <div class="loader-content">
13
+ <div class="loader-spinner"></div>
14
+ <p id="loader-message">Processando sua solicitação...</p>
15
+ <div class="progress-bar-container">
16
+ <div id="progress-bar" class="progress-bar"></div>
17
+ </div>
18
+ </div>
19
+ </div>
20
+
21
+ <div class="container">
22
+ <div class="header-container">
23
+ <div>
24
+ <h1>Sistema Multi-Agente IA</h1>
25
+ <p>GROK ➔ Claude Sonnet ➔ Gemini</p>
26
+ </div>
27
+ <div class="controls-container">
28
+ <div class="mode-toggle">
29
+ <span>Modo Real</span>
30
+ <label class="switch"><input type="checkbox" id="mode-switch"><span class="slider round"></span></label>
31
+ <span>Modo Teste</span>
32
+ </div>
33
+ <button class="refresh-btn" onclick="window.location.href='/'" title="Limpar e começar de novo">Nova Consulta</button>
34
+ </div>
35
+ </div>
36
+
37
+ <div id="error-box-container"></div>
38
+
39
+ <div id="real-form-container">
40
+ <form id="request-form-real">
41
+ <label for="solicitacao_usuario">Digite sua solicitação (ou arraste arquivos aqui):</label>
42
+ <textarea name="solicitacao_usuario" id="solicitacao_usuario" rows="8" required></textarea>
43
+ <div id="file-list-container"><p>Arquivos Anexados:</p><ul id="file-list"></ul></div>
44
+ <button type="submit">Processar com IA</button>
45
+ </form>
46
+ </div>
47
+
48
+ <div id="mock-form-container" style="display: none;">
49
+ <form id="request-form-mock">
50
+ <label for="mock_text">Cole o texto de simulação aqui:</label>
51
+ <textarea name="mock_text" id="mock_text" rows="10" required></textarea>
52
+ <button type="submit">Simular Resposta</button>
53
+ </form>
54
+ </div>
55
+
56
+ <div id="results-container" class="results-container" style="display: none;">
57
+ <div class="result-column">
58
+ <div class="column-header"><h2>GROK</h2><button class="copy-btn" onclick="copyToClipboard('grok-output')">Copiar</button></div>
59
+ <div class="output-box" id="grok-output"></div>
60
+ </div>
61
+ <div class="result-column">
62
+ <div class="column-header"><h2>Claude Sonnet</h2><button class="copy-btn" onclick="copyToClipboard('sonnet-output')">Copiar</button></div>
63
+ <div class="output-box" id="sonnet-output"></div>
64
+ </div>
65
+ <div class="result-column">
66
+ <div class="column-header"><h2>Gemini</h2><button class="copy-btn" onclick="copyToClipboard('gemini-output')">Copiar</button></div>
67
+ <div class="output-box" id="gemini-output"></div>
68
+ </div>
69
+ </div>
70
+ </div>
71
+
72
+ <script>
73
+ // O JavaScript continua o mesmo, pois ele preenche os resultados
74
+ // pelos IDs, que não mudaram. Nenhuma alteração é necessária aqui.
75
+ const modeSwitch = document.getElementById('mode-switch');
76
+ const realContainer = document.getElementById('real-form-container');
77
+ const mockContainer = document.getElementById('mock-form-container');
78
+ const loader = document.getElementById('loader-overlay');
79
+ const loaderMessage = document.getElementById('loader-message');
80
+ const progressBar = document.getElementById('progress-bar');
81
+ const resultsContainer = document.getElementById('results-container');
82
+ const errorContainer = document.getElementById('error-box-container');
83
+ const textarea = document.getElementById('solicitacao_usuario');
84
+ const fileList = document.getElementById('file-list');
85
+ let attachedFiles = [];
86
+
87
+ modeSwitch.addEventListener('change', function() {
88
+ realContainer.style.display = this.checked ? 'none' : 'block';
89
+ mockContainer.style.display = this.checked ? 'block' : 'none';
90
+ });
91
+
92
+ ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
93
+ textarea.addEventListener(eventName, preventDefaults, false);
94
+ document.body.addEventListener(eventName, preventDefaults, false);
95
+ });
96
+ ['dragenter', 'dragover'].forEach(eventName => {
97
+ textarea.addEventListener(eventName, () => textarea.classList.add('drag-over'), false);
98
+ });
99
+ ['dragleave', 'drop'].forEach(eventName => {
100
+ textarea.addEventListener(eventName, () => textarea.classList.remove('drag-over'), false);
101
+ });
102
+ textarea.addEventListener('drop', handleDrop, false);
103
+
104
+ function preventDefaults(e) { e.preventDefault(); e.stopPropagation(); }
105
+ function handleDrop(e) { handleFiles(e.dataTransfer.files); }
106
+
107
+ function handleFiles(files) {
108
+ [...files].forEach(file => {
109
+ const allowedTypes = ['application/pdf', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'text/plain'];
110
+ if (!allowedTypes.includes(file.type)) {
111
+ showError(`Formato de arquivo não suportado: ${file.name}`);
112
+ return;
113
+ }
114
+ if (file.size > 100 * 1024 * 1024) { // 100MB
115
+ showError(`Arquivo muito grande (max 100MB): ${file.name}`);
116
+ return;
117
+ }
118
+ attachedFiles.push(file);
119
+ });
120
+ updateFileList();
121
+ }
122
+
123
+ function updateFileList() {
124
+ fileList.innerHTML = '';
125
+ document.getElementById('file-list-container').style.display = attachedFiles.length > 0 ? 'block' : 'none';
126
+ attachedFiles.forEach((file, index) => {
127
+ const li = document.createElement('li');
128
+ li.textContent = `${file.name} (${(file.size / 1024 / 1024).toFixed(2)} MB)`;
129
+ const removeBtn = document.createElement('span');
130
+ removeBtn.textContent = '×';
131
+ removeBtn.className = 'remove-file-btn';
132
+ removeBtn.onclick = () => {
133
+ attachedFiles.splice(index, 1);
134
+ updateFileList();
135
+ };
136
+ li.appendChild(removeBtn);
137
+ fileList.appendChild(li);
138
+ });
139
+ }
140
+
141
+ document.getElementById('request-form-real').addEventListener('submit', handleFormSubmit);
142
+ document.getElementById('request-form-mock').addEventListener('submit', handleFormSubmit);
143
+
144
+ async function handleFormSubmit(event) {
145
+ event.preventDefault();
146
+
147
+ errorContainer.innerHTML = '';
148
+ resultsContainer.style.display = 'none';
149
+ document.querySelectorAll('.output-box').forEach(box => box.innerHTML = '');
150
+
151
+ loaderMessage.textContent = 'Iniciando conexão...';
152
+ progressBar.style.width = '0%';
153
+ loader.style.display = 'flex';
154
+
155
+ const formData = new FormData();
156
+ if (modeSwitch.checked) {
157
+ formData.append('mode', 'test');
158
+ formData.append('mock_text', document.getElementById('mock_text').value);
159
+ } else {
160
+ formData.append('mode', 'real');
161
+ formData.append('solicitacao', document.getElementById('solicitacao_usuario').value);
162
+ attachedFiles.forEach(file => {
163
+ formData.append('files', file);
164
+ });
165
+ }
166
+
167
+ try {
168
+ const response = await fetch('/process', {
169
+ method: 'POST',
170
+ body: formData,
171
+ });
172
+
173
+ if (!response.ok || !response.body) {
174
+ throw new Error(`Erro na resposta do servidor: ${response.statusText}`);
175
+ }
176
+
177
+ const reader = response.body.getReader();
178
+ const decoder = new TextDecoder();
179
+
180
+ while (true) {
181
+ const { done, value } = await reader.read();
182
+ if (done) break;
183
+
184
+ const chunk = decoder.decode(value, { stream: true });
185
+ const lines = chunk.split('\n\n');
186
+
187
+ lines.forEach(line => {
188
+ if (line.startsWith('data: ')) {
189
+ const jsonData = line.substring(6);
190
+ if (jsonData.trim()) {
191
+ try {
192
+ const data = JSON.parse(jsonData);
193
+ processStreamData(data);
194
+ } catch (e) {
195
+ console.error("Erro ao parsear JSON do stream:", jsonData);
196
+ }
197
+ }
198
+ }
199
+ });
200
+ }
201
+ } catch (error) {
202
+ showError('A conexão com o servidor falhou. Verifique o console para detalhes.');
203
+ loader.style.display = 'none';
204
+ console.error("Fetch Error:", error);
205
+ }
206
+ }
207
+
208
+ function processStreamData(data) {
209
+ if (data.error) {
210
+ showError(data.error);
211
+ loader.style.display = 'none';
212
+ return;
213
+ }
214
+
215
+ loaderMessage.textContent = data.message;
216
+ progressBar.style.width = data.progress + '%';
217
+
218
+ if (data.partial_result) {
219
+ resultsContainer.style.display = 'flex';
220
+ const targetBox = document.getElementById(data.partial_result.id);
221
+ if (targetBox) {
222
+ targetBox.innerHTML = data.partial_result.content;
223
+ }
224
+ }
225
+
226
+ if (data.done) {
227
+ setTimeout(() => {
228
+ loader.style.display = 'none';
229
+ }, 1000);
230
+ }
231
+ }
232
+
233
+ function showError(message) {
234
+ errorContainer.innerHTML = `<div class="error-box"><strong>Erro:</strong> ${message}<span class="close-btn-error" onclick="this.parentElement.style.display='none';" title="Fechar">&times;</span></div>`;
235
+ }
236
+
237
+ function copyToClipboard(elementId) {
238
+ const element = document.getElementById(elementId);
239
+ navigator.clipboard.writeText(element.innerText).then(() => {
240
+ alert('Texto copiado!');
241
+ });
242
+ }
243
+ </script>
244
+ </body>
245
+ </html>