Ojuaragabriel commited on
Commit
b791a6d
·
0 Parent(s):

Initial commit - RAGStudy offline RAG chat

Browse files
Files changed (4) hide show
  1. .gitignore +20 -0
  2. RAGStudy.py +556 -0
  3. README.md +27 -0
  4. requirements.txt +2 -0
.gitignore ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Ambiente virtual
2
+ .venv/
3
+
4
+ # Build do PyInstaller
5
+ build/
6
+ dist/
7
+ *.spec
8
+
9
+ # Dados locais / materiais de estudo
10
+ materiais/
11
+ knowledge_base.json
12
+
13
+ # Arquivos temporários do Python
14
+ __pycache__/
15
+ *.py[cod]
16
+ *.pyo
17
+
18
+ # Lixo de sistema operacional
19
+ .DS_Store
20
+ Thumbs.db
RAGStudy.py ADDED
@@ -0,0 +1,556 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import re
3
+ import json
4
+ import sys
5
+ import pdfplumber
6
+ import requests
7
+ import tkinter as tk
8
+ from tkinter import scrolledtext, messagebox, simpledialog
9
+
10
+ # ===== CONFIGURAÇÕES GERAIS =====
11
+
12
+ # Diretório base:
13
+ # - se estiver rodando como .exe (PyInstaller), usa a pasta do .exe
14
+ # - se estiver rodando como .py, usa a pasta do arquivo .py
15
+ if getattr(sys, "frozen", False):
16
+ BASE_DIR = os.path.dirname(os.path.abspath(sys.argv[0]))
17
+ else:
18
+ BASE_DIR = os.path.dirname(os.path.abspath(__file__))
19
+
20
+ DOCS_DIR = os.path.join(BASE_DIR, "materiais")
21
+ KNOWLEDGE_PATH = os.path.join(BASE_DIR, "knowledge_base.json")
22
+
23
+ MAX_CONTEXT_CHARS = 6000
24
+
25
+ LM_STUDIO_URL = "http://127.0.0.1:1234/v1/chat/completions"
26
+ LM_API_KEY = "lm-studio"
27
+ MODEL_NAME = "meta-llama-3.1-8b-instruct"
28
+
29
+ # Base de conhecimento em memória
30
+ KB = []
31
+
32
+
33
+ # ===== BACKEND (RAG) =====
34
+
35
+ def extract_text_from_pdf(pdf_path: str) -> str:
36
+ text = ""
37
+ with pdfplumber.open(pdf_path) as pdf:
38
+ for page in pdf.pages:
39
+ page_text = page.extract_text()
40
+ if page_text:
41
+ text += page_text + "\n"
42
+ return text
43
+
44
+
45
+ def build_knowledge_base():
46
+ """
47
+ Lê todos os PDFs em DOCS_DIR e salva knowledge_base.json.
48
+ Retorna a lista de parágrafos (kb).
49
+ """
50
+ kb = []
51
+
52
+ if not os.path.isdir(DOCS_DIR):
53
+ os.makedirs(DOCS_DIR, exist_ok=True)
54
+
55
+ pdf_files = [f for f in os.listdir(DOCS_DIR) if f.lower().endswith(".pdf")]
56
+
57
+ if not pdf_files:
58
+ return []
59
+
60
+ for filename in pdf_files:
61
+ full_path = os.path.join(DOCS_DIR, filename)
62
+ raw_text = extract_text_from_pdf(full_path)
63
+
64
+ paragraphs = re.split(r"\n\s*\n", raw_text)
65
+ for p in paragraphs:
66
+ p_clean = p.strip()
67
+ if not p_clean:
68
+ continue
69
+ kb.append({"source": filename, "text": p_clean})
70
+
71
+ with open(KNOWLEDGE_PATH, "w", encoding="utf-8") as f:
72
+ json.dump(kb, f, ensure_ascii=False, indent=2)
73
+
74
+ return kb
75
+
76
+
77
+ def load_knowledge_base():
78
+ if not os.path.exists(KNOWLEDGE_PATH):
79
+ return []
80
+ with open(KNOWLEDGE_PATH, "r", encoding="utf-8") as f:
81
+ kb = json.load(f)
82
+ return kb
83
+
84
+
85
+ def search_paragraphs(question: str, kb, top_k: int = 5):
86
+ tokens = re.findall(r"\w+", question.lower())
87
+ tokens = [t for t in tokens if len(t) > 2]
88
+
89
+ scored = []
90
+ for item in kb:
91
+ text_lower = item["text"].lower()
92
+ score = sum(text_lower.count(tok) for tok in tokens)
93
+ if score > 0:
94
+ scored.append((score, item))
95
+
96
+ scored.sort(key=lambda x: x[0], reverse=True)
97
+ top_items = [item for score, item in scored[:top_k]]
98
+ return top_items
99
+
100
+
101
+ def build_context_string(chunks):
102
+ parts = []
103
+ for c in chunks:
104
+ parts.append(f"[Fonte: {c['source']}]\n{c['text']}")
105
+ context = "\n\n".join(parts)
106
+ if len(context) > MAX_CONTEXT_CHARS:
107
+ context = context[:MAX_CONTEXT_CHARS]
108
+ return context
109
+
110
+
111
+ def call_llm(system_prompt: str, user_prompt: str) -> str:
112
+ payload = {
113
+ "model": MODEL_NAME,
114
+ "messages": [
115
+ {"role": "system", "content": system_prompt},
116
+ {"role": "user", "content": user_prompt},
117
+ ],
118
+ "temperature": 0.2,
119
+ }
120
+
121
+ resp = requests.post(
122
+ LM_STUDIO_URL,
123
+ headers={
124
+ "Content-Type": "application/json",
125
+ "Authorization": f"Bearer {LM_API_KEY}"
126
+ },
127
+ json=payload,
128
+ timeout=600,
129
+ )
130
+ resp.raise_for_status()
131
+ data = resp.json()
132
+ return data["choices"][0]["message"]["content"].strip()
133
+
134
+
135
+ def resumo_geral(kb):
136
+ chunks = kb[:8] if len(kb) > 8 else kb
137
+ context = build_context_string(chunks)
138
+
139
+ system_prompt = (
140
+ "Você é um assistente de estudos. "
141
+ "Você recebe trechos de materiais em CONTEXTO e deve gerar um resumo didático em português, "
142
+ "organizando os principais tópicos e ideias de forma clara e objetiva. "
143
+ "Não invente informações fora do contexto."
144
+ )
145
+
146
+ user_prompt = (
147
+ "Use o CONTEXTO a seguir para gerar um resumo geral dos principais pontos, "
148
+ "organizado em tópicos, com frases curtas e linguagem simples.\n\n"
149
+ f"CONTEXTO:\n{context}"
150
+ )
151
+
152
+ return call_llm(system_prompt, user_prompt)
153
+
154
+
155
+ def pontos_chave(kb):
156
+ chunks = kb[:8] if len(kb) > 8 else kb
157
+ context = build_context_string(chunks)
158
+
159
+ system_prompt = (
160
+ "Você é um assistente de estudos. "
161
+ "Você recebe trechos de materiais em CONTEXTO e deve listar os pontos chave em português, "
162
+ "como se fossem itens de revisão rápida para prova."
163
+ )
164
+
165
+ user_prompt = (
166
+ "Use o CONTEXTO a seguir para listar os principais pontos que a pessoa precisa lembrar, "
167
+ "em formato de tópicos. Foque em conceitos importantes, definições e ideias centrais.\n\n"
168
+ f"CONTEXTO:\n{context}"
169
+ )
170
+
171
+ return call_llm(system_prompt, user_prompt)
172
+
173
+
174
+ def perguntas_estudo(kb, tema: str = "", n_questoes: int = 10):
175
+ if tema:
176
+ chunks = search_paragraphs(tema, kb, top_k=10)
177
+ if not chunks:
178
+ chunks = kb[:20] if len(kb) > 20 else kb
179
+ else:
180
+ chunks = kb[:20] if len(kb) > 20 else kb
181
+
182
+ context = build_context_string(chunks)
183
+
184
+ system_prompt = (
185
+ "Você é um professor ajudando um estudante a revisar o conteúdo. "
186
+ "Você recebe trechos de materiais em CONTEXTO e deve gerar perguntas de estudo em português. "
187
+ "Não inclua as respostas, apenas as perguntas."
188
+ )
189
+
190
+ user_prompt = (
191
+ f"Use o CONTEXTO abaixo para criar aproximadamente {n_questoes} perguntas de estudo. "
192
+ "Misture perguntas de definição, compreensão e comparação, mas sempre baseadas apenas no contexto.\n\n"
193
+ f"CONTEXTO:\n{context}"
194
+ )
195
+
196
+ return call_llm(system_prompt, user_prompt)
197
+
198
+
199
+ def responder_chat(kb, pergunta: str) -> str:
200
+ chunks = search_paragraphs(pergunta, kb, top_k=3)
201
+ if not chunks:
202
+ return "Não encontrei essa informação nos materiais carregados."
203
+
204
+ context = build_context_string(chunks)
205
+
206
+ system_prompt = (
207
+ "Você é um assistente de estudos. "
208
+ "Responda em português de forma clara, objetiva e didática, usando apenas o que está no CONTEXTO. "
209
+ "Se a resposta não estiver no contexto, diga apenas: "
210
+ "'Não encontrei essa informação nos materiais carregados.' "
211
+ "Não invente informações e não use conhecimento externo."
212
+ )
213
+
214
+ user_prompt = f"CONTEXTO:\n{context}\n\nPERGUNTA:\n{pergunta}"
215
+ return call_llm(system_prompt, user_prompt)
216
+
217
+
218
+ # ===== GUI ESTILO CHAT (RAGStudy) =====
219
+
220
+ class RAGStudyChatApp:
221
+ def __init__(self, root):
222
+ self.root = root
223
+ self.root.title("RAGStudy - Assistente de Estudos Offline")
224
+ self.root.geometry("950x620")
225
+ self.root.configure(bg="#202123")
226
+
227
+ # ===== TOPO =====
228
+ top_frame = tk.Frame(root, bg="#202123")
229
+ top_frame.pack(fill="x", padx=10, pady=(10, 5))
230
+
231
+ title_label = tk.Label(
232
+ top_frame,
233
+ text="RAGStudy",
234
+ font=("Segoe UI", 13, "bold"),
235
+ bg="#202123",
236
+ fg="white"
237
+ )
238
+ title_label.pack(side="left")
239
+
240
+ self.status_label = tk.Label(
241
+ top_frame,
242
+ text="Base não carregada.",
243
+ bg="#202123",
244
+ fg="#d1d5db",
245
+ anchor="w"
246
+ )
247
+ self.status_label.pack(side="left", padx=15)
248
+
249
+ self.btn_recarregar = tk.Button(
250
+ top_frame,
251
+ text="Recarregar PDFs",
252
+ command=self.on_recarregar,
253
+ bg="#10a37f",
254
+ fg="white",
255
+ relief="flat",
256
+ padx=10,
257
+ pady=3
258
+ )
259
+ self.btn_recarregar.pack(side="right")
260
+
261
+ # Caminho da pasta
262
+ path_frame = tk.Frame(root, bg="#202123")
263
+ path_frame.pack(fill="x", padx=10, pady=(0, 8))
264
+ path_label = tk.Label(
265
+ path_frame,
266
+ text=f"Pasta de materiais: {DOCS_DIR}",
267
+ bg="#202123",
268
+ fg="#9ca3af",
269
+ anchor="w",
270
+ justify="left"
271
+ )
272
+ path_label.pack(fill="x")
273
+
274
+ # ===== ATALHOS (Resumo, Pontos, Perguntas) =====
275
+ shortcuts_frame = tk.Frame(root, bg="#202123")
276
+ shortcuts_frame.pack(fill="x", padx=10, pady=(0, 5))
277
+
278
+ tk.Label(
279
+ shortcuts_frame,
280
+ text="Atalhos:",
281
+ bg="#202123",
282
+ fg="#d1d5db"
283
+ ).pack(side="left", padx=(0, 5))
284
+
285
+ self.btn_resumo = tk.Button(
286
+ shortcuts_frame,
287
+ text="Resumo geral",
288
+ command=self.on_resumo,
289
+ bg="#3a3b44",
290
+ fg="white",
291
+ relief="flat",
292
+ padx=8
293
+ )
294
+ self.btn_resumo.pack(side="left", padx=3)
295
+
296
+ self.btn_pontos = tk.Button(
297
+ shortcuts_frame,
298
+ text="Pontos chave",
299
+ command=self.on_pontos,
300
+ bg="#3a3b44",
301
+ fg="white",
302
+ relief="flat",
303
+ padx=8
304
+ )
305
+ self.btn_pontos.pack(side="left", padx=3)
306
+
307
+ self.btn_perguntas = tk.Button(
308
+ shortcuts_frame,
309
+ text="Perguntas de estudo",
310
+ command=self.on_perguntas,
311
+ bg="#3a3b44",
312
+ fg="white",
313
+ relief="flat",
314
+ padx=8
315
+ )
316
+ self.btn_perguntas.pack(side="left", padx=3)
317
+
318
+ # ===== ÁREA DE CHAT =====
319
+ chat_frame = tk.Frame(root, bg="#202123")
320
+ chat_frame.pack(fill="both", expand=True, padx=10, pady=(5, 5))
321
+
322
+ self.chat_box = scrolledtext.ScrolledText(
323
+ chat_frame,
324
+ wrap="word",
325
+ bg="#343541",
326
+ fg="white",
327
+ insertbackground="white",
328
+ bd=0,
329
+ padx=10,
330
+ pady=10
331
+ )
332
+ self.chat_box.pack(fill="both", expand=True)
333
+
334
+ # Configura tags de estilo
335
+ self.chat_box.tag_configure(
336
+ "user_name",
337
+ foreground="#10a37f",
338
+ font=("Segoe UI", 9, "bold")
339
+ )
340
+ self.chat_box.tag_configure(
341
+ "assistant_name",
342
+ foreground="#f97316",
343
+ font=("Segoe UI", 9, "bold")
344
+ )
345
+ self.chat_box.tag_configure(
346
+ "user_msg",
347
+ background="#444654",
348
+ foreground="white",
349
+ lmargin1=15,
350
+ lmargin2=15,
351
+ rmargin=50,
352
+ spacing3=8
353
+ )
354
+ self.chat_box.tag_configure(
355
+ "assistant_msg",
356
+ background="#343541",
357
+ foreground="white",
358
+ lmargin1=15,
359
+ lmargin2=15,
360
+ rmargin=50,
361
+ spacing3=12
362
+ )
363
+
364
+ self.chat_box.configure(state="disabled")
365
+
366
+ # ===== ÁREA DE INPUT (BAIXO) =====
367
+ input_frame = tk.Frame(root, bg="#202123")
368
+ input_frame.pack(fill="x", padx=10, pady=(0, 10))
369
+
370
+ self.entry = tk.Entry(
371
+ input_frame,
372
+ bg="#40414f",
373
+ fg="white",
374
+ insertbackground="white",
375
+ relief="flat"
376
+ )
377
+ self.entry.pack(side="left", fill="x", expand=True, padx=(0, 8), ipady=6)
378
+ self.entry.bind("<Return>", self.on_send)
379
+
380
+ send_btn = tk.Button(
381
+ input_frame,
382
+ text="Enviar",
383
+ command=self.on_send,
384
+ bg="#10a37f",
385
+ fg="white",
386
+ relief="flat",
387
+ padx=12,
388
+ pady=4
389
+ )
390
+ send_btn.pack(side="right")
391
+
392
+ # Mensagem inicial
393
+ self.log_assistant(
394
+ "Olá! Eu sou o RAGStudy, seu assistente de estudos offline.\n\n"
395
+ "- Coloque PDFs na pasta 'materiais'.\n"
396
+ "- Clique em 'Recarregar PDFs' para atualizar a base.\n"
397
+ "- Use os atalhos acima para resumo, pontos chave e perguntas.\n"
398
+ "- Ou digite qualquer pergunta no campo abaixo."
399
+ )
400
+
401
+ # Carrega base se já existir
402
+ self.load_initial_kb()
403
+
404
+ # ===== Helpers de log =====
405
+
406
+ def log_user(self, text: str):
407
+ self.chat_box.configure(state="normal")
408
+ self.chat_box.insert("end", "Você\n", "user_name")
409
+ self.chat_box.insert("end", text + "\n\n", "user_msg")
410
+ self.chat_box.configure(state="disabled")
411
+ self.chat_box.see("end")
412
+
413
+ def log_assistant(self, text: str):
414
+ self.chat_box.configure(state="normal")
415
+ self.chat_box.insert("end", "Assistente\n", "assistant_name")
416
+ self.chat_box.insert("end", text + "\n\n", "assistant_msg")
417
+ self.chat_box.configure(state="disabled")
418
+ self.chat_box.see("end")
419
+
420
+ def set_status(self, text: str):
421
+ self.status_label.config(text=text)
422
+
423
+ # ===== Gerenciamento da KB =====
424
+
425
+ def load_initial_kb(self):
426
+ global KB
427
+ kb = load_knowledge_base()
428
+ if kb:
429
+ KB = kb
430
+ self.set_status(f"Base carregada com {len(KB)} parágrafos.")
431
+ self.log_assistant(
432
+ "Base carregada a partir do arquivo existente.\n"
433
+ "Se quiser atualizar com novos PDFs, clique em 'Recarregar PDFs'."
434
+ )
435
+
436
+ def on_recarregar(self):
437
+ global KB
438
+
439
+ if not os.path.isdir(DOCS_DIR):
440
+ os.makedirs(DOCS_DIR, exist_ok=True)
441
+
442
+ pdf_files = [f for f in os.listdir(DOCS_DIR) if f.lower().endswith(".pdf")]
443
+
444
+ if not pdf_files:
445
+ msg = (
446
+ "Nenhum PDF encontrado na pasta:\n"
447
+ f"{DOCS_DIR}\n\n"
448
+ "Coloque os arquivos .pdf do assunto que você quer estudar nessa pasta\n"
449
+ "e clique em 'Recarregar PDFs' novamente."
450
+ )
451
+ self.set_status("Nenhum PDF encontrado.")
452
+ self.log_assistant(msg)
453
+ return
454
+
455
+ self.set_status("Reconstruindo base a partir dos PDFs...")
456
+ self.log_assistant("Reconstruindo base a partir dos PDFs... Isso pode levar alguns instantes.")
457
+
458
+ try:
459
+ kb = build_knowledge_base()
460
+ KB = kb
461
+ self.set_status(f"Base carregada com {len(KB)} parágrafos.")
462
+ self.log_assistant(f"Base criada com {len(KB)} parágrafos a partir de {len(pdf_files)} PDF(s).")
463
+ except Exception as e:
464
+ messagebox.showerror("Erro", f"Erro ao reconstruir base: {e}")
465
+ self.log_assistant(f"Erro ao reconstruir base: {e}")
466
+
467
+ def ensure_kb(self) -> bool:
468
+ global KB
469
+ if not KB:
470
+ messagebox.showwarning(
471
+ "Base vazia",
472
+ "Nenhum conteúdo carregado.\n\n"
473
+ "Coloque PDFs na pasta 'materiais' e clique em 'Recarregar PDFs'."
474
+ )
475
+ return False
476
+ return True
477
+
478
+ # ===== Ações dos botões =====
479
+
480
+ def on_resumo(self):
481
+ if not self.ensure_kb():
482
+ return
483
+ self.log_user("/resumo geral")
484
+ self.log_assistant("Gerando resumo geral... (isso pode demorar um pouco)")
485
+ try:
486
+ texto = resumo_geral(KB)
487
+ self.log_assistant(texto)
488
+ except Exception as e:
489
+ messagebox.showerror("Erro", f"Erro ao gerar resumo: {e}")
490
+ self.log_assistant(f"Erro ao gerar resumo: {e}")
491
+
492
+ def on_pontos(self):
493
+ if not self.ensure_kb():
494
+ return
495
+ self.log_user("/pontos chave")
496
+ self.log_assistant("Gerando pontos chave... (isso pode demorar um pouco)")
497
+ try:
498
+ texto = pontos_chave(KB)
499
+ self.log_assistant(texto)
500
+ except Exception as e:
501
+ messagebox.showerror("Erro", f"Erro ao gerar pontos chave: {e}")
502
+ self.log_assistant(f"Erro ao gerar pontos chave: {e}")
503
+
504
+ def on_perguntas(self):
505
+ if not self.ensure_kb():
506
+ return
507
+
508
+ tema = simpledialog.askstring(
509
+ "Perguntas de estudo",
510
+ "Tema/dúvida para gerar perguntas (deixe vazio para usar o conteúdo geral):"
511
+ )
512
+ if tema is None:
513
+ return
514
+
515
+ if tema.strip():
516
+ self.log_user(f"/perguntas de estudo sobre: {tema}")
517
+ else:
518
+ self.log_user("/perguntas de estudo (geral)")
519
+
520
+ self.log_assistant("Gerando perguntas de estudo... (isso pode demorar um pouco)")
521
+ try:
522
+ texto = perguntas_estudo(KB, tema=tema or "", n_questoes=10)
523
+ self.log_assistant(texto)
524
+ except Exception as e:
525
+ messagebox.showerror("Erro", f"Erro ao gerar perguntas: {e}")
526
+ self.log_assistant(f"Erro ao gerar perguntas: {e}")
527
+
528
+ # ===== Chat livre =====
529
+
530
+ def on_send(self, event=None):
531
+ if not self.ensure_kb():
532
+ return
533
+ pergunta = self.entry.get().strip()
534
+ if not pergunta:
535
+ return
536
+ self.entry.delete(0, tk.END)
537
+
538
+ self.log_user(pergunta)
539
+ self.log_assistant("Gerando resposta... (aguarde)")
540
+
541
+ try:
542
+ resposta = responder_chat(KB, pergunta)
543
+ self.log_assistant(resposta)
544
+ except Exception as e:
545
+ messagebox.showerror("Erro", f"Erro no chat: {e}")
546
+ self.log_assistant(f"Erro no chat: {e}")
547
+
548
+
549
+ def main():
550
+ root = tk.Tk()
551
+ app = RAGStudyChatApp(root)
552
+ root.mainloop()
553
+
554
+
555
+ if __name__ == "__main__":
556
+ main()
README.md ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # RAGStudy
2
+
3
+ RAGStudy é um assistente de estudo offline que lê PDFs de uma pasta local, constrói uma base de conhecimento e permite fazer perguntas em uma interface de chat usando um LLM local (via LM Studio).
4
+
5
+ ## Como funciona
6
+
7
+ - Lê todos os PDFs na pasta `materiais/`
8
+ - Extrai o texto e monta uma base de parágrafos
9
+ - Usa um RAG simples (busca por termos) para encontrar trechos relevantes
10
+ - Envia o contexto para um modelo local rodando no LM Studio (ex.: `meta-llama-3.1-8b-instruct`)
11
+ - Responde em uma interface tipo chat (Tkinter)
12
+
13
+ ## Requisitos
14
+
15
+ - Python 3.10+
16
+ - LM Studio instalado
17
+ - Um modelo carregado no LM Studio (ex.: `meta-llama-3.1-8b-instruct`)
18
+ - Servidor do LM Studio ativo em `http://127.0.0.1:1234`
19
+
20
+ ## Instalação
21
+
22
+ ```bash
23
+ git clone https://huggingface.co/<seu-usuario>/RAGStudy
24
+ cd RAGStudy
25
+ python -m venv .venv
26
+ source .venv/bin/activate # ou .venv\Scripts\Activate.ps1 no Windows
27
+ pip install -r requirements.txt
requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ pdfplumber
2
+ requests