Madras1 commited on
Commit
57907f8
·
verified ·
1 Parent(s): 6513eaa

Upload 13 files

Browse files
Files changed (4) hide show
  1. Dockerfile +4 -1
  2. app.py +129 -12
  3. jade/scholar_agent.py +438 -0
  4. requirements.txt +11 -1
Dockerfile CHANGED
@@ -1,6 +1,9 @@
1
  # Usa uma imagem Python leve e moderna
2
  FROM python:3.10-slim
3
 
 
 
 
4
  # Define a pasta de trabalho dentro do container
5
  WORKDIR /app
6
 
@@ -19,4 +22,4 @@ RUN mkdir -p /app/jade_memory_db && chmod 777 /app/jade_memory_db
19
 
20
  # Comando para ligar o servidor
21
  # O Hugging Face sempre espera a porta 7860
22
- CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
 
1
  # Usa uma imagem Python leve e moderna
2
  FROM python:3.10-slim
3
 
4
+ # Instala dependências do sistema (FFmpeg para áudio, Graphviz para mapas mentais)
5
+ RUN apt-get update && apt-get install -y ffmpeg graphviz && rm -rf /var/lib/apt/lists/*
6
+
7
  # Define a pasta de trabalho dentro do container
8
  WORKDIR /app
9
 
 
22
 
23
  # Comando para ligar o servidor
24
  # O Hugging Face sempre espera a porta 7860
25
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
app.py CHANGED
@@ -2,11 +2,15 @@
2
  import os
3
  import base64
4
  import io
5
- from fastapi import FastAPI
 
6
  from fastapi.middleware.cors import CORSMiddleware
 
 
7
  from pydantic import BaseModel
8
  from PIL import Image
9
- from jade.core import JadeAgent
 
10
 
11
  print("Iniciando a J.A.D.E. com FastAPI...")
12
  agent = JadeAgent()
@@ -18,14 +22,50 @@ app.add_middleware(
18
  allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"],
19
  )
20
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  # Dicionário global para armazenar sessões de usuários
22
  user_sessions = {}
 
23
 
24
  class UserRequest(BaseModel):
25
  user_input: str
26
  image_base64: str | None = None
27
  user_id: str | None = None
28
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
  @app.post("/chat")
30
  def handle_chat(request: UserRequest):
31
  try:
@@ -64,27 +104,104 @@ def handle_chat(request: UserRequest):
64
  # LÓGICA DO ÁUDIO: Converte o arquivo MP3 gerado para Base64
65
  audio_base64 = None
66
  if audio_path and os.path.exists(audio_path):
67
- print(f"Codificando arquivo de áudio: {audio_path}")
68
- with open(audio_path, "rb") as audio_file:
69
- audio_bytes = audio_file.read()
70
- audio_base64 = base64.b64encode(audio_bytes).decode('utf-8')
71
- os.remove(audio_path) # Importante: Limpa o arquivo temporário do servidor
72
 
73
  return {
74
  "success": True,
75
  "bot_response": bot_response_text,
76
- "audio_base64": audio_base64 # Envia o áudio como texto para o front-end
77
  }
78
  except Exception as e:
79
  print(f"Erro crítico no endpoint /chat: {e}")
80
  return {"success": False, "error": str(e)}
81
 
82
- @app.get("/")
83
- def root():
84
- return {"message": "Servidor J.A.D.E. com FastAPI está online."}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
 
86
  if __name__ == "__main__":
87
  import uvicorn
88
  port = int(os.environ.get("PORT", 7860))
89
  print(f"Iniciando o servidor Uvicorn em http://0.0.0.0:{port}")
90
- uvicorn.run(app, host="0.0.0.0", port=port)
 
2
  import os
3
  import base64
4
  import io
5
+ import shutil
6
+ from fastapi import FastAPI, HTTPException
7
  from fastapi.middleware.cors import CORSMiddleware
8
+ from fastapi.responses import FileResponse
9
+ from fastapi.staticfiles import StaticFiles
10
  from pydantic import BaseModel
11
  from PIL import Image
12
+ from jade.core import JadeAgent
13
+ from jade.scholar_agent import ScholarAgent
14
 
15
  print("Iniciando a J.A.D.E. com FastAPI...")
16
  agent = JadeAgent()
 
22
  allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"],
23
  )
24
 
25
+ # Mount frontend directory
26
+ frontend_path = os.path.join(os.path.dirname(__file__), "frontend")
27
+ if os.path.exists(frontend_path):
28
+ print(f"Montando frontend estático em: {frontend_path}")
29
+ app.mount("/static", StaticFiles(directory=frontend_path), name="static")
30
+
31
+ @app.get("/")
32
+ async def read_index():
33
+ return FileResponse(os.path.join(frontend_path, "index.html"))
34
+ else:
35
+ print(f"⚠️ Frontend não encontrado em: {frontend_path}")
36
+ @app.get("/")
37
+ def root():
38
+ return {"message": "Servidor J.A.D.E. com FastAPI está online. Frontend não encontrado."}
39
+
40
  # Dicionário global para armazenar sessões de usuários
41
  user_sessions = {}
42
+ scholar_sessions = {} # Armazena instâncias de ScholarAgent por usuário
43
 
44
  class UserRequest(BaseModel):
45
  user_input: str
46
  image_base64: str | None = None
47
  user_id: str | None = None
48
 
49
+ class ScholarRequest(BaseModel):
50
+ user_id: str
51
+ target: str | None = None # URL, Tópico ou Texto
52
+ action: str | None = None # ingest, summarize, mindmap, podcast, debate, quiz, flashcards, handout
53
+ mode: str | None = "lecture" # Para podcast/debate
54
+
55
+ def get_scholar_agent(user_id: str):
56
+ if user_id not in scholar_sessions:
57
+ print(f"Criando novo Agente Scholar para: {user_id}")
58
+ scholar_sessions[user_id] = ScholarAgent()
59
+ return scholar_sessions[user_id]
60
+
61
+ def encode_file_base64(filepath):
62
+ if filepath and os.path.exists(filepath):
63
+ print(f"Codificando arquivo: {filepath}")
64
+ with open(filepath, "rb") as f:
65
+ encoded = base64.b64encode(f.read()).decode('utf-8')
66
+ return encoded
67
+ return None
68
+
69
  @app.post("/chat")
70
  def handle_chat(request: UserRequest):
71
  try:
 
104
  # LÓGICA DO ÁUDIO: Converte o arquivo MP3 gerado para Base64
105
  audio_base64 = None
106
  if audio_path and os.path.exists(audio_path):
107
+ audio_base64 = encode_file_base64(audio_path)
108
+ os.remove(audio_path) # Limpa arquivo
 
 
 
109
 
110
  return {
111
  "success": True,
112
  "bot_response": bot_response_text,
113
+ "audio_base64": audio_base64
114
  }
115
  except Exception as e:
116
  print(f"Erro crítico no endpoint /chat: {e}")
117
  return {"success": False, "error": str(e)}
118
 
119
+ @app.post("/scholar")
120
+ def handle_scholar(request: ScholarRequest):
121
+ try:
122
+ user_id = request.user_id
123
+ scholar = get_scholar_agent(user_id)
124
+
125
+ response = {"success": True, "message": "", "data": None, "file_base64": None, "file_type": None}
126
+
127
+ if request.action == "ingest":
128
+ if not request.target:
129
+ raise HTTPException(status_code=400, detail="Target is required for ingest.")
130
+ success = scholar.ingest(request.target)
131
+ if success:
132
+ response["message"] = f"Conteúdo sobre '{request.target}' processado com sucesso!"
133
+ else:
134
+ response["success"] = False
135
+ response["message"] = "Falha ao processar conteúdo. Tente outro link ou tópico."
136
+
137
+ elif request.action == "summarize":
138
+ summary = scholar.summarize()
139
+ response["message"] = "Resumo gerado."
140
+ response["data"] = summary
141
+
142
+ elif request.action == "mindmap":
143
+ path = scholar.mindmap()
144
+ if path:
145
+ response["message"] = "Mapa Mental gerado."
146
+ response["file_base64"] = encode_file_base64(path)
147
+ response["file_type"] = "image/png"
148
+ os.remove(path) # Clean up file
149
+ else:
150
+ response["success"] = False
151
+ response["message"] = "Erro ao gerar Mapa Mental."
152
+
153
+ elif request.action == "podcast" or request.action == "debate":
154
+ mode = "debate" if request.action == "debate" else "lecture"
155
+ path = scholar.podcast(mode=mode)
156
+ if path:
157
+ response["message"] = f"Áudio ({mode}) gerado."
158
+ response["file_base64"] = encode_file_base64(path)
159
+ response["file_type"] = "audio/mp3"
160
+ os.remove(path) # Clean up file
161
+ else:
162
+ response["success"] = False
163
+ response["message"] = "Erro ao gerar áudio."
164
+
165
+ elif request.action == "quiz":
166
+ quiz = scholar.quiz()
167
+ response["message"] = "Quiz gerado."
168
+ response["data"] = quiz
169
+
170
+ elif request.action == "flashcards":
171
+ path = scholar.flashcards()
172
+ if path:
173
+ response["message"] = "Flashcards (.apkg) gerados."
174
+ response["file_base64"] = encode_file_base64(path)
175
+ response["file_type"] = "application/octet-stream"
176
+ response["filename"] = path # Enviar nome do arquivo para download
177
+ os.remove(path) # Clean up file
178
+ else:
179
+ response["success"] = False
180
+ response["message"] = "Erro ao gerar Flashcards."
181
+
182
+ elif request.action == "handout":
183
+ path = scholar.handout()
184
+ if path:
185
+ response["message"] = "Apostila PDF gerada."
186
+ response["file_base64"] = encode_file_base64(path)
187
+ response["file_type"] = "application/pdf"
188
+ os.remove(path) # Clean up file
189
+ else:
190
+ response["success"] = False
191
+ response["message"] = "Erro ao gerar Apostila (gere o Resumo primeiro)."
192
+
193
+ else:
194
+ response["success"] = False
195
+ response["message"] = "Ação inválida."
196
+
197
+ return response
198
+
199
+ except Exception as e:
200
+ print(f"Erro no Scholar Agent: {e}")
201
+ return {"success": False, "error": str(e)}
202
 
203
  if __name__ == "__main__":
204
  import uvicorn
205
  port = int(os.environ.get("PORT", 7860))
206
  print(f"Iniciando o servidor Uvicorn em http://0.0.0.0:{port}")
207
+ uvicorn.run(app, host="0.0.0.0", port=port)
jade/scholar_agent.py ADDED
@@ -0,0 +1,438 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # jade/scholar_agent.py
2
+
3
+ import os
4
+ import sys
5
+ import json
6
+ import time
7
+ import re
8
+ import random
9
+ import logging
10
+ from io import BytesIO
11
+ from typing import List, Dict, Any, Optional
12
+ import numpy as np
13
+
14
+ # Dependências (Assumindo que estão instaladas via requirements.txt)
15
+ import groq
16
+ import pypdf
17
+ import faiss
18
+ import graphviz
19
+ import genanki
20
+ from gtts import gTTS
21
+ from pydub import AudioSegment
22
+ import requests
23
+ from bs4 import BeautifulSoup
24
+ from youtube_transcript_api import YouTubeTranscriptApi
25
+ from sentence_transformers import SentenceTransformer
26
+ from fpdf import FPDF
27
+ from duckduckgo_search import DDGS
28
+
29
+ # Configuração de Logger
30
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s - SCHOLAR - %(levelname)s - %(message)s")
31
+
32
+ # --- 1. Ferramentas (ToolBox) ---
33
+
34
+ class ToolBox:
35
+ """Caixa de ferramentas para os agentes."""
36
+
37
+ @staticmethod
38
+ def read_pdf(filepath: str) -> str:
39
+ try:
40
+ logging.info(f"📄 [Ferramenta] Lendo PDF: {filepath}...")
41
+ reader = pypdf.PdfReader(filepath)
42
+ text = "".join([p.extract_text() or "" for p in reader.pages])
43
+ return re.sub(r'\s+', ' ', text).strip()
44
+ except Exception as e:
45
+ return f"Erro ao ler PDF: {str(e)}"
46
+
47
+ @staticmethod
48
+ def scrape_web(url: str) -> str:
49
+ try:
50
+ logging.info(f"🌐 [Ferramenta] Acessando URL: {url}...")
51
+ headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'}
52
+ response = requests.get(url, headers=headers, timeout=10)
53
+ soup = BeautifulSoup(response.content, 'html.parser')
54
+ for script in soup(["script", "style", "header", "footer", "nav"]):
55
+ script.extract()
56
+ text = soup.get_text()
57
+ return re.sub(r'\s+', ' ', text).strip()[:40000]
58
+ except Exception as e:
59
+ logging.error(f"Erro ao acessar {url}: {e}")
60
+ return ""
61
+
62
+ @staticmethod
63
+ def search_topic(topic: str) -> List[str]:
64
+ """Pesquisa no DuckDuckGo e retorna URLs."""
65
+ logging.info(f"🔎 [Ferramenta] Pesquisando na Web sobre: '{topic}'...")
66
+ urls = []
67
+ try:
68
+ with DDGS() as ddgs:
69
+ results = list(ddgs.text(topic, max_results=3))
70
+ for r in results:
71
+ urls.append(r['href'])
72
+ except Exception as e:
73
+ logging.error(f"Erro na busca: {e}")
74
+ return urls
75
+
76
+ @staticmethod
77
+ def get_youtube_transcript(url: str) -> str:
78
+ try:
79
+ logging.info(f"📺 [Ferramenta] Extraindo legendas do YouTube: {url}...")
80
+ video_id = url.split("v=")[-1].split("&")[0]
81
+ transcript = YouTubeTranscriptApi.get_transcript(video_id, languages=['pt', 'en'])
82
+ text = " ".join([t['text'] for t in transcript])
83
+ return text
84
+ except Exception as e:
85
+ return f"Erro ao pegar legendas do YouTube: {str(e)}"
86
+
87
+ @staticmethod
88
+ def generate_audio_mix(script: List[Dict], filename="aula_podcast.mp3"):
89
+ logging.info("🎙️ [Estúdio] Produzindo áudio imersivo...")
90
+ combined = AudioSegment.silent(duration=500)
91
+
92
+ for line in script:
93
+ speaker = line.get("speaker", "Narrador").upper()
94
+ text = line.get("text", "")
95
+
96
+ if "BERTA" in speaker or "PROFESSORA" in speaker or "AGENT B" in speaker:
97
+ tts = gTTS(text=text, lang='pt', tld='pt', slow=False)
98
+ else:
99
+ # Gabriel / Agent A
100
+ tts = gTTS(text=text, lang='pt', tld='com.br', slow=False)
101
+
102
+ fp = BytesIO()
103
+ tts.write_to_fp(fp)
104
+ fp.seek(0)
105
+
106
+ try:
107
+ segment = AudioSegment.from_file(fp, format="mp3")
108
+ combined += segment
109
+ combined += AudioSegment.silent(duration=300)
110
+ except: pass
111
+
112
+ combined.export(filename, format="mp3")
113
+ return filename
114
+
115
+ @staticmethod
116
+ def generate_mindmap_image(dot_code: str, filename="mapa_mental"):
117
+ try:
118
+ logging.info("🗺️ [Design] Renderizando Mapa Mental...")
119
+ clean_dot = dot_code.replace("```dot", "").replace("```", "").strip()
120
+ src = graphviz.Source(clean_dot)
121
+ src.format = 'png'
122
+ filepath = src.render(filename, view=False)
123
+ return filepath
124
+ except Exception as e:
125
+ logging.error(f"Erro ao gerar gráfico: {e}")
126
+ return None
127
+
128
+ @staticmethod
129
+ def generate_anki_deck(qa_pairs: List[Dict], deck_name="ScholarGraph Deck"):
130
+ logging.info("🧠 [Anki] Criando arquivo de Flashcards (.apkg)...")
131
+ try:
132
+ model_id = random.randrange(1 << 30, 1 << 31)
133
+ deck_id = random.randrange(1 << 30, 1 << 31)
134
+
135
+ my_model = genanki.Model(
136
+ model_id,
137
+ 'Simple Model',
138
+ fields=[{'name': 'Question'}, {'name': 'Answer'}],
139
+ templates=[{
140
+ 'name': 'Card 1',
141
+ 'qfmt': '{{Question}}',
142
+ 'afmt': '{{FrontSide}}<hr id="answer">{{Answer}}',
143
+ }]
144
+ )
145
+
146
+ my_deck = genanki.Deck(deck_id, deck_name)
147
+
148
+ for item in qa_pairs:
149
+ my_deck.add_note(genanki.Note(
150
+ model=my_model,
151
+ fields=[item['question'], item['answer']]
152
+ ))
153
+
154
+ filename = f"flashcards_{int(time.time())}.apkg"
155
+ genanki.Package(my_deck).write_to_file(filename)
156
+ return filename
157
+ except Exception as e:
158
+ logging.error(f"Erro ao criar Anki deck: {e}")
159
+ return None
160
+
161
+ # --- 2. Vector Store (RAG) ---
162
+
163
+ class VectorMemory:
164
+ def __init__(self):
165
+ logging.info("🧠 [Memória] Inicializando Banco de Vetores (RAG)...")
166
+ # Modelo leve para embeddings
167
+ self.model = SentenceTransformer('all-MiniLM-L6-v2')
168
+ self.index = None
169
+ self.chunks = []
170
+
171
+ def ingest(self, text: str, chunk_size=500):
172
+ words = text.split()
173
+ # Cria chunks sobrepostos para melhor contexto
174
+ self.chunks = [' '.join(words[i:i+chunk_size]) for i in range(0, len(words), int(chunk_size*0.8))]
175
+
176
+ logging.info(f"🧠 [Memória] Vetorizando {len(self.chunks)} fragmentos...")
177
+ if not self.chunks: return
178
+
179
+ embeddings = self.model.encode(self.chunks)
180
+ dimension = embeddings.shape[1]
181
+ self.index = faiss.IndexFlatL2(dimension)
182
+ self.index.add(np.array(embeddings).astype('float32'))
183
+ logging.info("🧠 [Memória] Indexação concluída.")
184
+
185
+ def retrieve(self, query: str, k=3) -> str:
186
+ if not self.index: return ""
187
+ query_vec = self.model.encode([query])
188
+ D, I = self.index.search(np.array(query_vec).astype('float32'), k)
189
+
190
+ results = [self.chunks[i] for i in I[0] if i < len(self.chunks)]
191
+ return "\n\n".join(results)
192
+
193
+ # --- 3. Estado e LLM ---
194
+
195
+ class GraphState:
196
+ def __init__(self):
197
+ self.raw_content: str = ""
198
+ self.summary: str = ""
199
+ self.script: List[Dict] = []
200
+ self.quiz_data: List[Dict] = []
201
+ self.mindmap_path: str = ""
202
+ self.flashcards: List[Dict] = []
203
+
204
+ class LLMEngine:
205
+ def __init__(self, api_key: str = None):
206
+ self.api_key = api_key or os.getenv("GROQ_API_KEY")
207
+ self.client = groq.Groq(api_key=self.api_key)
208
+ self.model = "llama-3.3-70b-versatile"
209
+
210
+ def chat(self, messages: List[Dict], json_mode=False) -> str:
211
+ try:
212
+ kwargs = {"messages": messages, "model": self.model, "temperature": 0.6}
213
+ if json_mode: kwargs["response_format"] = {"type": "json_object"}
214
+ return self.client.chat.completions.create(**kwargs).choices[0].message.content
215
+ except Exception as e:
216
+ return f"Erro na IA: {e}"
217
+
218
+ # --- 4. Agentes ---
219
+
220
+ class ResearcherAgent:
221
+ """Agente que pesquisa na web se o input for um tópico."""
222
+ def deep_research(self, topic: str) -> str:
223
+ logging.info(f"🕵️ [Pesquisador] Iniciando Deep Research sobre: {topic}")
224
+ urls = ToolBox.search_topic(topic)
225
+ if not urls:
226
+ return f"Não encontrei informações sobre {topic}."
227
+
228
+ full_text = ""
229
+ for url in urls:
230
+ content = ToolBox.scrape_web(url)
231
+ if content:
232
+ full_text += f"\n\n--- Fonte: {url} ---\n{content[:10000]}"
233
+
234
+ return full_text
235
+
236
+ class FlashcardAgent:
237
+ """Agente focado em memorização (Anki)."""
238
+ def __init__(self, llm: LLMEngine):
239
+ self.llm = llm
240
+
241
+ def create_deck(self, content: str) -> List[Dict]:
242
+ logging.info("🃏 [Flashcard] Gerando pares Pergunta-Resposta...")
243
+ prompt = f"""
244
+ Crie 10 Flashcards (Pergunta e Resposta) sobre o conteúdo para memorização.
245
+ SAÍDA JSON: {{ "cards": [ {{ "question": "...", "answer": "..." }} ] }}
246
+ Conteúdo: {content[:15000]}
247
+ """
248
+ try:
249
+ resp = self.llm.chat([{"role": "user", "content": prompt}], json_mode=True)
250
+ return json.loads(resp).get("cards", [])
251
+ except: return []
252
+
253
+ class IngestAgent:
254
+ def __init__(self, researcher: ResearcherAgent):
255
+ self.researcher = researcher
256
+
257
+ def process(self, user_input: str) -> str:
258
+ # Se for arquivo
259
+ if user_input.lower().endswith(".pdf") and os.path.exists(user_input):
260
+ return ToolBox.read_pdf(user_input)
261
+ # Se for URL
262
+ elif "youtube.com" in user_input or "youtu.be" in user_input:
263
+ return ToolBox.get_youtube_transcript(user_input)
264
+ elif user_input.startswith("http"):
265
+ return ToolBox.scrape_web(user_input)
266
+ # Se não for URL nem arquivo, assume que é Tópico para Pesquisa
267
+ else:
268
+ logging.info("🔍 Entrada detectada como Tópico. Ativando ResearcherAgent...")
269
+ return self.researcher.deep_research(user_input)
270
+
271
+ class ProfessorAgent:
272
+ def __init__(self, llm: LLMEngine):
273
+ self.llm = llm
274
+
275
+ def summarize(self, full_text: str) -> str:
276
+ logging.info("🧠 [Professor] Gerando resumo estratégico...")
277
+ prompt = f"""
278
+ Você é um Professor Universitário. Crie um resumo estruturado e profundo.
279
+ Texto: {full_text[:25000]}
280
+ Formato: # Título / ## Introdução / ## Pontos Chave / ## Conclusão
281
+ """
282
+ return self.llm.chat([{"role": "user", "content": prompt}])
283
+
284
+ class VisualizerAgent:
285
+ def __init__(self, llm: LLMEngine):
286
+ self.llm = llm
287
+
288
+ def create_mindmap(self, text: str) -> str:
289
+ logging.info("🎨 [Visualizador] Projetando Mapa Mental...")
290
+ prompt = f"""
291
+ Crie um código GRAPHVIZ (DOT) para um mapa mental deste conteúdo.
292
+ Use formas coloridas. NÃO explique, apenas dê o código DOT dentro de ```dot ... ```.
293
+ Texto: {text[:15000]}
294
+ """
295
+ response = self.llm.chat([{"role": "user", "content": prompt}])
296
+ match = re.search(r'```dot(.*?)```', response, re.DOTALL)
297
+ if match: return match.group(1).strip()
298
+ return response
299
+
300
+ class ScriptwriterAgent:
301
+ def __init__(self, llm: LLMEngine):
302
+ self.llm = llm
303
+
304
+ def create_script(self, content: str, mode="lecture") -> List[Dict]:
305
+ if mode == "debate":
306
+ logging.info("🔥 [Roteirista] Criando DEBATE INTENSO...")
307
+ prompt = f"""
308
+ Crie um DEBATE acalorado mas intelectual entre dois agentes (8 falas).
309
+ Personagens:
310
+ - AGENT A (Gabriel): A favor / Otimista / Pragmático.
311
+ - AGENT B (Berta): Contra / Cética / Filosófica.
312
+
313
+ SAÍDA JSON: {{ "dialogue": [ {{"speaker": "Agent A", "text": "..."}}, {{"speaker": "Agent B", "text": "..."}} ] }}
314
+ Tema Base: {content[:15000]}
315
+ """
316
+ else:
317
+ logging.info("✍️ [Roteirista] Escrevendo roteiro de aula...")
318
+ prompt = f"""
319
+ Crie um roteiro de podcast (8 falas).
320
+ Personagens: GABRIEL (Aluno BR) e BERTA (Professora PT).
321
+ SAÍDA JSON: {{ "dialogue": [ {{"speaker": "Gabriel", "text": "..."}}, ...] }}
322
+ Base: {content[:15000]}
323
+ """
324
+
325
+ try:
326
+ resp = self.llm.chat([{"role": "user", "content": prompt}], json_mode=True)
327
+ return json.loads(resp).get("dialogue", [])
328
+ except: return []
329
+
330
+ class ExaminerAgent:
331
+ def __init__(self, llm: LLMEngine):
332
+ self.llm = llm
333
+
334
+ def generate_quiz(self, content: str) -> List[Dict]:
335
+ logging.info("📝 [Examinador] Criando Prova Gamificada...")
336
+ prompt = f"""
337
+ Crie 5 perguntas de múltipla escolha (Difíceis).
338
+ SAÍDA JSON: {{ "quiz": [ {{ "question": "...", "options": ["A)..."], "correct_option": "A", "explanation": "..." }} ] }}
339
+ Base: {content[:15000]}
340
+ """
341
+ try:
342
+ resp = self.llm.chat([{"role": "user", "content": prompt}], json_mode=True)
343
+ return json.loads(resp).get("quiz", [])
344
+ except: return []
345
+
346
+ class PublisherAgent:
347
+ def create_handout(self, state: GraphState, filename=None):
348
+ if not filename:
349
+ filename = f"Apostila_{int(time.time())}.pdf"
350
+ logging.info(f"📚 [Editora] Diagramando Apostila PDF: {filename}...")
351
+ pdf = FPDF()
352
+ pdf.add_page()
353
+ pdf.set_font("Arial", size=12)
354
+ pdf.set_font("Arial", 'B', 16)
355
+ pdf.cell(0, 10, "Apostila de Estudos - Scholar Graph", ln=True, align='C')
356
+ pdf.ln(10)
357
+ pdf.set_font("Arial", size=11)
358
+ safe_summary = state.summary.encode('latin-1', 'replace').decode('latin-1')
359
+ pdf.multi_cell(0, 7, safe_summary)
360
+ if state.mindmap_path and os.path.exists(state.mindmap_path):
361
+ pdf.add_page()
362
+ pdf.image(state.mindmap_path, x=10, y=30, w=190)
363
+ pdf.output(filename)
364
+ return filename
365
+
366
+ # --- 5. Agente Mestre ---
367
+
368
+ class ScholarAgent:
369
+ def __init__(self, api_key: str = None):
370
+ self.state = GraphState()
371
+ self.memory = VectorMemory()
372
+
373
+ try:
374
+ self.llm = LLMEngine(api_key=api_key)
375
+ except:
376
+ self.llm = None
377
+ logging.error("Erro ao inicializar LLM. Verifique a chave de API.")
378
+
379
+ self.researcher = ResearcherAgent()
380
+ self.ingestor = IngestAgent(self.researcher)
381
+
382
+ if self.llm:
383
+ self.professor = ProfessorAgent(self.llm)
384
+ self.visualizer = VisualizerAgent(self.llm)
385
+ self.scriptwriter = ScriptwriterAgent(self.llm)
386
+ self.examiner = ExaminerAgent(self.llm)
387
+ self.flashcarder = FlashcardAgent(self.llm)
388
+ self.publisher = PublisherAgent()
389
+
390
+ def ingest(self, target: str) -> bool:
391
+ """Ingere conteúdo de URL, PDF ou tópico de pesquisa."""
392
+ content = self.ingestor.process(target)
393
+ if not content or len(content) < 50:
394
+ logging.error("Falha ao obter conteúdo suficiente.")
395
+ return False
396
+
397
+ self.state.raw_content = content
398
+ self.memory.ingest(content)
399
+ return True
400
+
401
+ def summarize(self) -> str:
402
+ if not self.state.raw_content: return "Sem conteúdo para resumir."
403
+ self.state.summary = self.professor.summarize(self.state.raw_content)
404
+ return self.state.summary
405
+
406
+ def mindmap(self) -> str:
407
+ if not self.state.raw_content: return None
408
+ dot = self.visualizer.create_mindmap(self.state.raw_content)
409
+ path = ToolBox.generate_mindmap_image(dot)
410
+ if path:
411
+ self.state.mindmap_path = path
412
+ return path
413
+
414
+ def podcast(self, mode="lecture") -> str:
415
+ """Gera podcast ou debate e retorna o caminho do arquivo mp3."""
416
+ if not self.state.raw_content: return None
417
+ script = self.scriptwriter.create_script(self.state.raw_content, mode=mode)
418
+ filename = f"podcast_{mode}_{int(time.time())}.mp3"
419
+ return ToolBox.generate_audio_mix(script, filename=filename)
420
+
421
+ def quiz(self) -> List[Dict]:
422
+ if not self.state.raw_content: return []
423
+ self.state.quiz_data = self.examiner.generate_quiz(self.state.raw_content)
424
+ return self.state.quiz_data
425
+
426
+ def flashcards(self) -> str:
427
+ """Gera flashcards e retorna o caminho do arquivo .apkg."""
428
+ if not self.state.raw_content: return None
429
+ cards = self.flashcarder.create_deck(self.state.raw_content)
430
+ if cards:
431
+ return ToolBox.generate_anki_deck(cards)
432
+ return None
433
+
434
+ def handout(self) -> str:
435
+ """Gera apostila PDF e retorna o caminho."""
436
+ if not self.state.summary:
437
+ return None # Necessário resumo
438
+ return self.publisher.create_handout(self.state)
requirements.txt CHANGED
@@ -14,4 +14,14 @@ joblib
14
  scikit-learn
15
  numpy
16
  einops
17
- timm
 
 
 
 
 
 
 
 
 
 
 
14
  scikit-learn
15
  numpy
16
  einops
17
+ timm
18
+ pypdf
19
+ pydub
20
+ beautifulsoup4
21
+ requests
22
+ fpdf
23
+ youtube_transcript_api
24
+ faiss-cpu
25
+ graphviz
26
+ duckduckgo-search
27
+ genanki