diff --git a/templates/agent_executor.py b/templates/agent_executor.py index ef78115..1f0b894 100644 --- a/templates/agent_executor.py +++ b/templates/agent_executor.py @@ -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()