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
83 changes: 74 additions & 9 deletions src/chatbot/chatbot_thread.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,15 +162,41 @@ def is_ollama_running():


def start_ollama(stop_flag=None):
"""Start Ollama server silently in the background (no terminal window)."""
cmd = ["ollama", "serve"]
if os.name == 'nt':
subprocess.Popen('start cmd /k "ollama serve"', shell=True)
else:
subprocess.Popen(
['bash', '-c',
'x-terminal-emulator -e "ollama serve" || '
'gnome-terminal -- ollama serve || '
'xterm -e "ollama serve"']
)
import shutil
# If 'ollama' is not directly callable from PATH, check default install locations
if not shutil.which("ollama"):
local_appdata = os.environ.get("LOCALAPPDATA", "")
possible_paths = [
os.path.join(local_appdata, "Programs", "Ollama", "ollama.exe"),
r"C:\Program Files\Ollama\ollama.exe",
r"C:\Program Files (x86)\Ollama\ollama.exe",
]
for path in possible_paths:
if os.path.exists(path):
cmd = [path, "serve"]
break
try:
if os.name == 'nt':
# Windows: CREATE_NO_WINDOW flag prevents a cmd popup
subprocess.Popen(
cmd,
creationflags=subprocess.CREATE_NO_WINDOW,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
else:
# Linux/macOS: redirect output to /dev/null, no terminal emulator needed
subprocess.Popen(
cmd,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
except FileNotFoundError:
# ollama binary not found in PATH or disk
return False
for _ in range(30):
if stop_flag is not None and stop_flag():
return False
Expand All @@ -179,7 +205,6 @@ def start_ollama(stop_flag=None):
return True
return False


# ── Topic switch detection ───────────────────────────────────────────────────

_STOP_WORDS = {
Expand Down Expand Up @@ -264,6 +289,46 @@ def run(self):
self.result_signal.emit([])


# ── Model Pull Worker ─────────────────────────────────────────────────────────
# Required models for the chatbot to function correctly
REQUIRED_MODELS = ["qwen2.5-coder:3b", "nomic-embed-text"]
VISION_MODEL = "minicpm-v" # optional — only needed for image analysis

class ModelPullWorker(QThread):
"""
Downloads a single Ollama model in the background.
Emits:
progress_signal(str) — human-readable status/percentage string
done_signal(bool) — True on success, False on failure
"""
progress_signal = pyqtSignal(str)
done_signal = pyqtSignal(bool)

def __init__(self, model_name: str):
super().__init__()
self.model_name = model_name

def run(self):
try:
self.progress_signal.emit(f"⬇️ Downloading {self.model_name}… 0%")
for update in ollama.pull(self.model_name, stream=True):
# update is a dict with keys: status, completed, total
status = update.get("status", "")
completed = update.get("completed", 0)
total = update.get("total", 0)
if total and completed:
pct = int((completed / total) * 100)
self.progress_signal.emit(
f"⬇️ Downloading {self.model_name}… {pct}%"
)
elif status:
self.progress_signal.emit(
f"⬇️ {self.model_name}: {status}"
)
self.done_signal.emit(True)
except Exception as e:
self.progress_signal.emit(f"❌ Failed to download {self.model_name}: {e}")
self.done_signal.emit(False)
# ── Smart token budget ───────────────────────────────────────────────────────

_COMPLEX_KEYWORDS = {
Expand Down
2 changes: 1 addition & 1 deletion src/configuration/Appconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
# REVISION: Thursday 29 June 2023
# =========================================================================

from PyQt5 import QtWidgets
from PyQt6 import QtWidgets
import os
import json
from configparser import ConfigParser
Expand Down
42 changes: 21 additions & 21 deletions src/frontEnd/Application.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@
current_dir = os.path.dirname(os.path.abspath(__file__))
init_path = os.path.abspath(os.path.join(current_dir, "..", "..")) + os.sep

from PyQt5 import QtGui, QtCore, QtWidgets
from PyQt5.Qt import QSize
from PyQt6 import QtGui, QtCore, QtWidgets

from configuration.Appconfig import Appconfig
from frontEnd import ProjectExplorer
from frontEnd import Workspace
Expand All @@ -42,7 +42,7 @@
from projManagement.Validation import Validation
from projManagement import Worker
from frontEnd.Chatbot import ChatbotGUI
from PyQt5.QtCore import QTimer
from PyQt6.QtCore import QTimer, Qsize
# Its our main window of application.


Expand Down Expand Up @@ -272,8 +272,8 @@ def initToolBar(self):
# corner in the application window.
self.spacer = QtWidgets.QWidget()
self.spacer.setSizePolicy(
QtWidgets.QSizePolicy.Expanding,
QtWidgets.QSizePolicy.Expanding)
QtWidgets.QSizePolicy.Policy.Expanding,
QtWidgets.QSizePolicy.Policy.Expanding)
self.topToolbar.addWidget(self.spacer)
self.logo = QtWidgets.QLabel()
self.logopic = QtGui.QPixmap(
Expand Down Expand Up @@ -359,7 +359,7 @@ def initToolBar(self):
self.lefttoolbar.addAction(self.omedit)
self.lefttoolbar.addAction(self.omoptim)
self.lefttoolbar.addAction(self.conToeSim)
self.lefttoolbar.setOrientation(QtCore.Qt.Vertical)
self.lefttoolbar.setOrientation(QtCore.Qt.Orientation.Vertical)
self.lefttoolbar.setIconSize(QSize(40, 40))

def closeEvent(self, event):
Expand All @@ -383,11 +383,11 @@ def closeEvent(self, event):
exit_msg = "Are you sure you want to exit the program?"
exit_msg += " All unsaved data will be lost."
reply = QtWidgets.QMessageBox.question(
self, 'Message', exit_msg, QtWidgets.QMessageBox.Yes,
QtWidgets.QMessageBox.No
self, 'Message', exit_msg, QtWidgets.QMessageBox.StandardButton.Yes,
QtWidgets.QMessageBox.StandardButton.No
)

if reply == QtWidgets.QMessageBox.Yes:
if reply == QtWidgets.QMessageBox.StandardButton.Yes:
for proc in self.obj_appconfig.procThread_list:
try:
proc.terminate()
Expand Down Expand Up @@ -417,7 +417,7 @@ def closeEvent(self, event):
event.accept()
self.systemTrayIcon.showMessage('Exit', 'eSim is Closed.')

elif reply == QtWidgets.QMessageBox.No:
elif reply == QtWidgets.QMessageBox.StandardButton.No:
event.ignore()

def new_project(self):
Expand Down Expand Up @@ -532,7 +532,7 @@ def plotSimulationData(self, exitCode, exitStatus):
self.msg.showMessage(
'Data could not be plotted. Please try again.'
)
self.msg.exec_()
self.msg.exec()
print("Exception Message:", str(e), traceback.format_exc())
self.obj_appconfig.print_error('Exception Message : '
+ str(e))
Expand Down Expand Up @@ -568,7 +568,7 @@ def open_ngspice(self):
self.msg.showMessage(
'Netlist (*.cir.out) not found.'
)
self.msg.exec_()
self.msg.exec()
return

self.obj_Mainview.obj_dockarea.ngspiceEditor(
Expand All @@ -587,7 +587,7 @@ def open_ngspice(self):
'Please select the project first.'
' You can either create new project or open existing project'
)
self.msg.exec_()
self.msg.exec()

def open_subcircuit(self):
"""
Expand Down Expand Up @@ -628,7 +628,7 @@ def open_nghdl(self):
'Please make sure it is installed')
self.obj_appconfig.print_error('Error while opening NGHDL. ' +
'Please make sure it is installed')
self.msg.exec_()
self.msg.exec()

def open_makerchip(self):
"""
Expand Down Expand Up @@ -684,7 +684,7 @@ def open_OMedit(self):
'Current project does not contain any Ngspice file. ' +
'Please create Ngspice file with extension .cir.out'
)
self.msg.exec_()
self.msg.exec()
else:
self.msg = QtWidgets.QErrorMessage()
self.msg.setModal(True)
Expand All @@ -693,7 +693,7 @@ def open_OMedit(self):
'Please select the project first. You can either ' +
'create a new project or open an existing project'
)
self.msg.exec_()
self.msg.exec()

def open_OMoptim(self):
"""
Expand Down Expand Up @@ -726,11 +726,11 @@ def open_OMoptim(self):
"https://www.openmodelica.org/download/download-windows"
">OpenModelica Windows</a> and install latest version.<br/>"
)
self.msg.setTextFormat(QtCore.Qt.RichText)
self.msg.setTextFormat(QtCore.Qt.TextFormat.RichText)
self.msg.setText(self.msgContent)
self.msg.setWindowTitle("Error Message")
self.obj_appconfig.print_info(self.msgContent)
self.msg.exec_()
self.msg.exec()

def open_conToeSim(self):
print("Function : Schematics converter")
Expand Down Expand Up @@ -783,7 +783,7 @@ def __init__(self, *args):
self.obj_projectExplorer = ProjectExplorer.ProjectExplorer()

# Adding content to vertical middle Split.
self.middleSplit.setOrientation(QtCore.Qt.Vertical)
self.middleSplit.setOrientation(QtCore.Qt.Orientation.Vertical)
self.middleSplit.addWidget(self.obj_dockarea)
self.middleSplit.addWidget(self.noteArea)

Expand Down Expand Up @@ -817,7 +817,7 @@ def main(args):

splash_pix = QtGui.QPixmap(init_path + 'images/splash_screen_esim.png')
splash = QtWidgets.QSplashScreen(
appView, splash_pix, QtCore.Qt.WindowStaysOnTopHint
appView, splash_pix, QtCore.Qt.WindowType.WindowStaysOnTopHint
)
splash.setMask(splash_pix.mask())
splash.setDisabled(True)
Expand All @@ -842,7 +842,7 @@ def main(args):
else:
appView.obj_workspace.show()

sys.exit(app.exec_())
sys.exit(app.exec())


# Call main function
Expand Down
Loading