- Add detect_git_repo_path() function to find repos in /data/repositories - Add detect_app_in_docker() to find containers by name - Planner now receives detected context automatically - Supports BotVPS, TrackSteel, and any repo in /data/repositories - Uses 'docker compose' (space) syntax Example: 'faz deploy do tracksteel' will detect TrackSteel repo path automatically
643 lines
20 KiB
Python
643 lines
20 KiB
Python
# ============================================================
|
|
# ORCHESTRATOR.PY - Orquestrador de Tarefas
|
|
# Planner (Gemini/OpenAI/Claude/Ollama) + Executor (Qwen/Ollama)
|
|
# ============================================================
|
|
|
|
import json
|
|
import re
|
|
import os
|
|
from typing import Dict, List, Optional
|
|
from llm_providers import (
|
|
call_planner, call_executor, 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
|
|
from credential_manager import sync_credentials, get_services_status
|
|
|
|
# ============================================================
|
|
# SYSTEM PROMPTS
|
|
# ============================================================
|
|
|
|
PLANNER_SYSTEM_PROMPT = """Você é o PLANNER AGENT do BotVPS.
|
|
Seu trabalho é decompor tarefas em passos executáveis CORRETOS.
|
|
|
|
### REPOSITORIOS CONHECIDOS:
|
|
- BotVPS: /data/applications/bw1erd9ww5121i1fsh420bcj
|
|
- TrackSteel: /data/repositories/0/5/5adtracksteel/AdmTrackSteel
|
|
|
|
{CONTEXT_INFO}
|
|
|
|
### REGRAS CRÍTICAS DE COMANDOS:
|
|
1. USE SEMPRE "docker compose" (COM ESPAÇO), NUNCA "docker-compose" (COM HÍFEN)
|
|
2. Para git, use o caminho ABSOLUTO completo do repositório
|
|
3. Para docker compose, use "cd /caminho && docker compose up -d"
|
|
4. Se não souber o caminho, use: find /data/repositories -name '*.git' -type d
|
|
|
|
### EXEMPLOS DE COMANDOS CORRETOS:
|
|
✅ CORRETO: cd /repo/path && git pull origin master
|
|
✅ CORRETO: cd /repo/path && docker compose up -d --build
|
|
✅ CORRETO: docker restart nome-do-container
|
|
|
|
### 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:
|
|
{{
|
|
"task_name": "Nome resumido",
|
|
"summary": "Resumo do que será feito",
|
|
"steps": [
|
|
{{
|
|
"order": 1,
|
|
"action": "Descrição clara",
|
|
"tool": "bash",
|
|
"command": "COMANDO LINUX COMPLETO E CORRETO",
|
|
"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
|
|
"""
|
|
|
|
# ============================================================
|
|
# HELPER FUNCTIONS
|
|
# ============================================================
|
|
|
|
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)
|
|
|
|
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.
|
|
"""
|
|
from tools_v2 import run_bash
|
|
|
|
# Normaliza o texto da tarefa
|
|
task_lower = task.lower()
|
|
|
|
# Lista de repositórios conhecidos no host
|
|
known_repos = [
|
|
"/data/repositories/0/5/5adtracksteel/AdmTrackSteel",
|
|
"/data/repositories/0/5/5adtracksteel/BotVPS",
|
|
"/data/applications/bw1erd9ww5121i1fsh420bcj",
|
|
"/data/coolify",
|
|
"/root",
|
|
"/app"
|
|
]
|
|
|
|
# Tenta detectar pelo nome mencionado na tarefa
|
|
if "tracksteel" in task_lower or "tracksteel" in task_lower:
|
|
return "/data/repositories/0/5/5adtracksteel/AdmTrackSteel"
|
|
if "botvps" in task_lower or "bot vps" in task_lower:
|
|
return "/data/applications/bw1erd9ww5121i1fsh420bcj"
|
|
if "coolify" in task_lower:
|
|
return "/data/coolify"
|
|
|
|
# Tenta encontrar repositório git válido
|
|
for repo_path in known_repos:
|
|
result = run_bash(f"test -d {repo_path}/.git && echo 'FOUND:{repo_path}' || true")
|
|
if result.get("success") and "FOUND:" in result.get("output", ""):
|
|
return result["output"].split("FOUND:")[1].strip()
|
|
|
|
# Procura em /data/repositories por repositórios git
|
|
result = run_bash("find /data/repositories -name '*.git' -type d 2>/dev/null | head -10")
|
|
if result.get("success") and result.get("output"):
|
|
# Retorna o primeiro repositório encontrado
|
|
first_repo = result["output"].split("\n")[0].replace("/.git", "")
|
|
return first_repo
|
|
|
|
# Fallback: retorna /app se existir
|
|
if os.path.exists("/app/.git"):
|
|
return "/app"
|
|
|
|
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()
|
|
|
|
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 ""
|
|
|
|
def _parse_json_response(text: str) -> Optional[Dict]:
|
|
"""Extrai JSON da resposta do LLM."""
|
|
# Tenta encontrar JSON no texto
|
|
json_match = re.search(r'\{[\s\S]*\}', text)
|
|
if json_match:
|
|
try:
|
|
return json.loads(json_match.group())
|
|
except json.JSONDecodeError:
|
|
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
|
|
# ============================================================
|
|
|
|
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}
|
|
]
|
|
}
|
|
"""
|
|
provider, model = get_planner_llm()
|
|
print(f"[PLANNER] Using: {provider}/{model}")
|
|
|
|
# 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:
|
|
- Repositório mais provável: {detected_repo}
|
|
- Aplicação mais provável: {detected_app}
|
|
- Para descobrir o repositório correto, use: find /data/repositories -name '*.git' -type d 2>/dev/null
|
|
"""
|
|
|
|
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
|
|
)
|
|
|
|
result = _parse_json_response(response)
|
|
if result:
|
|
return result
|
|
|
|
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")
|
|
}
|
|
"""
|
|
print(f"\n{'='*50}")
|
|
print(f">>> PLANNING: {task}")
|
|
print(f"{'='*50}\n")
|
|
|
|
# 1. Plana a tarefa
|
|
plan = plan_task(task)
|
|
|
|
# 2. Identifica passos perigosos
|
|
dangerous_steps = _classify_dangerous_steps(plan.get("steps", []))
|
|
|
|
# 3. Se há passos perigosos e não confirmou, pede confirmação
|
|
if dangerous_steps 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
|
|
]
|
|
}
|
|
|
|
# 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
|
|
})
|
|
break
|
|
|
|
# 5. Retorna resultado
|
|
return {
|
|
"status": "completed",
|
|
"plan": plan,
|
|
"results": results
|
|
}
|
|
|
|
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)"
|
|
|
|
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", [])
|
|
|
|
msg = f"[OK] Concluido: {plan.get('task_name', 'Tarefa')}\n\n"
|
|
|
|
success_count = sum(1 for r in results if r.get("success"))
|
|
total_count = len([r for r in results if r.get("step", 0) > 0])
|
|
|
|
msg += f"[STAT] Resultado: {success_count}/{total_count} passos executados com sucesso.\n\n"
|
|
|
|
for r in results:
|
|
step_num = r.get("step", 0)
|
|
if step_num > 0:
|
|
status = "[OK]" if r.get("success") else "[FAIL]"
|
|
output = r.get("output", "")[:500]
|
|
msg += f"{status} Passo {step_num}:\n```\n{output}\n```\n\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()
|
|
|
|
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)
|
|
}
|
|
|
|
def get_llm_config() -> Dict:
|
|
"""Retorna configuração de LLMs."""
|
|
planner_provider, planner_model = get_planner_llm()
|
|
executor_provider, executor_model = 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()
|
|
]
|
|
}
|
|
}
|
|
|
|
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."
|