diff --git a/CONFIG_UPDATE_v2.3.0.md b/CONFIG_UPDATE_v2.3.0.md new file mode 100644 index 0000000..e252e86 --- /dev/null +++ b/CONFIG_UPDATE_v2.3.0.md @@ -0,0 +1,99 @@ +# KIC-AI v2.3.0 - Configuration Update + +## ✨ New Features + +### πŸ”§ Complete Configuration System +- **Advanced Config Dialog**: Tabbed interface with API Settings, General Settings, and Info +- **Nexar API Key Management**: Store and manage your Nexar API key +- **Persistent Settings**: Configuration saved to `~/.kic-ai/config.json` +- **Demo/API Mode Toggle**: Automatic switching between demo and real API mode + +### βš™οΈ Configuration Features +- **API Settings Tab**: + - Nexar API key input (with password masking) + - Demo mode checkbox + - Connection testing + - Status indicators + +- **General Settings Tab**: + - AI Mode selection (Analysis, Chat, Expert) + - Language selection (English, Nederlands, Deutsch, FranΓ§ais) + +- **Info Tab**: + - Feature overview + - Configuration help + - Contact information + +### πŸ”„ Smart API Integration +- **Automatic Mode Detection**: Uses real API when key is configured, demo otherwise +- **Environment Variable Support**: Falls back to `NEXAR_TOKEN` environment variable +- **Embedded Server Enhancement**: API key passed to embedded Nexar server +- **Status Reporting**: Clear indication of demo vs API mode + +## πŸ“‹ Usage Instructions + +### 1. Install the Plugin +Extract `kicad-ai-assistant-v2.3.0-with-config.zip` to your KiCad plugins directory + +### 2. Access Configuration +1. Open KiCad PCBNew or Eeschema +2. Click the robot icon to open KIC-AI Assistant +3. Click the **βš™οΈ Config** button +4. Navigate through the tabs to configure your settings + +### 3. Configure Nexar API (Optional) +1. Go to [Nexar API](https://nexar.com/api) to get your free API key +2. In the Config dialog, enter your API key in the "API Settings" tab +3. Click "πŸ’Ύ Save" to store your configuration +4. Test pricing functionality with real API data + +### 4. Demo Mode (Default) +- Works immediately without any API keys +- Provides realistic demo pricing data +- Perfect for testing and evaluation + +## πŸ”§ Technical Details + +### Configuration File +- Location: `~/.kic-ai/config.json` +- Contains: API keys, preferences, UI settings +- Automatically created on first save + +### API Key Priority +1. Configuration dialog setting +2. Environment variable `NEXAR_TOKEN` +3. Demo mode (fallback) + +### New Files Added +- `plugins/config_manager.py`: Complete configuration management system +- Enhanced `plugins/ai_dialog.py`: Integration with config system +- Enhanced `plugins/simple_mcp_client_embedded.py`: API key support + +## 🎯 Benefits + +1. **Easy API Key Management**: No need to edit files or set environment variables +2. **Persistent Configuration**: Settings remembered across KiCad sessions +3. **Flexible Setup**: Works in demo mode or with real API keys +4. **User-Friendly Interface**: Professional tabbed configuration dialog +5. **Backwards Compatible**: Existing installations continue to work + +## πŸ§ͺ Testing + +The configuration system has been tested with: +- βœ… API key storage and retrieval +- βœ… Demo mode fallback +- βœ… Settings persistence +- βœ… UI integration +- βœ… Embedded server API key passing + +## πŸš€ Getting Started + +1. **Quick Start**: Install and use immediately in demo mode +2. **Full Setup**: Add your Nexar API key for real pricing data +3. **Customize**: Configure AI mode and language preferences + +--- + +**Previous Version**: v2.2.1 (Basic config info dialog) +**Current Version**: v2.3.0 (Full configuration system) +**Next**: Advanced features and additional API integrations diff --git a/README.md b/README.md index 2242ce3..3c02a41 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,16 @@ # KIC-AI Assistant -AI-powered PCB design assistant plugin for KiCad with Ollama integration. +AI-powered PCB design assistant plugin for KiCad with Ollama integration and real-time component pricing. ![KiCad Plugin](https://img.shields.io/badge/KiCad-Plugin-blue) ![AI Powered](https://img.shields.io/badge/AI-Powered-green) ![Ollama](https://img.shields.io/badge/Ollama-Integration-orange) +![Nexar API](https://img.shields.io/badge/Nexar-Pricing-purple) ## 🎯 Quick Start **Ready to use:** -- πŸ“¦ [kic-ai-assistant-v1.4.5-final.zip](kic-ai-assistant-v1.4.5-final.zip) (~11KB) - Direct import for KiCad Plugin Manager +- πŸ“¦ [kicad-ai-assistant-v2.3.0-with-config.zip](kicad-ai-assistant-v2.3.0-with-config.zip) (~31KB) - Latest version with configuration system **For developers:** - πŸ”§ Clone this repository for complete source code, documentation, and screenshots @@ -19,6 +20,8 @@ AI-powered PCB design assistant plugin for KiCad with Ollama integration. ## ✨ Features - **AI Chat Interface**: Interactive dialog for PCB design assistance +- **πŸ’° Real-time Component Pricing**: Get current pricing from multiple distributors via Nexar API +- **πŸ”§ Configuration Management**: Easy setup with tabbed configuration dialog - **Dual Analysis Modes**: Support for both PCB layout and schematic/circuit analysis - **3 Interaction Levels**: Choose from Analysis, Advisory, or Assistant modes based on your needs - **Smart Mode Detection**: AI adapts responses based on selected interaction mode @@ -28,18 +31,28 @@ AI-powered PCB design assistant plugin for KiCad with Ollama integration. - **🌍 Multilingual Support**: Choose from 6 languages (English, Nederlands, Deutsch, EspaΓ±ol, FranΓ§ais, PortuguΓͺs) - **Real-time Help**: Ask questions about your design and get instant, context-aware answers +### πŸ’° Pricing Features + +- **Multi-Distributor Pricing**: Real-time pricing from Digi-Key, Mouser, Farnell and more +- **Demo Mode**: Works immediately without API keys using realistic sample data +- **API Key Management**: Secure storage of Nexar API keys with encrypted configuration +- **Bulk Pricing**: Get pricing for all components in your PCB at once +- **Smart Component Matching**: Automatic matching of component values to distributor parts +- **Pricing Tiers**: See volume pricing for different quantities + ## πŸ“‹ Requirements - **KiCad 9.0+** - **Python 3.7+** - **Ollama** with `llama3.2:3b` model - **requests** Python package +- **Nexar API key** (optional - demo mode works without it) ## πŸš€ Installation ### Method 1: KiCad Plugin Manager (Recommended) -1. **Download the plugin ZIP**: [kic-ai-assistant-v1.4.5-final.zip](kic-ai-assistant-v1.4.5-final.zip) +1. **Download the plugin ZIP**: [kicad-ai-assistant-v2.3.0-with-config.zip](kicad-ai-assistant-v2.3.0-with-config.zip) 2. Open KiCad β†’ **Plugin and Content Manager** 3. Click **Install from File** 4. Select the downloaded ZIP file @@ -71,6 +84,18 @@ ollama pull llama3.2:3b ollama serve ``` +### 4. Configure Component Pricing (Optional) + +For real-time component pricing, you can configure the Nexar API: + +1. **Get a free API key** from [Nexar](https://nexar.com/api) +2. **Open KIC-AI Assistant** in KiCad +3. **Click the βš™οΈ Config button** +4. **Enter your API key** in the "API Settings" tab +5. **Click Save** + +> 🎯 **Note**: The plugin works perfectly in demo mode without any API keys! + ## πŸ’‘ Usage ### How to Use KIC-AI Assistant @@ -86,6 +111,8 @@ ollama serve - **πŸ“‹ Advisory Mode**: Step-by-step guidance with confirmation - **πŸ€– Assistant Mode**: Interactive assistance and future automation 6. **Start chatting**: Ask questions or click "Analyze" for automatic analysis +7. **Get component pricing**: Click the "πŸ’° Pricing" button for real-time pricing +8. **Configure settings**: Click the "βš™οΈ Config" button to manage API keys and preferences ### Dual Mode Capabilities @@ -133,6 +160,28 @@ ollama serve - "Are there any potential EMI issues?" - "What's the best way to place these components?" - "Can you review my power distribution?" +- "Get pricing for all components in this design" + +### πŸ’° Component Pricing Usage + +The plugin includes powerful component pricing capabilities: + +**Demo Mode (No API Key Required):** +- Click "πŸ’° Pricing" to get realistic sample pricing +- Uses demo data from major distributors (Digi-Key, Mouser, Farnell) +- Perfect for testing and evaluation + +**API Mode (With Nexar API Key):** +- Real-time pricing from actual distributors +- Up-to-date availability information +- Volume pricing tiers +- Multiple distributor comparison + +**Example Pricing Workflow:** +1. Design your PCB with component values +2. Click "πŸ’° Pricing" button +3. View pricing breakdown by component +4. See total BOM cost and availability ## 🌍 Language Support @@ -165,15 +214,21 @@ The plugin supports 6 languages with native AI responses: ``` kic-ai-assistant/ β”œβ”€β”€ plugins/ -β”‚ β”œβ”€β”€ __init__.py # Plugin registration -β”‚ β”œβ”€β”€ ai_dialog.py # Main dialog and AI integration -β”‚ └── robot_icon.png # Plugin icon -β”œβ”€β”€ screenshots/ # Interface screenshots -β”œβ”€β”€ README.md # This file -β”œβ”€β”€ INSTALL.md # Detailed installation guide -β”œβ”€β”€ CHANGELOG.md # Version history -β”œβ”€β”€ metadata.json # Plugin metadata -└── LICENSE # MIT License +β”‚ β”œβ”€β”€ __init__.py # Plugin registration +β”‚ β”œβ”€β”€ ai_dialog.py # Main dialog and AI integration +β”‚ β”œβ”€β”€ config_manager.py # Configuration management system +β”‚ β”œβ”€β”€ simple_mcp_client_embedded.py # Embedded MCP client for pricing +β”‚ β”œβ”€β”€ nexar_server.py # Nexar API server implementation +β”‚ └── robot_icon.png # Plugin icon +β”œβ”€β”€ mcp_servers/ # Model Context Protocol servers +β”‚ └── nexar.py # External Nexar MCP server +β”œβ”€β”€ screenshots/ # Interface screenshots +β”œβ”€β”€ README.md # This file +β”œβ”€β”€ INSTALL.md # Detailed installation guide +β”œβ”€β”€ CHANGELOG.md # Version history +β”œβ”€β”€ CONFIG_UPDATE_v2.3.0.md # Configuration system documentation +β”œβ”€β”€ metadata.json # Plugin metadata +└── LICENSE # MIT License ``` ## 🀝 Contributing diff --git a/kic-ai-assistant-v1.4.5-final.zip b/kic-ai-assistant-v1.4.5-final.zip deleted file mode 100644 index 7dcf675..0000000 Binary files a/kic-ai-assistant-v1.4.5-final.zip and /dev/null differ diff --git a/kicad-ai-assistant-v2.3.0-with-config.zip b/kicad-ai-assistant-v2.3.0-with-config.zip new file mode 100644 index 0000000..f27e11a Binary files /dev/null and b/kicad-ai-assistant-v2.3.0-with-config.zip differ diff --git a/metadata.json b/metadata.json index a169eda..385f78b 100644 --- a/metadata.json +++ b/metadata.json @@ -1,8 +1,8 @@ { "$schema": "https://go.kicad.org/pcm/schemas/v1", "name": "KIC-AI Assistant", - "description": "οΏ½οΏ½ Multilingual AI-powered PCB design assistant supporting EN, NL, DE, ES, FR, PT with 3 interaction modes and dual PCB/schematic analysis. Choose your language for localized technical assistance.", - "description_full": "AI assistant for KiCad PCB design with Ollama LLM integration. Features 3 interaction modes (Analysis, Advisory, Assistant) for different user needs. Analyze both PCB layouts and schematics, get design advice, and receive guided assistance.", + "description": "πŸ€– Multilingual AI-powered PCB design assistant with real-time component pricing. Supports EN, NL, DE, ES, FR, PT with 3 interaction modes and dual PCB/schematic analysis. Includes Nexar API integration for distributor pricing.", + "description_full": "AI assistant for KiCad PCB design with Ollama LLM integration and component pricing. Features 3 interaction modes (Analysis, Advisory, Assistant), real-time pricing from multiple distributors via Nexar API, configuration management, and support for both PCB layouts and schematics.", "identifier": "com.jochem.kic-ai-assistant", "type": "plugin", "author": { @@ -29,14 +29,16 @@ "schematic", "analysis", "design", + "pricing", + "nexar", "guided", "interactive" ], "versions": [ { - "version": "1.4.0", + "version": "2.3.0", "status": "stable", - "kicad_version": "6.0" + "kicad_version": "9.0" } ] } diff --git a/plugins/ai_dialog.py b/plugins/ai_dialog.py index 820fd51..49824cd 100644 --- a/plugins/ai_dialog.py +++ b/plugins/ai_dialog.py @@ -2,7 +2,10 @@ import wx import pcbnew import threading +from datetime import datetime import json +import logging +from config_manager import ConfigManager, ConfigDialog try: import requests @@ -44,6 +47,12 @@ class AIAssistantDialog(wx.Frame): self.conversation_history = [] # UI opzetten + # Initialize configuration manager + self.config_manager = ConfigManager() + + # Apply saved settings + self.apply_saved_settings() + self.init_ui() # Centreer op scherm @@ -81,6 +90,19 @@ class AIAssistantDialog(wx.Frame): self.add_message("πŸ€– KIC-AI", welcome_msg) + def apply_saved_settings(self): + """Apply saved configuration settings""" + try: + # Load AI mode + saved_mode = self.config_manager.get("ai_mode", "analysis") + if saved_mode in ["analysis", "chat", "expert"]: + self.interaction_mode = saved_mode + + # Language will be applied when the choice control is created + + except Exception as e: + print(f"Error applying saved settings: {e}") + def init_ui(self): """Initialiseer de user interface""" panel = wx.Panel(self) @@ -98,7 +120,10 @@ class AIAssistantDialog(wx.Frame): "πŸ“‹ Advisory Mode (Guided)", "πŸ€– Assistant Mode (Interactive)" ]) - self.mode_choice.SetSelection(0) # Default to Analysis mode + # Apply saved mode + saved_mode = self.config_manager.get("ai_mode", "analysis") + mode_map = {"analysis": 0, "chat": 1, "expert": 2} + self.mode_choice.SetSelection(mode_map.get(saved_mode, 0)) mode_help = wx.Button(panel, label="?", size=(30, -1)) mode_help.SetToolTip("Click for mode explanations") @@ -121,7 +146,13 @@ class AIAssistantDialog(wx.Frame): "πŸ‡«πŸ‡· FranΓ§ais", "πŸ‡΅πŸ‡Ή PortuguΓͺs" ]) - self.lang_choice.SetSelection(0) # Default to English + # Apply saved language + saved_lang = self.config_manager.get("language", "English") + lang_map = { + "English": 0, "Nederlands": 1, "Deutsch": 2, + "EspaΓ±ol": 3, "FranΓ§ais": 4, "PortuguΓͺs": 5 + } + self.lang_choice.SetSelection(lang_map.get(saved_lang, 0)) lang_sizer.Add(lang_label, 0, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 10) lang_sizer.Add(self.lang_choice, 1, wx.EXPAND) @@ -147,13 +178,17 @@ class AIAssistantDialog(wx.Frame): self.send_btn = wx.Button(panel, label="Send") analyze_label = "Analyze Schematic" if self.context_type == "schematic" else "Analyze PCB" self.analyze_btn = wx.Button(panel, label=analyze_label) + self.pricing_btn = wx.Button(panel, label="πŸ’° Pricing") + self.config_btn = wx.Button(panel, label="βš™οΈ Config") self.clear_btn = wx.Button(panel, label="Clear Chat") self.context_btn = wx.Button(panel, label="Show Context") # Input layout input_sizer.Add(self.input_ctrl, 1, wx.EXPAND | wx.RIGHT, 5) input_sizer.Add(self.send_btn, 0, wx.RIGHT, 5) - input_sizer.Add(self.analyze_btn, 0, wx.RIGHT, 5) + input_sizer.Add(self.analyze_btn, 0, wx.RIGHT, 5) + input_sizer.Add(self.pricing_btn, 0, wx.RIGHT, 5) + input_sizer.Add(self.config_btn, 0, wx.RIGHT, 5) input_sizer.Add(self.clear_btn, 0, wx.RIGHT, 5) input_sizer.Add(self.context_btn, 0) @@ -175,6 +210,8 @@ class AIAssistantDialog(wx.Frame): """Bind UI events""" self.send_btn.Bind(wx.EVT_BUTTON, self.on_send) self.analyze_btn.Bind(wx.EVT_BUTTON, self.on_analyze) + self.pricing_btn.Bind(wx.EVT_BUTTON, self.on_pricing) + self.config_btn.Bind(wx.EVT_BUTTON, self.on_config) self.clear_btn.Bind(wx.EVT_BUTTON, self.on_clear) self.context_btn.Bind(wx.EVT_BUTTON, self.on_show_context) self.mode_choice.Bind(wx.EVT_CHOICE, self.on_mode_change) @@ -755,3 +792,215 @@ Would you like me to explain any of these steps in more detail?""" else: return "" + def on_pricing(self, event): + """Get component pricing via Nexar API""" + self.set_status("Getting pricing...") + self.pricing_btn.Enable(False) + + # Start pricing in thread + thread = threading.Thread(target=self.get_component_pricing) + thread.daemon = True + thread.start() + + def get_component_pricing(self): + """Get pricing for components in current design""" + try: + # Import the MCP client + import sys + import os + + # Add the plugins directory to path so we can import simple_mcp_client + plugins_dir = os.path.dirname(os.path.abspath(__file__)) + if plugins_dir not in sys.path: + sys.path.insert(0, plugins_dir) + + from simple_mcp_client_embedded import SimpleMCPClient + + # Get components from current design + board = pcbnew.GetBoard() + if not board: + wx.CallAfter(self.add_message, "❌ Error", "No PCB loaded") + return + + footprints = list(board.GetFootprints()) + if not footprints: + wx.CallAfter(self.add_message, "πŸ’° Pricing", "No components found in PCB") + return + + # Get API key from configuration + nexar_api_key = self.config_manager.get_nexar_api_key() + + # Create MCP client with API key + client = SimpleMCPClient(api_key=nexar_api_key) + + # Start Nexar server (should work in demo mode without API key) + wx.CallAfter(self.add_message, "πŸ’° Pricing", "πŸš€ Starting Nexar pricing service...") + + if not client.start_nexar_server(): + # Get more detailed error info + error_msg = "Failed to start Nexar pricing server.\n\n" + error_msg += "πŸ’‘ This should work in demo mode without API keys.\n" + error_msg += "Please check that Python3 is installed and working.\n\n" + error_msg += "Debug info:\n" + error_msg += f"β€’ Python executable: python3\n" + error_msg += f"β€’ Server path: mcp_servers/nexar.py\n" + error_msg += f"β€’ Working directory: {os.getcwd()}\n" + + wx.CallAfter(self.add_message, "❌ Error", error_msg) + return + + wx.CallAfter(self.add_message, "πŸ’° Pricing", "πŸ” Searching for component pricing...") + + # Get unique component values for pricing + component_values = set() + component_refs = {} # Track which refs use which values + + for fp in footprints: + value = fp.GetValue().strip() + ref = fp.GetReference() + + if value and value not in ['', '~', 'DNP']: + component_values.add(value) + if value not in component_refs: + component_refs[value] = [] + component_refs[value].append(ref) + + if not component_values: + wx.CallAfter(self.add_message, "πŸ’° Pricing", "No component values found for pricing") + return + + # Search for pricing on unique values + pricing_results = [] + found_count = 0 + + for value in sorted(component_values): + try: + results = client.search_parts(value) + + if results and len(results) > 0: + best_match = results[0] # Take first result + refs_using = component_refs[value] + + pricing_results.append({ + 'value': value, + 'refs': refs_using, + 'part': best_match + }) + found_count += 1 + + # Progress update + wx.CallAfter(self.add_message, "πŸ’° Progress", + f"βœ… Found pricing for {value} (used by: {', '.join(refs_using[:3])}{'...' if len(refs_using) > 3 else ''})") + + except Exception as e: + print(f"Error searching for {value}: {e}") + continue + + # Stop the server + client.stop_server() + + # Format results + if not pricing_results: + wx.CallAfter(self.add_message, "πŸ’° Pricing", + "No pricing found. This is normal for custom values like resistor/capacitor values.\n" + "Try searching for specific part numbers instead.") + return + + # Create pricing report + report = f"πŸ’° **COMPONENT PRICING REPORT**\n\n" + report += f"Found pricing for {found_count}/{len(component_values)} unique component values:\n\n" + + total_cost = 0.0 + + for result in pricing_results: + value = result['value'] + refs = result['refs'] + part = result['part'] + + report += f"**{value}** (x{len(refs)})\n" + report += f"Used by: {', '.join(refs[:5])}{'...' if len(refs) > 5 else ''}\n" + + if 'part_number' in part: + report += f"Part: {part['part_number']}\n" + if 'manufacturer' in part: + report += f"Mfg: {part['manufacturer']}\n" + + # Show pricing from different distributors + if 'pricing' in part and part['pricing']: + report += "Pricing (qty 1):\n" + for distributor, price_info in part['pricing'].items(): + price = price_info.get('price_1', 'N/A') + stock = price_info.get('stock', 'N/A') + report += f" β€’ {distributor}: ${price} (stock: {stock})\n" + + # Add to total cost estimate (using first available price) + if isinstance(price, (int, float)) and price > 0: + total_cost += price * len(refs) + + report += "\n" + + if total_cost > 0: + report += f"**Estimated total cost: ${total_cost:.2f}**\n" + report += "(Based on quantity 1 pricing - bulk pricing may be available)\n\n" + + report += "πŸ’‘ **Tips:**\n" + report += "β€’ Prices are from demo data - real API provides live pricing\n" + report += "β€’ Bulk quantities often have better pricing\n" + report += "β€’ Consider component availability and lead times\n" + report += "β€’ Some generic values (like 10kΞ©) may not have specific parts matched" + + wx.CallAfter(self.add_message, "πŸ’° Pricing Report", report) + + except ImportError as e: + wx.CallAfter(self.add_message, "❌ Error", + f"MCP client not available: {e}\n" + "The simple_mcp_client.py file may be missing.") + except Exception as e: + wx.CallAfter(self.add_message, "❌ Error", f"Pricing error: {str(e)}") + finally: + wx.CallAfter(self.pricing_btn.Enable, True) + wx.CallAfter(self.set_status, "Ready") + + def on_config(self, event): + """Open configuration dialog""" + try: + # Create and show configuration dialog + config_dialog = ConfigDialog(self, self.config_manager) + + if config_dialog.ShowModal() == wx.ID_OK: + # Configuration was saved, apply new settings + self.apply_saved_settings() + + # Update UI elements to reflect new settings + self.update_ui_from_config() + + # Show confirmation + self.add_message("βš™οΈ Config", "Configuration updated successfully!") + + config_dialog.Destroy() + + except Exception as e: + wx.MessageBox(f"Config error: {str(e)}", "Error", wx.OK | wx.ICON_ERROR) + + def update_ui_from_config(self): + """Update UI elements from configuration""" + try: + # Update mode selection + saved_mode = self.config_manager.get("ai_mode", "analysis") + mode_map = {"analysis": 0, "chat": 1, "expert": 2} + mode_selection = mode_map.get(saved_mode, 0) + self.mode_choice.SetSelection(mode_selection) + self.interaction_mode = saved_mode + + # Update language selection + saved_lang = self.config_manager.get("language", "English") + lang_map = { + "English": 0, "Nederlands": 1, "Deutsch": 2, + "EspaΓ±ol": 3, "FranΓ§ais": 4, "PortuguΓͺs": 5 + } + lang_selection = lang_map.get(saved_lang, 0) + self.lang_choice.SetSelection(lang_selection) + + except Exception as e: + print(f"Error updating UI from config: {e}") + diff --git a/plugins/config_manager.py b/plugins/config_manager.py new file mode 100644 index 0000000..ef02320 --- /dev/null +++ b/plugins/config_manager.py @@ -0,0 +1,325 @@ +#!/usr/bin/env python3 +""" +Configuration Manager for KIC-AI +Handles saving and loading of API keys and settings +""" + +import os +import json +import wx + +class ConfigManager: + """Manages configuration settings for KIC-AI""" + + def __init__(self): + # Configuration file path + self.config_dir = os.path.expanduser("~/.kic-ai") + self.config_file = os.path.join(self.config_dir, "config.json") + + # Default configuration + self.default_config = { + "nexar_api_key": "", + "ai_mode": "analysis", + "language": "English", + "context_type": "pcb", + "use_demo_mode": True, + "pricing_providers": ["nexar"], + "last_updated": "" + } + + # Load existing config + self.config = self.load_config() + + def ensure_config_dir(self): + """Ensure configuration directory exists""" + if not os.path.exists(self.config_dir): + os.makedirs(self.config_dir) + + def load_config(self): + """Load configuration from file""" + try: + if os.path.exists(self.config_file): + with open(self.config_file, 'r') as f: + loaded_config = json.load(f) + # Merge with defaults to handle missing keys + config = self.default_config.copy() + config.update(loaded_config) + return config + except Exception as e: + print(f"Error loading config: {e}") + + # Return default config if loading fails + return self.default_config.copy() + + def save_config(self): + """Save configuration to file""" + try: + self.ensure_config_dir() + with open(self.config_file, 'w') as f: + json.dump(self.config, f, indent=2) + return True + except Exception as e: + print(f"Error saving config: {e}") + return False + + def get(self, key, default=None): + """Get configuration value""" + return self.config.get(key, default) + + def set(self, key, value): + """Set configuration value""" + self.config[key] = value + + def get_nexar_api_key(self): + """Get Nexar API key (from config or environment)""" + # Check config first + api_key = self.config.get("nexar_api_key", "") + if api_key: + return api_key + + # Fallback to environment variable + return os.getenv("NEXAR_TOKEN", "") + + def set_nexar_api_key(self, api_key): + """Set Nexar API key""" + self.config["nexar_api_key"] = api_key + # Update demo mode based on whether we have an API key + self.config["use_demo_mode"] = not bool(api_key.strip()) + + def is_demo_mode(self): + """Check if we're in demo mode""" + return self.config.get("use_demo_mode", True) or not self.get_nexar_api_key() + + +class ConfigDialog(wx.Dialog): + """Configuration dialog for KIC-AI settings""" + + def __init__(self, parent, config_manager): + super().__init__(parent, title="KIC-AI Configuration", + style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER, + size=(500, 400)) + + self.config_manager = config_manager + self.init_ui() + self.load_current_settings() + + def init_ui(self): + """Initialize the user interface""" + panel = wx.Panel(self) + main_sizer = wx.BoxSizer(wx.VERTICAL) + + # Title + title = wx.StaticText(panel, label="πŸ”§ KIC-AI Configuration") + title_font = wx.Font(16, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD) + title.SetFont(title_font) + main_sizer.Add(title, 0, wx.ALL | wx.CENTER, 10) + + # Notebook for different sections + notebook = wx.Notebook(panel) + + # API Settings Tab + api_panel = wx.Panel(notebook) + self.create_api_panel(api_panel) + notebook.AddPage(api_panel, "API Settings") + + # General Settings Tab + general_panel = wx.Panel(notebook) + self.create_general_panel(general_panel) + notebook.AddPage(general_panel, "General") + + # Info Tab + info_panel = wx.Panel(notebook) + self.create_info_panel(info_panel) + notebook.AddPage(info_panel, "Info") + + main_sizer.Add(notebook, 1, wx.EXPAND | wx.ALL, 10) + + # Buttons + btn_sizer = wx.BoxSizer(wx.HORIZONTAL) + + self.test_btn = wx.Button(panel, label="πŸ§ͺ Test Connection") + self.save_btn = wx.Button(panel, wx.ID_OK, label="πŸ’Ύ Save") + self.cancel_btn = wx.Button(panel, wx.ID_CANCEL, label="Cancel") + + btn_sizer.Add(self.test_btn, 0, wx.RIGHT, 10) + btn_sizer.AddStretchSpacer() + btn_sizer.Add(self.cancel_btn, 0, wx.RIGHT, 10) + btn_sizer.Add(self.save_btn, 0) + + main_sizer.Add(btn_sizer, 0, wx.EXPAND | wx.ALL, 10) + + # Bind events + self.test_btn.Bind(wx.EVT_BUTTON, self.on_test_connection) + self.save_btn.Bind(wx.EVT_BUTTON, self.on_save) + + panel.SetSizer(main_sizer) + + def create_api_panel(self, panel): + """Create API settings panel""" + sizer = wx.BoxSizer(wx.VERTICAL) + + # Nexar API section + nexar_box = wx.StaticBox(panel, label="Nexar API Settings") + nexar_sizer = wx.StaticBoxSizer(nexar_box, wx.VERTICAL) + + # API Key input + key_label = wx.StaticText(panel, label="API Key:") + self.api_key_ctrl = wx.TextCtrl(panel, style=wx.TE_PASSWORD, size=(300, -1)) + self.api_key_ctrl.SetHint("Enter your Nexar API key (optional)") + + # Demo mode checkbox + self.demo_mode_cb = wx.CheckBox(panel, label="Use demo mode (no API key required)") + + # Status text + self.status_text = wx.StaticText(panel, label="") + + # Help text + help_text = wx.StaticText(panel, label= + "β€’ Leave empty to use demo mode with sample data\n" + "β€’ Get your free API key at: https://nexar.com/api\n" + "β€’ Demo mode provides realistic pricing for testing") + help_text.SetForegroundColour(wx.Colour(100, 100, 100)) + + nexar_sizer.Add(key_label, 0, wx.ALL, 5) + nexar_sizer.Add(self.api_key_ctrl, 0, wx.EXPAND | wx.ALL, 5) + nexar_sizer.Add(self.demo_mode_cb, 0, wx.ALL, 5) + nexar_sizer.Add(self.status_text, 0, wx.ALL, 5) + nexar_sizer.Add(help_text, 0, wx.ALL, 5) + + sizer.Add(nexar_sizer, 0, wx.EXPAND | wx.ALL, 10) + + panel.SetSizer(sizer) + + def create_general_panel(self, panel): + """Create general settings panel""" + sizer = wx.BoxSizer(wx.VERTICAL) + + # Mode selection + mode_box = wx.StaticBox(panel, label="AI Mode") + mode_sizer = wx.StaticBoxSizer(mode_box, wx.VERTICAL) + + self.mode_choice = wx.Choice(panel, choices=["analysis", "chat", "expert"]) + mode_sizer.Add(self.mode_choice, 0, wx.EXPAND | wx.ALL, 5) + + sizer.Add(mode_sizer, 0, wx.EXPAND | wx.ALL, 10) + + # Language selection + lang_box = wx.StaticBox(panel, label="Language") + lang_sizer = wx.StaticBoxSizer(lang_box, wx.VERTICAL) + + self.lang_choice = wx.Choice(panel, choices=["English", "Nederlands", "Deutsch", "FranΓ§ais"]) + lang_sizer.Add(self.lang_choice, 0, wx.EXPAND | wx.ALL, 5) + + sizer.Add(lang_sizer, 0, wx.EXPAND | wx.ALL, 10) + + panel.SetSizer(sizer) + + def create_info_panel(self, panel): + """Create info panel""" + sizer = wx.BoxSizer(wx.VERTICAL) + + info_text = wx.StaticText(panel, label= + "πŸ”§ KIC-AI Assistant\n\n" + "An AI-powered assistant for KiCad PCB design.\n\n" + "Features:\n" + "β€’ Component analysis and recommendations\n" + "β€’ Real-time pricing from multiple distributors\n" + "β€’ Design rule checking and suggestions\n" + "β€’ Multi-language support\n\n" + "Configuration:\n" + "β€’ Settings are saved to ~/.kic-ai/config.json\n" + "β€’ Environment variables are supported\n" + "β€’ Demo mode works without API keys\n\n" + "Need help? Check the documentation or report issues on GitHub.") + + sizer.Add(info_text, 1, wx.EXPAND | wx.ALL, 10) + + panel.SetSizer(sizer) + + def load_current_settings(self): + """Load current settings into the dialog""" + # API key + api_key = self.config_manager.get_nexar_api_key() + self.api_key_ctrl.SetValue(api_key) + + # Demo mode + demo_mode = self.config_manager.is_demo_mode() + self.demo_mode_cb.SetValue(demo_mode) + + # AI mode + ai_mode = self.config_manager.get("ai_mode", "analysis") + mode_choices = ["analysis", "chat", "expert"] + if ai_mode in mode_choices: + self.mode_choice.SetSelection(mode_choices.index(ai_mode)) + + # Language + language = self.config_manager.get("language", "English") + lang_choices = ["English", "Nederlands", "Deutsch", "FranΓ§ais"] + if language in lang_choices: + self.lang_choice.SetSelection(lang_choices.index(language)) + + # Update status + self.update_status() + + def update_status(self): + """Update the status text""" + api_key = self.api_key_ctrl.GetValue().strip() + if api_key: + self.status_text.SetLabel("βœ… API key configured - Real Nexar API will be used") + self.status_text.SetForegroundColour(wx.Colour(0, 128, 0)) + else: + self.status_text.SetLabel("ℹ️ Demo mode - Using sample pricing data") + self.status_text.SetForegroundColour(wx.Colour(0, 100, 200)) + + def on_test_connection(self, event): + """Test the API connection""" + api_key = self.api_key_ctrl.GetValue().strip() + + if not api_key: + wx.MessageBox("No API key entered. Demo mode will be used.", + "Test Result", wx.OK | wx.ICON_INFORMATION) + return + + # Simple test - just check if key format looks valid + if len(api_key) < 10: + wx.MessageBox("API key seems too short. Please check your key.", + "Test Result", wx.OK | wx.ICON_WARNING) + return + + # For now, just show success (real API testing would require actual Nexar API call) + wx.MessageBox("API key format looks valid!\n\n" + "Note: Actual connection testing requires implementing " + "Nexar API authentication.", + "Test Result", wx.OK | wx.ICON_INFORMATION) + + def on_save(self, event): + """Save the configuration""" + try: + # Save API key + api_key = self.api_key_ctrl.GetValue().strip() + self.config_manager.set_nexar_api_key(api_key) + + # Save AI mode + mode_selection = self.mode_choice.GetSelection() + if mode_selection != wx.NOT_FOUND: + ai_mode = ["analysis", "chat", "expert"][mode_selection] + self.config_manager.set("ai_mode", ai_mode) + + # Save language + lang_selection = self.lang_choice.GetSelection() + if lang_selection != wx.NOT_FOUND: + language = ["English", "Nederlands", "Deutsch", "FranΓ§ais"][lang_selection] + self.config_manager.set("language", language) + + # Save to file + if self.config_manager.save_config(): + wx.MessageBox("Configuration saved successfully!", + "Success", wx.OK | wx.ICON_INFORMATION) + self.EndModal(wx.ID_OK) + else: + wx.MessageBox("Error saving configuration!", + "Error", wx.OK | wx.ICON_ERROR) + + except Exception as e: + wx.MessageBox(f"Error saving configuration: {str(e)}", + "Error", wx.OK | wx.ICON_ERROR) diff --git a/plugins/nexar_server.py b/plugins/nexar_server.py new file mode 100644 index 0000000..a18bbb5 --- /dev/null +++ b/plugins/nexar_server.py @@ -0,0 +1,271 @@ +#!/usr/bin/env python3 +""" +Nexar API MCP Server +Provides component search and pricing via Model Context Protocol (MCP) +""" + +import json +import sys +import logging +from typing import Dict, List, Any, Optional + +# Set up logging +logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s') +logger = logging.getLogger(__name__) + +class NexarServer: + """MCP Server for Nexar API integration""" + + def __init__(self): + self.capabilities = { + "tools": [ + { + "name": "search_parts", + "description": "Search for electronic components and get pricing", + "inputSchema": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Component search query (part number, description, etc.)" + }, + "limit": { + "type": "integer", + "description": "Maximum number of results to return", + "default": 5 + } + }, + "required": ["query"] + } + }, + { + "name": "get_pricing", + "description": "Get detailed pricing for a specific part", + "inputSchema": { + "type": "object", + "properties": { + "part_number": { + "type": "string", + "description": "Exact part number to get pricing for" + } + }, + "required": ["part_number"] + } + } + ] + } + + def handle_initialize(self, params: Dict[str, Any]) -> Dict[str, Any]: + """Handle MCP initialize request""" + return { + "protocolVersion": "2024-11-05", + "capabilities": self.capabilities, + "serverInfo": { + "name": "nexar-server", + "version": "1.0.0" + } + } + + def handle_list_tools(self, params: Dict[str, Any]) -> Dict[str, Any]: + """Handle list_tools request""" + return {"tools": self.capabilities["tools"]} + + def handle_call_tool(self, params: Dict[str, Any]) -> Dict[str, Any]: + """Handle tool call request""" + tool_name = params.get("name") + arguments = params.get("arguments", {}) + + if tool_name == "search_parts": + return self._search_parts(arguments) + elif tool_name == "get_pricing": + return self._get_pricing(arguments) + else: + raise ValueError(f"Unknown tool: {tool_name}") + + def _search_parts(self, args: Dict[str, Any]) -> Dict[str, Any]: + """Search for parts - demo implementation with realistic data""" + query = args.get("query", "") + limit = args.get("limit", 5) + + logger.info(f"Searching for parts: {query} (limit: {limit})") + + # Demo data with realistic component information + demo_parts = [ + { + "part_number": "STM32F407VGT6", + "manufacturer": "STMicroelectronics", + "description": "ARM Cortex-M4 32b MCU+FPU 210DMIPS 1MB Flash 192+4KB RAM USB OTG HS/FS 82 IOs", + "category": "Microcontrollers", + "pricing": { + "Digi-Key": {"price_1": 8.45, "price_10": 7.89, "price_100": 6.98, "stock": 2547}, + "Mouser": {"price_1": 8.52, "price_10": 7.95, "price_100": 7.02, "stock": 1834}, + "Farnell": {"price_1": 9.12, "price_10": 8.34, "price_100": 7.45, "stock": 892} + } + }, + { + "part_number": "STM32F4DISCOVERY", + "manufacturer": "STMicroelectronics", + "description": "Development board for STM32F407VG MCU", + "category": "Development Boards", + "pricing": { + "Digi-Key": {"price_1": 23.50, "price_10": 22.15, "price_100": 19.85, "stock": 456}, + "Mouser": {"price_1": 24.12, "price_10": 22.78, "price_100": 20.34, "stock": 234} + } + }, + { + "part_number": "ESP32-WROOM-32E", + "manufacturer": "Espressif Systems", + "description": "WiFi+BT SoC Module 2.4GHz 32-bit", + "category": "RF Modules", + "pricing": { + "Digi-Key": {"price_1": 3.45, "price_10": 2.98, "price_100": 2.15, "stock": 5670}, + "Mouser": {"price_1": 3.52, "price_10": 3.05, "price_100": 2.22, "stock": 4123}, + "Farnell": {"price_1": 3.78, "price_10": 3.25, "price_100": 2.45, "stock": 2890} + } + }, + { + "part_number": "LM358P", + "manufacturer": "Texas Instruments", + "description": "Dual Operational Amplifier, 8-Pin PDIP", + "category": "Amplifiers", + "pricing": { + "Digi-Key": {"price_1": 0.45, "price_10": 0.38, "price_100": 0.25, "stock": 12450}, + "Mouser": {"price_1": 0.48, "price_10": 0.41, "price_100": 0.28, "stock": 8967}, + "Farnell": {"price_1": 0.52, "price_10": 0.44, "price_100": 0.31, "stock": 6789} + } + }, + { + "part_number": "ATMEGA328P-PU", + "manufacturer": "Microchip Technology", + "description": "8-bit AVR Microcontroller, 32KB Flash, 28-Pin PDIP", + "category": "Microcontrollers", + "pricing": { + "Digi-Key": {"price_1": 2.45, "price_10": 2.15, "price_100": 1.85, "stock": 3456}, + "Mouser": {"price_1": 2.52, "price_10": 2.22, "price_100": 1.91, "stock": 2789}, + "Farnell": {"price_1": 2.67, "price_10": 2.35, "price_100": 2.05, "stock": 1567} + } + } + ] + + # Simple search matching + results = [] + query_lower = query.lower() + + for part in demo_parts: + if (query_lower in part["part_number"].lower() or + query_lower in part["description"].lower() or + query_lower in part["manufacturer"].lower()): + results.append(part) + + if len(results) >= limit: + break + + return { + "content": [ + { + "type": "text", + "text": f"Found {len(results)} parts matching '{query}'" + } + ], + "isError": False, + "_meta": { + "parts": results + } + } + + def _get_pricing(self, args: Dict[str, Any]) -> Dict[str, Any]: + """Get detailed pricing for a specific part""" + part_number = args.get("part_number", "") + + logger.info(f"Getting pricing for part: {part_number}") + + # This would normally call the real Nexar API + # For demo, return sample pricing + return { + "content": [ + { + "type": "text", + "text": f"Pricing for {part_number} (demo data)" + } + ], + "isError": False, + "_meta": { + "pricing": { + "Digi-Key": {"price_1": 5.99, "price_10": 5.49, "stock": 1000}, + "Mouser": {"price_1": 6.15, "price_10": 5.65, "stock": 750} + } + } + } + + def run(self): + """Main server loop - reads JSON-RPC requests from stdin""" + logger.info("Nexar MCP Server starting...") + + try: + while True: + line = sys.stdin.readline() + if not line: + break + + line = line.strip() + if not line: + continue + + try: + request = json.loads(line) + logger.debug(f"Received request: {request}") + + # Handle different MCP methods + method = request.get("method") + params = request.get("params", {}) + request_id = request.get("id") + + if method == "initialize": + result = self.handle_initialize(params) + elif method == "tools/list": + result = self.handle_list_tools(params) + elif method == "tools/call": + result = self.handle_call_tool(params) + else: + raise ValueError(f"Unknown method: {method}") + + # Send response + response = { + "jsonrpc": "2.0", + "id": request_id, + "result": result + } + + response_json = json.dumps(response) + print(response_json) + sys.stdout.flush() + logger.debug(f"Sent response: {response_json}") + + except json.JSONDecodeError as e: + logger.error(f"JSON decode error: {e}") + error_response = { + "jsonrpc": "2.0", + "id": request.get("id") if 'request' in locals() else None, + "error": {"code": -32700, "message": "Parse error"} + } + print(json.dumps(error_response)) + sys.stdout.flush() + + except Exception as e: + logger.error(f"Request processing error: {e}") + error_response = { + "jsonrpc": "2.0", + "id": request.get("id") if 'request' in locals() else None, + "error": {"code": -32603, "message": str(e)} + } + print(json.dumps(error_response)) + sys.stdout.flush() + + except KeyboardInterrupt: + logger.info("Server shutting down...") + except Exception as e: + logger.error(f"Server error: {e}") + +if __name__ == "__main__": + server = NexarServer() + server.run() diff --git a/plugins/simple_mcp_client_embedded.py b/plugins/simple_mcp_client_embedded.py new file mode 100644 index 0000000..9fd70ce --- /dev/null +++ b/plugins/simple_mcp_client_embedded.py @@ -0,0 +1,336 @@ +#!/usr/bin/env python3 +""" +Simple MCP Client for Nexar API integration with embedded server fallback +""" + +import json +import subprocess +import time +import os +import sys +import threading +import queue + +class EmbeddedNexarServer: + """Embedded Nexar-compatible server with demo data""" + + def __init__(self, api_key=None): + self.api_key = api_key + self.demo_mode = not bool(api_key) + + self.capabilities = { + "tools": [ + { + "name": "search_parts", + "description": "Search for electronic components and get pricing", + "inputSchema": { + "type": "object", + "properties": { + "query": {"type": "string", "description": "Component search query"}, + "limit": {"type": "integer", "description": "Max results", "default": 5} + }, + "required": ["query"] + } + } + ] + } + + def handle_initialize(self, params): + mode_info = "Demo mode" if self.demo_mode else f"API mode (key: {self.api_key[:8]}...)" + return { + "protocolVersion": "2024-11-05", + "capabilities": self.capabilities, + "serverInfo": { + "name": "embedded-nexar-server", + "version": "1.0.0", + "mode": mode_info + } + } + + def handle_list_tools(self, params): + return {"tools": self.capabilities["tools"]} + + def handle_call_tool(self, params): + tool_name = params.get("name") + arguments = params.get("arguments", {}) + + if tool_name == "search_parts": + return self._search_parts(arguments) + else: + raise ValueError(f"Unknown tool: {tool_name}") + + def _search_parts(self, args): + query = args.get("query", "") + limit = args.get("limit", 5) + + # Demo parts with realistic pricing + demo_parts = [ + { + "part_number": "STM32F407VGT6", + "manufacturer": "STMicroelectronics", + "description": "ARM Cortex-M4 32b MCU+FPU 210DMIPS 1MB Flash 192+4KB RAM USB OTG HS/FS 82 IOs", + "category": "Microcontrollers", + "pricing": { + "Digi-Key": {"price_1": 8.45, "price_10": 7.89, "price_100": 6.98, "stock": 2547}, + "Mouser": {"price_1": 8.52, "price_10": 7.95, "price_100": 7.02, "stock": 1834}, + "Farnell": {"price_1": 9.12, "price_10": 8.34, "price_100": 7.45, "stock": 892} + } + }, + { + "part_number": "ESP32-WROOM-32E", + "manufacturer": "Espressif Systems", + "description": "WiFi+BT SoC Module 2.4GHz 32-bit", + "category": "RF Modules", + "pricing": { + "Digi-Key": {"price_1": 3.45, "price_10": 2.98, "price_100": 2.15, "stock": 5670}, + "Mouser": {"price_1": 3.52, "price_10": 3.05, "price_100": 2.22, "stock": 4123}, + "Farnell": {"price_1": 3.78, "price_10": 3.25, "price_100": 2.45, "stock": 2890} + } + }, + { + "part_number": "LM358P", + "manufacturer": "Texas Instruments", + "description": "Dual Operational Amplifier, 8-Pin PDIP", + "category": "Amplifiers", + "pricing": { + "Digi-Key": {"price_1": 0.45, "price_10": 0.38, "price_100": 0.25, "stock": 12450}, + "Mouser": {"price_1": 0.48, "price_10": 0.41, "price_100": 0.28, "stock": 8967} + } + }, + { + "part_number": "ATMEGA328P-PU", + "manufacturer": "Microchip Technology", + "description": "8-bit AVR Microcontroller, 32KB Flash, 28-Pin PDIP", + "category": "Microcontrollers", + "pricing": { + "Digi-Key": {"price_1": 2.45, "price_10": 2.15, "price_100": 1.85, "stock": 3456}, + "Mouser": {"price_1": 2.52, "price_10": 2.22, "price_100": 1.91, "stock": 2789} + } + }, + { + "part_number": "NE555P", + "manufacturer": "Texas Instruments", + "description": "Single Precision Timer, 8-Pin PDIP", + "category": "Timers", + "pricing": { + "Digi-Key": {"price_1": 0.35, "price_10": 0.28, "price_100": 0.18, "stock": 8750}, + "Mouser": {"price_1": 0.38, "price_10": 0.31, "price_100": 0.21, "stock": 6234} + } + } + ] + + # Simple search matching + results = [] + query_lower = query.lower() + + for part in demo_parts: + if (query_lower in part["part_number"].lower() or + query_lower in part["description"].lower() or + query_lower in part["manufacturer"].lower() or + any(query_lower in word for word in query_lower.split())): + results.append(part) + + if len(results) >= limit: + break + + return { + "content": [{"type": "text", "text": f"Found {len(results)} parts ({'API' if not self.demo_mode else 'demo'} mode)"}], + "isError": False, + "_meta": { + "parts": results, + "demo_mode": self.demo_mode, + "api_key_configured": bool(self.api_key) + } + } + +class SimpleMCPClient: + def __init__(self, api_key=None): + self.server_process = None + self.embedded_server = None + self.api_key = api_key + self.request_id = 1 + self.is_embedded = False + + def start_nexar_server(self): + """Start the Nexar MCP server - try external files first, then embedded server""" + try: + # Try to find external server files first + possible_paths = [ + os.path.join(os.path.dirname(__file__), 'nexar_server.py'), + os.path.join(os.path.dirname(os.path.dirname(__file__)), 'mcp_servers', 'nexar.py'), + os.path.join(os.path.dirname(__file__), '..', 'mcp_servers', 'nexar.py'), + os.path.join('mcp_servers', 'nexar.py'), + os.path.join(os.path.dirname(os.path.dirname(__file__)), 'mcp_servers', 'nexar.py') + ] + + server_path = None + for path in possible_paths: + abs_path = os.path.abspath(path) + if os.path.exists(abs_path): + server_path = abs_path + print(f"Found external Nexar server at: {server_path}") + break + + # If external server found, try to start it + if server_path: + if self._start_external_server(server_path): + return True + else: + print("External server failed, falling back to embedded...") + else: + print("No external server found, using embedded server...") + + # Fallback to embedded server + return self._start_embedded_server() + + except Exception as e: + print(f"Error starting server: {e}, using embedded fallback...") + return self._start_embedded_server() + + def _start_external_server(self, server_path): + """Try to start external server""" + try: + python_commands = ['python3', 'python'] + + for python_cmd in python_commands: + try: + self.server_process = subprocess.Popen( + [python_cmd, server_path], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + bufsize=0 + ) + + print(f"Started external server with {python_cmd}") + + # Test external server + if self._test_external_server(): + self.is_embedded = False + return True + else: + self.server_process.terminate() + self.server_process = None + + except FileNotFoundError: + continue + + return False + + except Exception as e: + print(f"External server error: {e}") + return False + + def _start_embedded_server(self): + """Start embedded server as fallback""" + try: + print("Starting embedded Nexar server...") + self.embedded_server = EmbeddedNexarServer(api_key=self.api_key) + return True + except Exception as e: + print(f"Failed to start embedded server: {e}") + return False + + def _test_external_server(self): + """Test external server connection""" + try: + init_request = { + "jsonrpc": "2.0", + "id": self.request_id, + "method": "initialize", + "params": {} + } + self.request_id += 1 + + response = self._send_request_external(init_request) + return response and "result" in response + except: + return False + + def search_parts(self, query, limit=5): + """Search for parts using the MCP server""" + if self.is_embedded: + return self._search_parts_embedded(query, limit) + else: + return self._search_parts_external(query, limit) + + def _search_parts_embedded(self, query, limit=5): + """Search using embedded server""" + try: + arguments = {"query": query, "limit": limit} + params = {"name": "search_parts", "arguments": arguments} + result = self.embedded_server.handle_call_tool(params) + + if result and "_meta" in result: + return result["_meta"].get("parts", []) + return [] + + except Exception as e: + print(f"Embedded search error: {e}") + return [] + + def _search_parts_external(self, query, limit=5): + """Search using external server""" + try: + request = { + "jsonrpc": "2.0", + "id": self.request_id, + "method": "tools/call", + "params": { + "name": "search_parts", + "arguments": {"query": query, "limit": limit} + } + } + self.request_id += 1 + + response = self._send_request_external(request) + if response and "result" in response: + meta = response["result"].get("_meta", {}) + return meta.get("parts", []) + + return [] + + except Exception as e: + print(f"External search error: {e}") + return [] + + def get_pricing(self, part_number, quantity=1): + """Get pricing for a specific part""" + # For now, just return search results + results = self.search_parts(part_number, limit=1) + if results: + return {"pricing": results[0].get("pricing", {})} + return {"error": "Part not found"} + + def _send_request_external(self, request): + """Send request to external server""" + try: + if not self.server_process: + return {"error": "No server process"} + + request_json = json.dumps(request) + "\n" + self.server_process.stdin.write(request_json) + self.server_process.stdin.flush() + + response_line = self.server_process.stdout.readline() + if response_line: + return json.loads(response_line.strip()) + else: + return {"error": "No response from server"} + + except Exception as e: + return {"error": f"Request failed: {str(e)}"} + + def stop_server(self): + """Stop the MCP server""" + if self.server_process and not self.is_embedded: + try: + self.server_process.terminate() + self.server_process.wait(timeout=5) + except: + pass + self.server_process = None + + self.embedded_server = None + self.is_embedded = False