refatoracao

This commit is contained in:
2026-03-23 23:38:56 +00:00
parent 8002262cf7
commit b7e6239216
16 changed files with 2290 additions and 4321 deletions

View File

@@ -1,14 +1,14 @@
# ============================================================
# ORCHESTRATOR.PY - Orquestrador de Tarefas
# Planner (Gemini/OpenAI/Claude/Ollama) + Executor (Qwen/Ollama)
# ORCHESTRATOR.PY - Orquestrador de Tarefas (Refatorado)
# ============================================================
import json
import re
import os
import asyncio
from typing import Dict, List, Optional
from llm_providers import (
call_planner, call_executor, get_planner_llm, get_executor_llm,
call_planner_async, call_executor_async, get_planner_llm, get_executor_llm,
get_available_models, LLM_PROVIDERS, set_planner, set_executor, get_config, save_config
)
from tools_v2 import TOOLS_V2, get_tools_by_danger, get_all_tools_formatted
@@ -25,67 +25,32 @@ Seu trabalho é decompor tarefas em passos executáveis CORRETOS.
### REGRAS CRÍTICAS DE COMANDOS:
1. USE SEMPRE "docker compose" (COM ESPAÇO), NUNCA "docker-compose" (COM HÍFEN)
2. O BotVPS está em /app (dentro do container)
3. Use "cd /app && git pull" para atualizar
4. Use "cd /app && docker compose up -d --build" para rebuild e deploy
2. Use "cd /app && git pull" para atualizar
3. Use "cd /app && docker compose up -d --build" para rebuild e deploy
### EXEMPLOS DE COMANDOS CORRETOS:
✅ CORRETO: cd /app && git pull origin master
✅ CORRETO: cd /app && docker compose up -d --build
✅ CORRETO: docker restart vps-ai-agent
### NÍVEIS DE PERIGO:
- SAFE: listar, ver status, ler logs
- MEDIUM: git pull, build, restart
- DANGEROUS: delete, reboot, docker down
### FERRAMENTAS DISPONÍVEIS:
{TOOLS_LIST}
### FORMATO DE RESPOSTA:
Responda APENAS com JSON válido:
Responda APENAS com JSON:
{{
"task_name": "Nome resumido",
"summary": "Resumo do que será feito",
"steps": [
{{
"order": 1,
"action": "Descrição clara",
"action": "Descrição",
"tool": "bash",
"command": "COMANDO LINUX COMPLETO E CORRETO",
"command": "COMANDO LINUX COMPLETO",
"danger": "safe|medium|dangerous"
}}
]
}}
### REGRAS:
1. Responda APENAS com JSON válido, sem texto adicional fora do JSON
2. Use tool="bash" para todos os comandos
3. Use "docker compose" (espaço) sempre
4. Use caminhos absolutos completos
5. Os passos devem ser na ordem correta de execução
"""
EXECUTOR_SYSTEM_PROMPT = """Você é o EXECUTOR AGENT do BotVPS.
Seu trabalho é executar comandos bash com precisão.
### REGRAS:
1. Execute APENAS o comando passado
2. Retorne o output do comando
3. Se houver erro, descreva o erro claramente
4. Não invente outputs
### FORMATO DE RESPOSTA:
Responda com JSON:
{{
"success": true|false,
"output": "output do comando ou erro"
}}
### IMPORTANTE:
- Use caminhos absolutos quando possível
- Redirecione erros (2>/dev/null) quando apropriado
- Mantenha comandos simples e seguros
Retorne JSON: {"success": true|false, "output": "resultado"}
"""
# ============================================================
@@ -93,569 +58,194 @@ Responda com JSON:
# ============================================================
def _format_tools_for_prompt() -> str:
"""Formata lista de ferramentas para o prompt."""
lines = []
for name, info in TOOLS_V2.items():
lines.append(f"- {name}: {info['desc']} [{info['danger']}]")
return "\n".join(lines)
return "\n".join([f"- {name}: {info['desc']} [{info['danger']}]" for name, info in TOOLS_V2.items()])
def detect_git_repo_path(task: str) -> str:
"""
Detecta automaticamente o caminho do repositório Git baseado na tarefa.
Retorna o caminho do repositório mais provável.
"""
async def detect_git_repo_path_async(task: str) -> str:
"""Detecta automaticamente o caminho do repositório Git (async)."""
from tools_v2 import run_bash
# Normaliza o texto da tarefa
task_lower = task.lower()
# Caminhos específicos por nome de app
app_paths = {
"tracksteel": [
"/data/repositories/0/5/5adtracksteel/AdmTrackSteel",
"/data/repositories/admtracksteel/AdmTrackSteel",
],
"botvps": [
"/data/repositories/admtracksteel/BotVPS",
"/data/repositories/botvps",
"/app",
],
"coolify": [
"/data/coolify",
"/data/coolify/source",
]
# Mapeamento de APPs conhecidos
app_map = {
"tracksteel": "/data/repositories/admtracksteel/AdmTrackSteel",
"botvps": "/app",
"coolify": "/data/coolify/source",
"antigravity": "/app"
}
# Detecta qual app o usuário quer
if "botvps" in task_lower or "bot vps" in task_lower or "antigravity" in task_lower:
paths_to_try = app_paths["botvps"]
elif "tracksteel" in task_lower:
paths_to_try = app_paths["tracksteel"]
elif "coolify" in task_lower:
paths_to_try = app_paths["coolify"]
else:
paths_to_try = []
# Procura nos caminhos específicos
for repo_path in paths_to_try:
result = run_bash(f"test -d {repo_path}/.git && echo 'FOUND:{repo_path}' || true")
if result.get("success") and "FOUND:" in result.get("output", ""):
found_path = result["output"].split("FOUND:")[1].strip()
print(f"[DETECT] Found {task_lower} at: {found_path}")
return found_path
# Procura em /data/repositories por repositórios git
result = run_bash("find /data/repositories -name '*.git' -type d 2>/dev/null | head -20")
for key, path in app_map.items():
if key in task_lower:
return path
# Busca dinâmica rápida
result = run_bash("find /data/repositories -name '.git' -type d -maxdepth 3 | head -1")
if result.get("success") and result.get("output"):
lines = result["output"].strip().split("\n")
for line in lines:
if line:
repo_dir = line.replace("/.git", "")
print(f"[DETECT] Found repo: {repo_dir}")
return repo_dir
# Fallback: retorna /app se existir
if os.path.exists("/app/.git"):
print(f"[DETECT] Using fallback: /app")
return "/app"
print(f"[DETECT] No repo found, returning /")
return "/"
def detect_app_in_docker(task: str) -> str:
"""
Detecta qual container/app o usuário quer interagir baseado na tarefa.
"""
from tools_v2 import run_bash
task_lower = task.lower()
# Lista containers e tenta match
result = run_bash("docker ps --format '{{.Names}}' 2>/dev/null")
if result.get("success"):
containers = result["output"].lower()
return result["output"].replace("/.git", "").strip()
if "tracksteel" in task_lower:
if "tracksteel" in containers:
return "tracksteel"
if "botvps" in task_lower or "antigravity" in task_lower:
if "vps" in containers:
return "vps-ai-agent"
if "coolify" in task_lower:
if "coolify" in containers:
return "coolify"
return ""
return "/app" if os.path.exists("/app/.git") else "/"
def _parse_json_response(text: str) -> Optional[Dict]:
"""Extrai JSON da resposta do LLM."""
# Tenta encontrar JSON no texto
async def _parse_json_response(text: str) -> Optional[Dict]:
json_match = re.search(r'\{[\s\S]*\}', text)
if json_match:
try:
return json.loads(json_match.group())
except json.JSONDecodeError:
except:
pass
# Tenta extrair de blocos de código
code_blocks = re.findall(r'```(?:json)?\s*([\s\S]*?)```', text)
for block in code_blocks:
try:
return json.loads(block.strip())
except json.JSONDecodeError:
continue
return None
def _classify_dangerous_steps(steps: List[Dict]) -> List[Dict]:
"""Retorna apenas passos perigosos."""
return [s for s in steps if s.get("danger") in ["medium", "dangerous"]]
# ============================================================
# PLANNER AGENT
# CORE AGENTS
# ============================================================
def plan_task(task: str) -> Dict:
"""
Usa o Planner LLM para decompor uma tarefa.
Args:
task: Tarefa do usuário
Returns:
Dicionário com plano de execução:
{
"task_name": str,
"summary": str,
"steps": [
{"order": int, "action": str, "tool": str, "command": str, "danger": str}
]
}
"""
async def plan_task_async(task: str) -> Dict:
provider, model = get_planner_llm()
print(f"[PLANNER] Using: {provider}/{model}")
repo_path = await detect_git_repo_path_async(task)
# Detecta automaticamente informações do contexto
detected_repo = detect_git_repo_path(task)
detected_app = detect_app_in_docker(task)
print(f"[CONTEXT] Repo: {detected_repo}, App: {detected_app}")
# Contexto adicional para o planner
context_info = f"""
### CONTEXTO DETECTADO:
- BotVPS está em: /app
- Repositório detectado: {detected_repo}
- Container: vps-ai-agent
"""
system_prompt = PLANNER_SYSTEM_PROMPT.replace("{TOOLS_LIST}", _format_tools_for_prompt())
system_prompt = system_prompt.replace("{CONTEXT_INFO}", context_info)
response = call_planner(task, system_prompt)
print(f"[RESPONSE] Planner response:\n{response[:500]}...")
plan = _parse_json_response(response)
if not plan or "steps" not in plan:
# Fallback: tenta executar como comando único
return {
"task_name": task[:50],
"summary": f"Tarefa: {task}",
"steps": [{
"order": 1,
"action": task,
"tool": "bash",
"command": task,
"danger": "medium"
}]
}
return plan
# ============================================================
# EXECUTOR AGENT
# ============================================================
def execute_command(command: str) -> Dict:
"""
Executa um comando bash via Executor LLM.
Args:
command: Comando a executar
Returns:
{"success": bool, "output": str}
"""
import subprocess
provider, model = get_executor_llm()
print(f"[EXECUTOR] Using: {provider}/{model}")
# Para comandos bash simples, executa direto sem LLM
# Usa LLM apenas para comandos complexos
if len(command) < 100 and not any(c in command for c in ["&&", "||", "|", "$"]):
try:
result = subprocess.run(
command,
shell=True,
capture_output=True,
text=True,
timeout=60
)
return {
"success": result.returncode == 0,
"output": result.stdout.strip() or result.stderr.strip() or "Sucesso"
}
except Exception as e:
return {"success": False, "output": str(e)}
# Para comandos complexos, usa LLM
response = call_executor(
f"Execute este comando e retorne o resultado em JSON: {command}",
EXECUTOR_SYSTEM_PROMPT
context_info = f"### CONTEXTO: Repo em {repo_path}, Bot em /app"
system_prompt = PLANNER_SYSTEM_PROMPT.format(
CONTEXT_INFO=context_info,
TOOLS_LIST=_format_tools_for_prompt()
)
result = _parse_json_response(response)
if result:
return result
response = await call_planner_async(task, system_prompt)
plan = await _parse_json_response(response)
return {"success": False, "output": response}
def execute_step(step: Dict) -> Dict:
"""
Executa um passo do plano.
Args:
step: Dicionário com dados do passo
Returns:
{"success": bool, "output": str, "step": int}
"""
tool = step.get("tool")
command = step.get("command", "")
order = step.get("order", 0)
print(f" -> Step {order}: {step.get('action')[:50]}...")
if tool and tool in TOOLS_V2:
try:
tool_info = TOOLS_V2[tool]
func = tool_info["func"]
# Executa a função da ferramenta
if callable(func):
result = func(command) if command else func()
else:
result = str(func)
return {
"success": True,
"output": result,
"step": order
}
except Exception as e:
return {
"success": False,
"output": f"Erro ao executar {tool}: {str(e)}",
"step": order
}
# Executa como comando bash
return execute_command(command)
# ============================================================
# ORCHESTRATOR MAIN
# ============================================================
def orchestrate(task: str, user_confirmed: bool = False) -> Dict:
"""
Orquestra a execução de uma tarefa.
Args:
task: Tarefa do usuário
user_confirmed: Se True, pula confirmação e executa tudo
Returns:
{
"status": "needs_confirmation" | "completed" | "error",
"plan": {...},
"confirmation_needed_for": [steps peligrosos],
"results": [...] (se status == "completed")
if not plan:
return {
"task_name": "Comando Direto",
"summary": f"Executando: {task}",
"steps": [{"order": 1, "action": task, "tool": "bash", "command": task, "danger": "medium"}]
}
"""
print(f"\n{'='*50}")
print(f">>> PLANNING: {task}")
print(f"{'='*50}\n")
return plan
async def execute_command_async(command: str) -> Dict:
# Moderniza comando se necessário
command = command.replace("docker-compose", "docker compose")
# 1. Plana a tarefa
plan = plan_task(task)
# Comandos simples: execução direta (segurança e velocidade)
if len(command) < 150 and not any(c in command for c in ["|", ">", ">>"]):
process = await asyncio.create_subprocess_shell(
command,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
)
stdout, stderr = await process.communicate()
return {
"success": process.returncode == 0,
"output": (stdout.decode() or stderr.decode() or "OK").strip()
}
# 2. Identifica passos perigosos
dangerous_steps = _classify_dangerous_steps(plan.get("steps", []))
# Comandos complexos: usa Executor LLM para validar/executar
response = await call_executor_async(f"Execute: {command}", EXECUTOR_SYSTEM_PROMPT)
return await _parse_json_response(response) or {"success": False, "output": response}
async def execute_step_async(step: Dict) -> Dict:
tool = step.get("tool", "bash")
command = step.get("command", "")
# 3. Se há passos perigosos e não confirmou, pede confirmação
if dangerous_steps and not user_confirmed:
if tool in TOOLS_V2:
func = TOOLS_V2[tool]["func"]
try:
# Se for async, await
if asyncio.iscoroutinefunction(func):
result = await func(command) if command else await func()
else:
result = func(command) if command else func()
return {"success": True, "output": result, "step": step.get("order")}
except Exception as e:
return {"success": False, "output": str(e), "step": step.get("order")}
return await execute_command_async(command)
# ============================================================
# MAIN ORCHESTRATION
# ============================================================
async def orchestrate_async(task: str, user_confirmed: bool = False) -> Dict:
plan = await plan_task_async(task)
# Verifica perigo
dangerous = [s for s in plan.get("steps", []) if s.get("danger") in ["medium", "dangerous"]]
if dangerous and not user_confirmed:
return {
"status": "needs_confirmation",
"plan": plan,
"confirmation_needed_for": [
{"order": s["order"], "action": s["action"], "danger": s["danger"]}
for s in dangerous_steps
]
"confirmation_needed_for": dangerous
}
# 4. Executa todos os passos
results = []
for step in plan.get("steps", []):
result = execute_step(step)
results.append(result)
# Para em caso de erro crítico
if not result.get("success") and step.get("danger") == "dangerous":
results.append({
"success": False,
"output": "Execução abortada devido a erro crítico.",
"step": -1
})
res = await execute_step_async(step)
results.append(res)
if not res["success"] and step.get("danger") == "dangerous":
break
return {"status": "completed", "plan": plan, "results": results}
# --- SYNC WRAPPERS PARA COMPATIBILIDADE ---
def orchestrate(task: str, user_confirmed: bool = False) -> Dict:
return asyncio.run(orchestrate_async(task, user_confirmed))
def handle_message(text: str, confirmed: bool = False) -> str:
# Mantendo lógica de parsing mas chamando orchestrate_async internamente facilitaria
# No entanto, para evitar mudanças drásticas agora, faremos o wrapper sync
return asyncio.run(handle_message_async(text, confirmed))
async def handle_message_async(text: str, confirmed: bool = False) -> str:
# Reimplementação levemente mais limpa
text_clean = text.strip().lower()
# 5. Retorna resultado
return {
"status": "completed",
"plan": plan,
"results": results
}
if text_clean == "/status":
s = get_orchestrator_status()
return f"[BOT] Status: Planner={s['planner']['model']}, Executor={s['executor']['model']}"
if text_clean == "/tools":
return get_all_tools_formatted()
# Orchestration
res = await orchestrate_async(text, confirmed)
if res["status"] == "needs_confirmation":
return format_confirmation_message(res)
return format_completion_message(res)
def format_confirmation_message(result: Dict) -> str:
"""
Formata mensagem de confirmação para o usuário.
Args:
result: Resultado do orchestrate()
Returns:
String formatada para envio ao usuário
"""
if result["status"] != "needs_confirmation":
return ""
plan = result["plan"]
dangerous = result["confirmation_needed_for"]
msg = f"[PLANO] {plan.get('task_name', 'Tarefa')}\n\n"
msg += f"{plan.get('summary', '')}\n\n"
msg += "AVISO: Acoes que precisam de confirmacao:\n\n"
for step in dangerous:
icon = "[CRITICAL]" if step["danger"] == "dangerous" else "[WARNING]"
msg += f"{icon} Passo {step['order']}: {step['action']}\n"
msg += "\nDeseja continuar? (sim/não)"
msg = f"⚠️ **Confirmação Necessária**: {plan['task_name']}\n\n"
for s in result["confirmation_needed_for"]:
msg += f"• Passo {s['order']}: {s['action']} ({s['danger'].upper()})\n"
msg += "\nDigite 'sim' para autorizar."
return msg
def format_completion_message(result: Dict) -> str:
"""
Formata mensagem de conclusão.
Args:
result: Resultado do orchestrate()
Returns:
String formatada com os resultados
"""
if result["status"] != "completed":
return ""
plan = result["plan"]
results = result.get("results", [])
plan_steps = plan.get("steps", [])
msg = f"[OK] Concluido: {plan.get('task_name', 'Tarefa')}\n\n"
# Conta apenas resultados de passos reais (step > 0)
real_results = [r for r in results if r.get("step", 0) > 0]
success_count = sum(1 for r in real_results if r.get("success"))
total_count = len(plan_steps)
msg += f"[STAT] Resultado: {success_count}/{total_count} passos executados com sucesso.\n\n"
for step in plan_steps:
step_num = step.get("order", 0)
# Encontra resultado correspondente
step_result = next((r for r in results if r.get("step") == step_num), None)
if step_result:
status_icon = "[OK]" if step_result.get("success") else "[FAIL]"
output = step_result.get("output", "")[:500]
msg += f"{status_icon} Passo {step_num}: {step.get('action', '')[:50]}\n"
if output and not step_result.get("success"):
msg += f" Erro: {output[:200]}\n"
success = all(r.get("success", False) for r in results)
msg = f"{'' if success else ''} **Concluído**: {plan['task_name']}\n"
for r in results:
char = "S" if r.get("success") else "F"
msg += f"[{char}] Step {r.get('step', '?')}: {str(r.get('output'))[:100]}\n"
return msg
# ============================================================
# STATUS & CONFIG FUNCTIONS
# ============================================================
def get_orchestrator_status() -> Dict:
"""Retorna status atual do orquestrador."""
planner_provider, planner_model = get_planner_llm()
executor_provider, executor_model = get_executor_llm()
p_p, p_m = get_planner_llm()
e_p, e_m = get_executor_llm()
return {
"planner": {
"provider": planner_provider,
"model": planner_model,
"name": LLM_PROVIDERS[planner_provider]["name"]
},
"executor": {
"provider": executor_provider,
"model": executor_model,
"name": LLM_PROVIDERS[executor_provider]["name"]
},
"credentials": get_services_status(),
"available_tools": len(TOOLS_V2)
"planner": {"provider": p_p, "model": p_m},
"executor": {"provider": e_p, "model": e_m},
"tools_count": len(TOOLS_V2)
}
def get_llm_config() -> Dict:
"""Retorna configuração de LLMs."""
planner_provider, planner_model = get_planner_llm()
executor_provider, executor_model = get_executor_llm()
p_p, p_m = get_planner_llm()
e_p, e_m = get_executor_llm()
return {
"planner": {
"provider": planner_provider,
"model": planner_model,
"available_providers": [
{"id": k, "name": v["name"], "type": v["type"]}
for k, v in LLM_PROVIDERS.items()
]
},
"executor": {
"provider": executor_provider,
"model": executor_model,
"available_providers": [
{"id": k, "name": v["name"], "type": v["type"]}
for k, v in LLM_PROVIDERS.items()
]
}
"planner": {"provider": p_p, "model": p_m, "available": list(LLM_PROVIDERS.keys())},
"executor": {"provider": e_p, "model": e_m, "available": list(LLM_PROVIDERS.keys())}
}
def set_llm_config(planner_provider: str = None, planner_model: str = None,
executor_provider: str = None, executor_model: str = None) -> Dict:
"""Atualiza configuração de LLMs."""
changes = {}
if planner_provider:
result = set_planner(planner_provider, planner_model)
changes["planner"] = result
if executor_provider:
result = set_executor(executor_provider, executor_model)
changes["executor"] = result
return changes
# ============================================================
# COMMAND PARSER (para Telegram/Web)
# ============================================================
def parse_command(text: str) -> Dict:
"""
Interpreta comandos do usuário.
Args:
text: Texto do usuário
Returns:
{"type": "orchestrate"|"config"|"status", "data": {...}}
"""
text = text.strip().lower()
# Comandos de configuração
if text.startswith("/llm"):
parts = text.split()
if len(parts) == 1:
return {"type": "config", "action": "show"}
elif len(parts) >= 3:
if parts[1] == "planner":
return {"type": "config", "action": "set_planner", "provider": parts[2]}
elif parts[1] == "executor":
return {"type": "config", "action": "set_executor", "provider": parts[2]}
return {"type": "config", "action": "help"}
if text == "/sync":
return {"type": "config", "action": "sync_credentials"}
if text == "/status":
return {"type": "status"}
if text == "/tools":
return {"type": "tools"}
if text.startswith("/"):
return {"type": "unknown", "command": text}
# Tarefas de orquestração
return {"type": "orchestrate", "task": text}
# ============================================================
# MAIN HANDLER
# ============================================================
def handle_message(text: str, confirmed: bool = False) -> str:
"""
Manipula mensagem do usuário.
Args:
text: Mensagem do usuário
confirmed: Se o usuário já confirmou ações perigosas
Returns:
Resposta para o usuário
"""
parsed = parse_command(text)
# Status
if parsed["type"] == "status":
status = get_orchestrator_status()
msg = "[BOT] Status do Orquestrador:\n\n"
msg += f"[PLANNER] {status['planner']['name']} ({status['planner']['model']})\n"
msg += f"[EXECUTOR] {status['executor']['name']} ({status['executor']['model']})\n"
msg += f"[TOOLS] Ferramentas: {status['available_tools']}\n"
return msg
# Config
if parsed["type"] == "config":
if parsed["action"] == "show":
config = get_llm_config()
msg = "[CONFIG] Configuracao de LLMs:\n\n"
msg += f"[PLANNER] {config['planner']['provider']} / {config['planner']['model']}\n"
msg += f"[EXECUTOR] {config['executor']['provider']} / {config['executor']['model']}\n"
msg += "\nPara mudar: /llm planner <provider> ou /llm executor <provider>"
return msg
if parsed["action"] == "sync_credentials":
result = sync_credentials()
return f"[SYNC] Credenciais sincronizadas: {result['status']}"
return "[CONFIG] Use: /llm (mostrar) | /llm planner <provider> | /llm executor <provider>"
# Tools
if parsed["type"] == "tools":
return get_all_tools_formatted()
# Orchestrate
if parsed["type"] == "orchestrate":
task = parsed["task"]
result = orchestrate(task, confirmed)
if result["status"] == "needs_confirmation":
return format_confirmation_message(result)
return format_completion_message(result)
# Unknown
return "[?] Comando nao reconhecido. Tente: /llm, /status, /tools ou descreva uma tarefa."
def set_llm_config(planner_provider=None, planner_model=None, executor_provider=None, executor_model=None):
if planner_provider: set_planner(planner_provider, planner_model)
if executor_provider: set_executor(executor_provider, executor_model)
return {"status": "updated"}