Skip to content

AIChatBot Myo Thinzar Kyaw #320

New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Merged
Binary file added images/chatbot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
41 changes: 41 additions & 0 deletions src/chatbot/chatbot_thread.py
Original file line number Diff line number Diff line change
@@ -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
47 changes: 43 additions & 4 deletions src/frontEnd/Application.py
Original file line number Diff line number Diff line change
@@ -40,15 +40,16 @@
from projManagement.Kicad import Kicad
from projManagement.Validation import Validation
from projManagement import Worker

from frontEnd.Chatbot import ChatbotGUI
from PyQt5.QtCore import QTimer
# Its our main window of application.


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,16 +58,18 @@ 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()
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 +85,29 @@ 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(30, 30))
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() - 100, self.height() - 60)

def initToolBar(self):
"""
This function initializes Tool Bars.
@@ -293,6 +319,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.')

@@ -416,6 +444,17 @@ def plotSimulationData(self, exitCode, exitStatus):
self.obj_appconfig.print_error('Exception Message : '
+ str(e))

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.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"]
@@ -438,7 +477,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)
102 changes: 102 additions & 0 deletions src/frontEnd/Chatbot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
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
from configuration.Appconfig import Appconfig
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 = [line for line in f.readlines() 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)
self.user_input.setText(combined_text)
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()
4 changes: 2 additions & 2 deletions src/frontEnd/DockArea.py
Original file line number Diff line number Diff line change
@@ -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
18 changes: 12 additions & 6 deletions src/ngspiceSimulation/NgspiceWidget.py
Original file line number Diff line number Diff line change
@@ -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,12 +27,12 @@ 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)
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)
@@ -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,8 +135,7 @@ def finishSimulation(self, exitCode, exitStatus,
{} \
</span>'
self.terminalUi.simulationConsole.append(
successFormat.format("Simulation Completed Successfully!"))

successFormat.format("Simulation Completed Successfully!"))
else:
failedFormat = '<span style="color:#ff3333; font-size:26px;"> \
{} \
@@ -167,3 +166,10 @@ 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")
if self.chat.isVisible()and "Simulation Failed!" in console_output:
self.chat.debug_error(self.output_file)