From 494f6e29438c57eaf2b01b8edd9c383ad1d3d224 Mon Sep 17 00:00:00 2001 From: z060142 Date: Sat, 26 Apr 2025 14:00:26 +0800 Subject: [PATCH] Add Bot setup UI --- .gitignore | 4 +- Setup.py | 1163 ++++++++++++++++++++++++++++++++++++++ config.py | 134 ----- config_template.py | 62 ++ install.py | 137 +++++ window-monitor-script.py | 121 ---- window-setup-script.py | 103 ---- 7 files changed, 1365 insertions(+), 359 deletions(-) create mode 100644 Setup.py delete mode 100644 config.py create mode 100644 config_template.py create mode 100644 install.py delete mode 100644 window-monitor-script.py delete mode 100644 window-setup-script.py diff --git a/.gitignore b/.gitignore index 2e99f16..2f907ee 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ .env *.log llm_debug.log +config.py __pycache__/ debug_screenshots/ -chat_logs/ \ No newline at end of file +chat_logs/ +backup/ \ No newline at end of file diff --git a/Setup.py b/Setup.py new file mode 100644 index 0000000..b59ebbc --- /dev/null +++ b/Setup.py @@ -0,0 +1,1163 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Wolf Chat Setup Utility + +A graphical configuration tool for Wolf Chat that manages: +- API settings (OpenAI/compatible providers) +- MCP server configurations (Exa, Chroma, custom servers) +- Environment variables (.env) +- Config file generation (config.py) +""" + +import os +import sys +import json +import tkinter as tk +from tkinter import ttk, filedialog, messagebox, scrolledtext +import configparser +from pathlib import Path +import re +import shutil + +# =============================================================== +# Constants +# =============================================================== +VERSION = "1.0.0" +CONFIG_TEMPLATE_PATH = "config_template.py" +ENV_FILE_PATH = ".env" +DEFAULT_CHROMA_DATA_PATH = "chroma_data" +DEFAULT_CONFIG_SECTION = """# ==================================================================== +# Wolf Chat Configuration +# Generated by setup.py - Edit with care +# ==================================================================== +""" + +# =============================================================== +# Helper Functions +# =============================================================== +def load_env_file(): + """Load existing .env file if it exists""" + env_data = {} + env_path = Path(ENV_FILE_PATH) + if env_path.exists(): + with open(env_path, 'r', encoding='utf-8') as f: + for line in f: + line = line.strip() + if line and not line.startswith('#') and '=' in line: + key, value = line.split('=', 1) + env_data[key.strip()] = value.strip().strip('"\'') + return env_data + +def save_env_file(env_data): + """Save environment variables to .env file""" + with open(ENV_FILE_PATH, 'w', encoding='utf-8') as f: + f.write("# Environment variables for Wolf Chat\n") + f.write("# Generated by setup.py\n\n") + for key, value in env_data.items(): + if value: # Only write non-empty values + f.write(f"{key}={value}\n") + print(f"Saved environment variables to {ENV_FILE_PATH}") + +def load_current_config(): + """Extract settings from existing config.py if it exists""" + config_data = { + "OPENAI_API_BASE_URL": "", + "LLM_MODEL": "deepseek/deepseek-chat-v3-0324", + "MCP_SERVERS": {}, + "ENABLE_CHAT_LOGGING": True, + "LOG_DIR": "chat_logs", + "GAME_WINDOW_CONFIG": { + "WINDOW_TITLE": "Last War-Survival Game", + "ENABLE_SCHEDULED_RESTART": True, + "RESTART_INTERVAL_MINUTES": 60, + "GAME_EXECUTABLE_PATH": r"C:\Users\user\AppData\Local\TheLastWar\Launch.exe", + "GAME_WINDOW_X": 50, + "GAME_WINDOW_Y": 30, + "GAME_WINDOW_WIDTH": 600, + "GAME_WINDOW_HEIGHT": 1070, + "MONITOR_INTERVAL_SECONDS": 5 + } + } + + if os.path.exists("config.py"): + try: + with open("config.py", 'r', encoding='utf-8') as f: + config_content = f.read() + + # Extract OPENAI_API_BASE_URL + api_url_match = re.search(r'OPENAI_API_BASE_URL\s*=\s*["\'](.+?)["\']', config_content) + if api_url_match: + config_data["OPENAI_API_BASE_URL"] = api_url_match.group(1) + + # Extract LLM_MODEL + model_match = re.search(r'LLM_MODEL\s*=\s*["\'](.+?)["\']', config_content) + if model_match: + config_data["LLM_MODEL"] = model_match.group(1) + + # Extract logging settings + chat_logging_match = re.search(r'ENABLE_CHAT_LOGGING\s*=\s*(True|False)', config_content) + if chat_logging_match: + config_data["ENABLE_CHAT_LOGGING"] = (chat_logging_match.group(1) == "True") + + log_dir_match = re.search(r'LOG_DIR\s*=\s*["\'](.+?)["\']', config_content) + if log_dir_match: + config_data["LOG_DIR"] = log_dir_match.group(1) + + # Extract game window settings + window_title_match = re.search(r'WINDOW_TITLE\s*=\s*["\'](.+?)["\']', config_content) + if window_title_match: + config_data["GAME_WINDOW_CONFIG"]["WINDOW_TITLE"] = window_title_match.group(1) + + restart_match = re.search(r'ENABLE_SCHEDULED_RESTART\s*=\s*(True|False)', config_content) + if restart_match: + config_data["GAME_WINDOW_CONFIG"]["ENABLE_SCHEDULED_RESTART"] = (restart_match.group(1) == "True") + + interval_match = re.search(r'RESTART_INTERVAL_MINUTES\s*=\s*(\d+)', config_content) + if interval_match: + config_data["GAME_WINDOW_CONFIG"]["RESTART_INTERVAL_MINUTES"] = int(interval_match.group(1)) + + exec_path_match = re.search(r'GAME_EXECUTABLE_PATH\s*=\s*r"(.+?)"', config_content) + if exec_path_match: + config_data["GAME_WINDOW_CONFIG"]["GAME_EXECUTABLE_PATH"] = exec_path_match.group(1) + + x_match = re.search(r'GAME_WINDOW_X\s*=\s*(\d+)', config_content) + if x_match: + config_data["GAME_WINDOW_CONFIG"]["GAME_WINDOW_X"] = int(x_match.group(1)) + + y_match = re.search(r'GAME_WINDOW_Y\s*=\s*(\d+)', config_content) + if y_match: + config_data["GAME_WINDOW_CONFIG"]["GAME_WINDOW_Y"] = int(y_match.group(1)) + + width_match = re.search(r'GAME_WINDOW_WIDTH\s*=\s*(\d+)', config_content) + if width_match: + config_data["GAME_WINDOW_CONFIG"]["GAME_WINDOW_WIDTH"] = int(width_match.group(1)) + + height_match = re.search(r'GAME_WINDOW_HEIGHT\s*=\s*(\d+)', config_content) + if height_match: + config_data["GAME_WINDOW_CONFIG"]["GAME_WINDOW_HEIGHT"] = int(height_match.group(1)) + + monitor_interval_match = re.search(r'MONITOR_INTERVAL_SECONDS\s*=\s*(\d+)', config_content) + if monitor_interval_match: + config_data["GAME_WINDOW_CONFIG"]["MONITOR_INTERVAL_SECONDS"] = int(monitor_interval_match.group(1)) + + # Extract MCP_SERVERS (more complex parsing) + try: + servers_section = re.search(r'MCP_SERVERS\s*=\s*{(.+?)}(?=\n\n)', config_content, re.DOTALL) + if servers_section: + servers_text = servers_section.group(1) + + # Extract and parse each server definition + server_blocks = re.findall(r'"([^"]+)":\s*{(.+?)}(?=,\s*"|,?\s*})', servers_text, re.DOTALL) + + for server_name, server_block in server_blocks: + if server_name not in config_data["MCP_SERVERS"]: + config_data["MCP_SERVERS"][server_name] = {"enabled": True} + + # Skip disabled servers (commented out) + if server_block.strip().startswith("#"): + config_data["MCP_SERVERS"][server_name]["enabled"] = False + continue + + # Extract command + command_match = re.search(r'"command":\s*"([^"]+)"', server_block) + if command_match: + config_data["MCP_SERVERS"][server_name]["command"] = command_match.group(1) + + # For Exa server + if server_name == "exa": + # Check if using Smithery + if '"@smithery/cli@latest"' in server_block: + config_data["MCP_SERVERS"]["exa"]["use_smithery"] = True + else: + config_data["MCP_SERVERS"]["exa"]["use_smithery"] = False + + # Extract server path for local server + args_match = re.search(r'"args":\s*\[\s*"([^"]+)"', server_block) + if args_match: + config_data["MCP_SERVERS"]["exa"]["server_path"] = args_match.group(1) + + # For Chroma server + if server_name == "chroma": + # Extract data directory + data_dir_match = re.search(r'"--data-dir",\s*"([^"]+)"', server_block) + if data_dir_match: + config_data["MCP_SERVERS"]["chroma"]["data_dir"] = data_dir_match.group(1) + + # For custom servers, store the raw configuration + if server_name not in ["exa", "chroma"]: + config_data["MCP_SERVERS"][server_name]["raw_config"] = server_block + except Exception as e: + print(f"Error parsing MCP_SERVERS section: {e}") + import traceback + traceback.print_exc() + + except Exception as e: + print(f"Error reading config.py: {e}") + import traceback + traceback.print_exc() + + return config_data + +def generate_config_file(config_data, env_data): + """Generate config.py file based on user settings""" + # Create backup of existing config if it exists + if os.path.exists("config.py"): + backup_path = "config.py.bak" + shutil.copy2("config.py", backup_path) + print(f"Created backup of existing config at {backup_path}") + + with open("config.py", 'w', encoding='utf-8') as f: + f.write(DEFAULT_CONFIG_SECTION) + f.write("import os\n") + f.write("import json\n") + f.write("from dotenv import load_dotenv\n\n") + + f.write("# --- Load environment variables from .env file ---\n") + f.write("load_dotenv()\n") + f.write("print(\"Loaded environment variables from .env file.\")\n\n") + + # Write OpenAI API settings + f.write("# =============================================================================\n") + f.write("# OpenAI API Configuration / OpenAI-Compatible Provider Settings\n") + f.write("# =============================================================================\n") + f.write(f"OPENAI_API_BASE_URL = \"{config_data['OPENAI_API_BASE_URL']}\"\n") + f.write("OPENAI_API_KEY = os.getenv(\"OPENAI_API_KEY\")\n") + f.write(f"LLM_MODEL = \"{config_data['LLM_MODEL']}\"\n\n") + + # Write API Keys section + f.write("# =============================================================================\n") + f.write("# External API Keys (loaded from environment variables)\n") + f.write("# =============================================================================\n") + f.write("EXA_API_KEY = os.getenv(\"EXA_API_KEY\")\n\n") + + # Write Exa config utility + f.write("# --- Exa Configuration ---\n") + f.write("exa_config_dict = {\"exaApiKey\": EXA_API_KEY if EXA_API_KEY else \"YOUR_EXA_KEY_MISSING\"}\n") + f.write("exa_config_arg_string = json.dumps(exa_config_dict)\n\n") + + # Write MCP Server Configuration + f.write("# =============================================================================\n") + f.write("# MCP Server Configuration\n") + f.write("# =============================================================================\n") + f.write("MCP_SERVERS = {\n") + + # Add configured servers + for server_name, server_config in config_data["MCP_SERVERS"].items(): + if not server_config.get("enabled", True): + f.write(f" # \"{server_name}\": {{ # Disabled\n") + continue + + f.write(f" \"{server_name}\": {{\n") + + # Handle Exa server special config + if server_name == "exa": + if server_config.get("use_smithery", False): + # Smithery config + f.write(" \"command\": \"cmd\",\n") + f.write(" \"args\": [\n") + f.write(" \"/c\",\n") + f.write(" \"npx\",\n") + f.write(" \"-y\",\n") + f.write(" \"@smithery/cli@latest\",\n") + f.write(" \"run\",\n") + f.write(" \"exa\",\n") + f.write(" \"--config\",\n") + f.write(" exa_config_arg_string\n") + f.write(" ],\n") + else: + # Local server config + server_path = server_config.get("server_path", "exa-mcp-server") + f.write(" \"command\": \"npx\",\n") + f.write(" \"args\": [\n") + f.write(f" \"{server_path}\",\n") + f.write(" \"--tools=web_search,research_paper_search,twitter_search,company_research,crawling,competitor_finder\"\n") + f.write(" ],\n") + f.write(" \"env\": {\n") + f.write(" \"EXA_API_KEY\": EXA_API_KEY\n") + f.write(" }\n") + + # Handle Chroma server + elif server_name == "chroma": + data_dir = server_config.get("data_dir", DEFAULT_CHROMA_DATA_PATH) + f.write(" \"command\": \"uvx\",\n") + f.write(" \"args\": [\n") + f.write(" \"chroma-mcp\",\n") + f.write(" \"--client-type\",\n") + f.write(" \"persistent\",\n") + f.write(" \"--data-dir\",\n") + f.write(f" \"{data_dir}\"\n") + f.write(" ]\n") + + # Handle custom server - just write as raw JSON + elif server_name == "custom" and "raw_config" in server_config: + f.write(server_config["raw_config"]) + + f.write(" },\n") + + f.write("}\n\n") + + # Write remaining configuration sections + f.write("# =============================================================================\n") + f.write("# MCP Client Configuration\n") + f.write("# =============================================================================\n") + f.write("MCP_CONFIRM_TOOL_EXECUTION = False # True: Confirm before execution, False: Execute automatically\n\n") + + f.write("# =============================================================================\n") + f.write("# Chat Logging Configuration\n") + f.write("# =============================================================================\n") + f.write(f"ENABLE_CHAT_LOGGING = {str(config_data['ENABLE_CHAT_LOGGING'])}\n") + f.write(f"LOG_DIR = \"{config_data['LOG_DIR']}\"\n\n") + + f.write("# =============================================================================\n") + f.write("# Persona Configuration\n") + f.write("# =============================================================================\n") + f.write("PERSONA_NAME = \"Wolfhart\"\n\n") + + f.write("# =============================================================================\n") + f.write("# Game Window Configuration\n") + f.write("# =============================================================================\n") + game_config = config_data["GAME_WINDOW_CONFIG"] + f.write(f"WINDOW_TITLE = \"{game_config['WINDOW_TITLE']}\"\n") + f.write(f"ENABLE_SCHEDULED_RESTART = {str(game_config['ENABLE_SCHEDULED_RESTART'])}\n") + f.write(f"RESTART_INTERVAL_MINUTES = {game_config['RESTART_INTERVAL_MINUTES']}\n") + f.write(f"GAME_EXECUTABLE_PATH = r\"{game_config['GAME_EXECUTABLE_PATH']}\"\n") + f.write(f"GAME_WINDOW_X = {game_config['GAME_WINDOW_X']}\n") + f.write(f"GAME_WINDOW_Y = {game_config['GAME_WINDOW_Y']}\n") + f.write(f"GAME_WINDOW_WIDTH = {game_config['GAME_WINDOW_WIDTH']}\n") + f.write(f"GAME_WINDOW_HEIGHT = {game_config['GAME_WINDOW_HEIGHT']}\n") + f.write(f"MONITOR_INTERVAL_SECONDS = {game_config['MONITOR_INTERVAL_SECONDS']}\n") + + print("Generated config.py file successfully") + + +# =============================================================== +# Main Application +# =============================================================== +class WolfChatSetup(tk.Tk): + def __init__(self): + super().__init__() + self.title(f"Wolf Chat Setup v{VERSION}") + self.geometry("800x600") + self.minsize(750, 550) + + # Load existing data + self.env_data = load_env_file() + self.config_data = load_current_config() + + # Create the notebook for tabs + self.notebook = ttk.Notebook(self) + self.notebook.pack(expand=True, fill=tk.BOTH, padx=10, pady=10) + + # Create tabs + self.create_api_tab() + self.create_mcp_tab() + self.create_game_tab() + + # Create bottom buttons + self.create_bottom_buttons() + + # Set initial states based on loaded data + self.update_ui_from_data() + + def create_api_tab(self): + """Create the API Settings tab""" + tab = ttk.Frame(self.notebook) + self.notebook.add(tab, text="API Settings") + + # Main frame with padding + main_frame = ttk.Frame(tab, padding=10) + main_frame.pack(fill=tk.BOTH, expand=True) + + # Header + header = ttk.Label(main_frame, text="OpenAI API Settings", font=("", 12, "bold")) + header.pack(anchor=tk.W, pady=(0, 10)) + + # API Base URL + url_frame = ttk.Frame(main_frame) + url_frame.pack(fill=tk.X, pady=5) + + url_label = ttk.Label(url_frame, text="API Base URL:", width=15) + url_label.pack(side=tk.LEFT, padx=(0, 5)) + + self.api_url_var = tk.StringVar() + self.api_url_entry = ttk.Entry(url_frame, textvariable=self.api_url_var) + self.api_url_entry.pack(side=tk.LEFT, fill=tk.X, expand=True) + + # Predefined endpoints + url_presets_frame = ttk.Frame(main_frame) + url_presets_frame.pack(fill=tk.X, pady=2) + + preset_label = ttk.Label(url_presets_frame, text="Presets:", width=15) + preset_label.pack(side=tk.LEFT, padx=(0, 5)) + + openai_btn = ttk.Button(url_presets_frame, text="OpenAI", width=10, + command=lambda: self.api_url_var.set("")) + openai_btn.pack(side=tk.LEFT, padx=2) + + openrouter_btn = ttk.Button(url_presets_frame, text="OpenRouter", width=10, + command=lambda: self.api_url_var.set("https://openrouter.ai/api/v1")) + openrouter_btn.pack(side=tk.LEFT, padx=2) + + localhost_btn = ttk.Button(url_presets_frame, text="Localhost", width=10, + command=lambda: self.api_url_var.set("http://localhost:1234/v1")) + localhost_btn.pack(side=tk.LEFT, padx=2) + + # API Key + key_frame = ttk.Frame(main_frame) + key_frame.pack(fill=tk.X, pady=5) + + key_label = ttk.Label(key_frame, text="API Key:", width=15) + key_label.pack(side=tk.LEFT, padx=(0, 5)) + + self.api_key_var = tk.StringVar() + self.api_key_entry = ttk.Entry(key_frame, textvariable=self.api_key_var, show="*") + self.api_key_entry.pack(side=tk.LEFT, fill=tk.X, expand=True) + + self.show_key_var = tk.BooleanVar(value=False) + show_key_cb = ttk.Checkbutton(key_frame, text="Show", variable=self.show_key_var, + command=self.toggle_key_visibility) + show_key_cb.pack(side=tk.LEFT, padx=(5, 0)) + + # Model Selection + model_frame = ttk.Frame(main_frame) + model_frame.pack(fill=tk.X, pady=5) + + model_label = ttk.Label(model_frame, text="LLM Model:", width=15) + model_label.pack(side=tk.LEFT, padx=(0, 5)) + + self.model_var = tk.StringVar(value="deepseek/deepseek-chat-v3-0324") + self.model_entry = ttk.Entry(model_frame, textvariable=self.model_var) + self.model_entry.pack(side=tk.LEFT, fill=tk.X, expand=True) + + # Information box + info_frame = ttk.LabelFrame(main_frame, text="Information") + info_frame.pack(fill=tk.BOTH, expand=True, pady=10) + + info_text = ( + "• Leave the API Base URL empty to use the official OpenAI API\n" + "• For OpenRouter, you need an OpenRouter API key\n" + "• For local LLMs, configure your API to use OpenAI-compatible format\n" + "• Make sure the selected model is available with your chosen provider" + ) + + info_label = ttk.Label(info_frame, text=info_text, justify=tk.LEFT, wraplength=700) + info_label.pack(padx=10, pady=10, anchor=tk.W) + + def create_mcp_tab(self): + """Create the MCP Settings tab""" + tab = ttk.Frame(self.notebook) + self.notebook.add(tab, text="MCP Servers") + + # Main frame with padding + main_frame = ttk.Frame(tab, padding=10) + main_frame.pack(fill=tk.BOTH, expand=True) + + # Header + header = ttk.Label(main_frame, text="Modular Capability Provider (MCP) Servers", font=("", 12, "bold")) + header.pack(anchor=tk.W, pady=(0, 10)) + + # Create the main frame for server settings + servers_frame = ttk.Frame(main_frame) + servers_frame.pack(fill=tk.BOTH, expand=True) + + # Left side - Servers list + servers_list_frame = ttk.LabelFrame(servers_frame, text="Available Servers") + servers_list_frame.pack(side=tk.LEFT, fill=tk.Y, padx=(0, 5)) + + # Servers list + self.servers_listbox = tk.Listbox(servers_list_frame, width=20, height=10) + self.servers_listbox.pack(side=tk.LEFT, fill=tk.Y, expand=True, padx=5, pady=5) + self.servers_listbox.bind('<>', self.on_server_select) + + servers_scrollbar = ttk.Scrollbar(servers_list_frame, orient=tk.VERTICAL, command=self.servers_listbox.yview) + servers_scrollbar.pack(side=tk.RIGHT, fill=tk.Y) + self.servers_listbox.config(yscrollcommand=servers_scrollbar.set) + + # Buttons below listbox + servers_btn_frame = ttk.Frame(servers_list_frame) + servers_btn_frame.pack(fill=tk.X, pady=5, padx=5) + + add_btn = ttk.Button(servers_btn_frame, text="Add", command=self.add_custom_server) + add_btn.pack(side=tk.LEFT, padx=2, fill=tk.X, expand=True) + + self.remove_btn = ttk.Button(servers_btn_frame, text="Remove", command=self.remove_server, state=tk.DISABLED) + self.remove_btn.pack(side=tk.LEFT, padx=2, fill=tk.X, expand=True) + + # Right side - Server settings + self.server_settings_frame = ttk.LabelFrame(servers_frame, text="Server Settings") + self.server_settings_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True) + + # Empty label for initial state + self.empty_settings_label = ttk.Label(self.server_settings_frame, + text="Select a server from the list to configure it.", + justify=tk.CENTER) + self.empty_settings_label.pack(expand=True, pady=20) + + # Frames for each server type (initially hidden) + self.create_exa_settings_frame() + self.create_chroma_settings_frame() + self.create_custom_settings_frame() + + # Update the servers list + self.update_servers_list() + + def create_exa_settings_frame(self): + """Create settings frame for Exa server""" + self.exa_frame = ttk.Frame(self.server_settings_frame) + + # Enable checkbox + enable_frame = ttk.Frame(self.exa_frame) + enable_frame.pack(fill=tk.X, pady=5) + + self.exa_enable_var = tk.BooleanVar(value=True) + enable_cb = ttk.Checkbutton(enable_frame, text="Enable Exa Server", variable=self.exa_enable_var) + enable_cb.pack(anchor=tk.W) + + # API Key + key_frame = ttk.Frame(self.exa_frame) + key_frame.pack(fill=tk.X, pady=5) + + key_label = ttk.Label(key_frame, text="Exa API Key:", width=15) + key_label.pack(side=tk.LEFT) + + self.exa_key_var = tk.StringVar() + self.exa_key_entry = ttk.Entry(key_frame, textvariable=self.exa_key_var, show="*") + self.exa_key_entry.pack(side=tk.LEFT, fill=tk.X, expand=True) + + self.show_exa_key_var = tk.BooleanVar(value=False) + show_key_cb = ttk.Checkbutton(key_frame, text="Show", variable=self.show_exa_key_var, + command=lambda: self.toggle_field_visibility(self.exa_key_entry, self.show_exa_key_var)) + show_key_cb.pack(side=tk.LEFT, padx=(5, 0)) + + # Server Type + type_frame = ttk.Frame(self.exa_frame) + type_frame.pack(fill=tk.X, pady=5) + + type_label = ttk.Label(type_frame, text="Server Type:", width=15) + type_label.pack(side=tk.LEFT) + + self.exa_type_var = tk.StringVar(value="smithery") + + smithery_radio = ttk.Radiobutton(type_frame, text="Smithery (recommended)", + variable=self.exa_type_var, value="smithery", + command=self.update_exa_settings_visibility) + smithery_radio.pack(anchor=tk.W) + + local_radio = ttk.Radiobutton(type_frame, text="Local Server", + variable=self.exa_type_var, value="local", + command=self.update_exa_settings_visibility) + local_radio.pack(anchor=tk.W) + + # Local Server Path + self.exa_local_frame = ttk.Frame(self.exa_frame) + + local_path_label = ttk.Label(self.exa_local_frame, text="Server Path:", width=15) + local_path_label.pack(side=tk.LEFT) + + self.exa_path_var = tk.StringVar() + self.exa_path_entry = ttk.Entry(self.exa_local_frame, textvariable=self.exa_path_var) + self.exa_path_entry.pack(side=tk.LEFT, fill=tk.X, expand=True) + + browse_btn = ttk.Button(self.exa_local_frame, text="Browse", command=self.browse_exa_server) + browse_btn.pack(side=tk.LEFT, padx=(5, 0)) + + # Smithery Info + self.exa_smithery_frame = ttk.Frame(self.exa_frame) + + smithery_info = ttk.Label(self.exa_smithery_frame, + text="Smithery will automatically download and run the latest Exa MCP server.", + wraplength=400) + smithery_info.pack(pady=5) + + # Information text + info_frame = ttk.LabelFrame(self.exa_frame, text="Information") + info_frame.pack(fill=tk.X, pady=10) + + info_text = ( + "• Exa MCP provides web search and research capabilities\n" + "• An Exa API key is required (obtain from https://exa.ai)\n" + "• Smithery is easiest to use but requires an internet connection\n" + "• Local server requires manual installation of exa-mcp-server" + ) + + info_label = ttk.Label(info_frame, text=info_text, justify=tk.LEFT, wraplength=400) + info_label.pack(padx=10, pady=10, anchor=tk.W) + + def create_chroma_settings_frame(self): + """Create settings frame for Chroma MCP server""" + self.chroma_frame = ttk.Frame(self.server_settings_frame) + + # Enable checkbox + enable_frame = ttk.Frame(self.chroma_frame) + enable_frame.pack(fill=tk.X, pady=5) + + self.chroma_enable_var = tk.BooleanVar(value=True) + enable_cb = ttk.Checkbutton(enable_frame, text="Enable Chroma MCP Server", variable=self.chroma_enable_var) + enable_cb.pack(anchor=tk.W) + + # Data directory + dir_frame = ttk.Frame(self.chroma_frame) + dir_frame.pack(fill=tk.X, pady=5) + + dir_label = ttk.Label(dir_frame, text="Data Directory:", width=15) + dir_label.pack(side=tk.LEFT) + + self.chroma_dir_var = tk.StringVar(value=DEFAULT_CHROMA_DATA_PATH) + dir_entry = ttk.Entry(dir_frame, textvariable=self.chroma_dir_var) + dir_entry.pack(side=tk.LEFT, fill=tk.X, expand=True) + + browse_btn = ttk.Button(dir_frame, text="Browse", command=self.browse_chroma_dir) + browse_btn.pack(side=tk.LEFT, padx=(5, 0)) + + # Information text + info_frame = ttk.LabelFrame(self.chroma_frame, text="Information") + info_frame.pack(fill=tk.X, pady=10, expand=True) + + info_text = ( + "• Chroma MCP provides vector database storage for memory features\n" + "• The data directory will store vector embeddings and metadata\n" + "• Use a persistent directory to maintain memory between sessions\n" + "• Requires uvx to be installed in your Python environment" + ) + + info_label = ttk.Label(info_frame, text=info_text, justify=tk.LEFT, wraplength=400) + info_label.pack(padx=10, pady=10, anchor=tk.W) + + def create_custom_settings_frame(self): + """Create settings frame for custom server""" + self.custom_frame = ttk.Frame(self.server_settings_frame) + + # Name field + name_frame = ttk.Frame(self.custom_frame) + name_frame.pack(fill=tk.X, pady=5) + + name_label = ttk.Label(name_frame, text="Server Name:", width=15) + name_label.pack(side=tk.LEFT) + + self.custom_name_var = tk.StringVar() + name_entry = ttk.Entry(name_frame, textvariable=self.custom_name_var) + name_entry.pack(side=tk.LEFT, fill=tk.X, expand=True) + + # Enable checkbox + enable_frame = ttk.Frame(self.custom_frame) + enable_frame.pack(fill=tk.X, pady=5) + + self.custom_enable_var = tk.BooleanVar(value=True) + enable_cb = ttk.Checkbutton(enable_frame, text="Enable Server", variable=self.custom_enable_var) + enable_cb.pack(anchor=tk.W) + + # Raw configuration editor + config_label = ttk.Label(self.custom_frame, text="Server Configuration (Python Dictionary Format):") + config_label.pack(anchor=tk.W, pady=(10, 5)) + + self.custom_config_text = scrolledtext.ScrolledText(self.custom_frame, height=12, width=50) + self.custom_config_text.pack(fill=tk.BOTH, expand=True, pady=5) + + # Default configuration template + default_config = ( + ' "command": "npx",\n' + ' "args": [\n' + ' "some-mcp-server",\n' + ' "--option1",\n' + ' "--option2=value"\n' + ' ]' + ) + self.custom_config_text.insert(tk.END, default_config) + + # Information text + info_frame = ttk.LabelFrame(self.custom_frame, text="Information") + info_frame.pack(fill=tk.X, pady=10) + + info_text = ( + "• Enter the raw Python dictionary for your custom MCP server\n" + "• Format must match the structure in config.py\n" + "• Be careful with syntax - incorrect format may cause errors\n" + "• Server name must be unique" + ) + + info_label = ttk.Label(info_frame, text=info_text, justify=tk.LEFT, wraplength=400) + info_label.pack(padx=10, pady=10, anchor=tk.W) + + def create_game_tab(self): + """Create the Game Settings tab""" + tab = ttk.Frame(self.notebook) + self.notebook.add(tab, text="Game Settings") + + # Main frame with padding + main_frame = ttk.Frame(tab, padding=10) + main_frame.pack(fill=tk.BOTH, expand=True) + + # Header + header = ttk.Label(main_frame, text="Game Window Configuration", font=("", 12, "bold")) + header.pack(anchor=tk.W, pady=(0, 10)) + + # Game window title + title_frame = ttk.Frame(main_frame) + title_frame.pack(fill=tk.X, pady=5) + + title_label = ttk.Label(title_frame, text="Window Title:", width=20) + title_label.pack(side=tk.LEFT, padx=(0, 5)) + + self.window_title_var = tk.StringVar(value="Last War-Survival Game") + title_entry = ttk.Entry(title_frame, textvariable=self.window_title_var) + title_entry.pack(side=tk.LEFT, fill=tk.X, expand=True) + + # Game executable path + path_frame = ttk.Frame(main_frame) + path_frame.pack(fill=tk.X, pady=5) + + path_label = ttk.Label(path_frame, text="Game Executable Path:", width=20) + path_label.pack(side=tk.LEFT, padx=(0, 5)) + + self.game_path_var = tk.StringVar() + path_entry = ttk.Entry(path_frame, textvariable=self.game_path_var) + path_entry.pack(side=tk.LEFT, fill=tk.X, expand=True) + + browse_btn = ttk.Button(path_frame, text="Browse", command=self.browse_game_path) + browse_btn.pack(side=tk.LEFT, padx=(5, 0)) + + # Window position + pos_frame = ttk.Frame(main_frame) + pos_frame.pack(fill=tk.X, pady=5) + + pos_label = ttk.Label(pos_frame, text="Window Position:", width=20) + pos_label.pack(side=tk.LEFT, padx=(0, 5)) + + pos_x_label = ttk.Label(pos_frame, text="X:") + pos_x_label.pack(side=tk.LEFT) + + self.pos_x_var = tk.IntVar(value=50) + pos_x_entry = ttk.Spinbox(pos_frame, textvariable=self.pos_x_var, from_=0, to=3000, width=5) + pos_x_entry.pack(side=tk.LEFT, padx=(0, 10)) + + pos_y_label = ttk.Label(pos_frame, text="Y:") + pos_y_label.pack(side=tk.LEFT) + + self.pos_y_var = tk.IntVar(value=30) + pos_y_entry = ttk.Spinbox(pos_frame, textvariable=self.pos_y_var, from_=0, to=3000, width=5) + pos_y_entry.pack(side=tk.LEFT) + + # Window size + size_frame = ttk.Frame(main_frame) + size_frame.pack(fill=tk.X, pady=5) + + size_label = ttk.Label(size_frame, text="Window Size:", width=20) + size_label.pack(side=tk.LEFT, padx=(0, 5)) + + width_label = ttk.Label(size_frame, text="Width:") + width_label.pack(side=tk.LEFT) + + self.width_var = tk.IntVar(value=600) + width_entry = ttk.Spinbox(size_frame, textvariable=self.width_var, from_=300, to=3000, width=5) + width_entry.pack(side=tk.LEFT, padx=(0, 10)) + + height_label = ttk.Label(size_frame, text="Height:") + height_label.pack(side=tk.LEFT) + + self.height_var = tk.IntVar(value=1070) + height_entry = ttk.Spinbox(size_frame, textvariable=self.height_var, from_=300, to=3000, width=5) + height_entry.pack(side=tk.LEFT) + + # Auto-restart settings + restart_frame = ttk.LabelFrame(main_frame, text="Auto-Restart Settings") + restart_frame.pack(fill=tk.X, pady=10) + + self.restart_var = tk.BooleanVar(value=True) + restart_cb = ttk.Checkbutton(restart_frame, text="Enable scheduled game restart", variable=self.restart_var) + restart_cb.pack(anchor=tk.W, padx=10, pady=5) + + interval_frame = ttk.Frame(restart_frame) + interval_frame.pack(fill=tk.X, padx=10, pady=5) + + interval_label = ttk.Label(interval_frame, text="Restart interval (minutes):") + interval_label.pack(side=tk.LEFT) + + self.interval_var = tk.IntVar(value=60) + interval_entry = ttk.Spinbox(interval_frame, textvariable=self.interval_var, from_=15, to=1440, width=5) + interval_entry.pack(side=tk.LEFT, padx=(5, 0)) + + # Monitor interval + monitor_frame = ttk.Frame(main_frame) + monitor_frame.pack(fill=tk.X, pady=5) + + monitor_label = ttk.Label(monitor_frame, text="Monitor Interval (sec):", width=20) + monitor_label.pack(side=tk.LEFT, padx=(0, 5)) + + self.monitor_interval_var = tk.IntVar(value=5) + monitor_entry = ttk.Spinbox(monitor_frame, textvariable=self.monitor_interval_var, from_=1, to=60, width=5) + monitor_entry.pack(side=tk.LEFT) + + # Information text + info_frame = ttk.LabelFrame(main_frame, text="Information") + info_frame.pack(fill=tk.BOTH, expand=True, pady=10) + + info_text = ( + "• These settings control how the game window is monitored and positioned\n" + "• Window Title must match exactly what's shown in the title bar\n" + "• Scheduled restart helps prevent game crashes and memory leaks\n" + "• Monitor interval determines how often the window position is checked\n" + "• Changes will take effect after restarting Wolf Chat" + ) + + info_label = ttk.Label(info_frame, text=info_text, justify=tk.LEFT, wraplength=700) + info_label.pack(padx=10, pady=10, anchor=tk.W) + + def create_bottom_buttons(self): + """Create bottom action buttons""" + btn_frame = ttk.Frame(self) + btn_frame.pack(fill=tk.X, padx=10, pady=10) + + # Version label on left + version_label = ttk.Label(btn_frame, text=f"v{VERSION}") + version_label.pack(side=tk.LEFT, padx=5) + + # Install dependencies button + install_deps_btn = ttk.Button(btn_frame, text="Install Dependencies", command=self.install_dependencies) + install_deps_btn.pack(side=tk.RIGHT, padx=5) + + # Action buttons on right + save_btn = ttk.Button(btn_frame, text="Save Settings", command=self.save_settings) + save_btn.pack(side=tk.RIGHT, padx=5) + + cancel_btn = ttk.Button(btn_frame, text="Cancel", command=self.quit) + cancel_btn.pack(side=tk.RIGHT, padx=5) + + def install_dependencies(self): + """Run the installation script for dependencies""" + try: + import subprocess + import sys + + # Check if install.py exists + if not os.path.exists("install.py"): + messagebox.showerror("Error", "Could not find install.py script") + return + + # Run the installer script in a new process + subprocess.Popen([sys.executable, "install.py"]) + + except Exception as e: + messagebox.showerror("Error", f"Failed to launch installer: {str(e)}") + + + def update_ui_from_data(self): + """Update UI controls from loaded data""" + try: + # API Tab + self.api_url_var.set(self.config_data.get("OPENAI_API_BASE_URL", "")) + self.api_key_var.set(self.env_data.get("OPENAI_API_KEY", "")) + self.model_var.set(self.config_data.get("LLM_MODEL", "deepseek/deepseek-chat-v3-0324")) + + # MCP Servers + # Exa settings + if "exa" in self.config_data.get("MCP_SERVERS", {}): + exa_config = self.config_data["MCP_SERVERS"]["exa"] + self.exa_enable_var.set(exa_config.get("enabled", True)) + self.exa_key_var.set(self.env_data.get("EXA_API_KEY", "")) + + if exa_config.get("use_smithery", False): + self.exa_type_var.set("smithery") + else: + self.exa_type_var.set("local") + if "server_path" in exa_config: + self.exa_path_var.set(exa_config.get("server_path", "")) + + # Chroma settings + if "chroma" in self.config_data.get("MCP_SERVERS", {}): + chroma_config = self.config_data["MCP_SERVERS"]["chroma"] + self.chroma_enable_var.set(chroma_config.get("enabled", True)) + data_dir = chroma_config.get("data_dir", "") + if data_dir: + self.chroma_dir_var.set(data_dir) + + # Update servers list to include custom servers + self.update_servers_list() + + # Game settings + game_config = self.config_data.get("GAME_WINDOW_CONFIG", {}) + self.window_title_var.set(game_config.get("WINDOW_TITLE", "Last War-Survival Game")) + + game_path = game_config.get("GAME_EXECUTABLE_PATH", "") + if game_path: + self.game_path_var.set(game_path) + + self.pos_x_var.set(game_config.get("GAME_WINDOW_X", 50)) + self.pos_y_var.set(game_config.get("GAME_WINDOW_Y", 30)) + self.width_var.set(game_config.get("GAME_WINDOW_WIDTH", 600)) + self.height_var.set(game_config.get("GAME_WINDOW_HEIGHT", 1070)) + self.restart_var.set(game_config.get("ENABLE_SCHEDULED_RESTART", True)) + self.interval_var.set(game_config.get("RESTART_INTERVAL_MINUTES", 60)) + self.monitor_interval_var.set(game_config.get("MONITOR_INTERVAL_SECONDS", 5)) + + # Update visibility and states + self.update_exa_settings_visibility() + + except Exception as e: + print(f"Error updating UI from data: {e}") + import traceback + traceback.print_exc() + + # =============================================================== + # UI Event Handlers + # =============================================================== + def toggle_key_visibility(self): + """Toggle visibility of API key field""" + if self.show_key_var.get(): + self.api_key_entry.config(show="") + else: + self.api_key_entry.config(show="*") + + def toggle_field_visibility(self, entry_widget, show_var): + """Toggle visibility of a password field""" + if show_var.get(): + entry_widget.config(show="") + else: + entry_widget.config(show="*") + + def update_exa_settings_visibility(self): + """Update visibility of Exa settings based on server type""" + if self.exa_type_var.get() == "smithery": + self.exa_local_frame.pack_forget() + self.exa_smithery_frame.pack(fill=tk.X, pady=5) + else: + self.exa_smithery_frame.pack_forget() + self.exa_local_frame.pack(fill=tk.X, pady=5) + + def browse_exa_server(self): + """Browse for Exa server executable""" + file_path = filedialog.askopenfilename( + title="Select Exa MCP Server", + filetypes=[("All Files", "*.*")], + initialdir=os.path.expanduser("~") + ) + if file_path: + self.exa_path_var.set(file_path) + + def browse_chroma_dir(self): + """Browse for Chroma data directory""" + dir_path = filedialog.askdirectory( + title="Select Chroma Data Directory", + initialdir=os.path.abspath(DEFAULT_CHROMA_DATA_PATH) + ) + if dir_path: + self.chroma_dir_var.set(dir_path) + + def browse_game_path(self): + """Browse for game executable""" + file_path = filedialog.askopenfilename( + title="Select Game Executable", + filetypes=[("Executable Files", "*.exe"), ("All Files", "*.*")], + initialdir=os.path.expanduser("~") + ) + if file_path: + self.game_path_var.set(file_path) + + def update_servers_list(self): + """Update the servers listbox with current servers""" + self.servers_listbox.delete(0, tk.END) + + # Add built-in servers + self.servers_listbox.insert(tk.END, "exa") + self.servers_listbox.insert(tk.END, "chroma") + + # Add custom servers + for server_name in self.config_data.get("MCP_SERVERS", {}): + if server_name not in ("exa", "chroma"): + self.servers_listbox.insert(tk.END, server_name) + + def on_server_select(self, event): + """Handle server selection from the listbox""" + selection = self.servers_listbox.curselection() + if not selection: + return + + selected_server = self.servers_listbox.get(selection[0]) + + # Hide all settings frames + self.empty_settings_label.pack_forget() + self.exa_frame.pack_forget() + self.chroma_frame.pack_forget() + self.custom_frame.pack_forget() + + # Show the selected server's settings frame + if selected_server == "exa": + self.exa_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) + self.remove_btn.config(state=tk.DISABLED) # Can't remove built-in servers + elif selected_server == "chroma": + self.chroma_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) + self.remove_btn.config(state=tk.DISABLED) # Can't remove built-in servers + else: + # Custom server + if selected_server in self.config_data.get("MCP_SERVERS", {}): + # Load existing custom server config + server_config = self.config_data["MCP_SERVERS"][selected_server] + self.custom_name_var.set(selected_server) + self.custom_enable_var.set(server_config.get("enabled", True)) + + if "raw_config" in server_config: + self.custom_config_text.delete("1.0", tk.END) + self.custom_config_text.insert("1.0", server_config["raw_config"]) + + self.custom_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) + self.remove_btn.config(state=tk.NORMAL) # Can remove custom servers + + def add_custom_server(self): + """Add a new custom server""" + # Generate a unique name + new_name = "custom_server" + counter = 1 + while new_name in self.config_data.get("MCP_SERVERS", {}): + new_name = f"custom_server_{counter}" + counter += 1 + + # Add to config data + if "MCP_SERVERS" not in self.config_data: + self.config_data["MCP_SERVERS"] = {} + + self.config_data["MCP_SERVERS"][new_name] = { + "enabled": True, + "raw_config": ' "command": "npx",\n "args": [\n "custom-mcp-server"\n ]' + } + + # Update UI + self.update_servers_list() + + # Select the new server + idx = self.servers_listbox.get(0, tk.END).index(new_name) + self.servers_listbox.selection_clear(0, tk.END) + self.servers_listbox.selection_set(idx) + self.servers_listbox.see(idx) + self.on_server_select(None) # Trigger the selection handler + + def remove_server(self): + """Remove the selected custom server""" + selection = self.servers_listbox.curselection() + if not selection: + return + + selected_server = self.servers_listbox.get(selection[0]) + + # Confirmation + confirm = messagebox.askyesno( + "Confirm Removal", + f"Are you sure you want to remove the server '{selected_server}'?", + icon=messagebox.WARNING + ) + + if confirm: + # Remove from config data + if selected_server in self.config_data.get("MCP_SERVERS", {}): + del self.config_data["MCP_SERVERS"][selected_server] + + # Update UI + self.update_servers_list() + + # Clear selection + self.servers_listbox.selection_clear(0, tk.END) + + # Hide server settings and show empty label + self.custom_frame.pack_forget() + self.empty_settings_label.pack(expand=True, pady=20) + self.remove_btn.config(state=tk.DISABLED) + + def save_settings(self): + """Save all settings to config.py and .env files""" + try: + # Update config data from UI + + # API settings + self.config_data["OPENAI_API_BASE_URL"] = self.api_url_var.get() + self.config_data["LLM_MODEL"] = self.model_var.get() + + # Environment variables + self.env_data["OPENAI_API_KEY"] = self.api_key_var.get() + self.env_data["EXA_API_KEY"] = self.exa_key_var.get() + + # MCP Servers + if "MCP_SERVERS" not in self.config_data: + self.config_data["MCP_SERVERS"] = {} + + # Exa server + if "exa" not in self.config_data["MCP_SERVERS"]: + self.config_data["MCP_SERVERS"]["exa"] = {} + + self.config_data["MCP_SERVERS"]["exa"]["enabled"] = self.exa_enable_var.get() + self.config_data["MCP_SERVERS"]["exa"]["use_smithery"] = (self.exa_type_var.get() == "smithery") + if self.exa_type_var.get() == "local": + self.config_data["MCP_SERVERS"]["exa"]["server_path"] = self.exa_path_var.get() + + # Chroma server + if "chroma" not in self.config_data["MCP_SERVERS"]: + self.config_data["MCP_SERVERS"]["chroma"] = {} + + self.config_data["MCP_SERVERS"]["chroma"]["enabled"] = self.chroma_enable_var.get() + self.config_data["MCP_SERVERS"]["chroma"]["data_dir"] = self.chroma_dir_var.get() + + # Custom server - check if one is currently selected + selection = self.servers_listbox.curselection() + if selection: + selected_server = self.servers_listbox.get(selection[0]) + if selected_server not in ("exa", "chroma"): + # Update custom server settings + new_name = self.custom_name_var.get().strip() + + if not new_name: + messagebox.showerror("Error", "Custom server name cannot be empty") + return + + # Handle name change + if new_name != selected_server: + if new_name in self.config_data["MCP_SERVERS"]: + messagebox.showerror("Error", f"Server name '{new_name}' already exists") + return + + # Copy config and delete old entry + self.config_data["MCP_SERVERS"][new_name] = self.config_data["MCP_SERVERS"][selected_server].copy() + del self.config_data["MCP_SERVERS"][selected_server] + + # Update other settings + self.config_data["MCP_SERVERS"][new_name]["enabled"] = self.custom_enable_var.get() + self.config_data["MCP_SERVERS"][new_name]["raw_config"] = self.custom_config_text.get("1.0", tk.END).strip() + + # Game window settings + self.config_data["GAME_WINDOW_CONFIG"] = { + "WINDOW_TITLE": self.window_title_var.get(), + "ENABLE_SCHEDULED_RESTART": self.restart_var.get(), + "RESTART_INTERVAL_MINUTES": self.interval_var.get(), + "GAME_EXECUTABLE_PATH": self.game_path_var.get(), + "GAME_WINDOW_X": self.pos_x_var.get(), + "GAME_WINDOW_Y": self.pos_y_var.get(), + "GAME_WINDOW_WIDTH": self.width_var.get(), + "GAME_WINDOW_HEIGHT": self.height_var.get(), + "MONITOR_INTERVAL_SECONDS": self.monitor_interval_var.get() + } + + # Validate critical settings + if "exa" in self.config_data["MCP_SERVERS"] and self.config_data["MCP_SERVERS"]["exa"]["enabled"]: + if not self.exa_key_var.get(): + messagebox.showerror("Validation Error", "Exa API Key is required when Exa server is enabled") + return + + if self.exa_type_var.get() == "local" and not self.exa_path_var.get(): + messagebox.showerror("Validation Error", "Exa Server Path is required for local server type") + return + + # Generate config.py and .env files + save_env_file(self.env_data) + generate_config_file(self.config_data, self.env_data) + + messagebox.showinfo("Success", "Settings saved successfully.\nRestart Wolf Chat for changes to take effect.") + self.destroy() + + except Exception as e: + messagebox.showerror("Error", f"An error occurred while saving settings:\n{str(e)}") + import traceback + traceback.print_exc() + +# =============================================================== +# Main Entry Point +# =============================================================== +if __name__ == "__main__": + app = WolfChatSetup() + app.mainloop() \ No newline at end of file diff --git a/config.py b/config.py deleted file mode 100644 index 56a063c..0000000 --- a/config.py +++ /dev/null @@ -1,134 +0,0 @@ -# config.py -import os -import json # Import json for building args string -from dotenv import load_dotenv # Import load_dotenv - -# --- Load environment variables from .env file --- -load_dotenv() -print("Attempted to load environment variables from .env file.") -# --- End Load --- - -# OpenAI API Configuration / OpenAI-Compatible Provider Settings -# --- Modify these lines --- -# Leave OPENAI_API_BASE_URL as None or "" to use official OpenAI -OPENAI_API_BASE_URL = "https://openrouter.ai/api/v1" # <--- For example "http://localhost:1234/v1" or your provider URL -OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") -#LLM_MODEL = "anthropic/claude-3.7-sonnet" -#LLM_MODEL = "meta-llama/llama-4-maverick" -#LLM_MODEL = "deepseek/deepseek-chat-v3-0324:free" -#LLM_MODEL = "google/gemini-2.5-flash-preview" -LLM_MODEL = "deepseek/deepseek-chat-v3-0324" # <--- Ensure this matches the model name provided by your provider - -#LLM_MODEL = "openai/gpt-4.1-nano" - -EXA_API_KEY = os.getenv("EXA_API_KEY") -MCP_REDIS_API_KEY = os.getenv("MCP_REDIS_APU_KEY") -MCP_REDIS_PATH = os.getenv("MCP_REDIS_PATH") - -# --- Dynamically build Exa server args --- -exa_config_dict = {"exaApiKey": EXA_API_KEY if EXA_API_KEY else "YOUR_EXA_KEY_MISSING"} -# Need to dump dict to JSON string, then properly escape it for cmd arg -# Using json.dumps handles internal quotes correctly. -# The outer quotes for cmd might need careful handling depending on OS / shell. -# For cmd /c on Windows, embedding escaped JSON often works like this: -exa_config_arg_string = json.dumps(json.dumps(exa_config_dict)) # Double dump for cmd escaping? Or just one? Test needed. -# Let's try single dump first, often sufficient if passed correctly by subprocess -exa_config_arg_string_single_dump = json.dumps(exa_config_dict) # Use this one - -# --- MCP Server Configuration --- -MCP_SERVERS = { - #"exa": { # Temporarily commented out to prevent blocking startup - ## "command": "cmd", - # "args": [ - # "/c", - # "npx", - # "-y", - # "@smithery/cli@latest", - # "run", - # "exa", - # "--config", - # # Pass the dynamically created config string with the environment variable key - # exa_config_arg_string_single_dump # Use the single dump variable - # ], - #}, - "exa": { - "command": "npx", - "args": [ - "C:/Users/Bigspring/AppData/Roaming/npm/exa-mcp-server", - "--tools=web_search,research_paper_search,twitter_search,company_research,crawling,competitor_finder" - ], - "env": { - "EXA_API_KEY": EXA_API_KEY - } - }, - #"github.com/modelcontextprotocol/servers/tree/main/src/memory": { - # "command": "npx", - # "args": [ - # "-y", - # "@modelcontextprotocol/server-memory" - # ], - # "disabled": False - #}, - #"redis": { - # "command": "uv", - # "args": [ - # "--directory", - # MCP_REDIS_PATH, - # "run", - # "src/main.py" - # ], - # "env": { - # "REDIS_HOST": "127.0.0.1", - # "REDIS_PORT": "6379", - # "REDIS_SSL": "False", - # "REDIS_CLUSTER_MODE": "False" - # } - # } - #"basic-memory": { - # "command": "uvx", - # "args": [ - # "basic-memory", - # "mcp" - # ], - #} - "chroma": { - "command": "uvx", - "args": [ - "chroma-mcp", - "--client-type", - "persistent", - "--data-dir", - "Z:/mcp/Server/Chroma-MCP" - ] - } - -} - -# MCP Client Configuration -MCP_CONFIRM_TOOL_EXECUTION = False # True: Confirm before execution, False: Execute automatically - -# --- Chat Logging Configuration --- -ENABLE_CHAT_LOGGING = True # True: Enable logging, False: Disable logging -LOG_DIR = "chat_logs" # Directory to store chat logs - -# Persona Configuration -PERSONA_NAME = "Wolfhart" -# PERSONA_RESOURCE_URI = "persona://wolfhart/details" # Now using local file instead - -# Game window title (used in ui_interaction.py and game_monitor.py) -WINDOW_TITLE = "Last War-Survival Game" - -# --- Game Monitor Configuration --- -ENABLE_SCHEDULED_RESTART = True # 是否啟用定時重啟遊戲功能 -RESTART_INTERVAL_MINUTES = 60 # 定時重啟的間隔時間(分鐘),預設 4 小時 -GAME_EXECUTABLE_PATH = r"C:\Users\Bigspring\AppData\Local\TheLastWar\Launch.exe" # Path to the game launcher -GAME_WINDOW_X = 50 # Target X position for the game window -GAME_WINDOW_Y = 30 # Target Y position for the game window -GAME_WINDOW_WIDTH = 600 # Target width for the game window -GAME_WINDOW_HEIGHT = 1070 # Target height for the game window -MONITOR_INTERVAL_SECONDS = 5 # How often to check the window (in seconds) - -# --- Print loaded keys for verification (Optional - BE CAREFUL!) --- -# print(f"DEBUG: Loaded OPENAI_API_KEY: {'*' * (len(OPENAI_API_KEY) - 4) + OPENAI_API_KEY[-4:] if OPENAI_API_KEY else 'Not Found'}") -print(f"DEBUG: Loaded EXA_API_KEY: {'*' * (len(EXA_API_KEY) - 4) + EXA_API_KEY[-4:] if EXA_API_KEY else 'Not Found'}") # Uncommented Exa key check -# print(f"DEBUG: Exa args: {MCP_SERVERS['exa']['args']}") diff --git a/config_template.py b/config_template.py new file mode 100644 index 0000000..3268714 --- /dev/null +++ b/config_template.py @@ -0,0 +1,62 @@ +# ==================================================================== +# Wolf Chat Configuration Template +# This file is used by setup.py to generate the final config.py +# ==================================================================== +import os +import json +from dotenv import load_dotenv + +# --- Load environment variables from .env file --- +load_dotenv() +print("Loaded environment variables from .env file.") + +# ============================================================================= +# OpenAI API Configuration / OpenAI-Compatible Provider Settings +# ============================================================================= +# Leave OPENAI_API_BASE_URL as None or "" to use official OpenAI +OPENAI_API_BASE_URL = "${OPENAI_API_BASE_URL}" +OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") +LLM_MODEL = "${LLM_MODEL}" + +# ============================================================================= +# External API Keys +# ============================================================================= +EXA_API_KEY = os.getenv("EXA_API_KEY") + +# --- Exa Configuration --- +exa_config_dict = {"exaApiKey": EXA_API_KEY if EXA_API_KEY else "YOUR_EXA_KEY_MISSING"} +exa_config_arg_string = json.dumps(exa_config_dict) + +# ============================================================================= +# MCP Server Configuration +# ============================================================================= +MCP_SERVERS = ${MCP_SERVERS} + +# ============================================================================= +# MCP Client Configuration +# ============================================================================= +MCP_CONFIRM_TOOL_EXECUTION = False # True: Confirm before execution, False: Execute automatically + +# ============================================================================= +# Chat Logging Configuration +# ============================================================================= +ENABLE_CHAT_LOGGING = ${ENABLE_CHAT_LOGGING} +LOG_DIR = "${LOG_DIR}" + +# ============================================================================= +# Persona Configuration +# ============================================================================= +PERSONA_NAME = "Wolfhart" + +# ============================================================================= +# Game Window Configuration +# ============================================================================= +WINDOW_TITLE = "${WINDOW_TITLE}" +ENABLE_SCHEDULED_RESTART = ${ENABLE_SCHEDULED_RESTART} +RESTART_INTERVAL_MINUTES = ${RESTART_INTERVAL_MINUTES} +GAME_EXECUTABLE_PATH = r"${GAME_EXECUTABLE_PATH}" +GAME_WINDOW_X = ${GAME_WINDOW_X} +GAME_WINDOW_Y = ${GAME_WINDOW_Y} +GAME_WINDOW_WIDTH = ${GAME_WINDOW_WIDTH} +GAME_WINDOW_HEIGHT = ${GAME_WINDOW_HEIGHT} +MONITOR_INTERVAL_SECONDS = ${MONITOR_INTERVAL_SECONDS} \ No newline at end of file diff --git a/install.py b/install.py new file mode 100644 index 0000000..4c03162 --- /dev/null +++ b/install.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Wolf Chat Installation Script +Installs required dependencies for Wolf Chat +""" + +import os +import sys +import subprocess +import tkinter as tk +from tkinter import ttk, messagebox + +REQUIREMENTS = [ + "openai", + "mcp", + "pyautogui", + "opencv-python", + "numpy", + "pyperclip", + "pygetwindow", + "psutil", + "pywin32", + "python-dotenv", + "keyboard" +] + +def install_requirements(progress_var=None, status_label=None, root=None): + """Install all required packages using pip""" + + total = len(REQUIREMENTS) + success_count = 0 + failed_packages = [] + + for i, package in enumerate(REQUIREMENTS): + if status_label: + status_label.config(text=f"Installing {package}...") + if progress_var: + progress_var.set((i / total) * 100) + if root: + root.update() + + try: + print(f"Installing {package}...") + # Use subprocess to run pip install + process = subprocess.run( + [sys.executable, "-m", "pip", "install", package], + check=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + print(f"Successfully installed {package}") + success_count += 1 + + except subprocess.CalledProcessError as e: + print(f"Failed to install {package}: {e}") + print(f"Error output: {e.stderr}") + failed_packages.append(package) + + except Exception as e: + print(f"Unexpected error installing {package}: {str(e)}") + failed_packages.append(package) + + # Final progress update + if progress_var: + progress_var.set(100) + + # Report results + if not failed_packages: + result_message = f"All {success_count} packages installed successfully!" + print(result_message) + if status_label: + status_label.config(text=result_message) + return True, result_message + else: + result_message = f"Installed {success_count}/{total} packages. Failed: {', '.join(failed_packages)}" + print(result_message) + if status_label: + status_label.config(text=result_message) + return False, result_message + +def run_installer_gui(): + """Run a simple GUI for the installer""" + root = tk.Tk() + root.title("Wolf Chat Installer") + root.geometry("400x200") + root.resizable(False, False) + + # Main frame + main_frame = ttk.Frame(root, padding=20) + main_frame.pack(fill=tk.BOTH, expand=True) + + # Title + title_label = ttk.Label(main_frame, text="Wolf Chat Dependency Installer", font=("", 12, "bold")) + title_label.pack(pady=(0, 10)) + + # Info text + info_text = f"This will install {len(REQUIREMENTS)} required packages for Wolf Chat." + info_label = ttk.Label(main_frame, text=info_text) + info_label.pack(pady=(0, 15)) + + # Progress bar + progress_var = tk.DoubleVar() + progress_bar = ttk.Progressbar(main_frame, variable=progress_var, maximum=100) + progress_bar.pack(fill=tk.X, pady=(0, 10)) + + # Status label + status_label = ttk.Label(main_frame, text="Ready to install...") + status_label.pack(pady=(0, 15)) + + # Install button + def start_installation(): + # Disable button during installation + install_button.config(state=tk.DISABLED) + + # Run installation in a separate thread to keep UI responsive + success, message = install_requirements(progress_var, status_label, root) + + # Show completion message + if success: + messagebox.showinfo("Installation Complete", message) + else: + messagebox.showwarning("Installation Issues", message) + + # Close the window + root.destroy() + + install_button = ttk.Button(main_frame, text="Install Dependencies", command=start_installation) + install_button.pack() + + # Start the GUI loop + root.mainloop() + +if __name__ == "__main__": + # If run directly, show GUI + run_installer_gui() \ No newline at end of file diff --git a/window-monitor-script.py b/window-monitor-script.py deleted file mode 100644 index 6e75d07..0000000 --- a/window-monitor-script.py +++ /dev/null @@ -1,121 +0,0 @@ -#!/usr/bin/env python -""" -Game Window Monitor Script - Keep game window on top and in position - -This script monitors a specified game window, ensuring it stays -always on top and at the desired screen coordinates. -""" - -import time -import argparse -import pygetwindow as gw -import win32gui -import win32con - -def find_window_by_title(window_title): - """Find the first window matching the title.""" - try: - windows = gw.getWindowsWithTitle(window_title) - if windows: - return windows[0] - except Exception as e: - # pygetwindow can sometimes raise exceptions if a window disappears - # during enumeration. Ignore these for monitoring purposes. - # print(f"Error finding window: {e}") - pass - return None - -def set_window_always_on_top(hwnd): - """Set the window to be always on top.""" - try: - win32gui.SetWindowPos(hwnd, win32con.HWND_TOPMOST, 0, 0, 0, 0, - win32con.SWP_NOMOVE | win32con.SWP_NOSIZE | win32con.SWP_SHOWWINDOW) - # print(f"Window {hwnd} set to always on top.") - except Exception as e: - print(f"Error setting window always on top: {e}") - -def move_window_if_needed(window, target_x, target_y): - """Move the window to the target coordinates if it's not already there.""" - try: - current_x, current_y = window.topleft - if current_x != target_x or current_y != target_y: - print(f"Window moved from ({current_x}, {current_y}). Moving back to ({target_x}, {target_y}).") - window.moveTo(target_x, target_y) - # print(f"Window moved to ({target_x}, {target_y}).") - except gw.PyGetWindowException as e: - # Handle cases where the window might close unexpectedly - print(f"Error accessing window properties (might be closed): {e}") - except Exception as e: - print(f"Error moving window: {e}") - -def main(): - parser = argparse.ArgumentParser(description='Game Window Monitor Tool') - parser.add_argument('--window_title', default="Last War-Survival Game", help='Game window title to monitor') - parser.add_argument('--x', type=int, default=50, help='Target window X coordinate') - parser.add_argument('--y', type=int, default=30, help='Target window Y coordinate') - parser.add_argument('--interval', type=float, default=1.0, help='Check interval in seconds') - - args = parser.parse_args() - - print(f"Monitoring window: '{args.window_title}'") - print(f"Target position: ({args.x}, {args.y})") - print(f"Check interval: {args.interval} seconds") - print("Press Ctrl+C to stop.") - - hwnd = None - last_hwnd_check_time = 0 - - try: - while True: - current_time = time.time() - window = None - - # Find window handle (HWND) - less frequent check if already found - # pygetwindow can be slow, so avoid calling it too often if we have a valid handle - if not hwnd or current_time - last_hwnd_check_time > 5: # Re-check HWND every 5 seconds - window_obj = find_window_by_title(args.window_title) - if window_obj: - # Get the HWND (window handle) needed for win32gui - # Accessing _hWnd is using an internal attribute, but it's common practice with pygetwindow - try: - hwnd = window_obj._hWnd - window = window_obj # Keep the pygetwindow object for position checks - last_hwnd_check_time = current_time - # print(f"Found window HWND: {hwnd}") - except AttributeError: - print("Could not get HWND from window object. Retrying...") - hwnd = None - else: - if hwnd: - print(f"Window '{args.window_title}' lost.") - hwnd = None # Reset hwnd if window not found - - if hwnd: - # Ensure it's always on top - set_window_always_on_top(hwnd) - - # Check and correct position using the pygetwindow object if available - # Re-find the pygetwindow object if needed for position check - if not window: - window = find_window_by_title(args.window_title) - - if window: - move_window_if_needed(window, args.x, args.y) - else: - # If we have hwnd but can't get pygetwindow object, maybe it's closing - print(f"Have HWND {hwnd} but cannot get window object for position check.") - hwnd = None # Force re-find next cycle - - else: - # print(f"Window '{args.window_title}' not found. Waiting...") - pass # Wait for the window to appear - - time.sleep(args.interval) - - except KeyboardInterrupt: - print("\nMonitoring stopped by user.") - except Exception as e: - print(f"\nAn unexpected error occurred: {e}") - -if __name__ == "__main__": - main() diff --git a/window-setup-script.py b/window-setup-script.py deleted file mode 100644 index 652a3b8..0000000 --- a/window-setup-script.py +++ /dev/null @@ -1,103 +0,0 @@ -#!/usr/bin/env python -""" -Game Window Setup Script - Adjust game window position and size - -This script will launch the game and adjust its window to a specified position and size (100,100 1280x768), -making it easier to take screenshots of UI elements for later use. -""" - -import os -import time -import subprocess -import pygetwindow as gw -import psutil -import argparse - -def is_process_running(process_name): - """Check if a specified process is currently running""" - for proc in psutil.process_iter(['name']): - if proc.info['name'].lower() == process_name.lower(): - return True - return False - -def launch_game(game_path): - """Launch the game""" - if not os.path.exists(game_path): - print(f"Error: Game executable not found at {game_path}") - return False - - print(f"Launching game: {game_path}") - subprocess.Popen(game_path) - return True - -def find_game_window(window_title, max_wait=30): - """Find the game window""" - print(f"Searching for game window: {window_title}") - - start_time = time.time() - while time.time() - start_time < max_wait: - try: - windows = gw.getWindowsWithTitle(window_title) - if windows: - return windows[0] - except Exception as e: - print(f"Error finding window: {e}") - - print("Window not found, waiting 1 second before retrying...") - time.sleep(1) - - print(f"Error: Game window not found within {max_wait} seconds") - return None - -def set_window_position_size(window, x, y, width, height): - """Set window position and size""" - try: - print(f"Adjusting window position to ({x}, {y}) and size to {width}x{height}") - window.moveTo(x, y) - window.resizeTo(width, height) - print("Window adjustment completed") - return True - except Exception as e: - print(f"Error adjusting window: {e}") - return False - -def main(): - parser = argparse.ArgumentParser(description='Game Window Setup Tool') - parser.add_argument('--launch', action='store_true', help='Whether to launch the game') - parser.add_argument('--game_path', default=r"C:\Users\Bigspring\AppData\Local\TheLastWar\Launch.exe", help='Game launcher path') - parser.add_argument('--window_title', default="Last War-Survival Game", help='Game window title') - parser.add_argument('--process_name', default="LastWar.exe", help='Game process name') - parser.add_argument('--x', type=int, default=50, help='Window X coordinate') - parser.add_argument('--y', type=int, default=30, help='Window Y coordinate') - parser.add_argument('--width', type=int, default=600, help='Window width') - parser.add_argument('--height', type=int, default=1070, help='Window height') - - args = parser.parse_args() - - # Check if game is already running - if not is_process_running(args.process_name): - if args.launch: - # Launch the game - if not launch_game(args.game_path): - return - else: - print(f"Game process {args.process_name} is not running, please launch the game first or use the --launch parameter") - return - else: - print(f"Game process {args.process_name} is already running") - - # Find game window - window = find_game_window(args.window_title) - if not window: - return - - # Set window position and size - set_window_position_size(window, args.x, args.y, args.width, args.height) - - # Display final window state - print("\nFinal window state:") - print(f"Position: ({window.left}, {window.top})") - print(f"Size: {window.width}x{window.height}") - -if __name__ == "__main__": - main()