Madras1 commited on
Commit
43986ce
·
verified ·
1 Parent(s): f36efaa

Upload 5 files

Browse files
Files changed (4) hide show
  1. app.py +152 -90
  2. providers.py +178 -0
  3. requirements.txt +1 -0
  4. tools.py +175 -0
app.py CHANGED
@@ -1,9 +1,6 @@
1
  """
2
- Jade Code IDE - HuggingFace Space Backend
3
- BYOK (Bring Your Own Key) Architecture
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, Request, Header
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="2.0")
26
 
27
- # CORS - permitir GitHub Pages e localhost para dev
28
  app.add_middleware(
29
  CORSMiddleware,
30
- allow_origins=["*"], # Em produção, restringir para seu domínio
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
- current_file_content: Optional[str] = ""
41
- history: Optional[list] = []
 
 
 
 
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
- # ============== Helper: Get Groq Client ==============
53
- def get_groq_client(api_key: str):
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
- # ============== System Prompt ==============
60
- SYSTEM_PROMPT = """Você é CodeJade, um assistente de programação avançado.
61
- Seu objetivo é ajudar o usuário a escrever código, corrigir bugs e explicar conceitos.
 
62
 
63
- Regras:
 
64
  - Seja direto e técnico
65
- - Use código Python por padrão
66
  - Explique o que está fazendo
67
- - Se o usuário compartilhar código, analise e sugira melhorias
 
 
 
 
68
  """
69
 
70
 
@@ -72,69 +84,124 @@ Regras:
72
 
73
  @app.get("/")
74
  async def root():
75
- """Health check."""
76
- return {"status": "ok", "message": "Jade Code IDE API v2.0 - BYOK Mode"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 o agente usando a API key do usuário.
 
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
- client = get_groq_client(x_api_key)
 
89
 
90
- # Monta mensagens
91
  messages = [{"role": "system", "content": SYSTEM_PROMPT}]
92
 
93
- # Adiciona histórico se existir
94
- for msg in (req.history or [])[-10:]: # Últimas 10 mensagens
95
  messages.append(msg)
96
 
97
- # Adiciona contexto do arquivo se existir
 
 
 
 
 
 
 
98
  user_msg = req.message
99
- if req.current_file_content:
100
- user_msg = f"[Código atual do usuário]\n```\n{req.current_file_content[:3000]}\n```\n\n{req.message}"
101
 
102
  messages.append({"role": "user", "content": user_msg})
103
 
104
- # Chama o modelo
105
- response = client.chat.completions.create(
106
- messages=messages,
107
- model="llama-3.3-70b-versatile",
108
- temperature=0.3,
109
- max_tokens=2000
110
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
 
112
- return {"response": response.choices[0].message.content}
 
 
 
 
113
 
114
  except Exception as e:
 
115
  error_msg = str(e)
116
- if "authentication" in error_msg.lower() or "api key" in error_msg.lower():
117
- raise HTTPException(status_code=401, detail="API Key inválida ou expirada")
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
- client = get_groq_client(x_api_key)
199
 
200
- prompt = f"""Você é um autocomplete de código. Sugira completions para o código abaixo.
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 = client.chat.completions.create(
218
  messages=[
219
  {"role": "system", "content": "Responda APENAS JSON válido."},
220
  {"role": "user", "content": prompt}
221
  ],
222
- model="llama-3.3-70b-versatile",
223
- temperature=0.1,
224
- max_tokens=500
225
  )
226
 
227
- response_text = response.choices[0].message.content
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 Interface (for HF Spaces UI) ==============
242
- def gradio_chat(message, api_key):
243
- """Interface Gradio simples para testar."""
244
  if not api_key:
245
- return "❌ Coloque sua API Key do Groq"
246
 
247
  try:
248
- client = get_groq_client(api_key)
249
- response = client.chat.completions.create(
250
  messages=[
251
  {"role": "system", "content": SYSTEM_PROMPT},
252
  {"role": "user", "content": message}
253
  ],
254
- model="llama-3.3-70b-versatile",
255
- temperature=0.3
256
  )
257
- return response.choices[0].message.content
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("Esta é a API do Jade Code IDE. Use a IDE web para a experiência completa.")
266
- gr.Markdown("**API Docs**: `/docs`")
267
 
268
  with gr.Row():
269
- api_key_input = gr.Textbox(label="Sua API Key do Groq", type="password")
270
-
271
- with gr.Row():
272
- msg_input = gr.Textbox(label="Mensagem de teste", lines=3)
273
-
274
- with gr.Row():
275
- send_btn = gr.Button("Testar Chat")
 
 
 
276
 
 
 
 
277
  output = gr.Textbox(label="Resposta", lines=10)
278
 
279
- send_btn.click(gradio_chat, inputs=[msg_input, api_key_input], outputs=output)
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 - 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