Files
KiC-AI/plugins/ai_dialog.py
T
Jochem 9b26a2c6f7 🚀 KIC-AI Assistant v1.3.0 - Complete project with 3 AI interaction modes
 New Features:
- 3 AI Interaction Modes: Analysis (Safe), Advisory (Guided), Assistant (Interactive)
- Mode-specific AI responses and behavior
- Enhanced user interface with mode selection dropdown
- Step-by-step guidance in Advisory mode
- Interactive assistance framework for future automation

📸 Documentation:
- Added 3 screenshots showing main interface, mode selection, and PCB analysis
- Comprehensive README with mode explanations and examples
- Installation guide (INSTALL.md) with troubleshooting
- Upgrade notes and changelog

🔧 Technical Improvements:
- Enhanced AI dialog with mode detection
- Context-aware conversation system
- Improved safety warnings and user guidance
- Professional project structure with proper organization

📦 Ready for Distribution:
- Complete plugin package for KiCad 9.0+
- Compatible with Ollama LLM integration
- Supports both PCB and schematic analysis
- Three ZIP releases available for different use cases
2025-07-30 22:33:59 +02:00

619 lines
28 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import wx
import pcbnew
import threading
import json
try:
import requests
REQUESTS_AVAILABLE = True
except ImportError:
REQUESTS_AVAILABLE = False
class AIAssistantDialog(wx.Frame):
"""AI Assistant Dialog voor PCB en Schematic design hulp met 3 interactie modi"""
def __init__(self, parent, context_type="pcb"):
# Set title based on context
title = "KIC-AI Assistant - PCB" if context_type == "pcb" else "KIC-AI Assistant - Schematic"
super().__init__(parent, title=title, size=(850, 650))
self.context_type = context_type # "pcb" or "schematic"
# AI Interaction modes
self.ANALYSIS_MODE = "analysis" # Safe analysis and suggestions
self.ADVISORY_MODE = "advisory" # Step-by-step guidance with confirmation
self.ASSISTANT_MODE = "assistant" # Interactive recommendations (future: with actions)
self.interaction_mode = self.ANALYSIS_MODE # Default to safest mode
# Icon instellen (optioneel)
self.SetIcon(wx.Icon())
# Chat history voor context
self.conversation_history = []
# UI opzetten
self.init_ui()
# Centreer op scherm
self.CenterOnScreen()
# Welcome message based on context
if context_type == "schematic":
welcome_msg = ("Welcome to KIC-AI Assistant - Schematic Mode! 📋⚡\n\n"
"I can help you with:\n"
"• Schematic analysis and review\n"
"• Circuit design advice\n"
"• Component selection guidance\n"
"• Net connectivity analysis\n"
"• Symbol and annotation review\n\n"
" Interaction Modes:\n"
"• Analysis: Safe recommendations only\n"
"• Advisory: Step-by-step guidance\n"
"• Assistant: Interactive help (future automation)\n\n"
"💡 I remember our conversation!\n"
"Ask me about your circuit design!")
else:
welcome_msg = ("Welcome to KIC-AI Assistant - PCB Mode! 🔧🖥️\n\n"
"I can help you with:\n"
"• PCB layout analysis and advice\n"
"• Component placement optimization\n"
"• Routing suggestions\n"
"• Design rule checking tips\n"
"• Manufacturing considerations\n\n"
"🔧 Interaction Modes:\n"
"• Analysis: Safe recommendations only\n"
"• Advisory: Step-by-step guidance\n"
"• Assistant: Interactive help (future automation)\n\n"
"💡 I remember our conversation!\n"
"Ask me about your PCB design!")
self.add_message("🤖 KIC-AI", welcome_msg)
def init_ui(self):
"""Initialiseer de user interface"""
panel = wx.Panel(self)
# Main sizer
main_sizer = wx.BoxSizer(wx.VERTICAL)
# Mode selector
mode_sizer = wx.BoxSizer(wx.HORIZONTAL)
mode_label = wx.StaticText(panel, label="AI Interaction Mode:")
mode_label.SetFont(wx.Font(12, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD))
self.mode_choice = wx.Choice(panel, choices=[
"🔍 Analysis Mode (Safe)",
"📋 Advisory Mode (Guided)",
"🤖 Assistant Mode (Interactive)"
])
self.mode_choice.SetSelection(0) # Default to Analysis mode
mode_help = wx.Button(panel, label="?", size=(30, -1))
mode_help.SetToolTip("Click for mode explanations")
mode_sizer.Add(mode_label, 0, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 10)
mode_sizer.Add(self.mode_choice, 1, wx.EXPAND | wx.RIGHT, 5)
mode_sizer.Add(mode_help, 0, wx.ALIGN_CENTER_VERTICAL)
main_sizer.Add(mode_sizer, 0, wx.EXPAND | wx.ALL, 10)
# Chat area
self.chat_ctrl = wx.TextCtrl(
panel,
style=wx.TE_MULTILINE | wx.TE_READONLY | wx.TE_WORDWRAP
)
self.chat_ctrl.SetFont(wx.Font(14, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL))
# Input area
input_sizer = wx.BoxSizer(wx.HORIZONTAL)
self.input_ctrl = wx.TextCtrl(panel, style=wx.TE_PROCESS_ENTER)
self.input_ctrl.SetFont(wx.Font(12, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL))
self.input_ctrl.SetHint("Type your question here...")
# Buttons
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.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.clear_btn, 0, wx.RIGHT, 5)
input_sizer.Add(self.context_btn, 0)
# Status bar
self.status_text = wx.StaticText(panel, label="Ready")
self.status_text.SetFont(wx.Font(10, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_ITALIC, wx.FONTWEIGHT_NORMAL))
# Main layout
main_sizer.Add(self.chat_ctrl, 1, wx.EXPAND | wx.ALL, 10)
main_sizer.Add(input_sizer, 0, wx.EXPAND | wx.LEFT | wx.RIGHT | wx.BOTTOM, 10)
main_sizer.Add(self.status_text, 0, wx.EXPAND | wx.LEFT | wx.RIGHT | wx.BOTTOM, 10)
panel.SetSizer(main_sizer)
# Events
self.bind_events()
def bind_events(self):
"""Bind UI events"""
self.send_btn.Bind(wx.EVT_BUTTON, self.on_send)
self.analyze_btn.Bind(wx.EVT_BUTTON, self.on_analyze)
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)
self.input_ctrl.Bind(wx.EVT_TEXT_ENTER, self.on_send)
self.Bind(wx.EVT_CLOSE, self.on_close)
# Mode help button (find it in the parent)
for child in self.GetChildren():
for subchild in child.GetChildren():
if isinstance(subchild, wx.Button) and subchild.GetLabel() == "?":
subchild.Bind(wx.EVT_BUTTON, self.on_mode_help)
break
def add_message(self, sender, message):
"""Add message to chat"""
timestamp = wx.DateTime.Now().Format("%H:%M")
formatted_msg = f"[{timestamp}] {sender}:\n{message}\n\n"
self.chat_ctrl.AppendText(formatted_msg)
def set_status(self, status):
"""Update status tekst"""
self.status_text.SetLabel(status)
def on_send(self, event):
"""Send user message"""
message = self.input_ctrl.GetValue().strip()
if not message:
return
# Add message
self.add_message("🟢 You", message)
self.input_ctrl.Clear()
# Send to AI
self.process_user_message(message)
def on_analyze(self, event):
"""Analyze current PCB"""
self.set_status("Analyzing...")
self.analyze_btn.Enable(False)
# Start analyse in thread
thread = threading.Thread(target=self.analyze_pcb)
thread.daemon = True
thread.start()
def on_clear(self, event):
"""Clear chat history"""
self.chat_ctrl.Clear()
self.conversation_history.clear() # Wis ook conversatie geschiedenis
self.add_message("🤖 KIC-AI", "Chat cleared. How can I help you?")
def on_mode_change(self, event):
"""Handle AI interaction mode change"""
selection = self.mode_choice.GetSelection()
if selection == 0:
self.interaction_mode = self.ANALYSIS_MODE
mode_name = "Analysis Mode"
description = "Safe analysis and recommendations only"
elif selection == 1:
self.interaction_mode = self.ADVISORY_MODE
mode_name = "Advisory Mode"
description = "Step-by-step guidance with user confirmation"
elif selection == 2:
self.interaction_mode = self.ASSISTANT_MODE
mode_name = "Assistant Mode"
description = "Interactive recommendations and future automation"
self.add_message("⚙️ System", f"Switched to {mode_name}: {description}")
self.set_status(f"Mode: {mode_name}")
def on_mode_help(self, event):
"""Show mode explanations"""
help_text = """🔍 Analysis Mode (Safe):
• Analyzes your design and provides recommendations
• No modifications to your project
• Safe for all users and projects
📋 Advisory Mode (Guided):
• Provides step-by-step instructions
• Asks for confirmation before suggesting changes
• Guides you through design improvements
🤖 Assistant Mode (Interactive):
• Interactive design recommendations
• Future: Semi-automatic design assistance
• Advanced features for experienced users
Choose the mode that fits your experience level and comfort with AI assistance."""
self.add_message("️ Mode Help", help_text)
def on_show_context(self, event):
"""Toon huidige conversatie context"""
if not self.conversation_history:
self.add_message("️ Context", "No conversation history yet.")
return
context_info = f"📝 Memory: {len(self.conversation_history)//2} exchanges remembered\n\n"
# Show last 4 messages (2 exchanges)
recent_history = self.conversation_history[-4:] if len(self.conversation_history) > 4 else self.conversation_history
for entry in recent_history:
role_emoji = "🟢" if entry['role'] == "User" else "🤖"
content = entry['content'][:150] + "..." if len(entry['content']) > 150 else entry['content']
context_info += f"{role_emoji} {content}\n\n"
if len(self.conversation_history) > 4:
context_info += f"(+ {len(self.conversation_history)//2 - 2} older exchanges in memory)"
self.add_message("️ Context", context_info)
def on_close(self, event):
"""Sluit dialog"""
self.Destroy()
def analyze_pcb(self):
"""Analyze current design in background thread"""
try:
if self.context_type == "schematic":
# Analyze schematic
analysis = self.collect_schematic_info()
analysis_title = "📋 Schematic Analysis"
ai_prompt = f"Analyze this schematic design and provide circuit advice:\n\n{analysis}"
else:
# Analyze PCB
board = pcbnew.GetBoard()
if not board:
wx.CallAfter(self.add_message, "❌ Error", "No PCB loaded")
return
analysis = self.collect_pcb_info(board)
analysis_title = "📊 PCB Analysis"
ai_prompt = f"Analyze this PCB and provide design advice:\n\n{analysis}"
# Toon analyse
wx.CallAfter(self.add_message, analysis_title, analysis)
# Stuur naar AI voor advies
self.send_to_ai(ai_prompt, is_analysis=True)
except Exception as e:
wx.CallAfter(self.add_message, "❌ Error", f"Analysis error: {str(e)}")
finally:
wx.CallAfter(self.analyze_btn.Enable, True)
wx.CallAfter(self.set_status, "Ready")
def collect_pcb_info(self, board):
"""Collect PCB information with specific component details"""
info = []
# Board info
title = board.GetTitleBlock().GetTitle()
info.append(f"PCB: {title if title else 'Unknown'}")
# Afmetingen
bbox = board.GetBoardEdgesBoundingBox()
width_mm = bbox.GetWidth() / 1000000.0
height_mm = bbox.GetHeight() / 1000000.0
info.append(f"Dimensions: {width_mm:.1f} x {height_mm:.1f} mm")
# Componenten - GEDETAILLEERDE LIJST
footprints = list(board.GetFootprints())
info.append(f"Components: {len(footprints)}")
if footprints:
info.append("\n=== COMPONENT DETAILS ===")
# Sorteer componenten op referentie
sorted_footprints = sorted(footprints, key=lambda fp: fp.GetReference())
for fp in sorted_footprints:
ref = fp.GetReference()
value = fp.GetValue()
footprint = fp.GetFPID().GetLibItemName()
# Positie
pos = fp.GetPosition()
x_mm = pos.x / 1000000.0
y_mm = pos.y / 1000000.0
# Rotatie
rotation = fp.GetOrientation().AsDegrees()
# Layer (bovenkant/onderkant)
layer = "Top" if fp.IsFlipped() == False else "Bottom"
info.append(f"{ref}: {value}")
info.append(f" Footprint: {footprint}")
info.append(f" Position: ({x_mm:.1f}, {y_mm:.1f}) mm")
info.append(f" Rotation: {rotation:.0f}°, Layer: {layer}")
# Pads info
pads = list(fp.Pads())
if pads:
info.append(f" Pads: {len(pads)}")
info.append("") # Lege regel tussen componenten
# Nets met details
nets = board.GetNetInfo()
net_count = nets.GetNetCount()
info.append(f"\n=== NETS ({net_count}) ===")
# Toon belangrijke nets
for net_code in range(min(10, net_count)): # Eerste 10 nets
net = nets.GetNetItem(net_code)
if net:
net_name = net.GetNetname()
if net_name and net_name != "":
info.append(f"Net {net_code}: {net_name}")
# Tracks
tracks = list(board.GetTracks())
info.append(f"\nTracks: {len(tracks)}")
# Layers
layer_count = board.GetCopperLayerCount()
info.append(f"Copper layers: {layer_count}")
return "\n".join(info)
def collect_schematic_info(self):
"""Collect schematic information for analysis"""
info = []
try:
# Try to get schematic information via PCB board
# (KiCad stores schematic refs in PCB)
board = pcbnew.GetBoard()
if not board:
info.append("No board available for schematic analysis")
return "\n".join(info)
# Board/Project info
title = board.GetTitleBlock().GetTitle()
info.append(f"Project: {title if title else 'Unknown'}")
# Get footprints (which represent schematic symbols)
footprints = list(board.GetFootprints())
info.append(f"Components: {len(footprints)}")
if footprints:
info.append("\n=== SCHEMATIC COMPONENTS ===")
# Group by component type
components_by_type = {}
for fp in footprints:
ref = fp.GetReference()
value = fp.GetValue()
footprint = fp.GetFPID().GetLibItemName()
# Get component type from reference
comp_type = ref[0] if ref else "?"
if comp_type not in components_by_type:
components_by_type[comp_type] = []
components_by_type[comp_type].append({
'ref': ref,
'value': value,
'footprint': footprint
})
# Show components grouped by type
for comp_type in sorted(components_by_type.keys()):
components = components_by_type[comp_type]
info.append(f"\n{comp_type}-type components ({len(components)}):")
for comp in sorted(components, key=lambda x: x['ref']):
info.append(f" {comp['ref']}: {comp['value']} ({comp['footprint']})")
# Nets (connections between components)
nets = board.GetNetInfo()
net_count = nets.GetNetCount()
info.append(f"\n=== CONNECTIONS ({net_count} nets) ===")
# Show important nets
important_nets = []
for net_code in range(min(15, net_count)):
net = nets.GetNetItem(net_code)
if net:
net_name = net.GetNetname()
if net_name and net_name != "":
important_nets.append(net_name)
if important_nets:
info.append("Key nets:")
for net_name in important_nets:
info.append(f"{net_name}")
info.append(f"\nTotal design complexity: {len(footprints)} components, {net_count} connections")
except Exception as e:
info.append(f"Schematic analysis error: {str(e)}")
info.append("Note: Full schematic analysis requires KiCad eeschema integration")
return "\n".join(info)
def process_user_message(self, message):
"""Process user message"""
self.set_status("AI thinking...")
self.send_btn.Enable(False)
# Start AI processing in thread
thread = threading.Thread(target=self.send_to_ai, args=(message,))
thread.daemon = True
thread.start()
def send_to_ai(self, message, is_analysis=False):
"""Send message to AI (Ollama) with design context"""
if not REQUESTS_AVAILABLE:
wx.CallAfter(self.add_message, "❌ Error", "Requests module not available")
wx.CallAfter(self.send_btn.Enable, True)
wx.CallAfter(self.set_status, "Ready")
return
try:
# Ollama API endpoint
url = "http://localhost:11434/api/generate"
# Build simple conversation context
conversation_context = ""
if self.conversation_history:
# Include last 4 exchanges (8 messages) for context
recent_messages = self.conversation_history[-8:]
conversation_context = "\n\nRecent conversation:\n"
for msg in recent_messages:
conversation_context += f"{msg['role']}: {msg['content'][:200]}...\n"
conversation_context += "\n"
# Get current design context for user questions
design_context = ""
if not is_analysis:
try:
if self.context_type == "schematic":
design_context = f"\n\nCURRENT SCHEMATIC CONTEXT:\n{self.collect_schematic_info()}\n"
else:
board = pcbnew.GetBoard()
if board:
design_context = f"\n\nCURRENT PCB CONTEXT:\n{self.collect_pcb_info(board)}\n"
except:
pass # No design available
# Prepare system prompt based on context and interaction mode
mode_instructions = self.get_mode_instructions()
if is_analysis:
if self.context_type == "schematic":
system_prompt = ("You are an expert electronic circuit designer and schematic review specialist. "
"Analyze the provided schematic data thoroughly and provide specific, practical advice. "
"Look at component types, values, connections, and circuit topology. "
"Focus on circuit functionality, component selection, and design best practices. "
f"{mode_instructions} "
"Reference previous conversation if relevant.")
else:
system_prompt = ("You are an expert PCB design engineer. Analyze the provided PCB data thoroughly and provide specific, practical advice. "
"Look at individual components, their values, positions, and relationships. "
f"{mode_instructions} "
"Reference previous conversation if relevant.")
else:
if self.context_type == "schematic":
system_prompt = ("You are a helpful schematic design assistant with access to the current schematic design. "
"When users ask about specific components, circuits, or connections, reference the actual schematic data provided. "
"Give specific answers about component values, connections, and circuit topology when possible. "
"Focus on circuit functionality, component selection, and electrical design principles. "
f"{mode_instructions} "
"Remember our conversation and build upon previous topics when relevant.")
else:
system_prompt = ("You are a helpful PCB design assistant with access to the current PCB design. "
"When users ask about specific components (like resistors, capacitors, ICs), look up the actual component details from the PCB context provided. "
"Give specific answers referencing actual component values, positions, and designators when possible. "
"If asked about a specific component (e.g., 'R1', 'check this resistor'), find that component in the PCB data and provide detailed information about it. "
f"{mode_instructions} "
"Remember our conversation and build upon previous topics when relevant.")
# Build final prompt with design context
final_prompt = system_prompt + conversation_context + design_context + f"\nUser question: {message}\n\nPlease provide a specific, helpful response based on the actual design data when applicable:"
# API request
data = {
"model": "llama3.2:3b",
"prompt": final_prompt,
"stream": False,
"options": {
"temperature": 0.6,
"top_p": 0.9,
"num_ctx": 4096, # Verhoogd voor meer context
"repeat_penalty": 1.1
}
}
response = requests.post(url, json=data, timeout=60) # Langere timeout
if response.status_code == 200:
result = response.json()
ai_response = result.get('response', 'No response received').strip()
# Add to conversation history
self.conversation_history.append({"role": "User", "content": message})
self.conversation_history.append({"role": "Assistant", "content": ai_response})
# Keep only last 12 messages (6 exchanges)
if len(self.conversation_history) > 12:
self.conversation_history = self.conversation_history[-12:]
wx.CallAfter(self.add_message, "🤖 KIC-AI", ai_response)
else:
wx.CallAfter(self.add_message, "❌ Error", f"AI server error: {response.status_code}")
except requests.exceptions.ConnectionError:
wx.CallAfter(self.add_message, "❌ Error",
"Cannot connect to Ollama.\n"
"Start Ollama server:\n"
" ollama serve\n"
" ollama run llama3.2:3b")
except requests.exceptions.Timeout:
wx.CallAfter(self.add_message, "❌ Error", "AI timeout - please try again")
except Exception as e:
wx.CallAfter(self.add_message, "❌ Error", f"AI error: {str(e)}")
finally:
wx.CallAfter(self.send_btn.Enable, True)
wx.CallAfter(self.set_status, "Ready")
def get_mode_instructions(self):
"""Get instructions based on current interaction mode"""
if self.interaction_mode == self.ANALYSIS_MODE:
return ("ANALYSIS MODE: Provide only analysis and recommendations. "
"Do not suggest any direct modifications to the design. "
"Focus on observations and general advice.")
elif self.interaction_mode == self.ADVISORY_MODE:
return ("ADVISORY MODE: Provide step-by-step instructions when suggesting changes. "
"Always ask for user confirmation before proceeding with multi-step processes. "
"Format suggestions as clear actionable steps with safety warnings when needed. "
"Example: 'To remove R14: 1) First select the component 2) Press Delete 3) Update the netlist. Would you like me to guide you through this?'")
elif self.interaction_mode == self.ASSISTANT_MODE:
return ("ASSISTANT MODE: Provide interactive design assistance. "
"You can suggest specific actions and modifications. "
"When appropriate, indicate what changes could be made automatically in the future. "
"Be proactive in offering to help with implementation.")
return ""
def process_user_message(self, message):
"""Process user message with mode-specific handling"""
self.send_btn.Enable(False)
self.set_status("Processing...")
# Check for mode-specific commands
if self.interaction_mode == self.ADVISORY_MODE:
if any(word in message.lower() for word in ['remove', 'delete', 'change', 'modify', 'update']):
# Add advisory warning
self.add_message("⚠️ Advisory",
"I'll provide step-by-step guidance for this modification. "
"Please confirm each step before proceeding.")
elif self.interaction_mode == self.ASSISTANT_MODE:
if any(word in message.lower() for word in ['remove', 'delete', 'change', 'modify']):
# Add assistant note
self.add_message("🤖 Assistant",
"I'll provide interactive guidance. In future versions, "
"I may be able to help automate some of these tasks.")
# Start AI processing in thread
thread = threading.Thread(target=self.send_to_ai, args=(message,))
thread.daemon = True
thread.start()