From aa54c729a9cf55af6c024c09a0433d3b7ee22e75 Mon Sep 17 00:00:00 2001 From: Myo-Thinzar Date: Tue, 25 Mar 2025 10:11:01 +0530 Subject: [PATCH 1/9] A pretrained Gen AI model Qwen 2.5 Coder with 3B parameters:2Gb is integrated into eSim using Ollama --- images/chatbot.png | Bin 0 -> 2710 bytes src/chatbot/chatbot_thread.py | 41 +++++++++++ src/frontEnd/Application.py | 38 +++++++++- src/frontEnd/Chatbot.py | 95 +++++++++++++++++++++++++ src/frontEnd/DockArea.py | 4 +- src/ngspiceSimulation/NgspiceWidget.py | 17 +++-- 6 files changed, 188 insertions(+), 7 deletions(-) create mode 100644 images/chatbot.png create mode 100644 src/chatbot/chatbot_thread.py create mode 100644 src/frontEnd/Chatbot.py 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 diff --git a/src/chatbot/chatbot_thread.py b/src/chatbot/chatbot_thread.py new file mode 100644 index 000000000..6e2817d37 --- /dev/null +++ b/src/chatbot/chatbot_thread.py @@ -0,0 +1,41 @@ +import subprocess +import os +import json +from PyQt5.QtCore import QThread, pyqtSignal + +os.environ["QT_QPA_PLATFORM"] = "xcb" + +class OllamaWorker(QThread): + """Runs Ollama in a separate thread.""" + response_signal = pyqtSignal(str) # Signal to send response back to the UI + + def __init__(self, user_text): + super().__init__() + self.user_text = user_text + + def run(self): + try: + messages = [ + { + "role": "system", + "content": ("You are a professional electronic engineer advising users or help debugging on " + "EDA tool eSim's KiCad, and NgSPICE simulation. " + "Explain concisely in at MOST 30 words or 5 sentences to minimize wait time.Do not exceed limit. " + "Here is the maintained chat history.") + }, + {"role": "user", "content": self.user_text} + ] + + response = subprocess.run( + ["ollama", "run", "qwen2.5-coder:3b", json.dumps(messages)], + capture_output=True, text=True, check=True + ) + + bot_response = response.stdout.strip() or "No response received." + + except subprocess.CalledProcessError as e: + bot_response = f"Error: Ollama execution failed - {e.stderr.strip()}" + except Exception as e: + bot_response = f"Error: {str(e)}" + + self.response_signal.emit(bot_response) # Send response to main thread diff --git a/src/frontEnd/Application.py b/src/frontEnd/Application.py index 5d76bf9d7..3f1891b49 100644 --- a/src/frontEnd/Application.py +++ b/src/frontEnd/Application.py @@ -40,6 +40,7 @@ from projManagement.Kicad import Kicad from projManagement.Validation import Validation from projManagement import Worker +from frontEnd.Chatbot import ChatbotGUI # Its our main window of application. @@ -48,6 +49,7 @@ 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.""" @@ -57,6 +59,7 @@ def __init__(self, *args): # Set slot for simulation end signal to plot simulation data self.simulationEndSignal.connect(self.plotSimulationData) + self.errorDetectedSignal.connect(self.handleError) # Creating require Object self.obj_workspace = Workspace.Workspace() @@ -64,9 +67,11 @@ def __init__(self, *args): self.obj_kicad = Kicad(self.obj_Mainview.obj_dockarea) self.obj_appconfig = Appconfig() self.obj_validation = Validation() + self.chatbot_window = ChatbotGUI() # Initialize all widget self.setCentralWidget(self.obj_Mainview) self.initToolBar() + self.initchatbot() self.setGeometry(self.obj_appconfig._app_xpos, self.obj_appconfig._app_ypos, @@ -82,6 +87,35 @@ def __init__(self, *args): self.systemTrayIcon.setIcon(QtGui.QIcon(init_path + 'images/logo.png')) self.systemTrayIcon.setVisible(True) + def initchatbot(self): + """ + This function initializes ChatbotIcon. + """ + self.chatboticon = QtWidgets.QPushButton(self, icon=QtGui.QIcon(init_path + 'images/chatbot.png')) + self.chatboticon.setIconSize(QtCore.QSize(40, 40)) + self.chatboticon.setStyleSheet("border-radius: 30px;") + self.chatboticon.clicked.connect(self.openChatbot) + + def openChatbot(self): + if not hasattr(self, 'chatbot_window') or not self.chatbot_window.isVisible(): + self.chatbot_window.setWindowModality(QtCore.Qt.WindowModal) + self.chatbot_window.setWindowFlags(QtCore.Qt.Dialog | QtCore.Qt.WindowStaysOnTopHint) + self.chatbot_window.show() + self.obj_appconfig.print_info('Chat Bot function is called') + + def resizeEvent(self, event): + """ + Adjust debug button position during window resize. + """ + super().resizeEvent(event) + self.chatboticon.move(self.width() - 90, self.height() - 90) + + def handleError(self): + self.projDir = self.obj_appconfig.current_project["ProjectName"] + self.output_file = os.path.join(self.projDir, "ngspice_error.log") + if self.chatbot_window.isVisible(): + self.chatbot_window.debug_error(self.output_file) + def initToolBar(self): """ This function initializes Tool Bars. @@ -415,6 +449,8 @@ def plotSimulationData(self, exitCode, exitStatus): print("Exception Message:", str(e), traceback.format_exc()) self.obj_appconfig.print_error('Exception Message : ' + str(e)) + else: + self.errorDetectedSignal.emit("Simulation failed.") def open_ngspice(self): """This Function execute ngspice on current project.""" @@ -438,7 +474,7 @@ def open_ngspice(self): return self.obj_Mainview.obj_dockarea.ngspiceEditor( - projName, ngspiceNetlist, self.simulationEndSignal) + projName, ngspiceNetlist, self.simulationEndSignal,self.chatbot_window) self.ngspice.setEnabled(False) self.conversion.setEnabled(False) diff --git a/src/frontEnd/Chatbot.py b/src/frontEnd/Chatbot.py new file mode 100644 index 000000000..6dd576907 --- /dev/null +++ b/src/frontEnd/Chatbot.py @@ -0,0 +1,95 @@ +from chatbot.chatbot_thread import OllamaWorker +from PyQt5.QtWidgets import QWidget, QHBoxLayout, QTextEdit, QVBoxLayout, QLineEdit, QPushButton +from PyQt5.QtCore import QSize +from PyQt5.QtGui import QIcon +from PyQt5.QtWidgets import QApplication +import os +if os.name == 'nt': + from frontEnd import pathmagic # noqa:F401 + init_path = '' +else: + import pathmagic # noqa:F401 + init_path = '../../' + +class ChatbotGUI(QWidget): + def __init__(self): + super().__init__() + self.setWindowTitle("AI Chatbot") + self.setFixedSize(400, 250) + + self.chat_history = [] + + layout = QVBoxLayout(self) + self.chat_display = QTextEdit(self, readOnly=True) + layout.addWidget(self.chat_display) + + input_layout = QHBoxLayout() + self.user_input = QLineEdit(self, placeholderText="Type your query here...") + self.user_input.setStyleSheet("font-size: 14px;") + self.user_input.returnPressed.connect(self.ask_ollama) + input_layout.addWidget(self.user_input) + + self.clear_button = QPushButton(self, icon=QIcon(init_path + 'images/clear.png')) + self.clear_button.setIconSize(QSize(18, 18)) + self.clear_button.setStyleSheet("font-size: 14px; padding: 5px;") + self.clear_button.clicked.connect(self.clear_session) + input_layout.addWidget(self.clear_button) + + layout.addLayout(input_layout) + self.move_to_bottom_right() + + def ask_ollama(self): + user_text = self.user_input.text().strip() + if not user_text: + return + + self.chat_history = (self.chat_history + [f"User: {user_text}"])[-4:] + self.chat_display.append(f"You: {user_text}") + + self.worker = OllamaWorker(self.chat_history) + self.worker.response_signal.connect(self.display_response) + self.worker.start() + + self.user_input.clear() + def move_to_bottom_right(self): + """Move the chatbot window to the bottom-right corner of the screen.""" + screen = QApplication.desktop().screenGeometry() + widget = self.geometry() + x = screen.width() - widget.width() - 10 # 10px margin from the right + y = screen.height() - widget.height() - 50 # 50px margin from the bottom + self.move(x, y) + + def display_response(self, bot_response): + """Display the bot's response in the chat display.""" + self.chat_display.append(f"Bot: {bot_response}\n") + self.chat_history.append(f"Bot: {bot_response}\n") + + def clear_session(self): + """Clear the chat display.""" + self.chat_display.clear() + self.chat_history=[] + def debug_ollama(self): + """Send log to Ollama and get response asynchronously.""" + self.chat_display.append(f"============Simulation Failed=============\n") + user_text = self.user_input.text().strip() + self.worker = OllamaWorker(user_text) + self.worker.response_signal.connect(self.display_response) + self.worker.start() + self.user_input.clear() # Clear input field + + def debug_error(self, log): + self.chat_history = [] + if os.path.exists(log): + with open(log, "r") as f: + lines = f.readlines() + + start_index = next((i for i, line in enumerate(lines) if "No compatibility mode selected" in line), len(lines)) + cutoff_index = next((i for i, line in enumerate(lines) if "Total CPU time (seconds)" in line), len(lines)) + + # Keep only the lines before the cutoff index + filtered_lines = [line for line in lines[start_index+1:cutoff_index] if line.strip()] + + combined_text = "".join(filtered_lines) + print(combined_text) + self.user_input.setText(combined_text) + self.debug_ollama() diff --git a/src/frontEnd/DockArea.py b/src/frontEnd/DockArea.py index 01e0d715a..7f7116465 100755 --- a/src/frontEnd/DockArea.py +++ b/src/frontEnd/DockArea.py @@ -127,14 +127,14 @@ def plottingEditor(self): ) count = count + 1 - def ngspiceEditor(self, projName, netlist, simEndSignal): + 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) + NgspiceWidget(netlist, simEndSignal,chatbot) ) # Adding to main Layout diff --git a/src/ngspiceSimulation/NgspiceWidget.py b/src/ngspiceSimulation/NgspiceWidget.py index 94368cddf..16577e9b3 100644 --- a/src/ngspiceSimulation/NgspiceWidget.py +++ b/src/ngspiceSimulation/NgspiceWidget.py @@ -7,7 +7,7 @@ # This Class creates NgSpice Window class NgspiceWidget(QtWidgets.QWidget): - def __init__(self, netlist, simEndSignal): + def __init__(self, netlist, simEndSignal,chatbot): """ - Creates constructor for NgspiceWidget class. - Creates NgspiceWindow and runs the process @@ -27,7 +27,7 @@ def __init__(self, netlist, simEndSignal): self.projDir = self.obj_appconfig.current_project["ProjectName"] self.args = ['-b', '-r', netlist.replace(".cir.out", ".raw"), netlist] print("Argument to ngspice: ", self.args) - + self.chat=chatbot self.process = QtCore.QProcess(self) self.terminalUi = TerminalUi.TerminalUi(self.process, self.args) self.layout = QtWidgets.QVBoxLayout(self) @@ -69,7 +69,7 @@ def readyReadAll(self): stderror = str(self.process.readAllStandardError().data(), encoding='utf-8') - + print(stderror) # Suppressing the Ngspice PrinterOnly error that batch mode throws stderror = '\n'.join([errLine for errLine in stderror.split('\n') if ('PrinterOnly' not in errLine and @@ -135,7 +135,11 @@ def finishSimulation(self, exitCode, exitStatus, {} \ ' self.terminalUi.simulationConsole.append( - successFormat.format("Simulation Completed Successfully!")) + successFormat.format("Simulation Completed Successfully!")) + self.projDir = self.obj_appconfig.current_project["ProjectName"] + self.output_file = os.path.join(self.projDir, "ngspice_error.log") + if self.chat.isVisible(): + self.chat.debug_error(self.output_file) else: failedFormat = ' \ @@ -167,3 +171,8 @@ def finishSimulation(self, exitCode, exitStatus, ) simEndSignal.emit(exitStatus, exitCode) + console_output = self.terminalUi.simulationConsole.toPlainText() + # Save console output to a log file + error_log_path = os.path.join(self.projDir, "ngspice_error.log") + with open(error_log_path, "w", encoding="utf-8") as error_log: + error_log.write(console_output + "\n") From 693c8f1c5eb04652650faf5f6a8d2092cdc35b2d Mon Sep 17 00:00:00 2001 From: Myo-Thinzar Date: Wed, 26 Mar 2025 01:12:58 +0530 Subject: [PATCH 2/9] close chatbot window when eSim is closed --- src/frontEnd/Application.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/frontEnd/Application.py b/src/frontEnd/Application.py index 3f1891b49..dfd22a1b3 100644 --- a/src/frontEnd/Application.py +++ b/src/frontEnd/Application.py @@ -92,7 +92,7 @@ def initchatbot(self): This function initializes ChatbotIcon. """ self.chatboticon = QtWidgets.QPushButton(self, icon=QtGui.QIcon(init_path + 'images/chatbot.png')) - self.chatboticon.setIconSize(QtCore.QSize(40, 40)) + self.chatboticon.setIconSize(QtCore.QSize(30, 30)) self.chatboticon.setStyleSheet("border-radius: 30px;") self.chatboticon.clicked.connect(self.openChatbot) @@ -327,6 +327,8 @@ def closeEvent(self, event): self.project.close() except BaseException: pass + if self.chatbot_window.isVisible(): + self.chatbot_window.close() event.accept() self.systemTrayIcon.showMessage('Exit', 'eSim is Closed.') From b1078332b11f9affd17dfb589ec996937a8ff28f Mon Sep 17 00:00:00 2001 From: Myo-Thinzar Date: Wed, 26 Mar 2025 01:33:38 +0530 Subject: [PATCH 3/9] enable chatbot to response when error occurs --- src/frontEnd/Application.py | 3 +-- src/ngspiceSimulation/NgspiceWidget.py | 8 +++----- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/frontEnd/Application.py b/src/frontEnd/Application.py index dfd22a1b3..040a32911 100644 --- a/src/frontEnd/Application.py +++ b/src/frontEnd/Application.py @@ -451,8 +451,7 @@ def plotSimulationData(self, exitCode, exitStatus): print("Exception Message:", str(e), traceback.format_exc()) self.obj_appconfig.print_error('Exception Message : ' + str(e)) - else: - self.errorDetectedSignal.emit("Simulation failed.") + self.errorDetectedSignal.emit("Simulation failed.") def open_ngspice(self): """This Function execute ngspice on current project.""" diff --git a/src/ngspiceSimulation/NgspiceWidget.py b/src/ngspiceSimulation/NgspiceWidget.py index 16577e9b3..20628a10f 100644 --- a/src/ngspiceSimulation/NgspiceWidget.py +++ b/src/ngspiceSimulation/NgspiceWidget.py @@ -32,7 +32,7 @@ def __init__(self, netlist, simEndSignal,chatbot): self.terminalUi = TerminalUi.TerminalUi(self.process, self.args) self.layout = QtWidgets.QVBoxLayout(self) self.layout.addWidget(self.terminalUi) - + self.output_file = os.path.join(self.projDir, "ngspice_error.log") self.process.setWorkingDirectory(self.projDir) self.process.setProcessChannelMode(QtCore.QProcess.MergedChannels) self.process.readyRead.connect(self.readyReadAll) @@ -136,12 +136,10 @@ def finishSimulation(self, exitCode, exitStatus, ' self.terminalUi.simulationConsole.append( successFormat.format("Simulation Completed Successfully!")) - self.projDir = self.obj_appconfig.current_project["ProjectName"] - self.output_file = os.path.join(self.projDir, "ngspice_error.log") + else: if self.chat.isVisible(): self.chat.debug_error(self.output_file) - - else: + failedFormat = ' \ {} \ ' From 701a4e781d7666436844a5930f81cb96fd80d406 Mon Sep 17 00:00:00 2001 From: Myo-Thinzar Date: Wed, 26 Mar 2025 02:04:16 +0530 Subject: [PATCH 4/9] filter error message from console --- src/frontEnd/Chatbot.py | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src/frontEnd/Chatbot.py b/src/frontEnd/Chatbot.py index 6dd576907..bf6625ba1 100644 --- a/src/frontEnd/Chatbot.py +++ b/src/frontEnd/Chatbot.py @@ -3,6 +3,7 @@ from PyQt5.QtCore import QSize from PyQt5.QtGui import QIcon from PyQt5.QtWidgets import QApplication +from configuration.Appconfig import Appconfig import os if os.name == 'nt': from frontEnd import pathmagic # noqa:F401 @@ -81,15 +82,21 @@ def debug_error(self, log): self.chat_history = [] if os.path.exists(log): with open(log, "r") as f: - lines = f.readlines() + lines = [line for line in f.readlines() if line.strip()] - start_index = next((i for i, line in enumerate(lines) if "No compatibility mode selected" in line), len(lines)) - cutoff_index = next((i for i, line in enumerate(lines) if "Total CPU time (seconds)" in line), len(lines)) - - # Keep only the lines before the cutoff index - filtered_lines = [line for line in lines[start_index+1:cutoff_index] if line.strip()] + no_compat_index = next((i for i, line in enumerate(lines) if "No compatibility mode selected!" in line), None) + circuit_index = next((i for i, line in enumerate(lines) if "Circuit:" in line), None) + total_cpu_index = next((i for i, line in enumerate(lines) if "Total CPU time (seconds)" in line), None) + + before_no_compat = lines[:no_compat_index] if no_compat_index else [] + between_circuit_and_cpu = lines[circuit_index + 1:total_cpu_index] if circuit_index is not None and total_cpu_index is not None else [] + filtered_lines = before_no_compat + between_circuit_and_cpu combined_text = "".join(filtered_lines) - print(combined_text) self.user_input.setText(combined_text) - self.debug_ollama() + self.obj_appconfig = Appconfig() + self.projDir = self.obj_appconfig.current_project["ProjectName"] + output_file = os.path.join(self.projDir, "erroroutput.txt") + with open(output_file, "w") as f: + f.writelines(filtered_lines) + self.debug_ollama() \ No newline at end of file From 1a23324f87c2872d35835a048c1140d6e5481d5f Mon Sep 17 00:00:00 2001 From: Myo-Thinzar Date: Wed, 26 Mar 2025 03:13:35 +0530 Subject: [PATCH 5/9] error log is passed to chatbot only if log is saved in output file --- src/ngspiceSimulation/NgspiceWidget.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/ngspiceSimulation/NgspiceWidget.py b/src/ngspiceSimulation/NgspiceWidget.py index 20628a10f..65b9de9f7 100644 --- a/src/ngspiceSimulation/NgspiceWidget.py +++ b/src/ngspiceSimulation/NgspiceWidget.py @@ -136,10 +136,7 @@ def finishSimulation(self, exitCode, exitStatus, ' self.terminalUi.simulationConsole.append( successFormat.format("Simulation Completed Successfully!")) - else: - if self.chat.isVisible(): - self.chat.debug_error(self.output_file) - + else: failedFormat = ' \ {} \ ' @@ -174,3 +171,5 @@ def finishSimulation(self, exitCode, exitStatus, error_log_path = os.path.join(self.projDir, "ngspice_error.log") with open(error_log_path, "w", encoding="utf-8") as error_log: error_log.write(console_output + "\n") + if "Simulation Failed!" in console_output and self.chat.isVisible(): + self.chat.debug_error(self.output_file) \ No newline at end of file From fafe410c99d13764ae8d172a99987ff6a98360d5 Mon Sep 17 00:00:00 2001 From: Myo-Thinzar Date: Wed, 26 Mar 2025 03:38:04 +0530 Subject: [PATCH 6/9] error log is pass to chatbot just after the log is saved --- src/frontEnd/Application.py | 13 ++----------- src/ngspiceSimulation/NgspiceWidget.py | 12 +++++++----- 2 files changed, 9 insertions(+), 16 deletions(-) diff --git a/src/frontEnd/Application.py b/src/frontEnd/Application.py index 040a32911..cc3db9b0d 100644 --- a/src/frontEnd/Application.py +++ b/src/frontEnd/Application.py @@ -49,7 +49,6 @@ 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.""" @@ -59,7 +58,6 @@ def __init__(self, *args): # Set slot for simulation end signal to plot simulation data self.simulationEndSignal.connect(self.plotSimulationData) - self.errorDetectedSignal.connect(self.handleError) # Creating require Object self.obj_workspace = Workspace.Workspace() @@ -108,14 +106,8 @@ def resizeEvent(self, event): Adjust debug button position during window resize. """ super().resizeEvent(event) - self.chatboticon.move(self.width() - 90, self.height() - 90) - - def handleError(self): - self.projDir = self.obj_appconfig.current_project["ProjectName"] - self.output_file = os.path.join(self.projDir, "ngspice_error.log") - if self.chatbot_window.isVisible(): - self.chatbot_window.debug_error(self.output_file) - + self.chatboticon.move(self.width() - 100, self.height() - 60) + def initToolBar(self): """ This function initializes Tool Bars. @@ -451,7 +443,6 @@ def plotSimulationData(self, exitCode, exitStatus): print("Exception Message:", str(e), traceback.format_exc()) self.obj_appconfig.print_error('Exception Message : ' + str(e)) - self.errorDetectedSignal.emit("Simulation failed.") def open_ngspice(self): """This Function execute ngspice on current project.""" diff --git a/src/ngspiceSimulation/NgspiceWidget.py b/src/ngspiceSimulation/NgspiceWidget.py index 65b9de9f7..6d4e832a2 100644 --- a/src/ngspiceSimulation/NgspiceWidget.py +++ b/src/ngspiceSimulation/NgspiceWidget.py @@ -32,7 +32,7 @@ def __init__(self, netlist, simEndSignal,chatbot): self.terminalUi = TerminalUi.TerminalUi(self.process, self.args) self.layout = QtWidgets.QVBoxLayout(self) self.layout.addWidget(self.terminalUi) - self.output_file = os.path.join(self.projDir, "ngspice_error.log") + self.output_file = os.path.join(self.projDir, "ngspice_error.log") self.process.setWorkingDirectory(self.projDir) self.process.setProcessChannelMode(QtCore.QProcess.MergedChannels) self.process.readyRead.connect(self.readyReadAll) @@ -135,8 +135,8 @@ def finishSimulation(self, exitCode, exitStatus, {} \ ' self.terminalUi.simulationConsole.append( - successFormat.format("Simulation Completed Successfully!")) - else: + successFormat.format("Simulation Completed Successfully!")) + else: failedFormat = ' \ {} \ ' @@ -171,5 +171,7 @@ def finishSimulation(self, exitCode, exitStatus, error_log_path = os.path.join(self.projDir, "ngspice_error.log") with open(error_log_path, "w", encoding="utf-8") as error_log: error_log.write(console_output + "\n") - if "Simulation Failed!" in console_output and self.chat.isVisible(): - self.chat.debug_error(self.output_file) \ No newline at end of file + if self.chat.isVisible(): + if (exitStatus == QtCore.QProcess.NormalExit and exitCode == 0 \ + and errorType == QtCore.QProcess.UnknownError) or "Simulation Failed!" in console_output: + self.chat.debug_error(self.output_file) \ No newline at end of file From 1dc4d0e24dc8699b3a722dd6a2f84695c430092c Mon Sep 17 00:00:00 2001 From: Myo-Thinzar Date: Wed, 26 Mar 2025 04:03:48 +0530 Subject: [PATCH 7/9] automate passing error message to chatbot --- src/frontEnd/Application.py | 13 ++++++++++--- src/ngspiceSimulation/NgspiceWidget.py | 6 ++---- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/frontEnd/Application.py b/src/frontEnd/Application.py index cc3db9b0d..7729f4d3a 100644 --- a/src/frontEnd/Application.py +++ b/src/frontEnd/Application.py @@ -41,7 +41,7 @@ from projManagement.Validation import Validation from projManagement import Worker from frontEnd.Chatbot import ChatbotGUI - +import time # Its our main window of application. @@ -49,7 +49,7 @@ 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.""" @@ -58,7 +58,7 @@ def __init__(self, *args): # Set slot for simulation end signal to plot simulation data self.simulationEndSignal.connect(self.plotSimulationData) - + self.errorDetectedSignal.connect(self.handleError) # Creating require Object self.obj_workspace = Workspace.Workspace() self.obj_Mainview = MainView() @@ -443,7 +443,14 @@ def plotSimulationData(self, exitCode, exitStatus): print("Exception Message:", str(e), traceback.format_exc()) self.obj_appconfig.print_error('Exception Message : ' + str(e)) + time.sleep(3) + self.errorDetectedSignal.emit("Simulation failed.") + def handleError(self): + self.projDir = self.obj_appconfig.current_project["ProjectName"] + self.output_file = os.path.join(self.projDir, "ngspice_error.log") + if self.chatbot_window.isVisible(): + self.chatbot_window.debug_error(self.output_file) def open_ngspice(self): """This Function execute ngspice on current project.""" projDir = self.obj_appconfig.current_project["ProjectName"] diff --git a/src/ngspiceSimulation/NgspiceWidget.py b/src/ngspiceSimulation/NgspiceWidget.py index 6d4e832a2..271a271a9 100644 --- a/src/ngspiceSimulation/NgspiceWidget.py +++ b/src/ngspiceSimulation/NgspiceWidget.py @@ -171,7 +171,5 @@ def finishSimulation(self, exitCode, exitStatus, error_log_path = os.path.join(self.projDir, "ngspice_error.log") with open(error_log_path, "w", encoding="utf-8") as error_log: error_log.write(console_output + "\n") - if self.chat.isVisible(): - if (exitStatus == QtCore.QProcess.NormalExit and exitCode == 0 \ - and errorType == QtCore.QProcess.UnknownError) or "Simulation Failed!" in console_output: - self.chat.debug_error(self.output_file) \ No newline at end of file + if self.chat.isVisible()and "Simulation Failed!" in console_output: + self.chat.debug_error(self.output_file) \ No newline at end of file From 64eb68cb1dfde3d4de45878a01af1adb14f87f36 Mon Sep 17 00:00:00 2001 From: Myo-Thinzar Date: Wed, 26 Mar 2025 05:16:53 +0530 Subject: [PATCH 8/9] set delay to make sure log is save in output file --- src/frontEnd/Application.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/frontEnd/Application.py b/src/frontEnd/Application.py index 7729f4d3a..464920499 100644 --- a/src/frontEnd/Application.py +++ b/src/frontEnd/Application.py @@ -41,7 +41,7 @@ from projManagement.Validation import Validation from projManagement import Worker from frontEnd.Chatbot import ChatbotGUI -import time +from PyQt5.QtCore import QTimer # Its our main window of application. @@ -443,14 +443,18 @@ def plotSimulationData(self, exitCode, exitStatus): print("Exception Message:", str(e), traceback.format_exc()) self.obj_appconfig.print_error('Exception Message : ' + str(e)) - time.sleep(3) + self.errorDetectedSignal.emit("Simulation failed.") def handleError(self): self.projDir = self.obj_appconfig.current_project["ProjectName"] self.output_file = os.path.join(self.projDir, "ngspice_error.log") if self.chatbot_window.isVisible(): - self.chatbot_window.debug_error(self.output_file) + self.delayed_function_call() + + def delayed_function_call(self): + QTimer.singleShot(2000, lambda: self.chatbot_window.debug_error(self.output_file)) + def open_ngspice(self): """This Function execute ngspice on current project.""" projDir = self.obj_appconfig.current_project["ProjectName"] From e3a3496c4d747e30aa555b8d3950b22d3fe77106 Mon Sep 17 00:00:00 2001 From: nidhiii128 Date: Sat, 14 Mar 2026 13:28:47 +0000 Subject: [PATCH 9/9] feat: implement design-aware AI assistance via automated netlist feeding --- src/chatbot/chatbot_thread.py | 16 ++++----- src/frontEnd/Chatbot.py | 65 ++++++++++++++++++++++++++++++----- 2 files changed, 63 insertions(+), 18 deletions(-) diff --git a/src/chatbot/chatbot_thread.py b/src/chatbot/chatbot_thread.py index 6e2817d37..75a36bf57 100644 --- a/src/chatbot/chatbot_thread.py +++ b/src/chatbot/chatbot_thread.py @@ -6,8 +6,7 @@ os.environ["QT_QPA_PLATFORM"] = "xcb" class OllamaWorker(QThread): - """Runs Ollama in a separate thread.""" - response_signal = pyqtSignal(str) # Signal to send response back to the UI + response_signal = pyqtSignal(str) def __init__(self, user_text): super().__init__() @@ -15,17 +14,18 @@ def __init__(self, user_text): def run(self): try: + # We explicitly tell the AI to prioritize the netlist context messages = [ { "role": "system", - "content": ("You are a professional electronic engineer advising users or help debugging on " - "EDA tool eSim's KiCad, and NgSPICE simulation. " - "Explain concisely in at MOST 30 words or 5 sentences to minimize wait time.Do not exceed limit. " - "Here is the maintained chat history.") + "content": ("You are a professional electronic engineer for eSim. " + "Use the provided netlist to analyze nodes, components, and connections. " + "Explain concisely in at MOST 30 words.") }, {"role": "user", "content": self.user_text} ] + # Using JSON dump for robust message passing response = subprocess.run( ["ollama", "run", "qwen2.5-coder:3b", json.dumps(messages)], capture_output=True, text=True, check=True @@ -33,9 +33,7 @@ def run(self): bot_response = response.stdout.strip() or "No response received." - except subprocess.CalledProcessError as e: - bot_response = f"Error: Ollama execution failed - {e.stderr.strip()}" except Exception as e: bot_response = f"Error: {str(e)}" - self.response_signal.emit(bot_response) # Send response to main thread + self.response_signal.emit(bot_response) \ No newline at end of file diff --git a/src/frontEnd/Chatbot.py b/src/frontEnd/Chatbot.py index bf6625ba1..c1e249e3e 100644 --- a/src/frontEnd/Chatbot.py +++ b/src/frontEnd/Chatbot.py @@ -17,7 +17,6 @@ def __init__(self): super().__init__() self.setWindowTitle("AI Chatbot") self.setFixedSize(400, 250) - self.chat_history = [] layout = QVBoxLayout(self) @@ -39,15 +38,47 @@ def __init__(self): layout.addLayout(input_layout) self.move_to_bottom_right() + def get_netlist_content(self): + """Finds and reads the current project's .cir file.""" + try: + self.obj_appconfig = Appconfig() + proj_info = self.obj_appconfig.current_project + if proj_info and "ProjectName" in proj_info: + proj_dir = proj_info["ProjectName"] + proj_name = os.path.basename(proj_dir.rstrip(os.sep)) + netlist_path = os.path.join(proj_dir, f"{proj_name}.cir") + + if os.path.exists(netlist_path): + with open(netlist_path, "r") as f: + return f.read() + except Exception as e: + print(f"Error fetching netlist: {e}") + return None + def ask_ollama(self): user_text = self.user_input.text().strip() if not user_text: return + # 1. Fetch Netlist Context (The Proposal Implementation) + netlist = self.get_netlist_content() + + # 2. Update History self.chat_history = (self.chat_history + [f"User: {user_text}"])[-4:] self.chat_display.append(f"You: {user_text}") - self.worker = OllamaWorker(self.chat_history) + # 3. Create a context-aware prompt + if netlist: + # We explicitly tell the AI to look at the netlist + context_prompt = ( + f"Analyze this eSim Netlist:\n{netlist}\n\n" + f"User Question: {user_text}" + ) + else: + context_prompt = user_text + + # 4. Pass the context_prompt to the worker + self.worker = OllamaWorker(context_prompt) self.worker.response_signal.connect(self.display_response) self.worker.start() @@ -69,15 +100,30 @@ def clear_session(self): """Clear the chat display.""" self.chat_display.clear() self.chat_history=[] + def debug_ollama(self): - """Send log to Ollama and get response asynchronously.""" - self.chat_display.append(f"============Simulation Failed=============\n") - user_text = self.user_input.text().strip() - self.worker = OllamaWorker(user_text) + """Send log AND netlist to Ollama for failed simulation analysis.""" + self.chat_display.append(f"============ Simulation Failed =============\n") + error_log = self.user_input.text().strip() + + # Get the netlist to help the AI understand the context of the error + netlist = self.get_netlist_content() + + if netlist: + combined_query = ( + f"SIMULATION ERROR LOG:\n{error_log}\n\n" + f"CORRESPONDING NETLIST:\n{netlist}\n\n" + "Please analyze the error based on this netlist." + ) + else: + combined_query = error_log + + # Pass the combined data to the worker + self.worker = OllamaWorker(combined_query) self.worker.response_signal.connect(self.display_response) self.worker.start() - self.user_input.clear() # Clear input field - + self.user_input.clear() + def debug_error(self, log): self.chat_history = [] if os.path.exists(log): @@ -99,4 +145,5 @@ def debug_error(self, log): output_file = os.path.join(self.projDir, "erroroutput.txt") with open(output_file, "w") as f: f.writelines(filtered_lines) - self.debug_ollama() \ No newline at end of file + self.debug_ollama() +