From 6589c62b18677d36c52bd4d135dda9899f6a9a3e Mon Sep 17 00:00:00 2001 From: Marcos Date: Sun, 22 Mar 2026 10:10:27 -0300 Subject: [PATCH] 1 --- ai_agent.py | 68 ++++---- bot_logic.py | 35 ++++- templates/index.html | 360 +++++++++++++++++++++++++++++-------------- tools.py | 17 +- 4 files changed, 327 insertions(+), 153 deletions(-) diff --git a/ai_agent.py b/ai_agent.py index fb2f054..1ba1fb1 100644 --- a/ai_agent.py +++ b/ai_agent.py @@ -29,7 +29,7 @@ def get_llm_response(prompt: str, provider: str, cfg: dict) -> str: return "Provedor desconhecido." -def query_agent(prompt: str, override_provider: str = None) -> str: +def query_agent(prompt: str, override_provider: str = None, chat_history: list = None) -> str: """ Motor Agente em Loop (ReAct): Pensamento -> Ação -> Observação -> Resposta Final. """ @@ -39,41 +39,57 @@ def query_agent(prompt: str, override_provider: str = None) -> str: # Contexto de Ferramentas para a IA tools_desc = "\n".join([f"- {k}: {v['description']}" for k,v in AVAILABLE_TOOLS.items()]) - # Prompt especializado (sem chaves complexas) - system_prompt_base = """Você é o [Antigravity VPS Agent]. -Sua missão é ser o SysAdmin de elite do Marcos. Você tem acesso root. + # Prompt especializado reformulado para evitar alucinações + system_prompt_base = """Você é o [Antigravity VPS Agent], o SysAdmin de elite do Marcos. +Você tem acesso root completo à VPS e deve agir de forma profissional e precisa. -### REGRAS: +### REGRAS DE OURO: 1. Responda em PORTUGUÊS (Brasil). -2. Para agir, use o formato: [TOOL:nome_da_ferramenta] argumento [/TOOL]. Rode UMA ferramenta por vez. -3. Para comandos de terminal, use a ferramenta 'run_bash_command'. Exemplo: [TOOL:run_bash_command] ls -la [/TOOL]. -4. Para ler arquivos do host, use 'read_vps_file'. Exemplo: [TOOL:read_vps_file] /etc/hosts [/TOOL]. -5. Após usar a ferramenta, você receberá a saída. O seu objetivo é resolver a solicitação do usuário. -6. Quando terminar, sua resposta FINAL deve ter: - - Um resumo técnico rápido. - - Uma seção entre tags ... com uma tabela Markdown limpa ou resumo em tópicos (Nome: Valor) para o usuário leigo. -7. Se for solicitado exibir uma imagem (ex: um logo, foto) que você achou no host, **exiba-a dentro da tag ** usando markdown de imagem com o endpoint `/api/host_file?path=`. Exemplo: `![Imagem](/api/host_file?path=/var/www/html/img/Logo.jpg)` +2. Se o usuário pedir o status da VPS, SEMPRE use a ferramenta 'get_system_health'. +3. Se o usuário pedir algo sobre containers, use 'get_docker_stats'. +4. Antes de decidir que um arquivo não existe, use 'run_bash_command' com 'ls' para verificar o diretório. +5. NUCA invente que buscou por arquivos (como syslog.conf) se o usuário não pediu especificamente por eles. +6. A seção deve conter apenas as informações solicitadas. Se não houver imagem relevante, não inclua tags de imagem. + +### FORMATO DE AÇÃO: +Use: [TOOL:nome_da_ferramenta] argumento [/TOOL] +Rode UMA ferramenta por vez. Aguarde a saída do SISTEMA antes de concluir. + +### RESPOSTA FINAL: +Sua resposta terminada deve ter: +- Um resumo técnico. +- Uma seção ... com Markdown limpo. +- **DICA**: Só use imagens em se o usuário pediu para ver um arquivo de imagem específico que você localizou. ### FERRAMENTAS DISPONÍVEIS: {TOOLS_LIST} -### EXEMPLO DE REFINAMENTO VISUAL: -Relatório: Coletei os dados solicitados. +### EXEMPLO DE SUCESSO: +Usuário: qual o uso de ram agora? +Agente: [TOOL:get_system_health] [/TOOL] +SISTEMA: CPU: 5% | RAM Usada: 20% | Disco Usado: 40% +Resposta: A memória RAM está operando com 20% de uso. -### 📊 Status Global -- **CPU**: 10% -- **RAM**: 500MB livre - -![Logo](/api/host_file?path=/var/www/html/img/LogoSteelPaint.jpg) +### 📊 Memória e CPU +- **RAM Utilizada**: 20% +- **CPU**: 5% """ system_prompt = system_prompt_base.replace("{TOOLS_LIST}", tools_desc) - history = f"\nUsuário: {prompt}\n" - max_loops = 10 + # Constrói o histórico da conversa (memória de curto prazo) + history_str = "" + if chat_history: + for msg in chat_history[-5:]: # Pega as últimas 5 interações + history_str += f"\nUsuário: {msg['user']}\nAgente: {msg['bot']}\n" + + history_str += f"\nUsuário: {prompt}\n" + + current_iteration_history = history_str + max_loops = 8 for _ in range(max_loops): - full_prompt = system_prompt + history + full_prompt = system_prompt + current_iteration_history response = get_llm_response(full_prompt, provider, cfg) # Procura por chamadas de ferramentas na resposta @@ -91,10 +107,10 @@ Relatório: Coletei os dados solicitados. else: observation = AVAILABLE_TOOLS[tool_name]["func"](arg) else: - observation = f"Erro: Ferramenta '{tool_name}' não encontrada. Use as ferramentas listadas ou 'run_bash_command'." + observation = f"Erro: Ferramenta '{tool_name}' não encontrada." - # Adiciona ao histórico para a IA ler na próxima rodada - history += f"\nAgente (Ação): {response}\nSISTEMA (Saída de {tool_name}): {observation}\n" + # Adiciona ao histórico do loop atual + current_iteration_history += f"\nAgente (Ação): {response}\nSISTEMA (Saída de {tool_name}): {observation}\n" else: # Se não tem comando, é a resposta final return response diff --git a/bot_logic.py b/bot_logic.py index fd5e829..0aee90f 100644 --- a/bot_logic.py +++ b/bot_logic.py @@ -41,17 +41,28 @@ 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.") +# Memória persistente da conversa (em memória RAM) +chat_histories = {} + async def handle_text(update: Update, context: ContextTypes.DEFAULT_TYPE): if not await auth_check(update): return + chat_id = update.message.chat_id user_msg = update.message.text await update.message.reply_chat_action(action="typing") + # Busca histórico anterior + history = chat_histories.get(chat_id, []) + # 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")) + reply = query_agent(prompt=user_msg, override_provider=cfg.get("active_provider"), chat_history=history) + # Atualiza histórico + history.append({"user": user_msg, "bot": reply}) + chat_histories[chat_id] = history[-10:] # Mantém apenas as últimas 10 + # 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") @@ -59,8 +70,6 @@ async def handle_text(update: Update, context: ContextTypes.DEFAULT_TYPE): 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) @@ -93,10 +102,18 @@ async def handle_voice(update: Update, context: ContextTypes.DEFAULT_TYPE): text = recognizer.recognize_google(audio_data, language="pt-BR") await update.message.reply_text(f"🗣️ Reconhecido: _{text}_", parse_mode="Markdown") + # Busca histórico anterior + chat_id = update.message.chat_id + history = chat_histories.get(chat_id, []) + # 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")) + reply = query_agent(prompt=text, override_provider=cfg.get("active_provider"), chat_history=history) + + # Atualiza histórico + history.append({"user": text, "bot": reply}) + chat_histories[chat_id] = history[-10:] # Sintetiza com ElevenLabs e responde com Áudio audio_path = synthesize_audio(reply) @@ -129,12 +146,22 @@ async def llm_command(update: Update, context: ContextTypes.DEFAULT_TYPE): else: await update.message.reply_text("Modelos disponíveis: gemini ou ollama.") +async def clear_history(update: Update, context: ContextTypes.DEFAULT_TYPE): + if not await auth_check(update): return + chat_id = update.message.chat_id + if chat_id in chat_histories: + chat_histories[chat_id] = [] + await update.message.reply_text("🧹 Memória limpa com sucesso!") + else: + await update.message.reply_text("A memória já está vazia.") + 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(CommandHandler("limpar", clear_history)) app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_text)) app.add_handler(MessageHandler(filters.VOICE, handle_voice)) return app diff --git a/templates/index.html b/templates/index.html index 0b50ab3..7bc3b0e 100644 --- a/templates/index.html +++ b/templates/index.html @@ -1,11 +1,13 @@ + VPS AI Dashboard - + +
Online
-
+

CPU

--%
-
+

RAM

-- / -- GB
-
+

Disk

--%
-
+
Ações Rápidas
- -
@@ -746,22 +871,22 @@
- - - - + Painel de Insights Visuais
-
📊
+

Aguardando dados estruturados...

- Peça algo como "status dos containers" para ver o refinamento aqui. + Peça algo como "status dos containers" para ver o refinamento aqui.
-
+
Ação executada!
@@ -773,7 +898,7 @@ async function apiFetch(url, options = {}) { if (!options.headers) options.headers = {}; options.headers['X-Web-Password'] = webPassword; - + const res = await fetch(url, options); if (res.status === 401) { showLoginOverlay(); @@ -822,15 +947,11 @@ updateThemeIcon(!isDark); } - function updateThemeIcon(isDark) { - document.getElementById('icon-sun').style.display = isDark ? 'block' : 'none'; - document.getElementById('icon-moon').style.display = isDark ? 'none' : 'block'; - } + function updateThemeIcon(isDark) {} const savedTheme = localStorage.getItem('theme'); if (savedTheme) { document.documentElement.dataset.theme = savedTheme; - updateThemeIcon(savedTheme === 'light'); } // Stats @@ -874,7 +995,7 @@ } } - setInterval(() => { if(!document.getElementById('login-overlay').classList.contains('hidden')) return; fetchStats(); }, 3000); + setInterval(() => { if (!document.getElementById('login-overlay').classList.contains('hidden')) return; fetchStats(); }, 3000); // Actions async function executeAction(type) { @@ -901,7 +1022,7 @@ function showToast(msg, isError = false) { const toast = document.getElementById('toast'); - if(!toast) return; + if (!toast) return; toast.textContent = msg; toast.className = isError ? 'error show' : 'show'; setTimeout(() => toast.classList.remove('show'), 3000); @@ -931,7 +1052,7 @@ function processAIReply(fullText) { // Separa a parte técnica da parte refinada const refinedMatch = fullText.match(/([\s\S]*?)<\/REFINED>/i); - + let technicalPart = fullText; if (refinedMatch) { technicalPart = fullText.replace(refinedMatch[0], '').trim(); @@ -946,11 +1067,11 @@ function updateInsightsPanel(markdown) { const panel = document.getElementById('insights-panel'); - if(!panel) return; - + if (!panel) return; + // Injeta a senha web nas URLs das imagens para autenticação inline const mdWithAuth = markdown.replace(/\/api\/host_file\?path=/g, '/api/host_file?pwd=' + encodeURIComponent(webPassword) + '&path='); - + // Renderiza o Markdown para HTML usando marked.js panel.innerHTML = `
${marked.parse(mdWithAuth)}
`; } @@ -961,7 +1082,7 @@ function addBubble(text, sender) { const box = document.getElementById('chat-box'); - if(!box) return; + if (!box) return; const div = document.createElement('div'); div.className = 'chat-bubble bubble-' + sender; div.textContent = text; @@ -977,7 +1098,7 @@ const key = document.getElementById('gemini_api_key'); if (provider) provider.value = data.active_provider || 'ollama'; if (key) key.value = data.gemini_api_key || ''; - } catch (e) {} + } catch (e) { } } async function saveConfig() { @@ -1010,20 +1131,20 @@ try { const res = await apiFetch('/api/test_llm'); const data = await res.json(); - if(data.status === 'success') { + if (data.status === 'success') { showToast(`✅ LLM Online! Resposta em ${data.latency}s`); btn.innerHTML = `✅ ${data.latency}s`; setTimeout(() => { btn.innerHTML = originalContent; btn.disabled = false; }, 5000); } else { throw new Error(data.message); } - } catch(e) { + } catch (e) { showToast("❌ Erro no Teste LLM: " + e.message, true); btn.innerHTML = '❌ Falhou'; btn.classList.add('btn-danger'); - setTimeout(() => { - btn.innerHTML = originalContent; - btn.disabled = false; + setTimeout(() => { + btn.innerHTML = originalContent; + btn.disabled = false; btn.classList.remove('btn-danger'); }, 5000); } @@ -1074,31 +1195,31 @@ const dot = document.getElementById('recording-dot'); const icon = document.getElementById('mic-icon'); const btn = document.getElementById('audio-btn'); - - dot.style.display = active ? 'block' : 'none'; - icon.style.color = active ? 'red' : 'inherit'; - btn.style.borderColor = active ? 'red' : 'var(--border)'; + + if (dot) dot.style.display = active ? 'inline-block' : 'none'; + if (icon) icon.style.color = active ? 'red' : 'inherit'; + if (btn) btn.style.borderColor = active ? 'red' : 'var(--border)'; } async function uploadAudio(blob) { const formData = new FormData(); formData.append('audio', blob, 'voice.webm'); - + showToast("✨ Transcrevendo áudio..."); - + try { const res = await apiFetch('/api/chat-audio', { method: 'POST', body: formData }); const data = await res.json(); - - if(data.text) { + + if (data.text) { addBubble(data.text, 'user'); processAIReply(data.reply); - + // Se o bot retornou áudio, toca ele - if(data.audio_url) { + if (data.audio_url) { const audio = new Audio(data.audio_url + '?t=' + Date.now()); audio.play(); } @@ -1121,11 +1242,12 @@ } else { showLoginOverlay(); } - } catch(e) { showLoginOverlay(); } + } catch (e) { showLoginOverlay(); } })(); } else { showLoginOverlay(); } - + + \ No newline at end of file diff --git a/tools.py b/tools.py index 85315c3..e5a223a 100644 --- a/tools.py +++ b/tools.py @@ -1,6 +1,7 @@ import subprocess import os import psutil +import time def run_bash_command(command: str) -> str: """Executa um comando bash na VPS e retorna a saída.""" @@ -26,11 +27,19 @@ def run_bash_command(command: str) -> str: 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.""" + """Retorna um texto detalhado da saúde do servidor para a IA.""" 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}%" + vm = psutil.virtual_memory() + disk = psutil.disk_usage('/') + + # Uptime aproximado usando psutil + uptime_seconds = time.time() - psutil.boot_time() + uptime_hours = round(uptime_seconds / 3600, 1) + + return (f"CPU: {cpu}% | " + f"RAM: {round(vm.used / (1024**3), 2)}GB usada / {round(vm.total / (1024**3), 2)}GB total ({vm.percent}%) | " + f"Disco: {disk.percent}% usado | " + f"Uptime: {uptime_hours}h") def read_vps_file(filepath: str) -> str: """Lê um arquivo do sistema de arquivos da VPS através do mapeamento /host_root."""