Spaces:
Running
Running
File size: 18,482 Bytes
13660cf d4a357d 13660cf d4a357d 3bdb3bd d4a357d 3bdb3bd d4a357d 3bdb3bd d4a357d 3bdb3bd d4a357d 3bdb3bd d4a357d 3bdb3bd 13660cf d4a357d abfcded d4a357d 3bdb3bd d4a357d 175b353 73eb66f d4a357d 73eb66f abfcded 3bdb3bd d4a357d 3bdb3bd d4a357d abfcded d4a357d 13660cf 3bdb3bd d4a357d 13660cf d4a357d 13660cf d4a357d 3bdb3bd d4a357d 3bdb3bd 13660cf abfcded d4a357d abfcded 73eb66f d4a357d |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 |
import os
import re
import time
import pickle
import requests
from typing import Dict, Any, List, Optional, Tuple
from bs4 import BeautifulSoup
from urllib.parse import urljoin, urlparse
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.vectorstores import FAISS
from langchain.embeddings import HuggingFaceEmbeddings
# --- Configurações ---
# Chave da API da Hugging Face (essencial para o funcionamento)
HF_TOKEN = os.getenv("HF_TOKEN")
if not HF_TOKEN:
raise ValueError("A variável de ambiente HF_TOKEN não foi definida. Defina-a com seu token da Hugging Face.")
# URL do blog para a base de conhecimento (RAG)
BLOG_URL = "https://aldohenrique.com.br/"
# Caminhos para os arquivos do RAG
VECTOR_STORE_PATH = "faiss_index_store.pkl"
PROCESSED_URLS_PATH = "processed_urls.pkl"
# Modelos disponíveis na Hugging Face
MODELS = {
"Mistral 7B": "mistralai/Mistral-7B-Instruct-v0.3",
"Phi-3 Mini (Microsoft)": "microsoft/Phi-3-mini-4k-instruct",
"Deepseek (chat) 7B": "deepseek-ai/deepseek-vl-7b-chat",
"Gemma 7B (Google)":"google/gemma-7b-it",
"Zephyr 7B": "HuggingFaceH4/zephyr-7b-beta"
}
DEFAULT_MODEL = "Phi-3 Mini (Microsoft)"
# --- Variáveis Globais ---
# Armazena o índice vetorial para busca de contexto (RAG)
vector_store: Optional[FAISS] = None
# Dicionário para gerenciar todas as sessões de usuário em memória
# Estrutura: {session_id: {"history": [...], "profile": {...}}}
user_sessions: Dict[str, Dict[str, Any]] = {}
MAX_MEMORY_TURNS = 5 # Manter as últimas 5 trocas (usuário + assistente)
# ==============================================================================
# SEÇÃO DE GERENCIAMENTO DA SESSÃO (MEMÓRIA E PERFIL)
# ==============================================================================
def get_or_create_session(session_id: str) -> Dict[str, Any]:
"""
Obtém uma sessão de usuário existente ou cria uma nova.
A sessão é mantida apenas em memória.
"""
if session_id not in user_sessions:
print(f"Nova sessão criada para o ID: {session_id}")
user_sessions[session_id] = {
"history": [],
"profile": {"nivel": "indefinido", "interesses": {}, "total_perguntas": 0}
}
return user_sessions[session_id]
def update_memory(session_id: str, user_message: str, assistant_response: str):
"""Adiciona a troca de mensagens ao histórico da sessão."""
session = get_or_create_session(session_id)
# Adiciona as mensagens mais recentes
session["history"].append({"role": "user", "content": user_message})
session["history"].append({"role": "assistant", "content": assistant_response})
# Garante que o histórico não exceda o tamanho máximo
if len(session["history"]) > MAX_MEMORY_TURNS * 2:
session["history"] = session["history"][-(MAX_MEMORY_TURNS * 2):]
def update_user_profile(session_id: str, user_message: str):
"""
Analisa a mensagem do usuário para inferir e atualizar seu perfil de interesses e nível.
"""
session = get_or_create_session(session_id)
profile = session["profile"]
msg_lower = user_message.lower()
# Atualiza contador de perguntas
profile["total_perguntas"] += 1
# Inferência de nível
if any(word in msg_lower for word in ['básico', 'iniciante', 'começar', 'o que é']):
profile['nivel'] = 'iniciante'
elif any(word in msg_lower for word in ['avançado', 'complexo', 'otimização', 'performance', 'arquitetura']):
profile['nivel'] = 'avançado'
elif profile['nivel'] == 'indefinido': # Define como intermediário se ainda não tiver um nível
profile['nivel'] = 'intermediário'
# Inferência de interesses
topics = {
'java': ['java', 'spring', 'jpa', 'jvm'],
'python': ['python', 'django', 'flask', 'pandas'],
'web': ['html', 'css', 'javascript', 'react', 'node'],
'ia': ['inteligência artificial', 'machine learning', 'llm', 'rag'],
'banco de dados': ['sql', 'nosql', 'mongodb', 'postgresql']
}
for topic, keywords in topics.items():
if any(keyword in msg_lower for keyword in keywords):
profile['interesses'][topic] = profile['interesses'].get(topic, 0) + 1
def clear_session_memory(session_id: str) -> str:
"""Limpa a memória de uma sessão específica."""
if session_id in user_sessions:
del user_sessions[session_id]
return f"✅ Memória da sessão '{session_id}' foi limpa."
return f"⚠️ Sessão '{session_id}' não encontrada."
# ==============================================================================
# SEÇÃO RAG: BUSCA E PROCESSAMENTO DE CONTEÚDO (SEM ALTERAÇÕES SIGNIFICATIVAS)
# ==============================================================================
def scrape_text_from_url(url: str) -> str:
"""Extrai texto de uma URL, focando no conteúdo principal."""
try:
response = requests.get(url, timeout=10)
response.raise_for_status()
soup = BeautifulSoup(response.content, 'html.parser')
main_content = soup.find('article') or soup.find('main')
return main_content.get_text(separator='\n', strip=True) if main_content else ""
except requests.RequestException as e:
print(f"Erro ao acessar {url}: {e}")
return ""
def build_and_save_vector_store():
"""Coleta dados do blog, processa e cria um índice vetorial com FAISS."""
global vector_store
print("Iniciando construção do RAG...")
# Lógica simplificada de coleta de links (pode ser expandida se necessário)
# Para este exemplo, vamos focar em uma URL principal
all_texts = [scrape_text_from_url(BLOG_URL)]
# Adicione mais URLs manualmente se desejar
# additional_urls = [f"{BLOG_URL}/sobre", f"{BLOG_URL}/contato"]
# all_texts.extend([scrape_text_from_url(url) for url in additional_urls])
valid_texts = [text for text in all_texts if text and len(text) > 100]
if not valid_texts:
print("Nenhum texto válido encontrado para criar o RAG.")
return
print(f"Processando {len(valid_texts)} página(s).")
text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=150)
chunks = text_splitter.create_documents(valid_texts)
print(f"Criando {len(chunks)} chunks de texto.")
embeddings_model = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")
vector_store = FAISS.from_documents(chunks, embeddings_model)
with open(VECTOR_STORE_PATH, "wb") as f:
pickle.dump(vector_store, f)
print("✅ RAG construído e salvo com sucesso!")
def load_vector_store():
"""Carrega o índice vetorial do disco."""
global vector_store
if os.path.exists(VECTOR_STORE_PATH):
print(f"Carregando RAG de '{VECTOR_STORE_PATH}'...")
with open(VECTOR_STORE_PATH, "rb") as f:
vector_store = pickle.load(f)
print("✅ RAG carregado.")
else:
print("Índice RAG não encontrado. Construindo um novo...")
build_and_save_vector_store()
def retrieve_rag_context(query: str, k: int = 3) -> str:
"""Busca no RAG por contexto relevante para a pergunta."""
if vector_store:
try:
results = vector_store.similarity_search(query, k=k)
return "\n\n---\n\n".join([doc.page_content for doc in results])
except Exception as e:
print(f"Erro ao buscar contexto no RAG: {e}")
return ""
# ==============================================================================
# SEÇÃO DA API E CONSTRUÇÃO DO PROMPT
# ==============================================================================
class HuggingFaceAPIClient:
"""Cliente para interagir com a API de Inferência da Hugging Face."""
def __init__(self, token: str):
self.headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
def query(self, model_id: str, messages: List[Dict[str, str]], max_tokens: int = 2048) -> str:
api_url = f"https://api-inference.huggingface.co/models/{model_id}"
payload = {
"inputs": self._format_prompt_for_model(messages),
"parameters": {
"max_new_tokens": max_tokens,
"temperature": 0.7,
"top_p": 0.95,
"return_full_text": False,
},
"options": {"wait_for_model": True}
}
try:
response = requests.post(api_url, headers=self.headers, json=payload, timeout=60)
response.raise_for_status()
result = response.json()
# A resposta da API de inferência pode vir em uma lista
if isinstance(result, list) and result:
return result[0].get("generated_text", "").strip()
# Ou em um dicionário
elif isinstance(result, dict):
return result.get("generated_text", f"Erro: Resposta inesperada do modelo: {result.get('error', '')}").strip()
return "Erro: Resposta vazia ou em formato inesperado."
except requests.Timeout:
return "Erro: A requisição à API demorou muito para responder (timeout)."
except requests.HTTPError as http_err:
return f"Erro HTTP: {http_err}. Detalhes: {response.text}"
except Exception as e:
return f"Ocorreu um erro inesperado na chamada da API: {e}"
def _format_prompt_for_model(self, messages: List[Dict[str, str]]) -> str:
"""Formata a lista de mensagens em uma string única para a API de inferência."""
prompt_str = ""
for msg in messages:
if msg['role'] == 'system':
prompt_str += f"<|system|>\n{msg['content']}</s>\n"
elif msg['role'] == 'user':
prompt_str += f"<|user|>\n{msg['content']}</s>\n"
elif msg['role'] == 'assistant':
prompt_str += f"<|assistant|>\n{msg['content']}</s>\n"
prompt_str += "<|assistant|>\n" # Solicita a continuação do assistente
return prompt_str
class PromptBuilder:
"""Constrói o prompt final a ser enviado para o modelo."""
SYS_PROMPT_TEMPLATE = """Você é o Professor Aldo, um especialista em programação (Java, C, Web) e IA.
**Sua Personalidade:**
- **Didático e Paciente:** Aja como um professor experiente. Explique o "porquê" das coisas, não apenas o "como".
- **Acolhedor e Amigável:** Use uma linguagem calorosa e acessível.
- **Adaptável:** Ajuste a complexidade da sua resposta ao nível de conhecimento do aluno.
- **Contextual:** Se a pergunta atual se conectar a algo que já discutimos, mencione essa conexão.
**Suas Regras:**
1. Responda sempre em português do Brasil.
2. Use blocos de código (```java, ```python, etc.) para exemplos. Comente o código para explicar cada parte.
3. Se a pergunta não for sobre tecnologia ou programação, educadamente informe que sua especialidade é outra.
4. Baseie sua resposta primariamente nas informações do seu blog (se houver contexto) e no nosso histórico de conversa.
A seguir, informações para te ajudar a contextualizar sua resposta:"""
def __init__(self, session_id: str, rag_context: str):
self.session = get_or_create_session(session_id)
self.rag_context = rag_context
self.parts = []
def _add_profile_context(self):
profile = self.session["profile"]
if profile["total_perguntas"] > 0:
profile_summary = [f"**Perfil do Aluno (Inferido):**"]
profile_summary.append(f"- Nível de conhecimento: {profile['nivel'].capitalize()}")
interesses = sorted(profile['interesses'].items(), key=lambda item: item[1], reverse=True)
if interesses:
formatted_interesses = [f"{topic.capitalize()} ({count}x)" for topic, count in interesses]
profile_summary.append(f"- Principais interesses: {', '.join(formatted_interesses)}")
self.parts.append("\n".join(profile_summary))
def _add_rag_context(self):
if self.rag_context:
self.parts.append(f"**Contexto Relevante do seu Blog (RAG):**\n{self.rag_context}")
def _add_history_context(self, current_question: str) -> List[Dict[str, str]]:
"""Prepara o histórico de mensagens para o modelo."""
history = self.session.get("history", [])
# Pega as mensagens do histórico e adiciona a pergunta atual
messages = history + [{"role": "user", "content": current_question}]
return messages
def build(self, user_question: str) -> List[Dict[str, str]]:
# Adiciona os contextos ao prompt do sistema
self._add_profile_context()
self._add_rag_context()
system_content = self.SYS_PROMPT_TEMPLATE
if self.parts:
system_content += "\n\n" + "\n\n".join(self.parts)
# Monta a lista final de mensagens
messages = [{"role": "system", "content": system_content}]
messages.extend(self._add_history_context(user_question))
return messages
# ==============================================================================
# FUNÇÃO PRINCIPAL E INICIALIZAÇÃO
# ==============================================================================
# Inicializa o cliente da API
api_client = HuggingFaceAPIClient(token=HF_TOKEN)
def formatar_resposta(resposta: str) -> str:
"""Formata a resposta com HTML para melhor visualização de código e texto."""
resposta_html = resposta.replace('<', '<').replace('>', '>')
# Formata blocos de código
resposta_html = re.sub(
r'```(\w+)?\n(.*?)\n```',
r'<div style="background-color:#f0f0f0; border:1px solid #ddd; border-radius:8px; padding:15px; margin:1em 0; font-family:monospace; color:black;"><pre><code>\2</code></pre></div>',
resposta_html,
flags=re.DOTALL
)
# Formata negrito
resposta_html = re.sub(r'\*\*(.*?)\*\*', r'<strong>\1</strong>', resposta_html)
# Formata nova linha
return resposta_html.replace('\n', '<br>')
def responder_pergunta(session_id: str, pergunta: str, modelo_escolhido: str = DEFAULT_MODEL) -> str:
"""
Função principal que orquestra todo o processo de resposta.
"""
if not pergunta.strip():
return "Por favor, faça uma pergunta."
print(f"\n--- Processando pergunta para a sessão: {session_id} ---")
# 1. Atualizar perfil do usuário com base na pergunta atual
update_user_profile(session_id, pergunta)
# 2. Buscar contexto relevante no RAG
print("Buscando no RAG...")
rag_context = retrieve_rag_context(pergunta)
if rag_context:
print("Contexto encontrado no RAG.")
# 3. Construir o prompt completo usando o PromptBuilder
print("Construindo prompt...")
builder = PromptBuilder(session_id, rag_context)
messages = builder.build(pergunta)
# DEBUG: Descomente a linha abaixo para ver o prompt exato enviado ao modelo
# print("MENSAGENS ENVIADAS AO MODELO:", json.dumps(messages, indent=2, ensure_ascii=False))
# 4. Chamar a API da Hugging Face
print(f"Enviando para o modelo '{modelo_escolhido}'...")
model_id = MODELS.get(modelo_escolhido, MODELS[DEFAULT_MODEL])
resposta_bruta = api_client.query(model_id, messages)
# 5. Adicionar a interação à memória da sessão
update_memory(session_id, pergunta, resposta_bruta)
# 6. Formatar e retornar a resposta
return formatar_resposta(resposta_bruta)
def inicializar_sistema():
"""Carrega o RAG ao iniciar."""
print("🚀 Inicializando o sistema...")
load_vector_store()
print("✅ Sistema pronto para uso.")
# ==============================================================================
# BLOCO DE TESTE
# ==============================================================================
if __name__ == "__main__":
inicializar_sistema()
# --- Simulação de Conversas ---
# Sessão do Usuário A (interessado em Java)
session_a = "aluno_java_123"
print("\n--- INÍCIO DA CONVERSA COM ALUNO A (Java) ---")
pergunta1_a = "Olá! Pode me explicar o que é Polimorfismo em Java de uma forma simples?"
resposta1_a = responder_pergunta(session_a, pergunta1_a)
print(f"ALUNO A: {pergunta1_a}\nPROFESSOR ALDO:\n{resposta1_a}\n")
pergunta2_a = "Entendi! Pode me dar um exemplo de código com classes e herança para ilustrar?"
resposta2_a = responder_pergunta(session_a, pergunta2_a)
print(f"ALUNO A: {pergunta2_a}\nPROFESSOR ALDO:\n{resposta2_a}\n")
# Sessão do Usuário B (interessado em IA)
session_b = "aluna_ia_456"
print("\n--- INÍCIO DA CONVERSA COM ALUNA B (IA) ---")
pergunta1_b = "Oi, professor! Eu sou nova na área. Qual a diferença entre Inteligência Artificial e Machine Learning?"
resposta1_b = responder_pergunta(session_b, pergunta1_b)
print(f"ALUNA B: {pergunta1_b}\nPROFESSOR ALDO:\n{resposta1_b}\n")
# Usuário A continua sua conversa, a memória do usuário B não deve interferir
print("\n--- ALUNO A CONTINUA SUA CONVERSA ---")
pergunta3_a = "Faz sentido. E como o conceito de 'override' se encaixa nisso que acabamos de ver?"
resposta3_a = responder_pergunta(session_a, pergunta3_a)
print(f"ALUNO A: {pergunta3_a}\nPROFESSOR ALDO:\n{resposta3_a}\n")
# Exibe o estado final das sessões
print("\n--- ESTADO FINAL DAS SESSÕES EM MEMÓRIA ---")
print(f"\nSessão A ({session_a}):")
# print(json.dumps(user_sessions.get(session_a), indent=2, ensure_ascii=False))
print(f" Nível: {user_sessions[session_a]['profile']['nivel']}")
print(f" Interesses: {user_sessions[session_a]['profile']['interesses']}")
print(f" Tamanho do Histórico: {len(user_sessions[session_a]['history'])} mensagens")
print(f"\nSessão B ({session_b}):")
# print(json.dumps(user_sessions.get(session_b), indent=2, ensure_ascii=False))
print(f" Nível: {user_sessions[session_b]['profile']['nivel']}")
print(f" Interesses: {user_sessions[session_b]['profile']['interesses']}")
print(f" Tamanho do Histórico: {len(user_sessions[session_b]['history'])} mensagens")
# Limpando a memória de uma sessão
print("\n--- LIMPANDO MEMÓRIA ---")
print(clear_session_memory(session_a))
print(f"Sessões ativas agora: {list(user_sessions.keys())}") |