Spaces:
Sleeping
Sleeping
Upload 5 files
Browse files- app.py +152 -90
- providers.py +178 -0
- requirements.txt +1 -0
- tools.py +175 -0
app.py
CHANGED
|
@@ -1,9 +1,6 @@
|
|
| 1 |
"""
|
| 2 |
-
Jade Code IDE - HuggingFace Space Backend
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
Cada usuário fornece sua própria API key via header.
|
| 6 |
-
O backend nunca armazena keys - só repassa pro LLM.
|
| 7 |
"""
|
| 8 |
|
| 9 |
import os
|
|
@@ -14,20 +11,22 @@ import base64
|
|
| 14 |
import traceback
|
| 15 |
import re
|
| 16 |
from contextlib import redirect_stdout, redirect_stderr
|
| 17 |
-
from typing import Optional
|
| 18 |
|
| 19 |
-
from fastapi import FastAPI, HTTPException,
|
| 20 |
from fastapi.middleware.cors import CORSMiddleware
|
| 21 |
from pydantic import BaseModel
|
| 22 |
import gradio as gr
|
| 23 |
|
|
|
|
|
|
|
|
|
|
| 24 |
# ============== App Setup ==============
|
| 25 |
-
app = FastAPI(title="Jade Code IDE API", version="
|
| 26 |
|
| 27 |
-
# CORS - permitir GitHub Pages e localhost para dev
|
| 28 |
app.add_middleware(
|
| 29 |
CORSMiddleware,
|
| 30 |
-
allow_origins=["*"],
|
| 31 |
allow_credentials=True,
|
| 32 |
allow_methods=["*"],
|
| 33 |
allow_headers=["*"],
|
|
@@ -37,8 +36,12 @@ app.add_middleware(
|
|
| 37 |
# ============== Request Models ==============
|
| 38 |
class ChatRequest(BaseModel):
|
| 39 |
message: str
|
| 40 |
-
|
| 41 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
|
| 43 |
class RunRequest(BaseModel):
|
| 44 |
code: str
|
|
@@ -47,24 +50,33 @@ class CompleteRequest(BaseModel):
|
|
| 47 |
prefix: str
|
| 48 |
suffix: str = ""
|
| 49 |
language: str = "python"
|
|
|
|
|
|
|
| 50 |
|
| 51 |
|
| 52 |
-
# ==============
|
| 53 |
-
|
| 54 |
-
"""Cria cliente Groq com a key do usuário (não armazena)."""
|
| 55 |
-
from groq import Groq
|
| 56 |
-
return Groq(api_key=api_key)
|
| 57 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
|
|
|
| 62 |
|
| 63 |
-
|
|
|
|
| 64 |
- Seja direto e técnico
|
| 65 |
-
-
|
| 66 |
- Explique o que está fazendo
|
| 67 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
"""
|
| 69 |
|
| 70 |
|
|
@@ -72,69 +84,124 @@ Regras:
|
|
| 72 |
|
| 73 |
@app.get("/")
|
| 74 |
async def root():
|
| 75 |
-
"""
|
| 76 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
|
| 78 |
|
| 79 |
@app.post("/chat")
|
| 80 |
async def chat(req: ChatRequest, x_api_key: str = Header(..., alias="X-API-Key")):
|
| 81 |
"""
|
| 82 |
-
Chat com
|
|
|
|
| 83 |
"""
|
| 84 |
if not x_api_key or len(x_api_key) < 10:
|
| 85 |
raise HTTPException(status_code=401, detail="API Key inválida")
|
| 86 |
|
| 87 |
try:
|
| 88 |
-
|
|
|
|
| 89 |
|
| 90 |
-
# Monta mensagens
|
| 91 |
messages = [{"role": "system", "content": SYSTEM_PROMPT}]
|
| 92 |
|
| 93 |
-
# Adiciona histórico
|
| 94 |
-
for msg in (req.history or [])[-10:]:
|
| 95 |
messages.append(msg)
|
| 96 |
|
| 97 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
user_msg = req.message
|
| 99 |
-
if
|
| 100 |
-
user_msg =
|
| 101 |
|
| 102 |
messages.append({"role": "user", "content": user_msg})
|
| 103 |
|
| 104 |
-
#
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
|
| 112 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
|
| 114 |
except Exception as e:
|
|
|
|
| 115 |
error_msg = str(e)
|
| 116 |
-
if "
|
| 117 |
-
raise HTTPException(status_code=401, detail="API Key inválida
|
| 118 |
raise HTTPException(status_code=500, detail=f"Erro: {error_msg}")
|
| 119 |
|
| 120 |
|
| 121 |
@app.post("/run")
|
| 122 |
async def run_code(req: RunRequest):
|
| 123 |
-
"""
|
| 124 |
-
Executa código Python (sandbox limitado).
|
| 125 |
-
Não precisa de API key - é só execução local.
|
| 126 |
-
"""
|
| 127 |
code = req.code
|
| 128 |
-
|
| 129 |
-
# Captura stdout/stderr
|
| 130 |
f_out = io.StringIO()
|
| 131 |
f_err = io.StringIO()
|
| 132 |
-
|
| 133 |
-
# Variável para capturar matplotlib
|
| 134 |
image_base64 = None
|
| 135 |
|
| 136 |
try:
|
| 137 |
-
# Prepara ambiente
|
| 138 |
import matplotlib
|
| 139 |
matplotlib.use('Agg')
|
| 140 |
import matplotlib.pyplot as plt
|
|
@@ -145,7 +212,6 @@ async def run_code(req: RunRequest):
|
|
| 145 |
"plt": plt,
|
| 146 |
}
|
| 147 |
|
| 148 |
-
# Importa libs comuns
|
| 149 |
try:
|
| 150 |
import numpy as np
|
| 151 |
exec_globals["np"] = np
|
|
@@ -159,11 +225,9 @@ async def run_code(req: RunRequest):
|
|
| 159 |
except ImportError:
|
| 160 |
pass
|
| 161 |
|
| 162 |
-
# Executa
|
| 163 |
with redirect_stdout(f_out), redirect_stderr(f_err):
|
| 164 |
exec(code, exec_globals)
|
| 165 |
|
| 166 |
-
# Captura figura matplotlib
|
| 167 |
if plt.get_fignums():
|
| 168 |
buf = io.BytesIO()
|
| 169 |
plt.savefig(buf, format='png', dpi=100, bbox_inches='tight',
|
|
@@ -188,16 +252,14 @@ async def run_code(req: RunRequest):
|
|
| 188 |
|
| 189 |
@app.post("/complete")
|
| 190 |
async def autocomplete(req: CompleteRequest, x_api_key: str = Header(..., alias="X-API-Key")):
|
| 191 |
-
"""
|
| 192 |
-
Autocomplete via LLM usando a key do usuário.
|
| 193 |
-
"""
|
| 194 |
if not x_api_key or len(x_api_key) < 10:
|
| 195 |
raise HTTPException(status_code=401, detail="API Key inválida")
|
| 196 |
|
| 197 |
try:
|
| 198 |
-
|
| 199 |
|
| 200 |
-
prompt = f"""Você é um autocomplete de código. Sugira completions
|
| 201 |
|
| 202 |
CÓDIGO ANTES DO CURSOR:
|
| 203 |
```{req.language}
|
|
@@ -214,19 +276,16 @@ Responda APENAS com um JSON array:
|
|
| 214 |
|
| 215 |
Máximo 5 sugestões. Tipos: Function, Variable, Class, Module, Keyword, Snippet"""
|
| 216 |
|
| 217 |
-
response =
|
| 218 |
messages=[
|
| 219 |
{"role": "system", "content": "Responda APENAS JSON válido."},
|
| 220 |
{"role": "user", "content": prompt}
|
| 221 |
],
|
| 222 |
-
model=
|
| 223 |
-
temperature=0.1,
|
| 224 |
-
max_tokens=500
|
| 225 |
)
|
| 226 |
|
| 227 |
-
response_text = response
|
| 228 |
|
| 229 |
-
# Parse JSON
|
| 230 |
match = re.search(r'\[.*\]', response_text, re.DOTALL)
|
| 231 |
if match:
|
| 232 |
suggestions = json.loads(match.group(0))
|
|
@@ -238,52 +297,55 @@ Máximo 5 sugestões. Tipos: Function, Variable, Class, Module, Keyword, Snippet
|
|
| 238 |
return {"suggestions": [], "error": str(e)}
|
| 239 |
|
| 240 |
|
| 241 |
-
# ============== Gradio
|
| 242 |
-
def gradio_chat(message, api_key):
|
| 243 |
-
"""Interface Gradio simples para testar."""
|
| 244 |
if not api_key:
|
| 245 |
-
return "❌ Coloque sua API Key
|
| 246 |
|
| 247 |
try:
|
| 248 |
-
|
| 249 |
-
response =
|
| 250 |
messages=[
|
| 251 |
{"role": "system", "content": SYSTEM_PROMPT},
|
| 252 |
{"role": "user", "content": message}
|
| 253 |
],
|
| 254 |
-
model=
|
| 255 |
-
temperature=0.3
|
| 256 |
)
|
| 257 |
-
return response
|
| 258 |
except Exception as e:
|
| 259 |
return f"❌ Erro: {str(e)}"
|
| 260 |
|
| 261 |
|
| 262 |
-
# Gradio UI
|
| 263 |
with gr.Blocks(title="Jade Code IDE API") as demo:
|
| 264 |
-
gr.Markdown("# 🟢 Jade Code IDE - API Backend")
|
| 265 |
-
gr.Markdown("
|
| 266 |
-
gr.Markdown("**API Docs**: `/docs`")
|
| 267 |
|
| 268 |
with gr.Row():
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
|
|
|
|
|
|
|
|
|
| 276 |
|
|
|
|
|
|
|
|
|
|
| 277 |
output = gr.Textbox(label="Resposta", lines=10)
|
| 278 |
|
| 279 |
-
send_btn.click(
|
| 280 |
-
|
|
|
|
|
|
|
|
|
|
| 281 |
|
| 282 |
-
# Mount Gradio app
|
| 283 |
app = gr.mount_gradio_app(app, demo, path="/ui")
|
| 284 |
|
| 285 |
|
| 286 |
-
# ============== Run ==============
|
| 287 |
if __name__ == "__main__":
|
| 288 |
import uvicorn
|
| 289 |
uvicorn.run(app, host="0.0.0.0", port=7860)
|
|
|
|
| 1 |
"""
|
| 2 |
+
Jade Code IDE - HuggingFace Space Backend v3.0
|
| 3 |
+
Modo Agêntico + Multi-Provider (Groq, Cerebras, OpenRouter)
|
|
|
|
|
|
|
|
|
|
| 4 |
"""
|
| 5 |
|
| 6 |
import os
|
|
|
|
| 11 |
import traceback
|
| 12 |
import re
|
| 13 |
from contextlib import redirect_stdout, redirect_stderr
|
| 14 |
+
from typing import Optional, List, Dict
|
| 15 |
|
| 16 |
+
from fastapi import FastAPI, HTTPException, Header
|
| 17 |
from fastapi.middleware.cors import CORSMiddleware
|
| 18 |
from pydantic import BaseModel
|
| 19 |
import gradio as gr
|
| 20 |
|
| 21 |
+
from providers import get_provider, list_all_providers, GroqProvider, CerebrasProvider, OpenRouterProvider
|
| 22 |
+
from tools import AGENT_TOOLS, ToolExecutor, parse_tool_calls, get_response_content, has_tool_calls
|
| 23 |
+
|
| 24 |
# ============== App Setup ==============
|
| 25 |
+
app = FastAPI(title="Jade Code IDE API", version="3.0")
|
| 26 |
|
|
|
|
| 27 |
app.add_middleware(
|
| 28 |
CORSMiddleware,
|
| 29 |
+
allow_origins=["*"],
|
| 30 |
allow_credentials=True,
|
| 31 |
allow_methods=["*"],
|
| 32 |
allow_headers=["*"],
|
|
|
|
| 36 |
# ============== Request Models ==============
|
| 37 |
class ChatRequest(BaseModel):
|
| 38 |
message: str
|
| 39 |
+
provider: str = "groq"
|
| 40 |
+
model: str = "llama-3.3-70b-versatile"
|
| 41 |
+
files: Optional[Dict[str, str]] = {} # {filename: content}
|
| 42 |
+
current_file: Optional[str] = None
|
| 43 |
+
agentic: bool = True # Modo agêntico ativado por padrão
|
| 44 |
+
history: Optional[List[Dict]] = []
|
| 45 |
|
| 46 |
class RunRequest(BaseModel):
|
| 47 |
code: str
|
|
|
|
| 50 |
prefix: str
|
| 51 |
suffix: str = ""
|
| 52 |
language: str = "python"
|
| 53 |
+
provider: str = "groq"
|
| 54 |
+
model: str = "llama-3.3-70b-versatile"
|
| 55 |
|
| 56 |
|
| 57 |
+
# ============== System Prompt ==============
|
| 58 |
+
SYSTEM_PROMPT = """Você é CodeJade, um assistente de programação avançado integrado a uma IDE.
|
|
|
|
|
|
|
|
|
|
| 59 |
|
| 60 |
+
CAPACIDADES:
|
| 61 |
+
- Você pode explorar o projeto do usuário usando ferramentas
|
| 62 |
+
- Você tem acesso aos arquivos do projeto
|
| 63 |
+
- Você pode ajudar a escrever, debugar e explicar código
|
| 64 |
|
| 65 |
+
FERRAMENTAS DISPONÍVEIS:
|
| 66 |
+
1. list_files - Lista todos os arquivos do projeto
|
| 67 |
+
2. read_file - Lê o conteúdo de um arquivo específico
|
| 68 |
+
3. search_code - Busca texto nos arquivos do projeto
|
| 69 |
|
| 70 |
+
REGRAS:
|
| 71 |
+
- Use as ferramentas quando precisar entender o contexto do projeto
|
| 72 |
- Seja direto e técnico
|
| 73 |
+
- Quando sugerir código, use blocos ```python
|
| 74 |
- Explique o que está fazendo
|
| 75 |
+
|
| 76 |
+
EXEMPLO:
|
| 77 |
+
Usuário: "Me explica o que esse projeto faz"
|
| 78 |
+
Você: [usa list_files para ver estrutura, depois read_file nos arquivos importantes]
|
| 79 |
+
Depois explica baseado no que leu.
|
| 80 |
"""
|
| 81 |
|
| 82 |
|
|
|
|
| 84 |
|
| 85 |
@app.get("/")
|
| 86 |
async def root():
|
| 87 |
+
return {"status": "ok", "message": "Jade Code IDE API v3.0 - Agentic Mode", "version": "3.0"}
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
@app.get("/providers")
|
| 91 |
+
async def get_providers():
|
| 92 |
+
"""Lista providers disponíveis."""
|
| 93 |
+
return {"providers": list_all_providers()}
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
@app.get("/models/{provider}")
|
| 97 |
+
async def get_models(provider: str):
|
| 98 |
+
"""Lista modelos de um provider."""
|
| 99 |
+
try:
|
| 100 |
+
# Cria provider com key dummy só pra pegar os modelos
|
| 101 |
+
prov = get_provider(provider, "dummy")
|
| 102 |
+
return {"models": prov.list_models()}
|
| 103 |
+
except ValueError as e:
|
| 104 |
+
raise HTTPException(status_code=400, detail=str(e))
|
| 105 |
|
| 106 |
|
| 107 |
@app.post("/chat")
|
| 108 |
async def chat(req: ChatRequest, x_api_key: str = Header(..., alias="X-API-Key")):
|
| 109 |
"""
|
| 110 |
+
Chat com modo agêntico.
|
| 111 |
+
O LLM pode usar ferramentas para explorar o projeto.
|
| 112 |
"""
|
| 113 |
if not x_api_key or len(x_api_key) < 10:
|
| 114 |
raise HTTPException(status_code=401, detail="API Key inválida")
|
| 115 |
|
| 116 |
try:
|
| 117 |
+
provider = get_provider(req.provider, x_api_key)
|
| 118 |
+
tool_executor = ToolExecutor(req.files)
|
| 119 |
|
| 120 |
+
# Monta mensagens iniciais
|
| 121 |
messages = [{"role": "system", "content": SYSTEM_PROMPT}]
|
| 122 |
|
| 123 |
+
# Adiciona histórico
|
| 124 |
+
for msg in (req.history or [])[-10:]:
|
| 125 |
messages.append(msg)
|
| 126 |
|
| 127 |
+
# Contexto do arquivo atual
|
| 128 |
+
context_parts = []
|
| 129 |
+
if req.files:
|
| 130 |
+
context_parts.append(f"📁 Projeto tem {len(req.files)} arquivo(s): {', '.join(req.files.keys())}")
|
| 131 |
+
if req.current_file and req.current_file in (req.files or {}):
|
| 132 |
+
content = req.files[req.current_file][:2000]
|
| 133 |
+
context_parts.append(f"📄 Arquivo aberto ({req.current_file}):\n```\n{content}\n```")
|
| 134 |
+
|
| 135 |
user_msg = req.message
|
| 136 |
+
if context_parts:
|
| 137 |
+
user_msg = "\n".join(context_parts) + f"\n\n💬 Mensagem: {req.message}"
|
| 138 |
|
| 139 |
messages.append({"role": "user", "content": user_msg})
|
| 140 |
|
| 141 |
+
# Loop agêntico
|
| 142 |
+
max_iterations = 5
|
| 143 |
+
tool_results = []
|
| 144 |
+
|
| 145 |
+
for i in range(max_iterations):
|
| 146 |
+
# Chama LLM
|
| 147 |
+
if req.agentic:
|
| 148 |
+
response = provider.chat(messages, req.model, tools=AGENT_TOOLS)
|
| 149 |
+
else:
|
| 150 |
+
response = provider.chat(messages, req.model)
|
| 151 |
+
|
| 152 |
+
# Verifica se tem tool calls
|
| 153 |
+
if has_tool_calls(response):
|
| 154 |
+
tool_calls = parse_tool_calls(response)
|
| 155 |
+
|
| 156 |
+
# Adiciona resposta do assistente com tool calls
|
| 157 |
+
assistant_msg = response["choices"][0]["message"]
|
| 158 |
+
messages.append(assistant_msg)
|
| 159 |
+
|
| 160 |
+
# Executa cada tool
|
| 161 |
+
for tc in tool_calls:
|
| 162 |
+
result = tool_executor.execute(tc["name"], tc["args"])
|
| 163 |
+
tool_results.append({"tool": tc["name"], "result": result[:500]})
|
| 164 |
+
|
| 165 |
+
# Adiciona resultado como mensagem de tool
|
| 166 |
+
messages.append({
|
| 167 |
+
"role": "tool",
|
| 168 |
+
"tool_call_id": tc["id"],
|
| 169 |
+
"content": result
|
| 170 |
+
})
|
| 171 |
+
|
| 172 |
+
# Continua o loop
|
| 173 |
+
continue
|
| 174 |
+
else:
|
| 175 |
+
# Resposta final
|
| 176 |
+
content = get_response_content(response)
|
| 177 |
+
return {
|
| 178 |
+
"response": content,
|
| 179 |
+
"tool_calls": tool_results if tool_results else None
|
| 180 |
+
}
|
| 181 |
|
| 182 |
+
# Se chegou aqui, atingiu limite
|
| 183 |
+
return {
|
| 184 |
+
"response": "⚠️ Limite de iterações atingido. Tente uma pergunta mais específica.",
|
| 185 |
+
"tool_calls": tool_results
|
| 186 |
+
}
|
| 187 |
|
| 188 |
except Exception as e:
|
| 189 |
+
traceback.print_exc()
|
| 190 |
error_msg = str(e)
|
| 191 |
+
if "401" in error_msg or "authentication" in error_msg.lower():
|
| 192 |
+
raise HTTPException(status_code=401, detail="API Key inválida para este provider")
|
| 193 |
raise HTTPException(status_code=500, detail=f"Erro: {error_msg}")
|
| 194 |
|
| 195 |
|
| 196 |
@app.post("/run")
|
| 197 |
async def run_code(req: RunRequest):
|
| 198 |
+
"""Executa código Python."""
|
|
|
|
|
|
|
|
|
|
| 199 |
code = req.code
|
|
|
|
|
|
|
| 200 |
f_out = io.StringIO()
|
| 201 |
f_err = io.StringIO()
|
|
|
|
|
|
|
| 202 |
image_base64 = None
|
| 203 |
|
| 204 |
try:
|
|
|
|
| 205 |
import matplotlib
|
| 206 |
matplotlib.use('Agg')
|
| 207 |
import matplotlib.pyplot as plt
|
|
|
|
| 212 |
"plt": plt,
|
| 213 |
}
|
| 214 |
|
|
|
|
| 215 |
try:
|
| 216 |
import numpy as np
|
| 217 |
exec_globals["np"] = np
|
|
|
|
| 225 |
except ImportError:
|
| 226 |
pass
|
| 227 |
|
|
|
|
| 228 |
with redirect_stdout(f_out), redirect_stderr(f_err):
|
| 229 |
exec(code, exec_globals)
|
| 230 |
|
|
|
|
| 231 |
if plt.get_fignums():
|
| 232 |
buf = io.BytesIO()
|
| 233 |
plt.savefig(buf, format='png', dpi=100, bbox_inches='tight',
|
|
|
|
| 252 |
|
| 253 |
@app.post("/complete")
|
| 254 |
async def autocomplete(req: CompleteRequest, x_api_key: str = Header(..., alias="X-API-Key")):
|
| 255 |
+
"""Autocomplete via LLM."""
|
|
|
|
|
|
|
| 256 |
if not x_api_key or len(x_api_key) < 10:
|
| 257 |
raise HTTPException(status_code=401, detail="API Key inválida")
|
| 258 |
|
| 259 |
try:
|
| 260 |
+
provider = get_provider(req.provider, x_api_key)
|
| 261 |
|
| 262 |
+
prompt = f"""Você é um autocomplete de código. Sugira completions.
|
| 263 |
|
| 264 |
CÓDIGO ANTES DO CURSOR:
|
| 265 |
```{req.language}
|
|
|
|
| 276 |
|
| 277 |
Máximo 5 sugestões. Tipos: Function, Variable, Class, Module, Keyword, Snippet"""
|
| 278 |
|
| 279 |
+
response = provider.chat(
|
| 280 |
messages=[
|
| 281 |
{"role": "system", "content": "Responda APENAS JSON válido."},
|
| 282 |
{"role": "user", "content": prompt}
|
| 283 |
],
|
| 284 |
+
model=req.model
|
|
|
|
|
|
|
| 285 |
)
|
| 286 |
|
| 287 |
+
response_text = get_response_content(response) or ""
|
| 288 |
|
|
|
|
| 289 |
match = re.search(r'\[.*\]', response_text, re.DOTALL)
|
| 290 |
if match:
|
| 291 |
suggestions = json.loads(match.group(0))
|
|
|
|
| 297 |
return {"suggestions": [], "error": str(e)}
|
| 298 |
|
| 299 |
|
| 300 |
+
# ============== Gradio UI ==============
|
| 301 |
+
def gradio_chat(message, provider, model, api_key):
|
|
|
|
| 302 |
if not api_key:
|
| 303 |
+
return "❌ Coloque sua API Key"
|
| 304 |
|
| 305 |
try:
|
| 306 |
+
prov = get_provider(provider, api_key)
|
| 307 |
+
response = prov.chat(
|
| 308 |
messages=[
|
| 309 |
{"role": "system", "content": SYSTEM_PROMPT},
|
| 310 |
{"role": "user", "content": message}
|
| 311 |
],
|
| 312 |
+
model=model
|
|
|
|
| 313 |
)
|
| 314 |
+
return get_response_content(response) or "Sem resposta"
|
| 315 |
except Exception as e:
|
| 316 |
return f"❌ Erro: {str(e)}"
|
| 317 |
|
| 318 |
|
|
|
|
| 319 |
with gr.Blocks(title="Jade Code IDE API") as demo:
|
| 320 |
+
gr.Markdown("# 🟢 Jade Code IDE - API Backend v3.0")
|
| 321 |
+
gr.Markdown("**Modo Agêntico** | Multi-Provider | API Docs: `/docs`")
|
|
|
|
| 322 |
|
| 323 |
with gr.Row():
|
| 324 |
+
provider_dd = gr.Dropdown(
|
| 325 |
+
choices=["groq", "cerebras", "openrouter"],
|
| 326 |
+
value="groq",
|
| 327 |
+
label="Provider"
|
| 328 |
+
)
|
| 329 |
+
model_dd = gr.Dropdown(
|
| 330 |
+
choices=["llama-3.3-70b-versatile", "llama-3.1-8b-instant"],
|
| 331 |
+
value="llama-3.3-70b-versatile",
|
| 332 |
+
label="Model"
|
| 333 |
+
)
|
| 334 |
|
| 335 |
+
api_key_input = gr.Textbox(label="API Key", type="password")
|
| 336 |
+
msg_input = gr.Textbox(label="Mensagem", lines=3)
|
| 337 |
+
send_btn = gr.Button("Enviar")
|
| 338 |
output = gr.Textbox(label="Resposta", lines=10)
|
| 339 |
|
| 340 |
+
send_btn.click(
|
| 341 |
+
gradio_chat,
|
| 342 |
+
inputs=[msg_input, provider_dd, model_dd, api_key_input],
|
| 343 |
+
outputs=output
|
| 344 |
+
)
|
| 345 |
|
|
|
|
| 346 |
app = gr.mount_gradio_app(app, demo, path="/ui")
|
| 347 |
|
| 348 |
|
|
|
|
| 349 |
if __name__ == "__main__":
|
| 350 |
import uvicorn
|
| 351 |
uvicorn.run(app, host="0.0.0.0", port=7860)
|
providers.py
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Jade Code IDE - LLM Providers
|
| 3 |
+
Suporte a múltiplos providers: Groq, Cerebras, OpenRouter
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from abc import ABC, abstractmethod
|
| 7 |
+
from typing import Optional, List, Dict, Any
|
| 8 |
+
import httpx
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class LLMProvider(ABC):
|
| 12 |
+
"""Classe base para providers de LLM."""
|
| 13 |
+
|
| 14 |
+
def __init__(self, api_key: str):
|
| 15 |
+
self.api_key = api_key
|
| 16 |
+
|
| 17 |
+
@abstractmethod
|
| 18 |
+
def chat(self, messages: List[Dict], model: str, tools: Optional[List] = None) -> Dict:
|
| 19 |
+
"""Envia mensagem pro LLM e retorna resposta."""
|
| 20 |
+
pass
|
| 21 |
+
|
| 22 |
+
@abstractmethod
|
| 23 |
+
def list_models(self) -> List[Dict]:
|
| 24 |
+
"""Lista modelos disponíveis."""
|
| 25 |
+
pass
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
class GroqProvider(LLMProvider):
|
| 29 |
+
"""Provider para Groq API."""
|
| 30 |
+
|
| 31 |
+
BASE_URL = "https://api.groq.com/openai/v1"
|
| 32 |
+
|
| 33 |
+
MODELS = [
|
| 34 |
+
{"id": "llama-3.3-70b-versatile", "name": "Llama 3.3 70B", "context": 128000},
|
| 35 |
+
{"id": "llama-3.1-8b-instant", "name": "Llama 3.1 8B (Fast)", "context": 128000},
|
| 36 |
+
{"id": "mixtral-8x7b-32768", "name": "Mixtral 8x7B", "context": 32768},
|
| 37 |
+
{"id": "gemma2-9b-it", "name": "Gemma 2 9B", "context": 8192},
|
| 38 |
+
]
|
| 39 |
+
|
| 40 |
+
def chat(self, messages: List[Dict], model: str, tools: Optional[List] = None) -> Dict:
|
| 41 |
+
headers = {
|
| 42 |
+
"Authorization": f"Bearer {self.api_key}",
|
| 43 |
+
"Content-Type": "application/json"
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
payload = {
|
| 47 |
+
"model": model,
|
| 48 |
+
"messages": messages,
|
| 49 |
+
"temperature": 0.3,
|
| 50 |
+
"max_tokens": 4096
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
if tools:
|
| 54 |
+
payload["tools"] = tools
|
| 55 |
+
payload["tool_choice"] = "auto"
|
| 56 |
+
|
| 57 |
+
with httpx.Client(timeout=60.0) as client:
|
| 58 |
+
response = client.post(
|
| 59 |
+
f"{self.BASE_URL}/chat/completions",
|
| 60 |
+
headers=headers,
|
| 61 |
+
json=payload
|
| 62 |
+
)
|
| 63 |
+
response.raise_for_status()
|
| 64 |
+
return response.json()
|
| 65 |
+
|
| 66 |
+
def list_models(self) -> List[Dict]:
|
| 67 |
+
return self.MODELS
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
class CerebrasProvider(LLMProvider):
|
| 71 |
+
"""Provider para Cerebras API (extremamente rápido)."""
|
| 72 |
+
|
| 73 |
+
BASE_URL = "https://api.cerebras.ai/v1"
|
| 74 |
+
|
| 75 |
+
MODELS = [
|
| 76 |
+
{"id": "llama3.1-70b", "name": "Llama 3.1 70B", "context": 128000},
|
| 77 |
+
{"id": "llama3.1-8b", "name": "Llama 3.1 8B (Ultra Fast)", "context": 128000},
|
| 78 |
+
]
|
| 79 |
+
|
| 80 |
+
def chat(self, messages: List[Dict], model: str, tools: Optional[List] = None) -> Dict:
|
| 81 |
+
headers = {
|
| 82 |
+
"Authorization": f"Bearer {self.api_key}",
|
| 83 |
+
"Content-Type": "application/json"
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
payload = {
|
| 87 |
+
"model": model,
|
| 88 |
+
"messages": messages,
|
| 89 |
+
"temperature": 0.3,
|
| 90 |
+
"max_tokens": 4096
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
# Cerebras suporta tools também
|
| 94 |
+
if tools:
|
| 95 |
+
payload["tools"] = tools
|
| 96 |
+
payload["tool_choice"] = "auto"
|
| 97 |
+
|
| 98 |
+
with httpx.Client(timeout=60.0) as client:
|
| 99 |
+
response = client.post(
|
| 100 |
+
f"{self.BASE_URL}/chat/completions",
|
| 101 |
+
headers=headers,
|
| 102 |
+
json=payload
|
| 103 |
+
)
|
| 104 |
+
response.raise_for_status()
|
| 105 |
+
return response.json()
|
| 106 |
+
|
| 107 |
+
def list_models(self) -> List[Dict]:
|
| 108 |
+
return self.MODELS
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
class OpenRouterProvider(LLMProvider):
|
| 112 |
+
"""Provider para OpenRouter (acesso a múltiplos modelos)."""
|
| 113 |
+
|
| 114 |
+
BASE_URL = "https://openrouter.ai/api/v1"
|
| 115 |
+
|
| 116 |
+
MODELS = [
|
| 117 |
+
{"id": "anthropic/claude-3.5-sonnet", "name": "Claude 3.5 Sonnet", "context": 200000},
|
| 118 |
+
{"id": "google/gemini-2.0-flash-exp:free", "name": "Gemini 2.0 Flash (Free)", "context": 1000000},
|
| 119 |
+
{"id": "meta-llama/llama-3.3-70b-instruct", "name": "Llama 3.3 70B", "context": 128000},
|
| 120 |
+
{"id": "deepseek/deepseek-chat", "name": "DeepSeek V3", "context": 64000},
|
| 121 |
+
{"id": "qwen/qwen-2.5-coder-32b-instruct", "name": "Qwen 2.5 Coder 32B", "context": 32000},
|
| 122 |
+
]
|
| 123 |
+
|
| 124 |
+
def chat(self, messages: List[Dict], model: str, tools: Optional[List] = None) -> Dict:
|
| 125 |
+
headers = {
|
| 126 |
+
"Authorization": f"Bearer {self.api_key}",
|
| 127 |
+
"Content-Type": "application/json",
|
| 128 |
+
"HTTP-Referer": "https://jade-code-ide.github.io",
|
| 129 |
+
"X-Title": "Jade Code IDE"
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
payload = {
|
| 133 |
+
"model": model,
|
| 134 |
+
"messages": messages,
|
| 135 |
+
"temperature": 0.3,
|
| 136 |
+
"max_tokens": 4096
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
if tools:
|
| 140 |
+
payload["tools"] = tools
|
| 141 |
+
payload["tool_choice"] = "auto"
|
| 142 |
+
|
| 143 |
+
with httpx.Client(timeout=120.0) as client:
|
| 144 |
+
response = client.post(
|
| 145 |
+
f"{self.BASE_URL}/chat/completions",
|
| 146 |
+
headers=headers,
|
| 147 |
+
json=payload
|
| 148 |
+
)
|
| 149 |
+
response.raise_for_status()
|
| 150 |
+
return response.json()
|
| 151 |
+
|
| 152 |
+
def list_models(self) -> List[Dict]:
|
| 153 |
+
return self.MODELS
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
# === Factory ===
|
| 157 |
+
def get_provider(provider_name: str, api_key: str) -> LLMProvider:
|
| 158 |
+
"""Retorna o provider correto baseado no nome."""
|
| 159 |
+
providers = {
|
| 160 |
+
"groq": GroqProvider,
|
| 161 |
+
"cerebras": CerebrasProvider,
|
| 162 |
+
"openrouter": OpenRouterProvider
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
provider_class = providers.get(provider_name.lower())
|
| 166 |
+
if not provider_class:
|
| 167 |
+
raise ValueError(f"Provider '{provider_name}' não suportado. Use: {list(providers.keys())}")
|
| 168 |
+
|
| 169 |
+
return provider_class(api_key)
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
def list_all_providers() -> List[Dict]:
|
| 173 |
+
"""Lista todos os providers disponíveis."""
|
| 174 |
+
return [
|
| 175 |
+
{"id": "groq", "name": "Groq", "description": "Rápido e gratuito"},
|
| 176 |
+
{"id": "cerebras", "name": "Cerebras", "description": "Ultra rápido"},
|
| 177 |
+
{"id": "openrouter", "name": "OpenRouter", "description": "Acesso a múltiplos modelos"},
|
| 178 |
+
]
|
requirements.txt
CHANGED
|
@@ -7,3 +7,4 @@ pydantic
|
|
| 7 |
matplotlib
|
| 8 |
numpy
|
| 9 |
pandas
|
|
|
|
|
|
| 7 |
matplotlib
|
| 8 |
numpy
|
| 9 |
pandas
|
| 10 |
+
httpx
|
tools.py
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Jade Code IDE - Agentic Tools
|
| 3 |
+
Ferramentas que o LLM pode usar para explorar o projeto
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from typing import Dict, List, Any, Optional
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
# === Tool Definitions (OpenAI format) ===
|
| 10 |
+
AGENT_TOOLS = [
|
| 11 |
+
{
|
| 12 |
+
"type": "function",
|
| 13 |
+
"function": {
|
| 14 |
+
"name": "list_files",
|
| 15 |
+
"description": "Lista todos os arquivos do projeto do usuário. Use para entender a estrutura do projeto.",
|
| 16 |
+
"parameters": {
|
| 17 |
+
"type": "object",
|
| 18 |
+
"properties": {},
|
| 19 |
+
"required": []
|
| 20 |
+
}
|
| 21 |
+
}
|
| 22 |
+
},
|
| 23 |
+
{
|
| 24 |
+
"type": "function",
|
| 25 |
+
"function": {
|
| 26 |
+
"name": "read_file",
|
| 27 |
+
"description": "Lê o conteúdo de um arquivo específico do projeto.",
|
| 28 |
+
"parameters": {
|
| 29 |
+
"type": "object",
|
| 30 |
+
"properties": {
|
| 31 |
+
"filename": {
|
| 32 |
+
"type": "string",
|
| 33 |
+
"description": "Nome do arquivo para ler (ex: 'utils.py', 'README.md')"
|
| 34 |
+
}
|
| 35 |
+
},
|
| 36 |
+
"required": ["filename"]
|
| 37 |
+
}
|
| 38 |
+
}
|
| 39 |
+
},
|
| 40 |
+
{
|
| 41 |
+
"type": "function",
|
| 42 |
+
"function": {
|
| 43 |
+
"name": "search_code",
|
| 44 |
+
"description": "Busca um texto em todos os arquivos do projeto.",
|
| 45 |
+
"parameters": {
|
| 46 |
+
"type": "object",
|
| 47 |
+
"properties": {
|
| 48 |
+
"query": {
|
| 49 |
+
"type": "string",
|
| 50 |
+
"description": "Texto ou padrão para buscar nos arquivos"
|
| 51 |
+
}
|
| 52 |
+
},
|
| 53 |
+
"required": ["query"]
|
| 54 |
+
}
|
| 55 |
+
}
|
| 56 |
+
}
|
| 57 |
+
]
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
class ToolExecutor:
|
| 61 |
+
"""Executa ferramentas do agente usando os arquivos fornecidos pelo frontend."""
|
| 62 |
+
|
| 63 |
+
def __init__(self, files: Dict[str, str]):
|
| 64 |
+
"""
|
| 65 |
+
Args:
|
| 66 |
+
files: Dicionário {filename: content} com os arquivos do projeto
|
| 67 |
+
"""
|
| 68 |
+
self.files = files or {}
|
| 69 |
+
|
| 70 |
+
def execute(self, tool_name: str, args: Dict[str, Any]) -> str:
|
| 71 |
+
"""Executa uma ferramenta e retorna o resultado como string."""
|
| 72 |
+
|
| 73 |
+
if tool_name == "list_files":
|
| 74 |
+
return self._list_files()
|
| 75 |
+
elif tool_name == "read_file":
|
| 76 |
+
return self._read_file(args.get("filename", ""))
|
| 77 |
+
elif tool_name == "search_code":
|
| 78 |
+
return self._search_code(args.get("query", ""))
|
| 79 |
+
else:
|
| 80 |
+
return f"❌ Ferramenta desconhecida: {tool_name}"
|
| 81 |
+
|
| 82 |
+
def _list_files(self) -> str:
|
| 83 |
+
"""Lista arquivos do projeto."""
|
| 84 |
+
if not self.files:
|
| 85 |
+
return "📁 Nenhum arquivo no projeto."
|
| 86 |
+
|
| 87 |
+
file_list = []
|
| 88 |
+
for name, content in self.files.items():
|
| 89 |
+
lines = content.count('\n') + 1 if content else 0
|
| 90 |
+
size = len(content)
|
| 91 |
+
file_list.append(f" 📄 {name} ({lines} linhas, {size} bytes)")
|
| 92 |
+
|
| 93 |
+
return f"📁 Arquivos do projeto ({len(self.files)}):\n" + "\n".join(file_list)
|
| 94 |
+
|
| 95 |
+
def _read_file(self, filename: str) -> str:
|
| 96 |
+
"""Lê conteúdo de um arquivo."""
|
| 97 |
+
if not filename:
|
| 98 |
+
return "❌ Nome do arquivo não especificado."
|
| 99 |
+
|
| 100 |
+
content = self.files.get(filename)
|
| 101 |
+
if content is None:
|
| 102 |
+
available = ", ".join(self.files.keys()) if self.files else "nenhum"
|
| 103 |
+
return f"❌ Arquivo '{filename}' não encontrado. Disponíveis: {available}"
|
| 104 |
+
|
| 105 |
+
# Limita tamanho pra não estourar contexto
|
| 106 |
+
if len(content) > 5000:
|
| 107 |
+
content = content[:5000] + f"\n\n... [truncado, {len(content)} bytes total]"
|
| 108 |
+
|
| 109 |
+
return f"📄 Conteúdo de '{filename}':\n```\n{content}\n```"
|
| 110 |
+
|
| 111 |
+
def _search_code(self, query: str) -> str:
|
| 112 |
+
"""Busca texto nos arquivos."""
|
| 113 |
+
if not query:
|
| 114 |
+
return "❌ Query de busca não especificada."
|
| 115 |
+
|
| 116 |
+
query_lower = query.lower()
|
| 117 |
+
results = []
|
| 118 |
+
|
| 119 |
+
for filename, content in self.files.items():
|
| 120 |
+
lines = content.split('\n')
|
| 121 |
+
for i, line in enumerate(lines, 1):
|
| 122 |
+
if query_lower in line.lower():
|
| 123 |
+
# Mostra contexto
|
| 124 |
+
snippet = line.strip()[:100]
|
| 125 |
+
results.append(f" {filename}:{i} → {snippet}")
|
| 126 |
+
|
| 127 |
+
if not results:
|
| 128 |
+
return f"🔍 Nenhum resultado para '{query}'."
|
| 129 |
+
|
| 130 |
+
return f"🔍 Resultados para '{query}' ({len(results)} encontrados):\n" + "\n".join(results[:20])
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
def parse_tool_calls(response: Dict) -> List[Dict]:
|
| 134 |
+
"""Extrai tool calls da resposta do LLM."""
|
| 135 |
+
choices = response.get("choices", [])
|
| 136 |
+
if not choices:
|
| 137 |
+
return []
|
| 138 |
+
|
| 139 |
+
message = choices[0].get("message", {})
|
| 140 |
+
tool_calls = message.get("tool_calls", [])
|
| 141 |
+
|
| 142 |
+
parsed = []
|
| 143 |
+
for tc in tool_calls:
|
| 144 |
+
try:
|
| 145 |
+
import json
|
| 146 |
+
parsed.append({
|
| 147 |
+
"id": tc.get("id"),
|
| 148 |
+
"name": tc.get("function", {}).get("name"),
|
| 149 |
+
"args": json.loads(tc.get("function", {}).get("arguments", "{}"))
|
| 150 |
+
})
|
| 151 |
+
except:
|
| 152 |
+
pass
|
| 153 |
+
|
| 154 |
+
return parsed
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
def get_response_content(response: Dict) -> Optional[str]:
|
| 158 |
+
"""Extrai o conteúdo de texto da resposta."""
|
| 159 |
+
choices = response.get("choices", [])
|
| 160 |
+
if not choices:
|
| 161 |
+
return None
|
| 162 |
+
|
| 163 |
+
message = choices[0].get("message", {})
|
| 164 |
+
return message.get("content")
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
def has_tool_calls(response: Dict) -> bool:
|
| 168 |
+
"""Verifica se a resposta contém tool calls."""
|
| 169 |
+
choices = response.get("choices", [])
|
| 170 |
+
if not choices:
|
| 171 |
+
return False
|
| 172 |
+
|
| 173 |
+
message = choices[0].get("message", {})
|
| 174 |
+
tool_calls = message.get("tool_calls", [])
|
| 175 |
+
return len(tool_calls) > 0
|