← Back to ChatHandler documentation
ChatHandler - Source Code¶
File: Distributed_Design_Optimizer/postprocess/handlers/ChatHandler.py
# Copyright (C) The DistributedDesignOptimizer Contributors
# Licensed under the GNU General Public License v3.0. See LICENSE file for details.
#
# Additional Request:
# We kindly request that any modifications or changes to the source code be
# shared with us by submitting them as pull requests to our GitHub repository.
# This request is not legally binding but is made in the spirit of
# collaboration and open-source development.
"""Handler for LLM-based chat interactions with optimization results."""
import logging
from collections.abc import Callable
from typing import Any
from PySide6.QtCore import QObject, Signal
from ..workers.LLMWorker import LLMWorker
logger = logging.getLogger(__name__)
class ChatHandler(QObject):
"""Manages LLM interaction for analyzing optimization results."""
response_ready = Signal(str)
error = Signal(str)
def __init__(self, config: dict[str, str], parent: QObject | None = None) -> None:
"""Initialize the chat handler.
Args:
config: Dictionary containing LLM client credentials.
parent: Optional parent QObject.
"""
super().__init__(parent)
self._config = config
self._analysis_type: str = ""
self._context_data: str = ""
self._history: list[dict] = []
self._worker: LLMWorker | None = None
self._last_question: str = ""
self._call_fn: Callable[..., Any] | None = None
# Import the LLM call function from existing codebase
try:
from Distributed_Design_Optimizer.postprocess.handlers.llm_api import call_llm_api
from Distributed_Design_Optimizer.postprocess.handlers.system_prompt import SYSTEM_PROMPT
self._call_fn = call_llm_api
self._system_prompt = SYSTEM_PROMPT
except ImportError:
logger.warning("LLM API module not available. Chat will not function.")
self._system_prompt = "You are an AI assistant analyzing optimization data."
@property
def is_available(self) -> bool:
"""Whether the LLM backend is usable.
Returns:
bool: True if the LLM call function is available.
"""
return self._call_fn is not None
def set_context(self, analysis_type: str, context_data: dict) -> None:
"""Update the current analysis context and reset conversation history.
Args:
analysis_type: The type of analysis being viewed.
context_data: Dictionary of context information for the LLM.
"""
self._analysis_type = analysis_type
self._context_data = self._format_context(analysis_type, context_data)
self._history.clear()
def ask(self, question: str) -> None:
"""Send a question to the LLM in a background thread.
Args:
question: The user's question to send to the LLM.
"""
if self._call_fn is None:
self.error.emit("LLM API not available. Check that llm_api module is installed.")
return
# Prevent concurrent requests — cancel previous worker if still running
if self._worker is not None and self._worker.isRunning():
self._worker.response_ready.disconnect(self._on_response)
self._worker.error.disconnect(self._on_error)
self._worker.requestInterruption()
self._worker.wait(1000)
self._last_question = question
messages = [
{"role": "system", "content": self._system_prompt},
{"role": "system", "content": f"The user is currently viewing the following data:\n{self._context_data}"},
]
messages.extend(self._history)
messages.append({"role": "user", "content": question})
self._worker = LLMWorker(self._call_fn, messages, self._config)
self._worker.response_ready.connect(self._on_response)
self._worker.error.connect(self._on_error)
self._worker.start()
def clear_history(self) -> None:
"""Clear the conversation history."""
self._history.clear()
def cancel(self) -> None:
"""Stop any running LLM worker."""
if self._worker is not None and self._worker.isRunning():
self._worker.requestInterruption()
self._worker.wait(1000)
self._worker = None
# ------------------------------------------------------------------
# Private
# ------------------------------------------------------------------
def _on_response(self, response: str) -> None:
# Store in history for multi-turn conversation
if self._last_question:
self._history.append({"role": "user", "content": self._last_question})
self._history.append({"role": "assistant", "content": response})
self.response_ready.emit(response)
self._worker = None
def _on_error(self, error_msg: str) -> None:
self.error.emit(f"LLM Error: {error_msg}")
self._worker = None
@staticmethod
def _format_context(analysis_type: str, data: dict) -> str:
"""Format context data into a readable string for the LLM."""
lines = [f"Analysis Type: {analysis_type}"]
context_fields = [
("mode", "Mode"),
("n_iterations", "Number of iterations"),
("n_nodes", "Graph nodes"),
("n_edges", "Graph edges"),
("cluster_info", "Cluster info"),
("centrality_info", "Centrality info"),
]
for key, label in context_fields:
if key in data:
lines.append(f"{label}: {data[key]}")
return "\n".join(lines)