#!/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('', 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('', 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('<>', 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('', 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()