refatoracao
This commit is contained in:
698
orchestrator.py
698
orchestrator.py
@@ -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"}
|
||||
|
||||
Reference in New Issue
Block a user