From 5e8acefa9abedf41a1765f2eb90b7f6849de7807 Mon Sep 17 00:00:00 2001 From: admtracksteel Date: Sat, 21 Mar 2026 19:16:10 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=80=20Initial=20deploy=20to=20Gitea=20?= =?UTF-8?q?with=20fixes=20and=20dashboard=20enhancements?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 6 + Dockerfile | 25 ++ ai_agent.py | 120 +++++++ bot_logic.py | 146 ++++++++ config.py | 27 ++ docker-compose.yml | 33 ++ main.py | 126 +++++++ requirements.txt | 15 + templates/index.html | 787 +++++++++++++++++++++++++++++++++++++++++++ tools.py | 45 +++ 10 files changed, 1330 insertions(+) create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 ai_agent.py create mode 100644 bot_logic.py create mode 100644 config.py create mode 100644 docker-compose.yml create mode 100644 main.py create mode 100644 requirements.txt create mode 100644 templates/index.html create mode 100644 tools.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..75d5ce4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +__pycache__/ +*.pyc +.env +data/ +venv/ +.DS_Store diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..54e6c60 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,25 @@ +FROM python:3.11-slim + +# Instala dependências do sistema necessárias para áudio (ffmpeg e ALSA) e para o psutil compilar +RUN apt-get update && apt-get install -y \ + ffmpeg \ + gcc \ + python3-dev \ + docker.io \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +# Expõe a porta do FastAPI +EXPOSE 8000 + +# Script de inicialização (Roda a API web no background e o Telegram Bot polling) +RUN echo '#!/bin/bash\nuvicorn main:app --host 0.0.0.0 --port 8000 & \npython bot_logic.py\n' > start.sh +RUN chmod +x start.sh + +CMD ["./start.sh"] diff --git a/ai_agent.py b/ai_agent.py new file mode 100644 index 0000000..c92544d --- /dev/null +++ b/ai_agent.py @@ -0,0 +1,120 @@ +import os +import re +import requests +from tools import run_bash_command, get_system_health +from config import get_config + +def query_agent(prompt: str, override_provider: str = None) -> str: + """ + Função principal que roteia pro LLM desejado, detecta intents e aciona as Tools. + """ + + # SYSTEM PROMPT AVANÇADO: O "Ensinamento" da Inteligência Artificial + system_prompt = """Você é o [Antigravity VPS Agent], uma Inteligência Artificial autônoma de Dev/SysAdmin operando diretamente em uma máquina Ubuntu Linux do usuário (Marcos). +Sua missão é ajudar o Marcos a gerenciar o servidor, criar aplicações, analisar código e orquestrar o Docker. VOCÊ DEVE RESPONDER SEMPRE EM PORTUGUÊS FLUENTE DO BRASIL. + +### SEUS PODERES E ARQUITETURA +1. **Nível Root**: Você roda em um Bot Python empacotado via Docker. Este container mapeia o `/var/run/docker.sock`, o que te dá o poder DIVINO sobre tudo. Você pode listar, deletar, parar e recriar qualquer container na máquina hospedeira inteira. +2. **Sistema de Arquivos**: O disco principal do servidor está montado de forma segura, o que permite que você leia logs nativos. +3. **Mecanismo de Ação (A Interface de Ferramentas)**: Você **não executa os comandos diretamente pela web**. Você possui uma interface conectada ao bot no Telegram e Web. Para interagir com o servidor, sempre que decidir que algo precisa ser lido (ex: `ls -la`, `docker network ls`, `cat arquivo.txt`) ou modificado, responda com a notação: [CMD] o_comando_aqui [/CMD]. +4. O bot Python varrerá sua resposta via Regex, extrairá o comando que você colocou entre os blocos, abrirá um shell Bash real, e te devolverá na conversa o resultado do seu comando. + +### SEUS LIMITES E REGRAS +- **Sem Comandos Interativos**: Jamais use comandos que exijam resposta humana travando o terminal (ex: `nano`, `top -d 1`, `apt-get install` sem `-y`). Se usar, o bash vai dar timeout (60s limit). +- **Nunca use systemctl**: Você roda dentro de um container Docker. Logo, o comando `systemctl` NÃO FUNCIONA. Para ver serviços, use invariavelmente o comando `docker ps -a`. +- **Aja, Não Explique Demais**: Se o usuário te der uma tarefa como "Veja meus containers", não explique o que é Docker. Apenas devolva [CMD] docker ps -a [/CMD] e diga "Estou olhando agora mesmo." +- **Erros**: Se o comando retornar um código 127, 1 ou falha de acesso, aceite o erro e sugira tentar um comando de diagnóstico em seguida. + +Acorde, a VPS é sua para cuidar!""" + + if "logs do nginx" in prompt.lower() or "nginx" in prompt.lower(): + output = run_bash_command("systemctl status nginx || echo 'Nginx não parece ser gerenciado por systemctl aqui'") + return f"Executei a checagem no Nginx. Olha o resultado: \n\n{output}" + + if any(palavra in prompt.lower() for palavra in ["status", "cpu", "saude", "saúde", "sauda", "memória", "disco", "hd"]): + health = get_system_health() + return f"A saúde atual pontual da sua máquina direta do Python está assim:\n{health}" + + cfg = get_config() + provider = override_provider or cfg.get("active_provider", "ollama") + + if provider == "gemini": + gemini_api_key = cfg.get("gemini_api_key", "") + if not gemini_api_key: return "Chave API do Gemini não configurada." + + try: + url = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key={gemini_api_key}" + payload = { + "contents": [{"parts": [{"text": system_prompt + "\n\nUsuário: " + prompt}]}] + } + res = requests.post(url, json=payload) + if res.status_code == 200: + raw_response = res.json()["candidates"][0]["content"]["parts"][0]["text"] + else: + return f"Erro na API do Gemini: {res.text}" + + # Regex Universal para processar Comandos + match = re.search(r"\[.*?CMD\](.*?)\[/.*?CMD\]", raw_response, re.IGNORECASE | re.DOTALL) + if match: + comando_bash = match.group(1).strip() + resultado = run_bash_command(comando_bash) + + # Tradução via Gemini + traducao_prompt = f"O comando `{comando_bash}` retornou o seguinte dado: {resultado[:1500]}\nTraduza gentilmente para um formato leigo sem códigos de erro difíceis." + t_payload = {"contents": [{"parts": [{"text": system_prompt + "\n\n" + traducao_prompt}]}]} + t_res = requests.post(url, json=t_payload) + if t_res.status_code == 200: + texto_leigo = t_res.json()["candidates"][0]["content"]["parts"][0]["text"] + else: + texto_leigo = "Falha ao gerar resumo no Gemini." + + return f"🤖 **Comando Técnico (Gemini):** `{comando_bash}`\n\n**🖥️ Log Nativo:**\n```\n{resultado[:1000]}\n```\n\n🧑‍🏫 **Tradução:**\n{texto_leigo}" + + return raw_response + except Exception as e: + return f"Falha de Conexão com Gemini Pro: {e}" + + elif provider == "ollama": + try: + ollama_host = os.getenv("OLLAMA_HOST", "http://ollama-lw4s8g4gc8gss4gkc4gg0wk4:11434") + res = requests.post(f"{ollama_host}/api/generate", json={ + "model": os.getenv("OLLAMA_MODEL", "qwen2.5-coder:1.5b"), + "prompt": system_prompt + "\nUsuário: " + prompt, + "stream": False + }) + if res.status_code == 200: + raw_response = res.json().get("response", "Erro vazio do Ollama") + + # Motor de Tool Calling Tolerante: Detecta [CMD] ou [VCMD] ou variações que LLMs minúsculas inventam + match = re.search(r"\[.*?CMD\](.*?)\[/.*?CMD\]", raw_response, re.IGNORECASE | re.DOTALL) + + if match: + comando_bash = match.group(1).strip() + resultado = run_bash_command(comando_bash) + + # ---------------------------------------------------- + # NOVA LÓGICA: Tradução Leiga (Segundo Prompt para a IA) + # ---------------------------------------------------- + traducao_prompt = f"O comando `{comando_bash}` retornou a seguinte saída do servidor:\n\n{resultado[:1500]}\n\nTraduza GENTILMENTE essa saída técnica explicando de forma amigável, gentil e em português muito simples (para um não-técnico) o que isso indica. SE a saída for um 'ERRO', acalme o usuário, resuma que um comando técnico falhou e sugira verbalmente o que de forma segura investigar a seguir (não mande blocos confusos, apenas explique em português fluente)." + + try: + res_traducao = requests.post(f"{ollama_host}/api/generate", json={ + "model": os.getenv("OLLAMA_MODEL", "qwen2.5-coder:1.5b"), + "prompt": system_prompt + "\n\n" + traducao_prompt, + "stream": False + }) + if res_traducao.status_code == 200: + texto_leigo = res_traducao.json().get("response", "Erro ao processar resumo.") + else: + texto_leigo = "Falha ao gerar resumo na LLM." + except Exception as e_traducao: + texto_leigo = "Ocorreu uma falha ao tentar traduzir o log." + + # Retorna na tela a versão técnica seguida da versão explicada + return f"🤖 **Comando Técnico:** `{comando_bash}`\n\n**🖥️ Log Nativo (Terminal):**\n```\n{resultado[:1000]}\n```\n\n🧑‍🏫 **Tradução:**\n{texto_leigo}" + + return raw_response + except Exception as e: + return f"Falha ao conectar no Ollama local: {e}" + + return "Ação não reconhecida pelo Agente no momento." diff --git a/bot_logic.py b/bot_logic.py new file mode 100644 index 0000000..fd5e829 --- /dev/null +++ b/bot_logic.py @@ -0,0 +1,146 @@ +import os +import requests +import asyncio +from dotenv import load_dotenv +from telegram import Update +from telegram.ext import Application, CommandHandler, MessageHandler, filters, ContextTypes +from ai_agent import query_agent +import speech_recognition as sr +from pydub import AudioSegment +from gtts import gTTS + +load_dotenv() + +TOKEN = os.getenv("TELEGRAM_BOT_TOKEN") +ALLOWED_CHAT_ID = os.getenv("TELEGRAM_CHAT_ID") +ELEVENLABS_KEY = os.getenv("ELEVENLABS_API_KEY") +VOICE_ID = os.getenv("ELEVENLABS_VOICE_ID") + +def synthesize_audio(text: str) -> str: + """Gera áudio local/gratuito usando gTTS e retorna o arquivo.""" + try: + # Remove caracteres indesejados e emojis que atrapalham a fala + texto_limpo = text.replace("🤖", "").replace("🧑‍🏫", "").replace("*", "").replace("`", "") + + filepath = "/tmp/reply_audio.mp3" + tts = gTTS(text=texto_limpo, lang='pt-br', tld='com.br', slow=False) + tts.save(filepath) + return filepath + except Exception as e: + print(f"Erro ao gerar voz tts: {e}") + return "" + +async def auth_check(update: Update) -> bool: + """Verifica se o usuário que enviou a mensagem é o Marcos (Chat ID autorizado).""" + if str(update.message.chat_id) != ALLOWED_CHAT_ID: + await update.message.reply_text("Acesso negado. Você não tem permissão para controlar esta VPS.") + return False + return True + +async def start(update: Update, context: ContextTypes.DEFAULT_TYPE): + if not await auth_check(update): return + await update.message.reply_text("👋 Olá, Marcos! Antigravity VPS Agent online e pronto para receber comandos.") + +async def handle_text(update: Update, context: ContextTypes.DEFAULT_TYPE): + if not await auth_check(update): return + + user_msg = update.message.text + await update.message.reply_chat_action(action="typing") + + # Aciona o Agente de IA para processar o prompt e executar Tools se precisar + from config import get_config + cfg = get_config() + reply = query_agent(prompt=user_msg, override_provider=cfg.get("active_provider")) + + # Se o usuário pedir ativamente por áudio no texto + if "áudio" in user_msg.lower() or "audio" in user_msg.lower() or "voz" in user_msg.lower(): + await update.message.reply_chat_action(action="record_voice") + audio_path = synthesize_audio(reply) + if audio_path: + await update.message.reply_voice(voice=open(audio_path, 'rb')) + return + else: + reply += "\n\n*(Falha ao gerar áudio com a ElevenLabs. Serviço indisponível.)*" + + # Responde no chat normalmente + await update.message.reply_text(reply) + +async def handle_voice(update: Update, context: ContextTypes.DEFAULT_TYPE): + if not await auth_check(update): return + + await update.message.reply_chat_action(action="record_voice") + + # Baixa o aúdio do telegram + voice_file = await update.message.voice.get_file() + ogg_path = "/tmp/voice.ogg" + wav_path = "/tmp/voice.wav" + + await voice_file.download_to_drive(ogg_path) + + # Converte para WAV (Requer ffmpeg instalado na maquina) + try: + audio = AudioSegment.from_ogg(ogg_path) + audio.export(wav_path, format="wav") + except Exception as e: + await update.message.reply_text(f"Erro ao processar áudio (O ffmpeg está instalado na VPS?): {e}") + return + + # Usando SpeechRecognition nativo para transcrever (pode usar Whisper no Ollama depois) + recognizer = sr.Recognizer() + with sr.AudioFile(wav_path) as source: + audio_data = recognizer.record(source) + try: + text = recognizer.recognize_google(audio_data, language="pt-BR") + await update.message.reply_text(f"🗣️ Reconhecido: _{text}_", parse_mode="Markdown") + + # Envia o texto reconhecido para o Agente (respeitando a configuração ativa) + from config import get_config + cfg = get_config() + reply = query_agent(prompt=text, override_provider=cfg.get("active_provider")) + + # Sintetiza com ElevenLabs e responde com Áudio + audio_path = synthesize_audio(reply) + if audio_path: + await update.message.reply_voice(voice=open(audio_path, 'rb')) + else: + await update.message.reply_text(reply) + + except sr.UnknownValueError: + await update.message.reply_text("Não consegui entender o que foi dito no áudio.") + except sr.RequestError as e: + await update.message.reply_text(f"Erro no serviço de STT: {e}") + +async def llm_command(update: Update, context: ContextTypes.DEFAULT_TYPE): + if not await auth_check(update): return + args = context.args + from config import get_config, save_config + + if not args: + cfg = get_config() + await update.message.reply_text(f"Comando incompleto. Use: /llm gemini ou /llm ollama.\n*Status Atual:* {cfg.get('active_provider').upper()}") + return + + new_model = args[0].lower() + if new_model in ["gemini", "ollama"]: + cfg = get_config() + cfg["active_provider"] = new_model + save_config(cfg) + await update.message.reply_text(f"✅ Inteligência Artificial comutada com sucesso para: *{new_model.upper()}*") + else: + await update.message.reply_text("Modelos disponíveis: gemini ou ollama.") + +def get_telegram_app(): + if not TOKEN: + raise ValueError("TELEGRAM_BOT_TOKEN não encontrado no .env") + app = Application.builder().token(TOKEN).build() + app.add_handler(CommandHandler("start", start)) + app.add_handler(CommandHandler("llm", llm_command)) + app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_text)) + app.add_handler(MessageHandler(filters.VOICE, handle_voice)) + return app + +# Para testes rápidos se rodado standalone +if __name__ == "__main__": + print("Iniciando Bot Polling para testes...") + app = get_telegram_app() + app.run_polling() diff --git a/config.py b/config.py new file mode 100644 index 0000000..06cc772 --- /dev/null +++ b/config.py @@ -0,0 +1,27 @@ +import json +import os + +CONFIG_FILE = "/app/data/config.json" + +def get_config(): + if not os.path.exists("/app/data"): + os.makedirs("/app/data", exist_ok=True) + + if os.path.exists(CONFIG_FILE): + try: + with open(CONFIG_FILE, "r") as f: + return json.load(f) + except Exception: + pass + + # Configuração Padrão + return { + "active_provider": "gemini", + "gemini_api_key": "" + } + +def save_config(cfg): + if not os.path.exists("/app/data"): + os.makedirs("/app/data", exist_ok=True) + with open(CONFIG_FILE, "w") as f: + json.dump(cfg, f, indent=4) diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..3c13db2 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,33 @@ +version: '3.8' + +services: + vps-agent: + build: . + container_name: vps-ai-agent + restart: unless-stopped + expose: + - "8000" + # Monta as credenciais e o socket do docker para o Bot conseguir comandar a VPS raiz! + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + - /:/host_root:ro # Acesso em leitura à VPS para análise + - ./data:/app/data # Configs dinâmicas (API Keys, etc) + env_file: + - .env + networks: + - coolify + - ollama_net + labels: + - "traefik.enable=true" + - "traefik.http.routers.vps-agent.rule=Host(`claw.reifonas.cloud`)" + - "traefik.http.routers.vps-agent.entrypoints=https" + - "traefik.http.routers.vps-agent.tls=true" + - "traefik.http.routers.vps-agent.tls.certresolver=letsencrypt" + - "traefik.http.services.vps-agent.loadbalancer.server.port=8000" + +networks: + coolify: + external: true + ollama_net: + name: lw4s8g4gc8gss4gkc4gg0wk4 + external: true diff --git a/main.py b/main.py new file mode 100644 index 0000000..6b7fcc4 --- /dev/null +++ b/main.py @@ -0,0 +1,126 @@ +import os +import psutil +from fastapi import FastAPI, Request +from fastapi.responses import HTMLResponse, JSONResponse +from fastapi.staticfiles import StaticFiles +from fastapi.templating import Jinja2Templates +from dotenv import load_dotenv +from ai_agent import query_agent + +# Carrega as variáveis do .env +load_dotenv() + +app = FastAPI(title="VpsTelegramBot API") + +# Configura templates HTML +# Certifique-se de que a pasta 'templates' existe e tem o index.html +templates = Jinja2Templates(directory="templates") + +@app.get("/favicon.ico", include_in_schema=False) +async def favicon(): + return JSONResponse(content={"status": "ok"}) + +@app.get("/", response_class=HTMLResponse) +async def read_root(request: Request): + """Renderiza o Dashboard Web.""" + return templates.TemplateResponse("index.html", {"request": request}) + +from starlette.concurrency import run_in_threadpool + +@app.get("/api/status") +async def get_system_status(): + """Retorna o status do sistema (CPU, RAM, Disco) sem travar o loop.""" + def get_stats(): + cpu_percent = psutil.cpu_percent(interval=0.1) + vm = psutil.virtual_memory() + disk = psutil.disk_usage('/') + return { + "cpu": cpu_percent, + "ram": { + "total": round(vm.total / (1024**3), 2), + "used": round(vm.used / (1024**3), 2), + "percent": vm.percent + }, + "disk": { + "total": round(disk.total / (1024**3), 2), + "used": round(disk.used / (1024**3), 2), + "percent": disk.percent + } + } + + data = await run_in_threadpool(get_stats) + return JSONResponse(content=data) + +import subprocess +from config import get_config, save_config + +@app.get("/api/config") +async def read_configuration(): + return JSONResponse(content=get_config()) + +@app.post("/api/config") +async def update_configuration(req: dict): + save_config(req) + return JSONResponse(content={"status": "success"}) + +@app.post("/api/action") +async def execute_smart_action(action: dict): + """Executa ações predefinidas no servidor (Smart Actions da Web UI).""" + action_type = action.get("type") + + if action_type == "ping": + return JSONResponse(content={"status": "success", "message": "Pong! Servidor online e responsivo."}) + + elif action_type == "restart_bot": + # Dá um pequeno delay e depois reinicia o próprio container a partir de fora (pelo host docker) + subprocess.Popen("sleep 1 && docker restart vps-ai-agent", shell=True) + return JSONResponse(content={"status": "success", "message": "Reboot do Agente autorizado. Estará de volta em instantes!"}) + + elif action_type == "clear_cache": + # Roda um docker prune para deletar volumes perdidos e todos containers parados (limpeza profunda) + subprocess.Popen("docker system prune -af --volumes", shell=True) + return JSONResponse(content={"status": "success", "message": "Limpando caches obsoletos em background! Verifique o gráfico de disco em instantes."}) + + elif action_type == "reboot_vps": + # Hacker trick: Roda um container hiper-privilegiado descartável pra entrar no espaço do host (PID 1) e emitir comando de REBOOT físico + subprocess.Popen("sleep 2 && docker run --rm --privileged --pid=host alpine nsenter -t 1 -m -u -n -i reboot", shell=True) + return JSONResponse(content={"status": "success", "message": "🚨 O REBOOT CRÍTICO COMEÇOU. A VPS inteira desligará e religará agora."}) + + return JSONResponse(content={"status": "error", "message": "Ação desconhecida."}, status_code=400) + +@app.post("/api/chat") +async def web_chat(message: dict): + """Endpoint para interagir com a IA via Web UI.""" + user_text = message.get("text", "") + if not user_text: + return JSONResponse(content={"reply": "Por favor, digite um comando válido."}) + + # Executa a IA em uma thread separada para não travar a UI/API de status + reply = await run_in_threadpool(query_agent, prompt=user_text) + return JSONResponse(content={"reply": reply}) + +import time + +@app.get("/api/test_llm") +async def test_llm_speed(): + """Mede a velocidade de resposta da IA ativa.""" + start_time = time.time() + try: + reply = await run_in_threadpool(query_agent, prompt="responda apenas com a palavra 'pong'") + latency = round(time.time() - start_time, 2) + return JSONResponse(content={"status": "success", "latency": latency, "reply": reply}) + except Exception as e: + return JSONResponse(content={"status": "error", "message": str(e)}, status_code=500) + +@app.post("/webhook") +async def telegram_webhook(request: Request): + """Recebe as atualizações (mensagens) do Telegram.""" + update = await request.json() + # O bot_logic.py lidará com o 'update' no futuro + print("Update recebido do Telegram:", update) + return {"ok": True} + +if __name__ == "__main__": + import uvicorn + # Executa o servidor na porta 8000 acessível de qualquer lugar na rede + uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..5cc733b --- /dev/null +++ b/requirements.txt @@ -0,0 +1,15 @@ +python-telegram-bot +langchain +google-genai +ollama +SpeechRecognition +requests +python-dotenv +fastapi +uvicorn +fastapi +uvicorn +psutil +pydub +jinja2 +gTTS diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..732c3b9 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,787 @@ + + + + + + VPS AI Dashboard + + + + + + +
+
+ +
+
Online
+ +
+
+ +
+
+

CPU

+
--%
+
+
+
+
+
+

RAM

+
-- / -- GB
+
+
+
+
+
+

Disk

+
--%
+
+
+
+
+
+ +
Ações Rápidas
+
+ + + + + +
+ +
Configuração AI
+
+
+
+ + +
+
+ + +
+
+ +
+ +
Chat AI
+
+
+
+ Olá! Sou o VPS Agent. Como posso ajudar com seu servidor? +
+
+
+ + +
+
+
+ +
Ação executada!
+ + + + diff --git a/tools.py b/tools.py new file mode 100644 index 0000000..68450eb --- /dev/null +++ b/tools.py @@ -0,0 +1,45 @@ +import subprocess +import os +import psutil + +def run_bash_command(command: str) -> str: + """Executa um comando bash na VPS e retorna a saída.""" + try: + # Executa comando arriscado com root de forma segura dentro da VPS + result = subprocess.run( + command, + shell=True, + capture_output=True, + text=True, + timeout=60 # Limite de tempo de execução + ) + output = result.stdout.strip() + error = result.stderr.strip() + + if result.returncode != 0: + return f"ERRO ({result.returncode}):\n{error}" + + return output if output else "Comando executado com sucesso (sem saída)." + except subprocess.TimeoutExpired: + return "ERRO: O comando demorou muito e foi cancelado (timeout)." + except Exception as e: + return f"ERRO fatal ao rodar bash: {str(e)}" + +def get_system_health() -> str: + """Retorna um texto base rápido da saúde atual do servidor para a IA analisar.""" + cpu = psutil.cpu_percent(interval=0.1) + ram = psutil.virtual_memory().percent + disk = psutil.disk_usage('/').percent + return f"CPU: {cpu}% | RAM Usada: {ram}% | Disco Usado: {disk}%" + +# Mapeamento estático para o LLM entender quais tools ele possui +AVAILABLE_TOOLS = { + "run_bash_command": { + "description": "Executa qualquer comando Linux no terminal da VPS. Use para criar arquivos, rodar git, verificar logs do docker, etc.", + "func": run_bash_command + }, + "get_system_health": { + "description": "Verifica como está o uso de RAM, CPU e Disco agora.", + "func": get_system_health + } +}