Madras1 commited on
Commit
594f99d
·
verified ·
1 Parent(s): 1e07757

Upload 14 files

Browse files
Files changed (4) hide show
  1. app.py +28 -17
  2. gitattributes +35 -0
  3. jade/heavy_mode.py +226 -0
  4. requirements.txt +3 -0
app.py CHANGED
@@ -2,6 +2,7 @@
2
  import os
3
  import base64
4
  import io
 
5
  from fastapi import FastAPI
6
  from fastapi.middleware.cors import CORSMiddleware
7
  from fastapi.staticfiles import StaticFiles
@@ -9,10 +10,14 @@ from pydantic import BaseModel
9
  from PIL import Image
10
  from jade.core import JadeAgent
11
  from jade.scholar import ScholarAgent
 
12
 
13
  print("Iniciando a J.A.D.E. com FastAPI...")
14
  jade_agent = JadeAgent()
15
  scholar_agent = ScholarAgent()
 
 
 
16
  print("J.A.D.E. pronta para receber requisições.")
17
 
18
  app = FastAPI(title="J.A.D.E. API")
@@ -26,17 +31,17 @@ os.makedirs("backend/generated", exist_ok=True)
26
  app.mount("/generated", StaticFiles(directory="backend/generated"), name="generated")
27
 
28
  # Dicionário global para armazenar sessões de usuários
29
- # Structure: user_sessions[user_id] = { "jade": [...], "scholar": [...] }
30
  user_sessions = {}
31
 
32
  class UserRequest(BaseModel):
33
  user_input: str
34
  image_base64: str | None = None
35
  user_id: str | None = None
36
- agent_type: str = "jade" # "jade" or "scholar"
37
 
38
  @app.post("/chat")
39
- def handle_chat(request: UserRequest):
40
  try:
41
  user_id = request.user_id if request.user_id else "default_user"
42
  agent_type = request.agent_type.lower()
@@ -45,12 +50,14 @@ def handle_chat(request: UserRequest):
45
  print(f"Nova sessão criada para: {user_id}")
46
  user_sessions[user_id] = {
47
  "jade": [jade_agent.system_prompt],
48
- "scholar": []
 
49
  }
50
 
51
- # Ensure sub-keys exist if session existed but new agent type is used
52
  if "jade" not in user_sessions[user_id]: user_sessions[user_id]["jade"] = [jade_agent.system_prompt]
53
  if "scholar" not in user_sessions[user_id]: user_sessions[user_id]["scholar"] = []
 
54
 
55
  vision_context = None
56
  if request.image_base64:
@@ -58,7 +65,7 @@ def handle_chat(request: UserRequest):
58
  header, encoded_data = request.image_base64.split(",", 1)
59
  image_bytes = base64.b64decode(encoded_data)
60
  pil_image = Image.open(io.BytesIO(image_bytes))
61
- # Only Jade handles vision for now, but we can pass context if needed
62
  vision_context = jade_agent.image_handler.process_pil_image(pil_image)
63
  except Exception as img_e:
64
  print(f"Erro ao processar imagem Base64: {img_e}")
@@ -78,9 +85,22 @@ def handle_chat(request: UserRequest):
78
  vision_context=vision_context
79
  )
80
  user_sessions[user_id]["scholar"] = updated_history
 
 
 
 
 
 
 
 
 
 
 
 
81
  else:
82
  # Default to J.A.D.E.
83
  current_history = user_sessions[user_id]["jade"]
 
84
  bot_response_text, audio_path, updated_history = jade_agent.respond(
85
  history=current_history,
86
  user_input=final_user_input,
@@ -89,16 +109,7 @@ def handle_chat(request: UserRequest):
89
  )
90
  user_sessions[user_id]["jade"] = updated_history
91
 
92
- # LÓGICA DO ÁUDIO: Converte o arquivo MP3 gerado para Base64
93
- # Scholar agent might return a path to a static file instead of a temp file to be deleted.
94
- # We need to distinguish.
95
- # JadeAgent returns a temp file that is deleted.
96
- # ScholarAgent returns a file in /generated/ that should PROBABLY remain accessible via URL,
97
- # OR we can send it as base64 too.
98
- # If the path starts with "backend/generated", we assume it is static and we might want to return the URL?
99
- # BUT the frontend expects audio_base64 to play it immediately.
100
- # So we can still base64 encode it for immediate playback.
101
-
102
  audio_base64 = None
103
  if audio_path and os.path.exists(audio_path):
104
  print(f"Codificando arquivo de áudio: {audio_path}")
@@ -106,7 +117,7 @@ def handle_chat(request: UserRequest):
106
  audio_bytes = audio_file.read()
107
  audio_base64 = base64.b64encode(audio_bytes).decode('utf-8')
108
 
109
- # Only remove if it's NOT in generated (i.e. it's a temp file from Jade)
110
  if "backend/generated" not in audio_path:
111
  os.remove(audio_path)
112
 
 
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
 
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")
 
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()
 
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:
 
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}")
 
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,
 
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}")
 
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
 
gitattributes ADDED
@@ -0,0 +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
jade/heavy_mode.py ADDED
@@ -0,0 +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": "meta-llama/llama-4-maverick-17b-128e-instruct", # Groq
49
+ "Qwen": "qwen/qwen-2.5-coder-32b-instruct" # 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 = "llama-3.3-70b-versatile" # 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-235b-a22b: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 = "llama-3.3-70b-versatile"
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.1
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.4
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
requirements.txt CHANGED
@@ -25,3 +25,6 @@ faiss-cpu
25
  graphviz
26
  duckduckgo-search
27
  genanki
 
 
 
 
25
  graphviz
26
  duckduckgo-search
27
  genanki
28
+ mistralai
29
+ openai
30
+ colorama