was hältst du von diesem code einer multistep agent chat anwendung, die auf einem lokalen rechner und mit einem über ollama ausgeführtem LLM ausgeführt werden soll. Falls es notwendig ist soll eine chat antwort nicht zwingend in einem schritt generiert werden. Es soll je nach komplexität ein plan erstellt werden, dessen bearbeitung in mehreren schritten bearbeitet wird und am ende eine finale antwort auf eine nutzer frage geben. Dies soll technisch umgesetzt werden in dem diverse dabei entstehende probleme durch verwendung von speziellen methoden und strategien gelöst werden, wie zum beispiel im bereich effizientes kontext management zur reduzierung der hardware nutzung. Prüfe ob das skript der anwendung in einer geeigneten art und weise umgesetzt ist und ob es möglichkeiten gibt mit denen der code noch besser werden kann. Wenn du verbesserungen am code durchführen kannst, nenne anschließend aufjedenfall den vollständigen code der anwendung ohne fehlende inhalte. Ich benötige das gesamte skript, auch die abschnitte in denen du keine änderungen am code machst.

import sys
import os
import re
import subprocess
import json
import threading
import sqlite3
import time
import logging
import requests
from PyQt5.QtWidgets import (
    QApplication, QWidget, QVBoxLayout, QHBoxLayout, QLineEdit,
    QPushButton, QLabel, QDialog, QFormLayout, QSlider, QMessageBox, QComboBox,
    QTextBrowser, QScrollArea, QListWidget, QListWidgetItem, QProgressBar, QInputDialog, QTextEdit
)
from PyQt5.QtCore import Qt, QTimer, pyqtSignal, QObject, QRegularExpression
from PyQt5.QtGui import QTextCursor, QColor, QFont, QTextCharFormat, QSyntaxHighlighter, QPainter, QPen

# NOTE: The entire code and application interface have been translated to English for consistency and broader accessibility.

# Logging configuration
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')

def find_ollama_executable():
    if os.name == 'nt':  # Windows
        direct_path = "C:\\Users\\Admin\\AppData\\Local\\Programs\\Ollama\\ollama.exe"
        if os.path.exists(direct_path):
            return direct_path
            
        alt_path = os.path.join("C:", "Users", "Admin", "AppData", "Local", "Programs", "Ollama", "ollama.exe")
        if os.path.exists(alt_path):
            return alt_path

    return None

# Database Manager
class DatabaseManager:
    def __init__(self, db_name="autollm_context.db", max_context_length=10000):
        try:
            self.conn = sqlite3.connect(db_name, check_same_thread=False)
            self.max_context_length = max_context_length
            self.create_table()
        except sqlite3.Error as e:
            logging.error(f"Database connection error: {e}")
    
    def create_table(self):
        with self.conn:
            self.conn.execute('''
                CREATE TABLE IF NOT EXISTS context (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    sender TEXT NOT NULL,
                    message TEXT NOT NULL,
                    timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
                )
            ''')

    def add_message(self, sender, message):
        with self.conn:
            self.conn.execute("INSERT INTO context (sender, message) VALUES (?, ?)", (sender, message))
            self.ensure_context_limit()

    def get_context(self, max_length=None):
        if max_length is None:
            max_length = self.max_context_length
        cursor = self.conn.cursor()
        cursor.execute("SELECT sender, message FROM context ORDER BY id DESC")
        rows = cursor.fetchall()
        context = []
        current_length = 0
        for row in reversed(rows):
            msg = f"{row[0]}: {row[1]}"
            msg_length = len(msg)
            if current_length + msg_length > max_length:
                break
            context.insert(0, msg)
            current_length += msg_length
        return "\n".join(context)

    def clear_context(self):
        with self.conn:
            self.conn.execute("DELETE FROM context")

    def ensure_context_limit(self):
        cursor = self.conn.cursor()
        cursor.execute("SELECT SUM(LENGTH(message)) FROM context")
        total_length = cursor.fetchone()[0] or 0
        if total_length > self.max_context_length:
            cursor.execute("DELETE FROM context WHERE id IN (SELECT id FROM context ORDER BY id ASC LIMIT 100)")
            self.conn.commit()

    def close_connection(self):
        if self.conn:
            self.conn.close()

# TaskWidget for visual representation of tasks
class TaskWidget(QWidget):
    def __init__(self, task_name, goal, status="Pending"):
        super().__init__()
        self.task_name = task_name
        self.goal = goal
        self.status = status
        self.dependencies = []  # List of task names this task depends on
        self.init_ui()

    def init_ui(self):
        layout = QVBoxLayout()
        self.name_label = QLabel(f"Task: {self.task_name}")
        self.goal_label = QLabel(f"Goal: {self.goal}")
        self.status_label = QLabel(f"Status: {self.status}")
        layout.addWidget(self.name_label)
        layout.addWidget(self.goal_label)
        layout.addWidget(self.status_label)
        self.setLayout(layout)

    def set_status(self, status):
        self.status = status
        self.status_label.setText(f"Status: {self.status}")
        self.update()

    def add_dependency(self, task_name):
        if task_name not in self.dependencies:
            self.dependencies.append(task_name)

# Code Highlighter
class CodeHighlighter(QSyntaxHighlighter):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.highlightingRules = []
        format_keyword = QTextCharFormat()
        format_keyword.setForeground(QColor("orange"))
        keywords = [
            "if", "else", "for", "while", "def", "return", "class", "import", "from",
            "try", "except", "elif", "with", "as", "and", "or", "not", "in", "is", "lambda"
        ]
        for keyword in keywords:
            pattern = QRegularExpression(r"\b" + keyword + r"\b")
            self.highlightingRules.append((pattern, format_keyword))

    def highlightBlock(self, text):
        for pattern, format in self.highlightingRules:
            match_iterator = pattern.globalMatch(text)
            while match_iterator.hasNext():
                match = match_iterator.next()
                self.setFormat(match.capturedStart(), match.capturedLength(), format)

# Ollama Controller
class OllamaController(QObject):
    server_status_changed = pyqtSignal(bool)
    response_received = pyqtSignal(dict)
    error_occurred = pyqtSignal(str)
    llm_list_updated = pyqtSignal(list)
    summarization_received = pyqtSignal(dict)

    def __init__(self):
        super().__init__()
        self.process = None
        self.selected_llm = None
        self.api_port = 11434
        self.server_started = False
        self.interrupt_event = threading.Event()
        self.thread_lock = threading.Lock()
        self.check_server_status()

    def check_server_status(self):
        try:
            response = requests.get(f"http://localhost:{self.api_port}/api/tags", timeout=2)
            if response.status_code == 200:
                self.server_started = True
                self.server_status_changed.emit(True)
                self.update_llm_list()
            else:
                self.server_started = False
                self.server_status_changed.emit(False)
                self.error_occurred.emit(f"API check failed: {response.status_code}")
        except requests.exceptions.RequestException:
            self.server_started = False
            self.server_status_changed.emit(False)
            self.error_occurred.emit("Failed to connect to the server.")

    def update_llm_list(self):
        llms = self.get_installed_llms()
        if llms:
            self.llm_list_updated.emit(llms)
            if not self.selected_llm:
                self.selected_llm = llms[0]
        else:
            self.llm_list_updated.emit([])
            self.selected_llm = None

    def start_server(self):
        if not self.is_server_running():
            try:
                ollama_path = find_ollama_executable()
                if not ollama_path:
                    self.error_occurred.emit("Ollama executable not found.")
                    return

                startupinfo = None
                if os.name == 'nt':
                    startupinfo = subprocess.STARTUPINFO()
                    startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW

                self.process = subprocess.Popen(
                    [ollama_path, 'serve'],
                    stdout=subprocess.PIPE,
                    stderr=subprocess.PIPE,
                    shell=False,
                    startupinfo=startupinfo,
                    cwd=os.path.expanduser("~")
                )
                threading.Thread(target=self.delayed_check, daemon=True).start()
                
            except Exception as e:
                self.error_occurred.emit(f"Error starting server: {str(e)}")
        else:
            self.server_started = True
            self.server_status_changed.emit(True)
            self.update_llm_list()

    def delayed_check(self):
        time.sleep(5)
        self.check_server_status()

    def stop_server(self):
        if self.is_server_running() and self.process:
            try:
                self.interrupt_event.set()
                self.process.terminate()
                self.process.wait(timeout=10)
                self.server_started = False
                self.server_status_changed.emit(False)
                self.process = None
                self.selected_llm = None
                self.llm_list_updated.emit([])
            except Exception as e:
                self.error_occurred.emit(f"Error stopping server: {e}")

    def get_installed_llms(self):
        try:
            response = requests.get(f"http://localhost:{self.api_port}/api/tags", timeout=2)
            response.raise_for_status()
            data = response.json()
            return [model["name"] for model in data.get("models", [])]
        except requests.exceptions.RequestException as e:
            self.error_occurred.emit(f"Error fetching models: {e}")
            return []

    def is_server_running(self):
        try:
            response = requests.get(f"http://localhost:{self.api_port}/api/tags", timeout=2)
            return response.status_code == 200
        except Exception:
            return False

    def set_selected_llm(self, llm_name):
        self.selected_llm = llm_name

    def send_request(self, endpoint, data):
        if not self.is_server_running():
            self.error_occurred.emit("Ollama server is not running.")
            return
        threading.Thread(target=self._handle_request, args=(endpoint, data), daemon=True).start()

    def _handle_request(self, endpoint, data):
        self.interrupt_event.clear()
        try:
            url = f"http://localhost:{self.api_port}{endpoint}"
            response = requests.post(url, json=data, stream=True)
            response.raise_for_status()

            for line in response.iter_lines():
                if self.interrupt_event.is_set():
                    raise InterruptedError("User interrupted the request.")
                if line:
                    decoded_line = line.decode('utf-8')
                    try:
                        data = json.loads(decoded_line)
                        if "error" in data:
                            raise Exception(f"Ollama API Error: {data['error']}")
                        self.response_received.emit(data)
                    except json.JSONDecodeError as e:
                        self.error_occurred.emit(f"Invalid JSON response: {e}")
                        return
        except InterruptedError:
            self.error_occurred.emit("Request was interrupted.")
        except requests.exceptions.RequestException as e:
            self.error_occurred.emit(f"API request failed: {e}")
        except Exception as e:
            self.error_occurred.emit(f"Error processing request: {e}")

# Task Manager
class TaskManager(QObject):
    task_completed = pyqtSignal(str)
    task_failed = pyqtSignal(str)
    progress_updated = pyqtSignal(int, int)  # (completed, total)
    final_answer_ready = pyqtSignal(str)
    message_sent = pyqtSignal(str, str, str)  # (sender, message, message_type)

    def __init__(self, ollama_controller, context_manager):
        super().__init__()
        self.ollama = ollama_controller
        self.context_manager = context_manager
        self.tasks = {}  # task_name: TaskWidget
        self.task_queue = []
        self.completed_tasks = []
        self.failed_tasks = []
        self.current_task = None
        self.total_tasks = 0
        self.completed_count = 0
        self.finalizing = False
        self.ollama.response_received.connect(self.handle_response)
        self.ollama.error_occurred.connect(self.on_error)

    def add_task(self, user_request):
        try:
            planning_prompt = (
                "Please create a detailed plan to break down the following request into meaningful subtasks. "
                "Each subtask should be a clear, independent step that can be completed in one go. "
                "Return the steps in a numbered list format with a summary at the end. Use '__END__' to mark the end of the response."
                "\n\n"
                f"Request: {user_request}"
            )
            self.current_user_request = user_request
            # Assuming planning uses the Planning LLM
            self.ollama.set_selected_llm(self.get_planning_llm())
            self.ollama.send_request("/api/generate", {
                "model": self.get_planning_llm(),
                "prompt": planning_prompt,
                "options": {
                    "temperature": 0.5,
                    "max_new_tokens": 1500,
                    "stream": True,  # Enable streaming for planning
                    "format": "json"
                }
            })
            self.total_tasks = 0
            self.completed_count = 0
            self.task_queue.clear()
            self.completed_tasks.clear()
            self.failed_tasks.clear()
            self.current_task = None
            self.finalizing = False
            self.progress_updated.emit(self.completed_count, self.total_tasks)
        except requests.exceptions.RequestException as e:
            self.message_sent.emit("System", f"Error creating plan: {e}", "error")
            logging.error(f"Error creating plan: {e}")

    def handle_response(self, response):
        if self.finalizing:
            if "response" in response:
                final_answer = response["response"]
                if "__END__" in final_answer:
                    final_answer = final_answer.replace("__END__", "")
                    self.final_answer_ready.emit(final_answer)
            return

        if self.current_task:
            # Handling subtask response
            if "response" in response:
                result = response["response"].strip()
                self.append_subtask_result(result)
        else:
            # Handling planning response
            if "response" in response:
                chunk = response["response"]
                if "__END__" in chunk:
                    chunk = chunk.replace("__END__", "")
                    steps, summary = self.parse_planning(chunk)
                    if steps:
                        self.task_queue.extend(steps)
                        self.total_tasks += len(steps)
                        self.progress_updated.emit(self.completed_count, self.total_tasks)
                        self.process_next_task()
                    if summary:
                        self.context_manager.add_message("System", f"Summary: {summary}")
                elif "__CONTINUE__" in chunk:
                    # Await more chunks
                    pass
                else:
                    steps, summary = self.parse_planning(chunk)
                    if steps:
                        self.task_queue.extend(steps)
                        self.total_tasks += len(steps)
                        self.progress_updated.emit(self.completed_count, self.total_tasks)
                        self.process_next_task()
                    if summary:
                        self.context_manager.add_message("System", f"Summary: {summary}")
            else:
                self.task_failed.emit("No response from LLM.")

    def parse_planning(self, planning_text):
        """
        Parses the planning response to extract subtasks and summary.
        Expects a numbered list of subtasks followed by a summary.
        """
        steps = re.findall(r'^\d+\.\s+(.*)', planning_text, re.MULTILINE)
        summary_match = re.search(r'Summary:\s*(.*)', planning_text, re.IGNORECASE)
        summary = summary_match.group(1).strip() if summary_match else ""
        return steps, summary

    def process_next_task(self):
        if self.current_task is not None:
            return
        if not self.task_queue:
            if not self.finalizing:
                self.finalize()
            return
        self.current_task = self.task_queue.pop(0)
        subtask = self.current_task
        task_prompt = (
            f"Please execute the following task in one step and provide the result. "
            f"Use '__END__' to mark the end of your response.\n\n{subtask}\n\nResult:"
        )
        # Reset context for subtask processing
        self.context_manager.clear_context()
        self.context_manager.add_message("System", f"Executing subtask: {subtask}")
        # Assuming processing uses the Processing LLM
        self.ollama.set_selected_llm(self.get_processing_llm())
        self.ollama.send_request("/api/generate", {
            "model": self.get_processing_llm(),
            "prompt": task_prompt,
            "options": {
                "temperature": 0.5,
                "max_new_tokens": 1500,
                "stream": False,  # No streaming for subtasks
                "format": "json"
            }
        })
        self.start_typing_animation()

    def append_subtask_result(self, result):
        """
        Append the result of a subtask, verify goals, and save if appropriate.
        """
        # Append result to completed tasks
        self.completed_tasks.append(self.current_task)
        self.message_sent.emit("System", f"Task '{self.current_task}' completed with result: {result}", "info")
        self.completed_count += 1
        self.progress_updated.emit(self.completed_count, self.total_tasks)
        self.current_task = None
        self.process_next_task()

    def finalize(self):
        if self.finalizing:
            return
        self.finalizing = True
        results = "\n".join([f"{i+1}. {task}" for i, task in enumerate(self.completed_tasks)])
        verification_prompt = (
            f"Please verify that the following tasks have achieved all their goals. "
            f"Respond with 'Ok to save' if all goals are met, or provide detailed feedback otherwise."
            f"\n\nTasks:\n{results}"
            f"\n\nVerification Result:"
        )
        # Assuming verification uses the Final Answer LLM
        self.ollama.set_selected_llm(self.get_final_llm())
        self.ollama.send_request("/api/generate", {
            "model": self.get_final_llm(),
            "prompt": verification_prompt,
            "options": {
                "temperature": 0.3,
                "max_new_tokens": 500,
                "stream": False,  # No streaming for verification
                "format": "json"
            }
        })
        self.start_typing_animation()

    def save_summaries(self):
        # Summarize the results
        summary_prompt = (
            f"Please summarize the results of the following tasks without losing critical information:\n\n"
            f"{chr(10).join([f'Task {i+1}: {task}' for i, task in enumerate(self.completed_tasks)])}\n\nSummary:"
        )
        # Assuming summarization uses the Final Answer LLM
        self.ollama.set_selected_llm(self.get_final_llm())
        self.ollama.send_request("/api/generate", {
            "model": self.get_final_llm(),
            "prompt": summary_prompt,
            "options": {
                "temperature": 0.5,
                "max_new_tokens": 1000,
                "stream": False,  # No streaming for summarization
                "format": "json"
            }
        })
        self.start_typing_animation()

    def handle_verification_response(self, response):
        if "response" in response:
            verification_result = response["response"].strip()
            if verification_result.lower() == "ok to save":
                # Proceed to save summarized results
                self.save_summaries()
            else:
                self.task_failed.emit(f"Verification failed: {verification_result}")
        else:
            self.task_failed.emit("No verification response from LLM.")
        self.stop_typing_animation()

    def handle_summarization_response(self, response):
        if "response" in response:
            summary = response["response"].strip()
            if summary:
                self.context_manager.add_message("System", f"Summary: {summary}")
                self.message_sent.emit("System", "Ok to save", "info")
                self.final_answer_ready.emit("All tasks verified and summaries saved.")
        else:
            self.task_failed.emit("No summarization response from LLM.")
        self.stop_typing_animation()

    def on_error(self, error_message):
        if self.finalizing:
            # Handle verification or summarization errors
            if "save" in error_message.lower():
                self.task_failed.emit(f"Error during saving summaries: {error_message}")
        elif self.current_task:
            self.failed_tasks.append(self.current_task)
            self.completed_count += 1
            self.progress_updated.emit(self.completed_count, self.total_tasks)
            self.task_failed.emit(f"Error in task '{self.current_task}': {error_message}")
            self.current_task = None
            self.process_next_task()
        else:
            self.task_failed.emit(f"Error: {error_message}")

    def interrupt_processing(self):
        try:
            self.ollama.interrupt_event.set()
            self.message_sent.emit("System", "LLM processing has been interrupted.", "warning")
            logging.info("LLM processing has been interrupted.")
        except Exception as e:
            self.message_sent.emit("System", f"Error interrupting process: {e}", "error")
            logging.error(f"Error interrupting process: {e}")

    # Helper methods to get selected LLMs
    def get_planning_llm(self):
        return self.planning_llm_combo.currentText()

    def get_processing_llm(self):
        return self.processing_llm_combo.currentText()

    def get_final_llm(self):
        return self.final_llm_combo.currentText()

# Context Manager
class ContextManager(QObject):
    summarization_requested = pyqtSignal(str)

    def __init__(self, ollama_controller):
        super().__init__()
        self.db = DatabaseManager()
        self.summary = ""
        self.ollama = ollama_controller
        self.ollama.summarization_received.connect(self.handle_summarization_response)
        self.summarization_requested.connect(self.summarize_context)
        self.summarize_request_event = threading.Event()
        self.summarization_result = ""
        self.lock = threading.Lock()

    def add_message(self, sender, message):
        self.db.add_message(sender, message)
        self.check_and_summarize_context()

    def get_context(self):
        with self.lock:
            if self.summary:
                return self.summary + "\n" + self.db.get_context()
            return self.db.get_context()

    def clear_context(self):
        self.db.clear_context()
        with self.lock:
            self.summary = ""

    def check_and_summarize_context(self):
        context = self.db.get_context()
        if len(context) > self.db.max_context_length * 0.8:
            self.summarization_requested.emit(context)

    def summarize_context(self, context_text):
        summary_prompt = (
            "Please summarize the following context to make room for new information. "
            "Keep all important details and key points:\n\n"
            f"{context_text}"
        )
        self.summarization_result = ""
        self.summarize_request_event.clear()

        self.ollama.send_request("/api/generate", {
            "model": self.ollama.selected_llm,
            "prompt": summary_prompt,
            "options": {
                "temperature": 0.5,
                "max_new_tokens": 1000,
                "stream": False,
                "format": "json"
            }
        })

        threading.Thread(target=self.wait_for_summary, daemon=True).start()

    def wait_for_summary(self):
        if self.summarize_request_event.wait(timeout=60):
            if self.summarization_result:
                with self.lock:
                    self.summary = self.summarization_result
                self.db.clear_context()
                self.db.add_message("System", f"Summary of previous context:\n{self.summary}")
        else:
            logging.error("Timeout during context summarization.")
            self.summarize_request_event.set()

    def handle_summarization_response(self, response):
        if "response" in response:
            summary = response["response"].strip()
            if summary:
                with self.lock:
                    self.summarization_result = summary
        else:
            logging.error("No response received for summarization.")
        self.summarize_request_event.set()

# Settings Dialog
class SettingsDialog(QDialog):
    settings_updated = pyqtSignal(dict)

    def __init__(self, current_settings, parent=None):
        super().__init__(parent)
        self.setWindowTitle("Settings")
        self.current_settings = current_settings
        self.init_ui()

    def init_ui(self):
        layout = QFormLayout()

        # Temperature Slider
        self.temperature_slider = QSlider(Qt.Horizontal)
        self.temperature_slider.setRange(0, 100)
        self.temperature_slider.setValue(int(self.current_settings.get("temperature", 0.5) * 100))
        self.temperature_slider.valueChanged.connect(self.update_temperature_label)
        self.temperature_label = QLabel(f"{self.current_settings.get('temperature', 0.5):.2f}")
        temp_layout = QHBoxLayout()
        temp_layout.addWidget(self.temperature_slider)
        temp_layout.addWidget(self.temperature_label)
        layout.addRow("Temperature:", temp_layout)

        # Max Tokens Slider
        self.output_slider = QSlider(Qt.Horizontal)
        self.output_slider.setRange(100, 2000)
        self.output_slider.setValue(self.current_settings.get("max_new_tokens", 1500))
        self.output_slider.valueChanged.connect(self.update_output_label)
        self.output_label = QLabel(str(self.current_settings.get("max_new_tokens", 1500)))
        output_layout = QHBoxLayout()
        output_layout.addWidget(self.output_slider)
        output_layout.addWidget(self.output_label)
        layout.addRow("Max Tokens:", output_layout)

        # Additional options
        self.additional_options_box = QLabel("Additional Options (JSON):")
        self.additional_options_input = QLineEdit()
        self.additional_options_input.setPlaceholderText('e.g., "top_p": 0.9, "frequency_penalty": 1.0')
        layout.addRow(self.additional_options_box, self.additional_options_input)

        # Save and Cancel Buttons
        buttons_layout = QHBoxLayout()
        self.save_button = QPushButton("Save")
        self.save_button.clicked.connect(self.save_settings)
        self.cancel_button = QPushButton("Cancel")
        self.cancel_button.clicked.connect(self.reject)
        buttons_layout.addWidget(self.save_button)
        buttons_layout.addWidget(self.cancel_button)
        layout.addRow(buttons_layout)

        self.setLayout(layout)

    def update_temperature_label(self, value):
        temperature = value / 100
        self.temperature_label.setText(f"{temperature:.2f}")

    def update_output_label(self, value):
        self.output_label.setText(str(value))

    def save_settings(self):
        try:
            additional_text = self.additional_options_input.text().strip()
            additional_options = {}
            if additional_text:
                additional_options = json.loads("{" + additional_text + "}")
            temperature = self.temperature_slider.value() / 100
            output_length = self.output_slider.value()
            settings = {
                "temperature": temperature,
                "max_new_tokens": output_length,
                "additional_options": additional_options
            }
            self.settings_updated.emit(settings)
            self.accept()
        except json.JSONDecodeError:
            QMessageBox.warning(self, "Invalid JSON", "Please enter valid JSON for additional options.")

# Connection Widget to draw lines between tasks based on dependencies
class ConnectionWidget(QWidget):
    def __init__(self, task_widgets):
        super().__init__()
        self.task_widgets = task_widgets  # Dictionary mapping task_name to TaskWidget
        self.setMinimumWidth(400)  # Adjust as needed

    def update_connections(self):
        self.update()  # Trigger paintEvent

    def paintEvent(self, event):
        painter = QPainter(self)
        pen = QPen(QColor("white"), 2, Qt.SolidLine)
        painter.setPen(pen)

        # Example: Traverse through all tasks and draw lines from dependencies
        for task_name, widget in self.task_widgets.items():
            for dep in widget.dependencies:
                if dep in self.task_widgets:
                    start_widget = self.task_widgets[dep]
                    end_widget = widget
                    # Calculate positions relative to the ConnectionWidget
                    parent_widget = self.parent()
                    if parent_widget:
                        # Map widget positions to the coordinate system of the ConnectionWidget
                        start_pos = parent_widget.mapTo(self, start_widget.pos()) + start_widget.rect().bottomRight()
                        end_pos = parent_widget.mapTo(self, end_widget.pos()) + end_widget.rect().topLeft()
                        painter.drawLine(start_pos, end_pos)

        painter.end()

# AutoLLM App
class AutoLLMApp(QWidget):
    loading_signal = pyqtSignal(bool)  # Loading signal for process indication

    def __init__(self):
        super().__init__()
        self.setWindowTitle("AutoLLM")
        self.resize(1600, 1000)  # Larger window size for better overview
        self.ollama = OllamaController()
        self.context_manager = ContextManager(self.ollama)
        self.current_parameters = {"temperature": 0.5, "max_new_tokens": 1500, "additional_options": {}}

        # Initialize UI
        self.init_ui()
        self.ollama.start_server()
        self.enable_ui(False)

        # Task Manager
        self.task_manager = TaskManager(self.ollama, self.context_manager)
        self.task_manager.task_completed.connect(self.on_task_completed_step)
        self.task_manager.task_failed.connect(self.on_task_failed_step)
        self.task_manager.progress_updated.connect(self.update_progress)
        self.task_manager.final_answer_ready.connect(self.display_final_answer)
        self.task_manager.message_sent.connect(self.display_message)

        self.ollama.server_status_changed.connect(self.update_server_status)
        self.ollama.error_occurred.connect(self.show_error)
        self.ollama.llm_list_updated.connect(self.update_llm_list)

        # Initialize Code Highlighter
        self.highlighter = CodeHighlighter(self.chat_area.document())

        # For blinking "..."
        self.blink_timer = QTimer()
        self.blink_timer.timeout.connect(self.toggle_ellipsis)
        self.ellipsis_visible = True

        # Connect message_sent signal
        self.task_manager.message_sent.connect(self.display_message)

    def init_ui(self):
        main_layout = QHBoxLayout()

        left_layout = QVBoxLayout()

        # Chat area with scrollbar using QTextEdit for HTML support
        self.chat_area = QTextEdit()
        self.chat_area.setReadOnly(True)
        self.chat_area.setFont(QFont("Courier", 10))
        self.chat_area.setStyleSheet("""
            QTextEdit {
                background-color: #2b2b2b;
                color: #ffffff;
                border: 1px solid #5c5c5c;
                padding: 5px;
            }
        """)

        chat_scroll_area = QScrollArea()
        chat_scroll_area.setWidgetResizable(True)
        chat_scroll_area.setWidget(self.chat_area)
        left_layout.addWidget(chat_scroll_area)

        # Input field and send button
        input_layout = QHBoxLayout()
        self.input_field = QLineEdit()
        self.input_field.setPlaceholderText("Enter your message here...")
        self.input_field.returnPressed.connect(self.send_message)
        self.send_button = QPushButton("Send")
        self.send_button.clicked.connect(self.send_message)
        input_layout.addWidget(self.input_field)
        input_layout.addWidget(self.send_button)
        left_layout.addLayout(input_layout)

        # Interrupt button
        self.interrupt_button = QPushButton("Interrupt")
        self.interrupt_button.clicked.connect(self.interrupt_processing)
        left_layout.addWidget(self.interrupt_button)

        # Server monitoring area
        server_layout = QHBoxLayout()
        self.server_status = QLabel("🔴 Server Stopped")
        self.server_button = QPushButton("Start Server")
        self.server_button.clicked.connect(self.toggle_server)
        server_layout.addWidget(self.server_status)
        server_layout.addWidget(self.server_button)
        left_layout.addLayout(server_layout)

        # LLM selection for Planning, Processing, and Final Answer
        llm_selection_layout = QFormLayout()

        self.planning_llm_combo = QComboBox()
        self.planning_llm_combo.setEnabled(False)
        self.planning_llm_combo.setToolTip("Select LLM for Planning")
        llm_selection_layout.addRow("Planning LLM:", self.planning_llm_combo)

        self.processing_llm_combo = QComboBox()
        self.processing_llm_combo.setEnabled(False)
        self.processing_llm_combo.setToolTip("Select LLM for Processing Subtasks")
        llm_selection_layout.addRow("Processing LLM:", self.processing_llm_combo)

        self.final_llm_combo = QComboBox()
        self.final_llm_combo.setEnabled(False)
        self.final_llm_combo.setToolTip("Select LLM for Final Answer")
        llm_selection_layout.addRow("Final Answer LLM:", self.final_llm_combo)

        left_layout.addLayout(llm_selection_layout)

        # Settings button
        self.settings_button = QPushButton("Settings")
        self.settings_button.clicked.connect(self.open_settings)
        left_layout.addWidget(self.settings_button)

        # Progress area
        progress_layout = QVBoxLayout()
        self.progress_bar_planning = QProgressBar()
        self.progress_bar_planning.setValue(0)
        self.progress_label_planning = QLabel("Planning Progress:")
        self.progress_bar_processing = QProgressBar()
        self.progress_bar_processing.setValue(0)
        self.progress_label_processing = QLabel("Processing Progress:")
        progress_layout.addWidget(self.progress_label_planning)
        progress_layout.addWidget(self.progress_bar_planning)
        progress_layout.addWidget(self.progress_label_processing)
        progress_layout.addWidget(self.progress_bar_processing)
        left_layout.addLayout(progress_layout)

        # Plan and steps area with TaskWidget
        self.task_area = QVBoxLayout()
        self.task_area.setSpacing(10)
        self.task_area.setContentsMargins(10, 10, 10, 10)

        self.task_widgets = {}  # Mapping from task_name to TaskWidget

        self.task_scroll_area = QScrollArea()
        self.task_scroll_area.setWidgetResizable(True)
        task_container = QWidget()
        task_container.setLayout(self.task_area)
        self.task_scroll_area.setWidget(task_container)
        left_layout.addWidget(self.task_scroll_area)

        main_layout.addLayout(left_layout, 3)

        # Right layout for visual connections between tasks
        right_layout = QVBoxLayout()
        self.connection_widget = ConnectionWidget(self.task_widgets)
        self.connection_widget.setMinimumWidth(400)
        right_layout.addWidget(self.connection_widget)
        main_layout.addLayout(right_layout, 1)

        # Add loading indicator to the main layout
        self.loading_indicator = QLabel("Loading...")
        self.loading_indicator.setVisible(False)  # Initially hidden
        self.loading_indicator.setStyleSheet("color: yellow; font-weight: bold;")
        main_layout.addWidget(self.loading_indicator)

        self.setLayout(main_layout)

    def classify_message(self, message):
        """
        Determine if the message is simple or requires planning.
        Returns 'simple' or 'complex'.
        """
        # Simple heuristic: if message length is short or contains certain keywords
        simple_keywords = ['hello', 'hi', 'whats up', 'how are you', 'bye']
        if len(message) < 20:
            for keyword in simple_keywords:
                if keyword.lower() in message.lower():
                    return 'simple'
        return 'complex'

    def send_message(self):
        user_message = self.input_field.text().strip()
        if user_message:
            self.context_manager.add_message("User", user_message)
            self.append_message("User", user_message)
            self.input_field.clear()
            
            message_type = self.classify_message(user_message)
            if message_type == 'simple':
                self.handle_simple_message(user_message)
            else:
                self.task_manager.add_task(user_message)
                self.start_typing_animation()  # Start loading indicator

    def handle_simple_message(self, message):
        """
        Handle simple user messages with direct LLM response.
        """
        direct_prompt = (
            f"Respond to the user's message in a clear and concise manner.\n\nUser: {message}\n\nResponse:"
        )
        # Assuming simple messages use the Processing LLM
        self.ollama.set_selected_llm(self.get_processing_llm())
        self.ollama.send_request("/api/generate", {
            "model": self.get_processing_llm(),
            "prompt": direct_prompt,
            "options": {
                "temperature": 0.5,
                "max_new_tokens": 500,
                "stream": False,  # No streaming for simple messages
                "format": "json"
            }
        })
        self.start_typing_animation()  # Start loading indicator
        self.ollama.response_received.connect(self.handle_simple_response)

    def handle_simple_response(self, response):
        if "response" in response:
            reply = response["response"].strip()
            self.append_message("LLM", reply)
        else:
            self.append_message("System", "No response from LLM.", message_type="error")
        self.stop_typing_animation()  # Stop loading indicator
        # Disconnect to prevent multiple connections
        self.ollama.response_received.disconnect(self.handle_simple_response)

    def append_message(self, sender, message, message_type="normal"):
        color = "gray"
        if sender == "User":
            color = "#00ccff"  # Change color for better visibility
            prefix = "<b>User:</b> "
        elif sender == "LLM":
            color = "green"
            prefix = "<b>LLM:</b> "
        else:
            prefix = "<b>System:</b> "

        formatted_message = self.format_message(message)
        html_message = f'<p style="color: {color};">{prefix}{formatted_message}</p>'
        self.chat_area.insertHtml(html_message + "<br>")
        self.chat_area.moveCursor(QTextCursor.End)

    def format_message(self, message):
        message = re.sub(r'(https?://\S+)', r'<a href="\1">\1</a>', message)
        message = re.sub(
            r'```(.*?)```',
            r'<pre style="background-color:#f0f0f0; padding:5px; font-family: monospace; white-space: pre-wrap;"><code>\1</code></pre>',
            message,
            flags=re.DOTALL | re.MULTILINE
        )
        return message

    def display_message(self, sender, message, message_type="normal"):
        self.append_message(sender, message, message_type)

    def interrupt_processing(self):
        self.task_manager.interrupt_processing()
        self.display_message("System", "LLM processing has been interrupted.", "warning")
        self.stop_typing_animation()  # Stop loading animation on interrupt

    def toggle_server(self):
        if self.ollama.is_server_running():
            reply = QMessageBox.question(self, 'Stop Server',
                                         'Are you sure you want to stop the Ollama server?',
                                         QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
            if reply == QMessageBox.Yes:
                self.ollama.stop_server()
        else:
            self.ollama.start_server()

    def open_settings(self):
        settings_dialog = SettingsDialog(self.current_parameters, self)
        settings_dialog.settings_updated.connect(self.update_parameters)
        settings_dialog.exec_()

    def update_parameters(self, params):
        self.current_parameters = params
        QMessageBox.information(self, "Settings Updated", "LLM parameters have been updated.")

    def on_task_completed_step(self, result):
        self.append_message("LLM", f"Result: {result}")
        self.task_manager.completed_tasks.append(self.task_manager.current_task)
        self.task_manager.completed_count += 1
        self.update_progress(self.task_manager.completed_count, self.task_manager.total_tasks)
        self.task_manager.current_task = None
        self.task_manager.process_next_task()
        self.stop_typing_animation()  # Stop loading indicator
        self.update_plan_list()

    def on_task_failed_step(self, error):
        self.append_message("System", f"Error: {error}", message_type="error")
        self.task_manager.failed_tasks.append(self.task_manager.current_task)
        self.task_manager.completed_count += 1
        self.update_progress(self.task_manager.completed_count, self.task_manager.total_tasks)
        self.task_manager.current_task = None
        self.task_manager.process_next_task()
        self.stop_typing_animation()  # Stop loading indicator
        self.update_plan_list()

    def update_progress(self, completed, total):
        if total == 0:
            self.progress_bar_planning.setValue(0)
            self.progress_bar_processing.setValue(0)
        else:
            planning_progress = int((completed / total) * 100)
            processing_progress = planning_progress  # Adjust as needed

            self.progress_bar_planning.setValue(planning_progress)
            self.progress_bar_processing.setValue(processing_progress)
        self.connection_widget.update_connections()

    def update_plan_list(self):
        self.clear_task_area()
        # Display completed tasks
        for task in self.task_manager.completed_tasks:
            widget = TaskWidget(task, goal="Completed", status="Completed")
            self.task_area.addWidget(widget)
            self.task_widgets[task] = widget
        # Display current task
        if self.task_manager.current_task:
            widget = TaskWidget(self.task_manager.current_task, goal="In Progress", status="Processing")
            self.task_area.addWidget(widget)
            self.task_widgets[self.task_manager.current_task] = widget
        # Display queued tasks
        for task in self.task_manager.task_queue:
            widget = TaskWidget(task, goal="Planned", status="Pending")
            self.task_area.addWidget(widget)
            self.task_widgets[task] = widget
        # Display failed tasks
        for task in self.task_manager.failed_tasks:
            widget = TaskWidget(task, goal="Failed", status="Error")
            self.task_area.addWidget(widget)
            self.task_widgets[task] = widget

        # Update connections
        self.connection_widget.update_connections()

    def clear_task_area(self):
        while self.task_area.count():
            child = self.task_area.takeAt(0)
            if child.widget():
                child.widget().deleteLater()

    def display_final_answer(self, answer):
        self.append_message("LLM", f"Final Answer: {answer}")
        self.stop_typing_animation()  # Stop loading indicator

    # Helper methods to get selected LLMs
    def get_planning_llm(self):
        return self.planning_llm_combo.currentText()

    def get_processing_llm(self):
        return self.processing_llm_combo.currentText()

    def get_final_llm(self):
        return self.final_llm_combo.currentText()

# Connection Widget to draw lines between tasks based on dependencies
class ConnectionWidget(QWidget):
    def __init__(self, task_widgets):
        super().__init__()
        self.task_widgets = task_widgets  # Dictionary mapping task_name to TaskWidget
        self.setMinimumWidth(400)  # Adjust as needed

    def update_connections(self):
        self.update()  # Trigger paintEvent

    def paintEvent(self, event):
        painter = QPainter(self)
        pen = QPen(QColor("white"), 2, Qt.SolidLine)
        painter.setPen(pen)

        # Example: Traverse through all tasks and draw lines from dependencies
        for task_name, widget in self.task_widgets.items():
            for dep in widget.dependencies:
                if dep in self.task_widgets:
                    start_widget = self.task_widgets[dep]
                    end_widget = widget
                    # Calculate positions relative to the ConnectionWidget
                    parent_widget = self.parent()
                    if parent_widget:
                        # Map widget positions to the coordinate system of the ConnectionWidget
                        start_pos = parent_widget.mapTo(self, start_widget.pos()) + start_widget.rect().bottomRight()
                        end_pos = parent_widget.mapTo(self, end_widget.pos()) + end_widget.rect().topLeft()
                        painter.drawLine(start_pos, end_pos)

        painter.end()

# Main Execution
if __name__ == "__main__":
    app = QApplication(sys.argv)
    app.setStyleSheet("""
        QWidget {
            background-color: #2b2b2b;
            color: #ffffff;
        }
        QPushButton {
            background-color: #3c3f41;
            border: none;
            padding: 5px;
            color: #ffffff;
            border-radius: 3px;
        }
        QPushButton:hover {
            background-color: #4b4e52;
        }
        QLineEdit {
            background-color: #3c3f41;
            border: 1px solid #5c5c5c;
            padding: 5px;
            color: #ffffff;
        }
        QTextEdit {
            background-color: #2b2b2b;
            color: #ffffff;
            padding: 5px;
        }
        QTextBrowser {
            background-color: #2b2b2b;
            border: 1px solid #5c5c5c;
            padding: 5px;
            color: #ffffff;
        }
        QComboBox {
            background-color: #3c3f41;
            border: 1px solid #5c5c5c;
            padding: 5px;
            color: #ffffff;
        }
        QLabel {
            color: #ffffff;
        }
        QListWidget {
            background-color: #2b2b2b;
            border: 1px solid #5c5c5c;
            color: #ffffff;
        }
        QProgressBar {
            border: 1px solid #5c5c5c;
            text-align: center;
            color: #ffffff;
        }
        QProgressBar::chunk {
            background-color: #4caf50;
        }
    """)
    window = AutoLLMApp()
    window.show()
    sys.exit(app.exec_())