From 72e828e5c90a0ffadbe547415015d9bf94aeaefe Mon Sep 17 00:00:00 2001 From: Hariom Thakur Date: Wed, 7 Jan 2026 16:45:18 +0000 Subject: [PATCH 01/34] Refactor Application.py and integrate chatbot features Refactor Application.py to improve structure and readability. Added chatbot integration and updated comments for clarity. --- src/frontEnd/Application.py | 512 ++++++++++++++++++------------------ 1 file changed, 259 insertions(+), 253 deletions(-) diff --git a/src/frontEnd/Application.py b/src/frontEnd/Application.py index 73c626013..21360bd2e 100644 --- a/src/frontEnd/Application.py +++ b/src/frontEnd/Application.py @@ -1,34 +1,56 @@ # ========================================================================= -# FILE: Application.py +# FILE: Application.py # -# USAGE: --- +# USAGE: --- # -# DESCRIPTION: This main file use to start the Application +# DESCRIPTION: This main file use to start the Application # -# OPTIONS: --- -# REQUIREMENTS: --- -# BUGS: --- -# NOTES: --- -# AUTHOR: Fahim Khan, fahim.elex@gmail.com -# MAINTAINED: Rahul Paknikar, rahulp@iitb.ac.in -# Sumanto Kar, sumantokar@iitb.ac.in -# Pranav P, pranavsdreams@gmail.com -# ORGANIZATION: eSim Team at FOSSEE, IIT Bombay -# CREATED: Tuesday 24 February 2015 -# REVISION: Wednesday 07 June 2023 +# OPTIONS: --- +# REQUIREMENTS: --- +# BUGS: --- +# NOTES: --- +# AUTHOR: Fahim Khan, fahim.elex@gmail.com +# MAINTAINED: Rahul Paknikar, rahulp@iitb.ac.in +# Sumanto Kar, sumantokar@iitb.ac.in +# Pranav P, pranavsdreams@gmail.com +# ORGANIZATION: eSim Team at FOSSEE, IIT Bombay +# CREATED: Tuesday 24 February 2015 +# REVISION: Wednesday 07 June 2023 # ========================================================================= import os import sys import traceback -import webbrowser -if os.name == 'nt': - from frontEnd import pathmagic # noqa:F401 - init_path = '' -else: - import pathmagic # noqa:F401 - init_path = '../../' +current_dir = os.path.dirname(os.path.abspath(__file__)) +if current_dir not in sys.path: + sys.path.insert(0, current_dir) + +# ================= GLOBAL STATE ================= +CHATBOT_AVAILABLE = False + + +try: + if os.name == 'nt': + from frontEnd import pathmagic + init_path = '' + else: + import pathmagic + init_path = '../../' + print(f"[BOOT] pathmagic imported successfully, init_path='{init_path}'") +except ImportError as e: + print(f"[BOOT WARNING] Could not import pathmagic: {e}") + print("[BOOT WARNING] Using fallback path settings") + + if os.name == 'nt': + init_path = '' + else: + init_path = '../../' + + +os.environ['DISABLE_MODEL_SOURCE_CHECK'] = 'True' +print("[BOOT] DISABLE_MODEL_SOURCE_CHECK set to True") + from PyQt5 import QtGui, QtCore, QtWidgets from PyQt5.Qt import QSize @@ -41,34 +63,38 @@ from projManagement.Kicad import Kicad from projManagement.Validation import Validation from projManagement import Worker +from PyQt5.QtCore import QTimer -# Its our main window of application. +try: + from frontEnd.Chatbot import ChatbotGUI + CHATBOT_AVAILABLE = True +except ImportError: + CHATBOT_AVAILABLE = False + print("Chatbot module not available. Chatbot features will be disabled.") class Application(QtWidgets.QMainWindow): + """This class initializes all objects used in this file.""" global project_name simulationEndSignal = QtCore.pyqtSignal(QtCore.QProcess.ExitStatus, int) + errorDetectedSignal = QtCore.pyqtSignal(str) def __init__(self, *args): """Initialize main Application window.""" - # Calling __init__ of super class QtWidgets.QMainWindow.__init__(self, *args) - # Set slot for simulation end signal to plot simulation data + # Set slot for simulation end signal self.simulationEndSignal.connect(self.plotSimulationData) + self.errorDetectedSignal.connect(self.handleError) - #the plotFlag - self.plotFlag = False - - # Creating require Object self.obj_workspace = Workspace.Workspace() self.obj_Mainview = MainView() self.obj_kicad = Kicad(self.obj_Mainview.obj_dockarea) self.obj_appconfig = Appconfig() self.obj_validation = Validation() - # Initialize all widget + self.setCentralWidget(self.obj_Mainview) self.initToolBar() @@ -86,16 +112,47 @@ def __init__(self, *args): self.systemTrayIcon.setIcon(QtGui.QIcon(init_path + 'images/logo.png')) self.systemTrayIcon.setVisible(True) + def initChatbot(self): + """Initialize chatbot with proper context.""" + if not CHATBOT_AVAILABLE: + return + + try: + self.chatbot_window = ChatbotGUI(self) + + self.errorDetectedSignal.connect(self.auto_debug_error) + + except Exception as e: + print(f"Failed to initialize chatbot: {e}") + + def auto_debug_error(self, error_message): + """Automatically send simulation errors to chatbot.""" + if not CHATBOT_AVAILABLE or not hasattr(self, 'chatbot_window'): + return + + self.projDir = self.obj_appconfig.current_project["ProjectName"] + if not self.projDir: + return + + # Look for error logs + error_log_path = os.path.join(self.projDir, "ngspice_error.log") + if os.path.exists(error_log_path): + + if (hasattr(self, 'chatbot_window') and + self.chatbot_window.isVisible()): + + QTimer.singleShot(1000, lambda: self.send_error_to_chatbot(error_log_path)) + def initToolBar(self): """ This function initializes Tool Bars. It setups the icons, short-cuts and defining functonality for: - - Top-tool-bar (New project, Open project, Close project, \ - Mode switch, Help option) - - Left-tool-bar (Open Schematic, Convert KiCad to Ngspice, \ - Simuation, Model Editor, Subcircuit, NGHDL, Modelica \ - Converter, OM Optimisation) + - Top-tool-bar (New project, Open project, Close project, \ + Mode switch, Help option) + - Left-tool-bar (Open Schematic, Convert KiCad to Ngspice, \ + Simuation, Model Editor, Subcircuit, NGHDL, Modelica \ + Converter, OM Optimisation) """ # Top Tool bar self.newproj = QtWidgets.QAction( @@ -133,23 +190,14 @@ def initToolBar(self): self.helpfile.setShortcut('Ctrl+H') self.helpfile.triggered.connect(self.help_project) - # added devDocs logo and called functions - self.devdocs = QtWidgets.QAction( - QtGui.QIcon(init_path + 'images/dev_docs.png'), - 'Dev Docs', self - ) - self.devdocs.setShortcut('Ctrl+D') - self.devdocs.triggered.connect(self.dev_docs) - self.topToolbar = self.addToolBar('Top Tool Bar') self.topToolbar.addAction(self.newproj) self.topToolbar.addAction(self.openproj) self.topToolbar.addAction(self.closeproj) self.topToolbar.addAction(self.wrkspce) self.topToolbar.addAction(self.helpfile) - self.topToolbar.addAction(self.devdocs) - # ## This part is meant for SoC Generation which is currently ## + # ## This part is meant for SoC Generation which is currently ## # ## under development and will be will be required in future. ## # self.soc = QtWidgets.QToolButton(self) # self.soc.setText('Generate SoC') @@ -160,7 +208,7 @@ def initToolBar(self): # '

Thank you for your patience!!!' # ) # self.soc.setStyleSheet(" \ - # QWidget { border-radius: 15px; border: 1px \ + # QWidget { border-radius: 15px; border: 1px \ # solid gray; padding: 10px; margin-left: 20px; } \ # ") # self.soc.clicked.connect(self.showSoCRelease) @@ -201,7 +249,7 @@ def initToolBar(self): QtGui.QIcon(init_path + 'images/ngspice.png'), 'Simulate', self ) - self.ngspice.triggered.connect(self.plotFlagPopBox) + self.ngspice.triggered.connect(self.open_ngspice) self.model = QtWidgets.QAction( QtGui.QIcon(init_path + 'images/model.png'), @@ -240,9 +288,17 @@ def initToolBar(self): self.conToeSim = QtWidgets.QAction( QtGui.QIcon(init_path + 'images/icon.png'), - 'Schematic converter', self + 'Schematics converter', self ) self.conToeSim.triggered.connect(self.open_conToeSim) + # ... existing actions ... + + self.copilot_action = QtWidgets.QAction( + QtGui.QIcon(init_path + 'images/chatbot.png'), # Ensure this icon exists or use fallback + 'eSim Copilot', self + ) + self.copilot_action.setToolTip("AI Circuit Assistant") + self.copilot_action.triggered.connect(self.openChatbot) # Adding Action Widget to tool bar self.lefttoolbar = QtWidgets.QToolBar('Left ToolBar') @@ -257,47 +313,28 @@ def initToolBar(self): self.lefttoolbar.addAction(self.omedit) self.lefttoolbar.addAction(self.omoptim) self.lefttoolbar.addAction(self.conToeSim) + self.lefttoolbar.addSeparator() + self.lefttoolbar.addAction(self.copilot_action) self.lefttoolbar.setOrientation(QtCore.Qt.Vertical) self.lefttoolbar.setIconSize(QSize(40, 40)) - def plotFlagPopBox(self): - """This function displays a pop-up box with message- Do you want Ngspice plots? and oprions Yes and NO. - - If the user clicks on Yes, both the NgSpice and python plots are displayed and if No is clicked then only the python plots.""" - - msg_box = QtWidgets.QMessageBox(self) - msg_box.setWindowTitle("Ngspice Plots") - msg_box.setText("Do you want Ngspice plots?") - - yes_button = msg_box.addButton("Yes", QtWidgets.QMessageBox.YesRole) - no_button = msg_box.addButton("No", QtWidgets.QMessageBox.NoRole) - - msg_box.exec_() - - if msg_box.clickedButton() == yes_button: - self.plotFlag = True - else: - self.plotFlag = False - - self.open_ngspice() - def closeEvent(self, event): ''' This function closes the ongoing program (process). When exit button is pressed a Message box pops out with \ exit message and buttons 'Yes', 'No'. - 1. If 'Yes' is pressed: - - check that program (process) in procThread_list \ - (a list made in Appconfig.py): + 1. If 'Yes' is pressed: + - check that program (process) in procThread_list \ + (a list made in Appconfig.py): - - if available it terminates that program. - - if the program (process) is not available, \ - then check it in process_obj (a list made in \ - Appconfig.py) and if found, it closes the program. + - if available it terminates that program. + - if the program (process) is not available, \ + then check it in process_obj (a list made in \ + Appconfig.py) and if found, it closes the program. - 2. If 'No' is pressed: - - the program just continues as it was doing earlier. + 2. If 'No' is pressed: + - the program just continues as it was doing earlier. ''' exit_msg = "Are you sure you want to exit the program?" exit_msg += " All unsaved data will be lost." @@ -327,6 +364,11 @@ def closeEvent(self, event): self.project.close() except BaseException: pass + + # Close chatbot if open + if CHATBOT_AVAILABLE and hasattr(self, 'chatbot_window') and self.chatbot_window.isVisible(): + self.chatbot_window.close() + event.accept() self.systemTrayIcon.showMessage('Exit', 'eSim is Closed.') @@ -362,6 +404,41 @@ def new_project(self): except BaseException: pass + + def openChatbot(self): + if not CHATBOT_AVAILABLE: + QtWidgets.QMessageBox.warning( + self, "Error", + "Chatbot unavailable. Please check backend dependencies." + ) + return + + try: + if not hasattr(self, "chatbotDock") or self.chatbotDock is None: + from frontEnd.Chatbot import createchatbotdock + self.chatbotDock = createchatbotdock(self) + + self.chatbotDock.setAllowedAreas(QtCore.Qt.NoDockWidgetArea) + self.addDockWidget(QtCore.Qt.RightDockWidgetArea, self.chatbotDock) + + self.chatbotDock.setFloating(True) + g = self.geometry() + self.chatbotDock.resize(450, 600) + self.chatbotDock.move(g.x() + g.width() - 470, g.y() + 50) + self.chatbotDock.show() + self.chatbotDock.raise_() + + # Keep a reference to the widget for error‑debug integration + self.chatbot_window = self.chatbotDock.widget() + + # No need to call set_project_context here anymore + + except Exception as e: + print("Error opening chatbot:", e) + QtWidgets.QMessageBox.warning( + self, "Error", f"Could not open chatbot: {str(e)}" + ) + def open_project(self): """This project call Open Project Info class.""" print("Function : Open Project") @@ -378,12 +455,12 @@ def close_project(self): This function closes the saved project. It first checks whether project (file) is present in list. - - If present: - - it first kills that process-id. - - closes that file. - - Shows message "Current project is closed" + - If present: + - it first kills that process-id. + - closes that file. + - Shows message "Current project is closed" - - If not present: pass + - If not present: pass """ print("Function : Close Project") current_project = self.obj_appconfig.current_project['ProjectName'] @@ -415,29 +492,20 @@ def change_workspace(self): def help_project(self): """ This function opens usermanual in dockarea. - - It prints the message ""Function : Help"" - - Uses print_info() method of class Appconfig - from Configuration/Appconfig.py file. - - Call method usermanual() from ./DockArea.py. + - It prints the message ""Function : Help"" + - Uses print_info() method of class Appconfig + from Configuration/Appconfig.py file. + - Call method usermanual() from ./DockArea.py. """ print("Function : Help") self.obj_appconfig.print_info('Help is called') print("Current Project is : ", self.obj_appconfig.current_project) self.obj_Mainview.obj_dockarea.usermanual() - def dev_docs(self): - """ - This function guides the user to readthedocs website for the developer docs - """ - print("Function : DevDocs") - self.obj_appconfig.print_info('DevDocs is called') - print("Current Project is : ", self.obj_appconfig.current_project) - webbrowser.open("https://esim.readthedocs.io/en/latest/index.html") - @QtCore.pyqtSlot(QtCore.QProcess.ExitStatus, int) def plotSimulationData(self, exitCode, exitStatus): """Enables interaction for new simulation and - displays the plotter dock where graphs can be plotted. + displays the plotter dock where graphs can be plotted. """ self.ngspice.setEnabled(True) self.conversion.setEnabled(True) @@ -459,6 +527,40 @@ def plotSimulationData(self, exitCode, exitStatus): self.obj_appconfig.print_error('Exception Message : ' + str(e)) + self.errorDetectedSignal.emit("Simulation failed.") + + def handleError(self): + """Slot called when a simulation error happens.""" + if not CHATBOT_AVAILABLE: + return + + self.projDir = self.obj_appconfig.current_project["ProjectName"] + if not self.projDir: + return + + error_log_path = os.path.join(self.projDir, "ngspice_error.log") + + # Only try to send if chatbot is visible and has debug_error() + if (hasattr(self, 'chatbot_window') and + self.chatbot_window.isVisible() and + hasattr(self.chatbot_window, 'debug_error')): + # Use a small delay to ensure the error log is written + QTimer.singleShot( + 1000, + lambda: self.send_error_to_chatbot(error_log_path) + ) + + def send_error_to_chatbot(self, error_log_path: str): + """Send ngspice error log to chatbot for debugging.""" + try: + if os.path.exists(error_log_path): + with open(error_log_path, 'r') as f: + error_content = f.read() + if error_content.strip(): + self.chatbot_window.debug_error(error_log_path) + except Exception as e: + print(f"Error sending to chatbot: {e}") + def open_ngspice(self): """This Function execute ngspice on current project.""" projDir = self.obj_appconfig.current_project["ProjectName"] @@ -468,20 +570,24 @@ def open_ngspice(self): ngspiceNetlist = os.path.join(projDir, projName + ".cir.out") if not os.path.isfile(ngspiceNetlist): - print( - "Netlist file (*.cir.out) not found." - ) + print("Netlist file (*.cir.out) not found.") self.msg = QtWidgets.QErrorMessage() self.msg.setModal(True) self.msg.setWindowTitle("Error Message") - self.msg.showMessage( - 'Netlist (*.cir.out) not found.' - ) + self.msg.showMessage('Netlist (*.cir.out) not found.') self.msg.exec_() return + # Pass chatbot reference into ngspiceEditor + chatbot_ref = ( + self.chatbot_window + if CHATBOT_AVAILABLE and hasattr(self, "chatbot_window") + else None + ) + self.obj_Mainview.obj_dockarea.ngspiceEditor( - projName, ngspiceNetlist, self.simulationEndSignal, self.plotFlag) + projName, ngspiceNetlist, self.simulationEndSignal, chatbot_ref + ) self.ngspice.setEnabled(False) self.conversion.setEnabled(False) @@ -504,9 +610,9 @@ def open_subcircuit(self): When 'subcircuit' icon is clicked wich is present in left-tool-bar of main page: - - Meassge shown on screen "Subcircuit editor is called". - - 'subcircuiteditor()' function is called using object - 'obj_dockarea' of class 'Mainview'. + - Meassge shown on screen "Subcircuit editor is called". + - 'subcircuiteditor()' function is called using object + 'obj_dockarea' of class 'Mainview'. """ print("Function : Subcircuit editor") self.obj_appconfig.print_info('Subcircuit editor is called') @@ -517,10 +623,10 @@ def open_nghdl(self): This function calls NGHDL option in left-tool-bar. It uses validateTool() method from Validation.py: - - If 'nghdl' is present in executables list then - it passes command 'nghdl -e' to WorkerThread class of - Worker.py. - - If 'nghdl' is not present, then it shows error message. + - If 'nghdl' is present in executables list then + it passes command 'nghdl -e' to WorkerThread class of + Worker.py. + - If 'nghdl' is not present, then it shows error message. """ print("Function : NGHDL") self.obj_appconfig.print_info('NGHDL is called') @@ -545,9 +651,9 @@ def open_makerchip(self): When 'subcircuit' icon is clicked wich is present in left-tool-bar of main page: - - Meassge shown on screen "Subcircuit editor is called". - - 'subcircuiteditor()' function is called using object - 'obj_dockarea' of class 'Mainview'. + - Meassge shown on screen "Subcircuit editor is called". + - 'subcircuiteditor()' function is called using object + 'obj_dockarea' of class 'Mainview'. """ print("Function : Makerchip and Verilator to Ngspice Converter") self.obj_appconfig.print_info('Makerchip is called') @@ -559,9 +665,9 @@ def open_modelEditor(self): When model editor icon is clicked which is present in left-tool-bar of main page: - - Meassge shown on screen "Model editor is called". - - 'modeleditor()' function is called using object - 'obj_dockarea' of class 'Mainview'. + - Meassge shown on screen "Model editor is called". + - 'modeleditor()' function is called using object + 'obj_dockarea' of class 'Mainview'. """ print("Function : Model editor") self.obj_appconfig.print_info('Model editor is called') @@ -588,8 +694,8 @@ def open_OMedit(self): try: # Creating a command for Ngspice to Modelica converter self.cmd1 = " - python3 ../ngspicetoModelica/NgspicetoModelica.py "\ - + self.ngspiceNetlist + python3 ../ngspicetoModelica/NgspicetoModelica.py "\ + + self.ngspiceNetlist self.obj_workThread1 = Worker.WorkerThread(self.cmd1) self.obj_workThread1.start() if self.obj_validation.validateTool("OMEdit"): @@ -600,17 +706,17 @@ def open_OMedit(self): else: self.msg = QtWidgets.QMessageBox() self.msgContent = "There was an error while - opening OMEdit.
\ + opening OMEdit.
\ Please make sure OpenModelica is installed in your\ - system.
\ + system.
\ To install it on Linux : Go to\ - OpenModelica Linux and \ - install nigthly build release.
\ + OpenModelica Linux and \ + install nigthly build release.
\ To install it on Windows : Go to\ - OpenModelica Windows\ - and install latest version.
" + and install latest version.
" self.msg.setTextFormat(QtCore.Qt.RichText) self.msg.setText(self.msgContent) self.msg.setWindowTitle("Missing OpenModelica") @@ -624,7 +730,7 @@ def open_OMedit(self): "Ngspice to Modelica conversion error") self.msg.showMessage( 'Unable to convert NgSpice netlist to\ - Modelica netlist :'+str(e)) + Modelica netlist :'+str(e)) self.msg.exec_() self.obj_appconfig.print_error(str(e)) """ @@ -654,10 +760,10 @@ def open_OMoptim(self): """ This function uses validateTool() method from Validation.py: - - If 'OMOptim' is present in executables list then - it passes command 'OMOptim' to WorkerThread class of Worker.py - - If 'OMOptim' is not present, then it shows error message with - link to download it on Linux and Windows. + - If 'OMOptim' is present in executables list then + it passes command 'OMOptim' to WorkerThread class of Worker.py + - If 'OMOptim' is not present, then it shows error message with + link to download it on Linux and Windows. """ print("Function : OMOptim") self.obj_appconfig.print_info('OMOptim is called') @@ -688,24 +794,27 @@ def open_OMoptim(self): self.msg.exec_() def open_conToeSim(self): - print("Function : Schematic converter") - self.obj_appconfig.print_info('Schematic converter is called') + print("Function : Schematics converter") + self.obj_appconfig.print_info('Schematics converter is called') self.obj_Mainview.obj_dockarea.eSimConverter() # This class initialize the Main View of Application + + class MainView(QtWidgets.QWidget): """ This class defines whole view and style of main page: - - Position of tool bars: - - Top tool bar. - - Left tool bar. - - Project explorer Area. - - Dock area. - - Console area. + - Position of tool bars: + - Top tool bar. + - Left tool bar. + - Project explorer Area. + - Dock area. + - Console area. """ def __init__(self, *args): + # call init method of superclass QtWidgets.QWidget.__init__(self, *args) @@ -722,120 +831,16 @@ def __init__(self, *args): # Area to be included in MainView self.noteArea = QtWidgets.QTextEdit() self.noteArea.setReadOnly(True) - - # Set explicit scrollbar policy - self.noteArea.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) - self.noteArea.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) - self.obj_appconfig.noteArea['Note'] = self.noteArea self.obj_appconfig.noteArea['Note'].append( - ' eSim Started......') + ' eSim Started......') self.obj_appconfig.noteArea['Note'].append('Project Selected : None') self.obj_appconfig.noteArea['Note'].append('\n') - - # Enhanced CSS with proper scrollbar styling - self.noteArea.setStyleSheet(""" - QTextEdit { - border-radius: 15px; - border: 1px solid gray; - padding: 5px; - background-color: white; - } - - QScrollBar:vertical { - border: 1px solid #999999; - background: #f0f0f0; - width: 16px; - margin: 16px 0 16px 0; - border-radius: 3px; - } - - QScrollBar::handle:vertical { - background: #606060; - min-height: 20px; - border-radius: 3px; - margin: 1px; - } - - QScrollBar::handle:vertical:hover { - background: #505050; - } - - QScrollBar::add-line:vertical { - border: 1px solid #999999; - background: #d0d0d0; - height: 15px; - width: 16px; - subcontrol-position: bottom; - subcontrol-origin: margin; - border-radius: 2px; - } - - QScrollBar::sub-line:vertical { - border: 1px solid #999999; - background: #d0d0d0; - height: 15px; - width: 16px; - subcontrol-position: top; - subcontrol-origin: margin; - border-radius: 2px; - } - - QScrollBar::add-line:vertical:hover, - QScrollBar::sub-line:vertical:hover { - background: #c0c0c0; - } - - QScrollBar::add-page:vertical, - QScrollBar::sub-page:vertical { - background: none; - } - - QScrollBar::up-arrow:vertical { - width: 8px; - height: 8px; - background-color: #606060; - } - - QScrollBar::down-arrow:vertical { - width: 8px; - height: 8px; - background-color: #606060; - } - - QScrollBar:horizontal { - border: 1px solid #999999; - background: #f0f0f0; - height: 16px; - margin: 0 16px 0 16px; - border-radius: 3px; - } - - QScrollBar::handle:horizontal { - background: #606060; - min-width: 20px; - border-radius: 3px; - margin: 1px; - } - - QScrollBar::handle:horizontal:hover { - background: #505050; - } - - QScrollBar::add-line:horizontal, - QScrollBar::sub-line:horizontal { - border: 1px solid #999999; - background: #d0d0d0; - width: 15px; - height: 16px; - border-radius: 2px; - } - - QScrollBar::add-line:horizontal:hover, - QScrollBar::sub-line:horizontal:hover { - background: #c0c0c0; - } - """) + # CSS + self.noteArea.setStyleSheet(" \ + QWidget { border-radius: 15px; border: 1px \ + solid gray; padding: 5px; } \ + ") self.obj_dockarea = DockArea.DockArea() self.obj_projectExplorer = ProjectExplorer.ProjectExplorer() @@ -862,14 +867,16 @@ def __init__(self, *args): # It is main function of the module and starts the application def main(args): - """ - The splash screen opened at the starting of screen is performed - by this function. - """ + """The splash screen opened at the starting of screen is performed by this function.""" print("Starting eSim......") + + # Set environment variable before creating QApplication to suppress model hoster warnings + os.environ['DISABLE_MODEL_SOURCE_CHECK'] = 'True' + app = QtWidgets.QApplication(args) app.setApplicationName("eSim") + appView = Application() appView.hide() @@ -903,7 +910,6 @@ def main(args): sys.exit(app.exec_()) - # Call main function if __name__ == '__main__': # Create and display the splash screen From baef3643f0d499e687937ea0b97d76889f8e758c Mon Sep 17 00:00:00 2001 From: Hariom Thakur Date: Wed, 7 Jan 2026 16:46:38 +0000 Subject: [PATCH 02/34] Refactor DockArea.py for chatbot integration --- src/frontEnd/DockArea.py | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/src/frontEnd/DockArea.py b/src/frontEnd/DockArea.py index d68085f57..2fc238319 100755 --- a/src/frontEnd/DockArea.py +++ b/src/frontEnd/DockArea.py @@ -12,11 +12,12 @@ from PyQt5.QtWidgets import QLineEdit, QLabel, QPushButton, QVBoxLayout, QHBoxLayout from PyQt5.QtCore import Qt import os +from frontEnd.Chatbot import create_chatbot_dock from converter.pspiceToKicad import PspiceConverter from converter.ltspiceToKicad import LTspiceConverter from converter.LtspiceLibConverter import LTspiceLibConverter from converter.libConverter import PspiceLibConverter -from converter.browseSchematic import browse_path +from converter.browseSchematics import browse_path dockList = ['Welcome'] count = 1 dock = {} @@ -127,14 +128,14 @@ def plottingEditor(self): ) count = count + 1 - def ngspiceEditor(self, projName, netlist, simEndSignal, plotFlag): + def ngspiceEditor(self, projName, netlist, simEndSignal,chatbot): """ This function creates widget for Ngspice window.""" global count self.ngspiceWidget = QtWidgets.QWidget() self.ngspiceLayout = QtWidgets.QVBoxLayout() self.ngspiceLayout.addWidget( - NgspiceWidget(netlist, simEndSignal, plotFlag) + NgspiceWidget(netlist, simEndSignal,chatbot) ) # Adding to main Layout @@ -172,7 +173,7 @@ def eSimConverter(self): """This function creates a widget for eSimConverter.""" global count - dockName = 'Schematic Converter-' + dockName = 'Schematics Converter-' self.eConWidget = QtWidgets.QWidget() self.eConLayout = QVBoxLayout() # QVBoxLayout for the main layout @@ -205,7 +206,7 @@ def eSimConverter(self): upload_button2.clicked.connect(lambda: self.pspiceLib_converter.upload_file_Pspice(file_path_text_box.text())) button_layout.addWidget(upload_button2) - upload_button1 = QPushButton("Convert Pspice schematic") + upload_button1 = QPushButton("Convert Pspice schematics") upload_button1.setFixedSize(180, 30) upload_button1.clicked.connect(lambda: self.pspice_converter.upload_file_Pspice(file_path_text_box.text())) button_layout.addWidget(upload_button1) @@ -215,7 +216,7 @@ def eSimConverter(self): upload_button3.clicked.connect(lambda: self.ltspiceLib_converter.upload_file_LTspice(file_path_text_box.text())) button_layout.addWidget(upload_button3) - upload_button = QPushButton("Convert LTspice schematic") + upload_button = QPushButton("Convert LTspice schematics") upload_button.setFixedSize(184, 30) upload_button.clicked.connect(lambda: self.ltspice_converter.upload_file_LTspice(file_path_text_box.text())) button_layout.addWidget(upload_button) @@ -267,9 +268,9 @@ def eSimConverter(self):

Pspice to eSim will convert the PSpice Schematic and Library files to KiCad Schematic and Library files respectively with proper mapping of the components and the wiring. By this way one - will be able to simulate their schematic in PSpice and get the PCB layout in KiCad. + will be able to simulate their schematics in PSpice and get the PCB layout in KiCad.

- LTspice to eSim will convert symbols and schematic from LTspice to Kicad.The goal is to design and + LTspice to eSim will convert symbols and schematics from LTspice to Kicad.The goal is to design and simulate under LTspice and to automatically transfer the circuit under Kicad to draw the PCB.

@@ -569,3 +570,17 @@ def closeDock(self): self.temp = self.obj_appconfig.current_project['ProjectName'] for dockwidget in self.obj_appconfig.dock_dict[self.temp]: dockwidget.close() + + def chatbotEditor(self): + """ + Creates the eSim Copilot (Chatbot) dock. + """ + global count + + self.chatbot_dock = create_chatbot_dock(self) + + self.addDockWidget(QtCore.Qt.BottomDockWidgetArea, self.chatbot_dock) + + self.chatbot_dock.setVisible(True) + self.chatbot_dock.raise_() + From 3930c61d0988fa7e8929a14e0e0be456f1682c9a Mon Sep 17 00:00:00 2001 From: Hariom Thakur Date: Wed, 7 Jan 2026 16:47:35 +0000 Subject: [PATCH 03/34] Enhance ProjectExplorer with netlist analysis Updated context menu actions for project handling and added netlist analysis functionality. --- src/frontEnd/ProjectExplorer.py | 89 ++++++++++++++++++++++++++------- 1 file changed, 70 insertions(+), 19 deletions(-) diff --git a/src/frontEnd/ProjectExplorer.py b/src/frontEnd/ProjectExplorer.py index 997723787..bc55dac9c 100755 --- a/src/frontEnd/ProjectExplorer.py +++ b/src/frontEnd/ProjectExplorer.py @@ -1,11 +1,10 @@ from PyQt5 import QtCore, QtWidgets +from PyQt5.QtWidgets import QDockWidget, QMessageBox,QMenu import os import json from configuration.Appconfig import Appconfig from projManagement.Validation import Validation - -# This is main class for Project Explorer Area. class ProjectExplorer(QtWidgets.QWidget): """ This class contains function: @@ -104,25 +103,45 @@ def addTreeNode(self, parents, children): ) = [] def openMenu(self, position): - indexes = self.treewidget.selectedIndexes() - if len(indexes) > 0: - level = 0 - index = indexes[0] - while index.parent().isValid(): - index = index.parent() - level += 1 - - menu = QtWidgets.QMenu() + """Handle right-click context menu using QTreeWidget items.""" + # 1. Use the correct widget name: self.treewidget + items = self.treewidget.selectedItems() + + level = -1 + file_path = "" + + if len(items) > 0: + item = items[0] + file_path = item.text(1) + + if item.parent() is None: + level = 0 + else: + level = 1 + + menu = QMenu() + if level == 0: - renameProject = menu.addAction(self.tr("Rename Project")) - renameProject.triggered.connect(self.renameProject) - deleteproject = menu.addAction(self.tr("Remove Project")) - deleteproject.triggered.connect(self.removeProject) - refreshproject = menu.addAction(self.tr("Refresh")) - refreshproject.triggered.connect(self.refreshProject) + + analyze_action = menu.addAction("Analyze Project Netlist") + + project_name = item.text(0) + netlist_path = os.path.join(file_path, f"{project_name}.cir.out") + analyze_action.triggered.connect(lambda: self._analyze_netlist_in_copilot(netlist_path)) + + rename_action = menu.addAction("Rename Project") + rename_action.triggered.connect(self.renameProject) + remove_action = menu.addAction("Remove Project") + remove_action.triggered.connect(self.removeProject) + elif level == 1: - openfile = menu.addAction(self.tr("Open")) - openfile.triggered.connect(self.openProject) + + if file_path.endswith((".cir", ".cir.out", ".net")): + analyze_file_action = menu.addAction("Analyze this Netlist") + analyze_file_action.triggered.connect(lambda: self._analyze_netlist_in_copilot(file_path)) + + refresh_action = menu.addAction("Refresh") + refresh_action.triggered.connect(self.refreshInstant) menu.exec_(self.treewidget.viewport().mapToGlobal(position)) @@ -430,3 +449,35 @@ def renameProject(self): 'contain space between them' ) msg.exec_() + + def _analyze_netlist_in_copilot(self, netlist_path: str): + """Send selected .cir file to chatbot for analysis.""" + try: + # Get the main Application window (traverse up the widget hierarchy) + main_window = self + while main_window.parent() is not None: + main_window = main_window.parent() + + # Find the chatbot dock + for dock in main_window.findChildren(QDockWidget): + if "Copilot" in dock.windowTitle(): + chatbot_widget = dock.widget() + if hasattr(chatbot_widget, 'analyze_specific_netlist'): + chatbot_widget.analyze_specific_netlist(netlist_path) + # Show the dock if it's hidden + if not dock.isVisible(): + dock.show() + return + + QMessageBox.information( + self, + "Chatbot not open", + "Please open the eSim Copilot window first (View → eSim Copilot)." + ) + except Exception as e: + print(f"[COPILOT] Failed to trigger analysis: {e}") + QMessageBox.warning( + self, + "Error", + f"Could not connect to chatbot:\n{e}" + ) From 79c7317d96cabb0299eb3ec52b6fe4c2ffc3ef1e Mon Sep 17 00:00:00 2001 From: Hariom Thakur Date: Wed, 7 Jan 2026 16:48:48 +0000 Subject: [PATCH 04/34] Refactor redoSimulation method to simplify logic Removed unnecessary message box for plot confirmation and fixed typo in Flag assignment. --- src/frontEnd/TerminalUi.py | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/src/frontEnd/TerminalUi.py b/src/frontEnd/TerminalUi.py index f838ae076..4c53548f1 100644 --- a/src/frontEnd/TerminalUi.py +++ b/src/frontEnd/TerminalUi.py @@ -94,7 +94,6 @@ def cancelSimulation(self): def redoSimulation(self): """This function reruns the ngspice simulation """ - self.Flag = "Flase" self.cancelSimulationButton.setEnabled(True) self.redoSimulationButton.setEnabled(False) @@ -108,23 +107,6 @@ def redoSimulation(self): self.simulationConsole.setText("") self.simulationCancelled = False - msg_box = QtWidgets.QMessageBox(self) - msg_box.setWindowTitle("Ngspice Plots") - msg_box.setText("Do you want Ngspice plots?") - - yes_button = msg_box.addButton("Yes", QtWidgets.QMessageBox.YesRole) - no_button = msg_box.addButton("No", QtWidgets.QMessageBox.NoRole) - - msg_box.exec_() - - if msg_box.clickedButton() == yes_button: - self.Flag = True - else: - self.Flag = False - - # Emit a custom signal with name plotFlag2 depending upon the Flag - self.qProcess.setProperty("plotFlag2", self.Flag) - self.qProcess.start('ngspice', self.args) def changeColor(self): From f33b3d9043678f5279ba64f9770fd73979efe9bb Mon Sep 17 00:00:00 2001 From: Hariom Thakur Date: Wed, 7 Jan 2026 17:06:24 +0000 Subject: [PATCH 05/34] Create documentation for eSim netlist analysis Add eSim netlist analysis output contract documentation --- .../esim_netlist_analysis_output_contract.txt | 244 ++++++++++++++++++ 1 file changed, 244 insertions(+) create mode 100644 src/manuals/esim_netlist_analysis_output_contract.txt diff --git a/src/manuals/esim_netlist_analysis_output_contract.txt b/src/manuals/esim_netlist_analysis_output_contract.txt new file mode 100644 index 000000000..d12efaebc --- /dev/null +++ b/src/manuals/esim_netlist_analysis_output_contract.txt @@ -0,0 +1,244 @@ +Reference +====================================== + +TABLE OF CONTENTS +1. eSim Overview & Workflow +2. Schematic Design (KiCad) & Netlist Generation +3. SPICE Netlist Rules & Syntax +4. Simulation Types & Commands +5. Components & Libraries +6. Common Errors & Troubleshooting +7. IC Availability & Knowledge + +====================================================================== +1. ESIM OVERVIEW & WORKFLOW +====================================================================== +eSim is an open-source EDA tool for circuit design, simulation, and PCB layout. +It integrates KiCad (schematic), NgSpice (simulation), and Python (automation). + +1.1 ESIM USER INTERFACE & TOOLBAR ICONS (FROM TOP TO BOTTOM): +---------------------------------------------------------------------- +1. NEW PROJECT (Menu > New Project) + - Function: Creates a new project folder in ~/eSim-Workspace. + - Note: Project name must not have spaces. + +2. OPEN SCHEMATIC (Icon: Circuit Diagram) + - Function: Launches KiCad Eeschema (Schematic Editor). + - Usage: + - If new project: Confirms creation of schematic. + - If existing: Opens last saved schematic. + - Key Step: Use "Place Symbol" (A) to add components from eSim_Devices. + +3. CONVERT KICAD TO NGSPICE (Icon: Gear/Converter) + - Function: Converts the KiCad netlist (.cir) into an NgSpice-compatible netlist (.cir.out). + - Prerequisite: You MUST generate the netlist in KiCad first! + - Features (Tabs inside this tool): + a. Analysis: Set simulation type (.tran, .dc, .ac, .op). + b. Source Details: Set values for SINE, PULSE, AC, DC sources. + c. Ngspice Model: Add parameters for logic gates/flip-flops. + d. Device Modeling: Link diode/transistor models to symbols. + e. Subcircuits: Link subcircuit files to 'X' components. + - Action: Click "Convert" to generate the final simulation file. + +4. SIMULATION (Icon: Play Button/Waveform) + - Function: Launches NgSpice console and plotting window. + - Usage: Click "Simulate" after successful conversion. + - Output: Shows plots and simulation logs. + +5. MODEL BUILDER / DEVICE MODELING (Icon: Diode/Graph) + - Function: Create custom SPICE models from datasheet parameters. + - Supported Devices: Diode, BJT, MOSFET, IGBT, JFET. + - Usage: Enter datasheet values (Is, Rs, Cjo) -> Calculate -> Save Model. + +6. SUBCIRCUIT MAKER (Icon: Chip/IC) + - Function: Convert a schematic into a reusable block (.sub file). + - Usage: Create a schematic with ports -> Generate Netlist -> Click Subcircuit Maker. + +7. OPENMODELICA (Icon: OM Logo) + - Function: Mixed-signal simulation for mechanical-electrical systems. + +8. MAKERCHIP (Icon: Chip with 'M') + - Function: Cloud-based Verilog/FPGA design. + +STANDARD WORKFLOW: +1. Open eSim → New Project. +2. Open Schematic (Icon 1) → Draw Circuit → Generate Netlist (File > Export > Netlist). +3. Convert (Icon 2) → Set Analysis/Source values → Click Convert. +4. Simulate (Icon 3) → View waveforms. + +KEY SHORTCUTS: +- A: Add Component +- W: Add Wire +- M: Move +- R: Rotate +- V: Edit Value +- P: Add Power/Ground +- Delete: Remove item +- Esc: Cancel action + +====================================================================== +1.2 HANDLING FOLLOW-UP QUESTIONS +====================================================================== +- Context Awareness: eSim workflow is linear (Schematic -> Netlist -> Convert -> Simulate). +- If user asks "What next?" after drawing a schematic, the answer is "Generate Netlist". +- If user asks "What next?" after converting, the answer is "Simulate". + +====================================================================== +2. SCHEMATIC DESIGN (KICAD) & NETLIST GENERATION +====================================================================== +GROUND REQUIREMENT: +- SPICE requires a node "0" as ground reference. +- ALWAYS use the "GND" symbol from the "power" library. +- Do NOT use other grounds (Earth, Chassis) for simulation reference. + +FLOATING NODES: +- Every node must connect to at least two components. +- A node connecting to only one pin is "floating" and causes errors. +- Fix: Connect the pin or use a "No Connect" flag (X) if intentional (but careful with simulation). + +ADDING SOURCES: +- DC Voltage: eSim_Sources:vsource (set DC value) +- AC Voltage: eSim_Sources:vac (set magnitude/phase) +- Sine Wave: eSim_Sources:vsin (set offset, amplitude, freq) +- Pulse: eSim_Sources:vpulse (set V1, V2, delay, rise/fall, width, period) + +HOW TO GENERATE THE NETLIST (STEP-BY-STEP): +This is the most critical step to bridge Schematic and Simulation. + +Method 1: Top Toolbar (Easiest) +1. Look for the "Generate Netlist" icon in the top toolbar. + (It typically looks like a page with text 'NET' or a green plug icon). +2. Click it to open the Export Netlist dialog. + +Method 2: Menu Bar (If icon is missing) +1. Go to "File" menu. +2. Select "Export". +3. Click "Netlist...". + (Note: In some older versions, this may be under "Tools" → "Generate Netlist File"). + +IN THE NETLIST DIALOG: +1. Click the "Spice" tab (Do not use Pcbnew tab). +2. Ensure "Default" format is selected. +3. Click the "Generate Netlist" button. +4. A save dialog appears: + - Ensure the filename is `.cir`. + - Save it inside your project folder. +5. Close the dialog and close Schematic Editor. + +BACK IN ESIM: +1. Select your project in the explorer. +2. Click the "Convert KiCad to NgSpice" button on the toolbar. +3. If successful, you can now proceed to "Simulate". + +====================================================================== +3. SPICE NETLIST RULES & SYNTAX +====================================================================== +A netlist is a text file describing connections. eSim generates it automatically. + +COMPONENT PREFIXES (First letter matters!): +- R: Resistor (R1, R2) +- C: Capacitor (C1) +- L: Inductor (L1) +- D: Diode (D1) +- Q: BJT Transistor (Q1) +- M: MOSFET (M1) +- V: Voltage Source (V1) +- I: Current Source (I1) +- X: Subcircuit/IC (X1) + +SYNTAX EXAMPLES: +Resistor: R1 node1 node2 1k +Capacitor: C1 node1 0 10u +Diode: D1 anode cathode 1N4007 +BJT (NPN): Q1 collector base emitter BC547 +MOSFET: M1 drain gate source bulk IRF540 +Subcircuit: X1 node1 node2 ... subckt_name + +RULES: +- Floating Nodes: Fatal error. +- Voltage Loop: Two ideal voltage sources in parallel = Error. +- Model Definitions: Every diode/transistor needs a .model statement. +- Subcircuits: Every 'X' component needs a .subckt definition. + +====================================================================== +4. SIMULATION TYPES & COMMANDS +====================================================================== +You must define at least one analysis type in your netlist. + +A. TRANSIENT ANALYSIS (.tran) +- Time-domain simulation (like an oscilloscope). +- Syntax: .tran +- Example: .tran 1u 10m (1ms to 10ms) +- Use for: waveforms, pulses, switching circuits. + +B. DC ANALYSIS (.dc) +- Sweeps a source voltage/current. +- Syntax: .dc +- Example: .dc V1 0 5 0.1 (Sweep V1 from 0 to 5V) +- Use for: I-V curves, transistor characteristics. + +C. AC ANALYSIS (.ac) +- Frequency response (Bode plot). +- Syntax: .ac +- Example: .ac dec 10 10 100k (10 points/decade, 10Hz-100kHz) +- Use for: Filters, amplifiers gain/phase. + +D. OPERATING POINT (.op) +- Calculates DC bias points (steady state). +- Syntax: .op +- Result: Lists voltage at every node and current in sources. + +====================================================================== +5. COMPONENTS & LIBRARIES +====================================================================== +LIBRARY PATH: /usr/share/kicad/library/ + +COMMON LIBRARIES: +- eSim_Devices: R, C, L, D, Q, M (Main library) +- power: GND, VCC, +5V (Power symbols) +- eSim_Sources: vsource, vsin, vpulse (Signal sources) +- eSim_Subckt: OpAmps (LM741, LM358), Timers (NE555), Regulators (LM7805) + +HOW TO ADD MODELS: +1. Right-click component → Properties +2. Edit "Spice_Model" field +3. Paste .model or .subckt reference + +MODEL EXAMPLES (Copy-Paste): +.model 1N4007 D(Is=1e-14 Rs=0.1 Bv=1000) +.model BC547 NPN(Bf=200 Is=1e-14 Vaf=100) +.model 2N2222 NPN(Bf=255 Is=1e-14) + +====================================================================== +6. COMMON ERRORS & TROUBLESHOOTING +====================================================================== +ERROR: "Singular Matrix" / "Gmin stepping failed" +- Cause: Floating node, perfect switch, or bad circuit loop. +- Fix 1: Check for unconnected pins. +- Fix 2: Add 1GΩ resistor to ground at floating nodes. +- Fix 3: Add .options gmin=1e-10 to netlist. + +ERROR: "Model not found" / "Subcircuit not found" +- Cause: Component used (e.g., Q1) but no .model defined. +- Fix: Add the missing .model or .subckt definition to the netlist or schematic. + +ERROR: "Project does not contain Kicad netlist file" +- Cause: You forgot to generate the netlist in KiCad or didn't save it as .cir. +- Fix: Go back to Schematic, click File > Export > Netlist, and save as .cir. + +ERROR: "Permission denied" +- Fix: Run eSim as administrator (sudo) or fix workspace permissions. + +====================================================================== +7. IC AVAILABILITY & KNOWLEDGE +====================================================================== +SUPPORTED ICs (via eSim_Subckt library): +- Op-Amps: LM741, LM358, LM324, TL082, AD844 +- Timers: NE555, LM555 +- Regulators: LM7805, LM7812, LM7905, LM317 +- Logic: 7400, 7402, 7404, 7408, 7432, 7486, 7474 (Flip-Flop) +- Comparators: LM311, LM339 +- Optocouplers: 4N35, PC817 + +Status: All listed above are "Completed" and verified for eSim. +""" From d9fb82a0ccdd208ce921f16f13c13813409c04d4 Mon Sep 17 00:00:00 2001 From: Hariom Thakur Date: Wed, 7 Jan 2026 17:09:43 +0000 Subject: [PATCH 06/34] Create eSim netlist analysis output contract Added a comprehensive eSim netlist analysis output contract document detailing workflow, schematic design, SPICE rules, simulation types, components, common errors, and IC availability. --- .../esim_netlist_analysis_output_contract.txt | 244 ++++++++++++++++++ 1 file changed, 244 insertions(+) create mode 100644 src/frontEnd/manual/esim_netlist_analysis_output_contract.txt diff --git a/src/frontEnd/manual/esim_netlist_analysis_output_contract.txt b/src/frontEnd/manual/esim_netlist_analysis_output_contract.txt new file mode 100644 index 000000000..d12efaebc --- /dev/null +++ b/src/frontEnd/manual/esim_netlist_analysis_output_contract.txt @@ -0,0 +1,244 @@ +Reference +====================================== + +TABLE OF CONTENTS +1. eSim Overview & Workflow +2. Schematic Design (KiCad) & Netlist Generation +3. SPICE Netlist Rules & Syntax +4. Simulation Types & Commands +5. Components & Libraries +6. Common Errors & Troubleshooting +7. IC Availability & Knowledge + +====================================================================== +1. ESIM OVERVIEW & WORKFLOW +====================================================================== +eSim is an open-source EDA tool for circuit design, simulation, and PCB layout. +It integrates KiCad (schematic), NgSpice (simulation), and Python (automation). + +1.1 ESIM USER INTERFACE & TOOLBAR ICONS (FROM TOP TO BOTTOM): +---------------------------------------------------------------------- +1. NEW PROJECT (Menu > New Project) + - Function: Creates a new project folder in ~/eSim-Workspace. + - Note: Project name must not have spaces. + +2. OPEN SCHEMATIC (Icon: Circuit Diagram) + - Function: Launches KiCad Eeschema (Schematic Editor). + - Usage: + - If new project: Confirms creation of schematic. + - If existing: Opens last saved schematic. + - Key Step: Use "Place Symbol" (A) to add components from eSim_Devices. + +3. CONVERT KICAD TO NGSPICE (Icon: Gear/Converter) + - Function: Converts the KiCad netlist (.cir) into an NgSpice-compatible netlist (.cir.out). + - Prerequisite: You MUST generate the netlist in KiCad first! + - Features (Tabs inside this tool): + a. Analysis: Set simulation type (.tran, .dc, .ac, .op). + b. Source Details: Set values for SINE, PULSE, AC, DC sources. + c. Ngspice Model: Add parameters for logic gates/flip-flops. + d. Device Modeling: Link diode/transistor models to symbols. + e. Subcircuits: Link subcircuit files to 'X' components. + - Action: Click "Convert" to generate the final simulation file. + +4. SIMULATION (Icon: Play Button/Waveform) + - Function: Launches NgSpice console and plotting window. + - Usage: Click "Simulate" after successful conversion. + - Output: Shows plots and simulation logs. + +5. MODEL BUILDER / DEVICE MODELING (Icon: Diode/Graph) + - Function: Create custom SPICE models from datasheet parameters. + - Supported Devices: Diode, BJT, MOSFET, IGBT, JFET. + - Usage: Enter datasheet values (Is, Rs, Cjo) -> Calculate -> Save Model. + +6. SUBCIRCUIT MAKER (Icon: Chip/IC) + - Function: Convert a schematic into a reusable block (.sub file). + - Usage: Create a schematic with ports -> Generate Netlist -> Click Subcircuit Maker. + +7. OPENMODELICA (Icon: OM Logo) + - Function: Mixed-signal simulation for mechanical-electrical systems. + +8. MAKERCHIP (Icon: Chip with 'M') + - Function: Cloud-based Verilog/FPGA design. + +STANDARD WORKFLOW: +1. Open eSim → New Project. +2. Open Schematic (Icon 1) → Draw Circuit → Generate Netlist (File > Export > Netlist). +3. Convert (Icon 2) → Set Analysis/Source values → Click Convert. +4. Simulate (Icon 3) → View waveforms. + +KEY SHORTCUTS: +- A: Add Component +- W: Add Wire +- M: Move +- R: Rotate +- V: Edit Value +- P: Add Power/Ground +- Delete: Remove item +- Esc: Cancel action + +====================================================================== +1.2 HANDLING FOLLOW-UP QUESTIONS +====================================================================== +- Context Awareness: eSim workflow is linear (Schematic -> Netlist -> Convert -> Simulate). +- If user asks "What next?" after drawing a schematic, the answer is "Generate Netlist". +- If user asks "What next?" after converting, the answer is "Simulate". + +====================================================================== +2. SCHEMATIC DESIGN (KICAD) & NETLIST GENERATION +====================================================================== +GROUND REQUIREMENT: +- SPICE requires a node "0" as ground reference. +- ALWAYS use the "GND" symbol from the "power" library. +- Do NOT use other grounds (Earth, Chassis) for simulation reference. + +FLOATING NODES: +- Every node must connect to at least two components. +- A node connecting to only one pin is "floating" and causes errors. +- Fix: Connect the pin or use a "No Connect" flag (X) if intentional (but careful with simulation). + +ADDING SOURCES: +- DC Voltage: eSim_Sources:vsource (set DC value) +- AC Voltage: eSim_Sources:vac (set magnitude/phase) +- Sine Wave: eSim_Sources:vsin (set offset, amplitude, freq) +- Pulse: eSim_Sources:vpulse (set V1, V2, delay, rise/fall, width, period) + +HOW TO GENERATE THE NETLIST (STEP-BY-STEP): +This is the most critical step to bridge Schematic and Simulation. + +Method 1: Top Toolbar (Easiest) +1. Look for the "Generate Netlist" icon in the top toolbar. + (It typically looks like a page with text 'NET' or a green plug icon). +2. Click it to open the Export Netlist dialog. + +Method 2: Menu Bar (If icon is missing) +1. Go to "File" menu. +2. Select "Export". +3. Click "Netlist...". + (Note: In some older versions, this may be under "Tools" → "Generate Netlist File"). + +IN THE NETLIST DIALOG: +1. Click the "Spice" tab (Do not use Pcbnew tab). +2. Ensure "Default" format is selected. +3. Click the "Generate Netlist" button. +4. A save dialog appears: + - Ensure the filename is `.cir`. + - Save it inside your project folder. +5. Close the dialog and close Schematic Editor. + +BACK IN ESIM: +1. Select your project in the explorer. +2. Click the "Convert KiCad to NgSpice" button on the toolbar. +3. If successful, you can now proceed to "Simulate". + +====================================================================== +3. SPICE NETLIST RULES & SYNTAX +====================================================================== +A netlist is a text file describing connections. eSim generates it automatically. + +COMPONENT PREFIXES (First letter matters!): +- R: Resistor (R1, R2) +- C: Capacitor (C1) +- L: Inductor (L1) +- D: Diode (D1) +- Q: BJT Transistor (Q1) +- M: MOSFET (M1) +- V: Voltage Source (V1) +- I: Current Source (I1) +- X: Subcircuit/IC (X1) + +SYNTAX EXAMPLES: +Resistor: R1 node1 node2 1k +Capacitor: C1 node1 0 10u +Diode: D1 anode cathode 1N4007 +BJT (NPN): Q1 collector base emitter BC547 +MOSFET: M1 drain gate source bulk IRF540 +Subcircuit: X1 node1 node2 ... subckt_name + +RULES: +- Floating Nodes: Fatal error. +- Voltage Loop: Two ideal voltage sources in parallel = Error. +- Model Definitions: Every diode/transistor needs a .model statement. +- Subcircuits: Every 'X' component needs a .subckt definition. + +====================================================================== +4. SIMULATION TYPES & COMMANDS +====================================================================== +You must define at least one analysis type in your netlist. + +A. TRANSIENT ANALYSIS (.tran) +- Time-domain simulation (like an oscilloscope). +- Syntax: .tran +- Example: .tran 1u 10m (1ms to 10ms) +- Use for: waveforms, pulses, switching circuits. + +B. DC ANALYSIS (.dc) +- Sweeps a source voltage/current. +- Syntax: .dc +- Example: .dc V1 0 5 0.1 (Sweep V1 from 0 to 5V) +- Use for: I-V curves, transistor characteristics. + +C. AC ANALYSIS (.ac) +- Frequency response (Bode plot). +- Syntax: .ac +- Example: .ac dec 10 10 100k (10 points/decade, 10Hz-100kHz) +- Use for: Filters, amplifiers gain/phase. + +D. OPERATING POINT (.op) +- Calculates DC bias points (steady state). +- Syntax: .op +- Result: Lists voltage at every node and current in sources. + +====================================================================== +5. COMPONENTS & LIBRARIES +====================================================================== +LIBRARY PATH: /usr/share/kicad/library/ + +COMMON LIBRARIES: +- eSim_Devices: R, C, L, D, Q, M (Main library) +- power: GND, VCC, +5V (Power symbols) +- eSim_Sources: vsource, vsin, vpulse (Signal sources) +- eSim_Subckt: OpAmps (LM741, LM358), Timers (NE555), Regulators (LM7805) + +HOW TO ADD MODELS: +1. Right-click component → Properties +2. Edit "Spice_Model" field +3. Paste .model or .subckt reference + +MODEL EXAMPLES (Copy-Paste): +.model 1N4007 D(Is=1e-14 Rs=0.1 Bv=1000) +.model BC547 NPN(Bf=200 Is=1e-14 Vaf=100) +.model 2N2222 NPN(Bf=255 Is=1e-14) + +====================================================================== +6. COMMON ERRORS & TROUBLESHOOTING +====================================================================== +ERROR: "Singular Matrix" / "Gmin stepping failed" +- Cause: Floating node, perfect switch, or bad circuit loop. +- Fix 1: Check for unconnected pins. +- Fix 2: Add 1GΩ resistor to ground at floating nodes. +- Fix 3: Add .options gmin=1e-10 to netlist. + +ERROR: "Model not found" / "Subcircuit not found" +- Cause: Component used (e.g., Q1) but no .model defined. +- Fix: Add the missing .model or .subckt definition to the netlist or schematic. + +ERROR: "Project does not contain Kicad netlist file" +- Cause: You forgot to generate the netlist in KiCad or didn't save it as .cir. +- Fix: Go back to Schematic, click File > Export > Netlist, and save as .cir. + +ERROR: "Permission denied" +- Fix: Run eSim as administrator (sudo) or fix workspace permissions. + +====================================================================== +7. IC AVAILABILITY & KNOWLEDGE +====================================================================== +SUPPORTED ICs (via eSim_Subckt library): +- Op-Amps: LM741, LM358, LM324, TL082, AD844 +- Timers: NE555, LM555 +- Regulators: LM7805, LM7812, LM7905, LM317 +- Logic: 7400, 7402, 7404, 7408, 7432, 7486, 7474 (Flip-Flop) +- Comparators: LM311, LM339 +- Optocouplers: 4N35, PC817 + +Status: All listed above are "Completed" and verified for eSim. +""" From 4b06d30defc630fc2f87a5dd164cfa7cca775647 Mon Sep 17 00:00:00 2001 From: Hariom Thakur Date: Wed, 7 Jan 2026 17:10:56 +0000 Subject: [PATCH 07/34] Initialize eSim Chatbot package with core imports --- src/chatbot/__init__.py | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/chatbot/__init__.py diff --git a/src/chatbot/__init__.py b/src/chatbot/__init__.py new file mode 100644 index 000000000..2157cc829 --- /dev/null +++ b/src/chatbot/__init__.py @@ -0,0 +1,11 @@ +""" +eSim Chatbot Package +""" + +from .chatbot_core import handle_input, ESIMCopilotWrapper, analyze_schematic + +__all__ = [ + 'handle_input', + 'ESIMCopilotWrapper', + 'analyze_schematic' +] From 1a5b4a5dadd26915a570656cd88df47264695b08 Mon Sep 17 00:00:00 2001 From: Hariom Thakur Date: Wed, 7 Jan 2026 17:11:31 +0000 Subject: [PATCH 08/34] Add core functionality for eSim Copilot Implement core functionality for eSim Copilot, including error detection, question classification, and image analysis handling. --- src/chatbot/chatbot_core.py | 645 ++++++++++++++++++++++++++++++++++++ 1 file changed, 645 insertions(+) create mode 100644 src/chatbot/chatbot_core.py diff --git a/src/chatbot/chatbot_core.py b/src/chatbot/chatbot_core.py new file mode 100644 index 000000000..52842fb01 --- /dev/null +++ b/src/chatbot/chatbot_core.py @@ -0,0 +1,645 @@ +# chatbot_core.py + +import os +import re +import json +from typing import Dict, Any, Tuple, List + +from .error_solutions import get_error_solution +from .image_handler import analyze_and_extract +from .ollama_runner import run_ollama +from .knowledge_base import search_knowledge + +# ==================== ESIM WORKFLOW KNOWLEDGE ==================== + +ESIM_WORKFLOWS = """ +=== COMMON ESIM WORKFLOWS === + +HOW TO ADD GROUND: +1. In KiCad schematic, press 'A' key (Add Component) +2. Type "GND" in the search box +3. Select ground symbol from "power" library +4. Click to place it on schematic +5. Press 'W' to add wire and connect to circuit +6. Save (Ctrl+S) → eSim: Simulation → Convert KiCad to NgSpice + +HOW TO ADD ANY COMPONENT: +1. In KiCad schematic, press 'A' key +2. Type component name (e.g., "Q2N3904", "1N4148", "uA741") +3. Select from appropriate library (eSim_Devices, eSim_Subckt, etc.) +4. Place on schematic and connect with wires +5. Save → Convert KiCad to NgSpice + +HOW TO FIX MISSING SPICE MODELS (3 Methods): + +Method 1 - Direct Netlist Edit (FASTEST, but temporary): +1. eSim: Tools → Spice Editor (or Ctrl+E) +2. Open your_project.cir.out file +3. Scroll to bottom (before .end line) +4. Add model definition: + BJT: .model Q2N3904 NPN(Bf=200 Is=1e-14 Vaf=100) + Diode: .model 1N4148 D(Is=1e-14 Rs=1) + Zener: .model DZ5V1 D(Is=1e-14 Bv=5.1 Ibv=5m) +5. Save (Ctrl+S) → Run Simulation +NOTE: This gets overwritten when you "Convert KiCad to NgSpice" again + +Method 2 - Component Properties (PERMANENT): +1. Open KiCad schematic (double-click .proj in Project Explorer) +2. Find the component that uses the missing model (e.g., transistor Q1) +3. Right-click on it → Properties (or press E when hovering over it) +4. Click "Edit Spice Model" button in the Properties dialog +5. In the Spice Model field, paste the model definition: + .model Q2N3904 NPN(Bf=200 Is=1e-14 Vaf=100) +6. Click OK → Save schematic (Ctrl+S) +7. eSim: Simulation → Convert KiCad to NgSpice +NOTE: This permanently associates the model with the component + +Method 3 - Include Library: +1. Spice Editor → Open .cir.out +2. Add at top: .include /usr/share/ngspice/models/bjt.lib +3. Save → Simulate + +HOW TO FIX MISSING SUBCIRCUITS: +1. Spice Editor → Open .cir.out +2. Add before .end: + .subckt OPAMP_IDEAL inp inn out vdd vss + Rin inp inn 1Meg + E1 out 0 inp inn 100000 + Rout out 0 75 + .ends +3. Save → Simulate +OR: Replace with eSim library opamp (uA741, LM324) + +HOW TO FIX FLOATING NODES: +1. Open KiCad schematic +2. Find the unconnected pin/node +3. Either connect it with wire (press W) or delete component +4. For sense points: Add Rleak node 0 1Meg +5. Save → Convert to NgSpice + +KICAD SHORTCUTS: +A = Add component +W = Add wire +M = Move item +R = Rotate item +C = Copy item +Delete = Remove item +Ctrl+S = Save + +ESIM MENU PATHS: +Convert to NgSpice: Simulation → Convert KiCad to NgSpice +Run Simulation: Simulation → Simulate +Spice Editor: Tools → Spice Editor (Ctrl+E) +Model Editor: Tools → Model Editor +Open KiCad: Double-click .proj file in Project Explorer + +FILE LOCATIONS: +Project folder: ~/eSim-Workspace// +Netlist: .cir.out +Schematic: .proj +""" + +LAST_BOT_REPLY: str = "" +LAST_IMAGE_CONTEXT: Dict[str, Any] = {} +LAST_NETLIST_ISSUES: Dict[str, Any] = {} + + +def get_history() -> Dict[str, Any]: + return LAST_IMAGE_CONTEXT + + +def clear_history() -> None: + global LAST_IMAGE_CONTEXT, LAST_NETLIST_ISSUES + LAST_IMAGE_CONTEXT = {} + LAST_NETLIST_ISSUES = {} + + +# ==================== ESIM ERROR LOGIC ==================== + +def detect_esim_errors(image_context: Dict[str, Any], user_input: str) -> str: + """ + Display errors from hybrid analysis with SMART FILTERING to remove hallucinations. + """ + if not image_context: + return "" + + analysis = image_context.get("circuit_analysis", {}) + raw_errors = analysis.get("design_errors", []) + warnings = analysis.get("design_warnings", []) + + # === SMART FILTERING === + components_str = str(image_context.get("components", [])).lower() + summary_str = str(image_context.get("vision_summary", "")).lower() + context_text = components_str + summary_str + + filtered_errors: List[str] = [] + for err in raw_errors: + err_lower = err.lower() + + # 1. Filter "No ground" if ground is actually detected + if "ground" in err_lower and ( + "gnd" in context_text or "ground" in context_text or " 0 " in context_text + ): + continue + + # 2. Filter "Floating node" if it refers to Vin/Vout labels + if "floating" in err_lower and ( + "vin" in err_lower or "vout" in err_lower or "label" in err_lower + ): + continue + + filtered_errors.append(err) + + output: List[str] = [] + + if filtered_errors: + output.append("**🚨 CRITICAL ERRORS:**") + for err in filtered_errors: + output.append(f"❌ {err}") + + if warnings: + output.append("\n**⚠️ WARNINGS:**") + for warn in warnings: + output.append(f"⚠️ {warn}") + + text = user_input.lower() + if "singular matrix" in text: + output.append("\n**🔧 FIX:** Add 1GΩ resistors to all nodes → GND") + if "timestep" in text: + output.append("\n**🔧 FIX:** Reduce timestep or add 0.1Ω series R") + + if not output: + return "**✅ No errors detected**" + + return "\n".join(output) + + +# ==================== UTILITIES ==================== + +VALID_EXTS = (".png", ".jpg", ".jpeg", ".bmp", ".tiff", ".gif") + + +def _is_image_file(path: str) -> bool: + if not path: + return False + clean = re.sub(r"\[Image:\s*(.*?)\]", r"\1", path).strip() + return clean.lower().endswith(VALID_EXTS) + + +def _is_image_query(user_input: str) -> bool: + if not user_input: + return False + if "[Image:" in user_input: + return True + if "|" in user_input: + parts = user_input.split("|", 1) + if len(parts) == 2 and _is_image_file(parts[1]): + return True + return _is_image_file(user_input) + + +def _parse_image_query(user_input: str) -> Tuple[str, str]: + user_input = user_input.strip() + + match = re.search(r"\[Image:\s*(.*?)\]", user_input) + if match: + return user_input.replace(match.group(0), "").strip(), match.group(1).strip() + + if "|" in user_input: + q, p = [x.strip() for x in user_input.split("|", 1)] + if _is_image_file(p): + return q, p + if _is_image_file(q): + return p, q + + if _is_image_file(user_input): + return "", user_input + + return user_input, "" + + +def clean_response_raw(raw: str) -> str: + cleaned = re.sub(r"<\|.*?\|>", "", raw.strip()) + cleaned = re.sub(r"\[Context:.*?\]", "", cleaned, flags=re.DOTALL) + cleaned = re.sub(r"\[FACT .*?\]", "", cleaned, flags=re.MULTILINE) + cleaned = re.sub( + r"\[ESIM_NETLIST_START\].*?\[ESIM_NETLIST_END\]", "", cleaned, flags=re.DOTALL + ) + return cleaned.strip() + + +def _history_to_text(history: List[Dict[str, str]] | None, max_turns: int = 6) -> str: + """Convert history to readable text with MORE context (6 turns).""" + if not history: + return "" + recent = history[-max_turns:] + lines: List[str] = [] + for i, t in enumerate(recent, 1): + u = (t.get("user") or "").strip() + b = (t.get("bot") or "").strip() + if u: + lines.append(f"[Turn {i}] User: {u}") + if b: + # Truncate very long bot responses to save token space + if len(b) > 300: + b = b[:300] + "..." + lines.append(f"[Turn {i}] Assistant: {b}") + return "\n".join(lines).strip() + + +def _is_follow_up_question(user_input: str, history: List[Dict[str, str]] | None) -> bool: + """ + Detect if this is a follow-up question that needs history context. + Returns True if question lacks standalone context. + """ + if not history: + return False + + user_lower = user_input.lower().strip() + words = user_lower.split() + + + if len(words) <= 7: + return True + + # Questions with pronouns (referring to previous context) + pronouns = ["it", "that", "this", "those", "these", "they", "them"] + if any(pronoun in words for pronoun in pronouns): + return True + + # Continuation phrases + continuations = [ + "what next", "next step", "after that", "and then", "then what", + "what about", "how about", "what if", "but why", "why not" + ] + if any(phrase in user_lower for phrase in continuations): + return True + + # Question words at start without enough context + question_starters = ["why", "how", "where", "when", "what", "which"] + if words[0] in question_starters and len(words) <= 5: + return True + + return False + + +# ==================== QUESTION CLASSIFICATION ==================== + +def classify_question_type(user_input: str, has_image_context: bool, + history: List[Dict[str, str]] | None = None) -> str: + """ + Classify question type for smart routing. + Returns: 'greeting', 'simple', 'esim', 'image_query', 'follow_up_image', + 'follow_up', 'netlist' + """ + user_lower = user_input.lower() + + # Explicit netlist block + if "[ESIM_NETLIST_START]" in user_input: + return "netlist" + + # Image: new upload + if _is_image_query(user_input): + return "image_query" + + # Follow-up about image + if has_image_context: + follow_phrases = [ + "this circuit", "that circuit", "in this schematic", + "components here", "what is the value", "how many", + "the circuit", "this schematic","what","can","how" + ] + if any(p in user_lower for p in follow_phrases): + return "follow_up_image" + + # Simple greeting + greetings = ["hello", "hi", "hey", "howdy", "greetings"] + user_words = user_lower.strip().split() + if len(user_words) <= 3 and any(g in user_words for g in greetings): + return "greeting" + + # Detect generic follow-up (needs history) + if _is_follow_up_question(user_input, history): + return "follow_up" + + # eSim-related keywords + esim_keywords = [ + "esim", "kicad", "ngspice", "spice", "simulation", "netlist", + "schematic", "convert", "gnd", "ground", ".model", ".subckt", + "singular matrix", "floating", "timestep", "convergence" + ] + if any(keyword in user_lower for keyword in esim_keywords): + return "esim" + + # Error-related + error_keywords = [ + "error", "fix", "problem", "issue", "warning", "missing", + "not working", "failed", "crash" + ] + if any(keyword in user_lower for keyword in error_keywords): + return "esim" + + return "simple" + + +# ==================== HANDLERS ==================== + +def handle_greeting() -> str: + return ( + "Hello! I'm eSim Copilot. I can help you with:\n" + "• Circuit analysis and netlist debugging\n" + "• Electronics concepts and SPICE simulation\n" + "• Component selection and circuit design\n\n" + "What would you like to know?" + ) + + +def handle_simple_question(user_input: str) -> str: + prompt = ( + "You are an electronics expert. Answer this question concisely (2-3 sentences max).\n" + "Use your general electronics knowledge. Do NOT make up eSim-specific commands.\n\n" + f"Question: {user_input}\n\n" + "Answer (brief and factual):" + ) + return run_ollama(prompt, mode="default") + + +def handle_follow_up(user_input: str, + image_context: Dict[str, Any], + history: List[Dict[str, str]] | None = None) -> str: + """ + Handle follow-up questions that depend on conversation history. + This handler PRIORITIZES history over RAG. + """ + history_text = _history_to_text(history, max_turns=6) + + if not history_text: + return "I need more context. Could you provide more details about your question?" + + # Get minimal RAG context (only if keywords detected) + rag_context = "" + user_lower = user_input.lower() + if any(kw in user_lower for kw in ["model", "spice", "ground", "error", "netlist"]): + rag_context = search_knowledge(user_input, n_results=2) + + prompt = ( + "You are an eSim expert assistant. The user is asking a follow-up question.\n\n" + "=== CONVERSATION HISTORY (MOST IMPORTANT) ===\n" + f"{history_text}\n" + "=============================================\n\n" + f"=== CURRENT USER QUESTION (FOLLOW-UP) ===\n{user_input}\n\n" + ) + + if rag_context: + prompt += f"=== REFERENCE MANUAL (if needed) ===\n{rag_context}\n\n" + + if image_context: + prompt += ( + f"=== CURRENT CIRCUIT CONTEXT ===\n" + f"Type: {image_context.get('circuit_analysis', {}).get('circuit_type', 'Unknown')}\n" + f"Components: {image_context.get('components', [])}\n\n" + ) + + prompt += ( + "CRITICAL INSTRUCTIONS:\n" + "1. The user's question refers to the CONVERSATION HISTORY above.\n" + "2. Identify what 'it', 'that', 'this', or 'next step' refers to by reading the history.\n" + "3. Answer based on the conversation context first, then use manual/workflows if needed.\n" + "4. If the user asks 'why', explain based on what was just discussed.\n" + "5. If the user asks 'what next' or 'next step', continue from the last instruction.\n" + "6. Be specific and reference what you're talking about (e.g., 'In the previous step, I mentioned...').\n" + "7. Keep answer concise (max 150 words).\n\n" + "Answer:" + ) + + return run_ollama(prompt, mode="default") + + +def handle_esim_question(user_input: str, + image_context: Dict[str, Any], + history: List[Dict[str, str]] | None = None) -> str: + """ + Handle eSim-specific questions with RAG + conversation history. + """ + user_lower = user_input.lower() + + # Fast path: known ngspice error messages → structured fixes + sol = get_error_solution(user_input) + if sol and sol.get("description") != "General schematic error": + fixes = "\n".join(f"- {f}" for f in sol.get("fixes", [])) + cmd = sol.get("eSim_command", "") + answer = ( + f"**Detected issue:** {sol['description']}\n" + f"**Severity:** {sol.get('severity', 'unknown')}\n\n" + f"**Recommended fixes:**\n{fixes}\n\n" + ) + if cmd: + answer += f"**eSim action:** {cmd}\n" + return answer + + # Build history text + history_text = _history_to_text(history, max_turns=6) + + # RAG context + rag_context = search_knowledge(user_input, n_results=5) + + image_context_str = "" + if image_context: + image_context_str = ( + f"\n=== CURRENT CIRCUIT ===\n" + f"Type: {image_context.get('circuit_analysis', {}).get('circuit_type', 'Unknown')}\n" + f"Components: {image_context.get('components', [])}\n" + f"Values: {image_context.get('values', {})}\n" + ) + + prompt = ( + "You are an eSim expert. Answer using the workflows, manual, and conversation history.\n\n" + f"{ESIM_WORKFLOWS}\n\n" + f"=== MANUAL CONTEXT ===\n{rag_context}\n" + f"{image_context_str}\n" + ) + + if history_text: + prompt += f"=== CONVERSATION HISTORY ===\n{history_text}\n\n" + + prompt += ( + f"USER QUESTION: {user_input}\n\n" + "INSTRUCTIONS:\n" + "1. If the question refers to previous conversation, use the history.\n" + "2. Use exact menu paths and shortcuts from the workflows when relevant.\n" + "3. If the manual context does not contain the answer, say you need to check the manual.\n" + "4. Keep the answer concise (max 150 words).\n\n" + "Answer:" + ) + + return run_ollama(prompt, mode="default") + + +def handle_image_query(user_input: str) -> Tuple[str, Dict[str, Any]]: + """ + Handle image analysis queries. + Returns: (response_text, image_context_dict) + """ + question, image_path = _parse_image_query(user_input) + image_path = image_path.strip("'\"").strip() + + if not image_path or not os.path.exists(image_path): + return f"Error: Image not found: {image_path}", {} + + extraction = analyze_and_extract(image_path) + + if extraction.get("error"): + return f"Analysis Failed: {extraction['error']}", {} + + # No follow-up question → summary + if not question: + error_report = detect_esim_errors(extraction, "") + + summary = ( + "**Image Analysis Complete**\n" + f"**Type:** {extraction.get('circuit_analysis', {}).get('circuit_type', 'Unknown')}\n" + f"**Components:** {extraction.get('component_counts', {})}\n" + f"**Description:** {extraction.get('vision_summary', '')}\n\n" + ) + + if extraction.get("components"): + summary += f"**Detected Components:** {', '.join(extraction['components'])}\n" + + if extraction.get("values"): + summary += "**Component Values:**\n" + for comp, val in extraction["values"].items(): + summary += f" • {comp}: {val}\n" + + summary += ( + "\n**Note:** Vision analysis may have errors. Use 'Analyze netlist' for precise results.\n" + ) + + if "🚨" in error_report or "⚠️" in error_report: + summary += f"\n{error_report}" + + return summary, extraction + + # There is a textual question about this image + return handle_follow_up_image_question(question, extraction), extraction + + +def handle_follow_up_image_question(user_input: str, + image_context: Dict[str, Any]) -> str: + """ + Answer questions about an analyzed image using ONLY extracted data. + """ + image_context_str = ( + f"**Circuit Type:** {image_context.get('circuit_analysis', {}).get('circuit_type', 'Unknown')}\n" + f"**Components Detected:** {image_context.get('components', [])}\n" + f"**Component Values:** {image_context.get('values', {})}\n" + f"**Component Counts:** {image_context.get('component_counts', {})}\n" + f"**Description:** {image_context.get('vision_summary', '')}\n" + ) + + prompt = ( + "You are analyzing a circuit schematic. Answer using ONLY the circuit data below.\n\n" + "=== ANALYZED CIRCUIT DATA ===\n" + f"{image_context_str}\n" + "==============================\n\n" + f"USER QUESTION: {user_input}\n\n" + "STRICT INSTRUCTIONS:\n" + "1. Answer ONLY using the circuit data above - DO NOT use external knowledge.\n" + "2. For counts: use 'Component Counts'.\n" + "3. For values: use 'Component Values'.\n" + "4. For lists: use 'Components Detected'.\n" + "5. If data is missing, answer: 'The image analysis did not detect that information.'\n" + "6. Keep answer brief (2-3 sentences).\n\n" + "Answer:" + ) + + return run_ollama(prompt, mode="default") + + +def handle_netlist_analysis(user_input: str) -> str: + """ + Handle netlist analysis prompts (FACT-based prompt from GUI). + """ + raw_reply = run_ollama(user_input) + return clean_response_raw(raw_reply) + + +# ==================== MAIN ROUTER ==================== + +def handle_input(user_input: str, + history: List[Dict[str, str]] | None = None) -> str: + """ + Main router. Accepts optional conversation history for follow-up understanding. + """ + global LAST_IMAGE_CONTEXT, LAST_BOT_REPLY + + user_input = (user_input or "").strip() + if not user_input: + return "Please enter a query." + + # Special case: raw netlist block + if "[ESIM_NETLIST_START]" in user_input: + raw_reply = run_ollama(user_input) + cleaned = clean_response_raw(raw_reply) + LAST_BOT_REPLY = cleaned + return cleaned + + # Classify + question_type = classify_question_type( + user_input, bool(LAST_IMAGE_CONTEXT), history + ) + print(f"[COPILOT] Question type: {question_type}") + + try: + if question_type == "netlist": + response = handle_netlist_analysis(user_input) + + elif question_type == "greeting": + response = handle_greeting() + + elif question_type == "image_query": + response, LAST_IMAGE_CONTEXT = handle_image_query(user_input) + + elif question_type == "follow_up_image": + response = handle_follow_up_image_question(user_input, LAST_IMAGE_CONTEXT) + + elif question_type == "follow_up": + # NEW: Dedicated follow-up handler + response = handle_follow_up(user_input, LAST_IMAGE_CONTEXT, history) + + elif question_type == "simple": + response = handle_simple_question(user_input) + + else: # "esim" or fallback + response = handle_esim_question(user_input, LAST_IMAGE_CONTEXT, history) + + LAST_BOT_REPLY = response + return response + + except Exception as e: + error_msg = f"Error processing question: {str(e)}" + print(f"[COPILOT ERROR] {error_msg}") + return error_msg + + +# ==================== WRAPPER ==================== + +class ESIMCopilotWrapper: + def __init__(self) -> None: + self.history: List[Dict[str, str]] = [] + + def handle_input(self, user_input: str) -> str: + reply = handle_input(user_input, self.history) + self.history.append({"user": user_input, "bot": reply}) + if len(self.history) > 12: + self.history = self.history[-12:] + return reply + + def analyze_schematic(self, query: str) -> str: + return self.handle_input(query) + +# Global wrapper so history persists across calls from GUI +_GLOBAL_WRAPPER = ESIMCopilotWrapper() + + +def analyze_schematic(query: str) -> str: + return _GLOBAL_WRAPPER.handle_input(query) From 4d991d7cbf77fa0d85605d3b42fff984ae7fab46 Mon Sep 17 00:00:00 2001 From: Hariom Thakur Date: Wed, 7 Jan 2026 17:12:40 +0000 Subject: [PATCH 09/34] Add files via upload --- src/chatbot/error_solutions.py | 106 ++++++++++++++ src/chatbot/image_handler.py | 246 +++++++++++++++++++++++++++++++++ src/chatbot/knowledge_base.py | 119 ++++++++++++++++ src/chatbot/ollama_runner.py | 142 +++++++++++++++++++ src/chatbot/stt_handler.py | 70 ++++++++++ 5 files changed, 683 insertions(+) create mode 100644 src/chatbot/error_solutions.py create mode 100644 src/chatbot/image_handler.py create mode 100644 src/chatbot/knowledge_base.py create mode 100644 src/chatbot/ollama_runner.py create mode 100644 src/chatbot/stt_handler.py diff --git a/src/chatbot/error_solutions.py b/src/chatbot/error_solutions.py new file mode 100644 index 000000000..615a3d63c --- /dev/null +++ b/src/chatbot/error_solutions.py @@ -0,0 +1,106 @@ +# error_solutions.py +from typing import Dict,Any + +ERROR_SOLUTIONS = { + "no ground": { + "description": "Missing ground reference (Node 0)", + "severity": "critical", + "fixes": [ + "Add GND symbol (0) to schematic", + "Ensure all nodes have DC path to ground", + "Add 1GΩ resistors from floating nodes to GND for simulation stability", + "Use GND symbol from eSim power library" + ], + "eSim_command": "Add 'GND' symbol from 'power' library" + }, + + "floating pins": { + "description": "Unconnected component pins", + "severity": "moderate", + "fixes": [ + "Connect all unused pins to appropriate nets", + "For unused inputs: tie to VCC or GND through resistors", + "For unused outputs: leave unconnected but label properly" + ], + "eSim_command": "Use 'Place Wire' tool to connect pins" + }, + + "disconnected wires": { + "description": "Wires not properly connected to pins", + "severity": "critical", + "fixes": [ + "Zoom in and check wire endpoints touch pins", + "Use junction dots at wire intersections", + "Re-route wires to ensure proper connections" + ], + "eSim_command": "Press 'J' to add junction dots" + }, + + "missing spice model": { + "description": "Component lacks SPICE model definition", + "severity": "critical", + "fixes": [ + "Add .lib statement: .lib /usr/share/esim/models.lib", + "Check IC availability in Components/ICs.pdf", + "Use eSim library components only", + "Create custom model using Model Editor" + ], + "eSim_command": "Add '.lib /usr/share/esim/models.lib' in schematic" + }, + + "singular matrix": { + "description": "Simulation convergence error", + "severity": "critical", + "fixes": [ + "Add 1GΩ resistors from ALL nodes → GND", + "Add .options gmin=1e-12 reltol=0.01", + "Use .nodeset for initial voltages", + "Add 0.1Ω series resistors to voltage sources" + ], + "eSim_command": "Add '.options gmin=1e-12 reltol=0.01' in .cir file" + }, + + "missing component values": { + "description": "Components without specified values", + "severity": "moderate", + "fixes": [ + "Double-click components to edit values", + "Set R, C, L values before simulation", + "For ICs: specify model number", + "For sources: set voltage/current values" + ], + "eSim_command": "Double-click component → Edit Properties → Set Value" + }, + + "no load after rectifier": { + "description": "Rectifier output has no load capacitor", + "severity": "warning", + "fixes": [ + "Add filter capacitor after rectifier (100-1000μF)", + "Add load resistor to establish DC operating point", + "Add voltage regulator for stable output" + ], + "eSim_command": "Add capacitor between rectifier output and GND" + } +} + +def get_error_solution(error_message: str) -> Dict[str, Any]: + """Get detailed solution for specific error.""" + error_lower = error_message.lower() + + for error_key, solution in ERROR_SOLUTIONS.items(): + if error_key in error_lower: + return solution + + # Default solution for unknown errors + return { + "description": "General schematic error", + "severity": "unknown", + "fixes": [ + "Check all connections are proper", + "Verify component values are set", + "Ensure ground symbol is present", + "Check for duplicate component IDs" + ], + "eSim_command": "Run Design Rule Check (DRC) in KiCad" + } diff --git a/src/chatbot/image_handler.py b/src/chatbot/image_handler.py new file mode 100644 index 000000000..3938ec307 --- /dev/null +++ b/src/chatbot/image_handler.py @@ -0,0 +1,246 @@ +import os +import json +import base64 +import io +import time +from typing import Dict, Any +from PIL import Image +MAX_IMAGE_BYTES = int(0.5*1024 * 1024) +from .ollama_runner import run_ollama_vision + +# === IMPORT PADDLE OCR === +try: + from paddleocr import PaddleOCR + import logging + logging.getLogger("ppocr").setLevel(logging.ERROR) + + # CRITICAL FIX: Disabled MKLDNN and Angle Classification to prevent VM Crashes + ocr_engine = PaddleOCR( + use_angle_cls=False, # <--- MUST BE FALSE TO STOP SIGABRT + lang='en', + use_gpu=False, # Force CPU + enable_mkldnn=False, # <--- MUST BE FALSE FOR PADDLE v3 COMPATIBILITY + use_mp=False, # Disable multiprocessing + show_log=False + ) + HAS_PADDLE = True + print("[INIT] PaddleOCR initialized (Safe Mode).") +except Exception as e: + HAS_PADDLE = False + print(f"[INIT] PaddleOCR init failed: {e}") + + +def encode_image(image_path: str) -> str: + """Convert image to base64 string.""" + with open(image_path, "rb") as image_file: + return base64.b64encode(image_file.read()).decode("utf-8") + + +def optimize_image_for_vision(image_path: str) -> bytes: + """ + Resize large images to reduce vision model processing time. + Target: Max 1920x1080 while maintaining aspect ratio. + """ + try: + img = Image.open(image_path) + + if img.mode not in ('RGB', 'L'): + img = img.convert('RGB') + + max_width = 1920 + max_height = 1080 + + if img.width > max_width or img.height > max_height: + # Calculate scaling factor + scale = min(max_width / img.width, max_height / img.height) + new_size = (int(img.width * scale), int(img.height * scale)) + img = img.resize(new_size, Image.Resampling.LANCZOS) + print(f"[IMAGE] Resized from {img.width}x{img.height} to {new_size[0]}x{new_size[1]}") + + # Convert to bytes (PNG format prevents compression artifacts on text) + buffer = io.BytesIO() + img.save(buffer, format='PNG', optimize=True, quality=85) + return buffer.getvalue() + + except Exception as e: + print(f"[IMAGE] Optimization failed: {e}, using original") + with open(image_path, 'rb') as f: + return f.read() + + +def extract_text_with_paddle(image_path: str) -> str: + """Extract text using PaddleOCR (Handles rotated/vertical text excellently).""" + if not HAS_PADDLE: + return "" + try: + result = ocr_engine.ocr(image_path, cls=True) + detected_texts = [] + if result and result[0]: + for line in result[0]: + text = line[1][0] + conf = line[1][1] + + if conf > 0.6: + detected_texts.append(text) + + full_text = " ".join(detected_texts) + return full_text + + except Exception as e: + print(f"[OCR] PaddleOCR Failed: {e}") + return "" + +def analyze_and_extract(image_path: str) -> Dict[str, Any]: + """ + Analyze schematic with image optimization, PaddleOCR text injection, and timeout handling. + Rejects images larger than 0.5 MB. + """ + if not os.path.exists(image_path): + return { + "error": "Image file not found", + "vision_summary": "", + "component_counts": {}, + "circuit_analysis": { + "circuit_type": "Unknown", + "design_errors": [], + "design_warnings": [] + }, + "components": [], + "values": {} + } + + try: + file_size = os.path.getsize(image_path) + except OSError as e: + return { + "error": f"Could not read image size: {e}", + "vision_summary": "", + "component_counts": {}, + "circuit_analysis": { + "circuit_type": "Unknown", + "design_errors": [], + "design_warnings": [] + }, + "components": [], + "values": {} + } + + if file_size > MAX_IMAGE_BYTES: + size_mb = round(file_size / (1024 * 1024), 2) + return { + "error": f"Image too large ({size_mb} MB). Max allowed size is 0.5 MB.", + "vision_summary": "", + "component_counts": {}, + "circuit_analysis": { + "circuit_type": "Unknown", + "design_errors": ["Image file size exceeded 0.5 MB limit"], + "design_warnings": [] + }, + "components": [], + "values": {} + } + + # === OPTIMIZE IMAGE BEFORE SENDING === + print(f"[VISION] Processing image: {os.path.basename(image_path)}") + image_bytes = optimize_image_for_vision(image_path) + + # === EXTRACT OCR TEXT (CRITICAL STEP) === + ocr_text = extract_text_with_paddle(image_path) + + if ocr_text: + clean_ocr = ocr_text.strip() + print(f"[VISION] PaddleOCR Hints injected: {clean_ocr[:100]}...") + else: + clean_ocr = "No readable text detected." + + # === PROMPT WITH CONTEXT === + prompt = f""" +ANALYZE THIS ELECTRONICS SCHEMATIC IMAGE. + +CONTEXT FROM OCR SCAN (Text detected in image): +"{clean_ocr}" + +INSTRUCTIONS: +1. Use the OCR text to identify component labels (e.g., if you see "D1" text, there is a Diode, R1,R2,R3... for resistor). +2. Look for rotated text labels near symbols. +3. Identify the circuit topology. + +VERY IMPORTANT INSTRUCTIONS: +1. DON'T OVERCALCULATE MODEL COUNT LIKE MODEL COUNT + OCR COUNT +2. IF THERE IS ANY VALUE NOT PRESENT FOR ANY COMPONENT JUST ADD A QUESTION MARK IN FRONT OF IT + +OUTPUT RULES: +1. Return ONLY valid JSON. +2. Structure: + + +RESPOND WITH JSON ONLY. +""" + + max_retries = 2 + for attempt in range(max_retries): + try: + print(f"[VISION] Attempt {attempt + 1}/{max_retries}...") + + response_text = run_ollama_vision(prompt, image_bytes) + + cleaned_json = response_text.replace("```json", "").replace("```", "").strip() + + if "{" in cleaned_json and "}" in cleaned_json: + start = cleaned_json.index("{") + end = cleaned_json.rindex("}") + 1 + cleaned_json = cleaned_json[start:end] + + data = json.loads(cleaned_json) + + required_keys = ["vision_summary", "component_counts", "circuit_analysis", "components", "values"] + for key in required_keys: + if key not in data: + raise ValueError(f"Missing required key: {key}") + + if not isinstance(data.get("circuit_analysis"), dict): + data["circuit_analysis"] = {"circuit_type": "Unknown", "design_errors": [], "design_warnings": []} + + if "design_errors" not in data["circuit_analysis"]: + data["circuit_analysis"]["design_errors"] = [] + + if not data.get("component_counts") or all(v == 0 for v in data.get("component_counts", {}).values()): + counts = {"R": 0, "C": 0, "U": 0, "Q": 0, "D": 0, "L": 0, "Misc": 0} + for comp in data.get("components", []): + if isinstance(comp, str) and len(comp) > 0: + comp_type = comp[0].upper() + if comp_type in counts: + counts[comp_type] += 1 + elif "DIODE" in comp.upper() or comp.startswith("D"): + counts["D"] = counts.get("D", 0) + 1 + data["component_counts"] = counts + + if data.get("components"): + data["components"] = list(dict.fromkeys(data["components"])) + + print(f"[VISION] Success: {data.get('circuit_analysis', {}).get('circuit_type', 'Unknown')}") + return data + + except Exception as e: + print(f"[VISION] Attempt {attempt + 1} failed: {str(e)}") + if attempt == max_retries - 1: + return { + "error": f"Vision analysis failed: {str(e)}", + "vision_summary": "Unable to analyze circuit image", + "component_counts": {}, + "circuit_analysis": { + "circuit_type": "Unknown", + "design_errors": ["Analysis timed out or failed"], + "design_warnings": [] + }, + "components": [], + "values": {} + } + else: + import time + time.sleep(2) + + +def analyze_image(image_path: str, question: str | None = None, preprocess: bool = True) -> str: + """Helper for manual testing.""" + return str(analyze_and_extract(image_path)) \ No newline at end of file diff --git a/src/chatbot/knowledge_base.py b/src/chatbot/knowledge_base.py new file mode 100644 index 000000000..59b900aee --- /dev/null +++ b/src/chatbot/knowledge_base.py @@ -0,0 +1,119 @@ +import os +import chromadb +from .ollama_runner import get_embedding + +# ==================== DATABASE SETUP ==================== + +# Persistent DB directory (relative to this file) +db_path = os.path.join(os.path.dirname(__file__), "esim_knowledge_db") +chroma_client = chromadb.PersistentClient(path=db_path) + +collection = chroma_client.get_or_create_collection(name="esim_manuals") + +# ==================== INGESTION ==================== +def ingest_pdfs(manuals_directory: str) -> None: + """ + Read the single master text file and index it. + Call this once from src/ingest.py. + """ + if not os.path.exists(manuals_directory): + print("Directory not found.") + return + + # Clear existing DB to ensure no duplicates from old files + print("Clearing old database...") + try: + chroma_client.delete_collection("esim_manuals") + global collection + collection = chroma_client.get_or_create_collection(name="esim_manuals") + except Exception as e: + print(f"Warning clearing DB: {e}") + + # Look for .txt files only + files = [f for f in os.listdir(manuals_directory) if f.lower().endswith(".txt")] + + if not files: + print("❌ No .txt files found to ingest!") + return + + for filename in files: + path = os.path.join(manuals_directory, filename) + print(f"\n📄 Processing Master File: {filename}") + + try: + with open(path, "r", encoding="utf-8") as f: + text = f.read() + + raw_sections = text.split("======================================") + + documents, embeddings, metadatas, ids = [], [], [], [] + + chunk_counter = 0 + for section in raw_sections: + section = section.strip() + if len(section) < 50: + continue + + # Further split large sections by double newlines if needed + sub_chunks = [c.strip() for c in section.split("\n\n") if len(c) > 50] + + for chunk in sub_chunks: + embed = get_embedding(chunk) + if embed: + documents.append(chunk) + embeddings.append(embed) + metadatas.append({"source": filename, "type": "master_ref"}) + ids.append(f"{filename}_{chunk_counter}") + chunk_counter += 1 + + if documents: + collection.add( + documents=documents, + embeddings=embeddings, + metadatas=metadatas, + ids=ids, + ) + print(f" ✅ Indexed {len(documents)} chunks from {filename}") + else: + print(f" ⚠️ No valid chunks found in {filename}") + + except Exception as e: + print(f" ❌ Failed to process {filename}: {e}") + + +# ==================== SEARCH ==================== + +def search_knowledge(query: str, n_results: int = 4) -> str: + """ + Simple semantic search against the single master knowledge file. + """ + try: + # Generate embedding for the user's question + query_embed = get_embedding(query) + if not query_embed: + return "" + + # Query the database + results = collection.query( + query_embeddings=[query_embed], + n_results=n_results, + ) + + docs_list = results.get("documents", []) + + if not docs_list or not docs_list[0]: + print("DEBUG: No relevant info found.") + return "" + + selected_chunks = docs_list[0] + context_text = "\n\n...\n\n".join(selected_chunks) + + if len(context_text) > 3500: + context_text = context_text[:3500] + + header = "=== ESIM OFFICIAL DOCUMENTATION ===\n" + return f"{header}{context_text}\n===================================\n" + + except Exception as e: + print(f"RAG Error: {e}") + return "" diff --git a/src/chatbot/ollama_runner.py b/src/chatbot/ollama_runner.py new file mode 100644 index 000000000..6fdf3b6cb --- /dev/null +++ b/src/chatbot/ollama_runner.py @@ -0,0 +1,142 @@ +import os +import ollama +import json,time + +# Model configuration +VISION_MODELS = {"primary": "minicpm-v:latest"} +TEXT_MODELS = {"default": "llama3.1:8b"} +EMBED_MODEL = "nomic-embed-text" + +ollama_client = ollama.Client( + host="http://localhost:11434", + timeout=300.0, +) + +def run_ollama_vision(prompt: str, image_input: str | bytes) -> str: + """Call minicpm-v:latest with Chain-of-Thought for better accuracy.""" + model = VISION_MODELS["primary"] + + try: + import base64 + + image_b64 = "" + + + if isinstance(image_input, bytes): + image_b64 = base64.b64encode(image_input).decode("utf-8") + + elif os.path.isfile(image_input): + with open(image_input, "rb") as f: + image_b64 = base64.b64encode(f.read()).decode("utf-8") + + elif isinstance(image_input, str) and len(image_input) > 100: + image_b64 = image_input + else: + raise ValueError("Invalid image input format") + + # === CHAIN OF THOUGHT === + system_prompt = ( + "You are an expert Electronics Engineer using eSim.\n" + "Analyze the schematic image carefully.\n\n" + "STEP 1: THINKING PROCESS\n" + "- List visible components (e.g., 'I see 4 diodes in a bridge...').\n" + "- Trace connections (e.g., 'Resistor R1 is in series...').\n" + "- Check against the OCR text provided.\n\n" + "STEP 2: JSON OUTPUT\n" + "After your analysis, output a SINGLE JSON object wrapped in ```json ... ```.\n" + "Structure:\n" + "{\n" + ' "vision_summary": "Summary string",\n' + ' "component_counts": {"R": 0, "C": 0, "D": 0, "Q": 0, "U": 0},\n' + ' "circuit_analysis": {\n' + ' "circuit_type": "Rectifier/Amplifier/etc",\n' + ' "design_errors": [],\n' + ' "design_warnings": []\n' + ' },\n' + ' "components": ["R1", "D1"],\n' + ' "values": {"R1": "1k"}\n' + "}\n" + ) + + resp = ollama_client.chat( + model=model, + messages=[ + {"role": "system", "content": system_prompt}, + { + "role": "user", + "content": prompt, + "images": [image_b64], # <--- MUST BE LIST OF BASE64 STRINGS + }, + ], + options={ + "temperature": 0.0, + "num_ctx": 8192, + "num_predict": 1024, + }, + ) + + content = resp["message"]["content"] + + # === PARSE JSON FROM MIXED OUTPUT === + import re + json_match = re.search(r'```json\s*(\{.*?\})\s*```', content, re.DOTALL) + if json_match: + return json_match.group(1) + + start = content.find('{') + end = content.rfind('}') + 1 + if start != -1 and end != -1: + return content[start:end] + + return "{}" + + except Exception as e: + print(f"[VISION ERROR] {e}") + return json.dumps({ + "vision_summary": f"Vision failed: {str(e)[:50]}", + "component_counts": {}, + "circuit_analysis": {"circuit_type": "Error", "design_errors": [], "design_warnings": []}, + "components": [], "values": {} + }) + +def run_ollama(prompt: str, mode: str = "default") -> str: + """ + OPTIMIZED: Run text model with focused parameters. + """ + model = TEXT_MODELS.get(mode, TEXT_MODELS["default"]) + + try: + resp = ollama_client.chat( + model=model, + messages=[ + { + "role": "system", + "content": "You are an eSim and electronics expert. Be concise, accurate, and practical." + }, + {"role": "user", "content": prompt}, + ], + options={ + "temperature": 0.05, + "num_ctx": 2048, + "num_predict": 400, + "top_p": 0.9, + "repeat_penalty": 1.1, + }, + ) + + return resp["message"]["content"].strip() + + except Exception as e: + return f"[Error] {str(e)}" + + +def get_embedding(text: str): + """ + OPTIMIZED: Get text embeddings for RAG. + """ + try: + r = ollama_client.embeddings(model=EMBED_MODEL, prompt=text) + return r["embedding"] + except Exception as e: + print(f"[EMBED ERROR] {e}") + return None diff --git a/src/chatbot/stt_handler.py b/src/chatbot/stt_handler.py new file mode 100644 index 000000000..0d3352f26 --- /dev/null +++ b/src/chatbot/stt_handler.py @@ -0,0 +1,70 @@ +import os +import json +import queue +import time + +import sounddevice as sd +from vosk import Model, KaldiRecognizer + +_MODEL = None + +def _get_model(): + global _MODEL + model_path = os.environ.get("VOSK_MODEL_PATH", "") + if not model_path or not os.path.isdir(model_path): + raise RuntimeError(f"VOSK_MODEL_PATH not set or not found: {model_path}") + if _MODEL is None: + _MODEL = Model(model_path) + return _MODEL + +def listen_to_mic(should_stop=lambda: False, max_silence_sec=3, samplerate=16000, phrase_limit_sec=8) -> str: + """ + Offline STT using Vosk. + Returns recognized text, or "" if cancelled / timed out. + """ + q = queue.Queue() + rec = KaldiRecognizer(_get_model(), samplerate) + + started = False + t0 = time.time() + t_speech = None + + def callback(indata, frames, time_info, status): + q.put(bytes(indata)) + + with sd.RawInputStream( + samplerate=samplerate, + channels=1, + dtype="int16", + blocksize=8000, + callback=callback, + ): + while True: + if should_stop(): + return "" + + now = time.time() + + # Stop after silence + if not started and (now - t0) >= max_silence_sec: + return "" + + if started and t_speech and (now - t_speech) >= phrase_limit_sec: + break + + try: + data = q.get(timeout=0.2) + except queue.Empty: + continue + + if rec.AcceptWaveform(data): + text = json.loads(rec.Result()).get("text", "").strip() + if text: + return text + else: + partial = json.loads(rec.PartialResult()).get("partial", "").strip() + if partial and not started: + started = True + t_speech = now + + return json.loads(rec.FinalResult()).get("text", "").strip() From 26ec6066a3bfafcbf4a4e03bb7dca822821cd528 Mon Sep 17 00:00:00 2001 From: Hariom Thakur Date: Wed, 7 Jan 2026 18:16:10 +0000 Subject: [PATCH 10/34] Revise eSim netlist analysis output contract Updated the eSim netlist analysis output contract to define chatbot response requirements and provide detailed instructions for users. --- .../esim_netlist_analysis_output_contract.txt | 270 +++--------------- 1 file changed, 39 insertions(+), 231 deletions(-) diff --git a/src/frontEnd/manual/esim_netlist_analysis_output_contract.txt b/src/frontEnd/manual/esim_netlist_analysis_output_contract.txt index d12efaebc..6b33e4b16 100644 --- a/src/frontEnd/manual/esim_netlist_analysis_output_contract.txt +++ b/src/frontEnd/manual/esim_netlist_analysis_output_contract.txt @@ -1,244 +1,52 @@ -Reference -====================================== +ESIM COPILOT NETLIST ANALYSIS OUTPUT CONTRACT +============================================= -TABLE OF CONTENTS -1. eSim Overview & Workflow -2. Schematic Design (KiCad) & Netlist Generation -3. SPICE Netlist Rules & Syntax -4. Simulation Types & Commands -5. Components & Libraries -6. Common Errors & Troubleshooting -7. IC Availability & Knowledge +This file defines HOW the chatbot MUST respond. -====================================================================== -1. ESIM OVERVIEW & WORKFLOW -====================================================================== -eSim is an open-source EDA tool for circuit design, simulation, and PCB layout. -It integrates KiCad (schematic), NgSpice (simulation), and Python (automation). +-------------------------------------------------- +1. INPUT SOURCE +-------------------------------------------------- -1.1 ESIM USER INTERFACE & TOOLBAR ICONS (FROM TOP TO BOTTOM): ----------------------------------------------------------------------- -1. NEW PROJECT (Menu > New Project) - - Function: Creates a new project folder in ~/eSim-Workspace. - - Note: Project name must not have spaces. +The chatbot MUST rely ONLY on FACT blocks like: -2. OPEN SCHEMATIC (Icon: Circuit Diagram) - - Function: Launches KiCad Eeschema (Schematic Editor). - - Usage: - - If new project: Confirms creation of schematic. - - If existing: Opens last saved schematic. - - Key Step: Use "Place Symbol" (A) to add components from eSim_Devices. +[FACT NET_SYNTAX_VALID=YES] +[FACT FLOATING_NODES=NONE] +[FACT MISSING_MODELS=BC547] +... -3. CONVERT KICAD TO NGSPICE (Icon: Gear/Converter) - - Function: Converts the KiCad netlist (.cir) into an NgSpice-compatible netlist (.cir.out). - - Prerequisite: You MUST generate the netlist in KiCad first! - - Features (Tabs inside this tool): - a. Analysis: Set simulation type (.tran, .dc, .ac, .op). - b. Source Details: Set values for SINE, PULSE, AC, DC sources. - c. Ngspice Model: Add parameters for logic gates/flip-flops. - d. Device Modeling: Link diode/transistor models to symbols. - e. Subcircuits: Link subcircuit files to 'X' components. - - Action: Click "Convert" to generate the final simulation file. +The raw netlist is FOR REFERENCE ONLY. -4. SIMULATION (Icon: Play Button/Waveform) - - Function: Launches NgSpice console and plotting window. - - Usage: Click "Simulate" after successful conversion. - - Output: Shows plots and simulation logs. +-------------------------------------------------- +2. OUTPUT SECTIONS (MANDATORY) +-------------------------------------------------- -5. MODEL BUILDER / DEVICE MODELING (Icon: Diode/Graph) - - Function: Create custom SPICE models from datasheet parameters. - - Supported Devices: Diode, BJT, MOSFET, IGBT, JFET. - - Usage: Enter datasheet values (Is, Rs, Cjo) -> Calculate -> Save Model. +The chatbot MUST output EXACTLY these sections: -6. SUBCIRCUIT MAKER (Icon: Chip/IC) - - Function: Convert a schematic into a reusable block (.sub file). - - Usage: Create a schematic with ports -> Generate Netlist -> Click Subcircuit Maker. +1. Syntax / SPICE rule errors +2. Topology / connection problems +3. Simulation setup issues (.ac/.tran/.op etc.) +4. Summary -7. OPENMODELICA (Icon: OM Logo) - - Function: Mixed-signal simulation for mechanical-electrical systems. +-------------------------------------------------- +3. RULES +-------------------------------------------------- -8. MAKERCHIP (Icon: Chip with 'M') - - Function: Cloud-based Verilog/FPGA design. +• If a FACT is NONE → DO NOT invent issues +• If a FACT is present → MUST report it +• Ground issues only if BOTH node0 and GND missing +• Count ALL issues in Summary -STANDARD WORKFLOW: -1. Open eSim → New Project. -2. Open Schematic (Icon 1) → Draw Circuit → Generate Netlist (File > Export > Netlist). -3. Convert (Icon 2) → Set Analysis/Source values → Click Convert. -4. Simulate (Icon 3) → View waveforms. +-------------------------------------------------- +4. SUMMARY FORMAT +-------------------------------------------------- -KEY SHORTCUTS: -- A: Add Component -- W: Add Wire -- M: Move -- R: Rotate -- V: Edit Value -- P: Add Power/Ground -- Delete: Remove item -- Esc: Cancel action +"Total issues detected: X + - Floating nodes: Y + - Missing models: Z + - Missing subcircuits: A + - Voltage conflicts: B + - Missing analysis: C" -====================================================================== -1.2 HANDLING FOLLOW-UP QUESTIONS -====================================================================== -- Context Awareness: eSim workflow is linear (Schematic -> Netlist -> Convert -> Simulate). -- If user asks "What next?" after drawing a schematic, the answer is "Generate Netlist". -- If user asks "What next?" after converting, the answer is "Simulate". - -====================================================================== -2. SCHEMATIC DESIGN (KICAD) & NETLIST GENERATION -====================================================================== -GROUND REQUIREMENT: -- SPICE requires a node "0" as ground reference. -- ALWAYS use the "GND" symbol from the "power" library. -- Do NOT use other grounds (Earth, Chassis) for simulation reference. - -FLOATING NODES: -- Every node must connect to at least two components. -- A node connecting to only one pin is "floating" and causes errors. -- Fix: Connect the pin or use a "No Connect" flag (X) if intentional (but careful with simulation). - -ADDING SOURCES: -- DC Voltage: eSim_Sources:vsource (set DC value) -- AC Voltage: eSim_Sources:vac (set magnitude/phase) -- Sine Wave: eSim_Sources:vsin (set offset, amplitude, freq) -- Pulse: eSim_Sources:vpulse (set V1, V2, delay, rise/fall, width, period) - -HOW TO GENERATE THE NETLIST (STEP-BY-STEP): -This is the most critical step to bridge Schematic and Simulation. - -Method 1: Top Toolbar (Easiest) -1. Look for the "Generate Netlist" icon in the top toolbar. - (It typically looks like a page with text 'NET' or a green plug icon). -2. Click it to open the Export Netlist dialog. - -Method 2: Menu Bar (If icon is missing) -1. Go to "File" menu. -2. Select "Export". -3. Click "Netlist...". - (Note: In some older versions, this may be under "Tools" → "Generate Netlist File"). - -IN THE NETLIST DIALOG: -1. Click the "Spice" tab (Do not use Pcbnew tab). -2. Ensure "Default" format is selected. -3. Click the "Generate Netlist" button. -4. A save dialog appears: - - Ensure the filename is `.cir`. - - Save it inside your project folder. -5. Close the dialog and close Schematic Editor. - -BACK IN ESIM: -1. Select your project in the explorer. -2. Click the "Convert KiCad to NgSpice" button on the toolbar. -3. If successful, you can now proceed to "Simulate". - -====================================================================== -3. SPICE NETLIST RULES & SYNTAX -====================================================================== -A netlist is a text file describing connections. eSim generates it automatically. - -COMPONENT PREFIXES (First letter matters!): -- R: Resistor (R1, R2) -- C: Capacitor (C1) -- L: Inductor (L1) -- D: Diode (D1) -- Q: BJT Transistor (Q1) -- M: MOSFET (M1) -- V: Voltage Source (V1) -- I: Current Source (I1) -- X: Subcircuit/IC (X1) - -SYNTAX EXAMPLES: -Resistor: R1 node1 node2 1k -Capacitor: C1 node1 0 10u -Diode: D1 anode cathode 1N4007 -BJT (NPN): Q1 collector base emitter BC547 -MOSFET: M1 drain gate source bulk IRF540 -Subcircuit: X1 node1 node2 ... subckt_name - -RULES: -- Floating Nodes: Fatal error. -- Voltage Loop: Two ideal voltage sources in parallel = Error. -- Model Definitions: Every diode/transistor needs a .model statement. -- Subcircuits: Every 'X' component needs a .subckt definition. - -====================================================================== -4. SIMULATION TYPES & COMMANDS -====================================================================== -You must define at least one analysis type in your netlist. - -A. TRANSIENT ANALYSIS (.tran) -- Time-domain simulation (like an oscilloscope). -- Syntax: .tran -- Example: .tran 1u 10m (1ms to 10ms) -- Use for: waveforms, pulses, switching circuits. - -B. DC ANALYSIS (.dc) -- Sweeps a source voltage/current. -- Syntax: .dc -- Example: .dc V1 0 5 0.1 (Sweep V1 from 0 to 5V) -- Use for: I-V curves, transistor characteristics. - -C. AC ANALYSIS (.ac) -- Frequency response (Bode plot). -- Syntax: .ac -- Example: .ac dec 10 10 100k (10 points/decade, 10Hz-100kHz) -- Use for: Filters, amplifiers gain/phase. - -D. OPERATING POINT (.op) -- Calculates DC bias points (steady state). -- Syntax: .op -- Result: Lists voltage at every node and current in sources. - -====================================================================== -5. COMPONENTS & LIBRARIES -====================================================================== -LIBRARY PATH: /usr/share/kicad/library/ - -COMMON LIBRARIES: -- eSim_Devices: R, C, L, D, Q, M (Main library) -- power: GND, VCC, +5V (Power symbols) -- eSim_Sources: vsource, vsin, vpulse (Signal sources) -- eSim_Subckt: OpAmps (LM741, LM358), Timers (NE555), Regulators (LM7805) - -HOW TO ADD MODELS: -1. Right-click component → Properties -2. Edit "Spice_Model" field -3. Paste .model or .subckt reference - -MODEL EXAMPLES (Copy-Paste): -.model 1N4007 D(Is=1e-14 Rs=0.1 Bv=1000) -.model BC547 NPN(Bf=200 Is=1e-14 Vaf=100) -.model 2N2222 NPN(Bf=255 Is=1e-14) - -====================================================================== -6. COMMON ERRORS & TROUBLESHOOTING -====================================================================== -ERROR: "Singular Matrix" / "Gmin stepping failed" -- Cause: Floating node, perfect switch, or bad circuit loop. -- Fix 1: Check for unconnected pins. -- Fix 2: Add 1GΩ resistor to ground at floating nodes. -- Fix 3: Add .options gmin=1e-10 to netlist. - -ERROR: "Model not found" / "Subcircuit not found" -- Cause: Component used (e.g., Q1) but no .model defined. -- Fix: Add the missing .model or .subckt definition to the netlist or schematic. - -ERROR: "Project does not contain Kicad netlist file" -- Cause: You forgot to generate the netlist in KiCad or didn't save it as .cir. -- Fix: Go back to Schematic, click File > Export > Netlist, and save as .cir. - -ERROR: "Permission denied" -- Fix: Run eSim as administrator (sudo) or fix workspace permissions. - -====================================================================== -7. IC AVAILABILITY & KNOWLEDGE -====================================================================== -SUPPORTED ICs (via eSim_Subckt library): -- Op-Amps: LM741, LM358, LM324, TL082, AD844 -- Timers: NE555, LM555 -- Regulators: LM7805, LM7812, LM7905, LM317 -- Logic: 7400, 7402, 7404, 7408, 7432, 7486, 7474 (Flip-Flop) -- Comparators: LM311, LM339 -- Optocouplers: 4N35, PC817 - -Status: All listed above are "Completed" and verified for eSim. -""" +-------------------------------------------------- +END OF FILE +-------------------------------------------------- From a5e1f33acabd374b15ff767c749bb0817993784c Mon Sep 17 00:00:00 2001 From: Hariom Thakur Date: Sat, 10 Jan 2026 15:00:21 +0530 Subject: [PATCH 11/34] Add files via upload --- src/frontEnd/Chatbot.py | 1576 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 1576 insertions(+) create mode 100644 src/frontEnd/Chatbot.py diff --git a/src/frontEnd/Chatbot.py b/src/frontEnd/Chatbot.py new file mode 100644 index 000000000..6091fbc8c --- /dev/null +++ b/src/frontEnd/Chatbot.py @@ -0,0 +1,1576 @@ +import sys +import os +import re,threading +from configuration.Appconfig import Appconfig +from chatbot.stt_handler import listen_to_mic +from PyQt5.QtGui import QTextCursor +from PyQt5.QtWidgets import ( + QWidget, QVBoxLayout, QHBoxLayout, QTextEdit, QLineEdit, + QPushButton, QLabel, QFileDialog, QMessageBox, QApplication, QWidget +) +from PyQt5.QtCore import Qt, QThread, pyqtSignal +from PyQt5.QtGui import QFont +MANUALS_DIR = os.path.join(os.path.dirname(__file__), "manuals") +NETLIST_CONTRACT = "" + +try: + contract_path = os.path.join(MANUALS_DIR, "esim_netlist_analysis_output_contract.txt") + with open(contract_path, "r", encoding="utf-8") as f: + NETLIST_CONTRACT = f.read() + print(f"[COPILOT] Loaded netlist contract from {contract_path}") +except Exception as e: + print(f"[COPILOT] WARNING: Could not load netlist contract: {e}") + NETLIST_CONTRACT = ( + "You are a SPICE netlist analyzer.\n" + "Use the FACT lines to detect issues.\n" + "Output sections:\n" + "1. Syntax / SPICE rule errors\n" + "2. Topology / connection problems\n" + "3. Simulation setup issues (.ac/.tran/.op etc.)\n" + "4. Summary\n" + "Do NOT invent issues not present in FACT lines.\n" + ) + +current_dir = os.path.dirname(os.path.abspath(__file__)) +src_dir = os.path.dirname(current_dir) +if src_dir not in sys.path: + sys.path.append(src_dir) + +from chatbot.chatbot_core import handle_input, ESIMCopilotWrapper, clear_history + +import subprocess +import tempfile + +def _validate_netlist_with_ngspice(netlist_text: str) -> bool: + """ + Run ngspice in batch mode to check for SYNTAX errors only. + Returns True if syntax is valid, False for actual parse errors. + Ignores model/library warnings. + """ + try: + with tempfile.NamedTemporaryFile( + mode='w', suffix='.cir', delete=False, encoding='utf-8' + ) as tmp: + tmp.write(netlist_text) + tmp_path = tmp.name + + result = subprocess.run( + ['ngspice', '-b', tmp_path], + capture_output=True, + text=True, + timeout=5 + ) + + try: + os.unlink(tmp_path) + except: + pass + + stderr_lower = result.stderr.lower() + + syntax_errors = [ + 'syntax error', + 'unrecognized', + 'parse error', + 'fatal', + ] + + ignore_patterns = [ + 'model', + 'library', + 'warning', + 'no such file', + 'cannot find', + ] + + for line in stderr_lower.split('\n'): + if any(pattern in line for pattern in ignore_patterns): + continue + if any(err in line for err in syntax_errors): + print(f"[COPILOT] Syntax error: {line}") + return False + + return True + + except Exception as e: + print(f"[COPILOT] Validation exception: {e}") + return True + + +def _detect_missing_subcircuits(netlist_text: str) -> list: + """ + Detect subcircuits that are referenced but not defined. + Returns list of (subckt_name, [(line_num, instance_name), ...]) tuples. + """ + import re + + referenced_subckts = {} + defined_subckts = set() + lines = netlist_text.split('\n') + + for line_num, line in enumerate(lines, start=1): + line = line.strip() + if not line or line.startswith('*'): + continue + + if line.lower().startswith('.subckt'): + tokens = line.split() + if len(tokens) >= 2: + defined_subckts.add(tokens[1].upper()) + + elif line.lower().startswith('.include') or line.lower().startswith('.lib'): + return [] + + elif line[0].upper() == 'X': + tokens = line.split() + if len(tokens) < 2: + continue + + instance_name = tokens[0] + subckt_name = tokens[-1].upper() + + if '=' in subckt_name: + for tok in reversed(tokens[1:]): + if '=' not in tok: + subckt_name = tok.upper() + break + + if subckt_name not in referenced_subckts: + referenced_subckts[subckt_name] = [] + referenced_subckts[subckt_name].append((line_num, instance_name)) + + missing = [] + for subckt, occurrences in referenced_subckts.items(): + if subckt not in defined_subckts: + missing.append((subckt, occurrences)) + + return missing + + +def _detect_voltage_source_conflicts(netlist_text: str) -> list: + """ + Detect multiple voltage sources connected to the same node pair. + Returns list of (node_pair, [(line_num, source_name, value), ...]) tuples. + """ + import re + + voltage_sources = {} + lines = netlist_text.split('\n') + + for line_num, line in enumerate(lines, start=1): + line = line.strip() + if not line or line.startswith('*') or line.startswith('.'): + continue + + tokens = line.split() + if len(tokens) < 4: + continue + + elem_name = tokens[0] + if elem_name[0].upper() != 'V': + continue + + node_plus = tokens[1] + node_minus = tokens[2] + + # Normalize node names + node_plus = re.sub(r'[^\w\-_]', '', node_plus) + node_minus = re.sub(r'[^\w\-_]', '', node_minus) + + if node_plus.lower() in ['0', 'gnd', 'ground', 'vss']: + node_plus = '0' + if node_minus.lower() in ['0', 'gnd', 'ground', 'vss']: + node_minus = '0' + + node_pair = tuple(sorted([node_plus, node_minus])) + + # Extract value + value = "?" + for i, tok in enumerate(tokens[3:], start=3): + tok_upper = tok.upper() + if tok_upper in ['DC', 'AC', 'PULSE', 'SIN', 'PWL']: + if i+1 < len(tokens): + value = tokens[i+1] + break + elif not tok_upper.startswith('.'): + value = tok + break + + if node_pair not in voltage_sources: + voltage_sources[node_pair] = [] + voltage_sources[node_pair].append((line_num, elem_name, value)) + + # Find node pairs with multiple sources + conflicts = [] + for node_pair, sources in voltage_sources.items(): + if len(sources) > 1: + conflicts.append((node_pair, sources)) + + return conflicts + +def _netlist_ground_info(netlist_text: str): + """ + Return (has_node0, has_gnd_label) based ONLY on actual node pins, + not on .tran/.ac parameters or numeric values. + """ + import re + + has_node0 = False + has_gnd_label = False + + lines = netlist_text.split('\n') + for line in lines: + line = line.strip() + # Skip comments, control lines, empty lines + if not line or line.startswith('*') or line.startswith('.'): + continue + + tokens = line.split() + if len(tokens) < 3: + continue + + elem_name = tokens[0] + elem_type = elem_name[0].upper() + nodes = [] + + # Extract nodes based on element type + if elem_type in ['R', 'C', 'L']: + nodes = [tokens[1], tokens[2]] + elif elem_type in ['V', 'I']: + nodes = [tokens[1], tokens[2]] + elif elem_type == 'D': + nodes = [tokens[1], tokens[2]] + elif elem_type == 'Q': + if len(tokens) >= 4: + nodes = [tokens[1], tokens[2], tokens[3]] + elif elem_type == 'M': + if len(tokens) >= 5: + nodes = [tokens[1], tokens[2], tokens[3], tokens[4]] + elif elem_type == 'S': + if len(tokens) >= 5: + nodes = [tokens[1], tokens[2], tokens[3], tokens[4]] + elif elem_type == 'W': + if len(tokens) >= 4: + nodes = [tokens[1], tokens[2]] + elif elem_type in ['E', 'G', 'H', 'F']: + # Controlled sources: check if VALUE-based or linear + if len(tokens) >= 3: + # Check if VALUE keyword exists + has_value = any(tok.upper() == 'VALUE' for tok in tokens) + if has_value: + # Behavioral source: only 2 output nodes + nodes = [tokens[1], tokens[2]] + elif len(tokens) >= 5: + # Linear source: 4 nodes (output pair + control pair) + nodes = [tokens[1], tokens[2], tokens[3], tokens[4]] + else: + # Fallback: at least output pair + nodes = [tokens[1], tokens[2]] + + elif elem_type == 'X': + if len(tokens) >= 3: + nodes = tokens[1:-1] + + for node in nodes: + node = re.sub(r'[=\(\)].*$', '', node) + node = re.sub(r'[^\w\-_]', '', node) + if not node: + continue + + nl = node.lower() + if nl == '0': + has_node0 = True + if nl in ['gnd', 'ground', 'vss']: + has_gnd_label = True + + return has_node0, has_gnd_label + +def _detect_floating_nodes(netlist_text: str) -> list: + """Detect nodes that appear only once (floating/unconnected).""" + import re + + floating_nodes = [] + node_counts = {} + lines = netlist_text.split('\n') + + for line_num, line in enumerate(lines, start=1): + line = line.strip() + if not line or line.startswith('*') or line.startswith('.'): + continue + + tokens = line.split() + if len(tokens) < 3: + continue + + elem_name = tokens[0] + elem_type = elem_name[0].upper() + nodes = [] + + # Extract ONLY nodes (not model names, keywords, or source names) + if elem_type in ['R', 'C', 'L']: + nodes = [tokens[1], tokens[2]] + + elif elem_type in ['V', 'I']: + nodes = [tokens[1], tokens[2]] + + elif elem_type == 'D': + nodes = [tokens[1], tokens[2]] + + elif elem_type == 'Q': + if len(tokens) >= 4: + nodes = [tokens[1], tokens[2], tokens[3]] + + elif elem_type == 'M': + if len(tokens) >= 5: + nodes = [tokens[1], tokens[2], tokens[3], tokens[4]] + + elif elem_type == 'S': + if len(tokens) >= 5: + nodes = [tokens[1], tokens[2], tokens[3], tokens[4]] + + elif elem_type == 'W': + # W n+ n- Vcontrol model + # Vcontrol is a voltage source NAME, not a node + if len(tokens) >= 3: + nodes = [tokens[1], tokens[2]] + + elif elem_type == 'T': + # T n1+ n1- n2+ n2- Z0=val TD=val + # Transmission line: 4 nodes + if len(tokens) >= 5: + nodes = [tokens[1], tokens[2], tokens[3], tokens[4]] + + elif elem_type == 'B': + # B n+ n- = {expr} + # Behavioral source: 2 output nodes + if len(tokens) >= 3: + nodes = [tokens[1], tokens[2]] + + elif elem_type in ['E', 'G']: + # Voltage-controlled sources + if len(tokens) >= 3: + # Check if VALUE keyword exists (behavioral) + line_upper = line.upper() + if 'VALUE' in line_upper or '=' in line: + # Behavioral: only 2 output nodes + nodes = [tokens[1], tokens[2]] + elif len(tokens) >= 5: + # Linear: 4 nodes (out+, out-, ctrl+, ctrl-) + nodes = [tokens[1], tokens[2], tokens[3], tokens[4]] + else: + nodes = [tokens[1], tokens[2]] + + elif elem_type == 'H': + if len(tokens) >= 3: + nodes = [tokens[1], tokens[2]] + + elif elem_type == 'F': + if len(tokens) >= 3: + nodes = [tokens[1], tokens[2]] + + elif elem_type == 'X': + # X node1 node2 ... subckt_name [params] + if len(tokens) >= 3: + candidate_nodes = tokens[1:-1] + nodes = [tok for tok in candidate_nodes if '=' not in tok] + + for node in nodes: + node = re.sub(r'[=\(\)].*$', '', node) + node = re.sub(r'[^\w\-_]', '', node) + + if not node or node[0].isdigit(): + continue + + if node.upper() in ['VALUE', 'V', 'I', 'IF', 'THEN', 'ELSE']: + continue + + # Normalize ground references + node_lower = node.lower() + if node_lower in ['0', 'gnd', 'ground', 'vss']: + node = '0' + + if node not in node_counts: + node_counts[node] = [] + node_counts[node].append((line_num, elem_name)) + + # Find nodes appearing only once (exclude ground) + for node, occurrences in node_counts.items(): + if len(occurrences) == 1 and node != '0': + line_num, elem = occurrences[0] + floating_nodes.append((node, line_num, elem)) + + return floating_nodes + +def _detect_missing_models(netlist_text: str) -> list: + """ + Detect device models that are referenced but not defined. + Returns list of (model_name, [(line_num, elem_name), ...]) tuples. + """ + import re + + referenced_models = {} + defined_models = set() + lines = netlist_text.split('\n') + + for line_num, line in enumerate(lines, start=1): + line = line.strip() + if not line or line.startswith('*'): + continue + + # Check for .model definitions + if line.lower().startswith('.model'): + tokens = line.split() + if len(tokens) >= 2: + defined_models.add(tokens[1].upper()) + + # Check for .include statements (external model libraries) + elif line.lower().startswith('.include') or line.lower().startswith('.lib'): + return [] + + # Extract model references from device lines + elif line[0].upper() in ['D', 'Q', 'M', 'J']: + tokens = line.split() + elem_name = tokens[0] + elem_type = elem_name[0].upper() + + if elem_type == 'D' and len(tokens) >= 4: + model = tokens[3].upper() + if model not in referenced_models: + referenced_models[model] = [] + referenced_models[model].append((line_num, elem_name)) + + elif elem_type == 'Q' and len(tokens) >= 5: + model = tokens[-1].upper() + if not model[0].isdigit(): + if model not in referenced_models: + referenced_models[model] = [] + referenced_models[model].append((line_num, elem_name)) + + elif elem_type == 'M' and len(tokens) >= 6: + model = tokens[5].upper() + if model not in referenced_models: + referenced_models[model] = [] + referenced_models[model].append((line_num, elem_name)) + + # Check for switch models + elif line[0].upper() in ['S', 'W']: + tokens = line.split() + if len(tokens) >= 5: + elem_name = tokens[0] + model = tokens[-1].upper() + if model not in referenced_models: + referenced_models[model] = [] + referenced_models[model].append((line_num, elem_name)) + + # Find models that are referenced but not defined + missing = [] + for model, occurrences in referenced_models.items(): + if model not in defined_models: + missing.append((model, occurrences)) + + return missing + + +class ChatWorker(QThread): + response_ready = pyqtSignal(str) + + def __init__(self, user_input, copilot): + super().__init__() + self.user_input = user_input + self.copilot = copilot + + def run(self): + response = self.copilot.handle_input(self.user_input) + self.response_ready.emit(response) + +class MicWorker(QThread): + result_ready = pyqtSignal(str) + error_occurred = pyqtSignal(str) + + def __init__(self): + super().__init__() + self._stop_requested = False + self._lock = threading.Lock() + + def request_stop(self): + with self._lock: + self._stop_requested = True + + def should_stop(self): + with self._lock: + return self._stop_requested + + def run(self): + try: + text = listen_to_mic(should_stop=self.should_stop, max_silence_sec=3) + self.result_ready.emit(text) + except Exception as e: + self.error_occurred.emit(f"[Error: {e}]") + +class ChatbotGUI(QWidget): + def __init__(self, parent=None): + super().__init__(parent) + self.copilot = ESIMCopilotWrapper() + self.current_image_path = None + self.worker = None + self._mic_worker = None + self._is_listening = False + + # Project context + self._project_dir = None + self._generation_id = 0 # used to ignore stale responses + + self.initUI() + + def set_project_context(self, project_dir: str): + """Called by Application to tell chatbot which project is active.""" + if project_dir and os.path.isdir(project_dir): + self._project_dir = project_dir + proj_name = os.path.basename(project_dir) + self.append_message( + "eSim", + f"Project context set to: {proj_name}\nPath: {project_dir}", + is_user=False, + ) + else: + self._project_dir = None + self.append_message( + "eSim", + "Project context cleared or invalid.", + is_user=False, + ) + + def analyze_current_netlist(self): + """Analyze the active project's netlist.""" + + if self.is_bot_busy(): + return + + if not self._project_dir: + try: + from configuration.Appconfig import Appconfig + obj_appconfig = Appconfig() + active_project = obj_appconfig.current_project.get("ProjectName") + if active_project and os.path.isdir(active_project): + self._project_dir = active_project + proj_name = os.path.basename(active_project) + print(f"[COPILOT] Auto-detected active project: {active_project}") + self.append_message( + "eSim", + f"Auto-detected project: {proj_name}\nPath: {active_project}", + is_user=False, + ) + except Exception as e: + print(f"[COPILOT] Could not auto-detect project: {e}") + + if not self._project_dir: + QMessageBox.warning( + self, + "No project", + "No active eSim project set for the chatbot.", + ) + return + + proj_name = os.path.basename(self._project_dir) + + try: + all_files = os.listdir(self._project_dir) + except Exception as e: + QMessageBox.warning(self, "Error", f"Cannot read project directory:\n{e}") + return + + cir_candidates = [f for f in all_files if f.endswith('.cir') or f.endswith('.cir.out')] + + if not cir_candidates: + QMessageBox.warning( + self, + "Netlist not found", + f"Could not find any .cir or .cir.out files in:\n{self._project_dir}", + ) + return + + netlist_path = None + preferred_out = proj_name + ".cir.out" + if preferred_out in cir_candidates: + netlist_path = os.path.join(self._project_dir, preferred_out) + else: + preferred_cir = proj_name + ".cir" + if preferred_cir in cir_candidates: + netlist_path = os.path.join(self._project_dir, preferred_cir) + else: + if len(cir_candidates) > 1: + from PyQt5.QtWidgets import QInputDialog + item, ok = QInputDialog.getItem( + self, + "Select netlist file", + "Multiple .cir/.cir.out files found in this project.\n" + "Select the one you want to analyze:", + cir_candidates, + 0, + False, + ) + if ok and item: + netlist_path = os.path.join(self._project_dir, item) + elif len(cir_candidates) == 1: + netlist_path = os.path.join(self._project_dir, cir_candidates[0]) + + if not netlist_path or not os.path.exists(netlist_path): + QMessageBox.warning(self, "Netlist not found", "Could not determine which netlist to use.") + return + + netlist_name = os.path.basename(netlist_path) + self.append_message( + "eSim", + f"Using netlist file:\n{netlist_name}", + is_user=False, + ) + + try: + with open(netlist_path, "r", encoding="utf-8", errors="ignore") as f: + netlist_text = f.read() + except Exception as e: + QMessageBox.warning(self, "Error", f"Failed to read netlist:\n{e}") + return + + # === RUN ALL DETECTORS === + print(f"[COPILOT] Analyzing netlist: {netlist_path}") + is_syntax_valid = _validate_netlist_with_ngspice(netlist_text) + print(f"[COPILOT] Ngspice syntax check: {'PASS' if is_syntax_valid else 'FAIL'}") + + floating_nodes = _detect_floating_nodes(netlist_text) + if floating_nodes: + print(f"[COPILOT] Found {len(floating_nodes)} floating node(s):") + for node, line_num, elem in floating_nodes: + print(f" - Node '{node}' at line {line_num} ({elem})") + + missing_models = _detect_missing_models(netlist_text) + if missing_models: + print(f"[COPILOT] Found {len(missing_models)} missing model(s):") + for model, occurrences in missing_models: + print(f" - Model '{model}' used {len(occurrences)} time(s) but not defined") + + missing_subckts = _detect_missing_subcircuits(netlist_text) + if missing_subckts: + print(f"[COPILOT] Found {len(missing_subckts)} missing subcircuit(s):") + for subckt, occurrences in missing_subckts: + print(f" - Subcircuit '{subckt}' used {len(occurrences)} time(s) but not defined") + + voltage_conflicts = _detect_voltage_source_conflicts(netlist_text) + if voltage_conflicts: + print(f"[COPILOT] Found {len(voltage_conflicts)} voltage source conflict(s):") + for node_pair, sources in voltage_conflicts: + print(f" - Nodes {node_pair}: {len(sources)} sources") + for line_num, name, val in sources: + print(f" * {name} (line {line_num}, value={val})") + + import re + text_lower = netlist_text.lower() + + has_tran = ".tran" in text_lower + has_ac = ".ac" in text_lower + has_op = ".op" in text_lower + + has_node0, has_gnd_label = _netlist_ground_info(netlist_text) + + if not has_node0 and not has_gnd_label: + print("[COPILOT] WARNING: No ground reference (node 0 or GND) found!") + + # Build descriptions + if floating_nodes: + floating_desc = "; ".join([f"{node} (line {line_num}, {elem})" + for node, line_num, elem in floating_nodes]) + else: + floating_desc = "NONE" + + if missing_models: + missing_desc = "; ".join([f"{model} (used {len(occs)} times)" + for model, occs in missing_models]) + else: + missing_desc = "NONE" + + if missing_subckts: + subckt_desc = "; ".join([f"{subckt} (used {len(occs)} times)" + for subckt, occs in missing_subckts]) + else: + subckt_desc = "NONE" + + if voltage_conflicts: + conflict_parts = [] + for node_pair, sources in voltage_conflicts: + src_desc = ", ".join([f"{name}={val}" for _, name, val in sources]) + conflict_parts.append(f"{node_pair}: {src_desc}") + voltage_conflict_desc = "; ".join(conflict_parts) + else: + voltage_conflict_desc = "NONE" + + facts = [ + f"NET_SYNTAX_VALID={'YES' if is_syntax_valid else 'NO'}", + f"NET_HAS_NODE_0={'YES' if has_node0 else 'NO'}", + f"NET_HAS_GND_LABEL={'YES' if has_gnd_label else 'NO'}", + f"NET_HAS_TRAN={'YES' if has_tran else 'NO'}", + f"NET_HAS_AC={'YES' if has_ac else 'NO'}", + f"NET_HAS_OP={'YES' if has_op else 'NO'}", + f"FLOATING_NODES={floating_desc}", + f"MISSING_MODELS={missing_desc}", + f"MISSING_SUBCKTS={subckt_desc}", + f"VOLTAGE_CONFLICTS={voltage_conflict_desc}", + ] + + facts_block = "\n".join(f"[FACT {f}]" for f in facts) + print(f"[COPILOT] FACTS being sent:\n{facts_block}") + + # === BUILD PROMPT (SIMPLIFIED USING CONTRACT FILE) === + + full_query = ( + f"{NETLIST_CONTRACT}\n\n" + "=== NETLIST FACTS (MACHINE-GENERATED) ===\n" + "The following lines describe the analyzed netlist in a structured way.\n" + "Each line has the form [FACT KEY=VALUE].\n" + "You MUST rely ONLY on these FACTS, not on the raw netlist.\n\n" + f"{facts_block}\n\n" + "=== RAW NETLIST (FOR REFERENCE ONLY, DO NOT RE-ANALYZE TO FIND NEW ERRORS) ===\n" + "[ESIM_NETLIST_START]\n" + f"{netlist_text}\n" + "[ESIM_NETLIST_END]\n\n" + "REMINDERS:\n" + "- Do NOT invent issues that are not present in the FACT lines.\n" + "- If a FACT says NONE, you MUST NOT report any issue for that category.\n" + "- Follow the output format and rules described in the contract above.\n" + ) + + + # Show synthetic user message + self.append_message( + "You", + f"Analyze current netlist of project '{proj_name}' for design mistakes, " + "missing connections, or bad values.", + is_user=True, + ) + + # Disable UI and run worker + self.input_field.setDisabled(True) + self.send_btn.setDisabled(True) + if hasattr(self, "attach_btn"): + self.attach_btn.setDisabled(True) + if hasattr(self, "mic_btn"): + self.mic_btn.setDisabled(True) + if hasattr(self, "analyze_netlist_btn"): + self.analyze_netlist_btn.setDisabled(True) + if hasattr(self, "clear_btn"): + self.clear_btn.setDisabled(True) + self.loading_label.show() + + self._generation_id += 1 + current_gen = self._generation_id + + self.worker = ChatWorker(full_query, self.copilot) + self.worker.response_ready.connect( + lambda resp, gen=current_gen: self._handle_response_with_id(resp, gen) + ) + self.worker.finished.connect(self.on_worker_finished) + self.worker.start() + + + def analyze_specific_netlist(self, netlist_path: str): + """Analyze a specific netlist file (called from ProjectExplorer context menu).""" + + if self.is_bot_busy(): + return + + if not os.path.exists(netlist_path): + QMessageBox.warning( + self, + "File not found", + f"Netlist file does not exist:\n{netlist_path}", + ) + return + + netlist_name = os.path.basename(netlist_path) + self.append_message( + "eSim", + f"Analyzing specific netlist:\n{netlist_name}", + is_user=False, + ) + + try: + with open(netlist_path, "r", encoding="utf-8", errors="ignore") as f: + netlist_text = f.read() + except Exception as e: + QMessageBox.warning(self, "Error", f"Failed to read netlist:\n{e}") + return + + # === RUN ALL DETECTORS (IDENTICAL TO analyze_current_netlist) === + print(f"[COPILOT] Analyzing netlist: {netlist_path}") + is_syntax_valid = _validate_netlist_with_ngspice(netlist_text) + print(f"[COPILOT] Ngspice syntax check: {'PASS' if is_syntax_valid else 'FAIL'}") + + floating_nodes = _detect_floating_nodes(netlist_text) + if floating_nodes: + print(f"[COPILOT] Found {len(floating_nodes)} floating node(s):") + for node, line_num, elem in floating_nodes: + print(f" - Node '{node}' at line {line_num} ({elem})") + + missing_models = _detect_missing_models(netlist_text) + if missing_models: + print(f"[COPILOT] Found {len(missing_models)} missing model(s):") + for model, occurrences in missing_models: + print(f" - Model '{model}' used {len(occurrences)} time(s) but not defined") + + missing_subckts = _detect_missing_subcircuits(netlist_text) + if missing_subckts: + print(f"[COPILOT] Found {len(missing_subckts)} missing subcircuit(s):") + for subckt, occurrences in missing_subckts: + print(f" - Subcircuit '{subckt}' used {len(occurrences)} time(s) but not defined") + + voltage_conflicts = _detect_voltage_source_conflicts(netlist_text) + if voltage_conflicts: + print(f"[COPILOT] Found {len(voltage_conflicts)} voltage source conflict(s):") + for node_pair, sources in voltage_conflicts: + print(f" - Nodes {node_pair}: {len(sources)} sources") + for line_num, name, val in sources: + print(f" * {name} (line {line_num}, value={val})") + + import re + text_lower = netlist_text.lower() + + has_tran = ".tran" in text_lower + has_ac = ".ac" in text_lower + has_op = ".op" in text_lower + + has_node0, has_gnd_label = _netlist_ground_info(netlist_text) + + if not has_node0 and not has_gnd_label: + print("[COPILOT] WARNING: No ground reference (node 0 or GND) found!") + + # Build descriptions (IDENTICAL TO analyze_current_netlist) + if floating_nodes: + floating_desc = "; ".join([f"{node} (line {line_num}, {elem})" + for node, line_num, elem in floating_nodes]) + else: + floating_desc = "NONE" + + if missing_models: + missing_desc = "; ".join([f"{model} (used {len(occs)} times)" + for model, occs in missing_models]) + else: + missing_desc = "NONE" + + if missing_subckts: + subckt_desc = "; ".join([f"{subckt} (used {len(occs)} times)" + for subckt, occs in missing_subckts]) + else: + subckt_desc = "NONE" + + if voltage_conflicts: + conflict_parts = [] + for node_pair, sources in voltage_conflicts: + src_desc = ", ".join([f"{name}={val}" for _, name, val in sources]) + conflict_parts.append(f"{node_pair}: {src_desc}") + voltage_conflict_desc = "; ".join(conflict_parts) + else: + voltage_conflict_desc = "NONE" + + facts = [ + f"NET_SYNTAX_VALID={'YES' if is_syntax_valid else 'NO'}", + f"NET_HAS_NODE_0={'YES' if has_node0 else 'NO'}", + f"NET_HAS_GND_LABEL={'YES' if has_gnd_label else 'NO'}", + f"NET_HAS_TRAN={'YES' if has_tran else 'NO'}", + f"NET_HAS_AC={'YES' if has_ac else 'NO'}", + f"NET_HAS_OP={'YES' if has_op else 'NO'}", + f"FLOATING_NODES={floating_desc}", + f"MISSING_MODELS={missing_desc}", + f"MISSING_SUBCKTS={subckt_desc}", + f"VOLTAGE_CONFLICTS={voltage_conflict_desc}", + ] + + facts_block = "\n".join(f"[FACT {f}]" for f in facts) + print(f"[COPILOT] FACTS being sent:\n{facts_block}") + + # === BUILD PROMPT (IDENTICAL TO analyze_current_netlist) === + # === BUILD PROMPT (SIMPLIFIED USING CONTRACT FILE) === + + full_query = ( + f"{NETLIST_CONTRACT}\n\n" + "=== NETLIST FACTS (MACHINE-GENERATED) ===\n" + "The following lines describe the analyzed netlist in a structured way.\n" + "Each line has the form [FACT KEY=VALUE].\n" + "You MUST rely ONLY on these FACTS, not on the raw netlist.\n\n" + f"{facts_block}\n\n" + "=== RAW NETLIST (FOR REFERENCE ONLY, DO NOT RE-ANALYZE TO FIND NEW ERRORS) ===\n" + "[ESIM_NETLIST_START]\n" + f"{netlist_text}\n" + "[ESIM_NETLIST_END]\n\n" + "REMINDERS:\n" + "- Do NOT invent issues that are not present in the FACT lines.\n" + "- If a FACT says NONE, you MUST NOT report any issue for that category.\n" + "- Follow the output format and rules described in the contract above.\n" + ) + + # Show synthetic user message + self.append_message( + "You", + f"Analyze netlist '{netlist_name}' for design mistakes, " + "missing connections, or bad values.", + is_user=True, + ) + + # Disable UI and run worker + self.input_field.setDisabled(True) + self.send_btn.setDisabled(True) + if hasattr(self, "attach_btn"): + self.attach_btn.setDisabled(True) + if hasattr(self, "mic_btn"): + self.mic_btn.setDisabled(True) + if hasattr(self, "analyze_netlist_btn"): + self.analyze_netlist_btn.setDisabled(True) + if hasattr(self, "clear_btn"): + self.clear_btn.setDisabled(True) + self.loading_label.show() + + self._generation_id += 1 + current_gen = self._generation_id + + self.worker = ChatWorker(full_query, self.copilot) + self.worker.response_ready.connect( + lambda resp, gen=current_gen: self._handle_response_with_id(resp, gen) + ) + self.worker.finished.connect(self.on_worker_finished) + self.worker.start() + + + def stop_analysis(self): + """Stop chat worker and mic worker safely.""" + try: + # Stop mic + if getattr(self, "_mic_worker", None) and self._mic_worker.isRunning(): + self._mic_worker.request_stop() + self._mic_worker.quit() + self._mic_worker.wait(200) + if self._mic_worker.isRunning(): + self._mic_worker.terminate() + self._reset_mic_ui() + + # Stop chat worker + if self.worker and self.worker.isRunning(): + self.worker.quit() + self.worker.wait(500) + if self.worker.isRunning(): + self.worker.terminate() + except Exception as e: + print(f"Stop analysis error: {e}") + + def start_listening(self): + # If already listening -> stop + if self._mic_worker and self._mic_worker.isRunning(): + self._mic_worker.request_stop() + return + + # Start listening (do NOT disable mic button) + self.mic_btn.setStyleSheet(""" + QPushButton { background-color: #e74c3c; color: white; border-radius: 20px; font-size: 18px; } + """) + self.mic_btn.setEnabled(True) + self.input_field.setPlaceholderText("Listening... (click mic to stop)") + QApplication.processEvents() + + self._mic_worker = MicWorker() + self._mic_worker.result_ready.connect(self._on_mic_result) + self._mic_worker.error_occurred.connect(self._on_mic_error) + self._mic_worker.finished.connect(self._reset_mic_ui) + self._mic_worker.start() + + def _on_mic_result(self, text): + self._reset_mic_ui() + if text and text.strip(): + self.input_field.setText(text.strip()) + self.input_field.setFocus() + + def _on_mic_error(self, error_msg): + """Handle speech recognition errors.""" + # Only show popup for REAL errors, not timeouts + if "[Error:" in error_msg and "No speech" not in error_msg: + QMessageBox.warning(self, "Microphone Error", error_msg) + + def _reset_mic_ui(self): + self.mic_btn.setStyleSheet(""" + QPushButton { + background-color: #ffffff; + border: 1px solid #bdc3c7; + border-radius: 20px; + font-size: 18px; + } + QPushButton:hover { + background-color: #ffebee; + border-color: #e74c3c; + } + """) + self.mic_btn.setEnabled(True) + self.input_field.setPlaceholderText("Ask eSim Copilot...") + + def initUI(self): + """Initialize the Chatbot GUI Layout.""" + + # Main Layout + self.layout = QVBoxLayout() + self.layout.setContentsMargins(10, 10, 10, 10) + self.layout.setSpacing(10) + + # --- HEADER AREA (Title + Netlist + Clear Button) --- + header_layout = QHBoxLayout() + + title_label = QLabel("eSim Copilot") + title_label.setStyleSheet("font-weight: bold; font-size: 14px; color: #34495e;") + header_layout.addWidget(title_label) + + header_layout.addStretch() # Push buttons to the right + + # NEW: Analyze Netlist button + self.analyze_netlist_btn = QPushButton("Netlist ▶") + self.analyze_netlist_btn.setFixedHeight(30) + self.analyze_netlist_btn.setToolTip("Analyze active project's netlist") + self.analyze_netlist_btn.setCursor(Qt.PointingHandCursor) + self.analyze_netlist_btn.setStyleSheet(""" + QPushButton { + background-color: #2ecc71; + color: white; + border-radius: 15px; + padding: 0 10px; + font-size: 12px; + } + QPushButton:hover { + background-color: #27ae60; + } + """) + # This method should be defined in ChatbotGUI + # def analyze_current_netlist(self): ... + self.analyze_netlist_btn.clicked.connect(self.analyze_current_netlist) + header_layout.addWidget(self.analyze_netlist_btn) + + # Clear button + self.clear_btn = QPushButton("🗑️") + self.clear_btn.setFixedSize(30, 30) + self.clear_btn.setToolTip("Clear Chat History") + self.clear_btn.setCursor(Qt.PointingHandCursor) + self.clear_btn.setStyleSheet(""" + QPushButton { + background-color: transparent; + border: 1px solid #ddd; + border-radius: 15px; + font-size: 14px; + } + QPushButton:hover { + background-color: #ffebee; + border-color: #ef9a9a; + } + """) + self.clear_btn.clicked.connect(self.clear_chat) + header_layout.addWidget(self.clear_btn) + + self.layout.addLayout(header_layout) + + # --- CHAT DISPLAY AREA --- + self.chat_display = QTextEdit() + self.chat_display.setReadOnly(True) + self.chat_display.setFont(QFont("Segoe UI", 10)) + self.chat_display.setStyleSheet(""" + QTextEdit { + background-color: #f5f6fa; + border: 1px solid #dcdcdc; + border-radius: 8px; + padding: 10px; + } + """) + self.layout.addWidget(self.chat_display) + + # PROGRESS INDICATOR (Hidden by default) + self.loading_label = QLabel("⏳ eSim Copilot is thinking...") + self.loading_label.setAlignment(Qt.AlignCenter) + self.loading_label.setStyleSheet(""" + background-color: #fff3cd; + color: #856404; + border: 1px solid #ffeeba; + border-radius: 5px; + padding: 5px; + font-weight: bold; + """) + self.loading_label.hide() + self.layout.addWidget(self.loading_label) + + # --- INPUT AREA CONTAINER --- + input_layout = QHBoxLayout() + input_layout.setSpacing(8) + + # A. ATTACH BUTTON + self.attach_btn = QPushButton("📎") + self.attach_btn.setFixedSize(40, 40) + self.attach_btn.setToolTip("Attach Circuit Image") + self.attach_btn.setCursor(Qt.PointingHandCursor) + self.attach_btn.setStyleSheet(""" + QPushButton { + border: 1px solid #bdc3c7; + border-radius: 20px; + background-color: #ffffff; + color: #555; + font-size: 18px; + } + QPushButton:hover { + background-color: #ecf0f1; + border-color: #95a5a6; + } + """) + self.attach_btn.clicked.connect(self.browse_image) + input_layout.addWidget(self.attach_btn) + + # B. TEXT INPUT FIELD + self.input_field = QLineEdit() + self.input_field.setPlaceholderText("Ask eSim Copilot...") + self.input_field.setFixedHeight(40) + self.input_field.setStyleSheet(""" + QLineEdit { + border: 1px solid #bdc3c7; + border-radius: 20px; + padding-left: 15px; + padding-right: 15px; + background-color: #ffffff; + font-size: 14px; + } + QLineEdit:focus { + border: 2px solid #3498db; + } + """) + self.input_field.returnPressed.connect(self.send_message) + input_layout.addWidget(self.input_field) + + # --- MIC BUTTON --- + self.mic_btn = QPushButton("🎤") + self.mic_btn.setFixedSize(40, 40) + self.mic_btn.setToolTip("Speak to type") + self.mic_btn.setCursor(Qt.PointingHandCursor) + self.mic_btn.setStyleSheet(""" + QPushButton { + background-color: #ffffff; + border: 1px solid #bdc3c7; + border-radius: 20px; + font-size: 18px; + } + QPushButton:hover { + background-color: #ffebee; /* Light red hover */ + border-color: #e74c3c; + } + """) + self.mic_btn.clicked.connect(self.start_listening) + input_layout.addWidget(self.mic_btn) + + # C. SEND BUTTON + self.send_btn = QPushButton("➤") + self.send_btn.setFixedSize(40, 40) + self.send_btn.setToolTip("Send Message") + self.send_btn.setCursor(Qt.PointingHandCursor) + self.send_btn.setStyleSheet(""" + QPushButton { + background-color: #3498db; + color: white; + border: none; + border-radius: 20px; + font-size: 16px; + padding-bottom: 2px; + } + QPushButton:hover { + background-color: #2980b9; + } + QPushButton:pressed { + background-color: #1abc9c; + } + """) + self.send_btn.clicked.connect(self.send_message) + input_layout.addWidget(self.send_btn) + + self.layout.addLayout(input_layout) + + # --- IMAGE STATUS ROW (label + remove button) --- + status_layout = QHBoxLayout() + status_layout.setSpacing(5) + status_layout.setContentsMargins(0, 0, 0, 0) + + self.filename_status = QLabel("No image attached") + self.filename_status.setStyleSheet("color: gray; font-size: 12px;") + self.filename_status.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) + status_layout.addWidget(self.filename_status) + + self.remove_btn = QPushButton("×") + self.remove_btn.setFixedSize(25, 25) + self.remove_btn.setStyleSheet(""" + QPushButton { + background: #ff6b6b; + color: white; + border: none; + border-radius: 12px; + font-weight: bold; + font-size: 14px; + } + QPushButton:hover { background: #ff5252; } + """) + self.remove_btn.clicked.connect(self.remove_image) + self.remove_btn.hide() # hidden by default + status_layout.addWidget(self.remove_btn) + + status_widget = QWidget() + status_widget.setLayout(status_layout) + self.layout.addWidget(status_widget) + + self.setLayout(self.layout) + + # Initial message + self.append_message( + "eSim Copilot", + "Hello! I am ready to help you analyze circuits.", + is_user=False, + ) + + # ---------- IMAGE HANDLING ---------- + + def browse_image(self): + """Open file dialog to select image (Updates Status Label ONLY).""" + options = QFileDialog.Options() + file_path, _ = QFileDialog.getOpenFileName( + self, + "Select Circuit Image", + "", + "Images (*.png *.jpg *.jpeg *.bmp *.tiff *.gif);;All Files (*)", + options=options + ) + + if file_path: + self.current_image_path = file_path # Store path internally + short_name = os.path.basename(file_path) + + # Update Status Row (Visual Feedback) + self.filename_status.setText(f"📎 {short_name} attached") + self.filename_status.setStyleSheet("color: green; font-weight: bold; font-size: 12px;") + self.remove_btn.show() + + # Focus input so user can start typing question immediately + self.input_field.setFocus() + + def is_bot_busy(self): + """Check if a background worker is currently running.""" + if hasattr(self, "worker") and self.worker is not None: + if self.worker.isRunning(): + QMessageBox.warning(self, "Busy", "Chatbot is currently busy processing a request.\nPlease wait.") + return True + return False + + + def remove_image(self): + """Clear selected image (status + input tag).""" + self.current_image_path = None + self.filename_status.setText("No image attached") + self.filename_status.setStyleSheet("color: gray; font-size: 12px;") + self.remove_btn.hide() + + # ---------- CHAT / HISTORY ---------- + + def clear_chat(self): + """Stop analysis, clear chat, and optionally export history.""" + # 1) Stop any ongoing analysis first + self.stop_analysis() + self._generation_id += 1 + + # 2) Ask user about exporting history + reply = QMessageBox.question( + self, + "Clear History", + "Clear chat history?\nPress 'Yes' to export to a file first, 'No' to clear without saving.", + QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel + ) + if reply == QMessageBox.Cancel: + return + if reply == QMessageBox.Yes: + self.export_history() + + # 3) Clear UI + self.chat_display.clear() + + # 4) Clear backend memory/context + try: + clear_history() + except Exception: + pass + + # 5) Reset welcome line + self.append_message("eSim Copilot", "Chat cleared. Ready for new queries.", is_user=False) + + + def export_history(self): + """Export chat to text file.""" + text = self.chat_display.toPlainText() + if not text.strip(): + return + + file_path, _ = QFileDialog.getSaveFileName( + self, + "Export Chat History", + "chat_history.txt", + "Text Files (*.txt)" + ) + if file_path: + with open(file_path, "w", encoding="utf-8") as f: + f.write(text) + QMessageBox.information(self, "Exported", f"History saved to:\n{file_path}") + + def send_message(self): + user_text = self.input_field.text().strip() + + # Don't send if empty and no image + if not user_text and not self.current_image_path: + return + + full_query = user_text + display_text = user_text + + if self.current_image_path: + short_name = os.path.basename(self.current_image_path) + + # 1) BACKEND QUERY (hidden tag with FULL PATH) + full_query = f"[Image: {self.current_image_path}] {user_text}".strip() + + # 2) USER-VISIBLE TEXT (show filename here, not in input box) + question_part = user_text if user_text else "" + if question_part: + display_text = f"📎 {short_name}\n\n{question_part}" + else: + display_text = f"📎 {short_name}" + + # Reset image state & status row + self.current_image_path = None + self.filename_status.setText("No image attached") + self.filename_status.setStyleSheet("color: gray; font-size: 12px;") + self.remove_btn.hide() + else: + full_query = user_text + display_text = user_text + + # Show user bubble with image name (if any) + self.append_message("You", display_text, is_user=True) + self.input_field.clear() + + # Disable while waiting + self.input_field.setDisabled(True) + self.send_btn.setDisabled(True) + if hasattr(self, "attach_btn"): + self.attach_btn.setDisabled(True) + if hasattr(self, 'mic_btn'): + self.mic_btn.setDisabled(True) + + # NEW: also disable Netlist and Clear during any answer + if hasattr(self, "analyze_netlist_btn"): + self.analyze_netlist_btn.setDisabled(True) + if hasattr(self, "clear_btn"): + self.clear_btn.setDisabled(True) + + self.loading_label.show() + + # NEW: bump generation id and use it to filter responses + self._generation_id += 1 + current_gen = self._generation_id + + self.worker = ChatWorker(full_query, self.copilot) + self.worker.response_ready.connect( + lambda resp, gen=current_gen: self._handle_response_with_id(resp, gen) + ) + self.worker.finished.connect(self.on_worker_finished) + self.worker.start() + + + def on_worker_finished(self): + """Re-enable UI after worker completes.""" + self.input_field.setEnabled(True) + self.send_btn.setEnabled(True) + if hasattr(self, 'attach_btn'): + self.attach_btn.setEnabled(True) + if hasattr(self, 'mic_btn'): + self.mic_btn.setEnabled(True) + + # NEW: re-enable Netlist and Clear + if hasattr(self, "analyze_netlist_btn"): + self.analyze_netlist_btn.setEnabled(True) + if hasattr(self, "clear_btn"): + self.clear_btn.setEnabled(True) + + self.loading_label.hide() + self.input_field.setFocus() + + def _handle_response_with_id(self, response: str, gen_id: int): + """Only accept responses from the current generation.""" + if gen_id != self._generation_id: + # Stale response from a cancelled/cleared analysis -> ignore + return + self.append_message("eSim Copilot", response, is_user=False) + + def handle_response(self, response): + # Kept for backward compatibility if used elsewhere, + # but route everything through _handle_response_with_id with current id. + self._handle_response_with_id(response, self._generation_id) + + + @staticmethod + def format_text_to_html(text): + """Helper to convert basic Markdown to HTML for the Qt TextEdit.""" + import html + # 1. Escape existing HTML to prevent injection + text = html.escape(text) + + # 2. Convert **bold** to bold + text = re.sub(r'\*\*(.*?)\*\*', r'\1', text) + + # 3. Convert headers ### to

+ text = re.sub(r'###\s*(.*)', r'

\1

', text) + + # 4. Convert newlines to
for HTML rendering + text = text.replace('\n', '
') + return text + + def append_message(self, sender, text, is_user): + """Append message INSTANTLY (Text Only, No Image Rendering).""" + if not text: + return + + # 1. Define Headers + if is_user: + header = "You" + else: + header = "eSim Copilot" + + cursor = self.chat_display.textCursor() + cursor.movePosition(QTextCursor.End) + + # 2. Insert Header + cursor.insertHtml(f"
{header}
") + + # 3. Format Text (Bold, Newlines) but NO Image generation + # Use the helper function if you added it inside the class + formatted_text = self.format_text_to_html(text) + + # 4. Insert Text Instantly + cursor.insertHtml(formatted_text) + + self.chat_display.setTextCursor(cursor) + self.chat_display.ensureCursorVisible() + + # ---------- CLEAN SHUTDOWN ---------- + + def closeEvent(self, event): + """Stop analysis when the chatbot window/dock is closed.""" + # Ensure worker is stopped so it doesn't keep using CPU + self.stop_analysis() + + # Clear backend context as well + try: + clear_history() + except Exception: + pass + + event.accept() + + def debug_error(self, error_log_path: str): + """ + Called by Application when a simulation error happens. + Reads ngspice_error.log and asks the copilot to explain + fix it in eSim. + """ + if not error_log_path or not os.path.exists(error_log_path): + QMessageBox.warning( + self, + "Error log missing", + f"Could not find error log at:\n{error_log_path}", + ) + return + + try: + with open(error_log_path, "r", encoding="utf-8", errors="ignore") as f: + log_text = f.read() + except Exception as e: + QMessageBox.warning(self, "Error", f"Failed to read error log:\n{e}") + return + + # Show trimmed log in the chat for user visibility + tail_lines = "\n".join(log_text.splitlines()[-40:]) # last 40 lines + display = ( + "Automatic ngspice error captured from eSim:\n\n" + "```" + f"{tail_lines}\n" + "```" + ) + self.append_message("eSim", display, is_user=False) + + # Build a focused query for the backend + full_query = ( + "The following is an ngspice error log from an eSim simulation.\n" + "1) Explain the exact root cause in simple terms.\n" + "2) Give concrete, step‑by‑step instructions to fix it INSIDE eSim " + "(KiCad schematic / sources / analysis settings).\n\n" + "[NGSPICE_ERROR_LOG_START]\n" + f"{log_text}\n" + "[NGSPICE_ERROR_LOG_END]" + ) + + # Disable UI while analysis is running + self.input_field.setDisabled(True) + self.send_btn.setDisabled(True) + if hasattr(self, "attach_btn"): + self.attach_btn.setDisabled(True) + if hasattr(self, "mic_btn"): + self.mic_btn.setDisabled(True) + if hasattr(self, "analyze_netlist_btn"): + self.analyze_netlist_btn.setDisabled(True) + if hasattr(self, "clear_btn"): + self.clear_btn.setDisabled(True) + + self.loading_label.show() + + # NEW: bump generation and bind response with this gen + self._generation_id += 1 + current_gen = self._generation_id + + self.worker = ChatWorker(full_query, self.copilot) + self.worker.response_ready.connect( + lambda resp, gen=current_gen: self._handle_response_with_id(resp, gen) + ) + self.worker.finished.connect(self.on_worker_finished) + self.worker.start() + +from PyQt5.QtWidgets import QDockWidget +from PyQt5.QtCore import Qt + +def createchatbotdock(parent=None): + """ + Factory function for DockArea / Application integration. + Returns a QDockWidget containing a ChatbotGUI instance. + """ + dock = QDockWidget("eSim Copilot", parent) + dock.setAllowedAreas(Qt.RightDockWidgetArea | Qt.LeftDockWidgetArea) + + chatbot_widget = ChatbotGUI(parent) + dock.setWidget(chatbot_widget) + return dock + + +# Standalone test +if __name__ == "__main__": + app = QApplication(sys.argv) + w = ChatbotGUI() + w.resize(500, 600) + w.show() + sys.exit(app.exec_()) + +def create_chatbot_dock(parent=None): + """Factory function for DockArea integration.""" + from PyQt5.QtWidgets import QDockWidget + from PyQt5.QtCore import Qt + + dock = QDockWidget("eSim Copilot", parent) + dock.setAllowedAreas(Qt.RightDockWidgetArea | Qt.LeftDockWidgetArea) + + chatbot_widget = ChatbotGUI(parent) + dock.setWidget(chatbot_widget) + + return dock From bbd3fc991f3c5c7b60b3af4f5e9e8f0c4df2cd61 Mon Sep 17 00:00:00 2001 From: Hariom Thakur Date: Wed, 14 Jan 2026 15:41:00 +0530 Subject: [PATCH 12/34] Add files via upload --- src/chatbot/setup_chatbot.py | 123 +++++++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 src/chatbot/setup_chatbot.py diff --git a/src/chatbot/setup_chatbot.py b/src/chatbot/setup_chatbot.py new file mode 100644 index 000000000..c7479dcc6 --- /dev/null +++ b/src/chatbot/setup_chatbot.py @@ -0,0 +1,123 @@ +""" +One-time setup for eSim Copilot (Chatbot). +Safe to run multiple times. +""" + +import os +import sys +import subprocess +import shutil +import urllib.request +import zipfile + +BASE_DIR = os.path.dirname(__file__) +MARKER = os.path.join(BASE_DIR, ".chatbot_ready") + +# ================= CONFIG ================= + +PYTHON_PACKAGES = [ + "ollama", + "chromadb", + "pillow", + "paddleocr", + "vosk", + "sounddevice", + "numpy", +] + +OLLAMA_MODELS = [ + "llama3.1:8b", + "minicpm-v", + "nomic-embed-text", +] + +VOSK_MODEL_URL = ( + "https://alphacephei.com/vosk/models/" + "vosk-model-small-en-us-0.15.zip" +) + +VOSK_DIR = os.path.join(BASE_DIR, "models", "vosk") + +# ========================================== + + +def run(cmd): + subprocess.check_call(cmd, shell=True) + + +def already_done(): + return os.path.exists(MARKER) + + +def mark_done(): + with open(MARKER, "w") as f: + f.write("ready") + + +def install_python_deps(): + print("📦 Installing Python dependencies...") + for pkg in PYTHON_PACKAGES: + run(f"{sys.executable} -m pip install {pkg}") + + +def check_ollama(): + print("🧠 Checking Ollama...") + try: + import ollama + ollama.list() + except Exception: + print("❌ Ollama not running") + print("👉 Start it using: ollama serve") + sys.exit(1) + + +def pull_ollama_models(): + print("⬇️ Pulling Ollama models (one-time)...") + for model in OLLAMA_MODELS: + run(f"ollama pull {model}") + + +def setup_vosk(): + print("🎙️ Setting up Vosk...") + if os.path.exists(VOSK_DIR): + print("✅ Vosk model already exists") + return + + os.makedirs(VOSK_DIR, exist_ok=True) + zip_path = os.path.join(VOSK_DIR, "vosk.zip") + + print("⬇️ Downloading Vosk model...") + urllib.request.urlretrieve(VOSK_MODEL_URL, zip_path) + + with zipfile.ZipFile(zip_path, "r") as z: + z.extractall(VOSK_DIR) + + os.remove(zip_path) + + extracted = os.listdir(VOSK_DIR)[0] + model_path = os.path.join(VOSK_DIR, extracted) + + print("\n⚠️ IMPORTANT:") + print(f"Set environment variable:") + print(f"VOSK_MODEL_PATH={model_path}\n") + + +def main(): + print("\n=== eSim Copilot One-Time Setup ===\n") + + if already_done(): + print("✅ Chatbot already set up. Nothing to do.") + return + + install_python_deps() + check_ollama() + pull_ollama_models() + setup_vosk() + + mark_done() + print("\n🎉 eSim Copilot setup COMPLETE!") + print("You can now launch eSim normally.") + + +if __name__ == "__main__": + main() From cbba3b077d69e2ea6a494814c1dc9c6522ea877b Mon Sep 17 00:00:00 2001 From: Hariom Thakur Date: Fri, 16 Jan 2026 11:23:50 +0530 Subject: [PATCH 13/34] Add files via upload --- images/chatbot.png | Bin 0 -> 2710 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 images/chatbot.png diff --git a/images/chatbot.png b/images/chatbot.png new file mode 100644 index 0000000000000000000000000000000000000000..4c36aee84f042c515142272d7abeb2aaa762218a GIT binary patch literal 2710 zcmV;H3TgF;P)5i;IgI8XA6neu063^z`(QkdXQL`RnWJ(b3WO_xERKXTrk5$jHc?oSfw3 zY_88$OR~-{LsH za2E+U&V0*c7rl+h@QnG`w_FzSn#b^rwr|O7Ve%-W7)9HTZ|SUHBxiU=6?7~@G5Trw zme3dv2*uMmQugiIK`BPjmv12rF<0Oi#lEF90`@UpQEI&doEkg^zU4GPU5#QCO?*pA z$G0lSrEf{ez|V~YzC|UYtJtv(9$_24MhHhv*Rz=0JUdzj-|Eb{(?=e;ainA zHn&ryc=RnP9cxvNB|SNQu~4J9@-3$c6wQEg=jfDT$R*FvspJBleG6#{+@L#@KXHYA zl4Z42dB7wO<<>Vh^Z<&R@^91&`YRd5hHudUgNEfztCjXH3~v@0&J@Glg=1$e7!>A@c!8x-iGgn(Af9Yc3`v{j z!9qSLbc#{*VM;NRlUc>}mj`c{z0~6`Q6$4^bm3H-4@1alck05MH_CgZTNjSugzd*^ zMfgHrsbgy)2S~qu`Z`T+m91NIUmq>rS0g)W6aGuIDV`x)GscW8a!Jhm}=Ayd$2m` zuq$YorJ}DIndj(0Fq=r^)VBhbrQ@jIT96pzp?6*+9Q8vU<>UCO{perd_*zYlvvs?9 zL*u)29KRch^zpQp=SMs+MVf{R$H`ms<9q)-Z;x!aD4kI}_}*`xmv1V5t3>SRD0Vn^ z<@Nh0$4<;=!I!4Z(-Zz>A7V6ACKWtdrJM<<92(>JCO+_L;-|DSBg`;!84#WCR0ET5 zFvo7**b#cdEN@WD1`b_AvNQWG^Gm36l$4576K0ZTLeRwrg}?0rsH7d?_#%6V*0T8| zTMil?084*;KS&}R;5e2)MTw69p9~NpMmQeMe*^DSkRw#AqaQ+o_S2*MOhUx}-K|(X zgPbE+&T`P^2nr(q!ZD6N3jI)0j$QaU-4F?~pQOqalRui_MH^C%QVns1CP!p7X+i_# z*v6|iWE|11KfP9QwDnA8@Sq$ApxyvEqC)AV^U!xKdu1rcMJ=~UNQEP|RW&y&x|;0B zMIVgtKap|VqfhEs=fF{m$l;PBzP3(3mBL`i;yJd^2cxM5M}?`05_iTSN91%oL#Omo zVV+R&9Q*j%Ixbd_zvEx{9u;PC>eQ3qdR10;;gaKDI3F$BInq(|H<0Zd1CHpEmhDxU z2kDd})(xRWu~z$mSCcxBPB|i0fNo;~azaW2noc>wBe^~`Db{Ma*I(QqPoTQx2p%8$ z)0Z58|)Jg(af|by}hH{*l)C5Ywg%DSGxT1Are@+<3j!}HC;nfBNj;R&{?59ls zfBPJNBlgIBqNW_9N9ZdPhe2ECc8OgbNPR@}y%(tM~db3TzdCBhC;3H1{{;U`eQ1@>BoG_Rog#H8@F2+aAY}a3v!&k%w9a_s_ggk z#@f=znq*D=`2hdya=f5Fe(3dPW1AB|SSyWw(=v~3|7{O^-nY^IFz`|XWoB5d132~m~4O;`1lPdSKQ(5E! zT;!BAuaTKx##5D@VVB6LU-x_l7aTA&o1KA+L=k-#wt!YEV{zaED#T(-K3*51vBMXj zlP@6TD^tV!B~x)>a?Nb2VK1~B#Z$3FgR2ozcT>KGG%zqd)M#t4k}w0H9m=rTmJmTP zdi!E_^0n@N7;CpK&Jr-34xepjTdbPx3lu+s-oJ)DEWffrcH?_Wz^mg`UZC?UPAtra z1sfAlOSTUM<*dMp4F=^WzmR9gV)KV}Sc+1#fV)`eug3~D_EexecL!>1@LRS4t5d~W zvPp^1Lul!bA?CJ?=x%;`26!TqFt>}$JVz@S<{SgkKkhiaQ0Zgp&aj+KpH_RW++-*F z!>+UI+n!(JQ4yANLO>SI5^TV*$GGPs5neJaOqik)np>uXMEd+Zics(q>J29#SU-90 z0VR}@`S(;PV1r=tjKy?^@<%8>rzTiTv_YmXG%8=Y&l8RJD&m~o;SFi(+oIXgG-|e(@Dtrhft7gO6GP5Gw?!9=BXozwI#_{As7+L9c-}N*}tEpJ8idc{s;xUY~xy+Z6jt&~E&qS6ZW0-?aHVhZ(w^n~E5Dbx#6 zqx~zVP%q%-l7E&n@1}s{eBAn$#2s#%ReG&f6)^dY;FPhDK?k$?uspWSf7&8>!fI{_ Q`Tzg`07*qoM6N<$f@?%MHvj+t literal 0 HcmV?d00001 From a2b38c0f7e69bbcf497df417519ed512ed3609c2 Mon Sep 17 00:00:00 2001 From: Hariom Thakur Date: Fri, 16 Jan 2026 17:12:48 +0530 Subject: [PATCH 14/34] Fix import statement for browse_path module --- src/frontEnd/DockArea.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/frontEnd/DockArea.py b/src/frontEnd/DockArea.py index 2fc238319..a0fcd4788 100755 --- a/src/frontEnd/DockArea.py +++ b/src/frontEnd/DockArea.py @@ -17,7 +17,7 @@ from converter.ltspiceToKicad import LTspiceConverter from converter.LtspiceLibConverter import LTspiceLibConverter from converter.libConverter import PspiceLibConverter -from converter.browseSchematics import browse_path +from converter.browseSchematic import browse_path dockList = ['Welcome'] count = 1 dock = {} From 026e9270cf6fbe503f56cb6cf533d372443b32fb Mon Sep 17 00:00:00 2001 From: Hariom Thakur Date: Fri, 16 Jan 2026 17:47:46 +0530 Subject: [PATCH 15/34] Create README for eSim Copilot project Added detailed documentation for eSim Copilot, including features, installation instructions, and system dependencies. --- README_CHATBOT.md | 90 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 README_CHATBOT.md diff --git a/README_CHATBOT.md b/README_CHATBOT.md new file mode 100644 index 000000000..b67326387 --- /dev/null +++ b/README_CHATBOT.md @@ -0,0 +1,90 @@ +# eSim Copilot – AI-Assisted Electronics Simulation Tool + +eSim Copilot is an AI-powered assistant integrated into **eSim**, designed to help users analyze electronic circuits, debug SPICE netlists, understand simulation errors, and interact using text, voice, and images. + +This project combines **PyQt5**, **ngspice**, **Ollama (LLMs)**, **RAG (ChromaDB)**, **OCR**, and **offline speech-to-text** into a single desktop application. + +--- + +## Key Features + +- AI assistant for electronics & eSim +- Netlist analysis and error explanation +- ngspice simulation integration +- Circuit image analysis (OCR + vision models) +- Offline speech-to-text (no internet required) +- Knowledge base using RAG (manuals + docs) +- Fully offline-capable (except model downloads) + +## Supported Platform + +- **Linux only** (Recommended: Ubuntu 22.04 / 23.04 / 24.04) +- Tested on **Ubuntu 22.04 & 24.04** + +--- + +## Python Version (VERY IMPORTANT) + +## Supported +- **Python 3.9 – 3.10 (RECOMMENDED)** + +Check version: +```bash +python --version + +## System Dependencies (Install First) +```bash + +sudo apt update +sudo apt upgrade + +sudo apt install ngspice +sudo apt install portaudio19-dev +sudo apt install libgl1 libglib2.0-0 + +## Ollama (LLM Backend) +```bash + +curl -fsSL https://ollama.com/install.sh | sh +ollama serve +ollama pull qwen2.5:3b +ollama pull minicpm-v +ollama pull nomic-embed-text + +## Offline Speech-to-Text (VOSK) +```bash + +mkdir -p ~/vosk-models +cd ~/vosk-models +wget https://alphacephei.com/vosk/models/vosk-model-small-en-us-0.15.zip +unzip vosk-model-small-en-us-0.15.zip + +export VOSK_MODEL_PATH=~/vosk-models/vosk-model-small-en-us-0.15 + +echo 'export VOSK_MODEL_PATH=~/vosk-models/vosk-model-small-en-us-0.15' >> ~/.bashrc +source ~/.bashrc + +## Python Virtual Environment (Recommended) +```bash + +python3.10 -m venv venv +source venv/bin/activate +pip install --upgrade pip setuptools wheel + +pip install -r requirements.txt + +pip install hdlparse==1.0.4 + + +## Running the Application +```bash +cd src/frontEnd +python Application.py + + +## Common Warnings (Safe to Ignore) + +PaddleOCR init failed: show_log +QSocketNotifier: Can only be used with threads started with QThread +libpng iCCP: incorrect sRGB profile +PyQt sipPyTypeDict() deprecation warnings From a3e7bcdff07870fb0d0792c1a5dae75575a2348a Mon Sep 17 00:00:00 2001 From: Hariom Thakur Date: Fri, 16 Jan 2026 17:55:36 +0530 Subject: [PATCH 16/34] Add new libraries to requirements.txt for chatbot --- requirements.txt | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 3715e0d09..a408dbcb0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -23,4 +23,21 @@ python-dateutil==2.9.0.post0 scipy==1.10.1 six==1.17.0 watchdog==4.0.2 -zipp==3.20.2 \ No newline at end of file +zipp==3.20.2 +ollama +chromadb +sentence-transformers +psutil +protobuf<5 +regex +opencv-python +paddleocr==2.7.0.3 +paddlepaddle==2.5.2 +vosk +sounddevice +requests +tqdm +pyyaml +setuptools==65.5.0 +wheel +PyQtWebEngine From 3d62a51dde13c1120fbe2d457cda081f25b95d79 Mon Sep 17 00:00:00 2001 From: Hariom Thakur Date: Fri, 16 Jan 2026 22:17:38 +0530 Subject: [PATCH 17/34] Add files via upload --- src/ingest.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 src/ingest.py diff --git a/src/ingest.py b/src/ingest.py new file mode 100644 index 000000000..c85474cfa --- /dev/null +++ b/src/ingest.py @@ -0,0 +1,30 @@ +import os +import sys +current_dir = os.path.dirname(os.path.abspath(__file__)) +sys.path.append(current_dir) + +from chatbot.knowledge_base import ingest_pdfs + +pdf_folder = os.path.join(current_dir, "manuals") + +if not os.path.exists(pdf_folder): + print(f"Error: Folder not found: {pdf_folder}") + sys.exit(1) + +print(f"📂 Scanning folder: {pdf_folder}") + +files = [f for f in os.listdir(pdf_folder) if f.endswith('.pdf') or f.endswith('.txt')] +print(f"📄 Found {len(files)} Document(s): {files}") + +if not files: + print("No PDFs or Text files found to ingest.") + sys.exit() + +print("\n🚀 Starting Ingestion... (Press Ctrl+C to stop)") +try: + ingest_pdfs(pdf_folder) + print("\n✅ Ingestion Complete!") +except KeyboardInterrupt: + print("\n⚠️ Ingestion stopped by user.") +except Exception as e: + print(f"\n❌ Error: {e}") From 99ad3d2a9e3bd85804522e40556c3712440ec254 Mon Sep 17 00:00:00 2001 From: Hariom Thakur Date: Fri, 16 Jan 2026 16:49:08 +0000 Subject: [PATCH 18/34] Change section splitting method in knowledge_base.py --- src/chatbot/knowledge_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/chatbot/knowledge_base.py b/src/chatbot/knowledge_base.py index 59b900aee..fb1d0821a 100644 --- a/src/chatbot/knowledge_base.py +++ b/src/chatbot/knowledge_base.py @@ -44,7 +44,7 @@ def ingest_pdfs(manuals_directory: str) -> None: with open(path, "r", encoding="utf-8") as f: text = f.read() - raw_sections = text.split("======================================") + raw_sections = text.split("\n\n") documents, embeddings, metadatas, ids = [], [], [], [] From a18715e0304656b8c0c1d75060d66b9173290ac4 Mon Sep 17 00:00:00 2001 From: Hariom Thakur Date: Fri, 16 Jan 2026 16:51:50 +0000 Subject: [PATCH 19/34] Add ingest manuals section to README Added instructions for ingesting manuals for RAG. --- README_CHATBOT.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README_CHATBOT.md b/README_CHATBOT.md index b67326387..3d35a5add 100644 --- a/README_CHATBOT.md +++ b/README_CHATBOT.md @@ -75,13 +75,16 @@ pip install -r requirements.txt pip install hdlparse==1.0.4 +## Ingest manuals for RAG +```bash +cd src +python Ingest.py ## Running the Application ```bash cd src/frontEnd python Application.py - ## Common Warnings (Safe to Ignore) PaddleOCR init failed: show_log From 4381a5188410bd7722dd634705cb7c21c8489985 Mon Sep 17 00:00:00 2001 From: Hariom Thakur Date: Fri, 16 Jan 2026 16:54:42 +0000 Subject: [PATCH 20/34] Delete src/chatbot/setup_chatbot.py --- src/chatbot/setup_chatbot.py | 123 ----------------------------------- 1 file changed, 123 deletions(-) delete mode 100644 src/chatbot/setup_chatbot.py diff --git a/src/chatbot/setup_chatbot.py b/src/chatbot/setup_chatbot.py deleted file mode 100644 index c7479dcc6..000000000 --- a/src/chatbot/setup_chatbot.py +++ /dev/null @@ -1,123 +0,0 @@ -""" -One-time setup for eSim Copilot (Chatbot). -Safe to run multiple times. -""" - -import os -import sys -import subprocess -import shutil -import urllib.request -import zipfile - -BASE_DIR = os.path.dirname(__file__) -MARKER = os.path.join(BASE_DIR, ".chatbot_ready") - -# ================= CONFIG ================= - -PYTHON_PACKAGES = [ - "ollama", - "chromadb", - "pillow", - "paddleocr", - "vosk", - "sounddevice", - "numpy", -] - -OLLAMA_MODELS = [ - "llama3.1:8b", - "minicpm-v", - "nomic-embed-text", -] - -VOSK_MODEL_URL = ( - "https://alphacephei.com/vosk/models/" - "vosk-model-small-en-us-0.15.zip" -) - -VOSK_DIR = os.path.join(BASE_DIR, "models", "vosk") - -# ========================================== - - -def run(cmd): - subprocess.check_call(cmd, shell=True) - - -def already_done(): - return os.path.exists(MARKER) - - -def mark_done(): - with open(MARKER, "w") as f: - f.write("ready") - - -def install_python_deps(): - print("📦 Installing Python dependencies...") - for pkg in PYTHON_PACKAGES: - run(f"{sys.executable} -m pip install {pkg}") - - -def check_ollama(): - print("🧠 Checking Ollama...") - try: - import ollama - ollama.list() - except Exception: - print("❌ Ollama not running") - print("👉 Start it using: ollama serve") - sys.exit(1) - - -def pull_ollama_models(): - print("⬇️ Pulling Ollama models (one-time)...") - for model in OLLAMA_MODELS: - run(f"ollama pull {model}") - - -def setup_vosk(): - print("🎙️ Setting up Vosk...") - if os.path.exists(VOSK_DIR): - print("✅ Vosk model already exists") - return - - os.makedirs(VOSK_DIR, exist_ok=True) - zip_path = os.path.join(VOSK_DIR, "vosk.zip") - - print("⬇️ Downloading Vosk model...") - urllib.request.urlretrieve(VOSK_MODEL_URL, zip_path) - - with zipfile.ZipFile(zip_path, "r") as z: - z.extractall(VOSK_DIR) - - os.remove(zip_path) - - extracted = os.listdir(VOSK_DIR)[0] - model_path = os.path.join(VOSK_DIR, extracted) - - print("\n⚠️ IMPORTANT:") - print(f"Set environment variable:") - print(f"VOSK_MODEL_PATH={model_path}\n") - - -def main(): - print("\n=== eSim Copilot One-Time Setup ===\n") - - if already_done(): - print("✅ Chatbot already set up. Nothing to do.") - return - - install_python_deps() - check_ollama() - pull_ollama_models() - setup_vosk() - - mark_done() - print("\n🎉 eSim Copilot setup COMPLETE!") - print("You can now launch eSim normally.") - - -if __name__ == "__main__": - main() From 7d40d766d0f35bc036d6651e7cc4543a2989d8b8 Mon Sep 17 00:00:00 2001 From: Hariom Thakur Date: Sat, 17 Jan 2026 10:22:53 +0530 Subject: [PATCH 21/34] Change default text model to qwen2.5:3b --- src/chatbot/ollama_runner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/chatbot/ollama_runner.py b/src/chatbot/ollama_runner.py index 6fdf3b6cb..dd84041d4 100644 --- a/src/chatbot/ollama_runner.py +++ b/src/chatbot/ollama_runner.py @@ -4,7 +4,7 @@ # Model configuration VISION_MODELS = {"primary": "minicpm-v:latest"} -TEXT_MODELS = {"default": "llama3.1:8b"} +TEXT_MODELS = {"default": "qwen2.5:3b"} EMBED_MODEL = "nomic-embed-text" ollama_client = ollama.Client( From d446cc638546d024238c3e4f6db31b537e3a4f5e Mon Sep 17 00:00:00 2001 From: Hariom Thakur Date: Sat, 17 Jan 2026 05:06:09 +0000 Subject: [PATCH 22/34] Update eSim reference manual with new sections --- .../esim_netlist_analysis_output_contract.txt | 175 ++++++++++++++++++ 1 file changed, 175 insertions(+) diff --git a/src/manuals/esim_netlist_analysis_output_contract.txt b/src/manuals/esim_netlist_analysis_output_contract.txt index d12efaebc..5b55af636 100644 --- a/src/manuals/esim_netlist_analysis_output_contract.txt +++ b/src/manuals/esim_netlist_analysis_output_contract.txt @@ -241,4 +241,179 @@ SUPPORTED ICs (via eSim_Subckt library): - Optocouplers: 4N35, PC817 Status: All listed above are "Completed" and verified for eSim. + + +====================================================================== +8. ABOUT ESIM PROJECT +====================================================================== + +WHO DEVELOPED eSim: +- eSim is developed and maintained by **FOSSEE (Free/Libre and Open Source Software in Education)**. +- FOSSEE is a project under the **Indian Institute of Technology (IIT) Bombay**. +- The goal of eSim is to promote **open-source Electronic Design Automation (EDA)** tools for education and research. + +FUNDING & SUPPORT: +- eSim is funded by the **Ministry of Education (MoE), Government of India**. +- It is part of the **National Mission on Education through ICT (NMEICT)**. + +WHY eSim EXISTS: +- To provide a **free alternative** to commercial EDA tools (like Proteus, Multisim). +- To help students learn circuit simulation, PCB design, and SPICE modeling. +- To integrate multiple open tools into one workflow: + - KiCad → Schematic & PCB + - NgSpice → Simulation + - Python → Automation & analysis + +OFFICIAL WEBSITE: +- https://esim.fossee.in + +====================================================================== +9. BASIC ELECTRONICS RULES (VERY IMPORTANT FOR SIMULATION) +====================================================================== + +These rules apply to **ALL circuits**, regardless of software. + +GENERAL CIRCUIT RULES: +1. Every circuit MUST have a closed loop. +2. Current always flows from higher potential to lower potential (conventional current). +3. Voltage is always measured between two nodes. +4. Power is consumed by loads, supplied by sources. + +GROUND RULE: +- Ground (node 0) is the **reference point** for all voltages. +- Without ground, SPICE cannot solve equations. +- One ground per circuit is enough (multiple grounds must be same node). + +KIRCHHOFF’S LAWS: +1. KCL (Current Law): + - Sum of currents entering a node = sum of currents leaving the node. +2. KVL (Voltage Law): + - Sum of voltages around a closed loop = 0. + +PASSIVE SIGN CONVENTION: +- If current enters the positive terminal of an element, power is absorbed. +- If current enters the negative terminal, power is delivered. + +====================================================================== +10. COMMON SPICE SIMULATION MISTAKES (STUDENTS OFTEN ASK) +====================================================================== + +MISTAKE 1: No ground in schematic +- Symptom: "singular matrix" error +- Fix: Add GND symbol from power library + +MISTAKE 2: Floating pins on ICs +- Symptom: convergence errors, random voltages +- Fix: Tie unused inputs to GND or VCC via resistors + +MISTAKE 3: Ideal voltage source loop +- Symptom: "Voltage source loop" error +- Cause: Two voltage sources directly connected +- Fix: Add small resistor (0.1Ω – 1Ω) + +MISTAKE 4: Missing simulation command +- Symptom: Simulation runs but no output +- Fix: Add .tran, .ac, .dc, or .op command + +MISTAKE 5: Extremely small timestep +- Symptom: Simulation very slow or fails +- Fix: Increase timestep or reduce stop time + +====================================================================== +11. HOW TO READ NGSPICE ERROR MESSAGES +====================================================================== + +ERROR: "Singular matrix" +Meaning: +- Circuit equations cannot be solved +Common causes: +- Floating nodes +- Missing ground +- Ideal switches +Fix: +- Add leakage resistors (1GΩ to ground) + +ERROR: "Time step too small" +Meaning: +- Solver cannot converge +Fix: +- Increase timestep +- Add series resistance +- Reduce frequency + +ERROR: "Model not found" +Meaning: +- Component model missing +Fix: +- Add .model statement +- Include model library +- Use eSim standard components + +====================================================================== +12. HOW eSim STORES FILES (USERS OFTEN ASK) +====================================================================== + +DEFAULT WORKSPACE: +- ~/eSim-Workspace/ + +PROJECT STRUCTURE: +/ + ├── .proj → KiCad project + ├── .sch → Schematic + ├── .cir → Raw netlist + ├── .cir.out → NgSpice netlist + ├── .raw → Simulation results + └── plots/ → Waveforms + +IMPORTANT: +- .cir.out file is overwritten every time you convert +- Manual edits in .cir.out are TEMPORARY unless added in schematic + +====================================================================== +13. FREQUENTLY ASKED QUESTIONS (FAQ) +====================================================================== + +Q: Can I use eSim offline? +A: Yes. eSim works fully offline once installed. + +Q: Is KiCad mandatory? +A: Yes. eSim uses KiCad for schematic and PCB design. + +Q: Can I edit netlist manually? +A: Yes, but changes will be lost after reconversion. + +Q: Why does simulation work in LTspice but not eSim? +A: eSim enforces stricter SPICE rules (ground, floating nodes). + +Q: Can eSim do PCB layout? +A: Yes, using KiCad PCB editor. + +====================================================================== +14. BEST PRACTICES FOR STUDENTS & PROJECTS +====================================================================== + +- Always name projects without spaces +- Always add ground first +- Simulate simple blocks before full circuit +- Save schematic before converting +- Use standard eSim components whenever possible +- Check netlist if simulation fails +- Keep backup of working projects + +====================================================================== +15. LIMITATIONS OF eSim (HONEST & IMPORTANT) +====================================================================== + +- Not all ICs are available by default +- Digital simulation is limited compared to Verilog tools +- Large circuits may simulate slowly +- PCB autorouting depends on KiCad + +These are normal limitations of SPICE-based tools. + +====================================================================== +END OF ESIM REFERENCE MANUAL +====================================================================== + +""" """ From 7980edaa435e959b0b5ab69d6866bb0844332f78 Mon Sep 17 00:00:00 2001 From: Hariom Thakur Date: Sat, 17 Jan 2026 05:10:07 +0000 Subject: [PATCH 23/34] Remove extra quotation marks in manual --- src/manuals/esim_netlist_analysis_output_contract.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/src/manuals/esim_netlist_analysis_output_contract.txt b/src/manuals/esim_netlist_analysis_output_contract.txt index 5b55af636..10c03d74b 100644 --- a/src/manuals/esim_netlist_analysis_output_contract.txt +++ b/src/manuals/esim_netlist_analysis_output_contract.txt @@ -416,4 +416,3 @@ END OF ESIM REFERENCE MANUAL ====================================================================== """ -""" From a4344042f1a2a380f022fd39fa6ccd48bc4d7dde Mon Sep 17 00:00:00 2001 From: Hariom Thakur Date: Sat, 17 Jan 2026 06:10:56 +0000 Subject: [PATCH 24/34] Update README with paddlepaddle installation command Add installation command for paddlepaddle version 2.5.2. --- README_CHATBOT.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README_CHATBOT.md b/README_CHATBOT.md index 3d35a5add..94ad41622 100644 --- a/README_CHATBOT.md +++ b/README_CHATBOT.md @@ -74,6 +74,8 @@ pip install --upgrade pip setuptools wheel pip install -r requirements.txt pip install hdlparse==1.0.4 +pip install paddlepaddle==2.5.2 \ + -f https://www.paddlepaddle.org.cn/whl/linux/mkl/avx/stable.html ## Ingest manuals for RAG ```bash From 37ff858d438b3202284cc5df4c487da6fffaa077 Mon Sep 17 00:00:00 2001 From: Hariom Thakur Date: Sat, 17 Jan 2026 06:30:46 +0000 Subject: [PATCH 25/34] Fix case sensitivity in Python script command --- README_CHATBOT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README_CHATBOT.md b/README_CHATBOT.md index 94ad41622..d6e53314e 100644 --- a/README_CHATBOT.md +++ b/README_CHATBOT.md @@ -80,7 +80,7 @@ pip install paddlepaddle==2.5.2 \ ## Ingest manuals for RAG ```bash cd src -python Ingest.py +python ingest.py ## Running the Application ```bash From 8807f8926ccb98420edcef5d724e06f730425b1b Mon Sep 17 00:00:00 2001 From: Hariom Thakur Date: Sat, 17 Jan 2026 17:47:56 +0000 Subject: [PATCH 26/34] Enhance response handling with RAG and topic detection Added RAG fallback mechanism to improve response accuracy. Implemented semantic topic switch detection for better context handling. --- src/chatbot/chatbot_core.py | 137 +++++++++++++++++++++++++----------- 1 file changed, 97 insertions(+), 40 deletions(-) diff --git a/src/chatbot/chatbot_core.py b/src/chatbot/chatbot_core.py index 52842fb01..0205e7d4c 100644 --- a/src/chatbot/chatbot_core.py +++ b/src/chatbot/chatbot_core.py @@ -4,11 +4,12 @@ import re import json from typing import Dict, Any, Tuple, List - +from sklearn.metrics.pairwise import cosine_similarity from .error_solutions import get_error_solution from .image_handler import analyze_and_extract from .ollama_runner import run_ollama from .knowledge_base import search_knowledge +from .ollama_runner import get_embedding # ==================== ESIM WORKFLOW KNOWLEDGE ==================== @@ -113,9 +114,40 @@ def clear_history() -> None: LAST_IMAGE_CONTEXT = {} LAST_NETLIST_ISSUES = {} - # ==================== ESIM ERROR LOGIC ==================== +def answer_with_rag_fallback(user_input: str) -> str: + """ + Try to answer using eSim manuals (RAG). + If nothing relevant is found, fallback to Ollama. + """ + + rag_context = search_knowledge(user_input) + + if rag_context.strip(): + prompt = f""" +You are eSim Copilot. + +Use ONLY the following official eSim documentation +to answer the question. Do NOT invent information. + +{rag_context} + +Question: +{user_input} + +Answer clearly and step-by-step. +""" + return run_ollama(prompt) + + # Fallback: general LLM answer + prompt = f""" +Answer the following question clearly: + +{user_input} +""" + return run_ollama(prompt) + def detect_esim_errors(image_context: Dict[str, Any], user_input: str) -> str: """ Display errors from hybrid analysis with SMART FILTERING to remove hallucinations. @@ -136,13 +168,11 @@ def detect_esim_errors(image_context: Dict[str, Any], user_input: str) -> str: for err in raw_errors: err_lower = err.lower() - # 1. Filter "No ground" if ground is actually detected if "ground" in err_lower and ( "gnd" in context_text or "ground" in context_text or " 0 " in context_text ): continue - # 2. Filter "Floating node" if it refers to Vin/Vout labels if "floating" in err_lower and ( "vin" in err_lower or "vout" in err_lower or "label" in err_lower ): @@ -240,7 +270,6 @@ def _history_to_text(history: List[Dict[str, str]] | None, max_turns: int = 6) - if u: lines.append(f"[Turn {i}] User: {u}") if b: - # Truncate very long bot responses to save token space if len(b) > 300: b = b[:300] + "..." lines.append(f"[Turn {i}] Assistant: {b}") @@ -262,12 +291,10 @@ def _is_follow_up_question(user_input: str, history: List[Dict[str, str]] | None if len(words) <= 7: return True - # Questions with pronouns (referring to previous context) pronouns = ["it", "that", "this", "those", "these", "they", "them"] if any(pronoun in words for pronoun in pronouns): return True - # Continuation phrases continuations = [ "what next", "next step", "after that", "and then", "then what", "what about", "how about", "what if", "but why", "why not" @@ -275,13 +302,56 @@ def _is_follow_up_question(user_input: str, history: List[Dict[str, str]] | None if any(phrase in user_lower for phrase in continuations): return True - # Question words at start without enough context question_starters = ["why", "how", "where", "when", "what", "which"] if words[0] in question_starters and len(words) <= 5: return True return False +import numpy as np + +def is_semantic_topic_switch( + user_input: str, + history: list, + threshold: float = 0.30 +) -> bool: + """ + Detect topic switch using embedding similarity. + Returns True if new question is unrelated to previous assistant reply. + """ + + if not history: + return False + + last_assistant_msg = None + for item in reversed(history): + if item.get("role") == "assistant": + last_assistant_msg = item.get("content") + break + if not last_assistant_msg: + return False + + try: + emb_new = get_embedding(user_input) + emb_prev = get_embedding(last_assistant_msg) + + if not emb_new or not emb_prev: + return False + + emb_new = np.array(emb_new) + emb_prev = np.array(emb_prev) + + similarity = np.dot(emb_new, emb_prev) / ( + np.linalg.norm(emb_new) * np.linalg.norm(emb_prev) + ) + + print(f"[COPILOT] Semantic similarity = {similarity:.3f}") + + return similarity < threshold + + except Exception as e: + print(f"[COPILOT] Topic switch check failed: {e}") + return False # ==================== QUESTION CLASSIFICATION ==================== @@ -294,15 +364,12 @@ def classify_question_type(user_input: str, has_image_context: bool, """ user_lower = user_input.lower() - # Explicit netlist block if "[ESIM_NETLIST_START]" in user_input: return "netlist" - # Image: new upload if _is_image_query(user_input): return "image_query" - # Follow-up about image if has_image_context: follow_phrases = [ "this circuit", "that circuit", "in this schematic", @@ -312,17 +379,20 @@ def classify_question_type(user_input: str, has_image_context: bool, if any(p in user_lower for p in follow_phrases): return "follow_up_image" - # Simple greeting greetings = ["hello", "hi", "hey", "howdy", "greetings"] user_words = user_lower.strip().split() if len(user_words) <= 3 and any(g in user_words for g in greetings): return "greeting" - # Detect generic follow-up (needs history) - if _is_follow_up_question(user_input, history): - return "follow_up" + is_followup = _is_follow_up_question(user_input, history) + if is_semantic_topic_switch(user_input, history): + print("[COPILOT] Topic switch detected (semantic)") + is_followup = False + + if not is_followup: + history.clear() + LAST_IMAGE_CONTEXT = None - # eSim-related keywords esim_keywords = [ "esim", "kicad", "ngspice", "spice", "simulation", "netlist", "schematic", "convert", "gnd", "ground", ".model", ".subckt", @@ -331,7 +401,6 @@ def classify_question_type(user_input: str, has_image_context: bool, if any(keyword in user_lower for keyword in esim_keywords): return "esim" - # Error-related error_keywords = [ "error", "fix", "problem", "issue", "warning", "missing", "not working", "failed", "crash" @@ -355,13 +424,12 @@ def handle_greeting() -> str: def handle_simple_question(user_input: str) -> str: - prompt = ( - "You are an electronics expert. Answer this question concisely (2-3 sentences max).\n" - "Use your general electronics knowledge. Do NOT make up eSim-specific commands.\n\n" - f"Question: {user_input}\n\n" - "Answer (brief and factual):" - ) - return run_ollama(prompt, mode="default") + """ + Handles standalone questions. + Uses RAG first, then falls back to Ollama. + keep in mind that your a copilot of eSim an EDA tool + """ + return answer_with_rag_fallback(user_input) def handle_follow_up(user_input: str, @@ -376,7 +444,6 @@ def handle_follow_up(user_input: str, if not history_text: return "I need more context. Could you provide more details about your question?" - # Get minimal RAG context (only if keywords detected) rag_context = "" user_lower = user_input.lower() if any(kw in user_lower for kw in ["model", "spice", "ground", "error", "netlist"]): @@ -423,7 +490,6 @@ def handle_esim_question(user_input: str, """ user_lower = user_input.lower() - # Fast path: known ngspice error messages → structured fixes sol = get_error_solution(user_input) if sol and sol.get("description") != "General schematic error": fixes = "\n".join(f"- {f}" for f in sol.get("fixes", [])) @@ -435,12 +501,10 @@ def handle_esim_question(user_input: str, ) if cmd: answer += f"**eSim action:** {cmd}\n" - return answer + return answer_with_rag_fallback(user_input) - # Build history text history_text = _history_to_text(history, max_turns=6) - # RAG context rag_context = search_knowledge(user_input, n_results=5) image_context_str = "" @@ -491,7 +555,6 @@ def handle_image_query(user_input: str) -> Tuple[str, Dict[str, Any]]: if extraction.get("error"): return f"Analysis Failed: {extraction['error']}", {} - # No follow-up question → summary if not question: error_report = detect_esim_errors(extraction, "") @@ -519,7 +582,6 @@ def handle_image_query(user_input: str) -> Tuple[str, Dict[str, Any]]: return summary, extraction - # There is a textual question about this image return handle_follow_up_image_question(question, extraction), extraction @@ -576,14 +638,12 @@ def handle_input(user_input: str, if not user_input: return "Please enter a query." - # Special case: raw netlist block if "[ESIM_NETLIST_START]" in user_input: raw_reply = run_ollama(user_input) cleaned = clean_response_raw(raw_reply) LAST_BOT_REPLY = cleaned return cleaned - # Classify question_type = classify_question_type( user_input, bool(LAST_IMAGE_CONTEXT), history ) @@ -602,15 +662,13 @@ def handle_input(user_input: str, elif question_type == "follow_up_image": response = handle_follow_up_image_question(user_input, LAST_IMAGE_CONTEXT) - elif question_type == "follow_up": - # NEW: Dedicated follow-up handler - response = handle_follow_up(user_input, LAST_IMAGE_CONTEXT, history) - elif question_type == "simple": response = handle_simple_question(user_input) - else: # "esim" or fallback - response = handle_esim_question(user_input, LAST_IMAGE_CONTEXT, history) + elif question_type == "follow_up" and history: + response = handle_follow_up(user_input, LAST_IMAGE_CONTEXT, history) + else: + response = handle_simple_question(user_input) LAST_BOT_REPLY = response return response @@ -637,7 +695,6 @@ def handle_input(self, user_input: str) -> str: def analyze_schematic(self, query: str) -> str: return self.handle_input(query) -# Global wrapper so history persists across calls from GUI _GLOBAL_WRAPPER = ESIMCopilotWrapper() From 544cd7a2e8817b097af0d2a2d42d48fb7f09cac0 Mon Sep 17 00:00:00 2001 From: Hariom Thakur Date: Sat, 17 Jan 2026 18:20:21 +0000 Subject: [PATCH 27/34] Revise README with updated dependencies and setup Updated installation instructions and added repository cloning steps. --- README_CHATBOT.md | 39 ++++++++++++++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/README_CHATBOT.md b/README_CHATBOT.md index d6e53314e..ff2e19fbd 100644 --- a/README_CHATBOT.md +++ b/README_CHATBOT.md @@ -38,9 +38,26 @@ python --version sudo apt update sudo apt upgrade -sudo apt install ngspice -sudo apt install portaudio19-dev -sudo apt install libgl1 libglib2.0-0 +sudo apt update +sudo apt install -y \ + libxcb-xinerama0 \ + libxcb-cursor0 \ + libxkbcommon-x11-0 \ + libxcb-icccm4 \ + libxcb-image0 \ + libxcb-keysyms1 \ + libxcb-render-util0 \ + libxcb-xinput0 \ + libxcb-shape0 \ + libxcb-randr0 \ + libxcb-util1 \ + libgl1 \ + libglib2.0-0 + +## Clone the Repository + +git clone +cd eSim-master ## Ollama (LLM Backend) ```bash @@ -69,14 +86,26 @@ source ~/.bashrc python3.10 -m venv venv source venv/bin/activate -pip install --upgrade pip setuptools wheel +pip uninstall -y pip +python -m ensurepip +python -m pip install pip==22.3.1 +python -m pip install setuptools==65.5.0 wheel==0.38.4 + +python -m pip install hdlparse==1.0.4 --no-build-isolation pip install -r requirements.txt -pip install hdlparse==1.0.4 pip install paddlepaddle==2.5.2 \ -f https://www.paddlepaddle.org.cn/whl/linux/mkl/avx/stable.html +pip uninstall -y opencv-python opencv-contrib-python opencv-python-headless +pip install opencv-python-headless==4.6.0.66 + +## Before running eSim + +unset QT_PLUGIN_PATH +export QT_QPA_PLATFORM=xcb + ## Ingest manuals for RAG ```bash cd src From 67f08043d924fb78eb983ed7242a6bd8d06642b2 Mon Sep 17 00:00:00 2001 From: eSim Copilot Dev Date: Tue, 3 Mar 2026 11:15:13 +0530 Subject: [PATCH 28/34] Chatbot_Enhancements: Ubuntu deployment support, optional STT, ChromaDB path fix Made-with: Cursor --- DEPLOY_UBUNTU.md | 113 ++++++++++++++++++++++++++++++++ README_CHATBOT.md | 2 + requirements-copilot.txt | 18 +++++ requirements.txt | 15 +---- scripts/setup_copilot_ubuntu.sh | 61 +++++++++++++++++ src/chatbot/chatbot_core.py | 1 - src/chatbot/knowledge_base.py | 10 ++- src/chatbot/stt_handler.py | 32 +++++++-- 8 files changed, 230 insertions(+), 22 deletions(-) create mode 100644 DEPLOY_UBUNTU.md create mode 100644 requirements-copilot.txt create mode 100644 scripts/setup_copilot_ubuntu.sh diff --git a/DEPLOY_UBUNTU.md b/DEPLOY_UBUNTU.md new file mode 100644 index 000000000..e4246cc90 --- /dev/null +++ b/DEPLOY_UBUNTU.md @@ -0,0 +1,113 @@ +# Deploy eSim Copilot on Ubuntu (VM or WSL2) + +Use this guide for **first-time deployment** of the AI chatbot on a Linux environment. + +--- + +## Option A: Ubuntu VM (recommended for full GUI) + +### 1. Create Ubuntu VM + +- **VirtualBox**: Download [Ubuntu 22.04 Desktop](https://ubuntu.com/download/desktop) and create a new VM (≥4 GB RAM, 20 GB disk). +- **Hyper-V** (Windows Pro): Create VM → Install Ubuntu 22.04 from ISO. +- **VMware**: Same steps as VirtualBox. + +### 2. Get the code into Ubuntu + +**Option 2a – Copy from Windows (if you have the repo locally):** + +- Use a shared folder, or zip `repos/eSim` and copy into the VM. +- Unzip in `~/work/eSim` and `cd ~/work/eSim`. +- Ensure you're on branch `Chatbot_Enhancements`: `git checkout Chatbot_Enhancements` + +**Option 2b – Clone from GitHub:** + +Inside Ubuntu, open Terminal and run: + +```bash +# Update system +sudo apt update && sudo apt upgrade -y + +# Clone repo and switch to Chatbot_Enhancements (or pr-434) +mkdir -p ~/work && cd ~/work +git clone https://github.com/FOSSEE/eSim.git +cd eSim +git fetch origin pull/434/head:pr-434 +git checkout -b Chatbot_Enhancements pr-434 +# If Chatbot_Enhancements is on your fork: git fetch Chatbot_Enhancements && git checkout Chatbot_Enhancements +``` + +### 3. Run one-command setup (installs deps, venv, Ollama, models, Vosk) + +```bash +chmod +x scripts/setup_copilot_ubuntu.sh +./scripts/setup_copilot_ubuntu.sh +``` + +### 4. Start Ollama (keep running in a separate terminal) + +```bash +ollama serve +``` + +### 5. Ingest manuals for RAG (optional but recommended) + +```bash +cd ~/work/eSim +source .venv/bin/activate +cd src +python ingest.py +``` + +### 6. Launch eSim with Copilot + +```bash +cd ~/work/eSim +source .venv/bin/activate +cd src/frontEnd +QT_QPA_PLATFORM=xcb python Application.py +``` + +Click the **eSim Copilot** button in the toolbar to open the AI chat. + +--- + +## Option B: WSL2 (Windows Subsystem for Linux) + +### 1. Install WSL2 + Ubuntu + +In **PowerShell (Admin)**: + +```powershell +wsl --install +wsl --set-default-version 2 +wsl --install -d Ubuntu-22.04 +``` + +Reboot if prompted, then open **Ubuntu 22.04** from Start. + +### 2. Follow steps 2–5 from Option A + +All commands are the same inside the Ubuntu terminal. + +--- + +## Troubleshooting + +| Issue | Fix | +|-------|-----| +| "Ollama not responding" | Run `ollama serve` in a separate terminal before launching eSim | +| GUI doesn't appear (WSL) | Ensure you use the Ubuntu app (not SSH). WSLg provides display automatically | +| Voice input fails | Microphone passthrough in WSL can be unreliable; text + image + netlist still work | +| `python ingest.py` finds no files | Add `.txt` manuals to `src/manuals/` before running ingest | + +--- + +## Branch: Chatbot_Enhancements + +This deployment uses the `Chatbot_Enhancements` branch, based on Hariom's PR 434, with: + +- Seamless install script for Ubuntu +- User-writable ChromaDB path +- Optional speech-to-text (graceful fallback if Vosk missing) +- Split requirements (base + copilot) diff --git a/README_CHATBOT.md b/README_CHATBOT.md index ff2e19fbd..6a41ae272 100644 --- a/README_CHATBOT.md +++ b/README_CHATBOT.md @@ -4,6 +4,8 @@ eSim Copilot is an AI-powered assistant integrated into **eSim**, designed to he This project combines **PyQt5**, **ngspice**, **Ollama (LLMs)**, **RAG (ChromaDB)**, **OCR**, and **offline speech-to-text** into a single desktop application. +**→ First-time deployment on Ubuntu (VM or WSL2): see [DEPLOY_UBUNTU.md](DEPLOY_UBUNTU.md)** + --- ## Key Features diff --git a/requirements-copilot.txt b/requirements-copilot.txt new file mode 100644 index 000000000..1100441b6 --- /dev/null +++ b/requirements-copilot.txt @@ -0,0 +1,18 @@ +# eSim Copilot AI extras (install after base requirements.txt) +ollama +chromadb +psutil +protobuf<5 +regex +requests +tqdm +pyyaml + +# Vision (PaddleOCR + OpenCV) +pillow==10.4.0 +opencv-python-headless==4.6.0.66 +paddleocr==2.7.0.3 + +# Speech-to-text +vosk +sounddevice diff --git a/requirements.txt b/requirements.txt index a408dbcb0..e452f9b8b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -24,20 +24,7 @@ scipy==1.10.1 six==1.17.0 watchdog==4.0.2 zipp==3.20.2 -ollama -chromadb -sentence-transformers -psutil -protobuf<5 -regex -opencv-python -paddleocr==2.7.0.3 -paddlepaddle==2.5.2 -vosk -sounddevice -requests -tqdm -pyyaml setuptools==65.5.0 wheel PyQtWebEngine +# Copilot AI deps: pip install -r requirements-copilot.txt diff --git a/scripts/setup_copilot_ubuntu.sh b/scripts/setup_copilot_ubuntu.sh new file mode 100644 index 000000000..dfe925d8c --- /dev/null +++ b/scripts/setup_copilot_ubuntu.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +echo "[1/7] Installing system packages (Ubuntu/Debian)…" +sudo apt-get update +sudo apt-get install -y \ + python3.10 python3.10-venv python3-pip \ + curl wget unzip \ + ngspice kicad \ + portaudio19-dev \ + libgl1 libglib2.0-0 \ + libxcb-xinerama0 libxcb-cursor0 libxkbcommon-x11-0 \ + libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-render-util0 \ + libxcb-xinput0 libxcb-shape0 libxcb-randr0 libxcb-util1 + +echo "[2/7] Creating Python virtualenv…" +cd "$ROOT_DIR" +python3.10 -m venv .venv +source .venv/bin/activate + +echo "[3/7] Installing Python dependencies…" +python -m pip install --upgrade pip setuptools wheel +python -m pip install -r requirements.txt +python -m pip install -r requirements-copilot.txt + +echo "[4/7] Installing PaddlePaddle (CPU, AVX, MKL)…" +python -m pip install "paddlepaddle==2.5.2" \ + -f https://www.paddlepaddle.org.cn/whl/linux/mkl/avx/stable.html + +echo "[5/7] Installing Ollama if missing…" +if ! command -v ollama >/dev/null 2>&1; then + curl -fsSL https://ollama.com/install.sh | sh +fi + +echo "[6/7] Pulling required Ollama models…" +ollama pull qwen2.5:3b +ollama pull minicpm-v +ollama pull nomic-embed-text + +echo "[7/7] Installing Vosk small English model…" +VOSK_BASE="${XDG_DATA_HOME:-$HOME/.local/share}/esim-copilot" +mkdir -p "$VOSK_BASE" +cd "$VOSK_BASE" +if [ ! -d "vosk-model-small-en-us-0.15" ]; then + wget -q https://alphacephei.com/vosk/models/vosk-model-small-en-us-0.15.zip -O vosk-model-small-en-us-0.15.zip + unzip -q vosk-model-small-en-us-0.15.zip + rm -f vosk-model-small-en-us-0.15.zip +fi + +echo +echo "Done." +echo "- Activate venv: source \"$ROOT_DIR/.venv/bin/activate\"" +echo "- Run ingestion (optional for RAG): (cd \"$ROOT_DIR/src\" && python ingest.py)" +echo "- Run eSim: (cd \"$ROOT_DIR/src/frontEnd\" && QT_QPA_PLATFORM=xcb python Application.py)" +echo +echo "Optional env vars:" +echo "- export VOSK_MODEL_PATH=\"$VOSK_BASE/vosk-model-small-en-us-0.15\"" +echo "- export ESIM_COPILOT_DB_PATH=\"${XDG_DATA_HOME:-$HOME/.local/share}/esim-copilot/chroma\"" + diff --git a/src/chatbot/chatbot_core.py b/src/chatbot/chatbot_core.py index 0205e7d4c..24b8eef09 100644 --- a/src/chatbot/chatbot_core.py +++ b/src/chatbot/chatbot_core.py @@ -4,7 +4,6 @@ import re import json from typing import Dict, Any, Tuple, List -from sklearn.metrics.pairwise import cosine_similarity from .error_solutions import get_error_solution from .image_handler import analyze_and_extract from .ollama_runner import run_ollama diff --git a/src/chatbot/knowledge_base.py b/src/chatbot/knowledge_base.py index fb1d0821a..398087ce8 100644 --- a/src/chatbot/knowledge_base.py +++ b/src/chatbot/knowledge_base.py @@ -4,8 +4,14 @@ # ==================== DATABASE SETUP ==================== -# Persistent DB directory (relative to this file) -db_path = os.path.join(os.path.dirname(__file__), "esim_knowledge_db") +def _default_db_path() -> str: + xdg_data_home = os.environ.get("XDG_DATA_HOME", "").strip() + if not xdg_data_home: + xdg_data_home = os.path.join(os.path.expanduser("~"), ".local", "share") + return os.path.join(xdg_data_home, "esim-copilot", "chroma") + +db_path = os.environ.get("ESIM_COPILOT_DB_PATH", "").strip() or _default_db_path() +os.makedirs(db_path, exist_ok=True) chroma_client = chromadb.PersistentClient(path=db_path) collection = chroma_client.get_or_create_collection(name="esim_manuals") diff --git a/src/chatbot/stt_handler.py b/src/chatbot/stt_handler.py index 0d3352f26..f2d536066 100644 --- a/src/chatbot/stt_handler.py +++ b/src/chatbot/stt_handler.py @@ -3,16 +3,36 @@ import queue import time -import sounddevice as sd -from vosk import Model, KaldiRecognizer +try: + import sounddevice as sd + from vosk import Model, KaldiRecognizer + _HAS_STT = True +except Exception: + sd = None + Model = None + KaldiRecognizer = None + _HAS_STT = False _MODEL = None +DEFAULT_VOSK_DIR = os.path.join( + os.path.expanduser("~"), ".local", "share", + "esim-copilot", "vosk-model-small-en-us-0.15", +) + def _get_model(): global _MODEL - model_path = os.environ.get("VOSK_MODEL_PATH", "") - if not model_path or not os.path.isdir(model_path): - raise RuntimeError(f"VOSK_MODEL_PATH not set or not found: {model_path}") + if not _HAS_STT: + raise RuntimeError( + "Speech-to-text is not available (missing vosk/sounddevice)." + ) + model_path = os.environ.get("VOSK_MODEL_PATH", "").strip() + if not model_path: + model_path = DEFAULT_VOSK_DIR + if not os.path.isdir(model_path): + raise RuntimeError( + f"Vosk model path not found. Set VOSK_MODEL_PATH or install at: {model_path}" + ) if _MODEL is None: _MODEL = Model(model_path) return _MODEL @@ -22,6 +42,8 @@ def listen_to_mic(should_stop=lambda: False, max_silence_sec=3, samplerate=16000 Offline STT using Vosk. Returns recognized text, or "" if cancelled / timed out. """ + if not _HAS_STT: + raise RuntimeError("Speech-to-text is not installed or failed to load.") q = queue.Queue() rec = KaldiRecognizer(_get_model(), samplerate) From 4c67304d7e22b346824a7b887a198e0307c906ad Mon Sep 17 00:00:00 2001 From: eSim Copilot Dev Date: Tue, 3 Mar 2026 15:09:16 +0530 Subject: [PATCH 29/34] Chatbot_Enhancements: Ubuntu deployment, hdlparse fix, Workspace.py fix, launch script, session summary Made-with: Cursor --- .gitattributes | 2 + DEPLOY_UBUNTU.md | 114 ++++++++++++++++++++---- SESSION_SUMMARY.md | 151 ++++++++++++++++++++++++++++++++ requirements.txt | 2 +- scripts/launch_esim.sh | 14 +++ scripts/setup_copilot_ubuntu.sh | 10 ++- scripts/zip_for_vm.ps1 | 19 ++++ src/frontEnd/Workspace.py | 4 +- 8 files changed, 295 insertions(+), 21 deletions(-) create mode 100644 .gitattributes create mode 100644 SESSION_SUMMARY.md create mode 100644 scripts/launch_esim.sh create mode 100644 scripts/zip_for_vm.ps1 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..efdba8764 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +* text=auto +*.sh text eol=lf diff --git a/DEPLOY_UBUNTU.md b/DEPLOY_UBUNTU.md index e4246cc90..878bf9482 100644 --- a/DEPLOY_UBUNTU.md +++ b/DEPLOY_UBUNTU.md @@ -4,52 +4,117 @@ Use this guide for **first-time deployment** of the AI chatbot on a Linux enviro --- +## VM setup checklist (Option A) + +| Step | Action | +|------|--------| +| 1 | Download [Ubuntu 22.04 Desktop](https://ubuntu.com/download/desktop) ISO | +| 2 | Create VM (VirtualBox/Hyper-V/VMware): 4–8 GB RAM, 25 GB disk | +| 3 | Install Ubuntu in VM, install Guest Additions (VirtualBox) if using shared folder | +| 4 | Copy eSim into VM (shared folder or zip) → `~/work/eSim` | +| 5 | Run `./scripts/setup_copilot_ubuntu.sh` | +| 6 | Start `ollama serve` in a second terminal | +| 7 | Run `python ingest.py` (optional) | +| 8 | Launch: `QT_QPA_PLATFORM=xcb python Application.py` from `src/frontEnd` | + +--- + ## Option A: Ubuntu VM (recommended for full GUI) ### 1. Create Ubuntu VM -- **VirtualBox**: Download [Ubuntu 22.04 Desktop](https://ubuntu.com/download/desktop) and create a new VM (≥4 GB RAM, 20 GB disk). -- **Hyper-V** (Windows Pro): Create VM → Install Ubuntu 22.04 from ISO. -- **VMware**: Same steps as VirtualBox. +#### 1a. Download Ubuntu 22.04 Desktop -### 2. Get the code into Ubuntu +- Go to [ubuntu.com/download/desktop](https://ubuntu.com/download/desktop) +- Download **Ubuntu 22.04 LTS** (64-bit, ~4 GB ISO) -**Option 2a – Copy from Windows (if you have the repo locally):** +#### 1b. Choose your hypervisor -- Use a shared folder, or zip `repos/eSim` and copy into the VM. -- Unzip in `~/work/eSim` and `cd ~/work/eSim`. -- Ensure you're on branch `Chatbot_Enhancements`: `git checkout Chatbot_Enhancements` +| Tool | Steps | +|------|-------| +| **VirtualBox** (free) | 1. Install [VirtualBox](https://www.virtualbox.org/wiki/Downloads)
2. New VM → Name: `eSim-Ubuntu`, Type: Linux, Version: Ubuntu (64-bit)
3. RAM: **4096 MB** (minimum), **8192 MB** recommended for Ollama
4. Create virtual disk: **VDI**, **Dynamically allocated**, **25 GB**
5. Settings → Storage → Empty → Choose Ubuntu ISO
6. Start VM → Install Ubuntu (normal install, minimal if offered) | +| **Hyper-V** (Windows Pro) | 1. Enable Hyper-V: `OptionalFeatures` → check Hyper-V
2. Hyper-V Manager → New → Virtual Machine
3. Generation 2, 4096 MB RAM, 25 GB disk
4. Connect to Ubuntu ISO, boot and install | +| **VMware Workstation** | Same as VirtualBox: New VM → Typical → Ubuntu ISO → 4 GB RAM, 25 GB disk | -**Option 2b – Clone from GitHub:** +**Important:** Allocate at least **4 GB RAM**; 8 GB is better if you run Ollama models inside the VM. -Inside Ubuntu, open Terminal and run: +--- -```bash -# Update system -sudo apt update && sudo apt upgrade -y +### 2. Get the code into Ubuntu -# Clone repo and switch to Chatbot_Enhancements (or pr-434) +#### Option 2a – Copy from Windows (shared folder or zip) + +**Using VirtualBox shared folder:** + +1. In VirtualBox: VM Settings → Shared Folders → Add +2. Folder path: `C:\Users\91900\Downloads\eSIM-Software-AIChatBot\repos` +3. Folder name: `repos` (or `esim_repo`) +4. Check **Auto-mount**, **Make permanent** +5. Inside Ubuntu VM, install Guest Additions (Devices → Insert Guest Additions CD, then run it) +6. Add your user to `vboxsf`: `sudo usermod -aG vboxsf $USER` → log out and back in +7. Copy eSim into your home: + ```bash + mkdir -p ~/work + cp -r /media/sf_repos/eSim ~/work/eSim + cd ~/work/eSim + git checkout Chatbot_Enhancements + ``` + +**Using zip (no shared folder):** + +1. On Windows, zip the folder: + - Right-click `repos\eSim` → Send to → Compressed folder + - Or in PowerShell (from workspace root): `.\repos\eSim\scripts\zip_for_vm.ps1` → creates `eSim-for-VM.zip` +2. Copy `eSim-for-VM.zip` into the VM (drag-drop, USB, or network share) +3. Inside Ubuntu: + ```bash + mkdir -p ~/work && cd ~/work + unzip /path/to/eSim-for-VM.zip # e.g. unzip ~/Downloads/eSim-for-VM.zip + cd eSim + git checkout Chatbot_Enhancements + ``` + +#### Option 2b – Clone from GitHub (if Chatbot_Enhancements is on your fork) + +```bash +sudo apt update && sudo apt install -y git mkdir -p ~/work && cd ~/work git clone https://github.com/FOSSEE/eSim.git cd eSim git fetch origin pull/434/head:pr-434 git checkout -b Chatbot_Enhancements pr-434 -# If Chatbot_Enhancements is on your fork: git fetch Chatbot_Enhancements && git checkout Chatbot_Enhancements +# If you pushed Chatbot_Enhancements to your fork: +# git remote add myfork https://github.com/YOUR_USER/eSim.git +# git fetch myfork Chatbot_Enhancements +# git checkout Chatbot_Enhancements ``` -### 3. Run one-command setup (installs deps, venv, Ollama, models, Vosk) +--- + +### 3. Run one-command setup ```bash +cd ~/work/eSim chmod +x scripts/setup_copilot_ubuntu.sh ./scripts/setup_copilot_ubuntu.sh ``` +This installs system packages, Python venv, Ollama, models (qwen2.5:3b, minicpm-v, nomic-embed-text), and Vosk. It may take 15–30 minutes depending on your connection. + +--- + ### 4. Start Ollama (keep running in a separate terminal) +Open a **new terminal** and run: + ```bash ollama serve ``` +Leave this running. The Copilot needs Ollama to answer questions. + +--- + ### 5. Ingest manuals for RAG (optional but recommended) ```bash @@ -59,6 +124,10 @@ cd src python ingest.py ``` +Add `.txt` manuals to `src/manuals/` first if you have them; otherwise ingest may find nothing (RAG will still work, but with less context). + +--- + ### 6. Launch eSim with Copilot ```bash @@ -72,6 +141,19 @@ Click the **eSim Copilot** button in the toolbar to open the AI chat. --- +### Quick copy-paste (after VM is ready and code is in ~/work/eSim) + +```bash +cd ~/work/eSim && git checkout Chatbot_Enhancements +chmod +x scripts/setup_copilot_ubuntu.sh && ./scripts/setup_copilot_ubuntu.sh +# When done: open a second terminal and run: ollama serve +# Then in first terminal: +source .venv/bin/activate && cd src && python ingest.py +cd ~/work/eSim && source .venv/bin/activate && cd src/frontEnd && QT_QPA_PLATFORM=xcb python Application.py +``` + +--- + ## Option B: WSL2 (Windows Subsystem for Linux) ### 1. Install WSL2 + Ubuntu diff --git a/SESSION_SUMMARY.md b/SESSION_SUMMARY.md new file mode 100644 index 000000000..19e9ec6b0 --- /dev/null +++ b/SESSION_SUMMARY.md @@ -0,0 +1,151 @@ +# eSim Copilot Deployment – Session Summary + +**Date:** March 3, 2025 +**Branch:** `Chatbot_Enhancements` (from `pr-434`) +**Goal:** First deployment of eSim AI Copilot on Ubuntu VM + +--- + +## 1. Project Context + +- **eSim Copilot** – AI-assisted electronics simulation tool (FOSSEE/eSim) +- Based on Hariom's PR 434 (PyQt5, RAG, vision, voice, netlist analysis) +- Target: Ubuntu 22.04 (VM or WSL2) + +--- + +## 2. Branch Setup + +- Created branch `Chatbot_Enhancements` from `pr-434` +- All enhancements committed to this branch + +--- + +## 3. Ubuntu VM Setup + +| Step | Action | +|------|--------| +| 1 | Downloaded Ubuntu 22.04 Desktop ISO | +| 2 | Created VM in VirtualBox (4–8 GB RAM, 25 GB disk) | +| 3 | Installed Ubuntu; user: `harvi`, password: `bhavin` | +| 4 | Installed SSH: `sudo apt install openssh-server` | +| 5 | Configured Bridged Adapter for network (after fixing "No Bridged Adapter" – selected host NIC) | +| 6 | VM IP: `192.168.29.208` (static config via Netplan) | +| 7 | Connected via MobaXterm SSH | + +--- + +## 4. Code Transfer to VM + +- **Method:** SCP from Windows to VM +- **Path:** `C:\Users\91900\Downloads\eSIM-Software-AIChatBot\repos\eSim` → `harvi@192.168.29.208:~/work/eSim` +- **Script:** `scripts/zip_for_vm.ps1` for zipping; `scp -r` for direct copy + +--- + +## 5. Code Fixes & Enhancements + +### 5.1 Line endings (CRLF → LF) + +- **Issue:** `bash\r: No such file or directory` when running setup script +- **Fix:** `sed -i 's/\r$//' scripts/setup_copilot_ubuntu.sh` +- **Prevention:** Added `.gitattributes` with `*.sh text eol=lf` + +### 5.2 hdlparse build failure + +- **Issue:** `use_2to3 is invalid` (setuptools ≥58 removed support) +- **Fix:** Install `setuptools==57.5.0` first; install `hdlparse==1.0.4 --no-build-isolation`; use `-c` constraint for remaining deps +- **Files:** `scripts/setup_copilot_ubuntu.sh`, `requirements.txt` + +### 5.3 Workspace directory missing + +- **Issue:** `FileNotFoundError: ~/.esim/workspace.txt` – `.esim` dir not created +- **Fix:** `Workspace.py` – add `os.makedirs(esim_dir, exist_ok=True)` before writing +- **Manual:** `mkdir -p ~/.esim` + +### 5.4 Other enhancements (from earlier) + +- Optional STT (graceful fallback if Vosk missing) +- ChromaDB path: `~/.local/share/esim-copilot/chroma` +- Split requirements: `requirements.txt` + `requirements-copilot.txt` +- `DEPLOY_UBUNTU.md` – deployment guide +- `scripts/setup_copilot_ubuntu.sh` – one-command setup + +--- + +## 6. Setup Script Flow + +```bash +./scripts/setup_copilot_ubuntu.sh +``` + +1. Install system packages (python3.10, ngspice, kicad, portaudio, xcb libs) +2. Create `.venv` +3. Install Python deps (with hdlparse workaround) +4. Install PaddlePaddle (CPU) +5. Install Ollama (`curl -fsSL https://ollama.com/install.sh | sh`) +6. Pull models: qwen2.5:3b, minicpm-v, nomic-embed-text +7. Download Vosk model to `~/.local/share/esim-copilot/` + +--- + +## 7. Launch Flow + +### Ollama + +- Installed as systemd service (runs automatically) +- API: `127.0.0.1:11434` +- CPU-only mode (no GPU in VM) + +### eSim + +```bash +./scripts/launch_esim.sh +``` + +Or manually: + +```bash +cd ~/work/eSim +source .venv/bin/activate +cd src/frontEnd +QT_QPA_PLATFORM=xcb python Application.py +``` + +--- + +## 8. Files Created/Modified + +| File | Change | +|------|--------| +| `.gitattributes` | LF for `.sh` files | +| `DEPLOY_UBUNTU.md` | VM setup, SSH, deployment steps | +| `requirements.txt` | setuptools<58 for hdlparse | +| `requirements-copilot.txt` | AI deps (new) | +| `scripts/setup_copilot_ubuntu.sh` | Full setup, hdlparse fix | +| `scripts/launch_esim.sh` | Single launch script | +| `scripts/zip_for_vm.ps1` | Zip eSim for VM transfer | +| `src/frontEnd/Workspace.py` | Create `.esim` dir before write | +| `src/chatbot/chatbot_core.py` | Removed unused sklearn | +| `src/chatbot/stt_handler.py` | Optional STT | +| `src/chatbot/knowledge_base.py` | ChromaDB path | + +--- + +## 9. Known Issues + +- **PaddleOCR:** `No module named 'paddle'` – vision features may be limited; text chat works +- **Netlist contract:** Missing `manuals/esim_netlist_analysis_output_contract.txt` – optional +- **DeprecationWarnings:** PyQt5/sip – safe to ignore + +--- + +## 10. Quick Reference + +| Item | Value | +|------|-------| +| VM IP | 192.168.29.208 | +| SSH user | harvi | +| Repo path | ~/work/eSim | +| Branch | Chatbot_Enhancements | +| Launch | `./scripts/launch_esim.sh` | diff --git a/requirements.txt b/requirements.txt index e452f9b8b..13f5c7819 100644 --- a/requirements.txt +++ b/requirements.txt @@ -24,7 +24,7 @@ scipy==1.10.1 six==1.17.0 watchdog==4.0.2 zipp==3.20.2 -setuptools==65.5.0 +setuptools>=57.5.0,<58 wheel PyQtWebEngine # Copilot AI deps: pip install -r requirements-copilot.txt diff --git a/scripts/launch_esim.sh b/scripts/launch_esim.sh new file mode 100644 index 000000000..cb0c66e57 --- /dev/null +++ b/scripts/launch_esim.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +# Launch eSim with Copilot +# Usage: ./scripts/launch_esim.sh or bash scripts/launch_esim.sh + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT" + +# Ensure .esim dir exists (avoids workspace error) +mkdir -p ~/.esim + +# Activate venv and launch +source .venv/bin/activate +cd src/frontEnd +QT_QPA_PLATFORM=xcb python Application.py diff --git a/scripts/setup_copilot_ubuntu.sh b/scripts/setup_copilot_ubuntu.sh index dfe925d8c..3e3af20d5 100644 --- a/scripts/setup_copilot_ubuntu.sh +++ b/scripts/setup_copilot_ubuntu.sh @@ -21,9 +21,13 @@ python3.10 -m venv .venv source .venv/bin/activate echo "[3/7] Installing Python dependencies…" -python -m pip install --upgrade pip setuptools wheel -python -m pip install -r requirements.txt -python -m pip install -r requirements-copilot.txt +python -m pip install --upgrade pip wheel +# hdlparse needs setuptools<58 (use_2to3 removed in setuptools 58+) +python -m pip install setuptools==57.5.0 +python -m pip install hdlparse==1.0.4 --no-build-isolation +echo "setuptools<58" > /tmp/pip-constraints.txt +python -m pip install -c /tmp/pip-constraints.txt -r requirements.txt +python -m pip install -c /tmp/pip-constraints.txt -r requirements-copilot.txt echo "[4/7] Installing PaddlePaddle (CPU, AVX, MKL)…" python -m pip install "paddlepaddle==2.5.2" \ diff --git a/scripts/zip_for_vm.ps1 b/scripts/zip_for_vm.ps1 new file mode 100644 index 000000000..1fac11573 --- /dev/null +++ b/scripts/zip_for_vm.ps1 @@ -0,0 +1,19 @@ +# Zip eSim for copying into Ubuntu VM +# Run from anywhere; script finds eSim repo relative to itself +param( + [string]$RepoPath = (Join-Path $PSScriptRoot ".."), + [string]$OutZip = (Join-Path (Split-Path (Split-Path (Split-Path $PSScriptRoot -Parent) -Parent) -Parent) "eSim-for-VM.zip") +) +$ErrorActionPreference = "Stop" +if (-not (Test-Path $RepoPath)) { + Write-Host "Repo not found: $RepoPath" -ForegroundColor Red + exit 1 +} +Write-Host "Zipping: $RepoPath" -ForegroundColor Cyan +Write-Host "Output: $OutZip" -ForegroundColor Cyan +Compress-Archive -Path $RepoPath -DestinationPath $OutZip -Force +Write-Host "Done. Copy $OutZip into your Ubuntu VM, then:" -ForegroundColor Green +Write-Host " mkdir -p ~/work && cd ~/work" +Write-Host " unzip /path/to/eSim-for-VM.zip" +Write-Host " cd eSim && git checkout Chatbot_Enhancements" +Write-Host " chmod +x scripts/setup_copilot_ubuntu.sh && ./scripts/setup_copilot_ubuntu.sh" diff --git a/src/frontEnd/Workspace.py b/src/frontEnd/Workspace.py index fca73e399..b6ebdd53a 100755 --- a/src/frontEnd/Workspace.py +++ b/src/frontEnd/Workspace.py @@ -130,7 +130,9 @@ def createWorkspace(self): else: user_home = os.path.expanduser('~') - file = open(os.path.join(user_home, ".esim/workspace.txt"), 'w') + esim_dir = os.path.join(user_home, ".esim") + os.makedirs(esim_dir, exist_ok=True) + file = open(os.path.join(esim_dir, "workspace.txt"), 'w') file.writelines( str(self.obj_appconfig.workspace_check) + " " + self.workspace_loc.text() From 71ed0f5c2423c68dbfd03b79be4257b082d0c1e9 Mon Sep 17 00:00:00 2001 From: eSim Copilot Dev Date: Tue, 3 Mar 2026 15:11:44 +0530 Subject: [PATCH 30/34] SESSION_SUMMARY: comprehensive guide with every item explained Made-with: Cursor --- SESSION_SUMMARY.md | 619 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 544 insertions(+), 75 deletions(-) diff --git a/SESSION_SUMMARY.md b/SESSION_SUMMARY.md index 19e9ec6b0..205a71ce3 100644 --- a/SESSION_SUMMARY.md +++ b/SESSION_SUMMARY.md @@ -1,109 +1,438 @@ -# eSim Copilot Deployment – Session Summary +# eSim Copilot Deployment – Complete Guide **Date:** March 3, 2025 **Branch:** `Chatbot_Enhancements` (from `pr-434`) **Goal:** First deployment of eSim AI Copilot on Ubuntu VM +This document explains every step, every file, every command, and every fix from this deployment session. + +--- + +# Table of Contents + +1. [Project Overview](#1-project-overview) +2. [Branch Setup](#2-branch-setup) +3. [Ubuntu VM Setup](#3-ubuntu-vm-setup) +4. [Networking & SSH](#4-networking--ssh) +5. [Code Transfer to VM](#5-code-transfer-to-vm) +6. [Code Fixes & Enhancements](#6-code-fixes--enhancements) +7. [Setup Script – Step by Step](#7-setup-script--step-by-step) +8. [Ollama](#8-ollama) +9. [Launch Flow](#9-launch-flow) +10. [Files Created/Modified – Full Details](#10-files-createdmodified--full-details) +11. [Known Issues](#11-known-issues) +12. [Troubleshooting](#12-troubleshooting) +13. [Push to GitHub](#13-push-to-github) +14. [Quick Reference](#14-quick-reference) + +--- + +# 1. Project Overview + +## What is eSim? + +**eSim** is an open-source electronics simulation tool (FOSSEE/eSim) that uses: +- **ngspice** – SPICE circuit simulator +- **KiCad** – Schematic capture +- **PyQt5** – Desktop GUI + +## What is eSim Copilot? + +**eSim Copilot** is an AI assistant integrated into eSim, based on Hariom's PR 434. It provides: + +| Feature | Technology | +|---------|------------| +| Text chat | Ollama (LLM) | +| RAG (Retrieval Augmented Generation) | ChromaDB | +| Vision (OCR, image analysis) | PaddleOCR, MiniCPM-V | +| Speech-to-text | Vosk | +| Netlist analysis | FACT-based contract | + +## Why Ubuntu VM? + +eSim is designed for Linux. Windows has issues with native dependencies (e.g. `contourpy` needs Visual Studio C++ build tools). Ubuntu VM or WSL2 provides a clean Linux environment. + +--- + +# 2. Branch Setup + +## Source + +- **Base:** `pr-434` (Hariom's Copilot implementation) +- **New branch:** `Chatbot_Enhancements` + +## Commands + +```bash +cd ~/work/eSim +git fetch origin pull/434/head:pr-434 +git checkout -b Chatbot_Enhancements pr-434 +``` + +All enhancements from this session are committed to `Chatbot_Enhancements`. + +--- + +# 3. Ubuntu VM Setup + +## 3.1 Download Ubuntu + +- **URL:** https://ubuntu.com/download/desktop +- **Version:** Ubuntu 22.04 LTS (64-bit) +- **Size:** ~4 GB ISO + +## 3.2 Create VM in VirtualBox + +| Setting | Value | +|---------|-------| +| Name | eSim-Ubuntu (or any name) | +| Type | Linux | +| Version | Ubuntu (64-bit) | +| RAM | 4096 MB minimum, 8192 MB recommended for Ollama | +| Disk | VDI, Dynamically allocated, 25 GB | +| ISO | Attach Ubuntu 22.04 ISO to optical drive | + +## 3.3 Install Ubuntu + +1. Start VM, boot from ISO +2. Choose "Install Ubuntu" +3. Normal install, minimal install optional +4. **Product key:** Leave empty (Ubuntu is free) +5. **User credentials:** e.g. `harvi` / `bhavin` +6. Host name: e.g. `harvi-VirtualBox` + +## 3.4 Install Guest Additions (VirtualBox) + +- If using shared folder: Devices → Insert Guest Additions CD +- Run the installer from the mounted CD +- Reboot if prompted + +## 3.5 Network Adapter + +- **Attached to:** Bridged Adapter +- **Name:** Select your host NIC (Wi-Fi or Ethernet) + +- If "No Bridged Network Adapter" or "Host Interface Networking driver" error: + - VirtualBox → Repair → ensure "VirtualBox NDIS6 Bridged Networking Driver" is installed + - Or use NAT with port forwarding (Host 2222 → Guest 22) + --- -## 1. Project Context +# 4. Networking & SSH + +## 4.1 Enable SSH on VM + +```bash +sudo apt update +sudo apt install -y openssh-server +sudo systemctl enable ssh +sudo systemctl start ssh +``` + +## 4.2 Find VM IP + +```bash +ip addr +``` + +- **Bridged:** Look for `inet` on `enp0s3` (e.g. `192.168.29.208`) +- **NAT:** `10.0.2.15` – use port forwarding for SSH from host + +## 4.3 Static IP (optional) + +```bash +sudo nano /etc/netplan/00-installer-config.yaml +``` + +```yaml +network: + version: 2 + ethernets: + enp0s3: + dhcp4: no + addresses: + - 192.168.29.208/24 + routes: + - to: default + via: 192.168.29.1 + nameservers: + addresses: + - 8.8.8.8 + - 8.8.4.4 +``` + +```bash +sudo netplan apply +``` + +## 4.4 Connect via MobaXterm + +1. Session → SSH +2. **Remote host:** `192.168.29.208` (or `localhost` for NAT port 2222) +3. **Port:** `22` (or `2222` for NAT) +4. **Username:** `harvi` +5. Click OK, enter password + +## 4.5 X11 Forwarding (for GUI) -- **eSim Copilot** – AI-assisted electronics simulation tool (FOSSEE/eSim) -- Based on Hariom's PR 434 (PyQt5, RAG, vision, voice, netlist analysis) -- Target: Ubuntu 22.04 (VM or WSL2) +MobaXterm includes an X server. X11 forwarding is usually enabled by default. When you run `python Application.py` over SSH, the eSim window appears on your Windows desktop. --- -## 2. Branch Setup +# 5. Code Transfer to VM + +## 5.1 Method A: SCP (direct copy) + +**On Windows (PowerShell):** + +```powershell +scp -r C:\Users\91900\Downloads\eSIM-Software-AIChatBot\repos\eSim harvi@192.168.29.208:~/ +``` + +Then on VM: + +```bash +mkdir -p ~/work +mv ~/eSim ~/work/eSim +cd ~/work/eSim +git checkout Chatbot_Enhancements +``` + +## 5.2 Method B: Zip (no shared folder) -- Created branch `Chatbot_Enhancements` from `pr-434` -- All enhancements committed to this branch +**On Windows (PowerShell):** + +```powershell +cd C:\Users\91900\Downloads\eSIM-Software-AIChatBot +.\repos\eSim\scripts\zip_for_vm.ps1 +``` + +Creates `eSim-for-VM.zip` in the workspace root. Copy into VM (drag-drop, USB, network share). + +**On VM:** + +```bash +mkdir -p ~/work +cd ~/work +unzip /path/to/eSim-for-VM.zip +cd eSim +git checkout Chatbot_Enhancements +``` + +## 5.3 Method C: Shared folder (VirtualBox) + +1. VM Settings → Shared Folders → Add +2. Folder path: `C:\Users\91900\Downloads\eSIM-Software-AIChatBot\repos` +3. Folder name: `repos` +4. Auto-mount, Make permanent +5. Install Guest Additions, add user to `vboxsf` group +6. On VM: `cp -r /media/sf_repos/eSim ~/work/eSim` --- -## 3. Ubuntu VM Setup +# 6. Code Fixes & Enhancements + +## 6.1 Line Endings (CRLF → LF) + +**Problem:** When running `./scripts/setup_copilot_ubuntu.sh`: + +``` +/usr/bin/env: 'bash\r': No such file or directory +``` + +**Cause:** Script was created/edited on Windows. Lines end with `\r\n` (CRLF). Linux expects `\n` (LF). The `\r` is interpreted as part of the shebang, so the system looks for `bash\r` instead of `bash`. + +**Fix (on VM):** -| Step | Action | -|------|--------| -| 1 | Downloaded Ubuntu 22.04 Desktop ISO | -| 2 | Created VM in VirtualBox (4–8 GB RAM, 25 GB disk) | -| 3 | Installed Ubuntu; user: `harvi`, password: `bhavin` | -| 4 | Installed SSH: `sudo apt install openssh-server` | -| 5 | Configured Bridged Adapter for network (after fixing "No Bridged Adapter" – selected host NIC) | -| 6 | VM IP: `192.168.29.208` (static config via Netplan) | -| 7 | Connected via MobaXterm SSH | +```bash +sed -i 's/\r$//' scripts/setup_copilot_ubuntu.sh +``` + +**Prevention:** Added `.gitattributes` with `*.sh text eol=lf` so Git always uses LF for shell scripts. --- -## 4. Code Transfer to VM +## 6.2 hdlparse Build Failure + +**Problem:** When installing `requirements.txt`: + +``` +error in hdlparse setup command: use_2to3 is invalid. +ERROR: Failed to build 'hdlparse' when getting requirements to build wheel +``` + +**Cause:** `hdlparse==1.0.4` uses `use_2to3` in its setup.py. Setuptools 58+ removed support for `use_2to3` (deprecated Python 2→3 conversion). + +**Fix:** Install `setuptools==57.5.0` first, then install hdlparse with `--no-build-isolation` (uses the environment's setuptools). Use a constraint file for remaining packages so setuptools stays < 58. + +**Changes in `scripts/setup_copilot_ubuntu.sh`:** + +```bash +python -m pip install setuptools==57.5.0 +python -m pip install hdlparse==1.0.4 --no-build-isolation +echo "setuptools<58" > /tmp/pip-constraints.txt +python -m pip install -c /tmp/pip-constraints.txt -r requirements.txt +python -m pip install -c /tmp/pip-constraints.txt -r requirements-copilot.txt +``` -- **Method:** SCP from Windows to VM -- **Path:** `C:\Users\91900\Downloads\eSIM-Software-AIChatBot\repos\eSim` → `harvi@192.168.29.208:~/work/eSim` -- **Script:** `scripts/zip_for_vm.ps1` for zipping; `scp -r` for direct copy +**Changes in `requirements.txt`:** `setuptools>=57.5.0,<58` --- -## 5. Code Fixes & Enhancements +## 6.3 Workspace Directory Missing -### 5.1 Line endings (CRLF → LF) +**Problem:** When launching eSim: -- **Issue:** `bash\r: No such file or directory` when running setup script -- **Fix:** `sed -i 's/\r$//' scripts/setup_copilot_ubuntu.sh` -- **Prevention:** Added `.gitattributes` with `*.sh text eol=lf` +``` +FileNotFoundError: [Errno 2] No such file or directory: '/home/harvi/.esim/workspace.txt' +``` -### 5.2 hdlparse build failure +**Cause:** `Workspace.py` tries to write `~/.esim/workspace.txt` but the `~/.esim` directory is never created. It assumes the directory exists. -- **Issue:** `use_2to3 is invalid` (setuptools ≥58 removed support) -- **Fix:** Install `setuptools==57.5.0` first; install `hdlparse==1.0.4 --no-build-isolation`; use `-c` constraint for remaining deps -- **Files:** `scripts/setup_copilot_ubuntu.sh`, `requirements.txt` +**Fix in `src/frontEnd/Workspace.py`:** -### 5.3 Workspace directory missing +```python +esim_dir = os.path.join(user_home, ".esim") +os.makedirs(esim_dir, exist_ok=True) +file = open(os.path.join(esim_dir, "workspace.txt"), 'w') +``` + +**Manual workaround (if you haven't updated the file):** + +```bash +mkdir -p ~/.esim +``` -- **Issue:** `FileNotFoundError: ~/.esim/workspace.txt` – `.esim` dir not created -- **Fix:** `Workspace.py` – add `os.makedirs(esim_dir, exist_ok=True)` before writing -- **Manual:** `mkdir -p ~/.esim` +--- -### 5.4 Other enhancements (from earlier) +## 6.4 Other Enhancements (from earlier) -- Optional STT (graceful fallback if Vosk missing) -- ChromaDB path: `~/.local/share/esim-copilot/chroma` -- Split requirements: `requirements.txt` + `requirements-copilot.txt` -- `DEPLOY_UBUNTU.md` – deployment guide -- `scripts/setup_copilot_ubuntu.sh` – one-command setup +| Enhancement | Description | +|-------------|-------------| +| Optional STT | `stt_handler.py` – graceful fallback if Vosk/sounddevice missing; app continues without voice | +| ChromaDB path | `knowledge_base.py` – uses `~/.local/share/esim-copilot/chroma` (user-writable) | +| Split requirements | `requirements.txt` (base) + `requirements-copilot.txt` (AI extras) | +| Unused import | `chatbot_core.py` – removed unused `sklearn` | --- -## 6. Setup Script Flow +# 7. Setup Script – Step by Step + +## Command ```bash +cd ~/work/eSim +chmod +x scripts/setup_copilot_ubuntu.sh ./scripts/setup_copilot_ubuntu.sh ``` -1. Install system packages (python3.10, ngspice, kicad, portaudio, xcb libs) -2. Create `.venv` -3. Install Python deps (with hdlparse workaround) -4. Install PaddlePaddle (CPU) -5. Install Ollama (`curl -fsSL https://ollama.com/install.sh | sh`) -6. Pull models: qwen2.5:3b, minicpm-v, nomic-embed-text -7. Download Vosk model to `~/.local/share/esim-copilot/` +## What Each Step Does + +### [1/7] System packages + +```bash +sudo apt-get install -y \ + python3.10 python3.10-venv python3-pip \ + curl wget unzip \ + ngspice kicad \ + portaudio19-dev \ + libgl1 libglib2.0-0 \ + libxcb-xinerama0 libxcb-cursor0 libxkbcommon-x11-0 \ + libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-render-util0 \ + libxcb-xinput0 libxcb-shape0 libxcb-randr0 libxcb-util1 +``` + +- **python3.10:** Python runtime +- **ngspice, kicad:** Circuit simulation and schematic capture +- **portaudio19-dev:** Audio for voice input +- **libxcb-*:** Qt/X11 display libraries + +### [2/7] Virtualenv + +Creates `.venv/` in the repo root. All Python packages are installed in this isolated environment. + +### [3/7] Python dependencies + +- Upgrades pip, wheel +- Installs setuptools 57.5.0, hdlparse (with workaround) +- Installs `requirements.txt` and `requirements-copilot.txt` with setuptools constraint + +### [4/7] PaddlePaddle + +Installs PaddlePaddle 2.5.2 (CPU) for PaddleOCR. If this fails, vision features may not work; text chat still works. + +### [5/7] Ollama + +```bash +curl -fsSL https://ollama.com/install.sh | sh +``` + +Installs Ollama (LLM server). Runs as systemd service by default. + +### [6/7] Ollama models + +```bash +ollama pull qwen2.5:3b +ollama pull minicpm-v +ollama pull nomic-embed-text +``` + +- **qwen2.5:3b:** Main text model +- **minicpm-v:** Vision model + +### [7/7] Vosk + +Downloads Vosk small English model to `~/.local/share/esim-copilot/vosk-model-small-en-us-0.15` for offline speech-to-text. --- -## 7. Launch Flow +# 8. Ollama + +## Installation + +Ollama install script installs it as a systemd service. It starts automatically. + +## API + +- **URL:** `http://127.0.0.1:11434` +- **Status:** `sudo systemctl status ollama` + +## CPU-only mode + +In a VM without GPU, Ollama runs in CPU-only mode. Responses may be slower but work. + +## Manual start (if not using systemd) + +```bash +ollama serve +``` + +Keep this terminal open while using eSim. + +## Run as systemd service (always on) + +```bash +sudo systemctl enable ollama +sudo systemctl start ollama +``` -### Ollama +--- -- Installed as systemd service (runs automatically) -- API: `127.0.0.1:11434` -- CPU-only mode (no GPU in VM) +# 9. Launch Flow -### eSim +## Launch script ```bash +cd ~/work/eSim ./scripts/launch_esim.sh ``` -Or manually: +**What it does:** +1. Creates `~/.esim` if missing +2. Activates `.venv` +3. Runs `QT_QPA_PLATFORM=xcb python Application.py` from `src/frontEnd` + +## Manual launch ```bash cd ~/work/eSim @@ -112,35 +441,173 @@ cd src/frontEnd QT_QPA_PLATFORM=xcb python Application.py ``` +## Why `QT_QPA_PLATFORM=xcb`? + +Sets Qt to use the XCB (X11) backend. Required for correct display when running over SSH with X11 forwarding. + +## Using the Copilot + +After eSim opens, click the **eSim Copilot** button in the toolbar to open the AI chat panel. + +--- + +# 10. Files Created/Modified – Full Details + +## `.gitattributes` + +**Purpose:** Force all `.sh` files to use LF line endings in Git. + +**Content:** +``` +* text=auto +*.sh text eol=lf +``` + --- -## 8. Files Created/Modified +## `DEPLOY_UBUNTU.md` + +**Purpose:** Deployment guide for Ubuntu VM and WSL2. -| File | Change | -|------|--------| -| `.gitattributes` | LF for `.sh` files | -| `DEPLOY_UBUNTU.md` | VM setup, SSH, deployment steps | -| `requirements.txt` | setuptools<58 for hdlparse | -| `requirements-copilot.txt` | AI deps (new) | -| `scripts/setup_copilot_ubuntu.sh` | Full setup, hdlparse fix | -| `scripts/launch_esim.sh` | Single launch script | -| `scripts/zip_for_vm.ps1` | Zip eSim for VM transfer | -| `src/frontEnd/Workspace.py` | Create `.esim` dir before write | -| `src/chatbot/chatbot_core.py` | Removed unused sklearn | -| `src/chatbot/stt_handler.py` | Optional STT | -| `src/chatbot/knowledge_base.py` | ChromaDB path | +**Contents:** +- VM setup checklist +- VirtualBox/Hyper-V/VMware instructions +- Shared folder and zip transfer methods +- Setup script steps +- Ollama, ingest, launch commands +- Troubleshooting table +- WSL2 option --- -## 9. Known Issues +## `SESSION_SUMMARY.md` + +**Purpose:** This document – session summary and deployment guide. + +--- + +## `requirements.txt` + +**Changes:** +- `setuptools==65.5.0` → `setuptools>=57.5.0,<58` (for hdlparse compatibility) + +--- + +## `requirements-copilot.txt` + +**Purpose:** AI-related dependencies (Ollama client, ChromaDB, PaddleOCR, Vosk, etc.). Separate from base eSim requirements. + +--- + +## `scripts/setup_copilot_ubuntu.sh` + +**Purpose:** One-command setup for Ubuntu. + +**Changes:** +- hdlparse workaround (setuptools 57.5.0, `--no-build-isolation`, constraint file) +- Full 7-step flow (system packages, venv, deps, PaddlePaddle, Ollama, models, Vosk) + +--- + +## `scripts/launch_esim.sh` + +**Purpose:** Single script to launch eSim. + +**Contents:** +- Creates `~/.esim` if missing +- Activates venv +- Runs `QT_QPA_PLATFORM=xcb python Application.py` from `src/frontEnd` + +**Usage:** `./scripts/launch_esim.sh` + +--- + +## `scripts/zip_for_vm.ps1` + +**Purpose:** PowerShell script to zip `eSim` for transfer to VM. + +**Usage:** +```powershell +.\repos\eSim\scripts\zip_for_vm.ps1 +``` + +**Output:** `eSim-for-VM.zip` in workspace root. + +--- + +## `src/frontEnd/Workspace.py` + +**Purpose:** Create workspace directory before writing. + +**Change:** Added `os.makedirs(esim_dir, exist_ok=True)` before opening `workspace.txt` for writing. + +--- + +## `src/chatbot/chatbot_core.py` + +**Change:** Removed unused `sklearn` import. + +--- + +## `src/chatbot/stt_handler.py` + +**Change:** Made STT optional; graceful fallback if Vosk/sounddevice missing. + +--- + +## `src/chatbot/knowledge_base.py` + +**Change:** ChromaDB path set to `~/.local/share/esim-copilot/chroma` (user-writable). + +--- + +# 11. Known Issues + +| Issue | Impact | Workaround | +|-------|--------|------------| +| `No module named 'paddle'` (PaddleOCR) | Vision features may not work | Text chat works; install PaddlePaddle if needed | +| Missing `manuals/esim_netlist_analysis_output_contract.txt` | Netlist contract not loaded | Optional; add file if needed | +| `DeprecationWarning: sipPyTypeDict()` | PyQt5/sip deprecation | Safe to ignore | +| `Cannot access Modelica map file` | Modelica config missing | Optional; config.ini in ~/.esim | + +--- + +# 12. Troubleshooting + +| Problem | Solution | +|---------|----------| +| `bash\r: No such file or directory` | Run `sed -i 's/\r$//' scripts/setup_copilot_ubuntu.sh` | +| `use_2to3 is invalid` (hdlparse) | Ensure setup script has hdlparse workaround; or manually: `pip install setuptools==57.5.0` then `pip install hdlparse==1.0.4 --no-build-isolation` | +| `FileNotFoundError: ~/.esim/workspace.txt` | Run `mkdir -p ~/.esim` or use updated Workspace.py | +| `ollama: command not found` | Run `curl -fsSL https://ollama.com/install.sh \| sh` | +| Ollama not responding | Run `ollama serve` or ensure systemd service: `sudo systemctl start ollama` | +| GUI doesn't appear (MobaXterm) | Enable X11 forwarding; check `echo $DISPLAY` | +| No Bridged Adapter | Select host NIC in dropdown; or use NAT + port forwarding | +| Host Interface Networking driver | Reinstall VirtualBox; or use NAT | + +--- + +# 13. Push to GitHub + +## Fork FOSSEE/eSim + +1. Go to https://github.com/FOSSEE/eSim +2. Click **Fork** +3. Fork to your account (e.g. harvi.bhavinpatel2024@gmail.com) + +## Add remote and push + +```powershell +cd C:\Users\91900\Downloads\eSIM-Software-AIChatBot\repos\eSim +git remote add myfork https://github.com/YOUR_GITHUB_USERNAME/eSim.git +git push myfork Chatbot_Enhancements +``` -- **PaddleOCR:** `No module named 'paddle'` – vision features may be limited; text chat works -- **Netlist contract:** Missing `manuals/esim_netlist_analysis_output_contract.txt` – optional -- **DeprecationWarnings:** PyQt5/sip – safe to ignore +Replace `YOUR_GITHUB_USERNAME` with your GitHub username. --- -## 10. Quick Reference +# 14. Quick Reference | Item | Value | |------|-------| @@ -149,3 +616,5 @@ QT_QPA_PLATFORM=xcb python Application.py | Repo path | ~/work/eSim | | Branch | Chatbot_Enhancements | | Launch | `./scripts/launch_esim.sh` | +| Ollama API | http://127.0.0.1:11434 | +| SCP copy | `scp -r repos\eSim harvi@192.168.29.208:~/work/` | From 36eded6fe3f55150261010e7555921388fbc4233 Mon Sep 17 00:00:00 2001 From: eSim Copilot Dev Date: Tue, 3 Mar 2026 16:06:54 +0530 Subject: [PATCH 31/34] Add CHATBOT_ENHANCEMENT_PROPOSAL, update SESSION_SUMMARY Made-with: Cursor --- CHATBOT_ENHANCEMENT_PROPOSAL.md | 238 ++++++++++++ SESSION_SUMMARY.md | 622 ++++---------------------------- 2 files changed, 316 insertions(+), 544 deletions(-) create mode 100644 CHATBOT_ENHANCEMENT_PROPOSAL.md diff --git a/CHATBOT_ENHANCEMENT_PROPOSAL.md b/CHATBOT_ENHANCEMENT_PROPOSAL.md new file mode 100644 index 000000000..fb2640c50 --- /dev/null +++ b/CHATBOT_ENHANCEMENT_PROPOSAL.md @@ -0,0 +1,238 @@ +# eSim Copilot – Chatbot Enhancement Proposal + +**Focus:** Hariom's approach (PR 434) +**Branch:** Chatbot_Enhancements +**Date:** March 2025 + +--- + +# Part 1: Document Summary + +## 1.1 Four Source Documents + +| Document | Author | Focus | +|----------|--------|-------| +| **Project Context.pdf** | Synthesis | Rationale, problems, comparison of 3 interns, proposed Federated Knowledge Sync | +| **5 hariom.pdf** | Hariom Thakur | Full technical report: RAG, vision, FACT-based netlist, PyQt5 integration, automated error capture | +| **18 radhika goyal.pdf** | Radhika Goyal | Rule-based fault identification, static schematic/netlist analysis, cross-validation | +| **1 Nicholas_Coutinho.pdf** | Nicholas Coutinho | Conversational memory, topic discontinuity, context retention | + +## 1.2 Three Intern Approaches (from Project Context) + +| Aspect | Radhika | Nicholas | **Hariom** | +|--------|---------|----------|-------------| +| **Intelligence** | Proactive static analysis, rule-based | Conversational memory, topic discontinuity | **FACT-based netlist, strict RAG grounding** | +| **UI Integration** | Standard chat | Standard chat | **Deep PyQt5: dock, toolbar, context menus, auto error capture** | +| **Multimodal** | Text + image | Text + image | **Text + image + voice (Vosk)** | +| **Tech stack** | Broad | Broad | **Specific: qwen2.5:3b, minicpm-v, nomic-embed-text, ChromaDB** | + +**Project Context conclusion:** Hariom's approach is best for **practical deployment & user experience** due to deep integration, automated error capture, and voice STT. + +--- + +# Part 2: Current Chatbot Functionality (Hariom's Implementation) + +## 2.1 Architecture Overview + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ Presentation Layer (PyQt5) │ +│ • Application.py: toolbar, openChatbot(), errorDetectedSignal │ +│ • Chatbot.py: dock widget, chat UI, input, image/voice buttons │ +│ • ProjectExplorer.py: context menu "Analyze this Netlist" │ +│ • DockArea.py: createchatbotdock() │ +└─────────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────┐ +│ Processing Layer (chatbot_core.py) │ +│ • classify_question_type() → routing │ +│ • handle_esim_question(), handle_image_query(), handle_netlist_analysis()│ +│ • handle_follow_up(), handle_simple_question() │ +└─────────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────┐ +│ Data Layer │ +│ • knowledge_base.py: ChromaDB, search_knowledge() │ +│ • image_handler.py: PaddleOCR + MiniCPM-V │ +│ • Chatbot.py: FACT-based netlist detection (_detect_floating_nodes, etc) │ +│ • error_solutions.py: pattern → fixes mapping │ +│ • ollama_runner.py: run_ollama(), run_ollama_vision(), get_embedding() │ +│ • stt_handler.py: Vosk offline STT │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +## 2.2 Current Features (Implemented) + +| Feature | Status | Location | +|---------|--------|----------| +| **Intelligent Router** | ✅ | `chatbot_core.py` – classify_question_type() | +| **RAG (ChromaDB)** | ✅ | `knowledge_base.py` – search_knowledge() | +| **FACT-based netlist** | ✅ | `Chatbot.py` – _detect_floating_nodes, _detect_missing_models, etc. | +| **Vision (PaddleOCR + MiniCPM-V)** | ✅ | `image_handler.py` – analyze_and_extract() | +| **Voice (Vosk)** | ✅ (optional) | `stt_handler.py` | +| **Project Explorer context menu** | ✅ | `ProjectExplorer.py` – "Analyze this Netlist" | +| **Automated error capture** | ✅ | `Application.py` – errorDetectedSignal → send_error_to_chatbot | +| **Dockable chat** | ✅ | `Chatbot.py`, `DockArea.py` | +| **Error pattern matching** | ✅ | `error_solutions.py` – ERROR_SOLUTIONS dict | + +## 2.3 Current Gaps / Limitations (from Hariom's report) + +1. **Model capability constraints:** qwen2.5:3b struggles with complex multi-step reasoning; minicpm-v misinterprets complex topologies; 2048-token context limit. +2. **Performance on low-end hardware:** 8GB RAM minimum; vision can take 10+ seconds on older CPUs. +3. **Scope limitations:** No training mode; fixed knowledge base; cannot dynamically add new docs. +4. **PaddleOCR:** Often fails on first run (no `paddle` module) – vision falls back to partial OCR. +5. **Netlist contract:** Missing `manuals/esim_netlist_analysis_output_contract.txt` in some setups. + +--- + +# Part 3: Proposed Enhancements (Hariom’s Approach) + +## 3.1 Near-Term (Next 6 Months) – from Hariom's report + +### 3.1.1 One-Click Netlist Fix + +**Current:** Copilot suggests fixes; user must manually edit in Spice Editor. + +**Proposal:** Add "Apply fix" button that auto-inserts suggested fixes into `.cir.out`: + +- Add missing `.model` statements +- Add `.options gmin=1e-12 reltol=0.01` for singular matrix +- Add 1G resistors for floating nodes (as comments with copy-paste snippet) + +**Implementation:** Extend `Chatbot.py` – parse FACT output, generate patch, offer "Insert into netlist" action. + +--- + +### 3.1.2 Real-Time Suggestions in KiCad + +**Current:** User must open Copilot and ask. + +**Proposal:** Optional "live hints" during schematic capture – e.g. when floating pin detected, show small tooltip: "R1 pin 2 is unconnected." + +**Implementation:** Requires KiCad callback or periodic checks; may need eSim/KiCad integration points. + +--- + +### 3.1.3 Batch Processing for Multiple Images/Netlists + +**Current:** One image or netlist at a time. + +**Proposal:** Allow selecting multiple `.cir.out` files or pasting multiple images; run analysis in batch; report summary. + +**Implementation:** Extend `Chatbot.py` – `analyze_specific_netlist()` accepts list; `handle_image_query()` can process multiple paths. + +--- + +### 3.1.4 RAG Relevance Threshold + +**Current:** `knowledge_base.py` returns top 4 chunks; no explicit cosine similarity filter. + +**Proposal:** Add relevance threshold (e.g. cosine similarity > 0.3) – Hariom's report mentions this; filter out low-similarity chunks to reduce hallucination. + +**Implementation:** ChromaDB query returns `distances`; filter by threshold before context assembly. + +--- + +### 3.1.5 Stricter FACT-Based Prompting + +**Current:** Netlist analysis uses FACT blocks; contract file may be missing. + +**Proposal:** Ensure contract is always available; bundle `esim_netlist_analysis_output_contract.txt` in repo; add fallback inline prompt if file missing. + +**Implementation:** Copy contract to `src/manuals/` or `src/frontEnd/manuals/`; ensure `Chatbot.py` loads from correct path. + +--- + +### 3.1.6 Model Selection (Optional) + +**Current:** Hardcoded qwen2.5:3b, minicpm-v. + +**Proposal:** Allow user to choose model in settings (e.g. llama3, deepseek-coder for code-heavy tasks). + +**Implementation:** Add `ollama_model` config; pass to `run_ollama()`. + +--- + +## 3.2 Medium-Term (6–18 Months) + +### 3.2.1 Circuit Optimization Suggestions + +**Proposal:** After simulation succeeds, analyze output and suggest improvements (e.g. "Add capacitor for stability"). + +**Implementation:** Parse `.raw` or simulation output; integrate with plotting tools; add optional "optimization" analysis mode. + +--- + +### 3.2.2 Predictive Error Detection During Schematic Capture + +**Proposal:** (Radhika's strength) – cross-validate schematic vs netlist before simulation; detect mismatches early. + +**Implementation:** Hook into eSim's netlist generation; run static analysis on generated netlist before user runs simulation. + +--- + +### 3.2.3 Enhanced Conversation Memory (Nicholas's strength) + +**Proposal:** Improve topic discontinuity detection; add reference resolution for "this", "that", "it". + +**Implementation:** Refine `_is_follow_up_question()` and `is_semantic_topic_switch()`; use embedding similarity for pronoun resolution. + +--- + +### 3.2.4 Federated Knowledge Sync (Project Context proposal) + +**Proposal:** When user fixes an error after Copilot failed, prompt: "What did you change?" – store locally; optionally sync anonymously to FOSSEE server; server clusters fixes; push updates to ChromaDB. + +**Implementation:** Large; requires server, encryption, consent UI. Defer to long-term. + +--- + +## 3.3 Long-Term (18+ Months) + +- **Autonomous design assistance:** Circuit synthesis from specs. + +- **Research platform:** Dataset from anonymized interactions; benchmarking suite. + +- **Ecosystem expansion:** Port to OpenModelica, Scilab; plugin architecture. + +--- + +# Part 4: Prioritized Implementation Roadmap + +| Priority | Enhancement | Effort | Impact | +|----------|-------------|--------|--------| +| 1 | RAG relevance threshold | Low | High (reduces hallucination) | +| 2 | Netlist contract bundling | Low | Medium (fixes missing contract) | +| 3 | One-click netlist fix | Medium | High (UX) | +| 4 | Batch netlist analysis | Low | Medium | +| 5 | Model selection in settings | Low | Medium | +| 6 | Enhanced conversation memory | Medium | Medium | +| 7 | Batch image processing | Low | Low | +| 8 | Real-time KiCad hints | High | Medium | + +--- + +# Part 5: Quick Wins (Immediate) + +1. **Add relevance threshold to `search_knowledge()`** – filter by distance/similarity. +2. **Bundle netlist contract** – ensure `esim_netlist_analysis_output_contract.txt` is in `src/manuals/` or `src/frontEnd/manuals/` and loaded correctly. +3. **Improve error message clarity** – when PaddleOCR fails, show: "Vision analysis unavailable. Text and netlist analysis still work." +4. **Add "Copy to clipboard" for netlist fixes** – so user can paste without manual retyping. + +--- + +# Part 6: File Reference + +| File | Purpose | +|------|---------| +| `src/chatbot/chatbot_core.py` | Router, handlers, classification | +| `src/chatbot/knowledge_base.py` | ChromaDB, search_knowledge | +| `src/chatbot/ollama_runner.py` | Ollama API, embeddings | +| `src/chatbot/image_handler.py` | PaddleOCR, MiniCPM-V | +| `src/chatbot/stt_handler.py` | Vosk STT | +| `src/chatbot/error_solutions.py` | Error pattern → fixes | +| `src/frontEnd/Chatbot.py` | UI, netlist FACT detection, analyze_specific_netlist | +| `src/frontEnd/Application.py` | errorDetectedSignal, openChatbot | +| `src/frontEnd/ProjectExplorer.py` | Context menu | diff --git a/SESSION_SUMMARY.md b/SESSION_SUMMARY.md index 205a71ce3..45ec1bd75 100644 --- a/SESSION_SUMMARY.md +++ b/SESSION_SUMMARY.md @@ -12,609 +12,143 @@ This document explains every step, every file, every command, and every fix from 1. [Project Overview](#1-project-overview) 2. [Branch Setup](#2-branch-setup) -3. [Ubuntu VM Setup](#3-ubuntu-vm-setup) -4. [Networking & SSH](#4-networking--ssh) -5. [Code Transfer to VM](#5-code-transfer-to-vm) -6. [Code Fixes & Enhancements](#6-code-fixes--enhancements) -7. [Setup Script – Step by Step](#7-setup-script--step-by-step) -8. [Ollama](#8-ollama) -9. [Launch Flow](#9-launch-flow) -10. [Files Created/Modified – Full Details](#10-files-createdmodified--full-details) -11. [Known Issues](#11-known-issues) -12. [Troubleshooting](#12-troubleshooting) -13. [Push to GitHub](#13-push-to-github) -14. [Quick Reference](#14-quick-reference) +3. [Branch Changes & Commit History](#3-branch-changes--commit-history) +4. [Technical Breakdown of Fixes](#4-technical-breakdown-of-fixes) +5. [Ubuntu VM Setup](#5-ubuntu-vm-setup) +6. [Networking & SSH](#6-networking--ssh) +7. [Code Transfer to VM](#7-code-transfer-to-vm) +8. [Setup Script – Full 7-Step Flow](#8-setup-script--full-7-step-flow) +9. [Ollama & Models](#9-ollama--models) +10. [Launch & Usage](#10-launch--usage) +11. [Known Issues & Troubleshooting](#11-known-issues--troubleshooting) +12. [Push to GitHub](#12-push-to-github) --- # 1. Project Overview ## What is eSim? - **eSim** is an open-source electronics simulation tool (FOSSEE/eSim) that uses: - **ngspice** – SPICE circuit simulator - **KiCad** – Schematic capture - **PyQt5** – Desktop GUI ## What is eSim Copilot? - -**eSim Copilot** is an AI assistant integrated into eSim, based on Hariom's PR 434. It provides: - -| Feature | Technology | -|---------|------------| -| Text chat | Ollama (LLM) | -| RAG (Retrieval Augmented Generation) | ChromaDB | -| Vision (OCR, image analysis) | PaddleOCR, MiniCPM-V | -| Speech-to-text | Vosk | -| Netlist analysis | FACT-based contract | - -## Why Ubuntu VM? - -eSim is designed for Linux. Windows has issues with native dependencies (e.g. `contourpy` needs Visual Studio C++ build tools). Ubuntu VM or WSL2 provides a clean Linux environment. +Enhanced assistant integrated into eSim, providing: +- **Text chat:** Ollama (LLM) +- **RAG:** ChromaDB +- **Vision:** PaddleOCR, MiniCPM-V +- **Speech-to-text:** Vosk --- # 2. Branch Setup -## Source - -- **Base:** `pr-434` (Hariom's Copilot implementation) -- **New branch:** `Chatbot_Enhancements` - -## Commands - ```bash -cd ~/work/eSim +# Created from Hariom's Copilot implementation git fetch origin pull/434/head:pr-434 git checkout -b Chatbot_Enhancements pr-434 ``` -All enhancements from this session are committed to `Chatbot_Enhancements`. - ---- - -# 3. Ubuntu VM Setup - -## 3.1 Download Ubuntu - -- **URL:** https://ubuntu.com/download/desktop -- **Version:** Ubuntu 22.04 LTS (64-bit) -- **Size:** ~4 GB ISO - -## 3.2 Create VM in VirtualBox - -| Setting | Value | -|---------|-------| -| Name | eSim-Ubuntu (or any name) | -| Type | Linux | -| Version | Ubuntu (64-bit) | -| RAM | 4096 MB minimum, 8192 MB recommended for Ollama | -| Disk | VDI, Dynamically allocated, 25 GB | -| ISO | Attach Ubuntu 22.04 ISO to optical drive | - -## 3.3 Install Ubuntu - -1. Start VM, boot from ISO -2. Choose "Install Ubuntu" -3. Normal install, minimal install optional -4. **Product key:** Leave empty (Ubuntu is free) -5. **User credentials:** e.g. `harvi` / `bhavin` -6. Host name: e.g. `harvi-VirtualBox` - -## 3.4 Install Guest Additions (VirtualBox) - -- If using shared folder: Devices → Insert Guest Additions CD -- Run the installer from the mounted CD -- Reboot if prompted - -## 3.5 Network Adapter - -- **Attached to:** Bridged Adapter -- **Name:** Select your host NIC (Wi-Fi or Ethernet) - -- If "No Bridged Network Adapter" or "Host Interface Networking driver" error: - - VirtualBox → Repair → ensure "VirtualBox NDIS6 Bridged Networking Driver" is installed - - Or use NAT with port forwarding (Host 2222 → Guest 22) - ---- - -# 4. Networking & SSH - -## 4.1 Enable SSH on VM - -```bash -sudo apt update -sudo apt install -y openssh-server -sudo systemctl enable ssh -sudo systemctl start ssh -``` - -## 4.2 Find VM IP - -```bash -ip addr -``` - -- **Bridged:** Look for `inet` on `enp0s3` (e.g. `192.168.29.208`) -- **NAT:** `10.0.2.15` – use port forwarding for SSH from host - -## 4.3 Static IP (optional) - -```bash -sudo nano /etc/netplan/00-installer-config.yaml -``` - -```yaml -network: - version: 2 - ethernets: - enp0s3: - dhcp4: no - addresses: - - 192.168.29.208/24 - routes: - - to: default - via: 192.168.29.1 - nameservers: - addresses: - - 8.8.8.8 - - 8.8.4.4 -``` - -```bash -sudo netplan apply -``` - -## 4.4 Connect via MobaXterm - -1. Session → SSH -2. **Remote host:** `192.168.29.208` (or `localhost` for NAT port 2222) -3. **Port:** `22` (or `2222` for NAT) -4. **Username:** `harvi` -5. Click OK, enter password - -## 4.5 X11 Forwarding (for GUI) - -MobaXterm includes an X server. X11 forwarding is usually enabled by default. When you run `python Application.py` over SSH, the eSim window appears on your Windows desktop. - --- -# 5. Code Transfer to VM - -## 5.1 Method A: SCP (direct copy) - -**On Windows (PowerShell):** - -```powershell -scp -r C:\Users\91900\Downloads\eSIM-Software-AIChatBot\repos\eSim harvi@192.168.29.208:~/ -``` - -Then on VM: - -```bash -mkdir -p ~/work -mv ~/eSim ~/work/eSim -cd ~/work/eSim -git checkout Chatbot_Enhancements -``` - -## 5.2 Method B: Zip (no shared folder) - -**On Windows (PowerShell):** - -```powershell -cd C:\Users\91900\Downloads\eSIM-Software-AIChatBot -.\repos\eSim\scripts\zip_for_vm.ps1 -``` - -Creates `eSim-for-VM.zip` in the workspace root. Copy into VM (drag-drop, USB, network share). - -**On VM:** +# 3. Branch Changes & Commit History -```bash -mkdir -p ~/work -cd ~/work -unzip /path/to/eSim-for-VM.zip -cd eSim -git checkout Chatbot_Enhancements -``` +### Key File Changes +- **`scripts/setup_copilot_ubuntu.sh`**: New comprehensive setup script. +- **`scripts/launch_esim.sh`**: New utility to launch with correct Qt backend. +- **`src/frontEnd/Workspace.py`**: Fixed directory creation bug. +- **`src/chatbot/stt_handler.py`**: Made STT optional with graceful fallback. +- **`.gitattributes`**: Added to force LF on shell scripts. -## 5.3 Method C: Shared folder (VirtualBox) - -1. VM Settings → Shared Folders → Add -2. Folder path: `C:\Users\91900\Downloads\eSIM-Software-AIChatBot\repos` -3. Folder name: `repos` -4. Auto-mount, Make permanent -5. Install Guest Additions, add user to `vboxsf` group -6. On VM: `cp -r /media/sf_repos/eSim ~/work/eSim` +### Recent Commits +- `71ed0f5c`: SESSION_SUMMARY: comprehensive guide with every item explained +- `4c67304d`: Ubuntu deployment, hdlparse fix, Workspace.py fix, launch script +- `67f08043`: Ubuntu deployment support, optional STT, ChromaDB path fix --- -# 6. Code Fixes & Enhancements - -## 6.1 Line Endings (CRLF → LF) - -**Problem:** When running `./scripts/setup_copilot_ubuntu.sh`: - -``` -/usr/bin/env: 'bash\r': No such file or directory -``` +# 4. Technical Breakdown of Fixes -**Cause:** Script was created/edited on Windows. Lines end with `\r\n` (CRLF). Linux expects `\n` (LF). The `\r` is interpreted as part of the shebang, so the system looks for `bash\r` instead of `bash`. +### 4.1 hdlparse & setuptools Fix +**Problem:** `hdlparse==1.0.4` uses `use_2to3`, which was removed in `setuptools 58+`. +**Fix:** +1. Manually install `setuptools==57.5.0`. +2. Install `hdlparse` with `--no-build-isolation` to force it to use the downgrade. +3. Use a `/tmp/pip-constraints.txt` with `setuptools<58` for all subsequent installs. -**Fix (on VM):** +### 4.2 Workspace.py Crash +**Problem:** `FileNotFoundError` when writing `~/.esim/workspace.txt` because the `.esim` folder was missing. +**Fix:** Added `os.makedirs(esim_dir, exist_ok=True)` in `src/frontEnd/Workspace.py`. -```bash -sed -i 's/\r$//' scripts/setup_copilot_ubuntu.sh -``` +### 4.3 Mandatory LF for Scripts +**Problem:** `bash\r: No such file or directory` due to Windows CRLF. +**Fix:** Created `.gitattributes` to enforce `*.sh text eol=lf`. -**Prevention:** Added `.gitattributes` with `*.sh text eol=lf` so Git always uses LF for shell scripts. +### 4.4 Graceful STT Fallback +**Problem:** App crashed if `vosk` or `sounddevice` were missing. +**Fix:** Wrapped imports in `try/except` and added `_HAS_STT` flag in `stt_handler.py`. --- -## 6.2 hdlparse Build Failure - -**Problem:** When installing `requirements.txt`: - -``` -error in hdlparse setup command: use_2to3 is invalid. -ERROR: Failed to build 'hdlparse' when getting requirements to build wheel -``` - -**Cause:** `hdlparse==1.0.4` uses `use_2to3` in its setup.py. Setuptools 58+ removed support for `use_2to3` (deprecated Python 2→3 conversion). - -**Fix:** Install `setuptools==57.5.0` first, then install hdlparse with `--no-build-isolation` (uses the environment's setuptools). Use a constraint file for remaining packages so setuptools stays < 58. - -**Changes in `scripts/setup_copilot_ubuntu.sh`:** - -```bash -python -m pip install setuptools==57.5.0 -python -m pip install hdlparse==1.0.4 --no-build-isolation -echo "setuptools<58" > /tmp/pip-constraints.txt -python -m pip install -c /tmp/pip-constraints.txt -r requirements.txt -python -m pip install -c /tmp/pip-constraints.txt -r requirements-copilot.txt -``` - -**Changes in `requirements.txt`:** `setuptools>=57.5.0,<58` +# 5. Ubuntu VM Setup +- **ISO:** Ubuntu 22.04 LTS +- **Hardware:** 4-8 GB RAM, 25 GB Disk +- **Network:** Bridged Adapter (preferred) or NAT with Port Forwarding (2222 -> 22) --- -## 6.3 Workspace Directory Missing - -**Problem:** When launching eSim: - -``` -FileNotFoundError: [Errno 2] No such file or directory: '/home/harvi/.esim/workspace.txt' -``` - -**Cause:** `Workspace.py` tries to write `~/.esim/workspace.txt` but the `~/.esim` directory is never created. It assumes the directory exists. - -**Fix in `src/frontEnd/Workspace.py`:** - -```python -esim_dir = os.path.join(user_home, ".esim") -os.makedirs(esim_dir, exist_ok=True) -file = open(os.path.join(esim_dir, "workspace.txt"), 'w') -``` - -**Manual workaround (if you haven't updated the file):** - +# 6. Networking & SSH ```bash -mkdir -p ~/.esim +sudo apt install -y openssh-server +sudo systemctl enable --now ssh +ip addr # Find IP ``` --- -## 6.4 Other Enhancements (from earlier) - -| Enhancement | Description | -|-------------|-------------| -| Optional STT | `stt_handler.py` – graceful fallback if Vosk/sounddevice missing; app continues without voice | -| ChromaDB path | `knowledge_base.py` – uses `~/.local/share/esim-copilot/chroma` (user-writable) | -| Split requirements | `requirements.txt` (base) + `requirements-copilot.txt` (AI extras) | -| Unused import | `chatbot_core.py` – removed unused `sklearn` | +# 7. Code Transfer to VM +- **Method A (SCP):** `scp -r repos/eSim user@ip:~/work/` +- **Method B (Zip):** Use `./scripts/zip_for_vm.ps1` on Windows, then transfer zip. --- -# 7. Setup Script – Step by Step - -## Command - -```bash -cd ~/work/eSim -chmod +x scripts/setup_copilot_ubuntu.sh -./scripts/setup_copilot_ubuntu.sh -``` - -## What Each Step Does - -### [1/7] System packages - -```bash -sudo apt-get install -y \ - python3.10 python3.10-venv python3-pip \ - curl wget unzip \ - ngspice kicad \ - portaudio19-dev \ - libgl1 libglib2.0-0 \ - libxcb-xinerama0 libxcb-cursor0 libxkbcommon-x11-0 \ - libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-render-util0 \ - libxcb-xinput0 libxcb-shape0 libxcb-randr0 libxcb-util1 -``` - -- **python3.10:** Python runtime -- **ngspice, kicad:** Circuit simulation and schematic capture -- **portaudio19-dev:** Audio for voice input -- **libxcb-*:** Qt/X11 display libraries - -### [2/7] Virtualenv - -Creates `.venv/` in the repo root. All Python packages are installed in this isolated environment. - -### [3/7] Python dependencies - -- Upgrades pip, wheel -- Installs setuptools 57.5.0, hdlparse (with workaround) -- Installs `requirements.txt` and `requirements-copilot.txt` with setuptools constraint - -### [4/7] PaddlePaddle - -Installs PaddlePaddle 2.5.2 (CPU) for PaddleOCR. If this fails, vision features may not work; text chat still works. - -### [5/7] Ollama - -```bash -curl -fsSL https://ollama.com/install.sh | sh -``` - -Installs Ollama (LLM server). Runs as systemd service by default. - -### [6/7] Ollama models - -```bash -ollama pull qwen2.5:3b -ollama pull minicpm-v -ollama pull nomic-embed-text -``` - -- **qwen2.5:3b:** Main text model -- **minicpm-v:** Vision model - -### [7/7] Vosk - -Downloads Vosk small English model to `~/.local/share/esim-copilot/vosk-model-small-en-us-0.15` for offline speech-to-text. +# 8. Setup Script – Full 7-Step Flow +Run `./scripts/setup_copilot_ubuntu.sh`: +1. **System Packages:** Install `ngspice`, `kicad`, `portaudio`, `libxcb` (Qt dependencies). +2. **Virtualenv:** Create isolated `.venv`. +3. **Python Deps:** Pip upgrade, `setuptools` downgrade, and RAG/AI requirements. +4. **PaddlePaddle:** Install CPU version for OCR. +5. **Ollama:** Download and install LLM server. +6. **Models:** Pull `qwen2.5:3b`, `minicpm-v`, and `nomic-embed-text`. +7. **Vosk:** Download offline English model to `~/.local/share/esim-copilot/`. --- -# 8. Ollama - -## Installation - -Ollama install script installs it as a systemd service. It starts automatically. - -## API - -- **URL:** `http://127.0.0.1:11434` -- **Status:** `sudo systemctl status ollama` - -## CPU-only mode - -In a VM without GPU, Ollama runs in CPU-only mode. Responses may be slower but work. - -## Manual start (if not using systemd) - -```bash -ollama serve -``` - -Keep this terminal open while using eSim. - -## Run as systemd service (always on) - -```bash -sudo systemctl enable ollama -sudo systemctl start ollama -``` +# 9. Ollama & Models +- **API:** `http://127.0.0.1:11434` +- **Service:** Managed via `systemctl` or manual `ollama serve`. --- -# 9. Launch Flow - -## Launch script - +# 10. Launch & Usage ```bash -cd ~/work/eSim ./scripts/launch_esim.sh ``` - -**What it does:** -1. Creates `~/.esim` if missing -2. Activates `.venv` -3. Runs `QT_QPA_PLATFORM=xcb python Application.py` from `src/frontEnd` - -## Manual launch - -```bash -cd ~/work/eSim -source .venv/bin/activate -cd src/frontEnd -QT_QPA_PLATFORM=xcb python Application.py -``` - -## Why `QT_QPA_PLATFORM=xcb`? - -Sets Qt to use the XCB (X11) backend. Required for correct display when running over SSH with X11 forwarding. - -## Using the Copilot - -After eSim opens, click the **eSim Copilot** button in the toolbar to open the AI chat panel. - ---- - -# 10. Files Created/Modified – Full Details - -## `.gitattributes` - -**Purpose:** Force all `.sh` files to use LF line endings in Git. - -**Content:** -``` -* text=auto -*.sh text eol=lf -``` - ---- - -## `DEPLOY_UBUNTU.md` - -**Purpose:** Deployment guide for Ubuntu VM and WSL2. - -**Contents:** -- VM setup checklist -- VirtualBox/Hyper-V/VMware instructions -- Shared folder and zip transfer methods -- Setup script steps -- Ollama, ingest, launch commands -- Troubleshooting table -- WSL2 option - ---- - -## `SESSION_SUMMARY.md` - -**Purpose:** This document – session summary and deployment guide. - ---- - -## `requirements.txt` - -**Changes:** -- `setuptools==65.5.0` → `setuptools>=57.5.0,<58` (for hdlparse compatibility) - ---- - -## `requirements-copilot.txt` - -**Purpose:** AI-related dependencies (Ollama client, ChromaDB, PaddleOCR, Vosk, etc.). Separate from base eSim requirements. - ---- - -## `scripts/setup_copilot_ubuntu.sh` - -**Purpose:** One-command setup for Ubuntu. - -**Changes:** -- hdlparse workaround (setuptools 57.5.0, `--no-build-isolation`, constraint file) -- Full 7-step flow (system packages, venv, deps, PaddlePaddle, Ollama, models, Vosk) - ---- - -## `scripts/launch_esim.sh` - -**Purpose:** Single script to launch eSim. - -**Contents:** -- Creates `~/.esim` if missing -- Activates venv -- Runs `QT_QPA_PLATFORM=xcb python Application.py` from `src/frontEnd` - -**Usage:** `./scripts/launch_esim.sh` - ---- - -## `scripts/zip_for_vm.ps1` - -**Purpose:** PowerShell script to zip `eSim` for transfer to VM. - -**Usage:** -```powershell -.\repos\eSim\scripts\zip_for_vm.ps1 -``` - -**Output:** `eSim-for-VM.zip` in workspace root. - ---- - -## `src/frontEnd/Workspace.py` - -**Purpose:** Create workspace directory before writing. - -**Change:** Added `os.makedirs(esim_dir, exist_ok=True)` before opening `workspace.txt` for writing. - ---- - -## `src/chatbot/chatbot_core.py` - -**Change:** Removed unused `sklearn` import. - ---- - -## `src/chatbot/stt_handler.py` - -**Change:** Made STT optional; graceful fallback if Vosk/sounddevice missing. - ---- - -## `src/chatbot/knowledge_base.py` - -**Change:** ChromaDB path set to `~/.local/share/esim-copilot/chroma` (user-writable). +*Note: Uses `QT_QPA_PLATFORM=xcb` for compatibility over SSH X11 forwarding.* --- -# 11. Known Issues - -| Issue | Impact | Workaround | -|-------|--------|------------| -| `No module named 'paddle'` (PaddleOCR) | Vision features may not work | Text chat works; install PaddlePaddle if needed | -| Missing `manuals/esim_netlist_analysis_output_contract.txt` | Netlist contract not loaded | Optional; add file if needed | -| `DeprecationWarning: sipPyTypeDict()` | PyQt5/sip deprecation | Safe to ignore | -| `Cannot access Modelica map file` | Modelica config missing | Optional; config.ini in ~/.esim | - ---- - -# 12. Troubleshooting - -| Problem | Solution | -|---------|----------| -| `bash\r: No such file or directory` | Run `sed -i 's/\r$//' scripts/setup_copilot_ubuntu.sh` | -| `use_2to3 is invalid` (hdlparse) | Ensure setup script has hdlparse workaround; or manually: `pip install setuptools==57.5.0` then `pip install hdlparse==1.0.4 --no-build-isolation` | -| `FileNotFoundError: ~/.esim/workspace.txt` | Run `mkdir -p ~/.esim` or use updated Workspace.py | -| `ollama: command not found` | Run `curl -fsSL https://ollama.com/install.sh \| sh` | -| Ollama not responding | Run `ollama serve` or ensure systemd service: `sudo systemctl start ollama` | -| GUI doesn't appear (MobaXterm) | Enable X11 forwarding; check `echo $DISPLAY` | -| No Bridged Adapter | Select host NIC in dropdown; or use NAT + port forwarding | -| Host Interface Networking driver | Reinstall VirtualBox; or use NAT | - ---- - -# 13. Push to GitHub - -## Fork FOSSEE/eSim - -1. Go to https://github.com/FOSSEE/eSim -2. Click **Fork** -3. Fork to your account (e.g. harvi.bhavinpatel2024@gmail.com) - -## Add remote and push - -```powershell -cd C:\Users\91900\Downloads\eSIM-Software-AIChatBot\repos\eSim -git remote add myfork https://github.com/YOUR_GITHUB_USERNAME/eSim.git -git push myfork Chatbot_Enhancements -``` - -Replace `YOUR_GITHUB_USERNAME` with your GitHub username. +# 11. Known Issues & Troubleshooting +| Issue | Fix | +|---|---| +| `bash\r` | `sed -i 's/\r$//' scripts/setup_copilot_ubuntu.sh` | +| `hdlparse` error | Ensure `setuptools<58` is used. | +| No GUI | Enable X11 forwarding in MobaXterm. | --- -# 14. Quick Reference - -| Item | Value | -|------|-------| -| VM IP | 192.168.29.208 | -| SSH user | harvi | -| Repo path | ~/work/eSim | -| Branch | Chatbot_Enhancements | -| Launch | `./scripts/launch_esim.sh` | -| Ollama API | http://127.0.0.1:11434 | -| SCP copy | `scp -r repos\eSim harvi@192.168.29.208:~/work/` | +# 12. Push to GitHub +1. Fork `FOSSEE/eSim` on GitHub. +2. `git remote add myfork https://github.com/YOUR_USER/eSim.git` +3. `git push myfork Chatbot_Enhancements` From 1ead1a25f1b8e1f9fc389ebc532b0d825baf543f Mon Sep 17 00:00:00 2001 From: eSim Copilot Dev Date: Tue, 3 Mar 2026 16:46:47 +0530 Subject: [PATCH 32/34] Copilot enhancements: RAG threshold, netlist contract, PaddleOCR msg, copy button, tests, deploy script Made-with: Cursor --- .gitignore | 3 + scripts/README_TESTS.md | 97 ++++++++++++++++++++++++++++ scripts/deploy_to_vm.ps1.example | 49 ++++++++++++++ scripts/launch_esim.sh | 6 ++ scripts/test_copilot_enhancements.py | 90 ++++++++++++++++++++++++++ scripts/test_copilot_enhancements.sh | 64 ++++++++++++++++++ src/chatbot/image_handler.py | 1 + src/chatbot/knowledge_base.py | 35 +++++++--- src/frontEnd/Chatbot.py | 65 ++++++++++++++++--- 9 files changed, 393 insertions(+), 17 deletions(-) create mode 100644 scripts/README_TESTS.md create mode 100644 scripts/deploy_to_vm.ps1.example create mode 100644 scripts/test_copilot_enhancements.py create mode 100644 scripts/test_copilot_enhancements.sh diff --git a/.gitignore b/.gitignore index f321b0e7a..78c7f16d0 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,6 @@ nghdl* tags build/ dist/ + +# Local deploy script (user-specific VM config) +scripts/deploy_to_vm.ps1 diff --git a/scripts/README_TESTS.md b/scripts/README_TESTS.md new file mode 100644 index 000000000..b0d87d0d8 --- /dev/null +++ b/scripts/README_TESTS.md @@ -0,0 +1,97 @@ +# Copilot Enhancement Tests + +Run these tests on the Ubuntu VM after activating the venv. + +## Copy Updated Code to VM + +From your **local machine** (Windows), sync the updated code to the Ubuntu VM: + +### Automated: deploy_to_vm.ps1 (recommended) + +```powershell +# First time: copy template (if needed) and edit VM_HOST, VM_USER +copy scripts\deploy_to_vm.ps1.example scripts\deploy_to_vm.ps1 # if deploy_to_vm.ps1 doesn't exist + +# Run from eSim repo root: +.\scripts\deploy_to_vm.ps1 +``` + +This script syncs `src` and `scripts` via SCP, stops any running eSim on the VM, and prints the final step to run in MobaXterm. (`deploy_to_vm.ps1` is in `.gitignore`.) + +### Manual options + +### Option A: rsync (recommended) + +```powershell +# From eSim repo root on local machine +cd +rsync -avz --exclude ".venv" --exclude "__pycache__" --exclude ".git" . harvi@192.168.29.208:~/work/eSim/ +``` + +### Option B: scp (specific files/folders) + +```powershell +# Copy entire src and scripts +scp -r src scripts harvi@192.168.29.208:~/work/eSim/ + +# Or copy only changed files +scp src/chatbot/knowledge_base.py src/chatbot/image_handler.py src/frontEnd/Chatbot.py harvi@192.168.29.208:~/work/eSim/src/ +scp -r scripts harvi@192.168.29.208:~/work/eSim/ +``` + +### Option C: Git (if both sides use the same repo) + +```bash +# On local: commit and push +git add -A && git commit -m "Copilot enhancements" && git push + +# On VM: pull +ssh harvi@192.168.29.208 "cd ~/work/eSim && git pull" +``` + +**Note:** Replace `192.168.29.208` and `harvi` if your VM uses different IP/user. On Windows, `scp` is available with OpenSSH; for `rsync`, use WSL or install via Git for Windows. + +--- + +## Prerequisites + +- Ubuntu VM (e.g. 192.168.29.208, user `harvi`) +- Repo at `~/work/eSim` +- Virtual environment with dependencies: `source .venv/bin/activate` +- Optional: `ollama serve` running for Ollama connectivity test +- Optional: RAG ingest run (`cd src && python ingest.py`) for RAG test + +## Run Tests + +### Option 1: Python script (recommended) + +```bash +cd ~/work/eSim +source .venv/bin/activate +python scripts/test_copilot_enhancements.py +``` + +### Option 2: Shell script + +```bash +cd ~/work/eSim +chmod +x scripts/test_copilot_enhancements.sh +./scripts/test_copilot_enhancements.sh +``` + +## What Is Tested + +| Test | Description | +|------|-------------| +| Netlist contract | Contract loads from one of the bundled paths | +| RAG relevance | `search_knowledge()` filters by relevance threshold | +| PaddleOCR | `image_handler` imports; `HAS_PADDLE` set | +| Copy button | `ChatbotGUI.copy_last_response` exists | +| Ollama | Optional: Ollama responds to a short prompt | + +## Expected Output + +- `[PASS]` – test succeeded +- `[SKIP]` – test skipped (e.g. RAG empty if ingest not run) +- `[WARN]` – non-blocking (e.g. Ollama not running) +- `[FAIL]` – test failed (investigate) diff --git a/scripts/deploy_to_vm.ps1.example b/scripts/deploy_to_vm.ps1.example new file mode 100644 index 000000000..3c0ff1bcb --- /dev/null +++ b/scripts/deploy_to_vm.ps1.example @@ -0,0 +1,49 @@ +# Deploy eSim to VM and restart - COPY THIS FILE TO deploy_to_vm.ps1 and edit config below. +# The actual deploy_to_vm.ps1 is in .gitignore (user-specific). +# +# Usage: .\scripts\deploy_to_vm.ps1 +# Prereq: OpenSSH client (scp, ssh) - built into Windows 10+ + +# ============ CONFIG (edit these) ============ +$VM_HOST = "192.168.29.208" +$VM_USER = "harvi" +$VM_REPO = "~/work/eSim" +# ============================================ + +$ErrorActionPreference = "Stop" +$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$RepoRoot = Split-Path -Parent $ScriptDir + +Write-Host "=== eSim Deploy to VM ===" -ForegroundColor Cyan +Write-Host "" + +# 1. Sync code +Write-Host "[1] Syncing code to ${VM_USER}@${VM_HOST}:${VM_REPO}..." -ForegroundColor Yellow +$dest = "${VM_USER}@${VM_HOST}:${VM_REPO}/" +Push-Location $RepoRoot +try { + # Use scp - rsync may not be on Windows + scp -r -o ConnectTimeout=10 src scripts $dest + if ($LASTEXITCODE -ne 0) { throw "scp failed" } +} finally { + Pop-Location +} +Write-Host " Done." -ForegroundColor Green + +# 2. Kill old eSim process +Write-Host "[2] Stopping old eSim process on VM..." -ForegroundColor Yellow +$remoteCmd = "cd $VM_REPO && pkill -f 'python.*Application.py' 2>/dev/null; pkill -f 'python.*esim' 2>/dev/null; echo 'Done.'" +ssh -o ConnectTimeout=10 "${VM_USER}@${VM_HOST}" $remoteCmd +Write-Host " Done." -ForegroundColor Green + +# 3. MobaXterm instructions +Write-Host "" +Write-Host "=== LAST STEP (do this in MobaXterm) ===" -ForegroundColor Cyan +Write-Host "" +Write-Host " 1. Open MobaXterm and start an SSH session to: ${VM_USER}@${VM_HOST}" -ForegroundColor White +Write-Host " 2. Run:" -ForegroundColor White +Write-Host "" +Write-Host " cd ~/work/eSim && ./scripts/launch_esim.sh" -ForegroundColor Yellow +Write-Host "" +Write-Host " (MobaXterm provides X11, so the eSim GUI will display.)" -ForegroundColor Gray +Write-Host "" diff --git a/scripts/launch_esim.sh b/scripts/launch_esim.sh index cb0c66e57..e4105d4da 100644 --- a/scripts/launch_esim.sh +++ b/scripts/launch_esim.sh @@ -8,6 +8,12 @@ cd "$ROOT" # Ensure .esim dir exists (avoids workspace error) mkdir -p ~/.esim +# Vosk STT model (if installed) +VOSK_DEFAULT="$HOME/.local/share/esim-copilot/vosk-model-small-en-us-0.15" +if [ -z "$VOSK_MODEL_PATH" ] && [ -d "$VOSK_DEFAULT" ]; then + export VOSK_MODEL_PATH="$VOSK_DEFAULT" +fi + # Activate venv and launch source .venv/bin/activate cd src/frontEnd diff --git a/scripts/test_copilot_enhancements.py b/scripts/test_copilot_enhancements.py new file mode 100644 index 000000000..20c6f9192 --- /dev/null +++ b/scripts/test_copilot_enhancements.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 +""" +Test script for Copilot enhancements - run on Ubuntu VM. +Usage: cd ~/work/eSim && source .venv/bin/activate && python scripts/test_copilot_enhancements.py +""" +import os +import sys + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +SRC = os.path.join(ROOT, "src") +if SRC not in sys.path: + sys.path.insert(0, SRC) + +os.chdir(SRC) + + +def test_netlist_contract(): + """Test that netlist contract loads from one of the bundled paths.""" + from frontEnd.Chatbot import NETLIST_CONTRACT + assert NETLIST_CONTRACT, "NETLIST_CONTRACT should not be empty" + assert "FACT" in NETLIST_CONTRACT or "SPICE" in NETLIST_CONTRACT + print("[PASS] Netlist contract loaded") + return True + + +def test_rag_relevance_threshold(): + """Test RAG search_knowledge with relevance threshold.""" + from chatbot.knowledge_base import search_knowledge, RELEVANCE_THRESHOLD + print(f" RELEVANCE_THRESHOLD = {RELEVANCE_THRESHOLD}") + result = search_knowledge("how to add ground", n_results=3) + # May be empty if ingest not run + if result: + assert "=== ESIM OFFICIAL DOCUMENTATION ===" in result + print("[PASS] RAG search returned filtered context") + else: + print("[SKIP] RAG empty (run: cd src && python ingest.py)") + return True + + +def test_paddleocr_message(): + """Test that image_handler imports and HAS_PADDLE is set.""" + from chatbot import image_handler + # Just verify it doesn't crash; message is printed at import + assert hasattr(image_handler, "HAS_PADDLE") + print(f"[PASS] image_handler.HAS_PADDLE = {image_handler.HAS_PADDLE}") + return True + + +def test_chatbot_copy_button(): + """Test that ChatbotGUI has copy_btn and copy_last_response.""" + from frontEnd.Chatbot import ChatbotGUI + assert hasattr(ChatbotGUI, "copy_last_response") + # Create instance would need QApplication - skip for headless + print("[PASS] ChatbotGUI has copy_last_response method") + return True + + +def test_ollama_connectivity(): + """Test Ollama is reachable (optional).""" + try: + from chatbot.ollama_runner import run_ollama + r = run_ollama("Reply with exactly: OK") + if r and "ok" in r.lower(): + print("[PASS] Ollama responded") + else: + print("[WARN] Ollama returned unexpected:", r[:50] if r else "empty") + except Exception as e: + print(f"[WARN] Ollama test failed: {e}") + return True + + +def main(): + print("=== eSim Copilot Enhancement Tests ===\n") + tests = [ + test_netlist_contract, + test_rag_relevance_threshold, + test_paddleocr_message, + test_chatbot_copy_button, + test_ollama_connectivity, + ] + for t in tests: + try: + t() + except Exception as e: + print(f"[FAIL] {t.__name__}: {e}") + print("\n=== Done ===") + + +if __name__ == "__main__": + main() diff --git a/scripts/test_copilot_enhancements.sh b/scripts/test_copilot_enhancements.sh new file mode 100644 index 000000000..3ab7f4569 --- /dev/null +++ b/scripts/test_copilot_enhancements.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +# Test script for Copilot enhancements - run on Ubuntu VM +# Usage: ./scripts/test_copilot_enhancements.sh +# Prereq: source .venv/bin/activate, ollama serve running + +set -e +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT" + +echo "=== eSim Copilot Enhancement Tests ===" +echo "" + +# Ensure venv +if [ -z "$VIRTUAL_ENV" ]; then + echo "[1] Activating venv..." + source .venv/bin/activate +fi + +# Add src to path +export PYTHONPATH="$ROOT/src:$PYTHONPATH" +cd src + +echo "[2] Test: Netlist contract loading" +python3 -c " +from frontEnd.Chatbot import NETLIST_CONTRACT +assert NETLIST_CONTRACT, 'NETLIST_CONTRACT should not be empty' +print(' OK: Contract loaded, length:', len(NETLIST_CONTRACT)) +" + +echo "[3] Test: RAG relevance threshold" +python3 -c " +from chatbot.knowledge_base import search_knowledge, RELEVANCE_THRESHOLD +print(' RELEVANCE_THRESHOLD:', RELEVANCE_THRESHOLD) +result = search_knowledge('how to add ground in eSim', n_results=2) +if result: + print(' OK: RAG returned context, length:', len(result)) +else: + print(' SKIP: RAG empty (run ingest.py first if needed)') +" + +echo "[4] Test: PaddleOCR / image_handler import" +python3 -c " +from chatbot.image_handler import HAS_PADDLE, analyze_and_extract +if HAS_PADDLE: + print(' OK: PaddleOCR available') +else: + print(' OK: PaddleOCR unavailable (expected message shown at import)') +" + +echo "[5] Test: Ollama connectivity (optional)" +python3 -c " +try: + from chatbot.ollama_runner import run_ollama + r = run_ollama('Say OK in one word.') + if r and len(r) > 0: + print(' OK: Ollama responded') + else: + print(' WARN: Ollama returned empty (is ollama serve running?)') +except Exception as e: + print(' WARN: Ollama test failed:', e) +" + +echo "" +echo "=== All tests completed ===" diff --git a/src/chatbot/image_handler.py b/src/chatbot/image_handler.py index 3938ec307..cd8744791 100644 --- a/src/chatbot/image_handler.py +++ b/src/chatbot/image_handler.py @@ -28,6 +28,7 @@ except Exception as e: HAS_PADDLE = False print(f"[INIT] PaddleOCR init failed: {e}") + print("[INIT] Vision analysis unavailable. Text and netlist analysis still work.") def encode_image(image_path: str) -> str: diff --git a/src/chatbot/knowledge_base.py b/src/chatbot/knowledge_base.py index 398087ce8..14ea4cc17 100644 --- a/src/chatbot/knowledge_base.py +++ b/src/chatbot/knowledge_base.py @@ -89,31 +89,50 @@ def ingest_pdfs(manuals_directory: str) -> None: # ==================== SEARCH ==================== +# Relevance threshold: ChromaDB returns distances (L2 or cosine). +# Lower distance = more similar. Filter out chunks with distance > threshold. +RELEVANCE_THRESHOLD = float(os.environ.get("ESIM_RAG_RELEVANCE_THRESHOLD", "1.0")) + + def search_knowledge(query: str, n_results: int = 4) -> str: """ - Simple semantic search against the single master knowledge file. + Semantic search with relevance threshold to reduce hallucination. + Filters out chunks with distance > RELEVANCE_THRESHOLD. """ try: - # Generate embedding for the user's question query_embed = get_embedding(query) if not query_embed: return "" - # Query the database results = collection.query( query_embeddings=[query_embed], n_results=n_results, + include=["documents", "distances"], ) - docs_list = results.get("documents", []) - + docs_list = results.get("documents", [[]]) + distances_list = results.get("distances", [[]]) + if not docs_list or not docs_list[0]: - print("DEBUG: No relevant info found.") return "" - selected_chunks = docs_list[0] - context_text = "\n\n...\n\n".join(selected_chunks) + docs = docs_list[0] + distances = distances_list[0] if distances_list else [] + + # Filter by relevance threshold (lower distance = more similar) + if distances and len(distances) == len(docs): + filtered = [ + (doc, d) for doc, d in zip(docs, distances) + if d <= RELEVANCE_THRESHOLD + ] + if filtered: + selected_chunks = [doc for doc, _ in filtered] + else: + return "" + else: + selected_chunks = docs + context_text = "\n\n...\n\n".join(selected_chunks) if len(context_text) > 3500: context_text = context_text[:3500] diff --git a/src/frontEnd/Chatbot.py b/src/frontEnd/Chatbot.py index 6091fbc8c..98c58a4d7 100644 --- a/src/frontEnd/Chatbot.py +++ b/src/frontEnd/Chatbot.py @@ -10,16 +10,23 @@ ) from PyQt5.QtCore import Qt, QThread, pyqtSignal from PyQt5.QtGui import QFont -MANUALS_DIR = os.path.join(os.path.dirname(__file__), "manuals") +# Try multiple paths for netlist contract (frontEnd/manual, frontEnd/manuals, src/manuals) +_CONTRACT_PATHS = [ + os.path.join(os.path.dirname(__file__), "manual", "esim_netlist_analysis_output_contract.txt"), + os.path.join(os.path.dirname(__file__), "manuals", "esim_netlist_analysis_output_contract.txt"), + os.path.join(os.path.dirname(os.path.dirname(__file__)), "manuals", "esim_netlist_analysis_output_contract.txt"), +] NETLIST_CONTRACT = "" - -try: - contract_path = os.path.join(MANUALS_DIR, "esim_netlist_analysis_output_contract.txt") - with open(contract_path, "r", encoding="utf-8") as f: - NETLIST_CONTRACT = f.read() - print(f"[COPILOT] Loaded netlist contract from {contract_path}") -except Exception as e: - print(f"[COPILOT] WARNING: Could not load netlist contract: {e}") +for contract_path in _CONTRACT_PATHS: + try: + with open(contract_path, "r", encoding="utf-8") as f: + NETLIST_CONTRACT = f.read() + print(f"[COPILOT] Loaded netlist contract from {contract_path}") + break + except Exception: + continue +if not NETLIST_CONTRACT: + print("[COPILOT] Using fallback netlist contract (file not found in any path)") NETLIST_CONTRACT = ( "You are a SPICE netlist analyzer.\n" "Use the FACT lines to detect issues.\n" @@ -519,6 +526,7 @@ def __init__(self, parent=None): # Project context self._project_dir = None self._generation_id = 0 # used to ignore stale responses + self._last_assistant_response = "" # for Copy button self.initUI() @@ -1046,6 +1054,26 @@ def initUI(self): self.analyze_netlist_btn.clicked.connect(self.analyze_current_netlist) header_layout.addWidget(self.analyze_netlist_btn) + # Copy button (copy last assistant response to clipboard) + self.copy_btn = QPushButton("📋") + self.copy_btn.setFixedSize(30, 30) + self.copy_btn.setToolTip("Copy last response to clipboard") + self.copy_btn.setCursor(Qt.PointingHandCursor) + self.copy_btn.setStyleSheet(""" + QPushButton { + background-color: transparent; + border: 1px solid #ddd; + border-radius: 15px; + font-size: 14px; + } + QPushButton:hover { + background-color: #e3f2fd; + border-color: #2196f3; + } + """) + self.copy_btn.clicked.connect(self.copy_last_response) + header_layout.addWidget(self.copy_btn) + # Clear button self.clear_btn = QPushButton("🗑️") self.clear_btn.setFixedSize(30, 30) @@ -1430,10 +1458,29 @@ def format_text_to_html(text): text = text.replace('\n', '
') return text + def copy_last_response(self): + """Copy last assistant response to clipboard for easy paste into netlist.""" + if self._last_assistant_response: + cb = QApplication.clipboard() + cb.setText(self._last_assistant_response) + QMessageBox.information( + self, "Copied", + "Last response copied to clipboard. Paste into Spice Editor (Ctrl+V).", + QMessageBox.Ok, + ) + else: + QMessageBox.information( + self, "Nothing to copy", + "No assistant response yet. Run a netlist analysis or ask a question first.", + QMessageBox.Ok, + ) + def append_message(self, sender, text, is_user): """Append message INSTANTLY (Text Only, No Image Rendering).""" if not text: return + if not is_user: + self._last_assistant_response = text # 1. Define Headers if is_user: From 4b9438b43839286a170dbb097f5b3b31c3af97eb Mon Sep 17 00:00:00 2001 From: Harvi-2215 Date: Fri, 6 Mar 2026 22:43:26 +0530 Subject: [PATCH 33/34] Add PR guide with fork/push/PR instructions for esim-RAGbot-enhacements branch Co-Authored-By: Claude Sonnet 4.6 --- PULL_REQUEST_GUIDE.md | 79 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 PULL_REQUEST_GUIDE.md diff --git a/PULL_REQUEST_GUIDE.md b/PULL_REQUEST_GUIDE.md new file mode 100644 index 000000000..78a5f5f85 --- /dev/null +++ b/PULL_REQUEST_GUIDE.md @@ -0,0 +1,79 @@ +# Pull Request Guide – eSim RAGbot Enhancements + +## 1. Git Configuration (Done ✓) + +- **Email:** `harvi.bhavinpatel2024@vitstudent.ac.in` +- **Name:** `Harvi-2215` + +## 2. Create Your Fork on GitHub + +If you don't have a fork yet: + +1. Go to **[https://github.com/FOSSEE/eSim](https://github.com/FOSSEE/eSim)** +2. Click **Fork** (top right) +3. Choose your account (ensure you're logged in as the account for `harvi.bhavinpatel2024@vitstudent.ac.in`) +4. The fork will be created at `https://github.com/YOUR_USERNAME/eSim` + +**Note:** Your GitHub username may be `Harvi-2215` or `HraviPatel` – use whichever matches your account. + +## 3. Add Your Fork as Remote (if needed) + +```powershell +cd C:\Users\91900\Downloads\eSIM-Software-AIChatBot\repos\eSim + +# If using Harvi-2215: +git remote add harvi-fork https://github.com/Harvi-2215/eSim.git + +# OR if using HraviPatel (already exists as hravipatel): +# git remote add hravipatel https://github.com/HraviPatel/eSim.git +``` + +## 4. Push Your Branch to the Fork + +```powershell +cd C:\Users\91900\Downloads\eSIM-Software-AIChatBot\repos\eSim + +# Push to Harvi-2215 fork: +git push harvi-fork esim-RAGbot-enhacements:esim-RAGbot-Enhacements-Harvi + +# OR push to HraviPatel fork: +git push hravipatel esim-RAGbot-enhacements:esim-RAGbot-Enhacements-Harvi +``` + +When prompted, sign in with `harvi.bhavinpatel2024@vitstudent.ac.in` (or your GitHub credentials). + +## 5. Create the Pull Request + +1. Open your fork: `https://github.com/YOUR_USERNAME/eSim` +2. You should see a banner: **"esim-RAGbot-Enhacements-Harvi had recent pushes"** with a **Compare & pull request** button +3. Click **Compare & pull request** +4. Set: + - **Base repository:** `FOSSEE/eSim` + - **Base branch:** `master` (or `main` if that's the default) + - **Head repository:** `YOUR_USERNAME/eSim` + - **Compare branch:** `esim-RAGbot-Enhacements-Harvi` +5. Add a title, e.g. **"Copilot enhancements: RAG threshold, netlist contract, PaddleOCR, copy button"** +6. Add a description of the changes +7. Click **Create pull request** + +## 6. Changes Included in This Branch + +| File | Change | +|------|--------| +| `.gitignore` | Added `scripts/deploy_to_vm.ps1` | +| `scripts/launch_esim.sh` | Vosk model path auto-detection | +| `src/chatbot/image_handler.py` | PaddleOCR error message | +| `src/chatbot/knowledge_base.py` | RAG relevance threshold | +| `src/frontEnd/Chatbot.py` | Netlist contract bundling, copy-to-clipboard | +| `scripts/README_TESTS.md` | Test instructions and deploy steps | +| `scripts/deploy_to_vm.ps1.example` | Deploy script template | +| `scripts/test_copilot_enhancements.py` | Enhancement tests | +| `scripts/test_copilot_enhancements.sh` | Shell wrapper for tests | + +## Troubleshooting + +| Issue | Fix | +|-------|-----| +| Push asks for credentials | Use GitHub CLI (`gh auth login`) or Git Credential Manager | +| "Repository not found" | Create the fork on GitHub first (Step 2) | +| Wrong GitHub account | Check `git config user.email` and ensure it matches your GitHub email | From 6c0bfefdac68c6c4c076b492fd0123f296511c37 Mon Sep 17 00:00:00 2001 From: Harvi-2215 Date: Fri, 6 Mar 2026 22:54:51 +0530 Subject: [PATCH 34/34] Add 5 enhancements: one-click fix, batch analysis, model settings, real-time hints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Priority 3 – One-click netlist fix (Chatbot.py) - After every netlist analysis, detect fixable issues from stored FACT dict - Show/hide "Apply Fixes" button automatically - _apply_netlist_fixes(): writes .options gmin, .model stubs, bleed resistors before .end; creates .bak backup before modifying Priority 4 & 7 – Batch netlist/image analysis (Chatbot.py) - New BatchWorker QThread: runs static FACT detectors on N netlists or analyze_and_extract() on N images without blocking the UI - "Batch" button opens picker dialog; streams per-file progress into chat; emits summary on completion Priority 5 – Model selection in settings (ollama_runner.py + Chatbot.py) - ollama_runner.py: load_model_settings / save_model_settings / reload_model_settings persist text+vision model choice to ~/.local/share/esim-copilot/settings.json - list_available_models() queries live Ollama list for the dialog - CopilotSettingsDialog: QComboBox for text & vision model; saves + hot-reloads - Settings button (gear icon) in chatbot header opens the dialog Priority 8 – Real-time KiCad hints (Chatbot.py) - Watch toggle button (eye icon) in chatbot header - QTimer fires every 30 s; runs _detect_floating_nodes, _detect_missing_models, _netlist_ground_info on the active project's .cir (no LLM call) - Only posts a hint when the set of detected issues changes (de-duplicated) - Timer stopped cleanly on window close Co-Authored-By: Claude Sonnet 4.6 --- src/chatbot/ollama_runner.py | 126 ++++--- src/frontEnd/Chatbot.py | 632 ++++++++++++++++++++++++++++++++++- 2 files changed, 712 insertions(+), 46 deletions(-) diff --git a/src/chatbot/ollama_runner.py b/src/chatbot/ollama_runner.py index dd84041d4..ae754bd0b 100644 --- a/src/chatbot/ollama_runner.py +++ b/src/chatbot/ollama_runner.py @@ -1,40 +1,90 @@ import os import ollama -import json,time +import json +import time -# Model configuration -VISION_MODELS = {"primary": "minicpm-v:latest"} -TEXT_MODELS = {"default": "qwen2.5:3b"} -EMBED_MODEL = "nomic-embed-text" +# ==================== CLIENT ==================== ollama_client = ollama.Client( host="http://localhost:11434", - timeout=300.0, + timeout=300.0, ) -def run_ollama_vision(prompt: str, image_input: str | bytes) -> str: - """Call minicpm-v:latest with Chain-of-Thought for better accuracy.""" +# ==================== SETTINGS ==================== + +_SETTINGS_DIR = os.path.join( + os.path.expanduser("~"), ".local", "share", "esim-copilot" +) +_SETTINGS_PATH = os.path.join(_SETTINGS_DIR, "settings.json") + +_DEFAULT_TEXT_MODEL = "qwen2.5:3b" +_DEFAULT_VISION_MODEL = "minicpm-v:latest" +EMBED_MODEL = "nomic-embed-text" + + +def load_model_settings() -> dict: + """Load persisted model preferences from disk.""" + try: + with open(_SETTINGS_PATH, "r", encoding="utf-8") as f: + return json.load(f) + except Exception: + return {} + + +def save_model_settings(text_model: str, vision_model: str) -> None: + """Persist model preferences to disk.""" + os.makedirs(_SETTINGS_DIR, exist_ok=True) + try: + with open(_SETTINGS_PATH, "w", encoding="utf-8") as f: + json.dump({"text_model": text_model, "vision_model": vision_model}, f, indent=2) + except Exception as e: + print(f"[SETTINGS] Failed to save: {e}") + + +def list_available_models() -> list: + """Query Ollama for installed models. Returns list of model name strings.""" + try: + resp = ollama_client.list() + names = [m["name"] for m in resp.get("models", [])] + return names if names else [_DEFAULT_TEXT_MODEL, _DEFAULT_VISION_MODEL] + except Exception: + return [_DEFAULT_TEXT_MODEL, _DEFAULT_VISION_MODEL, EMBED_MODEL] + + +# Load settings and initialise model dicts +_settings = load_model_settings() + +VISION_MODELS = {"primary": _settings.get("vision_model", _DEFAULT_VISION_MODEL)} +TEXT_MODELS = {"default": _settings.get("text_model", _DEFAULT_TEXT_MODEL)} + + +def reload_model_settings() -> None: + """Re-read settings from disk and update running dicts (called after save).""" + s = load_model_settings() + VISION_MODELS["primary"] = s.get("vision_model", _DEFAULT_VISION_MODEL) + TEXT_MODELS["default"] = s.get("text_model", _DEFAULT_TEXT_MODEL) + + +# ==================== VISION ==================== + +def run_ollama_vision(prompt: str, image_input) -> str: + """Call vision model with Chain-of-Thought for better accuracy.""" model = VISION_MODELS["primary"] - + try: import base64 - - image_b64 = "" - + image_b64 = "" if isinstance(image_input, bytes): image_b64 = base64.b64encode(image_input).decode("utf-8") - - elif os.path.isfile(image_input): + elif isinstance(image_input, str) and os.path.isfile(image_input): with open(image_input, "rb") as f: image_b64 = base64.b64encode(f.read()).decode("utf-8") - elif isinstance(image_input, str) and len(image_input) > 100: - image_b64 = image_input + image_b64 = image_input else: - raise ValueError("Invalid image input format") + raise ValueError("Invalid image input format") - # === CHAIN OF THOUGHT === system_prompt = ( "You are an expert Electronics Engineer using eSim.\n" "Analyze the schematic image carefully.\n\n" @@ -65,7 +115,7 @@ def run_ollama_vision(prompt: str, image_input: str | bytes) -> str: { "role": "user", "content": prompt, - "images": [image_b64], # <--- MUST BE LIST OF BASE64 STRINGS + "images": [image_b64], }, ], options={ @@ -76,18 +126,17 @@ def run_ollama_vision(prompt: str, image_input: str | bytes) -> str: ) content = resp["message"]["content"] - - # === PARSE JSON FROM MIXED OUTPUT === + import re json_match = re.search(r'```json\s*(\{.*?\})\s*```', content, re.DOTALL) if json_match: return json_match.group(1) - + start = content.find('{') end = content.rfind('}') + 1 if start != -1 and end != -1: return content[start:end] - + return "{}" except Exception as e: @@ -96,44 +145,45 @@ def run_ollama_vision(prompt: str, image_input: str | bytes) -> str: "vision_summary": f"Vision failed: {str(e)[:50]}", "component_counts": {}, "circuit_analysis": {"circuit_type": "Error", "design_errors": [], "design_warnings": []}, - "components": [], "values": {} + "components": [], + "values": {}, }) + +# ==================== TEXT ==================== + def run_ollama(prompt: str, mode: str = "default") -> str: - """ - OPTIMIZED: Run text model with focused parameters. - """ + """Run text model with focused parameters.""" model = TEXT_MODELS.get(mode, TEXT_MODELS["default"]) - + try: resp = ollama_client.chat( model=model, messages=[ { "role": "system", - "content": "You are an eSim and electronics expert. Be concise, accurate, and practical." + "content": "You are an eSim and electronics expert. Be concise, accurate, and practical.", }, {"role": "user", "content": prompt}, ], options={ - "temperature": 0.05, - "num_ctx": 2048, - "num_predict": 400, + "temperature": 0.05, + "num_ctx": 2048, + "num_predict": 400, "top_p": 0.9, - "repeat_penalty": 1.1, + "repeat_penalty": 1.1, }, ) - return resp["message"]["content"].strip() - + except Exception as e: return f"[Error] {str(e)}" +# ==================== EMBEDDINGS ==================== + def get_embedding(text: str): - """ - OPTIMIZED: Get text embeddings for RAG. - """ + """Get text embeddings for RAG.""" try: r = ollama_client.embeddings(model=EMBED_MODEL, prompt=text) return r["embedding"] diff --git a/src/frontEnd/Chatbot.py b/src/frontEnd/Chatbot.py index 98c58a4d7..82abd28ec 100644 --- a/src/frontEnd/Chatbot.py +++ b/src/frontEnd/Chatbot.py @@ -6,9 +6,10 @@ from PyQt5.QtGui import QTextCursor from PyQt5.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QTextEdit, QLineEdit, - QPushButton, QLabel, QFileDialog, QMessageBox, QApplication, QWidget + QPushButton, QLabel, QFileDialog, QMessageBox, QApplication, + QDialog, QComboBox, QFormLayout, QSizePolicy, ) -from PyQt5.QtCore import Qt, QThread, pyqtSignal +from PyQt5.QtCore import Qt, QThread, QTimer, pyqtSignal from PyQt5.QtGui import QFont # Try multiple paths for netlist contract (frontEnd/manual, frontEnd/manuals, src/manuals) _CONTRACT_PATHS = [ @@ -514,6 +515,79 @@ def run(self): except Exception as e: self.error_occurred.emit(f"[Error: {e}]") +# ==================== BATCH WORKER ==================== + +class BatchWorker(QThread): + """Runs static FACT analysis on multiple netlists or vision on images — no LLM.""" + file_started = pyqtSignal(int, int, str) # idx, total, filename + file_done = pyqtSignal(int, int, str, str) # idx, total, filename, summary + all_done = pyqtSignal(list) # list of (filename, summary) + + def __init__(self, mode: str, file_paths: list): + super().__init__() + self.mode = mode # "netlist" or "image" + self.file_paths = file_paths + + def run(self): + results = [] + total = len(self.file_paths) + for i, path in enumerate(self.file_paths): + name = os.path.basename(path) + self.file_started.emit(i + 1, total, name) + if self.mode == "netlist": + summary = self._analyze_netlist(path) + else: + summary = self._analyze_image(path) + results.append((name, summary)) + self.file_done.emit(i + 1, total, name, summary) + self.all_done.emit(results) + + def _analyze_netlist(self, path: str) -> str: + try: + with open(path, "r", encoding="utf-8", errors="ignore") as f: + text = f.read() + floating = _detect_floating_nodes(text) + missing_m = _detect_missing_models(text) + missing_s = _detect_missing_subcircuits(text) + conflicts = _detect_voltage_source_conflicts(text) + has_n0, has_gnd = _netlist_ground_info(text) + issues = [] + if not has_n0 and not has_gnd: + issues.append("No ground ref") + if floating: + issues.append(f"{len(floating)} floating node(s): " + + ", ".join(n for n, _, _ in floating[:3])) + if missing_m: + issues.append(f"{len(missing_m)} missing model(s): " + + ", ".join(m for m, _ in missing_m[:3])) + if missing_s: + issues.append(f"{len(missing_s)} missing subckt(s): " + + ", ".join(s for s, _ in missing_s[:3])) + if conflicts: + issues.append(f"{len(conflicts)} voltage conflict(s)") + return "; ".join(issues) if issues else "OK — no static issues found" + except Exception as e: + return f"Error reading file: {e}" + + def _analyze_image(self, path: str) -> str: + try: + from chatbot.image_handler import analyze_and_extract + result = analyze_and_extract(path) + if result.get("error"): + return f"Vision error: {result['error']}" + ctype = result.get("circuit_analysis", {}).get("circuit_type", "Unknown") + components = result.get("components", []) + errors = result.get("circuit_analysis", {}).get("design_errors", []) + summary = f"Type: {ctype}; Components: {', '.join(components[:5])}" + if errors: + summary += f"; Errors: {'; '.join(errors[:2])}" + return summary + except Exception as e: + return f"Error: {e}" + + +# ==================== MAIN CHATBOT GUI ==================== + class ChatbotGUI(QWidget): def __init__(self, parent=None): super().__init__(parent) @@ -521,6 +595,7 @@ def __init__(self, parent=None): self.current_image_path = None self.worker = None self._mic_worker = None + self._batch_worker = None self._is_listening = False # Project context @@ -528,6 +603,18 @@ def __init__(self, parent=None): self._generation_id = 0 # used to ignore stale responses self._last_assistant_response = "" # for Copy button + # One-click fix state + self._last_netlist_path = None + self._last_facts = {} + self._pending_fix_check = None # set during netlist analysis + + # Real-time hints watcher + self._watch_active = False + self._watch_last_facts = None + self._watch_timer = QTimer(self) + self._watch_timer.setInterval(30_000) # 30 s + self._watch_timer.timeout.connect(self._kicad_watch_tick) + self.initUI() def set_project_context(self, project_dir: str): @@ -747,6 +834,18 @@ def analyze_current_netlist(self): ) + # Store facts for one-click fix + facts_dict = { + "syntax_valid": is_syntax_valid, + "has_node0": has_node0, + "has_gnd_label": has_gnd_label, + "floating_nodes": floating_desc, + "missing_models": missing_desc, + "missing_subckts": subckt_desc, + "voltage_conflicts": voltage_conflict_desc, + } + self._pending_fix_check = (netlist_path, facts_dict) + # Show synthetic user message self.append_message( "You", @@ -914,6 +1013,18 @@ def analyze_specific_netlist(self, netlist_path: str): "- Follow the output format and rules described in the contract above.\n" ) + # Store facts for one-click fix + facts_dict = { + "syntax_valid": is_syntax_valid, + "has_node0": has_node0, + "has_gnd_label": has_gnd_label, + "floating_nodes": floating_desc, + "missing_models": missing_desc, + "missing_subckts": subckt_desc, + "voltage_conflicts": voltage_conflict_desc, + } + self._pending_fix_check = (netlist_path, facts_dict) + # Show synthetic user message self.append_message( "You", @@ -1074,6 +1185,57 @@ def initUI(self): self.copy_btn.clicked.connect(self.copy_last_response) header_layout.addWidget(self.copy_btn) + # Settings button + self.settings_btn = QPushButton("⚙️") + self.settings_btn.setFixedSize(30, 30) + self.settings_btn.setToolTip("Model settings (text & vision model selection)") + self.settings_btn.setCursor(Qt.PointingHandCursor) + self.settings_btn.setStyleSheet(""" + QPushButton { + background-color: transparent; + border: 1px solid #ddd; + border-radius: 15px; + font-size: 14px; + } + QPushButton:hover { background-color: #e8f5e9; border-color: #4caf50; } + """) + self.settings_btn.clicked.connect(self.open_settings) + header_layout.addWidget(self.settings_btn) + + # Batch analysis button + self.batch_btn = QPushButton("📁") + self.batch_btn.setFixedSize(30, 30) + self.batch_btn.setToolTip("Batch analyze multiple netlists or images") + self.batch_btn.setCursor(Qt.PointingHandCursor) + self.batch_btn.setStyleSheet(""" + QPushButton { + background-color: transparent; + border: 1px solid #ddd; + border-radius: 15px; + font-size: 14px; + } + QPushButton:hover { background-color: #fff3e0; border-color: #ff9800; } + """) + self.batch_btn.clicked.connect(self.analyze_batch_files) + header_layout.addWidget(self.batch_btn) + + # Real-time hints watcher button + self.watch_btn = QPushButton("👁") + self.watch_btn.setFixedSize(30, 30) + self.watch_btn.setToolTip("Toggle real-time hints (polls active project every 30 s)") + self.watch_btn.setCursor(Qt.PointingHandCursor) + self.watch_btn.setStyleSheet(""" + QPushButton { + background-color: transparent; + border: 1px solid #ddd; + border-radius: 15px; + font-size: 14px; + } + QPushButton:hover { background-color: #e3f2fd; border-color: #2196f3; } + """) + self.watch_btn.clicked.connect(self.toggle_kicad_watch) + header_layout.addWidget(self.watch_btn) + # Clear button self.clear_btn = QPushButton("🗑️") self.clear_btn.setFixedSize(30, 30) @@ -1124,6 +1286,27 @@ def initUI(self): self.loading_label.hide() self.layout.addWidget(self.loading_label) + # --- ONE-CLICK FIX BUTTON (hidden until issues detected) --- + self._apply_fixes_btn = QPushButton("🔧 Apply Fixes to Netlist") + self._apply_fixes_btn.setFixedHeight(32) + self._apply_fixes_btn.setCursor(Qt.PointingHandCursor) + self._apply_fixes_btn.setToolTip( + "Auto-insert .options, .model stubs, and bleed resistors into the netlist" + ) + self._apply_fixes_btn.setStyleSheet(""" + QPushButton { + background-color: #e74c3c; + color: white; + border-radius: 6px; + font-weight: bold; + font-size: 12px; + } + QPushButton:hover { background-color: #c0392b; } + """) + self._apply_fixes_btn.clicked.connect(self._apply_netlist_fixes) + self._apply_fixes_btn.hide() + self.layout.addWidget(self._apply_fixes_btn) + # --- INPUT AREA CONTAINER --- input_layout = QHBoxLayout() input_layout.setSpacing(8) @@ -1353,6 +1536,11 @@ def send_message(self): if not user_text and not self.current_image_path: return + # Hide fix button when user sends a new message + if hasattr(self, "_apply_fixes_btn"): + self._apply_fixes_btn.hide() + self._pending_fix_check = None + full_query = user_text display_text = user_text @@ -1418,14 +1606,19 @@ def on_worker_finished(self): self.attach_btn.setEnabled(True) if hasattr(self, 'mic_btn'): self.mic_btn.setEnabled(True) - - # NEW: re-enable Netlist and Clear if hasattr(self, "analyze_netlist_btn"): self.analyze_netlist_btn.setEnabled(True) if hasattr(self, "clear_btn"): self.clear_btn.setEnabled(True) self.loading_label.hide() + + # Check whether to show the Apply Fixes button + if self._pending_fix_check: + path, facts = self._pending_fix_check + self._pending_fix_check = None + self._check_show_fixes_btn(path, facts) + self.input_field.setFocus() def _handle_response_with_id(self, response: str, gen_id: int): @@ -1504,19 +1697,354 @@ def append_message(self, sender, text, is_user): self.chat_display.setTextCursor(cursor) self.chat_display.ensureCursorVisible() + # ==================== PRIORITY 3: ONE-CLICK FIX ==================== + + def _check_show_fixes_btn(self, netlist_path: str, facts: dict): + """Show the Apply Fixes button if there are auto-fixable issues.""" + self._last_netlist_path = netlist_path + self._last_facts = facts + has_fixes = ( + (facts.get("missing_models", "NONE") not in ("NONE", "")) or + (facts.get("floating_nodes", "NONE") not in ("NONE", "")) or + (not facts.get("has_node0") and not facts.get("has_gnd_label")) + ) + if has_fixes: + self._apply_fixes_btn.show() + else: + self._apply_fixes_btn.hide() + + def _apply_netlist_fixes(self): + """Auto-insert fixes (options, model stubs, bleed resistors) into netlist.""" + path = self._last_netlist_path + facts = self._last_facts + + if not path or not os.path.exists(path): + QMessageBox.warning(self, "No netlist", + "No recently analyzed netlist found. Run a netlist analysis first.") + return + + try: + with open(path, "r", encoding="utf-8", errors="ignore") as f: + lines = f.readlines() + except Exception as e: + QMessageBox.warning(self, "Read error", f"Cannot read netlist:\n{e}") + return + + insertions = [] + applied = [] + + # 1. Convergence options (safe when no .options present) + has_options = any(".options" in l.lower() for l in lines) + if not has_options: + insertions.append(".options gmin=1e-12 reltol=0.01\n") + applied.append("Added `.options gmin=1e-12 reltol=0.01` (convergence helper)") + + # 2. Missing model stubs + missing_str = facts.get("missing_models", "NONE") + if missing_str and missing_str != "NONE": + for part in missing_str.split(";"): + part = part.strip() + model_name = part.split("(")[0].strip() if "(" in part else part + model_name = model_name.strip() + if model_name and model_name.upper() != "NONE": + stub = _model_stub(model_name) + insertions.append(stub + "\n") + applied.append(f"Added stub: {stub}") + + # 3. Floating-node bleed resistors + floating_str = facts.get("floating_nodes", "NONE") + if floating_str and floating_str != "NONE": + for part in floating_str.split(";"): + node = part.strip().split(" ")[0].split("(")[0].strip() + if node and node != "0" and node.upper() != "NONE": + line = f"Rleak_{node} {node} 0 1G\n" + insertions.append(line) + applied.append(f"Added bleed resistor: {line.strip()}") + + if not insertions: + QMessageBox.information(self, "Nothing to fix", + "No auto-fixable issues detected in the last analysis.") + self._apply_fixes_btn.hide() + return + + # Confirm with user + msg = (f"Apply the following fixes to:\n{os.path.basename(path)}\n\n" + + "\n".join(f" \u2022 {a}" for a in applied) + + "\n\nA backup (.bak) will be created first.") + reply = QMessageBox.question(self, "Apply Fixes?", msg, + QMessageBox.Yes | QMessageBox.No) + if reply != QMessageBox.Yes: + return + + # Backup original + import shutil + try: + shutil.copy2(path, path + ".bak") + except Exception: + pass + + # Insert before .end (or append) + new_lines = [] + inserted = False + for line in lines: + if line.strip().lower() == ".end" and not inserted: + new_lines.append("* [COPILOT AUTO-FIX]\n") + new_lines.extend(insertions) + inserted = True + new_lines.append(line) + if not inserted: + new_lines.append("\n* [COPILOT AUTO-FIX]\n") + new_lines.extend(insertions) + new_lines.append(".end\n") + + try: + with open(path, "w", encoding="utf-8") as f: + f.writelines(new_lines) + except Exception as e: + QMessageBox.warning(self, "Write error", f"Failed to write file:\n{e}") + return + + self._apply_fixes_btn.hide() + self._last_netlist_path = None + self._last_facts = {} + self.append_message( + "eSim", + (f"Applied {len(applied)} fix(es) to {os.path.basename(path)}:\n" + + "\n".join(f" \u2022 {a}" for a in applied) + + "\n\nBackup saved as .bak — run simulation to verify."), + is_user=False, + ) + + # ==================== PRIORITY 4 & 7: BATCH ANALYSIS ==================== + + def analyze_batch_files(self): + """Let user pick multiple netlists or images for batch static analysis.""" + if self.is_bot_busy(): + return + + dlg = QMessageBox(self) + dlg.setWindowTitle("Batch Analysis") + dlg.setText("Select the type of files to batch analyze:") + netlist_btn = dlg.addButton("Netlists (.cir / .cir.out)", QMessageBox.AcceptRole) + image_btn = dlg.addButton("Images (.png / .jpg)", QMessageBox.AcceptRole) + dlg.addButton("Cancel", QMessageBox.RejectRole) + dlg.exec_() + + clicked = dlg.clickedButton() + if clicked == netlist_btn: + files, _ = QFileDialog.getOpenFileNames( + self, "Select Netlist Files", "", + "Netlists (*.cir *.cir.out *.net);;All Files (*)" + ) + if files: + self._run_batch_analysis("netlist", files) + elif clicked == image_btn: + files, _ = QFileDialog.getOpenFileNames( + self, "Select Image Files", "", + "Images (*.png *.jpg *.jpeg *.bmp *.tiff);;All Files (*)" + ) + if files: + QMessageBox.information( + self, "Vision batch", + f"Queuing {len(files)} image(s) for vision analysis.\n" + "This may take several minutes.", + ) + self._run_batch_analysis("image", files) + + def _run_batch_analysis(self, mode: str, file_paths: list): + """Start BatchWorker and stream progress into the chat.""" + total = len(file_paths) + label = "netlist(s)" if mode == "netlist" else "image(s)" + self.append_message( + "eSim", + f"Starting batch analysis of {total} {label}…", + is_user=False, + ) + + self._disable_ui_for_analysis() + self.loading_label.show() + self._apply_fixes_btn.hide() + + self._batch_worker = BatchWorker(mode, file_paths) + self._batch_worker.file_started.connect(self._on_batch_file_started) + self._batch_worker.all_done.connect(lambda results: self._on_batch_done(results, mode)) + self._batch_worker.finished.connect(self._on_batch_worker_finished) + self._batch_worker.start() + + def _disable_ui_for_analysis(self): + for attr in ("input_field", "send_btn", "attach_btn", "mic_btn", + "analyze_netlist_btn", "clear_btn", "batch_btn"): + w = getattr(self, attr, None) + if w: + w.setDisabled(True) + + def _enable_ui_after_analysis(self): + for attr in ("input_field", "send_btn", "attach_btn", "mic_btn", + "analyze_netlist_btn", "clear_btn", "batch_btn"): + w = getattr(self, attr, None) + if w: + w.setEnabled(True) + + def _on_batch_file_started(self, idx: int, total: int, name: str): + self.loading_label.setText(f"⏳ Analyzing {idx}/{total}: {name}…") + + def _on_batch_done(self, results: list, mode: str): + label = "Netlist" if mode == "netlist" else "Image" + lines = [f"**Batch {label} Analysis — {len(results)} file(s)**\n"] + ok_count = sum(1 for _, s in results if s.startswith("OK")) + err_count = len(results) - ok_count + for name, summary in results: + icon = "OK" if summary.startswith("OK") else "ISSUES" + lines.append(f"[{icon}] {name}: {summary}") + lines.append(f"\nSummary: {ok_count} OK, {err_count} with issues.") + self.append_message("eSim", "\n".join(lines), is_user=False) + + def _on_batch_worker_finished(self): + self.loading_label.setText("⏳ eSim Copilot is thinking…") + self.loading_label.hide() + self._enable_ui_after_analysis() + self.input_field.setFocus() + + # ==================== PRIORITY 5: MODEL SETTINGS ==================== + + def open_settings(self): + """Open the model-selection settings dialog.""" + from chatbot.ollama_runner import ( + list_available_models, save_model_settings, reload_model_settings, + TEXT_MODELS, VISION_MODELS, + ) + import chatbot.ollama_runner as runner + + dlg = CopilotSettingsDialog( + current_text = TEXT_MODELS.get("default", "qwen2.5:3b"), + current_vision = VISION_MODELS.get("primary", "minicpm-v:latest"), + parent = self, + ) + if dlg.exec_() == QDialog.Accepted: + text_m, vis_m = dlg.get_selections() + save_model_settings(text_m, vis_m) + runner.TEXT_MODELS["default"] = text_m + runner.VISION_MODELS["primary"] = vis_m + self.append_message( + "eSim", + f"Model settings saved:\n Text/reasoning: {text_m}\n Vision: {vis_m}", + is_user=False, + ) + + # ==================== PRIORITY 8: REAL-TIME KICAD HINTS ==================== + + def toggle_kicad_watch(self): + """Start / stop the real-time hints watcher.""" + if self._watch_active: + self._watch_timer.stop() + self._watch_active = False + self.watch_btn.setStyleSheet(""" + QPushButton { + background-color: transparent; + border: 1px solid #ddd; + border-radius: 15px; + font-size: 14px; + } + QPushButton:hover { background-color: #e3f2fd; border-color: #2196f3; } + """) + self.watch_btn.setToolTip("Toggle real-time hints (polls active project every 30 s)") + self.append_message("eSim", "Real-time hints: OFF", is_user=False) + else: + self._watch_active = True + self._watch_last_facts = None + self.watch_btn.setStyleSheet(""" + QPushButton { + background-color: #2196f3; + color: white; + border-radius: 15px; + font-size: 14px; + } + QPushButton:hover { background-color: #1565c0; } + """) + self.watch_btn.setToolTip("Real-time hints: ON — click to disable") + self.append_message( + "eSim", + "Real-time hints: ON\nPolling active project every 30 s for static issues " + "(no LLM call — instant feedback).", + is_user=False, + ) + self._kicad_watch_tick() # run immediately + self._watch_timer.start() + + def _kicad_watch_tick(self): + """Timer callback: run FACT detectors on active project without calling the LLM.""" + try: + from configuration.Appconfig import Appconfig + proj_dir = Appconfig().current_project.get("ProjectName") + if not proj_dir or not os.path.isdir(proj_dir): + return + + proj_name = os.path.basename(proj_dir) + # Prefer .cir (pre-simulation) over .cir.out + candidates = [ + os.path.join(proj_dir, proj_name + ".cir"), + os.path.join(proj_dir, proj_name + ".cir.out"), + ] + netlist_path = next((p for p in candidates if os.path.exists(p)), None) + if not netlist_path: + return + + with open(netlist_path, "r", encoding="utf-8", errors="ignore") as f: + text = f.read() + + floating = _detect_floating_nodes(text) + missing_m = _detect_missing_models(text) + has_n0, has_gnd = _netlist_ground_info(text) + + new_facts = { + "no_ground": not has_n0 and not has_gnd, + "floating": tuple(n for n, _, _ in floating), + "missing_models": tuple(m for m, _ in missing_m), + } + + if new_facts == self._watch_last_facts: + return # nothing changed + + self._watch_last_facts = new_facts + + hints = [] + fname = os.path.basename(netlist_path) + if new_facts["no_ground"]: + hints.append(" No ground reference (node 0)") + for node in new_facts["floating"]: + hints.append(f" Floating node: {node}") + for model in new_facts["missing_models"]: + hints.append(f" Missing model: {model}") + + if hints: + self.append_message( + "Hints", + f"[{fname}]\n" + "\n".join(hints), + is_user=False, + ) + else: + self.append_message( + "Hints", + f"[{fname}] No static issues detected.", + is_user=False, + ) + except Exception as e: + print(f"[WATCH TICK] {e}") + # ---------- CLEAN SHUTDOWN ---------- def closeEvent(self, event): """Stop analysis when the chatbot window/dock is closed.""" - # Ensure worker is stopped so it doesn't keep using CPU self.stop_analysis() - - # Clear backend context as well + if self._watch_active: + self._watch_timer.stop() + if self._batch_worker and self._batch_worker.isRunning(): + self._batch_worker.quit() + self._batch_worker.wait(300) try: clear_history() except Exception: pass - event.accept() def debug_error(self, error_log_path: str): @@ -1585,6 +2113,94 @@ def debug_error(self, error_log_path: str): self.worker.finished.connect(self.on_worker_finished) self.worker.start() +# ==================== MODULE-LEVEL HELPERS ==================== + +def _model_stub(model_name: str) -> str: + """Return a minimal SPICE .model stub inferred from the model name.""" + n = model_name.upper() + if "PNP" in n: + return f".model {model_name} PNP(Is=1e-14 Bf=200 Vaf=100)" + if "NPN" in n or n.startswith("Q2N") or n.startswith("BC") or n.startswith("2N"): + return f".model {model_name} NPN(Is=1e-14 Bf=200 Vaf=100)" + if n.startswith("1N") or "DIODE" in n or (n.startswith("D") and len(n) <= 8): + return f".model {model_name} D(Is=1e-14 Rs=1)" + if "NMOS" in n or n.startswith("NMOS"): + return f".model {model_name} NMOS(Kp=120u Vto=1.0 Gamma=0)" + if "PMOS" in n or n.startswith("PMOS"): + return f".model {model_name} PMOS(Kp=60u Vto=-1.0 Gamma=0)" + # Default: assume NPN BJT + return f".model {model_name} NPN(Is=1e-14 Bf=200 Vaf=100)" + + +# ==================== SETTINGS DIALOG ==================== + +class CopilotSettingsDialog(QDialog): + """Simple dialog for choosing text and vision models.""" + + def __init__(self, current_text: str, current_vision: str, parent=None): + super().__init__(parent) + self.setWindowTitle("eSim Copilot — Model Settings") + self.setMinimumWidth(400) + self.setModal(True) + + from chatbot.ollama_runner import list_available_models + available = list_available_models() + + # Ensure current selections appear even if Ollama is offline + for m in (current_text, current_vision): + if m not in available: + available.insert(0, m) + + layout = QVBoxLayout(self) + + title = QLabel("Select AI models served by Ollama") + title.setStyleSheet("font-weight: bold; font-size: 13px; margin-bottom: 6px;") + layout.addWidget(title) + + form = QFormLayout() + + self._text_combo = QComboBox() + self._text_combo.addItems(available) + idx = self._text_combo.findText(current_text) + if idx >= 0: + self._text_combo.setCurrentIndex(idx) + form.addRow("Text / Reasoning model:", self._text_combo) + + self._vision_combo = QComboBox() + self._vision_combo.addItems(available) + idx = self._vision_combo.findText(current_vision) + if idx >= 0: + self._vision_combo.setCurrentIndex(idx) + form.addRow("Vision model:", self._vision_combo) + + layout.addLayout(form) + + note = QLabel( + "Changes take effect immediately.\n" + "Models must already be pulled in Ollama\n" + "(e.g. ollama pull qwen2.5:3b)." + ) + note.setStyleSheet("color: #666; font-size: 11px; margin-top: 6px;") + layout.addWidget(note) + + btn_row = QHBoxLayout() + save_btn = QPushButton("Save") + cancel_btn = QPushButton("Cancel") + save_btn.setDefault(True) + save_btn.clicked.connect(self.accept) + cancel_btn.clicked.connect(self.reject) + btn_row.addStretch() + btn_row.addWidget(save_btn) + btn_row.addWidget(cancel_btn) + layout.addLayout(btn_row) + + def get_selections(self): + """Return (text_model, vision_model) chosen by the user.""" + return self._text_combo.currentText(), self._vision_combo.currentText() + + +# ==================== DOCK FACTORY ==================== + from PyQt5.QtWidgets import QDockWidget from PyQt5.QtCore import Qt