Upload 14 files
Browse files- app.py +28 -17
- gitattributes +35 -0
- jade/heavy_mode.py +226 -0
- 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"
|
| 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
|
| 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 |
-
#
|
| 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 |
-
#
|
| 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 |
-
#
|
| 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
|