feat: upgrade interface web e suporte a áudio completo

This commit is contained in:
2026-03-22 01:05:27 +00:00
parent 2d3da03ee6
commit 3e2e81bd64
7 changed files with 435 additions and 131 deletions

View File

@@ -1,120 +1,90 @@
import os
import re
import requests
from tools import run_bash_command, get_system_health
import json
from tools import AVAILABLE_TOOLS
from config import get_config
def get_llm_response(prompt: str, provider: str, cfg: dict) -> str:
"""Invoca o provedor de LLM configurado."""
if provider == "gemini":
api_key = cfg.get("gemini_api_key")
url = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key={api_key}"
payload = {"contents": [{"parts": [{"text": prompt}]}]}
res = requests.post(url, json=payload)
if res.status_code == 200:
return res.json()["candidates"][0]["content"]["parts"][0]["text"]
return f"Erro Gemini: {res.text}"
elif provider == "ollama":
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": prompt,
"stream": False
})
if res.status_code == 200:
return res.json().get("response", "")
return f"Erro Ollama: {res.text}"
return "Provedor desconhecido."
def query_agent(prompt: str, override_provider: str = None) -> str:
"""
Função principal que roteia pro LLM desejado, detecta intents e aciona as Tools.
Motor Agente em Loop (ReAct): Pensamento -> Ação -> Observação -> Resposta Final.
"""
# 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")
provider = override_provider or cfg.get("active_provider", "gemini")
if provider == "gemini":
gemini_api_key = cfg.get("gemini_api_key", "")
if not gemini_api_key: return "Chave API do Gemini não configurada."
# 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.
### REGRAS:
1. Responda em PORTUGUÊS (Brasil).
2. Para agir, use: [CMD] comando [/CMD]. Rode UM comando por vez.
3. Seus comandos devem ser diretos (docker, git, ls, rm, mkdir).
4. Após o comando, você receberá a saída. O seu objetivo é resolver a solicitação do usuário.
5. Quando terminar, sua resposta FINAL deve ter:
- Um resumo técnico rápido.
- Uma seção entre tags <REFINED> ... </REFINED> com uma tabela Markdown limpa ou resumo em tópicos (Nome: Valor) para o usuário leigo.
### FERRAMENTAS DISPONÍVEIS:
{TOOLS_LIST}
### EXEMPLO DE REFINAMENTO VISUAL:
Relatório: Coletei os dados solicitados.
<REFINED>
### 📊 Status Global
- **CPU**: 10%
- **RAM**: 500MB livre
</REFINED>
"""
system_prompt = system_prompt_base.replace("{TOOLS_LIST}", tools_desc)
history = f"\nUsuário: {prompt}\n"
max_loops = 10
for _ in range(max_loops):
full_prompt = system_prompt + history
response = get_llm_response(full_prompt, provider, cfg)
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}"
# Procura por comandos na resposta
match = re.search(r"\[CMD\](.*?)\[/CMD\]", response, re.IGNORECASE | re.DOTALL)
if match:
cmd = match.group(1).strip()
# Executa a tool (por enquanto focada em bash que é a mais poderosa)
print(f"Agente executando: {cmd}")
observation = AVAILABLE_TOOLS["run_bash_command"]["func"](cmd)
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."
# Adiciona ao histórico para a IA ler na próxima rodada
history += f"\nAgente (Pensamento/Ação): {response}\nSISTEMA (Saída do Terminal): {observation}\n"
else:
# Se não tem comando, é a resposta final
return response
return "O agente atingiu o limite de tentativas para esta tarefa."

42
audio_handler.py Normal file
View File

@@ -0,0 +1,42 @@
import os
import speech_recognition as sr
from pydub import AudioSegment
from gtts import gTTS
import uuid
import re
def transcribe_audio(file_path: str) -> str:
"""Converte áudio (qualquer formato compatível com pydub) para WAV e transcreve com Google Speech."""
recognizer = sr.Recognizer()
# Se não for wav, converte usando pydub (precisa de ffmpeg na VPS)
temp_wav = f"/tmp/{uuid.uuid4()}.wav"
try:
audio = AudioSegment.from_file(file_path)
audio.export(temp_wav, format="wav")
with sr.AudioFile(temp_wav) as source:
audio_data = recognizer.record(source)
text = recognizer.recognize_google(audio_data, language="pt-BR")
return text
finally:
if os.path.exists(temp_wav):
os.remove(temp_wav)
def text_to_speech(text: str) -> str:
"""Sintetiza texto em áudio MP3, removendo tags visuais e emojis."""
# Limpeza para narração
texto_limpo = text.replace("🤖", "").replace("🧑‍🏫", "").replace("*", "").replace("`", "")
# Remove o bloco <REFINED> se houver, pois ele é para leitura visual apenas
texto_limpo = re.sub(r'<REFINED>.*?</REFINED>', '', texto_limpo, flags=re.DOTALL).strip()
# Se sobrar texto vazio após limpar o refinado (raro), fala algo genérico
if not texto_limpo:
texto_limpo = "Relatório processado. Os detalhes estão no painel visual."
filename = f"audio_reply_{uuid.uuid4().hex[:8]}.mp3"
filepath = os.path.join("/tmp", filename)
tts = gTTS(text=texto_limpo, lang='pt-br', tld='com.br', slow=False)
tts.save(filepath)
return filename

View File

@@ -9,7 +9,8 @@ services:
- "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
- .:/app
- /var/run/docker.sock:/var/run/docker.sock:rw
- /:/host_root:ro # Acesso em leitura à VPS para análise
- ./data:/app/data # Configs dinâmicas (API Keys, etc)
env_file:

42
main.py
View File

@@ -3,11 +3,12 @@ import psutil
import subprocess
import time
import json
from fastapi import FastAPI, Request, Header, Depends, HTTPException, status
from fastapi.responses import HTMLResponse, JSONResponse
from fastapi import FastAPI, Request, Header, Depends, HTTPException, status, File, UploadFile
from fastapi.responses import HTMLResponse, JSONResponse, FileResponse
from fastapi.templating import Jinja2Templates
from dotenv import load_dotenv
from starlette.concurrency import run_in_threadpool
import audio_handler
from ai_agent import query_agent
from config import get_config, save_config
@@ -111,6 +112,43 @@ async def web_chat(message: dict, is_auth: bool = Depends(verify_password)):
reply = await run_in_threadpool(query_agent, prompt=user_text)
return JSONResponse(content={"reply": reply})
@app.post("/api/chat-audio")
async def web_chat_audio(audio: UploadFile = File(...), is_auth: bool = Depends(verify_password)):
"""Recebe áudio, transcreve, processa na IA e devolve texto + áudio de resposta."""
temp_path = f"/tmp/{audio.filename}"
with open(temp_path, "wb") as buffer:
buffer.write(await audio.read())
try:
# Transcrição (STT)
user_text = await run_in_threadpool(audio_handler.transcribe_audio, temp_path)
# IA (Processamento)
reply = await run_in_threadpool(query_agent, prompt=user_text)
# Síntese (TTS)
audio_filename = await run_in_threadpool(audio_handler.text_to_speech, reply)
audio_url = f"/api/audio-file/{audio_filename}"
return JSONResponse(content={
"text": user_text,
"reply": reply,
"audio_url": audio_url
})
except Exception as e:
return JSONResponse(content={"status": "error", "message": str(e)}, status_code=500)
finally:
if os.path.exists(temp_path):
os.remove(temp_path)
@app.get("/api/audio-file/{filename}")
async def get_audio_file(filename: str):
"""Serve os arquivos de áudio temporários gerados pelo TTS."""
filepath = os.path.join("/tmp", filename)
if os.path.exists(filepath):
return FileResponse(filepath, media_type="audio/mpeg")
raise HTTPException(status_code=404, detail="Arquivo de áudio não encontrado.")
@app.get("/api/test_llm")
async def test_llm_speed(is_auth: bool = Depends(verify_password)):
"""Mede a velocidade de resposta da IA ativa."""

3
start.sh Executable file
View File

@@ -0,0 +1,3 @@
#!/bin/bash
uvicorn main:app --host 0.0.0.0 --port 8000 --reload &
python bot_logic.py

View File

@@ -7,6 +7,7 @@
<!-- Favicon -->
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🤖</text></svg>">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<style>
:root {
--bg-base: #0f172a;
@@ -26,6 +27,36 @@
--transition: 0.25s ease;
}
/* Custom Scrollbar */
::-webkit-scrollbar { width: 8px; }
::-webkit-scrollbar-track { background: var(--bg-base); }
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; border: 2px solid var(--bg-base); }
::-webkit-scrollbar-thumb:hover { background: var(--accent); }
/* Scrollbar Fix for Firefox */
* { scrollbar-width: thin; scrollbar-color: var(--accent) transparent; }
@keyframes pulse {
0% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.1); opacity: 0.7; }
100% { transform: scale(1); opacity: 1; }
}
/* Custom Scrollbar */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: var(--bg-base);
}
::-webkit-scrollbar-thumb {
background: var(--border);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--accent);
}
[data-theme="light"] {
--bg-base: #f1f5f9;
--bg-card: #ffffff;
@@ -318,11 +349,77 @@
cursor: pointer;
}
/* Chat */
/* Chat & Insights Layout */
.chat-layout {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
height: clamp(400px, 60vh, 800px);
margin-bottom: 2rem;
}
.chat-wrapper {
display: flex;
flex-direction: column;
height: min(350px, 45vh);
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius);
overflow: hidden;
}
.insights-wrapper {
display: flex;
flex-direction: column;
background: var(--bg-card);
border: 2px solid var(--accent-glow);
border-radius: var(--radius);
overflow: hidden;
box-shadow: 0 0 20px rgba(59, 130, 246, 0.1);
}
.insights-header {
padding: 0.75rem 1rem;
background: rgba(59, 130, 246, 0.05);
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
gap: 0.5rem;
font-weight: 600;
color: var(--accent);
font-size: 0.9rem;
}
.insights-content {
flex: 1;
padding: 1.25rem;
overflow-y: auto;
font-size: 0.9rem;
}
.insights-content h3 { margin-bottom: 1rem; color: var(--accent); }
.insights-content table {
width: 100%;
border-collapse: collapse;
margin: 1rem 0;
background: var(--bg-input);
border-radius: 8px;
overflow: hidden;
}
.insights-content th, .insights-content td {
padding: 0.75rem;
text-align: left;
border-bottom: 1px solid var(--border);
}
.insights-content th { background: rgba(59, 130, 246, 0.1); color: var(--accent); font-size: 0.75rem; text-transform: uppercase; }
.insights-placeholder {
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: var(--text-muted);
text-align: center;
padding: 2rem;
}
.chat-messages {
@@ -617,21 +714,52 @@
</button>
</div>
<div class="section-title">Chat AI</div>
<div class="card chat-wrapper">
<div class="chat-messages" id="chat-box">
<div class="chat-bubble bubble-ai">
Olá! Sou o VPS Agent. Como posso ajudar com seu servidor?
<div class="section-title">Terminal & Insights da IA</div>
<div class="chat-layout">
<!-- Coluna 1: Chat Técnico -->
<div class="chat-wrapper">
<div class="chat-messages" id="chat-box">
<div class="chat-bubble bubble-ai">
Olá! Sou o VPS Agent. Como posso ajudar com seu servidor? Tudo o que eu fizer aparecerá aqui no terminal técnico.
</div>
</div>
<div class="chat-input-area" style="padding: 0.75rem; gap: 0.5rem;">
<input type="text" id="chat-input" class="chat-input" placeholder="Comande sua VPS aqui..." onkeypress="handleKeyPress(event)">
<button class="btn" id="audio-btn" onclick="toggleRecording()" title="Gravar Áudio">
<svg id="mic-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/>
<path d="M19 10v2a7 7 0 0 1-14 0v-2M12 19v4M8 23h8"/>
</svg>
<div id="recording-dot" style="display:none; width:8px; height:8px; background:red; border-radius:50%; animation: pulse 1s infinite;"></div>
</button>
<button class="btn btn-primary" onclick="sendMessage()">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="22" y1="2" x2="11" y2="13"/>
<polygon points="22 2 15 22 11 13 2 9 22 2"/>
</svg>
</button>
</div>
</div>
<div class="chat-input-area">
<input type="text" id="chat-input" class="chat-input" placeholder="Digite seu comando..." onkeypress="handleKeyPress(event)">
<button class="btn btn-primary" onclick="sendMessage()">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="22" y1="2" x2="11" y2="13"/>
<polygon points="22 2 15 22 11 13 2 9 22 2"/>
<!-- Coluna 2: Painel de Insights (Refinado) -->
<div class="insights-wrapper">
<div class="insights-header">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="18" height="18">
<path d="M21 12V7a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h7"/>
<line x1="16" y1="5" x2="16" y2="19"/>
<line x1="2" x2="16" y2="12"/>
</svg>
</button>
Painel de Insights Visuais
</div>
<div class="insights-content" id="insights-panel">
<div class="insights-placeholder">
<div style="font-size: 2.5rem; margin-bottom: 1rem;">📊</div>
<p>Aguardando dados estruturados...</p>
<small style="display:block; margin-top:0.5rem; opacity:0.6;">Peça algo como "status dos containers" para ver o refinamento aqui.</small>
</div>
</div>
</div>
</div>
</div>
@@ -794,12 +922,36 @@
body: JSON.stringify({ text })
});
const data = await res.json();
addBubble(data.reply, 'ai');
processAIReply(data.reply);
} catch (e) {
addBubble('Erro ao contatar servidor.', 'ai');
}
}
function processAIReply(fullText) {
// Separa a parte técnica da parte refinada
const refinedMatch = fullText.match(/<REFINED>([\s\S]*?)<\/REFINED>/i);
let technicalPart = fullText;
if (refinedMatch) {
technicalPart = fullText.replace(refinedMatch[0], '').trim();
const refinedContent = refinedMatch[1].trim();
updateInsightsPanel(refinedContent);
}
if (technicalPart) {
addBubble(technicalPart, 'ai');
}
}
function updateInsightsPanel(markdown) {
const panel = document.getElementById('insights-panel');
if(!panel) return;
// Renderiza o Markdown para HTML usando marked.js
panel.innerHTML = `<div class="animate-fade-in">${marked.parse(markdown)}</div>`;
}
function handleKeyPress(e) {
if (e.key === 'Enter') sendChat();
}
@@ -880,6 +1032,81 @@
window.testLLMSpeed = testLLMSpeed;
window.attemptLogin = attemptLogin;
// --- AUDIO SYSTEM ---
let mediaRecorder;
let audioChunks = [];
let isRecording = false;
async function toggleRecording() {
if (!isRecording) {
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
mediaRecorder = new MediaRecorder(stream);
audioChunks = [];
mediaRecorder.ondataavailable = (event) => {
audioChunks.push(event.data);
};
mediaRecorder.onstop = async () => {
const audioBlob = new Blob(audioChunks, { type: 'audio/webm' });
await uploadAudio(audioBlob);
};
mediaRecorder.start();
isRecording = true;
updateMicUI(true);
} catch (err) {
showToast("Erro ao acessar microfone", true);
}
} else {
mediaRecorder.stop();
mediaRecorder.stream.getTracks().forEach(t => t.stop());
isRecording = false;
updateMicUI(false);
}
}
function updateMicUI(active) {
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)';
}
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) {
addBubble(data.text, 'user');
processAIReply(data.reply);
// Se o bot retornou áudio, toca ele
if(data.audio_url) {
const audio = new Audio(data.audio_url + '?t=' + Date.now());
audio.play();
}
}
} catch (e) {
showToast("Erro ao processar áudio", true);
}
}
window.toggleRecording = toggleRecording;
// Auto-login se já tiver senha salva
if (webPassword) {
(async () => {

View File

@@ -5,13 +5,13 @@ 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
# Executa comando 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
timeout=120 # Aumentado para comandos mais pesados
)
output = result.stdout.strip()
error = result.stderr.strip()
@@ -32,14 +32,37 @@ def get_system_health() -> str:
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
def read_vps_file(filepath: str) -> str:
"""Lê um arquivo do sistema de arquivos da VPS através do mapeamento /host_root."""
host_path = f"/host_root{filepath}" if not filepath.startswith("/host_root") else filepath
try:
if not os.path.exists(host_path):
return f"Erro: Arquivo {filepath} não encontrado no host."
with open(host_path, 'r') as f:
return f.read(2000) # Limite para não estourar o contexto
except Exception as e:
return f"Erro ao ler arquivo: {e}"
def get_docker_stats() -> str:
"""Retorna o uso de CPU/RAM de todos os containers ativos via comando docker stats."""
return run_bash_command('docker stats --no-stream --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}"')
# Mapeamento para o Agente entender quais tools ele possui (será usado no loop ReAct)
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.",
"description": "Executa comandos Linux na VPS. Use para docker, git, mkdir, touch, etc.",
"func": run_bash_command
},
"get_system_health": {
"description": "Verifica como está o uso de RAM, CPU e Disco agora.",
"description": "Verifica RAM, CPU e Disco globais da VPS.",
"func": get_system_health
},
"read_vps_file": {
"description": "Lê o conteúdo de um arquivo na VPS (logs, configs).",
"func": read_vps_file
},
"get_docker_stats": {
"description": "Retorna uma tabela com o consumo de CPU e Memória de cada container.",
"func": get_docker_stats
}
}