Files
buddie-chat/buddie_chat_simple.py
Emanuel Almeida 48a3b98914 feat: projeto buddie-chat - interface nativa linux para MCPs
- aplicação GTK4/Python para servidores MCP
- interface moderna com Libadwaita
- suporte OpenAI e OpenRouter
- configuração múltiplos MCPs
- chat em tempo real

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-12 01:42:29 +01:00

427 lines
16 KiB
Python

#!/usr/bin/env python3
"""
Buddie Chat - Aplicação nativa Linux simples (Tkinter)
Descomplicar® Crescimento Digital
https://descomplicar.pt
"""
import tkinter as tk
from tkinter import ttk, scrolledtext, messagebox, simpledialog
import json
import os
import asyncio
import threading
from pathlib import Path
from typing import Dict, List, Optional, Any
import subprocess
import openai
from datetime import datetime
class MCPManager:
"""Gestor de conexões MCP"""
def __init__(self):
self.connections: Dict[str, Dict] = {}
self.config_file = Path.home() / '.buddie-chat' / 'config.json'
self.config_file.parent.mkdir(exist_ok=True)
def load_config(self) -> Dict:
"""Carrega configuração"""
if self.config_file.exists():
with open(self.config_file, 'r') as f:
return json.load(f)
return {
'openai_key': '',
'openrouter_key': '',
'mcps': [],
'theme': 'dark'
}
def save_config(self, config: Dict):
"""Guarda configuração"""
with open(self.config_file, 'w') as f:
json.dump(config, f, indent=2)
async def test_mcp_connection(self, mcp_config: Dict) -> bool:
"""Testa conexão MCP"""
try:
process = await asyncio.create_subprocess_exec(
mcp_config['command'],
*mcp_config.get('args', []),
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
)
# Timeout de 5 segundos para teste
try:
stdout, stderr = await asyncio.wait_for(
process.communicate(b'{"jsonrpc": "2.0", "method": "initialize", "id": 1}\n'),
timeout=5.0
)
return process.returncode == 0
except asyncio.TimeoutError:
process.kill()
return False
except Exception as e:
print(f"Erro ao testar MCP {mcp_config['name']}: {e}")
return False
async def send_chat_message(self, message: str, mcp_id: str, openai_key: str, openrouter_key: str = None) -> Dict:
"""Processa mensagem de chat"""
try:
client = openai.OpenAI(
api_key=openai_key if not openrouter_key else openrouter_key,
base_url="https://openrouter.ai/api/v1" if openrouter_key else None
)
model = "anthropic/claude-3.5-sonnet" if openrouter_key else "gpt-4o-mini"
response = client.chat.completions.create(
model=model,
messages=[{"role": "user", "content": message}],
max_tokens=2000
)
return {
'content': response.choices[0].message.content,
'tool_calls': [],
'success': True
}
except Exception as e:
return {
'content': f'Erro: {str(e)}',
'tool_calls': [],
'success': False
}
class BuddieChat:
"""Aplicação principal"""
def __init__(self):
self.mcp_manager = MCPManager()
self.config = self.mcp_manager.load_config()
self.active_mcp = None
self.messages = []
self.setup_ui()
def setup_ui(self):
"""Configura interface"""
self.root = tk.Tk()
self.root.title("Buddie Chat - MCP Interface")
self.root.geometry("1200x800")
# Style
style = ttk.Style()
style.theme_use('clam')
# Layout principal
main_frame = ttk.Frame(self.root)
main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# Painel esquerdo (configuração)
left_frame = ttk.LabelFrame(main_frame, text="Configuração", padding=10)
left_frame.pack(side=tk.LEFT, fill=tk.Y, padx=(0, 10))
self.setup_config_panel(left_frame)
# Painel direito (chat)
right_frame = ttk.Frame(main_frame)
right_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True)
self.setup_chat_panel(right_frame)
def setup_config_panel(self, parent):
"""Configura painel de configuração"""
# API Keys
api_frame = ttk.LabelFrame(parent, text="Chaves API", padding=5)
api_frame.pack(fill=tk.X, pady=(0, 10))
ttk.Label(api_frame, text="OpenAI Key:").pack(anchor=tk.W)
self.openai_var = tk.StringVar(value=self.config.get('openai_key', ''))
openai_entry = ttk.Entry(api_frame, textvariable=self.openai_var, show="*", width=40)
openai_entry.pack(fill=tk.X, pady=(0, 5))
openai_entry.bind('<FocusOut>', self.save_config)
ttk.Label(api_frame, text="OpenRouter Key:").pack(anchor=tk.W)
self.openrouter_var = tk.StringVar(value=self.config.get('openrouter_key', ''))
openrouter_entry = ttk.Entry(api_frame, textvariable=self.openrouter_var, show="*", width=40)
openrouter_entry.pack(fill=tk.X)
openrouter_entry.bind('<FocusOut>', self.save_config)
# MCPs
mcp_frame = ttk.LabelFrame(parent, text="Servidores MCP", padding=5)
mcp_frame.pack(fill=tk.BOTH, expand=True)
# Botões
btn_frame = ttk.Frame(mcp_frame)
btn_frame.pack(fill=tk.X, pady=(0, 5))
ttk.Button(btn_frame, text="Adicionar MCP", command=self.add_mcp).pack(side=tk.LEFT, padx=(0, 5))
ttk.Button(btn_frame, text="Remover", command=self.remove_mcp).pack(side=tk.LEFT)
# Lista MCPs
self.mcp_listbox = tk.Listbox(mcp_frame, height=10)
self.mcp_listbox.pack(fill=tk.BOTH, expand=True, pady=(0, 5))
self.mcp_listbox.bind('<<ListboxSelect>>', self.on_mcp_select)
# Status
self.status_label = ttk.Label(mcp_frame, text="Nenhum MCP selecionado")
self.status_label.pack(anchor=tk.W)
self.update_mcp_list()
def setup_chat_panel(self, parent):
"""Configura painel de chat"""
# Header
header_frame = ttk.Frame(parent)
header_frame.pack(fill=tk.X, pady=(0, 10))
self.chat_title = ttk.Label(header_frame, text="Buddie Chat", font=('TkDefaultFont', 16, 'bold'))
self.chat_title.pack(side=tk.LEFT)
ttk.Button(header_frame, text="Limpar", command=self.clear_chat).pack(side=tk.RIGHT)
# Área de mensagens
self.chat_area = scrolledtext.ScrolledText(parent, state=tk.DISABLED, wrap=tk.WORD, height=25)
self.chat_area.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
# Tags para formatação
self.chat_area.tag_config('user', background='#007acc', foreground='white', justify='right')
self.chat_area.tag_config('assistant', background='#f0f0f0', foreground='black')
self.chat_area.tag_config('error', background='#ffcccc', foreground='darkred')
self.chat_area.tag_config('system', foreground='gray', font=('TkDefaultFont', 8))
# Input
input_frame = ttk.Frame(parent)
input_frame.pack(fill=tk.X)
self.message_var = tk.StringVar()
self.message_entry = ttk.Entry(input_frame, textvariable=self.message_var, font=('TkDefaultFont', 11))
self.message_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 10))
self.message_entry.bind('<Return>', self.send_message)
self.send_button = ttk.Button(input_frame, text="Enviar", command=self.send_message)
self.send_button.pack(side=tk.RIGHT)
self.show_welcome_message()
def show_welcome_message(self):
"""Mostra mensagem de boas-vindas"""
self.add_system_message("=== Bem-vindo ao Buddie Chat ===\\n")
self.add_system_message("Configure suas chaves API e selecione um servidor MCP para começar.\\n")
self.add_system_message("Descomplicar® Crescimento Digital - https://descomplicar.pt\\n\\n")
def add_system_message(self, message):
"""Adiciona mensagem do sistema"""
self.chat_area.config(state=tk.NORMAL)
self.chat_area.insert(tk.END, message, 'system')
self.chat_area.config(state=tk.DISABLED)
self.chat_area.see(tk.END)
def add_user_message(self, message):
"""Adiciona mensagem do utilizador"""
timestamp = datetime.now().strftime("%H:%M:%S")
self.chat_area.config(state=tk.NORMAL)
self.chat_area.insert(tk.END, f"[{timestamp}] Tu: ", 'system')
self.chat_area.insert(tk.END, f"{message}\\n\\n", 'user')
self.chat_area.config(state=tk.DISABLED)
self.chat_area.see(tk.END)
def add_assistant_message(self, message, is_error=False):
"""Adiciona mensagem do assistente"""
timestamp = datetime.now().strftime("%H:%M:%S")
tag = 'error' if is_error else 'assistant'
name = "Erro" if is_error else "Assistente"
self.chat_area.config(state=tk.NORMAL)
self.chat_area.insert(tk.END, f"[{timestamp}] {name}: ", 'system')
self.chat_area.insert(tk.END, f"{message}\\n\\n", tag)
self.chat_area.config(state=tk.DISABLED)
self.chat_area.see(tk.END)
def update_mcp_list(self):
"""Atualiza lista de MCPs"""
self.mcp_listbox.delete(0, tk.END)
for mcp in self.config.get('mcps', []):
self.mcp_listbox.insert(tk.END, f"{mcp['name']} - {mcp['command']}")
def on_mcp_select(self, event):
"""Seleciona MCP"""
selection = self.mcp_listbox.curselection()
if selection:
index = selection[0]
self.active_mcp = self.config['mcps'][index]
self.status_label.config(text=f"MCP ativo: {self.active_mcp['name']}")
self.chat_title.config(text=f"Chat - {self.active_mcp['name']}")
def add_mcp(self):
"""Adiciona novo MCP"""
dialog = MCPConfigDialog(self.root, self.add_mcp_callback)
def add_mcp_callback(self, mcp_data):
"""Callback para adicionar MCP"""
if 'mcps' not in self.config:
self.config['mcps'] = []
mcp_data['id'] = f"mcp_{len(self.config['mcps'])}"
self.config['mcps'].append(mcp_data)
self.mcp_manager.save_config(self.config)
self.update_mcp_list()
def remove_mcp(self):
"""Remove MCP selecionado"""
selection = self.mcp_listbox.curselection()
if selection:
index = selection[0]
mcp_name = self.config['mcps'][index]['name']
if messagebox.askyesno("Confirmar", f"Remover MCP '{mcp_name}'?"):
del self.config['mcps'][index]
self.mcp_manager.save_config(self.config)
self.update_mcp_list()
if self.active_mcp and self.active_mcp['name'] == mcp_name:
self.active_mcp = None
self.status_label.config(text="Nenhum MCP selecionado")
self.chat_title.config(text="Buddie Chat")
def save_config(self, event=None):
"""Guarda configuração"""
self.config['openai_key'] = self.openai_var.get()
self.config['openrouter_key'] = self.openrouter_var.get()
self.mcp_manager.save_config(self.config)
def clear_chat(self):
"""Limpa chat"""
self.chat_area.config(state=tk.NORMAL)
self.chat_area.delete(1.0, tk.END)
self.chat_area.config(state=tk.DISABLED)
self.show_welcome_message()
def send_message(self, event=None):
"""Envia mensagem"""
message = self.message_var.get().strip()
if not message:
return
if not self.active_mcp:
messagebox.showwarning("Aviso", "Selecione um servidor MCP primeiro.")
return
if not self.config.get('openai_key') and not self.config.get('openrouter_key'):
messagebox.showwarning("Aviso", "Configure sua chave API primeiro.")
return
self.message_var.set("")
self.add_user_message(message)
self.send_button.config(state=tk.DISABLED, text="Processando...")
# Processa mensagem em thread separada
threading.Thread(
target=self.process_message_thread,
args=(message,),
daemon=True
).start()
def process_message_thread(self, message):
"""Processa mensagem em thread separada"""
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
response = loop.run_until_complete(
self.mcp_manager.send_chat_message(
message,
self.active_mcp['id'],
self.config.get('openai_key', ''),
self.config.get('openrouter_key', '')
)
)
self.root.after(0, self.add_assistant_message, response['content'], not response['success'])
except Exception as e:
self.root.after(0, self.add_assistant_message, f"Erro: {str(e)}", True)
finally:
self.root.after(0, lambda: self.send_button.config(state=tk.NORMAL, text="Enviar"))
loop.close()
def run(self):
"""Executa aplicação"""
self.root.mainloop()
class MCPConfigDialog:
"""Diálogo de configuração MCP"""
def __init__(self, parent, callback):
self.callback = callback
self.dialog = tk.Toplevel(parent)
self.dialog.title("Configurar MCP")
self.dialog.geometry("500x300")
self.dialog.transient(parent)
self.dialog.grab_set()
# Centralizar
self.dialog.geometry("+%d+%d" % (
parent.winfo_rootx() + 100,
parent.winfo_rooty() + 100
))
self.setup_dialog()
def setup_dialog(self):
"""Configura diálogo"""
main_frame = ttk.Frame(self.dialog, padding=20)
main_frame.pack(fill=tk.BOTH, expand=True)
# Campos
ttk.Label(main_frame, text="Nome:").grid(row=0, column=0, sticky=tk.W, pady=(0, 5))
self.name_var = tk.StringVar()
ttk.Entry(main_frame, textvariable=self.name_var, width=50).grid(row=0, column=1, pady=(0, 5))
ttk.Label(main_frame, text="Comando:").grid(row=1, column=0, sticky=tk.W, pady=(0, 5))
self.command_var = tk.StringVar()
ttk.Entry(main_frame, textvariable=self.command_var, width=50).grid(row=1, column=1, pady=(0, 5))
ttk.Label(main_frame, text="Argumentos:").grid(row=2, column=0, sticky=tk.W, pady=(0, 5))
self.args_var = tk.StringVar()
ttk.Entry(main_frame, textvariable=self.args_var, width=50).grid(row=2, column=1, pady=(0, 5))
ttk.Label(main_frame, text="(separados por espaço)", font=('TkDefaultFont', 8)).grid(row=3, column=1, sticky=tk.W, pady=(0, 10))
# Botões
btn_frame = ttk.Frame(main_frame)
btn_frame.grid(row=4, column=0, columnspan=2, pady=20)
ttk.Button(btn_frame, text="Cancelar", command=self.dialog.destroy).pack(side=tk.RIGHT, padx=(10, 0))
ttk.Button(btn_frame, text="Guardar", command=self.save).pack(side=tk.RIGHT)
def save(self):
"""Guarda configuração"""
name = self.name_var.get().strip()
command = self.command_var.get().strip()
args = self.args_var.get().strip().split() if self.args_var.get().strip() else []
if not name or not command:
messagebox.showwarning("Aviso", "Nome e comando são obrigatórios.")
return
mcp_data = {
'name': name,
'command': command,
'args': args,
'enabled': True
}
self.callback(mcp_data)
self.dialog.destroy()
def main():
"""Função principal"""
app = BuddieChat()
app.run()
if __name__ == '__main__':
main()