Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
@@ -3,25 +3,161 @@ import os
|
|
3 |
import json
|
4 |
import re
|
5 |
import gradio as gr
|
6 |
-
from typing import Dict, Any
|
|
|
7 |
|
8 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
9 |
HF_TOKEN = os.getenv("HF_TOKEN")
|
10 |
if not HF_TOKEN:
|
11 |
raise ValueError("Token HF_TOKEN não encontrado nas variáveis de ambiente")
|
12 |
|
13 |
-
# Modelos disponíveis via API (testados e funcionais)
|
14 |
MODELS = {
|
15 |
"Phi-3 Mini (Microsoft)": "microsoft/Phi-3-mini-4k-instruct",
|
16 |
"Mistral 7B": "mistralai/Mistral-7B-Instruct-v0.3",
|
17 |
"Zephyr 7B": "HuggingFaceH4/zephyr-7b-beta",
|
18 |
-
"Llama 3.2 3B (Meta)": "meta-llama/Llama-3.2-3B-Instruct",
|
19 |
-
"DeepSeek-Coder-V2": "deepseek-ai/DeepSeek-Coder-V2-Lite-Instruct",
|
20 |
}
|
21 |
-
|
22 |
-
# Modelo padrão (mais confiável)
|
23 |
DEFAULT_MODEL = "Phi-3 Mini (Microsoft)"
|
24 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
25 |
class HuggingFaceAPIClient:
|
26 |
def __init__(self, token: str):
|
27 |
self.token = token
|
@@ -29,11 +165,10 @@ class HuggingFaceAPIClient:
|
|
29 |
"Authorization": f"Bearer {token}",
|
30 |
"Content-Type": "application/json"
|
31 |
}
|
32 |
-
|
33 |
-
def query_model(self, model_name: str, messages: list, max_tokens: int =
|
34 |
"""Faz requisição para a API do Hugging Face"""
|
35 |
url = f"https://api-inference.huggingface.co/models/{model_name}/v1/chat/completions"
|
36 |
-
|
37 |
payload = {
|
38 |
"model": model_name,
|
39 |
"messages": messages,
|
@@ -42,363 +177,233 @@ class HuggingFaceAPIClient:
|
|
42 |
"top_p": 0.9,
|
43 |
"stream": False
|
44 |
}
|
45 |
-
|
46 |
try:
|
47 |
response = requests.post(url, headers=self.headers, json=payload, timeout=9999)
|
48 |
-
|
49 |
if response.status_code == 200:
|
50 |
result = response.json()
|
51 |
return result["choices"][0]["message"]["content"]
|
52 |
else:
|
53 |
-
# Fallback para API de texto simples se a API de chat não funcionar
|
54 |
return self._fallback_text_generation(model_name, messages, max_tokens)
|
55 |
-
|
56 |
except Exception as e:
|
57 |
return f"Erro na API: {str(e)}"
|
58 |
-
|
59 |
def _fallback_text_generation(self, model_name: str, messages: list, max_tokens: int) -> str:
|
60 |
-
"""Fallback usando API de geração de texto simples"""
|
61 |
url = f"https://api-inference.huggingface.co/models/{model_name}"
|
62 |
-
|
63 |
-
# Converte mensagens para prompt simples
|
64 |
prompt = self._messages_to_prompt(messages)
|
65 |
-
|
66 |
payload = {
|
67 |
"inputs": prompt,
|
68 |
"parameters": {
|
69 |
-
"max_new_tokens": max_tokens,
|
70 |
-
"
|
71 |
-
"top_p": 0.9,
|
72 |
-
"do_sample": True,
|
73 |
-
"return_full_text": False
|
74 |
},
|
75 |
-
"options": {
|
76 |
-
"wait_for_model": True, # Espera modelo carregar
|
77 |
-
"use_cache": False
|
78 |
-
}
|
79 |
}
|
80 |
-
|
81 |
try:
|
82 |
response = requests.post(url, headers=self.headers, json=payload, timeout=9999)
|
83 |
-
|
84 |
if response.status_code == 200:
|
85 |
result = response.json()
|
86 |
if isinstance(result, list) and len(result) > 0:
|
87 |
generated_text = result[0].get("generated_text", "")
|
88 |
-
# Limpa o texto gerado
|
89 |
if generated_text:
|
90 |
-
# Remove o prompt original da resposta
|
91 |
if "Assistente: " in generated_text:
|
92 |
parts = generated_text.split("Assistente: ")
|
93 |
-
if len(parts) > 1:
|
94 |
-
return parts[-1].strip()
|
95 |
return generated_text.strip()
|
96 |
return "Resposta vazia"
|
97 |
elif isinstance(result, dict):
|
98 |
-
if "error" in result:
|
99 |
-
|
100 |
-
elif "generated_text" in result:
|
101 |
-
return result["generated_text"].strip()
|
102 |
return "Formato de resposta inesperado"
|
103 |
-
elif response.status_code == 404:
|
104 |
-
|
105 |
-
elif response.status_code ==
|
106 |
-
|
107 |
-
elif response.status_code == 429:
|
108 |
-
return "⚠️ Muitas requisições. Aguarde um momento antes de tentar novamente."
|
109 |
-
else:
|
110 |
-
return f"Erro HTTP {response.status_code}: {response.text[:200]}..."
|
111 |
-
|
112 |
except requests.Timeout:
|
113 |
-
return "⏰ Timeout - Modelo demorou muito para responder.
|
114 |
except Exception as e:
|
115 |
return f"Erro na requisição: {str(e)}"
|
116 |
-
|
117 |
def _messages_to_prompt(self, messages: list) -> str:
|
118 |
-
"""Converte mensagens para formato de prompt"""
|
119 |
prompt = ""
|
120 |
for msg in messages:
|
121 |
-
|
122 |
-
prompt += f"Sistema: {msg['content']}\n\n"
|
123 |
-
elif msg["role"] == "user":
|
124 |
-
prompt += f"Usuário: {msg['content']}\n\n"
|
125 |
-
elif msg["role"] == "assistant":
|
126 |
-
prompt += f"Assistente: {msg['content']}\n\n"
|
127 |
-
|
128 |
prompt += "Assistente: "
|
129 |
return prompt
|
130 |
|
131 |
# Inicializar cliente da API
|
132 |
api_client = HuggingFaceAPIClient(HF_TOKEN)
|
133 |
|
|
|
|
|
|
|
|
|
134 |
def formatar_resposta_com_codigo(resposta: str) -> str:
|
135 |
-
"""Formata a resposta destacando códigos em blocos separados"""
|
136 |
-
if not resposta:
|
137 |
-
return resposta
|
138 |
-
|
139 |
-
# Detecta blocos de código com ```
|
140 |
resposta_formatada = re.sub(
|
141 |
r'```(\w+)?\n(.*?)\n```',
|
142 |
r'<div style="background-color: #f8f9fa; color: #1a1a1a; border: 1px solid #e9ecef; border-radius: 8px; padding: 15px; margin: 10px 0; font-family: Monaco, Consolas, monospace; overflow-x: auto;"><strong style="color: #1a1a1a;">💻 Código:</strong><br><pre style="color: #1a1a1a; margin: 5px 0; white-space: pre-wrap; word-wrap: break-word;"><code>\2</code></pre></div>',
|
143 |
-
resposta,
|
144 |
-
flags=re.DOTALL
|
145 |
)
|
146 |
-
|
147 |
-
# Detecta código inline com `
|
148 |
resposta_formatada = re.sub(
|
149 |
r'`([^`]+)`',
|
150 |
r'<code style="background-color: #f1f3f4; color: #1a1a1a; padding: 2px 4px; border-radius: 4px; font-family: Monaco, Consolas, monospace;">\1</code>',
|
151 |
resposta_formatada
|
152 |
)
|
153 |
-
|
154 |
-
# Adiciona quebras de linha para melhor visualização
|
155 |
-
resposta_formatada = resposta_formatada.replace('\n\n', '<br><br>')
|
156 |
resposta_formatada = resposta_formatada.replace('\n', '<br>')
|
157 |
-
|
158 |
-
# Destaca títulos/seções
|
159 |
resposta_formatada = re.sub(
|
160 |
r'^\*\*(.*?)\*\*',
|
161 |
r'<h3 style="color: #1a1a1a; margin-top: 20px; margin-bottom: 10px;">\1</h3>',
|
162 |
-
resposta_formatada,
|
163 |
-
flags=re.MULTILINE
|
164 |
)
|
165 |
-
|
166 |
return resposta_formatada
|
167 |
|
168 |
def responder_como_aldo(pergunta: str, modelo_escolhido: str = DEFAULT_MODEL) -> str:
|
169 |
-
"""Função principal para gerar respostas
|
170 |
if not pergunta.strip():
|
171 |
return "Por favor, faça uma pergunta."
|
172 |
-
|
173 |
try:
|
174 |
-
#
|
175 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
176 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
177 |
messages = [
|
178 |
-
{
|
179 |
-
|
180 |
-
"content": "Você é o professor Dr. Aldo Henrique, especialista em C, Java, desenvolvimento web e inteligência artificial. Sempre responda primeiro a explicação e depois modestre o código. Responda com clareza, profundidade e tom acadêmico, como um professor experiente. Evite respostas genéricas e forneça exemplos práticos quando aplicável. Foque em explicar e não em só mostrar o resultado. Responda sempre em português brasileiro. Use blocos de código formatados com ``` quando apresentar código. Não responda nada se a pergunta não for sobre o universo de programação e tecnologia."
|
181 |
-
},
|
182 |
-
{
|
183 |
-
"role": "user",
|
184 |
-
"content": pergunta_completa
|
185 |
-
}
|
186 |
]
|
187 |
|
188 |
-
# Obter o nome real do modelo
|
189 |
model_name = MODELS.get(modelo_escolhido, MODELS[DEFAULT_MODEL])
|
|
|
190 |
|
191 |
-
# Fazer requisição
|
192 |
-
resposta = api_client.query_model(model_name, messages, max_tokens=800)
|
193 |
-
|
194 |
-
# Limpar resposta se necessário
|
195 |
if resposta.startswith("Assistente: "):
|
196 |
resposta = resposta.replace("Assistente: ", "")
|
197 |
-
|
198 |
-
# Formatar resposta com código
|
199 |
resposta_formatada = formatar_resposta_com_codigo(resposta.strip())
|
200 |
-
|
201 |
return resposta_formatada
|
202 |
|
203 |
except Exception as e:
|
204 |
return f"Erro ao processar sua pergunta: {str(e)}"
|
205 |
|
|
|
206 |
def verificar_modelo_disponivel(model_name: str) -> str:
|
207 |
-
"""Verifica se um modelo está disponível na API"""
|
208 |
try:
|
209 |
url = f"https://api-inference.huggingface.co/models/{model_name}"
|
210 |
headers = {"Authorization": f"Bearer {HF_TOKEN}"}
|
211 |
-
|
212 |
-
# Teste simples
|
213 |
-
payload = {
|
214 |
-
"inputs": "Hello",
|
215 |
-
"parameters": {"max_new_tokens": 5}
|
216 |
-
}
|
217 |
-
|
218 |
response = requests.post(url, headers=headers, json=payload, timeout=9999)
|
219 |
-
|
220 |
-
|
221 |
-
|
222 |
-
|
223 |
-
return "❌ Não encontrado"
|
224 |
-
elif response.status_code == 503:
|
225 |
-
return "⏳ Carregando..."
|
226 |
-
else:
|
227 |
-
return f"⚠️ Status {response.status_code}"
|
228 |
-
|
229 |
except Exception as e:
|
230 |
return f"❌ Erro: {str(e)[:50]}..."
|
231 |
|
232 |
def testar_todos_modelos():
|
233 |
-
"""Testa todos os modelos disponíveis"""
|
234 |
resultados = []
|
235 |
for nome, modelo in MODELS.items():
|
236 |
status = verificar_modelo_disponivel(modelo)
|
237 |
resultados.append(f"{nome}: {status}")
|
238 |
return "\n".join(resultados)
|
239 |
|
240 |
-
# CSS
|
241 |
css_customizado = """
|
242 |
-
.gradio-container {
|
243 |
-
|
244 |
-
|
245 |
-
|
246 |
-
}
|
247 |
-
|
248 |
-
.
|
249 |
-
font-size: 14px !important;
|
250 |
-
line-height: 1.5 !important;
|
251 |
-
}
|
252 |
-
|
253 |
-
.resposta-container {
|
254 |
-
background-color: #ffffff !important;
|
255 |
-
color: #1a1a1a !important;
|
256 |
-
border: 1px solid #e0e0e0 !important;
|
257 |
-
border-radius: 20px !important;
|
258 |
-
padding: 20px !important;
|
259 |
-
margin: 20px 0 !important;
|
260 |
-
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05) !important;
|
261 |
-
}
|
262 |
-
|
263 |
-
.resposta-container pre code {
|
264 |
-
color: #1a1a1a !important;
|
265 |
-
background-color: #f8f9fa !important;
|
266 |
-
}
|
267 |
-
|
268 |
-
.pergunta-container {
|
269 |
-
background-color: #f0f8ff !important;
|
270 |
-
border-radius: 8px !important;
|
271 |
-
padding: 15px !important;
|
272 |
-
}
|
273 |
-
|
274 |
-
.titulo-principal {
|
275 |
-
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
|
276 |
-
color: white !important;
|
277 |
-
padding: 20px !important;
|
278 |
-
border-radius: 10px !important;
|
279 |
-
margin-bottom: 20px !important;
|
280 |
-
text-align: center !important;
|
281 |
-
}
|
282 |
-
|
283 |
-
.modelo-dropdown {
|
284 |
-
margin-bottom: 15px !important;
|
285 |
-
}
|
286 |
"""
|
287 |
|
288 |
# Interface Gradio
|
289 |
-
with gr.Blocks(
|
290 |
-
title="Dr. Aldo Henrique - API Externa",
|
291 |
-
theme=gr.themes.Soft(),
|
292 |
-
css=css_customizado
|
293 |
-
) as interface:
|
294 |
-
|
295 |
-
# Cabeçalho estilizado
|
296 |
gr.HTML("""
|
297 |
<div class="titulo-principal">
|
298 |
-
<h1>🤖 Dr. Aldo Henrique - Especialista em TI</h1>
|
299 |
-
<p style="font-size: 14px; opacity: 0.9;">
|
300 |
</div>
|
301 |
""")
|
302 |
|
303 |
with gr.Row():
|
304 |
-
# Coluna da esquerda - Entrada
|
305 |
with gr.Column(scale=2):
|
306 |
gr.Markdown("### 📝 Faça sua pergunta:")
|
307 |
-
entrada = gr.Textbox(
|
308 |
-
|
309 |
-
placeholder="Digite sua pergunta aqui.",
|
310 |
-
lines=6,
|
311 |
-
elem_classes="pergunta-container"
|
312 |
-
)
|
313 |
-
|
314 |
-
modelo_select = gr.Dropdown(
|
315 |
-
choices=list(MODELS.keys()),
|
316 |
-
value=DEFAULT_MODEL,
|
317 |
-
label="🧠 Selecione o Modelo de IA",
|
318 |
-
info="Escolha o modelo para responder sua pergunta",
|
319 |
-
elem_classes="modelo-dropdown"
|
320 |
-
)
|
321 |
|
322 |
with gr.Row():
|
323 |
-
botao_perguntar = gr.Button(
|
324 |
-
|
325 |
-
|
326 |
-
size="lg"
|
327 |
-
)
|
328 |
-
botao_testar = gr.Button(
|
329 |
-
"🔍 Testar Modelos",
|
330 |
-
variant="secondary"
|
331 |
-
)
|
332 |
-
|
333 |
-
# Coluna da direita - Saída
|
334 |
with gr.Column(scale=3):
|
335 |
gr.Markdown("### 💬 Resposta do Dr. Aldo Henrique:")
|
336 |
-
saida = gr.HTML(
|
337 |
-
|
338 |
-
|
339 |
-
|
340 |
-
|
341 |
-
|
342 |
-
|
343 |
with gr.Accordion("📚 Exemplos de Perguntas", open=False):
|
344 |
gr.Examples(
|
345 |
examples=[
|
346 |
["Como implementar uma lista ligada em C com todas as operações básicas?", DEFAULT_MODEL],
|
347 |
-
["
|
348 |
-
["
|
349 |
-
["Mostre como criar uma API REST completa com Spring Boot", "Zephyr 7B"]
|
350 |
],
|
351 |
inputs=[entrada, modelo_select]
|
352 |
)
|
353 |
|
354 |
-
# Status da API (expandível)
|
355 |
with gr.Accordion("🔧 Status da API", open=False):
|
356 |
-
status_api = gr.Textbox(
|
357 |
-
label="Status dos Modelos",
|
358 |
-
interactive=False,
|
359 |
-
lines=8
|
360 |
-
)
|
361 |
|
362 |
-
# Informações adicionais
|
363 |
with gr.Accordion("ℹ️ Informações", open=False):
|
364 |
gr.Markdown("""
|
365 |
### Sobre o Dr. Aldo Henrique:
|
366 |
- **Especialidade**: Linguagens C, Java, Desenvolvimento Web, Inteligência Artificial
|
367 |
-
- **
|
368 |
-
- **Abordagem**: Acadêmica e profissional
|
369 |
-
|
370 |
-
### Modelos Disponíveis:
|
371 |
-
- **Phi-3 Mini**: Modelo compacto e eficiente da Microsoft
|
372 |
-
- **Llama 3.2 3B**: Versão menor do modelo da Meta
|
373 |
-
- **Mistral 7B**: Modelo francês de alta qualidade
|
374 |
-
- **Zephyr 7B**: Versão otimizada para diálogo
|
375 |
-
|
376 |
### Dicas para melhores respostas:
|
377 |
-
-
|
378 |
-
-
|
379 |
-
- Peça exemplos práticos quando necessário
|
380 |
""")
|
381 |
-
|
382 |
# Eventos
|
383 |
-
botao_perguntar.click(
|
384 |
-
|
385 |
-
|
386 |
-
outputs=saida,
|
387 |
-
show_progress=True
|
388 |
-
)
|
389 |
-
|
390 |
-
botao_testar.click(
|
391 |
-
fn=testar_todos_modelos,
|
392 |
-
outputs=status_api,
|
393 |
-
show_progress=True
|
394 |
-
)
|
395 |
|
396 |
# Lançar aplicação
|
397 |
if __name__ == "__main__":
|
398 |
-
print("🚀 Iniciando Dr. Aldo Henrique com
|
|
|
|
|
|
|
|
|
399 |
print(f"🔑 Token HF encontrado: {HF_TOKEN[:8]}...")
|
400 |
-
print("🌐 Interface
|
401 |
-
print("💻 Formatação especial para códigos implementada!")
|
402 |
|
403 |
interface.launch(
|
404 |
server_name="0.0.0.0",
|
|
|
3 |
import json
|
4 |
import re
|
5 |
import gradio as gr
|
6 |
+
from typing import Dict, Any, List, Optional
|
7 |
+
import pickle
|
8 |
|
9 |
+
# --- Novas importações para o RAG ---
|
10 |
+
import time
|
11 |
+
from bs4 import BeautifulSoup
|
12 |
+
from urllib.parse import urljoin, urlparse
|
13 |
+
from langchain.text_splitter import RecursiveCharacterTextSplitter
|
14 |
+
from langchain.vectorstores import FAISS
|
15 |
+
from langchain.embeddings import HuggingFaceEmbeddings
|
16 |
+
|
17 |
+
# --- Configuração do RAG ---
|
18 |
+
BLOG_URL = "https://aldohenrique.com.br/"
|
19 |
+
VECTOR_STORE_PATH = "faiss_index_store.pkl"
|
20 |
+
PROCESSED_URLS_PATH = "processed_urls.pkl"
|
21 |
+
|
22 |
+
# --- Configuração da API Hugging Face ---
|
23 |
HF_TOKEN = os.getenv("HF_TOKEN")
|
24 |
if not HF_TOKEN:
|
25 |
raise ValueError("Token HF_TOKEN não encontrado nas variáveis de ambiente")
|
26 |
|
|
|
27 |
MODELS = {
|
28 |
"Phi-3 Mini (Microsoft)": "microsoft/Phi-3-mini-4k-instruct",
|
29 |
"Mistral 7B": "mistralai/Mistral-7B-Instruct-v0.3",
|
30 |
"Zephyr 7B": "HuggingFaceH4/zephyr-7b-beta",
|
31 |
+
"Llama 3.2 3B (Meta)": "meta-llama/Llama-3.2-3B-Instruct",
|
32 |
+
"DeepSeek-Coder-V2": "deepseek-ai/DeepSeek-Coder-V2-Lite-Instruct",
|
33 |
}
|
|
|
|
|
34 |
DEFAULT_MODEL = "Phi-3 Mini (Microsoft)"
|
35 |
|
36 |
+
# --- Variáveis Globais para o RAG ---
|
37 |
+
vector_store: Optional[FAISS] = None
|
38 |
+
|
39 |
+
# ==============================================================================
|
40 |
+
# SEÇÃO RAG: FUNÇÕES PARA CRAWLING, EMBEDDING E ARMAZENAMENTO
|
41 |
+
# ==============================================================================
|
42 |
+
|
43 |
+
def get_all_blog_links(url: str, processed_urls: set) -> set:
|
44 |
+
"""Navega pelo blog para encontrar todos os links de posts e páginas."""
|
45 |
+
links_to_visit = {url}
|
46 |
+
visited_links = set()
|
47 |
+
|
48 |
+
while links_to_visit:
|
49 |
+
current_url = links_to_visit.pop()
|
50 |
+
if current_url in visited_links:
|
51 |
+
continue
|
52 |
+
|
53 |
+
try:
|
54 |
+
response = requests.get(current_url, timeout=10)
|
55 |
+
response.raise_for_status()
|
56 |
+
soup = BeautifulSoup(response.content, 'html.parser')
|
57 |
+
visited_links.add(current_url)
|
58 |
+
print(f"Visitando: {current_url}")
|
59 |
+
|
60 |
+
for link in soup.find_all('a', href=True):
|
61 |
+
href = link['href']
|
62 |
+
full_url = urljoin(url, href)
|
63 |
+
# Garante que estamos no mesmo domínio e não é um link de âncora
|
64 |
+
if urlparse(full_url).netloc == urlparse(url).netloc and full_url not in visited_links:
|
65 |
+
links_to_visit.add(full_url)
|
66 |
+
except requests.RequestException as e:
|
67 |
+
print(f"Erro ao acessar {current_url}: {e}")
|
68 |
+
|
69 |
+
# Filtra apenas as páginas que parecem ser posts ou páginas de conteúdo
|
70 |
+
final_links = {link for link in visited_links if '/tag/' not in link and '/category/' not in link and '?' not in link}
|
71 |
+
return final_links
|
72 |
+
|
73 |
+
|
74 |
+
def scrape_text_from_url(url: str) -> str:
|
75 |
+
"""Extrai o texto principal (de artigos) de uma URL."""
|
76 |
+
try:
|
77 |
+
response = requests.get(url, timeout=10)
|
78 |
+
soup = BeautifulSoup(response.content, 'html.parser')
|
79 |
+
# Tenta encontrar a tag <article> ou <main> que geralmente contém o conteúdo principal
|
80 |
+
main_content = soup.find('article') or soup.find('main')
|
81 |
+
if main_content:
|
82 |
+
return main_content.get_text(separator='\n', strip=True)
|
83 |
+
return ""
|
84 |
+
except Exception as e:
|
85 |
+
print(f"Erro ao raspar {url}: {e}")
|
86 |
+
return ""
|
87 |
+
|
88 |
+
def build_and_save_vector_store() -> str:
|
89 |
+
"""
|
90 |
+
Função principal do RAG: raspa o blog, cria chunks, gera embeddings e salva o vector store.
|
91 |
+
Esta é a nossa função de "treino".
|
92 |
+
"""
|
93 |
+
global vector_store
|
94 |
+
start_time = time.time()
|
95 |
+
|
96 |
+
print("Iniciando o processo de retreino do RAG...")
|
97 |
+
processed_urls = set()
|
98 |
+
|
99 |
+
# 1. Obter todos os links do blog
|
100 |
+
all_links = get_all_blog_links(BLOG_URL, processed_urls)
|
101 |
+
print(f"Encontrados {len(all_links)} links para processar.")
|
102 |
+
|
103 |
+
# 2. Raspar o texto de cada link
|
104 |
+
all_texts = [scrape_text_from_url(link) for link in all_links if link not in processed_urls]
|
105 |
+
all_texts = [text for text in all_texts if text] # Remove textos vazios
|
106 |
+
print(f"Textos extraídos de {len(all_texts)} novas páginas.")
|
107 |
+
|
108 |
+
if not all_texts:
|
109 |
+
return "Nenhum novo conteúdo encontrado para treinar."
|
110 |
+
|
111 |
+
# 3. Dividir os textos em chunks
|
112 |
+
text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=150)
|
113 |
+
chunks = text_splitter.create_documents(all_texts)
|
114 |
+
print(f"Textos divididos em {len(chunks)} chunks.")
|
115 |
+
|
116 |
+
# 4. Criar embeddings e o vector store (FAISS)
|
117 |
+
print("Carregando modelo de embedding...")
|
118 |
+
embeddings_model = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")
|
119 |
+
|
120 |
+
print("Criando o vector store com FAISS...")
|
121 |
+
vector_store = FAISS.from_documents(chunks, embeddings_model)
|
122 |
+
|
123 |
+
# 5. Salvar o vector store e as URLs processadas em disco
|
124 |
+
with open(VECTOR_STORE_PATH, "wb") as f:
|
125 |
+
pickle.dump(vector_store, f)
|
126 |
+
|
127 |
+
with open(PROCESSED_URLS_PATH, "wb") as f:
|
128 |
+
pickle.dump(all_links, f)
|
129 |
+
|
130 |
+
end_time = time.time()
|
131 |
+
return f"✅ Retreino do RAG concluído em {end_time - start_time:.2f} segundos. {len(chunks)} chunks de texto processados."
|
132 |
+
|
133 |
+
def load_vector_store():
|
134 |
+
"""Carrega o vector store do arquivo, se existir."""
|
135 |
+
global vector_store
|
136 |
+
if os.path.exists(VECTOR_STORE_PATH):
|
137 |
+
print(f"Carregando vector store existente de '{VECTOR_STORE_PATH}'...")
|
138 |
+
with open(VECTOR_STORE_PATH, "rb") as f:
|
139 |
+
vector_store = pickle.load(f)
|
140 |
+
print("Vector store carregado com sucesso.")
|
141 |
+
else:
|
142 |
+
print("Nenhum vector store encontrado. É necessário treinar o modelo.")
|
143 |
+
# Inicia o treino automaticamente se não houver um índice
|
144 |
+
build_and_save_vector_store()
|
145 |
+
|
146 |
+
def retrieve_context_from_blog(query: str, k: int = 3) -> str:
|
147 |
+
"""Busca no vector store por chunks de texto similares à pergunta."""
|
148 |
+
if vector_store:
|
149 |
+
try:
|
150 |
+
results = vector_store.similarity_search(query, k=k)
|
151 |
+
context = "\n\n---\n\n".join([doc.page_content for doc in results])
|
152 |
+
return context
|
153 |
+
except Exception as e:
|
154 |
+
return f"Erro ao buscar contexto: {e}"
|
155 |
+
return ""
|
156 |
+
|
157 |
+
# ==============================================================================
|
158 |
+
# SEÇÃO API CLIENT: CÓDIGO ORIGINAL PARA CHAMAR A API DO HUGGING FACE
|
159 |
+
# ==============================================================================
|
160 |
+
|
161 |
class HuggingFaceAPIClient:
|
162 |
def __init__(self, token: str):
|
163 |
self.token = token
|
|
|
165 |
"Authorization": f"Bearer {token}",
|
166 |
"Content-Type": "application/json"
|
167 |
}
|
168 |
+
|
169 |
+
def query_model(self, model_name: str, messages: list, max_tokens: int = 1500) -> str:
|
170 |
"""Faz requisição para a API do Hugging Face"""
|
171 |
url = f"https://api-inference.huggingface.co/models/{model_name}/v1/chat/completions"
|
|
|
172 |
payload = {
|
173 |
"model": model_name,
|
174 |
"messages": messages,
|
|
|
177 |
"top_p": 0.9,
|
178 |
"stream": False
|
179 |
}
|
|
|
180 |
try:
|
181 |
response = requests.post(url, headers=self.headers, json=payload, timeout=9999)
|
|
|
182 |
if response.status_code == 200:
|
183 |
result = response.json()
|
184 |
return result["choices"][0]["message"]["content"]
|
185 |
else:
|
|
|
186 |
return self._fallback_text_generation(model_name, messages, max_tokens)
|
|
|
187 |
except Exception as e:
|
188 |
return f"Erro na API: {str(e)}"
|
189 |
+
|
190 |
def _fallback_text_generation(self, model_name: str, messages: list, max_tokens: int) -> str:
|
|
|
191 |
url = f"https://api-inference.huggingface.co/models/{model_name}"
|
|
|
|
|
192 |
prompt = self._messages_to_prompt(messages)
|
|
|
193 |
payload = {
|
194 |
"inputs": prompt,
|
195 |
"parameters": {
|
196 |
+
"max_new_tokens": max_tokens, "temperature": 0.7, "top_p": 0.9,
|
197 |
+
"do_sample": True, "return_full_text": False
|
|
|
|
|
|
|
198 |
},
|
199 |
+
"options": {"wait_for_model": True, "use_cache": False}
|
|
|
|
|
|
|
200 |
}
|
|
|
201 |
try:
|
202 |
response = requests.post(url, headers=self.headers, json=payload, timeout=9999)
|
|
|
203 |
if response.status_code == 200:
|
204 |
result = response.json()
|
205 |
if isinstance(result, list) and len(result) > 0:
|
206 |
generated_text = result[0].get("generated_text", "")
|
|
|
207 |
if generated_text:
|
|
|
208 |
if "Assistente: " in generated_text:
|
209 |
parts = generated_text.split("Assistente: ")
|
210 |
+
if len(parts) > 1: return parts[-1].strip()
|
|
|
211 |
return generated_text.strip()
|
212 |
return "Resposta vazia"
|
213 |
elif isinstance(result, dict):
|
214 |
+
if "error" in result: return f"Erro do modelo: {result['error']}"
|
215 |
+
elif "generated_text" in result: return result["generated_text"].strip()
|
|
|
|
|
216 |
return "Formato de resposta inesperado"
|
217 |
+
elif response.status_code == 404: return f"❌ Modelo '{model_name}' não encontrado."
|
218 |
+
elif response.status_code == 503: return "⏳ Modelo carregando... Tente novamente."
|
219 |
+
elif response.status_code == 429: return "⚠️ Muitas requisições. Tente novamente."
|
220 |
+
else: return f"Erro HTTP {response.status_code}: {response.text[:200]}..."
|
|
|
|
|
|
|
|
|
|
|
221 |
except requests.Timeout:
|
222 |
+
return "⏰ Timeout - Modelo demorou muito para responder."
|
223 |
except Exception as e:
|
224 |
return f"Erro na requisição: {str(e)}"
|
225 |
+
|
226 |
def _messages_to_prompt(self, messages: list) -> str:
|
|
|
227 |
prompt = ""
|
228 |
for msg in messages:
|
229 |
+
prompt += f"{msg['role'].capitalize()}: {msg['content']}\n\n"
|
|
|
|
|
|
|
|
|
|
|
|
|
230 |
prompt += "Assistente: "
|
231 |
return prompt
|
232 |
|
233 |
# Inicializar cliente da API
|
234 |
api_client = HuggingFaceAPIClient(HF_TOKEN)
|
235 |
|
236 |
+
# ==============================================================================
|
237 |
+
# SEÇÃO PRINCIPAL: LÓGICA DO CHATBOT E GRADIO
|
238 |
+
# ==============================================================================
|
239 |
+
|
240 |
def formatar_resposta_com_codigo(resposta: str) -> str:
|
241 |
+
"""Formata a resposta destacando códigos em blocos separados."""
|
242 |
+
if not resposta: return resposta
|
|
|
|
|
|
|
243 |
resposta_formatada = re.sub(
|
244 |
r'```(\w+)?\n(.*?)\n```',
|
245 |
r'<div style="background-color: #f8f9fa; color: #1a1a1a; border: 1px solid #e9ecef; border-radius: 8px; padding: 15px; margin: 10px 0; font-family: Monaco, Consolas, monospace; overflow-x: auto;"><strong style="color: #1a1a1a;">💻 Código:</strong><br><pre style="color: #1a1a1a; margin: 5px 0; white-space: pre-wrap; word-wrap: break-word;"><code>\2</code></pre></div>',
|
246 |
+
resposta, flags=re.DOTALL
|
|
|
247 |
)
|
|
|
|
|
248 |
resposta_formatada = re.sub(
|
249 |
r'`([^`]+)`',
|
250 |
r'<code style="background-color: #f1f3f4; color: #1a1a1a; padding: 2px 4px; border-radius: 4px; font-family: Monaco, Consolas, monospace;">\1</code>',
|
251 |
resposta_formatada
|
252 |
)
|
|
|
|
|
|
|
253 |
resposta_formatada = resposta_formatada.replace('\n', '<br>')
|
|
|
|
|
254 |
resposta_formatada = re.sub(
|
255 |
r'^\*\*(.*?)\*\*',
|
256 |
r'<h3 style="color: #1a1a1a; margin-top: 20px; margin-bottom: 10px;">\1</h3>',
|
257 |
+
resposta_formatada, flags=re.MULTILINE
|
|
|
258 |
)
|
|
|
259 |
return resposta_formatada
|
260 |
|
261 |
def responder_como_aldo(pergunta: str, modelo_escolhido: str = DEFAULT_MODEL) -> str:
|
262 |
+
"""Função principal para gerar respostas, agora com RAG."""
|
263 |
if not pergunta.strip():
|
264 |
return "Por favor, faça uma pergunta."
|
265 |
+
|
266 |
try:
|
267 |
+
# --- ETAPA DE RAG ---
|
268 |
+
print(f"Buscando contexto para a pergunta: '{pergunta[:50]}...'")
|
269 |
+
contexto_blog = retrieve_context_from_blog(pergunta)
|
270 |
+
|
271 |
+
# Montar o prompt do sistema com o contexto do RAG
|
272 |
+
system_prompt = (
|
273 |
+
"Você é o professor Dr. Aldo Henrique, especialista em C, Java, desenvolvimento web e inteligência artificial. "
|
274 |
+
"Responda com clareza, profundidade e tom acadêmico. Foque em explicar e não em só mostrar o resultado. "
|
275 |
+
"Responda sempre em português brasileiro. Use blocos de código formatados com ```. "
|
276 |
+
"Não responda nada se a pergunta não for sobre o universo de programação e tecnologia."
|
277 |
+
)
|
278 |
|
279 |
+
# Montar prompt do usuário, injetando o contexto do blog
|
280 |
+
if contexto_blog:
|
281 |
+
pergunta_completa = (
|
282 |
+
"Com base no seguinte contexto extraído do seu blog, responda à pergunta do usuário.\n\n"
|
283 |
+
"--- CONTEXTO DO BLOG ---\n"
|
284 |
+
f"{contexto_blog}\n"
|
285 |
+
"--- FIM DO CONTEXTO ---\n\n"
|
286 |
+
f"PERGUNTA DO USUÁRIO: {pergunta}"
|
287 |
+
)
|
288 |
+
print("Contexto encontrado e injetado no prompt.")
|
289 |
+
else:
|
290 |
+
pergunta_completa = f"{pergunta} Não responda nada se a pergunta não for sobre o universo de programação e tecnologia, informe que o Dr. Aldo Henrique só tem domínio em TI. Você é o Professor Dr. Aldo Henrique, foque em explicar e não em só mostrar o resultado. Quando apresentar código, use blocos de código formatados com ```. Sempre responda primeiro a explicação e depois modestre o código."
|
291 |
+
print("Nenhum contexto relevante encontrado no blog, usando prompt padrão.")
|
292 |
+
|
293 |
messages = [
|
294 |
+
{"role": "system", "content": system_prompt},
|
295 |
+
{"role": "user", "content": pergunta_completa}
|
|
|
|
|
|
|
|
|
|
|
|
|
296 |
]
|
297 |
|
|
|
298 |
model_name = MODELS.get(modelo_escolhido, MODELS[DEFAULT_MODEL])
|
299 |
+
resposta = api_client.query_model(model_name, messages)
|
300 |
|
|
|
|
|
|
|
|
|
301 |
if resposta.startswith("Assistente: "):
|
302 |
resposta = resposta.replace("Assistente: ", "")
|
303 |
+
|
|
|
304 |
resposta_formatada = formatar_resposta_com_codigo(resposta.strip())
|
|
|
305 |
return resposta_formatada
|
306 |
|
307 |
except Exception as e:
|
308 |
return f"Erro ao processar sua pergunta: {str(e)}"
|
309 |
|
310 |
+
# Funções de teste (inalteradas)
|
311 |
def verificar_modelo_disponivel(model_name: str) -> str:
|
|
|
312 |
try:
|
313 |
url = f"https://api-inference.huggingface.co/models/{model_name}"
|
314 |
headers = {"Authorization": f"Bearer {HF_TOKEN}"}
|
315 |
+
payload = {"inputs": "Hello", "parameters": {"max_new_tokens": 5}}
|
|
|
|
|
|
|
|
|
|
|
|
|
316 |
response = requests.post(url, headers=headers, json=payload, timeout=9999)
|
317 |
+
if response.status_code == 200: return "✅ Disponível"
|
318 |
+
elif response.status_code == 404: return "❌ Não encontrado"
|
319 |
+
elif response.status_code == 503: return "⏳ Carregando..."
|
320 |
+
else: return f"⚠️ Status {response.status_code}"
|
|
|
|
|
|
|
|
|
|
|
|
|
321 |
except Exception as e:
|
322 |
return f"❌ Erro: {str(e)[:50]}..."
|
323 |
|
324 |
def testar_todos_modelos():
|
|
|
325 |
resultados = []
|
326 |
for nome, modelo in MODELS.items():
|
327 |
status = verificar_modelo_disponivel(modelo)
|
328 |
resultados.append(f"{nome}: {status}")
|
329 |
return "\n".join(resultados)
|
330 |
|
331 |
+
# CSS (inalterado)
|
332 |
css_customizado = """
|
333 |
+
.gradio-container { max-width: 1400px !important; margin: 0 auto; width: 85%; }
|
334 |
+
.gr-textbox textarea { font-size: 14px !important; line-height: 1.5 !important; }
|
335 |
+
.resposta-container { background-color: #ffffff !important; color: #1a1a1a !important; border: 1px solid #e0e0e0 !important; border-radius: 20px !important; padding: 20px !important; margin: 20px 0 !important; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05) !important; }
|
336 |
+
.resposta-container pre code { color: #1a1a1a !important; background-color: #f8f9fa !important; }
|
337 |
+
.pergunta-container { background-color: #f0f8ff !important; border-radius: 8px !important; padding: 15px !important; }
|
338 |
+
.titulo-principal { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important; color: white !important; padding: 20px !important; border-radius: 10px !important; margin-bottom: 20px !important; text-align: center !important; }
|
339 |
+
.modelo-dropdown { margin-bottom: 15px !important; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
340 |
"""
|
341 |
|
342 |
# Interface Gradio
|
343 |
+
with gr.Blocks(title="Dr. Aldo Henrique - API Externa", theme=gr.themes.Soft(), css=css_customizado) as interface:
|
|
|
|
|
|
|
|
|
|
|
|
|
344 |
gr.HTML("""
|
345 |
<div class="titulo-principal">
|
346 |
+
<h1>🤖 Dr. Aldo Henrique - Especialista em TI (com RAG)</h1>
|
347 |
+
<p style="font-size: 14px; opacity: 0.9;">Conhecimento enriquecido com o conteúdo do <a href="https://aldohenrique.com.br/" style="color: white; text-decoration: underline;">Blog do Prof. Dr. Aldo Henrique</a></p>
|
348 |
</div>
|
349 |
""")
|
350 |
|
351 |
with gr.Row():
|
|
|
352 |
with gr.Column(scale=2):
|
353 |
gr.Markdown("### 📝 Faça sua pergunta:")
|
354 |
+
entrada = gr.Textbox(label="", placeholder="Digite sua pergunta aqui.", lines=6, elem_classes="pergunta-container")
|
355 |
+
modelo_select = gr.Dropdown(choices=list(MODELS.keys()), value=DEFAULT_MODEL, label="🧠 Selecione o Modelo de IA", info="Escolha o modelo para responder", elem_classes="modelo-dropdown")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
356 |
|
357 |
with gr.Row():
|
358 |
+
botao_perguntar = gr.Button("🤔 Perguntar ao Dr. Aldo", variant="primary", size="lg")
|
359 |
+
botao_testar = gr.Button("🔍 Testar Modelos", variant="secondary")
|
360 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
361 |
with gr.Column(scale=3):
|
362 |
gr.Markdown("### 💬 Resposta do Dr. Aldo Henrique:")
|
363 |
+
saida = gr.HTML(label="", value="<div style='padding: 20px; text-align: center; color: #1a1a1a;'>Aguardando sua pergunta...</div>", elem_classes="resposta-container")
|
364 |
+
|
365 |
+
# --- NOVA SEÇÃO PARA CONTROLE DO RAG ---
|
366 |
+
with gr.Accordion("⚙️ Controle do Conhecimento (RAG)", open=False):
|
367 |
+
status_rag = gr.Textbox(label="Status do Retreino", interactive=False)
|
368 |
+
botao_retreinar = gr.Button("🔄 Atualizar Conhecimento do Blog", variant="stop")
|
369 |
+
|
370 |
with gr.Accordion("📚 Exemplos de Perguntas", open=False):
|
371 |
gr.Examples(
|
372 |
examples=[
|
373 |
["Como implementar uma lista ligada em C com todas as operações básicas?", DEFAULT_MODEL],
|
374 |
+
["Qual a sua opinião sobre o uso de ponteiros em C++ moderno, baseada no seu blog?", "Mistral 7B"],
|
375 |
+
["Resuma o que você escreveu sobre machine learning no seu blog.", "Llama 3.2 3B (Meta)"],
|
|
|
376 |
],
|
377 |
inputs=[entrada, modelo_select]
|
378 |
)
|
379 |
|
|
|
380 |
with gr.Accordion("🔧 Status da API", open=False):
|
381 |
+
status_api = gr.Textbox(label="Status dos Modelos", interactive=False, lines=8)
|
|
|
|
|
|
|
|
|
382 |
|
|
|
383 |
with gr.Accordion("ℹ️ Informações", open=False):
|
384 |
gr.Markdown("""
|
385 |
### Sobre o Dr. Aldo Henrique:
|
386 |
- **Especialidade**: Linguagens C, Java, Desenvolvimento Web, Inteligência Artificial
|
387 |
+
- **Conhecimento Adicional**: Conteúdo do blog aldohenrique.com.br
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
388 |
### Dicas para melhores respostas:
|
389 |
+
- Faça perguntas específicas sobre o conteúdo do blog para ver o RAG em ação!
|
390 |
+
- Peça resumos ou opiniões sobre temas que o professor aborda.
|
|
|
391 |
""")
|
392 |
+
|
393 |
# Eventos
|
394 |
+
botao_perguntar.click(fn=responder_como_aldo, inputs=[entrada, modelo_select], outputs=saida, show_progress=True)
|
395 |
+
botao_testar.click(fn=testar_todos_modelos, outputs=status_api, show_progress=True)
|
396 |
+
botao_retreinar.click(fn=build_and_save_vector_store, outputs=status_rag, show_progress=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
397 |
|
398 |
# Lançar aplicação
|
399 |
if __name__ == "__main__":
|
400 |
+
print("🚀 Iniciando Dr. Aldo Henrique com RAG...")
|
401 |
+
|
402 |
+
# Carrega ou constrói o vector store na inicialização
|
403 |
+
load_vector_store()
|
404 |
+
|
405 |
print(f"🔑 Token HF encontrado: {HF_TOKEN[:8]}...")
|
406 |
+
print("🌐 Interface pronta!")
|
|
|
407 |
|
408 |
interface.launch(
|
409 |
server_name="0.0.0.0",
|