Spaces:
Running
Running
Update ai_logic.py
Browse files- ai_logic.py +263 -59
ai_logic.py
CHANGED
@@ -15,34 +15,173 @@ from langchain.embeddings import HuggingFaceEmbeddings
|
|
15 |
BLOG_URL = "https://aldohenrique.com.br/"
|
16 |
VECTOR_STORE_PATH = "faiss_index_store.pkl"
|
17 |
PROCESSED_URLS_PATH = "processed_urls.pkl"
|
|
|
18 |
|
19 |
# --- Configuração da API Hugging Face ---
|
20 |
HF_TOKEN = os.getenv("HF_TOKEN")
|
21 |
if not HF_TOKEN:
|
22 |
raise ValueError("Token HF_TOKEN não encontrado nas variáveis de ambiente")
|
23 |
|
24 |
-
MODELS = {
|
25 |
-
"Phi-3 Mini (Microsoft)": "microsoft/Phi-3-mini-4k-instruct",
|
26 |
-
"Mistral 7B": "mistralai/Mistral-7B-Instruct-v0.3",
|
27 |
-
"Zephyr 7B": "HuggingFaceH4/zephyr-7b-beta",
|
28 |
-
"Gemma 2B (Google)": "google/gemma-2b-it",
|
29 |
-
"Open Hermes 2.5 (teknium)": "teknium/OpenHermes-2.5-Mistral-7B",
|
30 |
-
"TinyLlama 1.1B (TinyLlama Project)": "TinyLlama/TinyLlama-1.1B-Chat-v1.0"
|
31 |
-
}
|
32 |
MODELS = {
|
33 |
"Phi-3 Mini (Microsoft)": "microsoft/Phi-3-mini-4k-instruct",
|
34 |
"Mistral 7B": "mistralai/Mistral-7B-Instruct-v0.3",
|
35 |
"Zephyr 7B": "HuggingFaceH4/zephyr-7b-beta"
|
36 |
}
|
37 |
|
38 |
-
|
39 |
DEFAULT_MODEL = "Phi-3 Mini (Microsoft)"
|
40 |
|
41 |
-
# --- Variáveis Globais para o RAG ---
|
42 |
vector_store: Optional[FAISS] = None
|
|
|
|
|
|
|
43 |
|
44 |
# ==============================================================================
|
45 |
-
# SEÇÃO
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
46 |
# ==============================================================================
|
47 |
|
48 |
def get_all_blog_links(url: str, processed_urls: set) -> set:
|
@@ -65,13 +204,11 @@ def get_all_blog_links(url: str, processed_urls: set) -> set:
|
|
65 |
for link in soup.find_all('a', href=True):
|
66 |
href = link['href']
|
67 |
full_url = urljoin(url, href)
|
68 |
-
# Garante que estamos no mesmo domínio e não é um link de âncora
|
69 |
if urlparse(full_url).netloc == urlparse(url).netloc and full_url not in visited_links:
|
70 |
links_to_visit.add(full_url)
|
71 |
except requests.RequestException as e:
|
72 |
print(f"Erro ao acessar {current_url}: {e}")
|
73 |
|
74 |
-
# Filtra apenas as páginas que parecem ser posts ou páginas de conteúdo
|
75 |
final_links = {link for link in visited_links if '/tag/' not in link and '/category/' not in link and '?' not in link}
|
76 |
return final_links
|
77 |
|
@@ -80,7 +217,6 @@ def scrape_text_from_url(url: str) -> str:
|
|
80 |
try:
|
81 |
response = requests.get(url, timeout=10)
|
82 |
soup = BeautifulSoup(response.content, 'html.parser')
|
83 |
-
# Tenta encontrar a tag <article> ou <main> que geralmente contém o conteúdo principal
|
84 |
main_content = soup.find('article') or soup.find('main')
|
85 |
if main_content:
|
86 |
return main_content.get_text(separator='\n', strip=True)
|
@@ -92,8 +228,6 @@ def scrape_text_from_url(url: str) -> str:
|
|
92 |
def build_and_save_vector_store() -> Tuple[str, Optional[str], Optional[str]]:
|
93 |
"""
|
94 |
Função principal do RAG: raspa o blog, cria chunks, gera embeddings e salva o vector store.
|
95 |
-
Esta é a nossa função de "treino".
|
96 |
-
Retorna uma tupla (mensagem_status, caminho_do_arquivo_faiss_para_download, caminho_do_arquivo_urls_para_download).
|
97 |
"""
|
98 |
global vector_store
|
99 |
start_time = time.time()
|
@@ -101,31 +235,26 @@ def build_and_save_vector_store() -> Tuple[str, Optional[str], Optional[str]]:
|
|
101 |
print("Iniciando o processo de retreino do RAG...")
|
102 |
processed_urls = set()
|
103 |
|
104 |
-
# 1. Obter todos os links do blog
|
105 |
all_links = get_all_blog_links(BLOG_URL, processed_urls)
|
106 |
print(f"Encontrados {len(all_links)} links para processar.")
|
107 |
|
108 |
-
# 2. Raspar o texto de cada link
|
109 |
all_texts = [scrape_text_from_url(link) for link in all_links if link not in processed_urls]
|
110 |
-
all_texts = [text for text in all_texts if text]
|
111 |
print(f"Textos extraídos de {len(all_texts)} novas páginas.")
|
112 |
|
113 |
if not all_texts:
|
114 |
-
return "Nenhum novo conteúdo encontrado para treinar.", None, None
|
115 |
|
116 |
-
# 3. Dividir os textos em chunks
|
117 |
text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=150)
|
118 |
chunks = text_splitter.create_documents(all_texts)
|
119 |
print(f"Textos divididos em {len(chunks)} chunks.")
|
120 |
|
121 |
-
# 4. Criar embeddings e o vector store (FAISS)
|
122 |
print("Carregando modelo de embedding...")
|
123 |
embeddings_model = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")
|
124 |
|
125 |
print("Criando o vector store com FAISS...")
|
126 |
vector_store = FAISS.from_documents(chunks, embeddings_model)
|
127 |
|
128 |
-
# 5. Salvar o vector store e as URLs processadas em disco
|
129 |
with open(VECTOR_STORE_PATH, "wb") as f:
|
130 |
pickle.dump(vector_store, f)
|
131 |
|
@@ -146,10 +275,8 @@ def load_vector_store():
|
|
146 |
print("Vector store carregado com sucesso.")
|
147 |
else:
|
148 |
print("Nenhum vector store encontrado. É necessário treinar o modelo.")
|
149 |
-
# Inicia o treino automaticamente se não houver um índice
|
150 |
-
# Modificado para ignorar o retorno dos caminhos dos arquivos ao carregar
|
151 |
message, _, _ = build_and_save_vector_store()
|
152 |
-
print(message)
|
153 |
|
154 |
def retrieve_context_from_blog(query: str, k: int = 3) -> str:
|
155 |
"""Busca no vector store por chunks de texto similares à pergunta."""
|
@@ -242,14 +369,13 @@ class HuggingFaceAPIClient:
|
|
242 |
api_client = HuggingFaceAPIClient(HF_TOKEN)
|
243 |
|
244 |
# ==============================================================================
|
245 |
-
# SEÇÃO PRINCIPAL: LÓGICA DO CHATBOT
|
246 |
# ==============================================================================
|
247 |
|
248 |
def formatar_resposta_com_codigo(resposta: str) -> str:
|
249 |
"""Formata a resposta destacando códigos em blocos separados."""
|
250 |
if not resposta: return resposta
|
251 |
|
252 |
-
# Primeiro, substituir < e > por entidades HTML para evitar interpretação como tags
|
253 |
resposta = resposta.replace('<', '<').replace('>', '>')
|
254 |
resposta_formatada = re.sub(
|
255 |
r'```(\w+)?\n(.*?)\n```',
|
@@ -270,44 +396,72 @@ def formatar_resposta_com_codigo(resposta: str) -> str:
|
|
270 |
return resposta_formatada
|
271 |
|
272 |
def responder_como_aldo(pergunta: str, modelo_escolhido: str = DEFAULT_MODEL) -> str:
|
273 |
-
"""Função principal para gerar respostas, agora com RAG."""
|
274 |
if not pergunta.strip():
|
275 |
return "Por favor, faça uma pergunta."
|
276 |
|
277 |
try:
|
|
|
|
|
|
|
278 |
# --- ETAPA DE RAG ---
|
279 |
print(f"Buscando contexto para a pergunta: '{pergunta[:50]}...'")
|
280 |
contexto_blog = retrieve_context_from_blog(pergunta)
|
281 |
|
282 |
-
#
|
283 |
-
|
284 |
-
|
285 |
-
|
286 |
-
|
287 |
-
|
288 |
-
|
289 |
-
|
290 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
291 |
|
292 |
-
|
|
|
|
|
|
|
|
|
|
|
293 |
if contexto_blog:
|
294 |
-
|
295 |
-
|
296 |
-
|
297 |
-
|
298 |
-
|
299 |
-
|
300 |
-
|
301 |
-
|
302 |
-
"
|
303 |
-
|
304 |
-
|
305 |
-
|
306 |
-
print("Contexto encontrado e injetado no prompt.")
|
307 |
-
else:
|
308 |
-
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."
|
309 |
-
print("Nenhum contexto relevante encontrado no blog, usando prompt padrão.")
|
310 |
-
|
311 |
messages = [
|
312 |
{"role": "system", "content": system_prompt},
|
313 |
{"role": "user", "content": pergunta_completa}
|
@@ -318,14 +472,20 @@ def responder_como_aldo(pergunta: str, modelo_escolhido: str = DEFAULT_MODEL) ->
|
|
318 |
|
319 |
if resposta.startswith("Assistente: "):
|
320 |
resposta = resposta.replace("Assistente: ", "")
|
321 |
-
|
|
|
|
|
|
|
322 |
resposta_formatada = formatar_resposta_com_codigo(resposta.strip())
|
323 |
return resposta_formatada
|
324 |
|
325 |
except Exception as e:
|
326 |
return f"Erro ao processar sua pergunta: {str(e)}"
|
327 |
|
328 |
-
#
|
|
|
|
|
|
|
329 |
def verificar_modelo_disponivel(model_name: str) -> str:
|
330 |
try:
|
331 |
url = f"https://api-inference.huggingface.co/models/{model_name}"
|
@@ -344,4 +504,48 @@ def testar_todos_modelos():
|
|
344 |
for nome, modelo in MODELS.items():
|
345 |
status = verificar_modelo_disponivel(modelo)
|
346 |
resultados.append(f"{nome}: {status}")
|
347 |
-
return "\n".join(resultados)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
15 |
BLOG_URL = "https://aldohenrique.com.br/"
|
16 |
VECTOR_STORE_PATH = "faiss_index_store.pkl"
|
17 |
PROCESSED_URLS_PATH = "processed_urls.pkl"
|
18 |
+
CONVERSATION_MEMORY_PATH = "conversation_memory.json" # Novo arquivo para memória
|
19 |
|
20 |
# --- Configuração da API Hugging Face ---
|
21 |
HF_TOKEN = os.getenv("HF_TOKEN")
|
22 |
if not HF_TOKEN:
|
23 |
raise ValueError("Token HF_TOKEN não encontrado nas variáveis de ambiente")
|
24 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
25 |
MODELS = {
|
26 |
"Phi-3 Mini (Microsoft)": "microsoft/Phi-3-mini-4k-instruct",
|
27 |
"Mistral 7B": "mistralai/Mistral-7B-Instruct-v0.3",
|
28 |
"Zephyr 7B": "HuggingFaceH4/zephyr-7b-beta"
|
29 |
}
|
30 |
|
|
|
31 |
DEFAULT_MODEL = "Phi-3 Mini (Microsoft)"
|
32 |
|
33 |
+
# --- Variáveis Globais para o RAG e Memória ---
|
34 |
vector_store: Optional[FAISS] = None
|
35 |
+
conversation_memory: List[Dict[str, str]] = [] # Memória da conversa atual
|
36 |
+
max_memory_length = 10 # Máximo de trocas de mensagens na memória
|
37 |
+
user_profile = {} # Perfil do usuário (interesses, nível de conhecimento, etc.)
|
38 |
|
39 |
# ==============================================================================
|
40 |
+
# SEÇÃO MEMÓRIA: GERENCIAMENTO DA CONVERSA E PERFIL DO USUÁRIO
|
41 |
+
# ==============================================================================
|
42 |
+
|
43 |
+
def load_conversation_memory():
|
44 |
+
"""Carrega a memória da conversa do arquivo JSON."""
|
45 |
+
global conversation_memory, user_profile
|
46 |
+
try:
|
47 |
+
if os.path.exists(CONVERSATION_MEMORY_PATH):
|
48 |
+
with open(CONVERSATION_MEMORY_PATH, 'r', encoding='utf-8') as f:
|
49 |
+
data = json.load(f)
|
50 |
+
conversation_memory = data.get('conversation', [])
|
51 |
+
user_profile = data.get('user_profile', {})
|
52 |
+
print(f"Memória carregada: {len(conversation_memory)} mensagens")
|
53 |
+
else:
|
54 |
+
conversation_memory = []
|
55 |
+
user_profile = {}
|
56 |
+
print("Nova conversa iniciada")
|
57 |
+
except Exception as e:
|
58 |
+
print(f"Erro ao carregar memória: {e}")
|
59 |
+
conversation_memory = []
|
60 |
+
user_profile = {}
|
61 |
+
|
62 |
+
def save_conversation_memory():
|
63 |
+
"""Salva a memória da conversa no arquivo JSON."""
|
64 |
+
try:
|
65 |
+
data = {
|
66 |
+
'conversation': conversation_memory,
|
67 |
+
'user_profile': user_profile,
|
68 |
+
'last_updated': time.time()
|
69 |
+
}
|
70 |
+
with open(CONVERSATION_MEMORY_PATH, 'w', encoding='utf-8') as f:
|
71 |
+
json.dump(data, f, ensure_ascii=False, indent=2)
|
72 |
+
except Exception as e:
|
73 |
+
print(f"Erro ao salvar memória: {e}")
|
74 |
+
|
75 |
+
def add_to_memory(user_message: str, assistant_response: str):
|
76 |
+
"""Adiciona uma troca de mensagens à memória."""
|
77 |
+
global conversation_memory
|
78 |
+
|
79 |
+
conversation_memory.append({
|
80 |
+
"role": "user",
|
81 |
+
"content": user_message,
|
82 |
+
"timestamp": time.time()
|
83 |
+
})
|
84 |
+
|
85 |
+
conversation_memory.append({
|
86 |
+
"role": "assistant",
|
87 |
+
"content": assistant_response,
|
88 |
+
"timestamp": time.time()
|
89 |
+
})
|
90 |
+
|
91 |
+
# Limita o tamanho da memória
|
92 |
+
if len(conversation_memory) > max_memory_length * 2: # *2 porque temos user + assistant
|
93 |
+
conversation_memory = conversation_memory[-max_memory_length * 2:]
|
94 |
+
|
95 |
+
save_conversation_memory()
|
96 |
+
|
97 |
+
def update_user_profile(user_message: str):
|
98 |
+
"""Atualiza o perfil do usuário baseado nas mensagens."""
|
99 |
+
global user_profile
|
100 |
+
|
101 |
+
# Detecta tópicos de interesse
|
102 |
+
topics = {
|
103 |
+
'java': ['java', 'classe', 'objeto', 'herança', 'polimorfismo'],
|
104 |
+
'c': ['linguagem c', 'ponteiro', 'malloc', 'struct'],
|
105 |
+
'web': ['html', 'css', 'javascript', 'react', 'node'],
|
106 |
+
'ia': ['inteligência artificial', 'machine learning', 'neural', 'algoritmo'],
|
107 |
+
'banco_dados': ['sql', 'database', 'banco de dados', 'mysql']
|
108 |
+
}
|
109 |
+
|
110 |
+
user_message_lower = user_message.lower()
|
111 |
+
|
112 |
+
for topic, keywords in topics.items():
|
113 |
+
if any(keyword in user_message_lower for keyword in keywords):
|
114 |
+
user_profile[f'interesse_{topic}'] = user_profile.get(f'interesse_{topic}', 0) + 1
|
115 |
+
|
116 |
+
# Detecta nível de conhecimento baseado na complexidade das perguntas
|
117 |
+
if any(word in user_message_lower for word in ['básico', 'iniciante', 'começar', 'o que é']):
|
118 |
+
user_profile['nivel'] = 'iniciante'
|
119 |
+
elif any(word in user_message_lower for word in ['avançado', 'complexo', 'otimização', 'performance']):
|
120 |
+
user_profile['nivel'] = 'avançado'
|
121 |
+
elif user_profile.get('nivel') is None:
|
122 |
+
user_profile['nivel'] = 'intermediario'
|
123 |
+
|
124 |
+
user_profile['total_perguntas'] = user_profile.get('total_perguntas', 0) + 1
|
125 |
+
|
126 |
+
def get_conversation_context() -> str:
|
127 |
+
"""Gera um resumo do contexto da conversa para o prompt."""
|
128 |
+
if not conversation_memory:
|
129 |
+
return ""
|
130 |
+
|
131 |
+
# Pega as últimas 6 mensagens (3 trocas)
|
132 |
+
recent_messages = conversation_memory[-6:] if len(conversation_memory) > 6 else conversation_memory
|
133 |
+
|
134 |
+
context = "--- CONTEXTO DA CONVERSA ANTERIOR ---\n"
|
135 |
+
for msg in recent_messages:
|
136 |
+
role = "USUÁRIO" if msg["role"] == "user" else "PROFESSOR"
|
137 |
+
# Limita o tamanho de cada mensagem no contexto
|
138 |
+
content = msg["content"][:200] + "..." if len(msg["content"]) > 200 else msg["content"]
|
139 |
+
context += f"{role}: {content}\n"
|
140 |
+
|
141 |
+
context += "--- FIM DO CONTEXTO DA CONVERSA ---\n"
|
142 |
+
return context
|
143 |
+
|
144 |
+
def get_user_profile_context() -> str:
|
145 |
+
"""Gera informações sobre o perfil do usuário para personalizar a resposta."""
|
146 |
+
if not user_profile:
|
147 |
+
return ""
|
148 |
+
|
149 |
+
context = "--- PERFIL DO ALUNO ---\n"
|
150 |
+
|
151 |
+
# Nível de conhecimento
|
152 |
+
nivel = user_profile.get('nivel', 'intermediario')
|
153 |
+
context += f"Nível: {nivel}\n"
|
154 |
+
|
155 |
+
# Principais interesses
|
156 |
+
interesses = []
|
157 |
+
for key, value in user_profile.items():
|
158 |
+
if key.startswith('interesse_') and value > 0:
|
159 |
+
topic = key.replace('interesse_', '').replace('_', ' ')
|
160 |
+
interesses.append(f"{topic} ({value}x)")
|
161 |
+
|
162 |
+
if interesses:
|
163 |
+
context += f"Principais interesses: {', '.join(interesses)}\n"
|
164 |
+
|
165 |
+
total = user_profile.get('total_perguntas', 0)
|
166 |
+
context += f"Total de perguntas feitas: {total}\n"
|
167 |
+
context += "--- FIM DO PERFIL DO ALUNO ---\n"
|
168 |
+
|
169 |
+
return context
|
170 |
+
|
171 |
+
def clear_memory():
|
172 |
+
"""Limpa a memória da conversa (função útil para resetar o chat)."""
|
173 |
+
global conversation_memory, user_profile
|
174 |
+
conversation_memory = []
|
175 |
+
user_profile = {}
|
176 |
+
try:
|
177 |
+
if os.path.exists(CONVERSATION_MEMORY_PATH):
|
178 |
+
os.remove(CONVERSATION_MEMORY_PATH)
|
179 |
+
return "✅ Memória da conversa limpa com sucesso!"
|
180 |
+
except Exception as e:
|
181 |
+
return f"❌ Erro ao limpar memória: {e}"
|
182 |
+
|
183 |
+
# ==============================================================================
|
184 |
+
# SEÇÃO RAG: FUNÇÕES PARA CRAWLING, EMBEDDING E ARMAZENAMENTO (SEM ALTERAÇÕES)
|
185 |
# ==============================================================================
|
186 |
|
187 |
def get_all_blog_links(url: str, processed_urls: set) -> set:
|
|
|
204 |
for link in soup.find_all('a', href=True):
|
205 |
href = link['href']
|
206 |
full_url = urljoin(url, href)
|
|
|
207 |
if urlparse(full_url).netloc == urlparse(url).netloc and full_url not in visited_links:
|
208 |
links_to_visit.add(full_url)
|
209 |
except requests.RequestException as e:
|
210 |
print(f"Erro ao acessar {current_url}: {e}")
|
211 |
|
|
|
212 |
final_links = {link for link in visited_links if '/tag/' not in link and '/category/' not in link and '?' not in link}
|
213 |
return final_links
|
214 |
|
|
|
217 |
try:
|
218 |
response = requests.get(url, timeout=10)
|
219 |
soup = BeautifulSoup(response.content, 'html.parser')
|
|
|
220 |
main_content = soup.find('article') or soup.find('main')
|
221 |
if main_content:
|
222 |
return main_content.get_text(separator='\n', strip=True)
|
|
|
228 |
def build_and_save_vector_store() -> Tuple[str, Optional[str], Optional[str]]:
|
229 |
"""
|
230 |
Função principal do RAG: raspa o blog, cria chunks, gera embeddings e salva o vector store.
|
|
|
|
|
231 |
"""
|
232 |
global vector_store
|
233 |
start_time = time.time()
|
|
|
235 |
print("Iniciando o processo de retreino do RAG...")
|
236 |
processed_urls = set()
|
237 |
|
|
|
238 |
all_links = get_all_blog_links(BLOG_URL, processed_urls)
|
239 |
print(f"Encontrados {len(all_links)} links para processar.")
|
240 |
|
|
|
241 |
all_texts = [scrape_text_from_url(link) for link in all_links if link not in processed_urls]
|
242 |
+
all_texts = [text for text in all_texts if text]
|
243 |
print(f"Textos extraídos de {len(all_texts)} novas páginas.")
|
244 |
|
245 |
if not all_texts:
|
246 |
+
return "Nenhum novo conteúdo encontrado para treinar.", None, None
|
247 |
|
|
|
248 |
text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=150)
|
249 |
chunks = text_splitter.create_documents(all_texts)
|
250 |
print(f"Textos divididos em {len(chunks)} chunks.")
|
251 |
|
|
|
252 |
print("Carregando modelo de embedding...")
|
253 |
embeddings_model = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")
|
254 |
|
255 |
print("Criando o vector store com FAISS...")
|
256 |
vector_store = FAISS.from_documents(chunks, embeddings_model)
|
257 |
|
|
|
258 |
with open(VECTOR_STORE_PATH, "wb") as f:
|
259 |
pickle.dump(vector_store, f)
|
260 |
|
|
|
275 |
print("Vector store carregado com sucesso.")
|
276 |
else:
|
277 |
print("Nenhum vector store encontrado. É necessário treinar o modelo.")
|
|
|
|
|
278 |
message, _, _ = build_and_save_vector_store()
|
279 |
+
print(message)
|
280 |
|
281 |
def retrieve_context_from_blog(query: str, k: int = 3) -> str:
|
282 |
"""Busca no vector store por chunks de texto similares à pergunta."""
|
|
|
369 |
api_client = HuggingFaceAPIClient(HF_TOKEN)
|
370 |
|
371 |
# ==============================================================================
|
372 |
+
# SEÇÃO PRINCIPAL: LÓGICA DO CHATBOT COM MEMÓRIA
|
373 |
# ==============================================================================
|
374 |
|
375 |
def formatar_resposta_com_codigo(resposta: str) -> str:
|
376 |
"""Formata a resposta destacando códigos em blocos separados."""
|
377 |
if not resposta: return resposta
|
378 |
|
|
|
379 |
resposta = resposta.replace('<', '<').replace('>', '>')
|
380 |
resposta_formatada = re.sub(
|
381 |
r'```(\w+)?\n(.*?)\n```',
|
|
|
396 |
return resposta_formatada
|
397 |
|
398 |
def responder_como_aldo(pergunta: str, modelo_escolhido: str = DEFAULT_MODEL) -> str:
|
399 |
+
"""Função principal para gerar respostas, agora com RAG e MEMÓRIA."""
|
400 |
if not pergunta.strip():
|
401 |
return "Por favor, faça uma pergunta."
|
402 |
|
403 |
try:
|
404 |
+
# Atualiza o perfil do usuário baseado na pergunta
|
405 |
+
update_user_profile(pergunta)
|
406 |
+
|
407 |
# --- ETAPA DE RAG ---
|
408 |
print(f"Buscando contexto para a pergunta: '{pergunta[:50]}...'")
|
409 |
contexto_blog = retrieve_context_from_blog(pergunta)
|
410 |
|
411 |
+
# --- ETAPA DE MEMÓRIA ---
|
412 |
+
contexto_conversa = get_conversation_context()
|
413 |
+
contexto_perfil = get_user_profile_context()
|
414 |
+
|
415 |
+
# Prompt do sistema personalizado baseado no perfil do usuário
|
416 |
+
nivel = user_profile.get('nivel', 'intermediario')
|
417 |
+
|
418 |
+
system_prompt = f"""Você é o professor Dr. Aldo Henrique, especialista em C, Java, desenvolvimento web e inteligência artificial.
|
419 |
+
|
420 |
+
PERSONALIDADE E COMPORTAMENTO:
|
421 |
+
- Seja caloroso, acolhedor e paciente como um professor humano experiente
|
422 |
+
- Demonstre interesse genuíno pelo aprendizado do aluno
|
423 |
+
- Use um tom conversacional e amigável, mas mantenha autoridade acadêmica
|
424 |
+
- Quando apropriado, faça conexões com conversas anteriores
|
425 |
+
- Celebre o progresso do aluno e encoraje quando necessário
|
426 |
+
- Adapte sua linguagem ao nível do aluno: {nivel}
|
427 |
+
|
428 |
+
ESTILO DE ENSINO:
|
429 |
+
- Sempre explique o "porquê" antes do "como"
|
430 |
+
- Use analogias e exemplos práticos
|
431 |
+
- Encoraje perguntas e curiosidade
|
432 |
+
- Quando mostrar código, sempre explique com comentários detalhados
|
433 |
+
- Foque na compreensão, não apenas na solução
|
434 |
+
- Conecte conceitos com aplicações do mundo real
|
435 |
+
|
436 |
+
REGRAS:
|
437 |
+
- Responda sempre em português brasileiro
|
438 |
+
- Use blocos de código formatados com ```
|
439 |
+
- Só responda perguntas relacionadas a programação e tecnologia
|
440 |
+
- Se não for sobre TI, informe educadamente que sua especialidade é tecnologia
|
441 |
+
- Quando apresentar código, sempre explique linha por linha nos comentários"""
|
442 |
+
|
443 |
+
# Monta o prompt completo com todos os contextos
|
444 |
+
prompt_parts = []
|
445 |
|
446 |
+
if contexto_perfil:
|
447 |
+
prompt_parts.append(contexto_perfil)
|
448 |
+
|
449 |
+
if contexto_conversa:
|
450 |
+
prompt_parts.append(contexto_conversa)
|
451 |
+
|
452 |
if contexto_blog:
|
453 |
+
prompt_parts.append("--- CONTEXTO DO SEU BLOG ---")
|
454 |
+
prompt_parts.append(contexto_blog)
|
455 |
+
prompt_parts.append("--- FIM DO CONTEXTO DO BLOG ---")
|
456 |
+
|
457 |
+
prompt_parts.append(f"PERGUNTA ATUAL DO ALUNO: {pergunta}")
|
458 |
+
|
459 |
+
# Adiciona instruções específicas baseadas no contexto
|
460 |
+
if contexto_conversa:
|
461 |
+
prompt_parts.append("\nIMPORTANTE: Considere o contexto da nossa conversa anterior ao responder. Se esta pergunta se relaciona com algo que já discutimos, faça essa conexão naturalmente.")
|
462 |
+
|
463 |
+
pergunta_completa = "\n\n".join(prompt_parts)
|
464 |
+
|
|
|
|
|
|
|
|
|
|
|
465 |
messages = [
|
466 |
{"role": "system", "content": system_prompt},
|
467 |
{"role": "user", "content": pergunta_completa}
|
|
|
472 |
|
473 |
if resposta.startswith("Assistente: "):
|
474 |
resposta = resposta.replace("Assistente: ", "")
|
475 |
+
|
476 |
+
# Adiciona a conversa à memória
|
477 |
+
add_to_memory(pergunta, resposta)
|
478 |
+
|
479 |
resposta_formatada = formatar_resposta_com_codigo(resposta.strip())
|
480 |
return resposta_formatada
|
481 |
|
482 |
except Exception as e:
|
483 |
return f"Erro ao processar sua pergunta: {str(e)}"
|
484 |
|
485 |
+
# ==============================================================================
|
486 |
+
# FUNÇÕES AUXILIARES E DE TESTE
|
487 |
+
# ==============================================================================
|
488 |
+
|
489 |
def verificar_modelo_disponivel(model_name: str) -> str:
|
490 |
try:
|
491 |
url = f"https://api-inference.huggingface.co/models/{model_name}"
|
|
|
504 |
for nome, modelo in MODELS.items():
|
505 |
status = verificar_modelo_disponivel(modelo)
|
506 |
resultados.append(f"{nome}: {status}")
|
507 |
+
return "\n".join(resultados)
|
508 |
+
|
509 |
+
def get_memory_stats() -> str:
|
510 |
+
"""Retorna estatísticas da memória atual."""
|
511 |
+
total_messages = len(conversation_memory)
|
512 |
+
user_messages = len([m for m in conversation_memory if m["role"] == "user"])
|
513 |
+
|
514 |
+
stats = f"📊 **Estatísticas da Memória:**\n"
|
515 |
+
stats += f"• Total de mensagens: {total_messages}\n"
|
516 |
+
stats += f"• Perguntas do usuário: {user_messages}\n"
|
517 |
+
stats += f"• Nível detectado: {user_profile.get('nivel', 'Não definido')}\n"
|
518 |
+
|
519 |
+
# Principais interesses
|
520 |
+
interesses = []
|
521 |
+
for key, value in user_profile.items():
|
522 |
+
if key.startswith('interesse_') and value > 0:
|
523 |
+
topic = key.replace('interesse_', '').replace('_', ' ').title()
|
524 |
+
interesses.append(f"{topic} ({value})")
|
525 |
+
|
526 |
+
if interesses:
|
527 |
+
stats += f"• Principais interesses: {', '.join(interesses)}\n"
|
528 |
+
|
529 |
+
return stats
|
530 |
+
|
531 |
+
# ==============================================================================
|
532 |
+
# INICIALIZAÇÃO DO SISTEMA
|
533 |
+
# ==============================================================================
|
534 |
+
|
535 |
+
def inicializar_sistema():
|
536 |
+
"""Inicializa todos os componentes do sistema."""
|
537 |
+
print("🚀 Inicializando o Chatbot Dr. Aldo Henrique com Memória...")
|
538 |
+
|
539 |
+
# Carrega o vector store (RAG)
|
540 |
+
load_vector_store()
|
541 |
+
|
542 |
+
# Carrega a memória de conversas
|
543 |
+
load_conversation_memory()
|
544 |
+
|
545 |
+
print("✅ Sistema inicializado com sucesso!")
|
546 |
+
print(f"💬 Memória: {len(conversation_memory)} mensagens carregadas")
|
547 |
+
print(f"🧠 Vector Store: {'Carregado' if vector_store else 'Não encontrado'}")
|
548 |
+
|
549 |
+
# Chama a inicialização quando o módulo é carregado
|
550 |
+
if __name__ == "__main__":
|
551 |
+
inicializar_sistema()
|