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('<', '&lt;').replace('>', '&gt;')
    
    # 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())}")