From 72e828e5c90a0ffadbe547415015d9bf94aeaefe Mon Sep 17 00:00:00 2001 From: Hariom Thakur Date: Wed, 7 Jan 2026 16:45:18 +0000 Subject: [PATCH 01/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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