Madras1 commited on
Commit
7e2816d
·
verified ·
1 Parent(s): 1dfefd9

Upload 17 files

Browse files
Files changed (14) hide show
  1. .gitattributes +35 -35
  2. Dockerfile +31 -31
  3. README.md +10 -10
  4. app.py +143 -141
  5. gitattributes +35 -35
  6. jade/config.json +7 -7
  7. jade/core.py +150 -139
  8. jade/handlers.py +54 -54
  9. jade/heavy_mode.py +226 -226
  10. jade/main.py +8 -8
  11. jade/scholar.py +584 -584
  12. jade/tts.py +25 -25
  13. jade/web_search.py +77 -0
  14. requirements.txt +31 -30
.gitattributes CHANGED
@@ -1,35 +1,35 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
- *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
Dockerfile CHANGED
@@ -1,32 +1,32 @@
1
- # Usa uma imagem base leve do Python 3.10
2
- FROM python:3.10-slim
3
-
4
- # 1. A MÁGICA DO SISTEMA (Instala o que falta)
5
- # ffmpeg: Para o áudio (Scholar Podcast)
6
- # graphviz: Para os mapas mentais
7
- RUN apt-get update && apt-get install -y \
8
- ffmpeg \
9
- graphviz \
10
- git \
11
- && rm -rf /var/lib/apt/lists/*
12
-
13
- # Configura o diretório de trabalho
14
- WORKDIR /app
15
-
16
- # Copia os requisitos e instala as bibliotecas Python
17
- COPY requirements.txt .
18
- RUN pip install --no-cache-dir -r requirements.txt
19
-
20
- # Copia todo o restante do código
21
- COPY . .
22
-
23
- # 2. A MÁGICA DAS PERMISSÕES
24
- # Cria a pasta onde os arquivos serão salvos e dá permissão total
25
- # Isso evita erros de "Permission Denied" quando a IA tentar salvar o MP3
26
- RUN mkdir -p backend/generated && chmod -R 777 backend/generated
27
-
28
- # Expõe a porta que o Hugging Face usa
29
- EXPOSE 7860
30
-
31
- # Comando para iniciar a J.A.D.E.
32
  CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
 
1
+ # Usa uma imagem base leve do Python 3.10
2
+ FROM python:3.10-slim
3
+
4
+ # 1. A MÁGICA DO SISTEMA (Instala o que falta)
5
+ # ffmpeg: Para o áudio (Scholar Podcast)
6
+ # graphviz: Para os mapas mentais
7
+ RUN apt-get update && apt-get install -y \
8
+ ffmpeg \
9
+ graphviz \
10
+ git \
11
+ && rm -rf /var/lib/apt/lists/*
12
+
13
+ # Configura o diretório de trabalho
14
+ WORKDIR /app
15
+
16
+ # Copia os requisitos e instala as bibliotecas Python
17
+ COPY requirements.txt .
18
+ RUN pip install --no-cache-dir -r requirements.txt
19
+
20
+ # Copia todo o restante do código
21
+ COPY . .
22
+
23
+ # 2. A MÁGICA DAS PERMISSÕES
24
+ # Cria a pasta onde os arquivos serão salvos e dá permissão total
25
+ # Isso evita erros de "Permission Denied" quando a IA tentar salvar o MP3
26
+ RUN mkdir -p backend/generated && chmod -R 777 backend/generated
27
+
28
+ # Expõe a porta que o Hugging Face usa
29
+ EXPOSE 7860
30
+
31
+ # Comando para iniciar a J.A.D.E.
32
  CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
README.md CHANGED
@@ -1,10 +1,10 @@
1
- ---
2
- title: Jade Port
3
- emoji: 🦊
4
- colorFrom: green
5
- colorTo: indigo
6
- sdk: docker
7
- pinned: false
8
- ---
9
-
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
+ ---
2
+ title: Jade Port
3
+ emoji: 🦊
4
+ colorFrom: green
5
+ colorTo: indigo
6
+ sdk: docker
7
+ pinned: false
8
+ ---
9
+
10
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
app.py CHANGED
@@ -1,141 +1,143 @@
1
- # app.py - VERSÃO COMPLETA COM VOZ (BASE64) E VISÃO
2
- import os
3
- import base64
4
- import io
5
- import asyncio
6
- from fastapi import FastAPI
7
- from fastapi.middleware.cors import CORSMiddleware
8
- from fastapi.staticfiles import StaticFiles
9
- from pydantic import BaseModel
10
- from PIL import Image
11
- from jade.core import JadeAgent
12
- from jade.scholar import ScholarAgent
13
- from jade.heavy_mode import JadeHeavyAgent
14
-
15
- print("Iniciando a J.A.D.E. com FastAPI...")
16
- jade_agent = JadeAgent()
17
- scholar_agent = ScholarAgent()
18
- # Instantiate Heavy Agent. It uses environment variables.
19
- jade_heavy_agent = JadeHeavyAgent()
20
-
21
- print("J.A.D.E. pronta para receber requisições.")
22
-
23
- app = FastAPI(title="J.A.D.E. API")
24
- app.add_middleware(
25
- CORSMiddleware,
26
- allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"],
27
- )
28
-
29
- # Mount generated directory for static files (PDFs, Images)
30
- os.makedirs("backend/generated", exist_ok=True)
31
- app.mount("/generated", StaticFiles(directory="backend/generated"), name="generated")
32
-
33
- # Dicionário global para armazenar sessões de usuários
34
- # Structure: user_sessions[user_id] = { "jade": [...], "scholar": [...], "heavy": [...] }
35
- user_sessions = {}
36
-
37
- class UserRequest(BaseModel):
38
- user_input: str
39
- image_base64: str | None = None
40
- user_id: str | None = None
41
- agent_type: str = "jade" # "jade", "scholar", "heavy"
42
-
43
- @app.post("/chat")
44
- async def handle_chat(request: UserRequest):
45
- try:
46
- user_id = request.user_id if request.user_id else "default_user"
47
- agent_type = request.agent_type.lower()
48
-
49
- if user_id not in user_sessions:
50
- print(f"Nova sessão criada para: {user_id}")
51
- user_sessions[user_id] = {
52
- "jade": [jade_agent.system_prompt],
53
- "scholar": [],
54
- "heavy": []
55
- }
56
-
57
- # Ensure sub-keys exist
58
- if "jade" not in user_sessions[user_id]: user_sessions[user_id]["jade"] = [jade_agent.system_prompt]
59
- if "scholar" not in user_sessions[user_id]: user_sessions[user_id]["scholar"] = []
60
- if "heavy" not in user_sessions[user_id]: user_sessions[user_id]["heavy"] = []
61
-
62
- vision_context = None
63
- if request.image_base64:
64
- try:
65
- header, encoded_data = request.image_base64.split(",", 1)
66
- image_bytes = base64.b64decode(encoded_data)
67
- pil_image = Image.open(io.BytesIO(image_bytes))
68
- # Jade handles vision processing
69
- vision_context = jade_agent.image_handler.process_pil_image(pil_image)
70
- except Exception as img_e:
71
- print(f"Erro ao processar imagem Base64: {img_e}")
72
- vision_context = "Houve um erro ao analisar a imagem."
73
-
74
- final_user_input = request.user_input if request.user_input else "Descreva a imagem em detalhes."
75
-
76
- bot_response_text = ""
77
- audio_path = None
78
-
79
- if agent_type == "scholar":
80
- current_history = user_sessions[user_id]["scholar"]
81
- bot_response_text, audio_path, updated_history = scholar_agent.respond(
82
- history=current_history,
83
- user_input=final_user_input,
84
- user_id=user_id,
85
- vision_context=vision_context
86
- )
87
- user_sessions[user_id]["scholar"] = updated_history
88
-
89
- elif agent_type == "heavy":
90
- current_history = user_sessions[user_id]["heavy"]
91
- # Heavy agent is async
92
- bot_response_text, audio_path, updated_history = await jade_heavy_agent.respond(
93
- history=current_history,
94
- user_input=final_user_input,
95
- user_id=user_id,
96
- vision_context=vision_context
97
- )
98
- user_sessions[user_id]["heavy"] = updated_history
99
-
100
- else:
101
- # Default to J.A.D.E.
102
- current_history = user_sessions[user_id]["jade"]
103
- # Jade agent is synchronous, run directly
104
- bot_response_text, audio_path, updated_history = jade_agent.respond(
105
- history=current_history,
106
- user_input=final_user_input,
107
- user_id=user_id,
108
- vision_context=vision_context
109
- )
110
- user_sessions[user_id]["jade"] = updated_history
111
-
112
- # Audio Logic
113
- audio_base64 = None
114
- if audio_path and os.path.exists(audio_path):
115
- print(f"Codificando arquivo de áudio: {audio_path}")
116
- with open(audio_path, "rb") as audio_file:
117
- audio_bytes = audio_file.read()
118
- audio_base64 = base64.b64encode(audio_bytes).decode('utf-8')
119
-
120
- # Remove only if temp file
121
- if "backend/generated" not in audio_path:
122
- os.remove(audio_path)
123
-
124
- return {
125
- "success": True,
126
- "bot_response": bot_response_text,
127
- "audio_base64": audio_base64
128
- }
129
- except Exception as e:
130
- print(f"Erro crítico no endpoint /chat: {e}")
131
- return {"success": False, "error": str(e)}
132
-
133
- @app.get("/")
134
- def root():
135
- return {"message": "Servidor J.A.D.E. com FastAPI está online."}
136
-
137
- if __name__ == "__main__":
138
- import uvicorn
139
- port = int(os.environ.get("PORT", 7860))
140
- print(f"Iniciando o servidor Uvicorn em http://0.0.0.0:{port}")
141
- uvicorn.run(app, host="0.0.0.0", port=port)
 
 
 
1
+ # app.py - VERSÃO COMPLETA COM VOZ (BASE64) E VISÃO
2
+ import os
3
+ import base64
4
+ import io
5
+ import asyncio
6
+ from fastapi import FastAPI
7
+ from fastapi.middleware.cors import CORSMiddleware
8
+ from fastapi.staticfiles import StaticFiles
9
+ from pydantic import BaseModel
10
+ from PIL import Image
11
+ from jade.core import JadeAgent
12
+ from jade.scholar import ScholarAgent
13
+ from jade.heavy_mode import JadeHeavyAgent
14
+
15
+ print("Iniciando a J.A.D.E. com FastAPI...")
16
+ jade_agent = JadeAgent()
17
+ scholar_agent = ScholarAgent()
18
+ # Instantiate Heavy Agent. It uses environment variables.
19
+ jade_heavy_agent = JadeHeavyAgent()
20
+
21
+ print("J.A.D.E. pronta para receber requisições.")
22
+
23
+ app = FastAPI(title="J.A.D.E. API")
24
+ app.add_middleware(
25
+ CORSMiddleware,
26
+ allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"],
27
+ )
28
+
29
+ # Mount generated directory for static files (PDFs, Images)
30
+ os.makedirs("backend/generated", exist_ok=True)
31
+ app.mount("/generated", StaticFiles(directory="backend/generated"), name="generated")
32
+
33
+ # Dicionário global para armazenar sessões de usuários
34
+ # Structure: user_sessions[user_id] = { "jade": [...], "scholar": [...], "heavy": [...] }
35
+ user_sessions = {}
36
+
37
+ class UserRequest(BaseModel):
38
+ user_input: str
39
+ image_base64: str | None = None
40
+ user_id: str | None = None
41
+ agent_type: str = "jade" # "jade", "scholar", "heavy"
42
+ web_search: bool = False # Toggle para busca web na J.A.D.E.
43
+
44
+ @app.post("/chat")
45
+ async def handle_chat(request: UserRequest):
46
+ try:
47
+ user_id = request.user_id if request.user_id else "default_user"
48
+ agent_type = request.agent_type.lower()
49
+
50
+ if user_id not in user_sessions:
51
+ print(f"Nova sessão criada para: {user_id}")
52
+ user_sessions[user_id] = {
53
+ "jade": [jade_agent.system_prompt],
54
+ "scholar": [],
55
+ "heavy": []
56
+ }
57
+
58
+ # Ensure sub-keys exist
59
+ if "jade" not in user_sessions[user_id]: user_sessions[user_id]["jade"] = [jade_agent.system_prompt]
60
+ if "scholar" not in user_sessions[user_id]: user_sessions[user_id]["scholar"] = []
61
+ if "heavy" not in user_sessions[user_id]: user_sessions[user_id]["heavy"] = []
62
+
63
+ vision_context = None
64
+ if request.image_base64:
65
+ try:
66
+ header, encoded_data = request.image_base64.split(",", 1)
67
+ image_bytes = base64.b64decode(encoded_data)
68
+ pil_image = Image.open(io.BytesIO(image_bytes))
69
+ # Jade handles vision processing
70
+ vision_context = jade_agent.image_handler.process_pil_image(pil_image)
71
+ except Exception as img_e:
72
+ print(f"Erro ao processar imagem Base64: {img_e}")
73
+ vision_context = "Houve um erro ao analisar a imagem."
74
+
75
+ final_user_input = request.user_input if request.user_input else "Descreva a imagem em detalhes."
76
+
77
+ bot_response_text = ""
78
+ audio_path = None
79
+
80
+ if agent_type == "scholar":
81
+ current_history = user_sessions[user_id]["scholar"]
82
+ bot_response_text, audio_path, updated_history = scholar_agent.respond(
83
+ history=current_history,
84
+ user_input=final_user_input,
85
+ user_id=user_id,
86
+ vision_context=vision_context
87
+ )
88
+ user_sessions[user_id]["scholar"] = updated_history
89
+
90
+ elif agent_type == "heavy":
91
+ current_history = user_sessions[user_id]["heavy"]
92
+ # Heavy agent is async
93
+ bot_response_text, audio_path, updated_history = await jade_heavy_agent.respond(
94
+ history=current_history,
95
+ user_input=final_user_input,
96
+ user_id=user_id,
97
+ vision_context=vision_context
98
+ )
99
+ user_sessions[user_id]["heavy"] = updated_history
100
+
101
+ else:
102
+ # Default to J.A.D.E.
103
+ current_history = user_sessions[user_id]["jade"]
104
+ # Jade agent is synchronous, run directly
105
+ bot_response_text, audio_path, updated_history = jade_agent.respond(
106
+ history=current_history,
107
+ user_input=final_user_input,
108
+ user_id=user_id,
109
+ vision_context=vision_context,
110
+ web_search=request.web_search # Passa o toggle de busca web
111
+ )
112
+ user_sessions[user_id]["jade"] = updated_history
113
+
114
+ # Audio Logic
115
+ audio_base64 = None
116
+ if audio_path and os.path.exists(audio_path):
117
+ print(f"Codificando arquivo de áudio: {audio_path}")
118
+ with open(audio_path, "rb") as audio_file:
119
+ audio_bytes = audio_file.read()
120
+ audio_base64 = base64.b64encode(audio_bytes).decode('utf-8')
121
+
122
+ # Remove only if temp file
123
+ if "backend/generated" not in audio_path:
124
+ os.remove(audio_path)
125
+
126
+ return {
127
+ "success": True,
128
+ "bot_response": bot_response_text,
129
+ "audio_base64": audio_base64
130
+ }
131
+ except Exception as e:
132
+ print(f"Erro crítico no endpoint /chat: {e}")
133
+ return {"success": False, "error": str(e)}
134
+
135
+ @app.get("/")
136
+ def root():
137
+ return {"message": "Servidor J.A.D.E. com FastAPI está online."}
138
+
139
+ if __name__ == "__main__":
140
+ import uvicorn
141
+ port = int(os.environ.get("PORT", 7860))
142
+ print(f"Iniciando o servidor Uvicorn em http://0.0.0.0:{port}")
143
+ uvicorn.run(app, host="0.0.0.0", port=port)
gitattributes CHANGED
@@ -1,35 +1,35 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
- *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
jade/config.json CHANGED
@@ -1,8 +1,8 @@
1
- {
2
- "groq_model": "meta-llama/llama-4-maverick-17b-128e-instruct",
3
- "audio_model": "whisper-large-v3",
4
- "caption_model": "microsoft/Florence-2-base-ft",
5
- "max_context": 12,
6
- "language": "pt",
7
- "local_mode": false
8
  }
 
1
+ {
2
+ "groq_model": "meta-llama/llama-4-maverick-17b-128e-instruct",
3
+ "audio_model": "whisper-large-v3",
4
+ "caption_model": "microsoft/Florence-2-base-ft",
5
+ "max_context": 12,
6
+ "language": "pt",
7
+ "local_mode": false
8
  }
jade/core.py CHANGED
@@ -1,139 +1,150 @@
1
- import json
2
- import logging
3
- import os
4
- import sys
5
- import time
6
- import uuid
7
-
8
- from groq import Groq
9
-
10
- # Importa nossos módulos customizados
11
- from .handlers import ImageHandler
12
- from .tts import TTSPlayer
13
- from .utils import slim_history
14
- from .shorestone import ShoreStoneMemory
15
- from .curator_heuristic import MemoryCuratorHeuristic
16
-
17
- # Configura o logger principal
18
- logging.basicConfig(level=logging.INFO, format="%(asctime)s - JADE - %(levelname)s - %(message)s")
19
-
20
- class JadeAgent:
21
- def __init__(self, config_path="jade/config.json"):
22
- # Carrega configurações
23
- # Try to load from absolute path first, then relative
24
- try:
25
- with open(config_path) as f:
26
- self.cfg = json.load(f)
27
- except FileNotFoundError:
28
- # Fallback: try to find it relative to this file
29
- base_dir = os.path.dirname(os.path.abspath(__file__))
30
- config_path = os.path.join(base_dir, "config.json")
31
- with open(config_path) as f:
32
- self.cfg = json.load(f)
33
-
34
- # --- Configuração da API Groq ---
35
- logging.info("Iniciando J.A.D.E. em modo API (Groq)...")
36
- self.api_key = self._get_api_key()
37
- self.client = Groq(api_key=self.api_key)
38
- self.model_name = self.cfg.get("groq_model", "meta-llama/llama-4-maverick-17b-128e-instruct")
39
-
40
- # System Prompt Base
41
- self.system_prompt = {"role": "system", "content": "Você é J.A.D.E., uma IA multimodal calma e inteligente. Seja direta. Responda de forma concisa e natural. NÃO explique seu processo de pensamento. Apenas responda à pergunta."}
42
-
43
- # --- Inicialização dos Módulos ---
44
- logging.info("Carregando módulos de percepção e memória...")
45
-
46
- # Visão e Fala
47
- self.image_handler = ImageHandler(self.cfg.get("caption_model", "Salesforce/blip-image-captioning-large"))
48
- self.tts = TTSPlayer(lang=self.cfg.get("language", "pt"))
49
-
50
- # 1. Memória ShoreStone (Persistente)
51
- self.memory = ShoreStoneMemory()
52
- # Inicializa com sessão padrão, mas será trocada dinamicamente no respond()
53
- self.memory.load_or_create_session("sessao_padrao_gabriel")
54
-
55
- # 2. Curador Heurístico (Manutenção Automática)
56
- self.curator = MemoryCuratorHeuristic(shorestone_memory=self.memory)
57
- self.response_count = 0
58
- self.maintenance_interval = 10 # Executar a manutenção a cada 10 interações
59
-
60
- logging.info(f"J.A.D.E. pronta e conectada ao modelo {self.model_name}.")
61
-
62
- def _get_api_key(self):
63
- """Recupera a chave da API do ambiente de forma segura."""
64
- key = os.getenv("GROQ_API_KEY")
65
- if not key:
66
- logging.error("Chave GROQ_API_KEY não encontrada nas variáveis de ambiente.")
67
- # For development, try to warn but not crash if possible, but Groq needs it.
68
- # raise RuntimeError("GROQ_API_KEY não encontrada. Defina a variável de ambiente.")
69
- print("WARNING: GROQ_API_KEY not found.")
70
- return key
71
-
72
- def _chat(self, messages):
73
- """Envia as mensagens para a Groq e retorna a resposta."""
74
- try:
75
- chat = self.client.chat.completions.create(
76
- messages=messages,
77
- model=self.model_name,
78
- temperature=0.7, # Criatividade balanceada
79
- max_tokens=1024 # Limite de resposta razoável
80
- )
81
- return chat.choices[0].message.content.strip()
82
- except Exception as e:
83
- logging.error(f"Erro na comunicação com a Groq: {e}")
84
- return "Desculpe, tive um problema ao me conectar com meu cérebro na nuvem."
85
-
86
- def respond(self, history, user_input, user_id="default", vision_context=None):
87
- """Processo principal de raciocínio: Lembrar -> Ver -> Responder -> Memorizar -> Manter."""
88
-
89
- # TROCA A SESSÃO DA MEMÓRIA PARA O USUÁRIO ATUAL
90
- session_name = f"user_{user_id}"
91
- self.memory.load_or_create_session(session_name)
92
-
93
- messages = history[:]
94
-
95
- # 1. Lembrar (Recuperação de Contexto)
96
- memories = self.memory.remember(user_input)
97
- if memories:
98
- memory_context = f"--- MEMÓRIAS RELEVANTES (ShoreStone) ---\n{memories}\n--- FIM DAS MEMÓRIAS ---"
99
- # Inserimos as memórias como contexto de sistema para guiar a resposta
100
- messages.append({"role": "system", "content": memory_context})
101
-
102
- # 2. Ver (Contexto Visual)
103
- if vision_context:
104
- messages.append({"role": "system", "content": f"Contexto visual da imagem que o usuário enviou: {vision_context}"})
105
-
106
- # Adiciona a pergunta atual ao histórico temporário e ao prompt
107
- history.append({"role": "user", "content": user_input})
108
- messages.append({"role": "user", "content": user_input})
109
-
110
- # 3. Responder (Geração)
111
- resposta = self._chat(messages)
112
-
113
- # Atualiza histórico
114
- history.append({"role": "assistant", "content": resposta})
115
- history = slim_history(history, keep=self.cfg.get("max_context", 12))
116
-
117
- # 4. Memorizar (Armazenamento Persistente)
118
- self.memory.memorize(user_input, resposta)
119
-
120
- print(f"\n🤖 J.A.D.E.: {resposta}")
121
-
122
- # Falar (TTS) - Modified for Backend compatibility
123
- audio_path = None
124
- try:
125
- # Uses the TTSPlayer from tts.py which has save_audio_to_file
126
- audio_path = self.tts.save_audio_to_file(resposta)
127
- except Exception as e:
128
- logging.warning(f"TTS falhou (silenciado): {e}")
129
-
130
- # 5. Manter (Ciclo de Curadoria Automática)
131
- self.response_count += 1
132
- if self.response_count % self.maintenance_interval == 0:
133
- logging.info(f"Ciclo de manutenção agendado (interação {self.response_count}). Verificando saúde da memória...")
134
- try:
135
- self.curator.run_maintenance_cycle()
136
- except Exception as e:
137
- logging.error(f"Erro no Curador de Memória: {e}")
138
-
139
- return resposta, audio_path, history
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import logging
3
+ import os
4
+ import sys
5
+ import time
6
+ import uuid
7
+
8
+ from groq import Groq
9
+
10
+ # Importa nossos módulos customizados
11
+ from .handlers import ImageHandler
12
+ from .tts import TTSPlayer
13
+ from .utils import slim_history
14
+ from .shorestone import ShoreStoneMemory
15
+ from .curator_heuristic import MemoryCuratorHeuristic
16
+ from .web_search import WebSearchHandler
17
+
18
+ # Configura o logger principal
19
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s - JADE - %(levelname)s - %(message)s")
20
+
21
+ class JadeAgent:
22
+ def __init__(self, config_path="jade/config.json"):
23
+ # Carrega configurações
24
+ # Try to load from absolute path first, then relative
25
+ try:
26
+ with open(config_path) as f:
27
+ self.cfg = json.load(f)
28
+ except FileNotFoundError:
29
+ # Fallback: try to find it relative to this file
30
+ base_dir = os.path.dirname(os.path.abspath(__file__))
31
+ config_path = os.path.join(base_dir, "config.json")
32
+ with open(config_path) as f:
33
+ self.cfg = json.load(f)
34
+
35
+ # --- Configuração da API Groq ---
36
+ logging.info("Iniciando J.A.D.E. em modo API (Groq)...")
37
+ self.api_key = self._get_api_key()
38
+ self.client = Groq(api_key=self.api_key)
39
+ self.model_name = self.cfg.get("groq_model", "meta-llama/llama-4-maverick-17b-128e-instruct")
40
+
41
+ # System Prompt Base
42
+ self.system_prompt = {"role": "system", "content": "Você é J.A.D.E., uma IA multimodal calma e inteligente. Seja direta. Responda de forma concisa e natural. NÃO explique seu processo de pensamento. Apenas responda à pergunta."}
43
+
44
+ # --- Inicialização dos Módulos ---
45
+ logging.info("Carregando módulos de percepção e memória...")
46
+
47
+ # Visão e Fala
48
+ self.image_handler = ImageHandler(self.cfg.get("caption_model", "Salesforce/blip-image-captioning-large"))
49
+ self.tts = TTSPlayer(lang=self.cfg.get("language", "pt"))
50
+
51
+ # 1. Memória ShoreStone (Persistente)
52
+ self.memory = ShoreStoneMemory()
53
+ # Inicializa com sessão padrão, mas será trocada dinamicamente no respond()
54
+ self.memory.load_or_create_session("sessao_padrao_gabriel")
55
+
56
+ # 2. Curador Heurístico (Manutenção Automática)
57
+ self.curator = MemoryCuratorHeuristic(shorestone_memory=self.memory)
58
+ self.response_count = 0
59
+ self.maintenance_interval = 10 # Executar a manutenção a cada 10 interações
60
+
61
+ # 3. Web Search (Tavily)
62
+ self.web_search_handler = WebSearchHandler()
63
+
64
+ logging.info(f"J.A.D.E. pronta e conectada ao modelo {self.model_name}.")
65
+
66
+ def _get_api_key(self):
67
+ """Recupera a chave da API do ambiente de forma segura."""
68
+ key = os.getenv("GROQ_API_KEY")
69
+ if not key:
70
+ logging.error("Chave GROQ_API_KEY não encontrada nas variáveis de ambiente.")
71
+ # For development, try to warn but not crash if possible, but Groq needs it.
72
+ # raise RuntimeError("❌ GROQ_API_KEY não encontrada. Defina a variável de ambiente.")
73
+ print("WARNING: GROQ_API_KEY not found.")
74
+ return key
75
+
76
+ def _chat(self, messages):
77
+ """Envia as mensagens para a Groq e retorna a resposta."""
78
+ try:
79
+ chat = self.client.chat.completions.create(
80
+ messages=messages,
81
+ model=self.model_name,
82
+ temperature=0.7, # Criatividade balanceada
83
+ max_tokens=1024 # Limite de resposta razoável
84
+ )
85
+ return chat.choices[0].message.content.strip()
86
+ except Exception as e:
87
+ logging.error(f"Erro na comunicação com a Groq: {e}")
88
+ return "Desculpe, tive um problema ao me conectar com meu cérebro na nuvem."
89
+
90
+ def respond(self, history, user_input, user_id="default", vision_context=None, web_search=False):
91
+ """Processo principal de raciocínio: Buscar -> Lembrar -> Ver -> Responder -> Memorizar -> Manter."""
92
+
93
+ # TROCA A SESSÃO DA MEMÓRIA PARA O USUÁRIO ATUAL
94
+ session_name = f"user_{user_id}"
95
+ self.memory.load_or_create_session(session_name)
96
+
97
+ messages = history[:]
98
+
99
+ # 0. Buscar na Web (se habilitado)
100
+ if web_search and self.web_search_handler.is_available():
101
+ search_results = self.web_search_handler.search(user_input)
102
+ if search_results:
103
+ search_context = f"--- RESULTADOS DA BUSCA WEB ---\n{search_results}\n--- FIM DA BUSCA ---"
104
+ messages.append({"role": "system", "content": search_context})
105
+
106
+ # 1. Lembrar (Recuperação de Contexto)
107
+ memories = self.memory.remember(user_input)
108
+ if memories:
109
+ memory_context = f"--- MEMÓRIAS RELEVANTES (ShoreStone) ---\n{memories}\n--- FIM DAS MEMÓRIAS ---"
110
+ # Inserimos as memórias como contexto de sistema para guiar a resposta
111
+ messages.append({"role": "system", "content": memory_context})
112
+
113
+ # 2. Ver (Contexto Visual)
114
+ if vision_context:
115
+ messages.append({"role": "system", "content": f"Contexto visual da imagem que o usuário enviou: {vision_context}"})
116
+
117
+ # Adiciona a pergunta atual ao histórico temporário e ao prompt
118
+ history.append({"role": "user", "content": user_input})
119
+ messages.append({"role": "user", "content": user_input})
120
+
121
+ # 3. Responder (Geração)
122
+ resposta = self._chat(messages)
123
+
124
+ # Atualiza histórico
125
+ history.append({"role": "assistant", "content": resposta})
126
+ history = slim_history(history, keep=self.cfg.get("max_context", 12))
127
+
128
+ # 4. Memorizar (Armazenamento Persistente)
129
+ self.memory.memorize(user_input, resposta)
130
+
131
+ print(f"\n🤖 J.A.D.E.: {resposta}")
132
+
133
+ # Falar (TTS) - Modified for Backend compatibility
134
+ audio_path = None
135
+ try:
136
+ # Uses the TTSPlayer from tts.py which has save_audio_to_file
137
+ audio_path = self.tts.save_audio_to_file(resposta)
138
+ except Exception as e:
139
+ logging.warning(f"TTS falhou (silenciado): {e}")
140
+
141
+ # 5. Manter (Ciclo de Curadoria Automática)
142
+ self.response_count += 1
143
+ if self.response_count % self.maintenance_interval == 0:
144
+ logging.info(f"Ciclo de manutenção agendado (interação {self.response_count}). Verificando saúde da memória...")
145
+ try:
146
+ self.curator.run_maintenance_cycle()
147
+ except Exception as e:
148
+ logging.error(f"Erro no Curador de Memória: {e}")
149
+
150
+ return resposta, audio_path, history
jade/handlers.py CHANGED
@@ -1,54 +1,54 @@
1
- from transformers import AutoProcessor, AutoModelForCausalLM
2
- from PIL import Image
3
- import torch
4
-
5
- class TextHandler:
6
- def process(self):
7
- return input("⌨️ Digite sua mensagem: ").strip()
8
-
9
- class AudioHandler:
10
- def __init__(self, client, audio_model):
11
- self.client = client
12
- self.audio_model = audio_model
13
-
14
- class ImageHandler:
15
- def __init__(self, model_name):
16
- self.processor = AutoProcessor.from_pretrained(model_name, trust_remote_code=True)
17
- self.model = AutoModelForCausalLM.from_pretrained(model_name, trust_remote_code=True)
18
- self.device = "cuda" if torch.cuda.is_available() else "cpu"
19
- self.model.to(self.device)
20
- self.model.eval()
21
-
22
- def process_pil_image(self, pil_image: Image.Image):
23
- """Processa um objeto PIL.Image vindo diretamente do Gradio."""
24
- if not isinstance(pil_image, Image.Image):
25
- raise TypeError("A entrada deve ser um objeto PIL.Image.")
26
- return self._generate_caption(pil_image.convert("RGB"))
27
-
28
- def _generate_caption(self, img):
29
- """Lógica de geração de legenda reutilizável usando Florence-2."""
30
- # Prompt para descrição detalhada
31
- prompt = "<MORE_DETAILED_CAPTION>"
32
-
33
- with torch.no_grad():
34
- inputs = self.processor(text=prompt, images=img, return_tensors="pt").to(self.device)
35
-
36
- generated_ids = self.model.generate(
37
- input_ids=inputs["input_ids"],
38
- pixel_values=inputs["pixel_values"],
39
- max_new_tokens=1024,
40
- do_sample=False,
41
- num_beams=3,
42
- )
43
-
44
- generated_text = self.processor.batch_decode(generated_ids, skip_special_tokens=False)[0]
45
-
46
- # O Florence-2 requer pós-processamento para extrair a resposta limpa
47
- parsed_answer = self.processor.post_process_generation(
48
- generated_text,
49
- task=prompt,
50
- image_size=(img.width, img.height)
51
- )
52
-
53
- # parsed_answer retorna um dict, ex: {'<MORE_DETAILED_CAPTION>': 'texto da legenda'}
54
- return parsed_answer.get(prompt, "")
 
1
+ from transformers import AutoProcessor, AutoModelForCausalLM
2
+ from PIL import Image
3
+ import torch
4
+
5
+ class TextHandler:
6
+ def process(self):
7
+ return input("⌨️ Digite sua mensagem: ").strip()
8
+
9
+ class AudioHandler:
10
+ def __init__(self, client, audio_model):
11
+ self.client = client
12
+ self.audio_model = audio_model
13
+
14
+ class ImageHandler:
15
+ def __init__(self, model_name):
16
+ self.processor = AutoProcessor.from_pretrained(model_name, trust_remote_code=True)
17
+ self.model = AutoModelForCausalLM.from_pretrained(model_name, trust_remote_code=True)
18
+ self.device = "cuda" if torch.cuda.is_available() else "cpu"
19
+ self.model.to(self.device)
20
+ self.model.eval()
21
+
22
+ def process_pil_image(self, pil_image: Image.Image):
23
+ """Processa um objeto PIL.Image vindo diretamente do Gradio."""
24
+ if not isinstance(pil_image, Image.Image):
25
+ raise TypeError("A entrada deve ser um objeto PIL.Image.")
26
+ return self._generate_caption(pil_image.convert("RGB"))
27
+
28
+ def _generate_caption(self, img):
29
+ """Lógica de geração de legenda reutilizável usando Florence-2."""
30
+ # Prompt para descrição detalhada
31
+ prompt = "<MORE_DETAILED_CAPTION>"
32
+
33
+ with torch.no_grad():
34
+ inputs = self.processor(text=prompt, images=img, return_tensors="pt").to(self.device)
35
+
36
+ generated_ids = self.model.generate(
37
+ input_ids=inputs["input_ids"],
38
+ pixel_values=inputs["pixel_values"],
39
+ max_new_tokens=1024,
40
+ do_sample=False,
41
+ num_beams=3,
42
+ )
43
+
44
+ generated_text = self.processor.batch_decode(generated_ids, skip_special_tokens=False)[0]
45
+
46
+ # O Florence-2 requer pós-processamento para extrair a resposta limpa
47
+ parsed_answer = self.processor.post_process_generation(
48
+ generated_text,
49
+ task=prompt,
50
+ image_size=(img.width, img.height)
51
+ )
52
+
53
+ # parsed_answer retorna um dict, ex: {'<MORE_DETAILED_CAPTION>': 'texto da legenda'}
54
+ return parsed_answer.get(prompt, "")
jade/heavy_mode.py CHANGED
@@ -1,226 +1,226 @@
1
-
2
- import os
3
- import asyncio
4
- import random
5
- import re
6
- import json
7
- import logging
8
- from colorama import Fore, Style
9
- from groq import AsyncGroq, RateLimitError
10
- from mistralai import Mistral
11
- from openai import AsyncOpenAI
12
- import traceback
13
-
14
- # Configura logger local
15
- logger = logging.getLogger("JadeHeavy")
16
- logger.setLevel(logging.INFO)
17
-
18
- class JadeHeavyAgent:
19
- def __init__(self):
20
- self.groq_api_key = os.getenv("GROQ_API_KEY")
21
- self.mistral_api_key = os.getenv("MISTRAL_API_KEY")
22
- self.openrouter_api_key = os.getenv("OPENROUTER_API_KEY")
23
-
24
- if not self.groq_api_key:
25
- logger.warning("GROQ_API_KEY not set. Jade Heavy may fail.")
26
-
27
- self.groq_client = AsyncGroq(api_key=self.groq_api_key)
28
-
29
- self.mistral = None
30
- if self.mistral_api_key:
31
- self.mistral = Mistral(api_key=self.mistral_api_key)
32
- else:
33
- logger.warning("MISTRAL_API_KEY not set. Mistral model will be skipped or substituted.")
34
-
35
- self.openrouter = None
36
- if self.openrouter_api_key:
37
- self.openrouter = AsyncOpenAI(
38
- base_url="https://openrouter.ai/api/v1",
39
- api_key=self.openrouter_api_key,
40
- )
41
- else:
42
- logger.warning("OPENROUTER_API_KEY not set. Qwen/OpenRouter models will be skipped.")
43
-
44
- # Updated Model Map for Generalist Chat
45
- self.models = {
46
- "Kimi": "moonshotai/kimi-k2-instruct-0905", # Groq (Logic/Reasoning)
47
- "Mistral": "mistral-large-latest", # Mistral API
48
- "Llama": "openai/gpt-oss-120b", # Groq
49
- "Qwen": "qwen/qwen3-coder:free" # OpenRouter (Fallback if key exists) or Groq equivalent
50
- # Note: The original script used qwen/qwen3-235b... on OpenRouter.
51
- # If no OpenRouter key, we might need a fallback on Groq or skip.
52
- }
53
-
54
- # Judge model (Groq is fast and cheap)
55
- self.judge_id = "moonshotai/kimi-k2-instruct-0905"
56
-
57
- async def _safe_propose(self, model_name, history_text):
58
- """Phase 1: Strategic Planning"""
59
- # Staggering to avoid rate limits
60
- delay_map = {"Kimi": 0, "Mistral": 1.0, "Llama": 2.0, "Qwen": 3.0}
61
- await asyncio.sleep(delay_map.get(model_name, 1) + random.uniform(0.1, 0.5))
62
-
63
- sys_prompt = (
64
- "You are a Strategic Architect. Create a high-level roadmap to answer the user's request comprehensively.\n"
65
- "DO NOT write the final response yet. Just plan the structure and key points.\n"
66
- "FORMAT: 1. [INTENT ANALYSIS] 2. [KEY POINTS] 3. [STRUCTURE PROPOSAL]"
67
- )
68
-
69
- messages = [{"role": "system", "content": sys_prompt}, {"role": "user", "content": history_text}]
70
-
71
- try:
72
- content = ""
73
- if model_name == "Mistral" and self.mistral:
74
- resp = await self.mistral.chat.complete_async(model=self.models["Mistral"], messages=messages)
75
- content = resp.choices[0].message.content
76
- elif model_name == "Qwen" and self.openrouter:
77
- # Use OpenRouter if available
78
- resp = await self.openrouter.chat.completions.create(model="qwen/qwen3-235b-a22b:free", messages=messages) # Using the large free one if possible
79
- content = resp.choices[0].message.content
80
- else:
81
- # Default to Groq (Kimi, Llama, or fallback for others)
82
- # If Mistral/OpenRouter key missing, fallback to Llama-3-70b on Groq for diversity?
83
- target_model = self.models.get(model_name)
84
- if not target_model or (model_name == "Mistral" and not self.mistral) or (model_name == "Qwen" and not self.openrouter):
85
- target_model = "openai/gpt-oss-120b" # Fallback
86
-
87
- resp = await self.groq_client.chat.completions.create(
88
- model=target_model,
89
- messages=messages,
90
- temperature=0.7
91
- )
92
- content = resp.choices[0].message.content
93
-
94
- if content:
95
- return f"--- {model_name} Plan ---\n{content}"
96
- except Exception as e:
97
- logger.error(f"Error in propose ({model_name}): {e}")
98
- return ""
99
- return ""
100
-
101
- async def _safe_expand(self, model_name, history_text, strategy):
102
- """Phase 3: Execution/Expansion"""
103
- delay_map = {"Kimi": 0, "Mistral": 1.5, "Llama": 3.0, "Qwen": 4.5}
104
- await asyncio.sleep(delay_map.get(model_name, 1))
105
-
106
- sys_prompt = (
107
- f"You are a Precision Engine. Execute the following plan to answer the user request:\n\n{strategy}\n\n"
108
- "Write a detailed, natural, and high-quality response following this plan.\n"
109
- "Do not output internal reasoning like '[DECOMPOSITION]', just the final response text."
110
- )
111
-
112
- messages = [{"role": "system", "content": sys_prompt}, {"role": "user", "content": history_text}]
113
-
114
- try:
115
- content = ""
116
- if model_name == "Mistral" and self.mistral:
117
- resp = await self.mistral.chat.complete_async(model=self.models["Mistral"], messages=messages)
118
- content = resp.choices[0].message.content
119
- elif model_name == "Qwen" and self.openrouter:
120
- resp = await self.openrouter.chat.completions.create(model="qwen/qwen3-coder:free", messages=messages)
121
- content = resp.choices[0].message.content
122
- else:
123
- target_model = self.models.get(model_name)
124
- if not target_model or (model_name == "Mistral" and not self.mistral) or (model_name == "Qwen" and not self.openrouter):
125
- target_model = "openai/gpt-oss-120b"
126
-
127
- resp = await self.groq_client.chat.completions.create(
128
- model=target_model,
129
- messages=messages,
130
- temperature=0.7
131
- )
132
- content = resp.choices[0].message.content
133
-
134
- if content:
135
- return f"[{model_name} Draft]:\n{content}"
136
- except Exception as e:
137
- logger.error(f"Error in expand ({model_name}): {e}")
138
- return ""
139
- return ""
140
-
141
- async def respond(self, history, user_input, user_id="default", vision_context=None):
142
- """
143
- Main entry point for the Heavy Agent.
144
- History is a list of dicts: [{"role": "user", "content": "..."}...]
145
- """
146
-
147
- # Prepare context
148
- full_context = ""
149
- for msg in history[-6:]: # Limit context to last few turns to avoid huge prompts
150
- full_context += f"{msg['role'].upper()}: {msg['content']}\n"
151
-
152
- if vision_context:
153
- full_context += f"SYSTEM (Vision): {vision_context}\n"
154
-
155
- full_context += f"USER: {user_input}\n"
156
-
157
- agents = ["Kimi", "Mistral", "Llama", "Qwen"]
158
-
159
- # --- PHASE 1: STRATEGY ---
160
- logger.info("Jade Heavy: Phase 1 - Planning...")
161
- tasks = [self._safe_propose(m, full_context) for m in agents]
162
- results = await asyncio.gather(*tasks)
163
- valid_strats = [s for s in results if s]
164
-
165
- if not valid_strats:
166
- return "Failed to generate a plan.", None, history
167
-
168
- # --- PHASE 2: PRUNING (Select Best Plan) ---
169
- logger.info("Jade Heavy: Phase 2 - Pruning...")
170
- prune_prompt = (
171
- f"User Request Context:\n{full_context}\n\nProposed Plans:\n" +
172
- "\n".join(valid_strats) +
173
- "\n\nTASK: SELECT THE SINGLE MOST ROBUST AND HELPFUL PLAN. Return ONLY the content of the best plan."
174
- )
175
- try:
176
- best_strat_resp = await self.groq_client.chat.completions.create(
177
- model=self.judge_id,
178
- messages=[{"role":"user","content":prune_prompt}],
179
- temperature=0.5
180
- )
181
- best_strat = best_strat_resp.choices[0].message.content
182
- except Exception as e:
183
- logger.error(f"Pruning failed: {e}")
184
- best_strat = valid_strats[0] # Fallback to first plan
185
-
186
- # --- PHASE 3: EXPANSION (Drafting Responses) ---
187
- logger.info("Jade Heavy: Phase 3 - Expansion...")
188
- tasks_exp = [self._safe_expand(m, full_context, best_strat) for m in agents]
189
- results_exp = await asyncio.gather(*tasks_exp)
190
- valid_sols = [s for s in results_exp if s]
191
-
192
- if not valid_sols:
193
- return "Failed to generate drafts.", None, history
194
-
195
- # --- PHASE 4: VERDICT (Synthesis) ---
196
- logger.info("Jade Heavy: Phase 4 - Verdict...")
197
- council_prompt = (
198
- f"User Request:\n{full_context}\n\nCandidate Responses:\n" +
199
- "\n".join(valid_sols) +
200
- "\n\nTASK: Synthesize the best parts of these drafts into a FINAL, PERFECT RESPONSE."
201
- "The response should be natural, helpful, and high-quality. Do not mention the agents or the process."
202
- )
203
-
204
- final_answer = ""
205
- try:
206
- resp = await self.groq_client.chat.completions.create(
207
- model=self.judge_id,
208
- messages=[{"role":"system","content":"You are the Chief Editor."},{"role":"user","content":council_prompt}],
209
- temperature=0.5
210
- )
211
- final_answer = resp.choices[0].message.content
212
- except Exception as e:
213
- logger.error(f"Verdict failed: {e}")
214
- final_answer = valid_sols[0].replace(f"[{agents[0]} Draft]:\n", "") # Fallback
215
-
216
- # Update History
217
- history.append({"role": "user", "content": user_input})
218
- history.append({"role": "assistant", "content": final_answer})
219
-
220
- # Audio (Optional/Placeholder - returning None for now or use TTS if needed)
221
- # The user said "backend focuses on request", so we can skip TTS generation here
222
- # or implement it if JadeAgent does it. The original code uses `jade_agent.tts`.
223
- # For Heavy mode, maybe we skip audio for speed, or add it later.
224
- # I'll return None for audio path.
225
-
226
- return final_answer, None, history
 
1
+
2
+ import os
3
+ import asyncio
4
+ import random
5
+ import re
6
+ import json
7
+ import logging
8
+ from colorama import Fore, Style
9
+ from groq import AsyncGroq, RateLimitError
10
+ from mistralai import Mistral
11
+ from openai import AsyncOpenAI
12
+ import traceback
13
+
14
+ # Configura logger local
15
+ logger = logging.getLogger("JadeHeavy")
16
+ logger.setLevel(logging.INFO)
17
+
18
+ class JadeHeavyAgent:
19
+ def __init__(self):
20
+ self.groq_api_key = os.getenv("GROQ_API_KEY")
21
+ self.mistral_api_key = os.getenv("MISTRAL_API_KEY")
22
+ self.openrouter_api_key = os.getenv("OPENROUTER_API_KEY")
23
+
24
+ if not self.groq_api_key:
25
+ logger.warning("GROQ_API_KEY not set. Jade Heavy may fail.")
26
+
27
+ self.groq_client = AsyncGroq(api_key=self.groq_api_key)
28
+
29
+ self.mistral = None
30
+ if self.mistral_api_key:
31
+ self.mistral = Mistral(api_key=self.mistral_api_key)
32
+ else:
33
+ logger.warning("MISTRAL_API_KEY not set. Mistral model will be skipped or substituted.")
34
+
35
+ self.openrouter = None
36
+ if self.openrouter_api_key:
37
+ self.openrouter = AsyncOpenAI(
38
+ base_url="https://openrouter.ai/api/v1",
39
+ api_key=self.openrouter_api_key,
40
+ )
41
+ else:
42
+ logger.warning("OPENROUTER_API_KEY not set. Qwen/OpenRouter models will be skipped.")
43
+
44
+ # Updated Model Map for Generalist Chat
45
+ self.models = {
46
+ "Kimi": "moonshotai/kimi-k2-instruct-0905", # Groq (Logic/Reasoning)
47
+ "Mistral": "mistral-large-latest", # Mistral API
48
+ "Llama": "openai/gpt-oss-120b", # Groq
49
+ "Qwen": "qwen/qwen3-coder:free" # OpenRouter (Fallback if key exists) or Groq equivalent
50
+ # Note: The original script used qwen/qwen3-235b... on OpenRouter.
51
+ # If no OpenRouter key, we might need a fallback on Groq or skip.
52
+ }
53
+
54
+ # Judge model (Groq is fast and cheap)
55
+ self.judge_id = "moonshotai/kimi-k2-instruct-0905"
56
+
57
+ async def _safe_propose(self, model_name, history_text):
58
+ """Phase 1: Strategic Planning"""
59
+ # Staggering to avoid rate limits
60
+ delay_map = {"Kimi": 0, "Mistral": 1.0, "Llama": 2.0, "Qwen": 3.0}
61
+ await asyncio.sleep(delay_map.get(model_name, 1) + random.uniform(0.1, 0.5))
62
+
63
+ sys_prompt = (
64
+ "You are a Strategic Architect. Create a high-level roadmap to answer the user's request comprehensively.\n"
65
+ "DO NOT write the final response yet. Just plan the structure and key points.\n"
66
+ "FORMAT: 1. [INTENT ANALYSIS] 2. [KEY POINTS] 3. [STRUCTURE PROPOSAL]"
67
+ )
68
+
69
+ messages = [{"role": "system", "content": sys_prompt}, {"role": "user", "content": history_text}]
70
+
71
+ try:
72
+ content = ""
73
+ if model_name == "Mistral" and self.mistral:
74
+ resp = await self.mistral.chat.complete_async(model=self.models["Mistral"], messages=messages)
75
+ content = resp.choices[0].message.content
76
+ elif model_name == "Qwen" and self.openrouter:
77
+ # Use OpenRouter if available
78
+ resp = await self.openrouter.chat.completions.create(model="qwen/qwen3-235b-a22b:free", messages=messages) # Using the large free one if possible
79
+ content = resp.choices[0].message.content
80
+ else:
81
+ # Default to Groq (Kimi, Llama, or fallback for others)
82
+ # If Mistral/OpenRouter key missing, fallback to Llama-3-70b on Groq for diversity?
83
+ target_model = self.models.get(model_name)
84
+ if not target_model or (model_name == "Mistral" and not self.mistral) or (model_name == "Qwen" and not self.openrouter):
85
+ target_model = "openai/gpt-oss-120b" # Fallback
86
+
87
+ resp = await self.groq_client.chat.completions.create(
88
+ model=target_model,
89
+ messages=messages,
90
+ temperature=0.7
91
+ )
92
+ content = resp.choices[0].message.content
93
+
94
+ if content:
95
+ return f"--- {model_name} Plan ---\n{content}"
96
+ except Exception as e:
97
+ logger.error(f"Error in propose ({model_name}): {e}")
98
+ return ""
99
+ return ""
100
+
101
+ async def _safe_expand(self, model_name, history_text, strategy):
102
+ """Phase 3: Execution/Expansion"""
103
+ delay_map = {"Kimi": 0, "Mistral": 1.5, "Llama": 3.0, "Qwen": 4.5}
104
+ await asyncio.sleep(delay_map.get(model_name, 1))
105
+
106
+ sys_prompt = (
107
+ f"You are a Precision Engine. Execute the following plan to answer the user request:\n\n{strategy}\n\n"
108
+ "Write a detailed, natural, and high-quality response following this plan.\n"
109
+ "Do not output internal reasoning like '[DECOMPOSITION]', just the final response text."
110
+ )
111
+
112
+ messages = [{"role": "system", "content": sys_prompt}, {"role": "user", "content": history_text}]
113
+
114
+ try:
115
+ content = ""
116
+ if model_name == "Mistral" and self.mistral:
117
+ resp = await self.mistral.chat.complete_async(model=self.models["Mistral"], messages=messages)
118
+ content = resp.choices[0].message.content
119
+ elif model_name == "Qwen" and self.openrouter:
120
+ resp = await self.openrouter.chat.completions.create(model="qwen/qwen3-coder:free", messages=messages)
121
+ content = resp.choices[0].message.content
122
+ else:
123
+ target_model = self.models.get(model_name)
124
+ if not target_model or (model_name == "Mistral" and not self.mistral) or (model_name == "Qwen" and not self.openrouter):
125
+ target_model = "openai/gpt-oss-120b"
126
+
127
+ resp = await self.groq_client.chat.completions.create(
128
+ model=target_model,
129
+ messages=messages,
130
+ temperature=0.7
131
+ )
132
+ content = resp.choices[0].message.content
133
+
134
+ if content:
135
+ return f"[{model_name} Draft]:\n{content}"
136
+ except Exception as e:
137
+ logger.error(f"Error in expand ({model_name}): {e}")
138
+ return ""
139
+ return ""
140
+
141
+ async def respond(self, history, user_input, user_id="default", vision_context=None):
142
+ """
143
+ Main entry point for the Heavy Agent.
144
+ History is a list of dicts: [{"role": "user", "content": "..."}...]
145
+ """
146
+
147
+ # Prepare context
148
+ full_context = ""
149
+ for msg in history[-6:]: # Limit context to last few turns to avoid huge prompts
150
+ full_context += f"{msg['role'].upper()}: {msg['content']}\n"
151
+
152
+ if vision_context:
153
+ full_context += f"SYSTEM (Vision): {vision_context}\n"
154
+
155
+ full_context += f"USER: {user_input}\n"
156
+
157
+ agents = ["Kimi", "Mistral", "Llama", "Qwen"]
158
+
159
+ # --- PHASE 1: STRATEGY ---
160
+ logger.info("Jade Heavy: Phase 1 - Planning...")
161
+ tasks = [self._safe_propose(m, full_context) for m in agents]
162
+ results = await asyncio.gather(*tasks)
163
+ valid_strats = [s for s in results if s]
164
+
165
+ if not valid_strats:
166
+ return "Failed to generate a plan.", None, history
167
+
168
+ # --- PHASE 2: PRUNING (Select Best Plan) ---
169
+ logger.info("Jade Heavy: Phase 2 - Pruning...")
170
+ prune_prompt = (
171
+ f"User Request Context:\n{full_context}\n\nProposed Plans:\n" +
172
+ "\n".join(valid_strats) +
173
+ "\n\nTASK: SELECT THE SINGLE MOST ROBUST AND HELPFUL PLAN. Return ONLY the content of the best plan."
174
+ )
175
+ try:
176
+ best_strat_resp = await self.groq_client.chat.completions.create(
177
+ model=self.judge_id,
178
+ messages=[{"role":"user","content":prune_prompt}],
179
+ temperature=0.5
180
+ )
181
+ best_strat = best_strat_resp.choices[0].message.content
182
+ except Exception as e:
183
+ logger.error(f"Pruning failed: {e}")
184
+ best_strat = valid_strats[0] # Fallback to first plan
185
+
186
+ # --- PHASE 3: EXPANSION (Drafting Responses) ---
187
+ logger.info("Jade Heavy: Phase 3 - Expansion...")
188
+ tasks_exp = [self._safe_expand(m, full_context, best_strat) for m in agents]
189
+ results_exp = await asyncio.gather(*tasks_exp)
190
+ valid_sols = [s for s in results_exp if s]
191
+
192
+ if not valid_sols:
193
+ return "Failed to generate drafts.", None, history
194
+
195
+ # --- PHASE 4: VERDICT (Synthesis) ---
196
+ logger.info("Jade Heavy: Phase 4 - Verdict...")
197
+ council_prompt = (
198
+ f"User Request:\n{full_context}\n\nCandidate Responses:\n" +
199
+ "\n".join(valid_sols) +
200
+ "\n\nTASK: Synthesize the best parts of these drafts into a FINAL, PERFECT RESPONSE."
201
+ "The response should be natural, helpful, and high-quality. Do not mention the agents or the process."
202
+ )
203
+
204
+ final_answer = ""
205
+ try:
206
+ resp = await self.groq_client.chat.completions.create(
207
+ model=self.judge_id,
208
+ messages=[{"role":"system","content":"You are the Chief Editor."},{"role":"user","content":council_prompt}],
209
+ temperature=0.5
210
+ )
211
+ final_answer = resp.choices[0].message.content
212
+ except Exception as e:
213
+ logger.error(f"Verdict failed: {e}")
214
+ final_answer = valid_sols[0].replace(f"[{agents[0]} Draft]:\n", "") # Fallback
215
+
216
+ # Update History
217
+ history.append({"role": "user", "content": user_input})
218
+ history.append({"role": "assistant", "content": final_answer})
219
+
220
+ # Audio (Optional/Placeholder - returning None for now or use TTS if needed)
221
+ # The user said "backend focuses on request", so we can skip TTS generation here
222
+ # or implement it if JadeAgent does it. The original code uses `jade_agent.tts`.
223
+ # For Heavy mode, maybe we skip audio for speed, or add it later.
224
+ # I'll return None for audio path.
225
+
226
+ return final_answer, None, history
jade/main.py CHANGED
@@ -1,8 +1,8 @@
1
- import os
2
- import uvicorn
3
-
4
- if __name__ == "__main__":
5
- port = int(os.environ.get("PORT", 7860))
6
- print(f"Iniciando o servidor Uvicorn em http://0.0.0.0:{port}")
7
- # Import app from backend.app (module path)
8
- uvicorn.run("backend.app:app", host="0.0.0.0", port=port, reload=True)
 
1
+ import os
2
+ import uvicorn
3
+
4
+ if __name__ == "__main__":
5
+ port = int(os.environ.get("PORT", 7860))
6
+ print(f"Iniciando o servidor Uvicorn em http://0.0.0.0:{port}")
7
+ # Import app from backend.app (module path)
8
+ uvicorn.run("backend.app:app", host="0.0.0.0", port=port, reload=True)
jade/scholar.py CHANGED
@@ -1,584 +1,584 @@
1
- # backend/jade/scholar.py
2
-
3
- import os
4
- import sys
5
- import json
6
- import time
7
- import re
8
- import random
9
- import uuid
10
- from io import BytesIO
11
- from typing import List, Dict, Any, Optional
12
- import numpy as np
13
-
14
- # --- 1. Setup e Dependências ---
15
- # Removido setup_environment() pois será tratado no requirements.txt e Dockerfile
16
-
17
- try:
18
- import groq
19
- import pypdf
20
- import faiss
21
- import graphviz
22
- import genanki
23
- from gtts import gTTS
24
- from pydub import AudioSegment
25
- import requests
26
- from bs4 import BeautifulSoup
27
- from youtube_transcript_api import YouTubeTranscriptApi
28
- from sentence_transformers import SentenceTransformer
29
- from fpdf import FPDF
30
- from duckduckgo_search import DDGS
31
- except ImportError:
32
- # Em produção, isso deve falhar se as dependências não estiverem instaladas
33
- pass
34
-
35
- # --- 2. Configuração Global ---
36
- # Usaremos a configuração passada ou variável de ambiente
37
- GROQ_API_KEY = os.getenv("GROQ_API_KEY")
38
-
39
- # --- 3. Camada de Ferramentas (Tooling Layer) ---
40
-
41
- class ToolBox:
42
- """Caixa de ferramentas para os agentes."""
43
-
44
- @staticmethod
45
- def read_pdf(filepath: str) -> str:
46
- try:
47
- print(f"📄 [Ferramenta] Lendo PDF: {filepath}...")
48
- reader = pypdf.PdfReader(filepath)
49
- text = "".join([p.extract_text() or "" for p in reader.pages])
50
- return re.sub(r'\s+', ' ', text).strip()
51
- except Exception as e:
52
- return f"Erro ao ler PDF: {str(e)}"
53
-
54
- @staticmethod
55
- def scrape_web(url: str) -> str:
56
- try:
57
- print(f"🌐 [Ferramenta] Acessando URL: {url}...")
58
- headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'}
59
- response = requests.get(url, headers=headers, timeout=10)
60
- soup = BeautifulSoup(response.content, 'html.parser')
61
- for script in soup(["script", "style", "header", "footer", "nav"]):
62
- script.extract()
63
- text = soup.get_text()
64
- return re.sub(r'\s+', ' ', text).strip()[:40000]
65
- except Exception as e:
66
- print(f"Erro ao acessar {url}: {e}")
67
- return ""
68
-
69
- @staticmethod
70
- def search_topic(topic: str) -> List[str]:
71
- """Pesquisa no DuckDuckGo e retorna URLs."""
72
- print(f"🔎 [Ferramenta] Pesquisando na Web sobre: '{topic}'...")
73
- urls = []
74
- try:
75
- with DDGS() as ddgs:
76
- results = list(ddgs.text(topic, max_results=3))
77
- for r in results:
78
- urls.append(r['href'])
79
- except Exception as e:
80
- print(f"Erro na busca: {e}")
81
- return urls
82
-
83
- @staticmethod
84
- def get_youtube_transcript(url: str) -> str:
85
- try:
86
- print(f"📺 [Ferramenta] Extraindo legendas do YouTube: {url}...")
87
- video_id = url.split("v=")[-1].split("&")[0]
88
- transcript = YouTubeTranscriptApi.get_transcript(video_id, languages=['pt', 'en'])
89
- text = " ".join([t['text'] for t in transcript])
90
- return text
91
- except Exception as e:
92
- return f"Erro ao pegar legendas do YouTube: {str(e)}"
93
-
94
- @staticmethod
95
- def generate_audio_mix(script: List[Dict], filename="aula_podcast.mp3"):
96
- print("🎙️ [Estúdio] Produzindo áudio imersivo...")
97
- combined = AudioSegment.silent(duration=500)
98
-
99
- for line in script:
100
- speaker = line.get("speaker", "Narrador").upper()
101
- text = line.get("text", "")
102
-
103
- if "BERTA" in speaker or "PROFESSORA" in speaker or "AGENT B" in speaker:
104
- tts = gTTS(text=text, lang='pt', tld='pt', slow=False)
105
- else:
106
- # Gabriel / Agent A
107
- tts = gTTS(text=text, lang='pt', tld='com.br', slow=False)
108
-
109
- fp = BytesIO()
110
- tts.write_to_fp(fp)
111
- fp.seek(0)
112
-
113
- try:
114
- segment = AudioSegment.from_file(fp, format="mp3")
115
- combined += segment
116
- combined += AudioSegment.silent(duration=300)
117
- except: pass
118
-
119
- output_path = os.path.join("backend/generated", filename)
120
- os.makedirs(os.path.dirname(output_path), exist_ok=True)
121
- combined.export(output_path, format="mp3")
122
- return output_path
123
-
124
- @staticmethod
125
- def generate_mindmap_image(dot_code: str, filename="mapa_mental"):
126
- try:
127
- print("🗺️ [Design] Renderizando Mapa Mental...")
128
- clean_dot = dot_code.replace("```dot", "").replace("```", "").strip()
129
-
130
- # Ensure generated directory exists
131
- output_dir = "backend/generated"
132
- os.makedirs(output_dir, exist_ok=True)
133
- output_path = os.path.join(output_dir, filename)
134
-
135
- src = graphviz.Source(clean_dot)
136
- src.format = 'png'
137
- filepath = src.render(output_path, view=False)
138
- return filepath
139
- except Exception as e:
140
- print(f"Erro ao gerar gráfico: {e}")
141
- return None
142
-
143
- @staticmethod
144
- def generate_anki_deck(qa_pairs: List[Dict], deck_name="ScholarGraph Deck"):
145
- print("🧠 [Anki] Criando arquivo de Flashcards (.apkg)...")
146
- try:
147
- model_id = random.randrange(1 << 30, 1 << 31)
148
- deck_id = random.randrange(1 << 30, 1 << 31)
149
-
150
- my_model = genanki.Model(
151
- model_id,
152
- 'Simple Model',
153
- fields=[{'name': 'Question'}, {'name': 'Answer'}],
154
- templates=[{
155
- 'name': 'Card 1',
156
- 'qfmt': '{{Question}}',
157
- 'afmt': '{{FrontSide}}<hr id="answer">{{Answer}}',
158
- }]
159
- )
160
-
161
- my_deck = genanki.Deck(deck_id, deck_name)
162
-
163
- for item in qa_pairs:
164
- my_deck.add_note(genanki.Note(
165
- model=my_model,
166
- fields=[item['question'], item['answer']]
167
- ))
168
-
169
- output_dir = "backend/generated"
170
- os.makedirs(output_dir, exist_ok=True)
171
- filename = os.path.join(output_dir, f"flashcards_{uuid.uuid4().hex[:8]}.apkg")
172
- genanki.Package(my_deck).write_to_file(filename)
173
- return filename
174
- except Exception as e:
175
- print(f"Erro ao criar Anki deck: {e}")
176
- return None
177
-
178
- # --- 4. Vector Store (RAG) ---
179
-
180
- class VectorMemory:
181
- def __init__(self):
182
- print("🧠 [Memória] Inicializando Banco de Vetores (RAG)...")
183
- # Modelo leve para embeddings
184
- self.model = SentenceTransformer('all-MiniLM-L6-v2')
185
- self.index = None
186
- self.chunks = []
187
-
188
- def ingest(self, text: str, chunk_size=500):
189
- words = text.split()
190
- # Cria chunks sobrepostos para melhor contexto
191
- self.chunks = [' '.join(words[i:i+chunk_size]) for i in range(0, len(words), int(chunk_size*0.8))]
192
-
193
- print(f"🧠 [Memória] Vetorizando {len(self.chunks)} fragmentos...")
194
- if not self.chunks: return
195
-
196
- embeddings = self.model.encode(self.chunks)
197
- dimension = embeddings.shape[1]
198
- self.index = faiss.IndexFlatL2(dimension)
199
- self.index.add(np.array(embeddings).astype('float32'))
200
- print("🧠 [Memória] Indexação concluída.")
201
-
202
- def retrieve(self, query: str, k=3) -> str:
203
- if not self.index: return ""
204
- query_vec = self.model.encode([query])
205
- D, I = self.index.search(np.array(query_vec).astype('float32'), k)
206
-
207
- results = [self.chunks[i] for i in I[0] if i < len(self.chunks)]
208
- return "\n\n".join(results)
209
-
210
- # --- 5. Estado e LLM ---
211
-
212
- class GraphState:
213
- def __init__(self):
214
- self.raw_content: str = ""
215
- self.summary: str = ""
216
- self.script: List[Dict] = []
217
- self.quiz_data: List[Dict] = []
218
- self.mindmap_path: str = ""
219
- self.flashcards: List[Dict] = []
220
- self.current_quiz_question: int = 0
221
- self.xp: int = 0
222
- self.mode: str = "input" # input, menu, quiz
223
-
224
- class LLMEngine:
225
- def __init__(self, api_key=None):
226
- self.api_key = api_key or os.environ.get("GROQ_API_KEY")
227
- self.client = groq.Groq(api_key=self.api_key)
228
- self.model = "moonshotai/kimi-k2-instruct-0905"
229
-
230
- def chat(self, messages: List[Dict], json_mode=False) -> str:
231
- try:
232
- kwargs = {"messages": messages, "model": self.model, "temperature": 0.8}
233
- if json_mode: kwargs["response_format"] = {"type": "json_object"}
234
- return self.client.chat.completions.create(**kwargs).choices[0].message.content
235
- except Exception as e:
236
- return f"Erro na IA: {e}"
237
-
238
- # --- 6. Agentes Avançados (GOD MODE) ---
239
-
240
- class ResearcherAgent:
241
- """Agente que pesquisa na web se o input for um tópico."""
242
- def deep_research(self, topic: str) -> str:
243
- print(f"🕵️ [Pesquisador] Iniciando Deep Research sobre: {topic}")
244
- urls = ToolBox.search_topic(topic)
245
- if not urls:
246
- return f"Não encontrei informações sobre {topic}."
247
-
248
- full_text = ""
249
- for url in urls:
250
- content = ToolBox.scrape_web(url)
251
- if content:
252
- full_text += f"\n\n--- Fonte: {url} ---\n{content[:10000]}"
253
-
254
- return full_text
255
-
256
- class FlashcardAgent:
257
- """Agente focado em memorização (Anki)."""
258
- def __init__(self, llm: LLMEngine):
259
- self.llm = llm
260
-
261
- def create_deck(self, content: str) -> List[Dict]:
262
- print("🃏 [Flashcard] Gerando pares Pergunta-Resposta...")
263
- prompt = f"""
264
- Crie 10 Flashcards (Pergunta e Resposta) sobre o conteúdo para memorização.
265
- SAÍDA JSON: {{ "cards": [ {{ "question": "...", "answer": "..." }} ] }}
266
- Conteúdo: {content[:15000]}
267
- """
268
- try:
269
- resp = self.llm.chat([{"role": "user", "content": prompt}], json_mode=True)
270
- return json.loads(resp).get("cards", [])
271
- except: return []
272
-
273
- class IngestAgent:
274
- def __init__(self, researcher: ResearcherAgent):
275
- self.researcher = researcher
276
-
277
- def process(self, user_input: str) -> str:
278
- # Se for arquivo
279
- if user_input.lower().endswith(".pdf") and os.path.exists(user_input):
280
- return ToolBox.read_pdf(user_input)
281
- # Se for URL
282
- elif "youtube.com" in user_input or "youtu.be" in user_input:
283
- return ToolBox.get_youtube_transcript(user_input)
284
- elif user_input.startswith("http"):
285
- return ToolBox.scrape_web(user_input)
286
- # Se não for URL nem arquivo, assume que é Tópico para Pesquisa
287
- else:
288
- print("🔍 Entrada detectada como Tópico. Ativando ResearcherAgent...")
289
- return self.researcher.deep_research(user_input)
290
-
291
- class ProfessorAgent:
292
- def __init__(self, llm: LLMEngine):
293
- self.llm = llm
294
-
295
- def summarize(self, full_text: str) -> str:
296
- print("🧠 [Professor] Gerando resumo estratégico...")
297
- prompt = f"""
298
- Você é um Professor Universitário. Crie um resumo estruturado e profundo.
299
- Texto: {full_text[:25000]}
300
- Formato: # Título / ## Introdução / ## Pontos Chave / ## Conclusão
301
- """
302
- return self.llm.chat([{"role": "user", "content": prompt}])
303
-
304
- class VisualizerAgent:
305
- def __init__(self, llm: LLMEngine):
306
- self.llm = llm
307
-
308
- def create_mindmap(self, text: str) -> str:
309
- print("🎨 [Visualizador] Projetando Mapa Mental...")
310
- prompt = f"""
311
- Crie um código GRAPHVIZ (DOT) para um mapa mental deste conteúdo.
312
- Use formas coloridas. NÃO explique, apenas dê o código DOT dentro de ```dot ... ```.
313
- Texto: {text[:15000]}
314
- """
315
- response = self.llm.chat([{"role": "user", "content": prompt}])
316
- match = re.search(r'```dot(.*?)```', response, re.DOTALL)
317
- if match: return match.group(1).strip()
318
- return response
319
-
320
- class ScriptwriterAgent:
321
- def __init__(self, llm: LLMEngine):
322
- self.llm = llm
323
-
324
- def create_script(self, content: str, mode="lecture") -> List[Dict]:
325
- if mode == "debate":
326
- print("🔥 [Roteirista] Criando DEBATE INTENSO...")
327
- prompt = f"""
328
- Crie um DEBATE acalorado mas intelectual entre dois agentes (8 falas).
329
- Personagens:
330
- - AGENT A (Gabriel): A favor / Otimista / Pragmático.
331
- - AGENT B (Berta): Contra / Cética / Filosófica.
332
-
333
- SAÍDA JSON: {{ "dialogue": [ {{"speaker": "Agent A", "text": "..."}}, {{"speaker": "Agent B", "text": "..."}} ] }}
334
- Tema Base: {content[:15000]}
335
- """
336
- else:
337
- print("✍️ [Roteirista] Escrevendo roteiro de aula...")
338
- prompt = f"""
339
- Crie um roteiro de podcast (8 falas).
340
- Personagens: GABRIEL (Aluno BR) e BERTA (Professora PT).
341
- SAÍDA JSON: {{ "dialogue": [ {{"speaker": "Gabriel", "text": "..."}}, ...] }}
342
- Base: {content[:15000]}
343
- """
344
-
345
- try:
346
- resp = self.llm.chat([{"role": "user", "content": prompt}], json_mode=True)
347
- return json.loads(resp).get("dialogue", [])
348
- except: return []
349
-
350
- class ExaminerAgent:
351
- def __init__(self, llm: LLMEngine):
352
- self.llm = llm
353
-
354
- def generate_quiz(self, content: str) -> List[Dict]:
355
- print("📝 [Examinador] Criando Prova Gamificada...")
356
- prompt = f"""
357
- Crie 5 perguntas de múltipla escolha (Difíceis).
358
- SAÍDA JSON: {{ "quiz": [ {{ "question": "...", "options": ["A)..."], "correct_option": "A", "explanation": "..." }} ] }}
359
- Base: {content[:15000]}
360
- """
361
- try:
362
- resp = self.llm.chat([{"role": "user", "content": prompt}], json_mode=True)
363
- return json.loads(resp).get("quiz", [])
364
- except: return []
365
-
366
- class PublisherAgent:
367
- def create_handout(self, state: GraphState, filename="Apostila_Estudos.pdf"):
368
- print("📚 [Editora] Diagramando Apostila PDF...")
369
- pdf = FPDF()
370
- pdf.add_page()
371
- pdf.set_font("Arial", size=12)
372
- pdf.set_font("Arial", 'B', 16)
373
- pdf.cell(0, 10, "Apostila de Estudos - Scholar Graph", ln=True, align='C')
374
- pdf.ln(10)
375
- pdf.set_font("Arial", size=11)
376
- safe_summary = state.summary.encode('latin-1', 'replace').decode('latin-1')
377
- pdf.multi_cell(0, 7, safe_summary)
378
- if state.mindmap_path and os.path.exists(state.mindmap_path):
379
- pdf.add_page()
380
- pdf.image(state.mindmap_path, x=10, y=30, w=190)
381
-
382
- output_dir = "backend/generated"
383
- os.makedirs(output_dir, exist_ok=True)
384
- filepath = os.path.join(output_dir, filename)
385
- pdf.output(filepath)
386
- return filepath
387
-
388
- # --- 7. Agent Class wrapper for backend integration ---
389
-
390
- class ScholarAgent:
391
- def __init__(self):
392
- self.user_states = {} # Map user_id to (ScholarGraphGodMode instance or GraphState)
393
- self.api_key = os.getenv("GROQ_API_KEY")
394
- # Initialize one engine for general use if needed, but we probably need instances per user or shared resources.
395
- # We'll create instances per user request if they don't exist?
396
- # Actually, let's keep it simple. We store state per user.
397
-
398
- def get_or_create_state(self, user_id):
399
- if user_id not in self.user_states:
400
- self.user_states[user_id] = {
401
- "state": GraphState(),
402
- "memory": VectorMemory(),
403
- "llm": LLMEngine(self.api_key),
404
- "researcher": ResearcherAgent(),
405
- "ingestor": None, # Will be init with researcher
406
- "professor": None,
407
- "visualizer": None,
408
- "scriptwriter": None,
409
- "examiner": None,
410
- "flashcarder": None,
411
- "publisher": None
412
- }
413
- # Wiring dependencies
414
- u = self.user_states[user_id]
415
- u["ingestor"] = IngestAgent(u["researcher"])
416
- u["professor"] = ProfessorAgent(u["llm"])
417
- u["visualizer"] = VisualizerAgent(u["llm"])
418
- u["scriptwriter"] = ScriptwriterAgent(u["llm"])
419
- u["examiner"] = ExaminerAgent(u["llm"])
420
- u["flashcarder"] = FlashcardAgent(u["llm"])
421
- u["publisher"] = PublisherAgent()
422
-
423
- return self.user_states[user_id]
424
-
425
- def respond(self, history, user_input, user_id="default", vision_context=None):
426
- """
427
- Adapts the CLI interaction loop to a Request/Response model.
428
- """
429
- u = self.get_or_create_state(user_id)
430
- state = u["state"]
431
-
432
- # Helper to format menu
433
- def get_menu():
434
- return (
435
- "\n\n🎓 *MENU SCHOLAR GRAPH*\n"
436
- "1. 🧠 Resumo Estratégico\n"
437
- "2. 🗺️ Mapa Mental Visual\n"
438
- "3. 🎧 Podcast (Aula Didática)\n"
439
- "4. 🔥 DEBATE IA (Visões Opostas)\n"
440
- "5. 🎮 Quiz Gamificado\n"
441
- "6. 🃏 Gerar Flashcards (Anki .apkg)\n"
442
- "7. 📚 Baixar Apostila Completa\n"
443
- "8. 🔄 Novo Tópico\n"
444
- "👉 Escolha uma opção (número ou texto):"
445
- )
446
-
447
- # Helper for response with optional file
448
- response_text = ""
449
- audio_path = None
450
-
451
- # State Machine Logic
452
-
453
- # 1. Input Mode: Waiting for topic/url/pdf
454
- if state.mode == "input":
455
- if not user_input.strip():
456
- return "Por favor, forneça um tópico, URL ou arquivo PDF para começar.", None, history
457
-
458
- response_text = f"🔄 Processando '{user_input}'... (Isso pode levar alguns segundos)"
459
-
460
- # Process content
461
- content = u["ingestor"].process(user_input)
462
- if not content or len(content) < 50:
463
- response_text = "❌ Falha ao obter conteúdo suficiente ou tópico não encontrado. Tente novamente."
464
- return response_text, None, history
465
-
466
- state.raw_content = content
467
- u["memory"].ingest(content)
468
- state.mode = "menu"
469
- response_text += "\n✅ Conteúdo processado com sucesso!" + get_menu()
470
-
471
- # Update history
472
- history.append({"role": "user", "content": user_input})
473
- history.append({"role": "assistant", "content": response_text})
474
- return response_text, None, history
475
-
476
- # 2. Quiz Mode
477
- elif state.mode == "quiz":
478
- # Check answer
479
- current_q = state.quiz_data[state.current_quiz_question]
480
- ans = user_input.strip().upper()
481
-
482
- feedback = ""
483
- if ans and ans[0] == current_q['correct_option'][0]:
484
- state.xp += 100
485
- feedback = f"✨ ACERTOU! +100 XP. (Total: {state.xp})\n"
486
- else:
487
- feedback = f"💀 Errou... A resposta era {current_q['correct_option']}.\nExplanation: {current_q.get('explanation', '')}\n"
488
-
489
- state.current_quiz_question += 1
490
-
491
- if state.current_quiz_question < len(state.quiz_data):
492
- # Next Question
493
- q = state.quiz_data[state.current_quiz_question]
494
- response_text = feedback + f"\n🔹 QUESTÃO {state.current_quiz_question+1}:\n{q['question']}\n" + "\n".join(q['options'])
495
- else:
496
- # End of Quiz
497
- response_text = feedback + f"\n🏆 FIM DO QUIZ! TOTAL DE XP: {state.xp}\n" + get_menu()
498
- state.mode = "menu"
499
-
500
- history.append({"role": "user", "content": user_input})
501
- history.append({"role": "assistant", "content": response_text})
502
- return response_text, None, history
503
-
504
- # 3. Menu Mode
505
- elif state.mode == "menu":
506
- opt = user_input.strip()
507
-
508
- if opt.startswith("1") or "resumo" in opt.lower():
509
- state.summary = u["professor"].summarize(state.raw_content)
510
- response_text = "📝 *RESUMO ESTRATÉGICO:*\n\n" + state.summary + get_menu()
511
-
512
- elif opt.startswith("2") or "mapa" in opt.lower():
513
- dot = u["visualizer"].create_mindmap(state.raw_content)
514
- filename = f"mindmap_{uuid.uuid4().hex[:8]}"
515
- path = ToolBox.generate_mindmap_image(dot, filename)
516
- if path:
517
- state.mindmap_path = path
518
- # Since we return text and audio only in this signature, we might need a way to send image.
519
- # The current app structure supports sending audio_base64.
520
- # We might need to hack it to send image link or modify app.py.
521
- # For now, let's return a link relative to backend/generated (assuming static serving)
522
- response_text = f"🗺️ Mapa Mental gerado: [Baixar Imagem](/generated/{os.path.basename(path)})\n" + get_menu()
523
- else:
524
- response_text = "❌ Erro ao gerar mapa mental." + get_menu()
525
-
526
- elif opt.startswith("3") or "podcast" in opt.lower():
527
- script = u["scriptwriter"].create_script(state.raw_content, mode="lecture")
528
- filename = f"podcast_{uuid.uuid4().hex[:8]}.mp3"
529
- path = ToolBox.generate_audio_mix(script, filename)
530
- audio_path = path # Return this to be played
531
- response_text = "🎧 Aqui está o seu Podcast sobre o tema." + get_menu()
532
-
533
- elif opt.startswith("4") or "debate" in opt.lower():
534
- script = u["scriptwriter"].create_script(state.raw_content, mode="debate")
535
- filename = f"debate_{uuid.uuid4().hex[:8]}.mp3"
536
- path = ToolBox.generate_audio_mix(script, filename)
537
- audio_path = path
538
- response_text = "🔥 Debate gerado com sucesso." + get_menu()
539
-
540
- elif opt.startswith("5") or "quiz" in opt.lower():
541
- state.quiz_data = u["examiner"].generate_quiz(state.raw_content)
542
- if state.quiz_data:
543
- state.mode = "quiz"
544
- state.current_quiz_question = 0
545
- state.xp = 0
546
- q = state.quiz_data[0]
547
- response_text = f"🎮 *MODO QUIZ INICIADO*\n\n🔹 QUESTÃO 1:\n{q['question']}\n" + "\n".join(q['options'])
548
- else:
549
- response_text = "❌ Não foi possível gerar o quiz." + get_menu()
550
-
551
- elif opt.startswith("6") or "flashcard" in opt.lower():
552
- cards = u["flashcarder"].create_deck(state.raw_content)
553
- if cards:
554
- path = ToolBox.generate_anki_deck(cards)
555
- if path:
556
- response_text = f"✅ Flashcards gerados: [Baixar Deck Anki](/generated/{os.path.basename(path)})" + get_menu()
557
- else:
558
- response_text = "❌ Erro ao salvar arquivo." + get_menu()
559
- else:
560
- response_text = "❌ Erro ao gerar flashcards." + get_menu()
561
-
562
- elif opt.startswith("7") or "apostila" in opt.lower():
563
- if state.summary:
564
- filename = f"apostila_{uuid.uuid4().hex[:8]}.pdf"
565
- path = u["publisher"].create_handout(state, filename)
566
- response_text = f"📚 Apostila pronta: [Baixar PDF](/generated/{os.path.basename(path)})" + get_menu()
567
- else:
568
- response_text = "⚠️ Gere o Resumo (Opção 1) primeiro!" + get_menu()
569
-
570
- elif opt.startswith("8") or "novo" in opt.lower() or "sair" in opt.lower():
571
- state.mode = "input"
572
- # Reset state?
573
- state.raw_content = ""
574
- state.summary = ""
575
- response_text = "🔄 Reiniciando... Qual o novo tópico, link ou PDF?"
576
-
577
- else:
578
- response_text = "Opção inválida. Tente novamente.\n" + get_menu()
579
-
580
- history.append({"role": "user", "content": user_input})
581
- history.append({"role": "assistant", "content": response_text})
582
- return response_text, audio_path, history
583
-
584
- return "Erro de estado.", None, history
 
1
+ # backend/jade/scholar.py
2
+
3
+ import os
4
+ import sys
5
+ import json
6
+ import time
7
+ import re
8
+ import random
9
+ import uuid
10
+ from io import BytesIO
11
+ from typing import List, Dict, Any, Optional
12
+ import numpy as np
13
+
14
+ # --- 1. Setup e Dependências ---
15
+ # Removido setup_environment() pois será tratado no requirements.txt e Dockerfile
16
+
17
+ try:
18
+ import groq
19
+ import pypdf
20
+ import faiss
21
+ import graphviz
22
+ import genanki
23
+ from gtts import gTTS
24
+ from pydub import AudioSegment
25
+ import requests
26
+ from bs4 import BeautifulSoup
27
+ from youtube_transcript_api import YouTubeTranscriptApi
28
+ from sentence_transformers import SentenceTransformer
29
+ from fpdf import FPDF
30
+ from duckduckgo_search import DDGS
31
+ except ImportError:
32
+ # Em produção, isso deve falhar se as dependências não estiverem instaladas
33
+ pass
34
+
35
+ # --- 2. Configuração Global ---
36
+ # Usaremos a configuração passada ou variável de ambiente
37
+ GROQ_API_KEY = os.getenv("GROQ_API_KEY")
38
+
39
+ # --- 3. Camada de Ferramentas (Tooling Layer) ---
40
+
41
+ class ToolBox:
42
+ """Caixa de ferramentas para os agentes."""
43
+
44
+ @staticmethod
45
+ def read_pdf(filepath: str) -> str:
46
+ try:
47
+ print(f"📄 [Ferramenta] Lendo PDF: {filepath}...")
48
+ reader = pypdf.PdfReader(filepath)
49
+ text = "".join([p.extract_text() or "" for p in reader.pages])
50
+ return re.sub(r'\s+', ' ', text).strip()
51
+ except Exception as e:
52
+ return f"Erro ao ler PDF: {str(e)}"
53
+
54
+ @staticmethod
55
+ def scrape_web(url: str) -> str:
56
+ try:
57
+ print(f"🌐 [Ferramenta] Acessando URL: {url}...")
58
+ headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'}
59
+ response = requests.get(url, headers=headers, timeout=10)
60
+ soup = BeautifulSoup(response.content, 'html.parser')
61
+ for script in soup(["script", "style", "header", "footer", "nav"]):
62
+ script.extract()
63
+ text = soup.get_text()
64
+ return re.sub(r'\s+', ' ', text).strip()[:40000]
65
+ except Exception as e:
66
+ print(f"Erro ao acessar {url}: {e}")
67
+ return ""
68
+
69
+ @staticmethod
70
+ def search_topic(topic: str) -> List[str]:
71
+ """Pesquisa no DuckDuckGo e retorna URLs."""
72
+ print(f"🔎 [Ferramenta] Pesquisando na Web sobre: '{topic}'...")
73
+ urls = []
74
+ try:
75
+ with DDGS() as ddgs:
76
+ results = list(ddgs.text(topic, max_results=3))
77
+ for r in results:
78
+ urls.append(r['href'])
79
+ except Exception as e:
80
+ print(f"Erro na busca: {e}")
81
+ return urls
82
+
83
+ @staticmethod
84
+ def get_youtube_transcript(url: str) -> str:
85
+ try:
86
+ print(f"📺 [Ferramenta] Extraindo legendas do YouTube: {url}...")
87
+ video_id = url.split("v=")[-1].split("&")[0]
88
+ transcript = YouTubeTranscriptApi.get_transcript(video_id, languages=['pt', 'en'])
89
+ text = " ".join([t['text'] for t in transcript])
90
+ return text
91
+ except Exception as e:
92
+ return f"Erro ao pegar legendas do YouTube: {str(e)}"
93
+
94
+ @staticmethod
95
+ def generate_audio_mix(script: List[Dict], filename="aula_podcast.mp3"):
96
+ print("🎙️ [Estúdio] Produzindo áudio imersivo...")
97
+ combined = AudioSegment.silent(duration=500)
98
+
99
+ for line in script:
100
+ speaker = line.get("speaker", "Narrador").upper()
101
+ text = line.get("text", "")
102
+
103
+ if "BERTA" in speaker or "PROFESSORA" in speaker or "AGENT B" in speaker:
104
+ tts = gTTS(text=text, lang='pt', tld='pt', slow=False)
105
+ else:
106
+ # Gabriel / Agent A
107
+ tts = gTTS(text=text, lang='pt', tld='com.br', slow=False)
108
+
109
+ fp = BytesIO()
110
+ tts.write_to_fp(fp)
111
+ fp.seek(0)
112
+
113
+ try:
114
+ segment = AudioSegment.from_file(fp, format="mp3")
115
+ combined += segment
116
+ combined += AudioSegment.silent(duration=300)
117
+ except: pass
118
+
119
+ output_path = os.path.join("backend/generated", filename)
120
+ os.makedirs(os.path.dirname(output_path), exist_ok=True)
121
+ combined.export(output_path, format="mp3")
122
+ return output_path
123
+
124
+ @staticmethod
125
+ def generate_mindmap_image(dot_code: str, filename="mapa_mental"):
126
+ try:
127
+ print("🗺️ [Design] Renderizando Mapa Mental...")
128
+ clean_dot = dot_code.replace("```dot", "").replace("```", "").strip()
129
+
130
+ # Ensure generated directory exists
131
+ output_dir = "backend/generated"
132
+ os.makedirs(output_dir, exist_ok=True)
133
+ output_path = os.path.join(output_dir, filename)
134
+
135
+ src = graphviz.Source(clean_dot)
136
+ src.format = 'png'
137
+ filepath = src.render(output_path, view=False)
138
+ return filepath
139
+ except Exception as e:
140
+ print(f"Erro ao gerar gráfico: {e}")
141
+ return None
142
+
143
+ @staticmethod
144
+ def generate_anki_deck(qa_pairs: List[Dict], deck_name="ScholarGraph Deck"):
145
+ print("🧠 [Anki] Criando arquivo de Flashcards (.apkg)...")
146
+ try:
147
+ model_id = random.randrange(1 << 30, 1 << 31)
148
+ deck_id = random.randrange(1 << 30, 1 << 31)
149
+
150
+ my_model = genanki.Model(
151
+ model_id,
152
+ 'Simple Model',
153
+ fields=[{'name': 'Question'}, {'name': 'Answer'}],
154
+ templates=[{
155
+ 'name': 'Card 1',
156
+ 'qfmt': '{{Question}}',
157
+ 'afmt': '{{FrontSide}}<hr id="answer">{{Answer}}',
158
+ }]
159
+ )
160
+
161
+ my_deck = genanki.Deck(deck_id, deck_name)
162
+
163
+ for item in qa_pairs:
164
+ my_deck.add_note(genanki.Note(
165
+ model=my_model,
166
+ fields=[item['question'], item['answer']]
167
+ ))
168
+
169
+ output_dir = "backend/generated"
170
+ os.makedirs(output_dir, exist_ok=True)
171
+ filename = os.path.join(output_dir, f"flashcards_{uuid.uuid4().hex[:8]}.apkg")
172
+ genanki.Package(my_deck).write_to_file(filename)
173
+ return filename
174
+ except Exception as e:
175
+ print(f"Erro ao criar Anki deck: {e}")
176
+ return None
177
+
178
+ # --- 4. Vector Store (RAG) ---
179
+
180
+ class VectorMemory:
181
+ def __init__(self):
182
+ print("🧠 [Memória] Inicializando Banco de Vetores (RAG)...")
183
+ # Modelo leve para embeddings
184
+ self.model = SentenceTransformer('all-MiniLM-L6-v2')
185
+ self.index = None
186
+ self.chunks = []
187
+
188
+ def ingest(self, text: str, chunk_size=500):
189
+ words = text.split()
190
+ # Cria chunks sobrepostos para melhor contexto
191
+ self.chunks = [' '.join(words[i:i+chunk_size]) for i in range(0, len(words), int(chunk_size*0.8))]
192
+
193
+ print(f"🧠 [Memória] Vetorizando {len(self.chunks)} fragmentos...")
194
+ if not self.chunks: return
195
+
196
+ embeddings = self.model.encode(self.chunks)
197
+ dimension = embeddings.shape[1]
198
+ self.index = faiss.IndexFlatL2(dimension)
199
+ self.index.add(np.array(embeddings).astype('float32'))
200
+ print("🧠 [Memória] Indexação concluída.")
201
+
202
+ def retrieve(self, query: str, k=3) -> str:
203
+ if not self.index: return ""
204
+ query_vec = self.model.encode([query])
205
+ D, I = self.index.search(np.array(query_vec).astype('float32'), k)
206
+
207
+ results = [self.chunks[i] for i in I[0] if i < len(self.chunks)]
208
+ return "\n\n".join(results)
209
+
210
+ # --- 5. Estado e LLM ---
211
+
212
+ class GraphState:
213
+ def __init__(self):
214
+ self.raw_content: str = ""
215
+ self.summary: str = ""
216
+ self.script: List[Dict] = []
217
+ self.quiz_data: List[Dict] = []
218
+ self.mindmap_path: str = ""
219
+ self.flashcards: List[Dict] = []
220
+ self.current_quiz_question: int = 0
221
+ self.xp: int = 0
222
+ self.mode: str = "input" # input, menu, quiz
223
+
224
+ class LLMEngine:
225
+ def __init__(self, api_key=None):
226
+ self.api_key = api_key or os.environ.get("GROQ_API_KEY")
227
+ self.client = groq.Groq(api_key=self.api_key)
228
+ self.model = "moonshotai/kimi-k2-instruct-0905"
229
+
230
+ def chat(self, messages: List[Dict], json_mode=False) -> str:
231
+ try:
232
+ kwargs = {"messages": messages, "model": self.model, "temperature": 0.8}
233
+ if json_mode: kwargs["response_format"] = {"type": "json_object"}
234
+ return self.client.chat.completions.create(**kwargs).choices[0].message.content
235
+ except Exception as e:
236
+ return f"Erro na IA: {e}"
237
+
238
+ # --- 6. Agentes Avançados (GOD MODE) ---
239
+
240
+ class ResearcherAgent:
241
+ """Agente que pesquisa na web se o input for um tópico."""
242
+ def deep_research(self, topic: str) -> str:
243
+ print(f"🕵️ [Pesquisador] Iniciando Deep Research sobre: {topic}")
244
+ urls = ToolBox.search_topic(topic)
245
+ if not urls:
246
+ return f"Não encontrei informações sobre {topic}."
247
+
248
+ full_text = ""
249
+ for url in urls:
250
+ content = ToolBox.scrape_web(url)
251
+ if content:
252
+ full_text += f"\n\n--- Fonte: {url} ---\n{content[:10000]}"
253
+
254
+ return full_text
255
+
256
+ class FlashcardAgent:
257
+ """Agente focado em memorização (Anki)."""
258
+ def __init__(self, llm: LLMEngine):
259
+ self.llm = llm
260
+
261
+ def create_deck(self, content: str) -> List[Dict]:
262
+ print("🃏 [Flashcard] Gerando pares Pergunta-Resposta...")
263
+ prompt = f"""
264
+ Crie 10 Flashcards (Pergunta e Resposta) sobre o conteúdo para memorização.
265
+ SAÍDA JSON: {{ "cards": [ {{ "question": "...", "answer": "..." }} ] }}
266
+ Conteúdo: {content[:15000]}
267
+ """
268
+ try:
269
+ resp = self.llm.chat([{"role": "user", "content": prompt}], json_mode=True)
270
+ return json.loads(resp).get("cards", [])
271
+ except: return []
272
+
273
+ class IngestAgent:
274
+ def __init__(self, researcher: ResearcherAgent):
275
+ self.researcher = researcher
276
+
277
+ def process(self, user_input: str) -> str:
278
+ # Se for arquivo
279
+ if user_input.lower().endswith(".pdf") and os.path.exists(user_input):
280
+ return ToolBox.read_pdf(user_input)
281
+ # Se for URL
282
+ elif "youtube.com" in user_input or "youtu.be" in user_input:
283
+ return ToolBox.get_youtube_transcript(user_input)
284
+ elif user_input.startswith("http"):
285
+ return ToolBox.scrape_web(user_input)
286
+ # Se não for URL nem arquivo, assume que é Tópico para Pesquisa
287
+ else:
288
+ print("🔍 Entrada detectada como Tópico. Ativando ResearcherAgent...")
289
+ return self.researcher.deep_research(user_input)
290
+
291
+ class ProfessorAgent:
292
+ def __init__(self, llm: LLMEngine):
293
+ self.llm = llm
294
+
295
+ def summarize(self, full_text: str) -> str:
296
+ print("🧠 [Professor] Gerando resumo estratégico...")
297
+ prompt = f"""
298
+ Você é um Professor Universitário. Crie um resumo estruturado e profundo.
299
+ Texto: {full_text[:25000]}
300
+ Formato: # Título / ## Introdução / ## Pontos Chave / ## Conclusão
301
+ """
302
+ return self.llm.chat([{"role": "user", "content": prompt}])
303
+
304
+ class VisualizerAgent:
305
+ def __init__(self, llm: LLMEngine):
306
+ self.llm = llm
307
+
308
+ def create_mindmap(self, text: str) -> str:
309
+ print("🎨 [Visualizador] Projetando Mapa Mental...")
310
+ prompt = f"""
311
+ Crie um código GRAPHVIZ (DOT) para um mapa mental deste conteúdo.
312
+ Use formas coloridas. NÃO explique, apenas dê o código DOT dentro de ```dot ... ```.
313
+ Texto: {text[:15000]}
314
+ """
315
+ response = self.llm.chat([{"role": "user", "content": prompt}])
316
+ match = re.search(r'```dot(.*?)```', response, re.DOTALL)
317
+ if match: return match.group(1).strip()
318
+ return response
319
+
320
+ class ScriptwriterAgent:
321
+ def __init__(self, llm: LLMEngine):
322
+ self.llm = llm
323
+
324
+ def create_script(self, content: str, mode="lecture") -> List[Dict]:
325
+ if mode == "debate":
326
+ print("🔥 [Roteirista] Criando DEBATE INTENSO...")
327
+ prompt = f"""
328
+ Crie um DEBATE acalorado mas intelectual entre dois agentes (8 falas).
329
+ Personagens:
330
+ - AGENT A (Gabriel): A favor / Otimista / Pragmático.
331
+ - AGENT B (Berta): Contra / Cética / Filosófica.
332
+
333
+ SAÍDA JSON: {{ "dialogue": [ {{"speaker": "Agent A", "text": "..."}}, {{"speaker": "Agent B", "text": "..."}} ] }}
334
+ Tema Base: {content[:15000]}
335
+ """
336
+ else:
337
+ print("✍️ [Roteirista] Escrevendo roteiro de aula...")
338
+ prompt = f"""
339
+ Crie um roteiro de podcast (8 falas).
340
+ Personagens: GABRIEL (Aluno BR) e BERTA (Professora PT).
341
+ SAÍDA JSON: {{ "dialogue": [ {{"speaker": "Gabriel", "text": "..."}}, ...] }}
342
+ Base: {content[:15000]}
343
+ """
344
+
345
+ try:
346
+ resp = self.llm.chat([{"role": "user", "content": prompt}], json_mode=True)
347
+ return json.loads(resp).get("dialogue", [])
348
+ except: return []
349
+
350
+ class ExaminerAgent:
351
+ def __init__(self, llm: LLMEngine):
352
+ self.llm = llm
353
+
354
+ def generate_quiz(self, content: str) -> List[Dict]:
355
+ print("📝 [Examinador] Criando Prova Gamificada...")
356
+ prompt = f"""
357
+ Crie 5 perguntas de múltipla escolha (Difíceis).
358
+ SAÍDA JSON: {{ "quiz": [ {{ "question": "...", "options": ["A)..."], "correct_option": "A", "explanation": "..." }} ] }}
359
+ Base: {content[:15000]}
360
+ """
361
+ try:
362
+ resp = self.llm.chat([{"role": "user", "content": prompt}], json_mode=True)
363
+ return json.loads(resp).get("quiz", [])
364
+ except: return []
365
+
366
+ class PublisherAgent:
367
+ def create_handout(self, state: GraphState, filename="Apostila_Estudos.pdf"):
368
+ print("📚 [Editora] Diagramando Apostila PDF...")
369
+ pdf = FPDF()
370
+ pdf.add_page()
371
+ pdf.set_font("Arial", size=12)
372
+ pdf.set_font("Arial", 'B', 16)
373
+ pdf.cell(0, 10, "Apostila de Estudos - Scholar Graph", ln=True, align='C')
374
+ pdf.ln(10)
375
+ pdf.set_font("Arial", size=11)
376
+ safe_summary = state.summary.encode('latin-1', 'replace').decode('latin-1')
377
+ pdf.multi_cell(0, 7, safe_summary)
378
+ if state.mindmap_path and os.path.exists(state.mindmap_path):
379
+ pdf.add_page()
380
+ pdf.image(state.mindmap_path, x=10, y=30, w=190)
381
+
382
+ output_dir = "backend/generated"
383
+ os.makedirs(output_dir, exist_ok=True)
384
+ filepath = os.path.join(output_dir, filename)
385
+ pdf.output(filepath)
386
+ return filepath
387
+
388
+ # --- 7. Agent Class wrapper for backend integration ---
389
+
390
+ class ScholarAgent:
391
+ def __init__(self):
392
+ self.user_states = {} # Map user_id to (ScholarGraphGodMode instance or GraphState)
393
+ self.api_key = os.getenv("GROQ_API_KEY")
394
+ # Initialize one engine for general use if needed, but we probably need instances per user or shared resources.
395
+ # We'll create instances per user request if they don't exist?
396
+ # Actually, let's keep it simple. We store state per user.
397
+
398
+ def get_or_create_state(self, user_id):
399
+ if user_id not in self.user_states:
400
+ self.user_states[user_id] = {
401
+ "state": GraphState(),
402
+ "memory": VectorMemory(),
403
+ "llm": LLMEngine(self.api_key),
404
+ "researcher": ResearcherAgent(),
405
+ "ingestor": None, # Will be init with researcher
406
+ "professor": None,
407
+ "visualizer": None,
408
+ "scriptwriter": None,
409
+ "examiner": None,
410
+ "flashcarder": None,
411
+ "publisher": None
412
+ }
413
+ # Wiring dependencies
414
+ u = self.user_states[user_id]
415
+ u["ingestor"] = IngestAgent(u["researcher"])
416
+ u["professor"] = ProfessorAgent(u["llm"])
417
+ u["visualizer"] = VisualizerAgent(u["llm"])
418
+ u["scriptwriter"] = ScriptwriterAgent(u["llm"])
419
+ u["examiner"] = ExaminerAgent(u["llm"])
420
+ u["flashcarder"] = FlashcardAgent(u["llm"])
421
+ u["publisher"] = PublisherAgent()
422
+
423
+ return self.user_states[user_id]
424
+
425
+ def respond(self, history, user_input, user_id="default", vision_context=None):
426
+ """
427
+ Adapts the CLI interaction loop to a Request/Response model.
428
+ """
429
+ u = self.get_or_create_state(user_id)
430
+ state = u["state"]
431
+
432
+ # Helper to format menu
433
+ def get_menu():
434
+ return (
435
+ "\n\n🎓 *MENU SCHOLAR GRAPH*\n"
436
+ "1. 🧠 Resumo Estratégico\n"
437
+ "2. 🗺️ Mapa Mental Visual\n"
438
+ "3. 🎧 Podcast (Aula Didática)\n"
439
+ "4. 🔥 DEBATE IA (Visões Opostas)\n"
440
+ "5. 🎮 Quiz Gamificado\n"
441
+ "6. 🃏 Gerar Flashcards (Anki .apkg)\n"
442
+ "7. 📚 Baixar Apostila Completa\n"
443
+ "8. 🔄 Novo Tópico\n"
444
+ "👉 Escolha uma opção (número ou texto):"
445
+ )
446
+
447
+ # Helper for response with optional file
448
+ response_text = ""
449
+ audio_path = None
450
+
451
+ # State Machine Logic
452
+
453
+ # 1. Input Mode: Waiting for topic/url/pdf
454
+ if state.mode == "input":
455
+ if not user_input.strip():
456
+ return "Por favor, forneça um tópico, URL ou arquivo PDF para começar.", None, history
457
+
458
+ response_text = f"🔄 Processando '{user_input}'... (Isso pode levar alguns segundos)"
459
+
460
+ # Process content
461
+ content = u["ingestor"].process(user_input)
462
+ if not content or len(content) < 50:
463
+ response_text = "❌ Falha ao obter conteúdo suficiente ou tópico não encontrado. Tente novamente."
464
+ return response_text, None, history
465
+
466
+ state.raw_content = content
467
+ u["memory"].ingest(content)
468
+ state.mode = "menu"
469
+ response_text += "\n✅ Conteúdo processado com sucesso!" + get_menu()
470
+
471
+ # Update history
472
+ history.append({"role": "user", "content": user_input})
473
+ history.append({"role": "assistant", "content": response_text})
474
+ return response_text, None, history
475
+
476
+ # 2. Quiz Mode
477
+ elif state.mode == "quiz":
478
+ # Check answer
479
+ current_q = state.quiz_data[state.current_quiz_question]
480
+ ans = user_input.strip().upper()
481
+
482
+ feedback = ""
483
+ if ans and ans[0] == current_q['correct_option'][0]:
484
+ state.xp += 100
485
+ feedback = f"✨ ACERTOU! +100 XP. (Total: {state.xp})\n"
486
+ else:
487
+ feedback = f"💀 Errou... A resposta era {current_q['correct_option']}.\nExplanation: {current_q.get('explanation', '')}\n"
488
+
489
+ state.current_quiz_question += 1
490
+
491
+ if state.current_quiz_question < len(state.quiz_data):
492
+ # Next Question
493
+ q = state.quiz_data[state.current_quiz_question]
494
+ response_text = feedback + f"\n🔹 QUESTÃO {state.current_quiz_question+1}:\n{q['question']}\n" + "\n".join(q['options'])
495
+ else:
496
+ # End of Quiz
497
+ response_text = feedback + f"\n🏆 FIM DO QUIZ! TOTAL DE XP: {state.xp}\n" + get_menu()
498
+ state.mode = "menu"
499
+
500
+ history.append({"role": "user", "content": user_input})
501
+ history.append({"role": "assistant", "content": response_text})
502
+ return response_text, None, history
503
+
504
+ # 3. Menu Mode
505
+ elif state.mode == "menu":
506
+ opt = user_input.strip()
507
+
508
+ if opt.startswith("1") or "resumo" in opt.lower():
509
+ state.summary = u["professor"].summarize(state.raw_content)
510
+ response_text = "📝 *RESUMO ESTRATÉGICO:*\n\n" + state.summary + get_menu()
511
+
512
+ elif opt.startswith("2") or "mapa" in opt.lower():
513
+ dot = u["visualizer"].create_mindmap(state.raw_content)
514
+ filename = f"mindmap_{uuid.uuid4().hex[:8]}"
515
+ path = ToolBox.generate_mindmap_image(dot, filename)
516
+ if path:
517
+ state.mindmap_path = path
518
+ # Since we return text and audio only in this signature, we might need a way to send image.
519
+ # The current app structure supports sending audio_base64.
520
+ # We might need to hack it to send image link or modify app.py.
521
+ # For now, let's return a link relative to backend/generated (assuming static serving)
522
+ response_text = f"🗺️ Mapa Mental gerado: [Baixar Imagem](/generated/{os.path.basename(path)})\n" + get_menu()
523
+ else:
524
+ response_text = "❌ Erro ao gerar mapa mental." + get_menu()
525
+
526
+ elif opt.startswith("3") or "podcast" in opt.lower():
527
+ script = u["scriptwriter"].create_script(state.raw_content, mode="lecture")
528
+ filename = f"podcast_{uuid.uuid4().hex[:8]}.mp3"
529
+ path = ToolBox.generate_audio_mix(script, filename)
530
+ audio_path = path # Return this to be played
531
+ response_text = "🎧 Aqui está o seu Podcast sobre o tema." + get_menu()
532
+
533
+ elif opt.startswith("4") or "debate" in opt.lower():
534
+ script = u["scriptwriter"].create_script(state.raw_content, mode="debate")
535
+ filename = f"debate_{uuid.uuid4().hex[:8]}.mp3"
536
+ path = ToolBox.generate_audio_mix(script, filename)
537
+ audio_path = path
538
+ response_text = "🔥 Debate gerado com sucesso." + get_menu()
539
+
540
+ elif opt.startswith("5") or "quiz" in opt.lower():
541
+ state.quiz_data = u["examiner"].generate_quiz(state.raw_content)
542
+ if state.quiz_data:
543
+ state.mode = "quiz"
544
+ state.current_quiz_question = 0
545
+ state.xp = 0
546
+ q = state.quiz_data[0]
547
+ response_text = f"🎮 *MODO QUIZ INICIADO*\n\n🔹 QUESTÃO 1:\n{q['question']}\n" + "\n".join(q['options'])
548
+ else:
549
+ response_text = "❌ Não foi possível gerar o quiz." + get_menu()
550
+
551
+ elif opt.startswith("6") or "flashcard" in opt.lower():
552
+ cards = u["flashcarder"].create_deck(state.raw_content)
553
+ if cards:
554
+ path = ToolBox.generate_anki_deck(cards)
555
+ if path:
556
+ response_text = f"✅ Flashcards gerados: [Baixar Deck Anki](/generated/{os.path.basename(path)})" + get_menu()
557
+ else:
558
+ response_text = "❌ Erro ao salvar arquivo." + get_menu()
559
+ else:
560
+ response_text = "❌ Erro ao gerar flashcards." + get_menu()
561
+
562
+ elif opt.startswith("7") or "apostila" in opt.lower():
563
+ if state.summary:
564
+ filename = f"apostila_{uuid.uuid4().hex[:8]}.pdf"
565
+ path = u["publisher"].create_handout(state, filename)
566
+ response_text = f"📚 Apostila pronta: [Baixar PDF](/generated/{os.path.basename(path)})" + get_menu()
567
+ else:
568
+ response_text = "⚠️ Gere o Resumo (Opção 1) primeiro!" + get_menu()
569
+
570
+ elif opt.startswith("8") or "novo" in opt.lower() or "sair" in opt.lower():
571
+ state.mode = "input"
572
+ # Reset state?
573
+ state.raw_content = ""
574
+ state.summary = ""
575
+ response_text = "🔄 Reiniciando... Qual o novo tópico, link ou PDF?"
576
+
577
+ else:
578
+ response_text = "Opção inválida. Tente novamente.\n" + get_menu()
579
+
580
+ history.append({"role": "user", "content": user_input})
581
+ history.append({"role": "assistant", "content": response_text})
582
+ return response_text, audio_path, history
583
+
584
+ return "Erro de estado.", None, history
jade/tts.py CHANGED
@@ -1,26 +1,26 @@
1
- from gtts import gTTS
2
- import tempfile
3
-
4
- class TTSPlayer:
5
- def __init__(self, lang="pt"):
6
- self.lang = lang
7
-
8
- def save_audio_to_file(self, text):
9
- """
10
- Gera o áudio a partir do texto e o salva em um arquivo MP3 temporário.
11
- Retorna o caminho (path) para o arquivo de áudio gerado.
12
- """
13
- try:
14
- tts = gTTS(text, lang=self.lang, slow=False)
15
-
16
- # Cria um arquivo temporário com a extensão .mp3
17
- with tempfile.NamedTemporaryFile(suffix=".mp3", delete=False) as fp:
18
- temp_filename = fp.name
19
-
20
- # Salva o áudio no arquivo temporário
21
- tts.save(temp_filename)
22
-
23
- return temp_filename
24
- except Exception as e:
25
- print(f"Erro ao gerar arquivo de áudio TTS: {e}")
26
  return None
 
1
+ from gtts import gTTS
2
+ import tempfile
3
+
4
+ class TTSPlayer:
5
+ def __init__(self, lang="pt"):
6
+ self.lang = lang
7
+
8
+ def save_audio_to_file(self, text):
9
+ """
10
+ Gera o áudio a partir do texto e o salva em um arquivo MP3 temporário.
11
+ Retorna o caminho (path) para o arquivo de áudio gerado.
12
+ """
13
+ try:
14
+ tts = gTTS(text, lang=self.lang, slow=False)
15
+
16
+ # Cria um arquivo temporário com a extensão .mp3
17
+ with tempfile.NamedTemporaryFile(suffix=".mp3", delete=False) as fp:
18
+ temp_filename = fp.name
19
+
20
+ # Salva o áudio no arquivo temporário
21
+ tts.save(temp_filename)
22
+
23
+ return temp_filename
24
+ except Exception as e:
25
+ print(f"Erro ao gerar arquivo de áudio TTS: {e}")
26
  return None
jade/web_search.py ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # jade/web_search.py - Tavily Web Search Handler
2
+ import os
3
+ import logging
4
+
5
+ logger = logging.getLogger("JadeWebSearch")
6
+
7
+ class WebSearchHandler:
8
+ """Handler para busca web em tempo real usando Tavily API."""
9
+
10
+ def __init__(self):
11
+ self.api_key = os.getenv("TAVILY_API_KEY")
12
+ self.client = None
13
+
14
+ if self.api_key:
15
+ try:
16
+ from tavily import TavilyClient
17
+ self.client = TavilyClient(api_key=self.api_key)
18
+ logger.info("✅ Tavily WebSearch inicializado com sucesso.")
19
+ except ImportError:
20
+ logger.warning("⚠️ tavily-python não instalado. Web search desabilitado.")
21
+ else:
22
+ logger.warning("⚠️ TAVILY_API_KEY não encontrada. Web search desabilitado.")
23
+
24
+ def search(self, query: str, max_results: int = 3) -> str:
25
+ """
26
+ Busca na web e retorna contexto formatado para a IA.
27
+
28
+ Args:
29
+ query: Termo de busca
30
+ max_results: Número máximo de resultados
31
+
32
+ Returns:
33
+ String formatada com os resultados da busca
34
+ """
35
+ if not self.client:
36
+ return ""
37
+
38
+ try:
39
+ logger.info(f"🔍 [WebSearch] Buscando: '{query}'")
40
+
41
+ response = self.client.search(
42
+ query=query,
43
+ search_depth="basic", # "basic" é mais rápido, "advanced" mais completo
44
+ max_results=max_results,
45
+ include_answer=True, # Tavily já gera um resumo
46
+ include_raw_content=False # Não precisamos do HTML cru
47
+ )
48
+
49
+ # Monta contexto formatado
50
+ context_parts = []
51
+
52
+ # Resposta resumida do Tavily (se disponível)
53
+ if response.get("answer"):
54
+ context_parts.append(f"📝 Resumo: {response['answer']}")
55
+
56
+ # Resultados individuais
57
+ results = response.get("results", [])
58
+ if results:
59
+ context_parts.append("\n📰 Fontes encontradas:")
60
+ for i, result in enumerate(results[:max_results], 1):
61
+ title = result.get("title", "Sem título")
62
+ url = result.get("url", "")
63
+ content = result.get("content", "")[:500] # Limita tamanho
64
+ context_parts.append(f"\n{i}. **{title}**\n URL: {url}\n {content}")
65
+
66
+ context = "\n".join(context_parts)
67
+ logger.info(f"🔍 [WebSearch] Encontrados {len(results)} resultados.")
68
+
69
+ return context
70
+
71
+ except Exception as e:
72
+ logger.error(f"❌ Erro na busca Tavily: {e}")
73
+ return f"Erro ao buscar na web: {str(e)}"
74
+
75
+ def is_available(self) -> bool:
76
+ """Verifica se o serviço de busca está disponível."""
77
+ return self.client is not None
requirements.txt CHANGED
@@ -1,30 +1,31 @@
1
- groq
2
- gtts
3
- transformers==4.45.2
4
- Pillow
5
- scipy
6
- torch
7
- torchvision
8
- chromadb
9
- sentence-transformers
10
- gradio
11
- fastapi
12
- uvicorn[standard]
13
- joblib
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
28
- mistralai
29
- openai
30
- colorama
 
 
1
+ groq
2
+ gtts
3
+ transformers==4.45.2
4
+ Pillow
5
+ scipy
6
+ torch
7
+ torchvision
8
+ chromadb
9
+ sentence-transformers
10
+ gradio
11
+ fastapi
12
+ uvicorn[standard]
13
+ joblib
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
28
+ mistralai
29
+ openai
30
+ colorama
31
+ tavily-python