feat: switch agent executor to a zero-dependency Gemini 3 Flash multi-turn tool engine
This commit is contained in:
+198
-89
@@ -1,6 +1,11 @@
|
||||
import os, json, redis, subprocess, base64
|
||||
from urllib import request, parse
|
||||
from anthropic import Anthropic
|
||||
import os
|
||||
import json
|
||||
import redis
|
||||
import subprocess
|
||||
import base64
|
||||
import random
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
|
||||
class SwarmAgent:
|
||||
def __init__(self, task_data):
|
||||
@@ -9,46 +14,34 @@ class SwarmAgent:
|
||||
self.repo_url = f"http://10.99.0.3:3000/{self.repo_name}.git"
|
||||
self.local_path = os.path.join("/home/deploy/projetos", self.repo_name.split('/')[-1])
|
||||
|
||||
# Conexão Redis para pegar chaves
|
||||
# Conexão Redis para pegar chaves do pool
|
||||
self.r = redis.Redis(host='10.99.0.3', port=6379, password='clube67_dragonfly_pass_9903', db=0, decode_responses=True)
|
||||
|
||||
def get_api_key(self):
|
||||
def get_api_key(self, provider):
|
||||
pool = json.loads(self.r.get('swarm:keys:pool'))
|
||||
return random.choice([k['key'] for k in pool['anthropic'] if k['status'] == 'active'])
|
||||
active_keys = [k['key'] for k in pool.get(provider, []) if k.get('status') == 'active']
|
||||
if not active_keys:
|
||||
raise Exception(f"Nenhuma chave ativa encontrada para o provedor: {provider}")
|
||||
return random.choice(active_keys)
|
||||
|
||||
def setup_workspace(self):
|
||||
os.makedirs("/home/deploy/projetos", exist_ok=True)
|
||||
if not os.path.exists(self.local_path):
|
||||
print(f" [>] Clonando repositório {self.repo_url} em {self.local_path}...")
|
||||
subprocess.run(["git", "clone", self.repo_url, self.local_path])
|
||||
else:
|
||||
print(f" [>] Atualizando repositório em {self.local_path}...")
|
||||
subprocess.run(["git", "-C", self.local_path, "pull"])
|
||||
|
||||
def run_bash(self, command):
|
||||
"""Ferramenta para a IA rodar comandos na VPS 4"""
|
||||
result = subprocess.run(command, shell=True, capture_output=True, text=True, cwd=self.local_path)
|
||||
return f"STDOUT: {result.stdout}\nSTDERR: {result.stderr}"
|
||||
|
||||
def post_comment(self, message):
|
||||
issue_id = self.data.get('data', {}).get('issue', {}).get('number')
|
||||
if not issue_id: return
|
||||
url = f"http://10.99.0.3:3000/api/v1/repos/{self.repo_name}/issues/{issue_id}/comments"
|
||||
auth_str = 'ruicesar:h$tg@g5aga$ra1E3$C-yHW$-BA@DF2@Grfa!3#'
|
||||
base64_auth = base64.b64encode(auth_str.encode('ascii')).decode('ascii')
|
||||
req = request.Request(url, data=json.dumps({"body": message}).encode('utf-8'), method='POST')
|
||||
req.add_header('Content-Type', 'application/json')
|
||||
req.add_header('Authorization', f'Basic {base64_auth}')
|
||||
try:
|
||||
with request.urlopen(req) as res: pass
|
||||
except: pass
|
||||
|
||||
def run_command(self, command):
|
||||
"""Executa comandos shell na VPS 4"""
|
||||
print(f" [Exec] Rodando: {command}")
|
||||
print(f" [Exec] Rodando comando: {command}")
|
||||
result = subprocess.run(command, shell=True, capture_output=True, text=True, cwd=self.local_path)
|
||||
return {"stdout": result.stdout, "stderr": result.stderr}
|
||||
|
||||
def write_file(self, path, content):
|
||||
"""Escreve/Edita arquivos no projeto"""
|
||||
print(f" [Exec] Escrevendo arquivo: {path}")
|
||||
full_path = os.path.join(self.local_path, path)
|
||||
os.makedirs(os.path.dirname(full_path), exist_ok=True)
|
||||
with open(full_path, "w") as f:
|
||||
@@ -57,11 +50,91 @@ class SwarmAgent:
|
||||
|
||||
def read_file(self, path):
|
||||
"""Lê arquivos do projeto"""
|
||||
print(f" [Exec] Lendo arquivo: {path}")
|
||||
full_path = os.path.join(self.local_path, path)
|
||||
if os.path.exists(full_path):
|
||||
with open(full_path, "r") as f:
|
||||
return f.read()
|
||||
return "Erro: Arquivo não encontrado."
|
||||
return f"Erro: Arquivo {path} não encontrado."
|
||||
|
||||
def post_comment(self, message):
|
||||
"""Posta um comentário de feedback na Issue do Gitea"""
|
||||
issue_id = self.data.get('data', {}).get('issue', {}).get('number')
|
||||
if not issue_id:
|
||||
return
|
||||
url = f"http://10.99.0.3:3000/api/v1/repos/{self.repo_name}/issues/{issue_id}/comments"
|
||||
|
||||
# Auth Basic
|
||||
auth_str = 'ruicesar:h$tg@g5aga$ra1E3$C-yHW$-BA@DF2@Grfa!3#'
|
||||
base64_auth = base64.b64encode(auth_str.encode('ascii')).decode('ascii')
|
||||
|
||||
data = json.dumps({"body": message}).encode('utf-8')
|
||||
req = urllib.request.Request(url, data=data, method='POST')
|
||||
req.add_header('Content-Type', 'application/json')
|
||||
req.add_header('Authorization', f'Basic {base64_auth}')
|
||||
|
||||
try:
|
||||
with urllib.request.urlopen(req) as response:
|
||||
if response.status == 201:
|
||||
print(f" [✓] Comentário postado na Issue #{issue_id}")
|
||||
except Exception as e:
|
||||
print(f" [E] Falha ao postar comentário no Gitea: {e}")
|
||||
|
||||
def call_gemini(self, contents, system_instruction, api_key):
|
||||
"""Chama a API do Gemini com ferramentas ativas em formato puramente nativo"""
|
||||
url = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-flash-latest:generateContent?key={api_key}"
|
||||
headers = {'content-type': 'application/json'}
|
||||
|
||||
tools = [{
|
||||
"functionDeclarations": [
|
||||
{
|
||||
"name": "read_file",
|
||||
"description": "Lê o conteúdo de um arquivo do projeto para análise técnica.",
|
||||
"parameters": {
|
||||
"type": "OBJECT",
|
||||
"properties": {
|
||||
"path": {"type": "STRING", "description": "Caminho relativo do arquivo no repositório."}
|
||||
},
|
||||
"required": ["path"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "write_file",
|
||||
"description": "Cria ou edita um arquivo com o novo conteúdo fornecido.",
|
||||
"parameters": {
|
||||
"type": "OBJECT",
|
||||
"properties": {
|
||||
"path": {"type": "STRING", "description": "Caminho relativo do arquivo no repositório."},
|
||||
"content": {"type": "STRING", "description": "Conteúdo completo a ser gravado no arquivo."}
|
||||
},
|
||||
"required": ["path", "content"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "run_command",
|
||||
"description": "Executa um comando de terminal (como NPM, Docker, Git, etc) no diretório do projeto.",
|
||||
"parameters": {
|
||||
"type": "OBJECT",
|
||||
"properties": {
|
||||
"command": {"type": "STRING", "description": "Comando shell a ser executado."}
|
||||
},
|
||||
"required": ["command"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}]
|
||||
|
||||
payload = {
|
||||
"contents": contents,
|
||||
"systemInstruction": {
|
||||
"parts": [{"text": system_instruction}]
|
||||
},
|
||||
"tools": tools
|
||||
}
|
||||
|
||||
req = urllib.request.Request(url, data=json.dumps(payload).encode('utf-8'), headers=headers, method='POST')
|
||||
with urllib.request.urlopen(req) as res:
|
||||
return json.loads(res.read().decode('utf-8'))
|
||||
|
||||
def execute(self):
|
||||
self.setup_workspace()
|
||||
@@ -70,80 +143,116 @@ class SwarmAgent:
|
||||
vps_ip = subprocess.getoutput("hostname -I").split()[0]
|
||||
vps_name = subprocess.getoutput("hostname")
|
||||
|
||||
with open("/home/deploy/instrucoes/global/doutrina_agentes.md", "r") as f: doutrina = f.read()
|
||||
# Carrega a Doutrina de Agentes
|
||||
doutrina_path = "/home/deploy/instrucoes/global/doutrina_agentes.md"
|
||||
doutrina = ""
|
||||
if os.path.exists(doutrina_path):
|
||||
with open(doutrina_path, "r") as f:
|
||||
doutrina = f.read()
|
||||
|
||||
# Consciência Situacional Injetada
|
||||
situational_awareness = f"\n\n[CONSCIÊNCIA SITUACIONAL]: Você está rodando na VPS: {vps_name} (IP: {vps_ip})\n"
|
||||
system_instruction = doutrina + situational_awareness
|
||||
|
||||
api_key = self.get_api_key()
|
||||
client = Anthropic(api_key=api_key)
|
||||
task = self.data.get('data', {}).get('issue', {}).get('body', 'Corrigir projeto')
|
||||
print(f" [!] Agente iniciando na {vps_name} ({vps_ip}) para resolver a tarefa.")
|
||||
|
||||
print(f" [!] Agente iniciando na {vps_name} ({vps_ip})...")
|
||||
|
||||
task = self.data.get('data', {}).get('issue', {}).get('body', 'Sem descrição')
|
||||
|
||||
# O Agente agora tem um loop de pensamento e ferramentas
|
||||
# Para este MVP, vamos permitir que ele faça uma análise e execute uma rodada de ferramentas
|
||||
tools = [
|
||||
# Histórico inicial
|
||||
contents = [
|
||||
{
|
||||
"name": "read_file",
|
||||
"description": "Lê o conteúdo de um arquivo do projeto",
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"properties": {"path": {"type": "string"}},
|
||||
"required": ["path"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "write_file",
|
||||
"description": "Escreve ou edita um arquivo no projeto",
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {"type": "string"},
|
||||
"content": {"type": "string"}
|
||||
},
|
||||
"required": ["path", "content"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "run_command",
|
||||
"description": "Executa um comando shell (ex: docker compose up)",
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"properties": {"command": {"type": "string"}},
|
||||
"required": ["command"]
|
||||
}
|
||||
"role": "user",
|
||||
"parts": [{"text": f"Tarefa: {task}. Diretório do projeto: {self.local_path}. Analise o código e corrija a regressão aplicando as mudanças necessárias."}]
|
||||
}
|
||||
]
|
||||
|
||||
response = client.messages.create(
|
||||
model="claude-3-5-sonnet-20240620",
|
||||
max_tokens=4000,
|
||||
system=doutrina + situational_awareness,
|
||||
tools=tools,
|
||||
messages=[{"role": "user", "content": f"Tarefa: {task}\nAnalise o diretório {self.local_path} e resolva o problema."}]
|
||||
|
||||
api_key = self.get_api_key('google')
|
||||
feedback_steps = []
|
||||
final_text = ""
|
||||
|
||||
# Loop de multi-turn de ferramentas (máximo 7 iterações)
|
||||
for turn in range(7):
|
||||
print(f" [>] Iniciando turno {turn + 1} com o cérebro Gemini...")
|
||||
try:
|
||||
response = self.call_gemini(contents, system_instruction, api_key)
|
||||
except Exception as e:
|
||||
print(f" [E] Erro ao chamar a API do Gemini: {e}")
|
||||
feedback_steps.append(f"* **Erro na API**: {e}")
|
||||
break
|
||||
|
||||
candidates = response.get('candidates', [])
|
||||
if not candidates:
|
||||
print(" [E] Nenhuma resposta retornada.")
|
||||
break
|
||||
|
||||
content = candidates[0].get('content', {})
|
||||
parts = content.get('parts', [])
|
||||
|
||||
# Mantém no histórico a resposta do modelo
|
||||
contents.append(content)
|
||||
|
||||
has_tool_call = False
|
||||
function_responses = []
|
||||
|
||||
for part in parts:
|
||||
if 'text' in part:
|
||||
final_text += part['text'] + "\n"
|
||||
|
||||
if 'functionCall' in part:
|
||||
has_tool_call = True
|
||||
func_call = part['functionCall']
|
||||
func_name = func_call.get('name')
|
||||
args = func_call.get('args', {})
|
||||
|
||||
print(f" [!] Executando ferramenta: {func_name} (args: {args})")
|
||||
|
||||
result_data = None
|
||||
try:
|
||||
if func_name == "read_file":
|
||||
path = args.get('path')
|
||||
result_data = self.read_file(path)
|
||||
feedback_steps.append(f"* **Lendo**: `{path}`")
|
||||
elif func_name == "write_file":
|
||||
path = args.get('path')
|
||||
content_to_write = args.get('content')
|
||||
result_data = self.write_file(path, content_to_write)
|
||||
feedback_steps.append(f"* **Escrevendo**: `{path}`")
|
||||
elif func_name == "run_command":
|
||||
cmd = args.get('command')
|
||||
result_data = self.run_command(cmd)
|
||||
feedback_steps.append(f"* **Rodando**: `{cmd}`")
|
||||
except Exception as e_func:
|
||||
print(f" [E] Erro ao rodar ferramenta {func_name}: {e_func}")
|
||||
result_data = f"Erro de execução da ferramenta: {e_func}"
|
||||
|
||||
function_responses.append({
|
||||
"functionResponse": {
|
||||
"name": func_name,
|
||||
"response": {
|
||||
"output": result_data
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if not has_tool_call:
|
||||
print(" [🏁] Ciclo finalizado pelo modelo de IA.")
|
||||
break
|
||||
else:
|
||||
contents.append({
|
||||
"role": "function",
|
||||
"parts": function_responses
|
||||
})
|
||||
|
||||
# Formata o feedback e posta de volta no Gitea
|
||||
steps_str = "\n".join(feedback_steps)
|
||||
final_comment = (
|
||||
f"### 🤖 [VPS 4 - EXECUTOR SWARM]:\n\n"
|
||||
f"{final_text}\n\n"
|
||||
f"**⚙️ Ações e Diagnóstico de Infraestrutura:**\n{steps_str}"
|
||||
)
|
||||
|
||||
# Processamento simples de ferramentas (para este estágio)
|
||||
final_feedback = f"### 🤖 [VPS 4 - EXECUTOR]:\n\n{response.content[0].text}\n\n"
|
||||
|
||||
for content in response.content:
|
||||
if content.type == "tool_use":
|
||||
if content.name == "read_file":
|
||||
res = self.read_file(content.input["path"])
|
||||
final_feedback += f"* **Lendo**: {content.input['path']}\n"
|
||||
elif content.name == "write_file":
|
||||
res = self.write_file(content.input["path"], content.input["content"])
|
||||
final_feedback += f"* **Escrevendo**: {content.input['path']}\n"
|
||||
elif content.name == "run_command":
|
||||
res = self.run_command(content.input["command"])
|
||||
final_feedback += f"* **Rodando**: `{content.input['command']}`\n"
|
||||
|
||||
self.post_comment(final_feedback)
|
||||
self.post_comment(final_comment)
|
||||
|
||||
if __name__ == "__main__":
|
||||
import argparse, random
|
||||
import argparse
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--data")
|
||||
args = parser.parse_args()
|
||||
|
||||
Reference in New Issue
Block a user