Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
39 changes: 39 additions & 0 deletions src/chatbot/chatbot_thread.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import subprocess
import os
import json
from PyQt5.QtCore import QThread, pyqtSignal

os.environ["QT_QPA_PLATFORM"] = "xcb"

class OllamaWorker(QThread):
response_signal = pyqtSignal(str)

def __init__(self, user_text):
super().__init__()
self.user_text = 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 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
)

bot_response = response.stdout.strip() or "No response received."

except Exception as e:
bot_response = f"Error: {str(e)}"

self.response_signal.emit(bot_response)
47 changes: 43 additions & 4 deletions src/frontEnd/Application.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""

Expand All @@ -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,
Expand All @@ -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.
Expand Down Expand Up @@ -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.')

Expand Down Expand Up @@ -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"]
Expand All @@ -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)
Expand Down
149 changes: 149 additions & 0 deletions src/frontEnd/Chatbot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
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 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}")

# 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()

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 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()

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
Expand Up @@ -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
Expand Down
18 changes: 12 additions & 6 deletions src/ngspiceSimulation/NgspiceWidget.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;"> \
{} \
Expand Down Expand Up @@ -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)