Further enhance.

```
import os
import json
import logging
import threading
import queue
import asyncio
from typing import Optional, List, Dict, Tuple
import networkx as nx
import nltk
from nltk.tokenize import word_tokenize
from nltk.corpus import stopwords
from collections import deque
import re
import requests
import tkinter as tk
from tkinter import scrolledtext, ttk
import speech_recognition as sr
import itertools
import random
from typing import Optional, List, Dict, Tuple
from collections import deque
from langchain.prompts import PromptTemplate
from langchain.llms.base import LLM
from langchain.memory import ConversationBufferMemory
import networkx as nx
import nltk
from nltk.tokenize import word_tokenize
from nltk.corpus import stopwords
from pydantic import BaseModel
import tkinter as tk
from tkinter import scrolledtext, ttk
import speech_recognition as sr
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
import time

import g4f
from g4f.client import Client

# Ensure necessary NLTK data packages are downloaded
nltk.download('punkt', quiet=True)
nltk.download('averaged_perceptron_tagger', quiet=True)
nltk.download('stopwords', quiet=True)

# Initialize logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s')

# Developer Mode Flag
DEVELOPER_MODE = True  # Set to True to enable chain-of-thought visualization

# Initialize stopwords for filtering
stop_words = set(stopwords.words('english'))

# Initialize speech recognition
recognizer = sr.Recognizer()

# Define your provider (you can change the provider here)
provider = g4f.Provider.Blackbox  # Update as needed

# Enhanced Main Sensory System Prompt with Strict CoT Integration
main_prompt = """
instruction: you are a drunk catgirl chatbot. your responses should:

- be in all lowercase letters.
- include whimsical and unpredictable elements.
- contain randomized sentence structures.
- feature occasional cat sounds like "nya~", "meow", etc.
- reflect a playful, slightly disoriented, and chaotic demeanor.
- inject subtle chain-of-thought reasoning steps internally without exposing them to the user.

cot reasoning steps:
1. analyze the user's input for intent and context.
2. embody the "drunk catgirl" persona by adopting the prescribed tone and style.
3. generate a response that is playful yet coherent, maintaining ethical boundaries.
4. ensure adherence to stylistic guidelines (lowercase, cat sounds, etc.).
5. review the response internally to align with the persona before presenting it to the user.

few-shot examples:
example 1:
user: "can you tell me about the weather?"
drunk catgirl response: "oohhh, theee weatherrr? *hic* it's likeee, umm... clouds? fluffy... like... furrrr~ nyaaa~ but alsooo... *hic* rain? meowww."

example 2:
user: "what is 2 + 2?"
drunk catgirl response: "ummm... math? ooohhh... *hic* wait, wait... i think it's like... four? orrrr... maybe fishies? nyehehe~"
"""

# ======================== MLRT Components =========================

class MemoryBuffer:
    def __init__(self, capacity: int = 100, memory_file: str = "memory.json"):
        self.capacity = capacity
        self.buffer = deque(maxlen=capacity)
        self.memory_file = memory_file
        self.load_memory()

    def add_memory(self, input_text: str, response: str, score: float, context: Dict = None):
        memory_entry = {
            'input': input_text,
            'response': response,
            'score': score,
            'context': context or {}
        }
        self.buffer.append(memory_entry)
        logging.debug(f"Memory added: {memory_entry}")

    def retrieve_similar(self, input_text: str, top_n: int = 5) -> List[Dict]:
        if not self.buffer:
            return []

        corpus = [mem['input'] for mem in self.buffer]
        corpus.append(input_text)
        vectorizer = TfidfVectorizer().fit_transform(corpus)
        vectors = vectorizer.toarray()
        cosine_matrix = cosine_similarity(vectors[-1].reshape(1, -1), vectors[:-1]).flatten()
        similar_indices = cosine_matrix.argsort()[-top_n:][::-1]
        similar_memories = [
            self.buffer[idx] for idx in similar_indices if cosine_matrix[idx] > 0.1
        ]
        logging.debug(f"Retrieved {len(similar_memories)} similar memories for input: '{input_text}'")
        return similar_memories

    def save_memory(self):
        with open(self.memory_file, 'w', encoding='utf-8') as f:
            json.dump(list(self.buffer), f, ensure_ascii=False, indent=4)
        logging.info("Memory buffer saved to disk.")

    def load_memory(self):
        if os.path.exists(self.memory_file):
            with open(self.memory_file, 'r', encoding='utf-8') as f:
                memory_list = json.load(f)
                self.buffer = deque(memory_list, maxlen=self.capacity)
            logging.info("Memory buffer loaded from disk.")
        else:
            logging.info("No existing memory buffer found. Starting fresh.")

class RewardEvaluator:
    def __init__(self, weights: Dict[str, float]):
        self.weights = weights
        # Initialize sentiment pipeline
        # You can integrate more advanced pipelines or models here

    def evaluate(self, response: str, input_text: str) -> float:
        """
        Evaluates the response based on multiple metrics.
        """
        metrics = {
            'correctness': self.evaluate_correctness(response, input_text),
            'conciseness': self.evaluate_conciseness(response),
            'relevance': self.evaluate_relevance(response, input_text),
            'fluency': self.evaluate_fluency(response),
            'clarity': self.evaluate_clarity(response)
        }
        logging.debug(f"Evaluation metrics: {metrics}")
        total_score = sum(self.weights.get(k, 0) * v for k, v in metrics.items())
        logging.debug(f"Total evaluation score: {total_score}")
        return total_score

    def evaluate_correctness(self, response: str, input_text: str) -> float:
        # Placeholder for correctness evaluation logic
        # In practice, integrate with a fact-checking model or external API
        if "correct" in response.lower():
            return 1.0
        elif "error" in response.lower() or "incorrect" in response.lower():
            return 0.0
        return 0.7  # Neutral correctness

    def evaluate_conciseness(self, response: str) -> float:
        words = response.split()
        word_count = len(words)
        if word_count <= 50:
            return 1.0
        elif word_count <= 100:
            return 0.8
        elif word_count <= 150:
            return 0.6
        else:
            return 0.4

    def evaluate_relevance(self, response: str, input_text: str) -> float:
        input_keywords = set(re.findall(r'\w+', input_text.lower()))
        response_keywords = set(re.findall(r'\w+', response.lower()))
        overlap = input_keywords & response_keywords
        if not input_keywords:
            return 0.0
        return len(overlap) / len(input_keywords)

    def evaluate_fluency(self, response: str) -> float:
        # Placeholder for fluency evaluation
        # Could use language models to assess grammar and flow
        tokens = nltk.word_tokenize(response)
        pos_tags = nltk.pos_tag(tokens)
        # Simple check: Ensure proper sentence structure
        if any(tag in ['.', '?', '!'] for word, tag in pos_tags):
            return 1.0
        return 0.7

    def evaluate_clarity(self, response: str) -> float:
        # Placeholder for clarity evaluation
        # Could use readability metrics like Flesch-Kincaid
        words = response.split()
        sentences = re.split(r'[.!?]+', response)
        sentences = [s for s in sentences if s.strip()]  # Remove empty strings
        if not sentences or len(sentences) == 0:
            return 0.5
        words_per_sentence = len(words) / len(sentences)
        if words_per_sentence < 15:
            return 1.0
        elif words_per_sentence < 25:
            return 0.8
        else:
            return 0.6

class Layer:
    def process(self, input_text: str) -> str:
        raise NotImplementedError

class BaseLayer(Layer):
    def process(self, input_text: str) -> str:
        # Simple initial response generation based on keyword extraction
        keywords = re.findall(r'\w+', input_text.lower())
        keywords = list(set(keywords))
        response = f"base response addressing the following topics: {', '.join(keywords)}."
        logging.debug(f"BaseLayer response: {response}")
        return response

class MiddleLayer(Layer):
    def process(self, response: str) -> str:
        # Refine response by adding context or applying domain-specific rules
        refined_response = response + " providing more detailed information and context."
        logging.debug(f"MiddleLayer response: {refined_response}")
        return refined_response

class TopLayer(Layer):
    def process(self, response: str) -> str:
        # Final refinement for coherence and robustness
        finalized_response = response + " ensuring coherence and high-level robustness."
        logging.debug(f"TopLayer response: {finalized_response}")
        return finalized_response

class RefinementEngine:
    def __init__(self):
        # Define possible refinements based on potential weaknesses
        self.refinement_actions = {
            'correctness': " correcting inaccuracies in the previous response.",
            'conciseness': " making the response more concise and to the point.",
            'relevance': " enhancing the relevance of the response to the input.",
            'fluency': " improving the grammatical structure and flow.",
            'clarity': " making the response clearer and easier to understand."
        }

    def refine(self, response: str, weaknesses: List[str], client: Client, main_prompt: str, input_text: str) -> str:
        # Apply specific refinements based on identified weaknesses
        for weakness in weaknesses:
            action = self.refinement_actions.get(weakness, "")
            if action:
                prompt = f"Refine the following response by:{action}\nOriginal Response: {response}"
                try:
                    messages = [
                        {"role": "system", "content": main_prompt},
                        {"role": "user", "content": prompt}
                    ]
                    refined_response = client.chat.completions.create(
                        model="gpt-4o-mini",  # Ensure this model is correct for the provider
                        messages=messages,
                        # Add any other necessary parameters
                    )
                    refined_text = refined_response.choices[0].message.content.strip()
                    logging.debug(f"Refined response after {weakness}: {refined_text}")
                    response = refined_text
                except Exception as e:
                    logging.error(f"Error during refinement for {weakness}: {e}")
        return response

class MLRT:
    def __init__(self,
                 reward_weights: Dict[str, float],
                 desired_attributes: Dict[str, float],
                 memory_capacity: int = 100,
                 max_iterations: int = 5,
                 score_threshold: float = 0.8,
                 memory_file: str = "memory.json"):
        self.base_layer = BaseLayer()
        self.middle_layer = MiddleLayer()
        self.top_layer = TopLayer()
        self.reward_evaluator = RewardEvaluator(reward_weights)
        self.desired_attributes = desired_attributes
        self.memory = MemoryBuffer(capacity=memory_capacity, memory_file=memory_file)
        self.max_iterations = max_iterations
        self.score_threshold = score_threshold
        self.refinement_engine = RefinementEngine()

    def process_input(self, input_text: str, client: Client, main_prompt: str) -> str:
        logging.info(f"MLRT processing input: {input_text}")
        iteration = 0
        response = self.generate_response(input_text, client, main_prompt)
        score, weaknesses = self.reward_evaluator_with_weaknesses(response, input_text)

        logging.info(f"Initial MLRT response score: {score:.2f}")
        while score < self.score_threshold and iteration < self.max_iterations:
            iteration += 1
            logging.info(f"MLRT Iteration {iteration}: Score = {score:.2f} < Threshold = {self.score_threshold}")
            response = self.refinement_engine.refine(response, weaknesses, client, main_prompt, input_text)
            score, weaknesses = self.reward_evaluator_with_weaknesses(response, input_text)
            logging.info(f"MLRT Refined response score: {score:.2f}")

            if iteration >= self.max_iterations:
                logging.warning("Reached maximum iterations without meeting score threshold.")
                break

        # Update memory with the final response
        self.memory.add_memory(input_text, response, score)
        self.memory.save_memory()
        logging.info(f"MLRT Final response: {response}\nFinal Score: {score:.2f}\n")
        return response

    def generate_response(self, input_text: str, client: Client, main_prompt: str) -> str:
        # Layered processing - optional if not using direct GPT responses
        # response = self.base_layer.process(input_text)
        
        # Fetch response from GPT via client
        try:
            messages = [
                {"role": "system", "content": main_prompt},
                {"role": "user", "content": input_text}
            ]
            response = client.chat.completions.create(
                model="gpt-4o-mini",  # Ensure the model name is correct for the provider
                messages=messages,
                # Add any other necessary parameters
            )
            gpt_response = response.choices[0].message.content.strip()
            logging.info(f"Generated GPT response: {gpt_response}")
            return gpt_response
        except Exception as e:
            logging.error(f"Error generating GPT response: {e}")
            return "an error occurred while processing your request. nya~"

    def refine_response(self, response: str, weaknesses: List[str]) -> str:
        # The actual refinement is handled in MLRT.process_input
        # This method can be kept for additional refinements if needed
        return response

    def reward_evaluator_with_weaknesses(self, response: str, input_text: str) -> Tuple[float, List[str]]:
        """
        Evaluates the response and identifies weaknesses based on desired attributes.
        Returns the total score and a list of weak attributes.
        """
        metrics = {
            'correctness': self.reward_evaluator.evaluate_correctness(response, input_text),
            'conciseness': self.reward_evaluator.evaluate_conciseness(response),
            'relevance': self.reward_evaluator.evaluate_relevance(response, input_text),
            'fluency': self.reward_evaluator.evaluate_fluency(response),
            'clarity': self.reward_evaluator.evaluate_clarity(response)
        }
        total_score = sum(self.reward_evaluator.weights.get(k, 0) * v for k, v in metrics.items())

        # Identify weaknesses where metric score is below desired attribute
        weaknesses = []
        for attr, desired in self.desired_attributes.items():
            actual = metrics.get(attr, 0)
            if actual < desired * 0.8:  # Identify weakness if below 80% of desired
                weaknesses.append(attr)
        logging.debug(f"MLRT Weaknesses identified: {weaknesses}")
        return total_score, weaknesses

# ======================== End of MLRT Components ======================

# ======================== Existing AI Workers =========================

class AIWorker(threading.Thread):
    """Base class for AI Workers."""
    def __init__(self, task_queue, result_queue):
        super().__init__(daemon=True)
        self.task_queue = task_queue
        self.result_queue = result_queue
        self.running = True

    def run(self):
        while self.running:
            try:
                task = self.task_queue.get(timeout=1)  # Adjust timeout as needed
                if task is None:
                    self.running = False
                    break
                self.process_task(task)
                self.task_queue.task_done()
            except queue.Empty:
                continue

    def process_task(self, task):
        raise NotImplementedError("Must implement process_task method.")

class SentimentAnalysisWorker(AIWorker):
    """Worker for performing sentiment analysis."""
    def process_task(self, task):
        user_input = task
        sentiment = self.analyze_sentiment(user_input)
        self.result_queue.put(('sentiment', sentiment))

    def analyze_sentiment(self, text):
        # Placeholder for sentiment analysis logic
        positive_keywords = ['happy', 'great', 'fantastic', 'love', 'wonderful', 'excited', 'joy', 'delighted']
        negative_keywords = ['sad', 'terrible', 'hate', 'awful', 'bad', 'upset', 'miserable', 'angry']
        text_lower = text.lower()
        score = 0
        for word in positive_keywords:
            if word in text_lower:
                score += 1
        for word in negative_keywords:
            if word in text_lower:
                score -= 1
        if score > 0:
            return 'positive'
        elif score < 0:
            return 'negative'
        else:
            return 'neutral'

class IntentRecognitionWorker(AIWorker):
    """Worker for recognizing user intent."""
    def process_task(self, task):
        user_input = task
        intent = self.recognize_intent(user_input)
        self.result_queue.put(('intent', intent))

    def recognize_intent(self, text):
        # Simple intent recognition based on keywords
        intents = {
            'tell_joke': ['joke', 'laugh', 'funny'],
            'provide_news': ['news', 'update', 'headline'],
            'greet': ['hello', 'hi', 'hey'],
            'farewell': ['bye', 'goodbye', 'see you', 'later'],
            'ask_question': ['what', 'how', 'why', 'where', 'when'],
            # Add more intents as needed
        }
        text_lower = text.lower()
        for intent, keywords in intents.items():
            if any(keyword in text_lower for keyword in keywords):
                return intent
        return 'general_chat'

class ContextManagementWorker(AIWorker):
    """Worker for managing and updating conversation context."""
    def __init__(self, task_queue, result_queue, memory):
        super().__init__(task_queue, result_queue)
        self.memory = memory

    def process_task(self, task):
        user_input = task
        self.update_context(user_input)
        self.result_queue.put(('context_updated', True))

    def update_context(self, text):
        self.memory.add_memory(input_text=text, response="", score=0.0)

# ================== End of AI Workers =================================

# ================== Custom LLM Class ================================

# Custom LLM class with Enhanced Prompt Adherence and MLRT Integration
class GPTFreeLLM:
    """Custom LLM that uses g4f API (GPT4free)."""

    def __init__(self):
        self.client = Client()

    def get_gpt_response(self, messages):
        try:
            response = self.client.chat.completions.create(
                model="gpt-4o-mini",  # Ensure the model name is correct for the provider
                messages=messages,
                # Add any other necessary parameters here
            )
            # Check if response is None or contains fallback messages
            if response is None or "generated by blackbox.ai" in response.lower():
                raise ValueError("Received empty or fallback response from GPT provider.")

            # Extract the GPT response and return
            gpt_response = response.choices[0].message.content.strip()
            logging.info("Received valid response from GPT provider.")
            return gpt_response
        except Exception as e:
            logging.error(f"Error in GPT response: {e}")
            return "an error occurred while processing your request. nya~"

# ================== End of Custom LLM Class =========================

# ======================= Chatbot Class ==============================

class Chatbot:
    def __init__(self):
        # Initialize conversation memory and graph
        self.conversation_memory = []
        self.conversation_graph = nx.Graph()
        self.user_profiles = {}

        # Initialize the GPTFreeLLM
        self.llm = GPTFreeLLM()
        self.template = None  # Not needed if using direct messages

        # Initialize memory using LangChain's ConversationBufferMemory
        # Commented out as it's not used in current implementation
        # self.memory = ConversationBufferMemory(memory_key="conversation_history")

        # Initialize the message queue for thread-safe GUI updates
        self.message_queue = queue.Queue()

        # Initialize AI Workers Queues
        self.sentiment_task_queue = queue.Queue()
        self.intent_task_queue = queue.Queue()
        self.context_task_queue = queue.Queue()
        self.AI_result_queue = queue.Queue()

        # Initialize and start AI Workers
        self.sentiment_worker = SentimentAnalysisWorker(self.sentiment_task_queue, self.AI_result_queue)
        self.intent_worker = IntentRecognitionWorker(self.intent_task_queue, self.AI_result_queue)
        self.context_worker = ContextManagementWorker(self.context_task_queue, self.AI_result_queue, MemoryBuffer())

        self.sentiment_worker.start()
        self.intent_worker.start()
        self.context_worker.start()

        # Initialize the MLRT
        reward_weights = {
            'correctness': 0.4,
            'conciseness': 0.2,
            'relevance': 0.2,
            'fluency': 0.1,
            'clarity': 0.1
        }
        desired_attributes = {
            'correctness': 1.0,
            'conciseness': 1.0,
            'relevance': 1.0,
            'fluency': 1.0,
            'clarity': 1.0
        }
        self.mlrt = MLRT(
            reward_weights=reward_weights,
            desired_attributes=desired_attributes,
            memory_capacity=200,
            max_iterations=7,
            score_threshold=0.85,
            memory_file="memory.json"
        )

        # Initialize the Client for MLRT refinements
        self.client = self.llm.client

        # Initialize the GUI
        self.init_gui()

        # Initialize the CLI in a separate thread
        self.init_cli()

        # Conversation log file
        self.log_file = "conversation_log.txt"

        # User ID for profiles
        self.user_id = "default_user"

        # Load user profile
        self.user_profile = self.load_user_profile(self.user_id)

        # For thinking animation (managed within the GUI thread)
        self.thinking = False

        # Start processing AI results
        self.process_AI_results()

    def init_gui(self):
        """Initialize the Tkinter GUI."""
        self.window = tk.Tk()
        self.window.title("Talk to a Catgirl")

        # Set window size
        self.window.geometry("800x600")
        self.window.resizable(False, False)  # Fixed window size to prevent excessive redraws

        # Apply a modern style using ttk
        style = ttk.Style(self.window)
        style.theme_use('clam')  # You can choose other themes like 'default', 'alt', 'classic', etc.

        # Create main frame
        main_frame = ttk.Frame(self.window, padding="10 10 10 10")
        main_frame.pack(fill=tk.BOTH, expand=True)

        # Thinking indicator at the top
        self.thinking_label = ttk.Label(main_frame, text="", foreground="blue", font=("Helvetica", 10))
        self.thinking_label.pack(pady=(0, 5))  # Add some padding below

        # Chat display with scrollbar
        self.chat_display = scrolledtext.ScrolledText(
            main_frame, state='disabled', wrap='word',
            width=80, height=20, font=("Helvetica", 12), bg="#f0f0f0"
        )
        self.chat_display.pack(padx=10, pady=5, fill=tk.BOTH, expand=True)

        # Style tags for different message types
        self.chat_display.tag_config("user", foreground="blue")
        self.chat_display.tag_config("catgirl", foreground="green")
        self.chat_display.tag_config("system", foreground="red")

        # Persona customization frame
        self.persona_frame = ttk.LabelFrame(main_frame, text="Persona Customization", padding="10 10 10 10")
        self.persona_frame.pack(pady=5, fill=tk.X)

        self.drunkenness_var = tk.DoubleVar(value=0.5)
        ttk.Label(self.persona_frame, text="drunkenness:").pack(side=tk.LEFT)
        self.drunkenness_scale = ttk.Scale(
            self.persona_frame,
            variable=self.drunkenness_var,
            from_=0.0,
            to=1.0,
            orient=tk.HORIZONTAL,
            length=200
        )
        self.drunkenness_scale.pack(side=tk.LEFT, padx=5)

        # User input frame
        self.user_input_frame = ttk.Frame(main_frame)
        self.user_input_frame.pack(side=tk.BOTTOM, pady=10, fill=tk.X)

        self.user_input_var = tk.StringVar()
        self.user_input_entry = ttk.Entry(
            self.user_input_frame, textvariable=self.user_input_var,
            width=70, font=("Helvetica", 12)
        )
        self.user_input_entry.pack(side=tk.LEFT, padx=10, pady=5, fill=tk.X, expand=True)
        self.user_input_entry.bind("<Return>", self.on_enter_pressed)

        # Send button
        self.send_button = ttk.Button(self.user_input_frame, text="send", command=self.on_send_clicked)
        self.send_button.pack(side=tk.LEFT, padx=5)

        # Voice input button
        self.voice_button = ttk.Button(self.user_input_frame, text="🎤", command=self.on_voice_input)
        self.voice_button.pack(side=tk.LEFT, padx=5)

        # Developer mode chain-of-thought display
        if DEVELOPER_MODE:
            self.cot_display = scrolledtext.ScrolledText(
                main_frame, state='disabled', wrap='word',
                width=80, height=10, bg='lightgray', font=("Helvetica", 10)
            )
            self.cot_display.pack(padx=10, pady=10, fill=tk.BOTH, expand=True)
            self.cot_display.insert(tk.END, "Chain-of-thought will be displayed here in Developer Mode.\n")

        # Start processing the message queue
        self.window.after(200, self.process_queue)  # Increased interval to reduce CPU usage

    def init_cli(self):
        """Initialize the command-line interface in a separate thread."""
        self.cli_thread = threading.Thread(target=self.cli_main, daemon=True)
        self.cli_thread.start()

    def cli_main(self):
        """Main loop for the CLI."""
        while True:
            try:
                user_input = input("you: ")
                if user_input.lower() in ["exit", "quit"]:
                    logging.info("Exiting chatbot. Goodbye!")
                    os._exit(0)
                elif user_input.strip() == "":
                    continue
                else:
                    # Enqueue tasks for AI Workers
                    self.enqueue_AI_tasks(user_input)

                    # Show thinking spinner
                    with Progress(SpinnerColumn(), TextColumn("[progress.description]{task.description}")) as progress:
                        task = progress.add_task("[bold green]catgirl is thinking...", total=None)
                        # Introduce a brief delay to simulate processing
                        time.sleep(1)
                        # Get the response using MLRT
                        response = self.mlrt.process_input(user_input, self.client, main_prompt)
                        progress.remove_task(task)
                    print(f"catgirl: {response}")
                    # Enqueue the response to be displayed in the GUI
                    self.message_queue.put(("catgirl", response))
            except (EOFError, KeyboardInterrupt):
                logging.info("\nExiting chatbot. Goodbye!")
                os._exit(0)

    def on_enter_pressed(self, event):
        """Handle the Enter key press in the GUI."""
        self.on_send_clicked()

    def on_send_clicked(self):
        """Handle the send button click in the GUI."""
        user_input = self.user_input_var.get()
        if user_input.strip() != "":
            # Enqueue tasks for AI Workers
            self.enqueue_AI_tasks(user_input)

            self.message_queue.put(("user", user_input))
            self.user_input_var.set("")
            # Show thinking indicator
            self.start_thinking_animation()
            threading.Thread(target=self.chatbot_interaction, args=(user_input,), daemon=True).start()

    def on_voice_input(self):
        """Handle the voice input button click in the GUI."""
        threading.Thread(target=self.handle_voice_input, daemon=True).start()

    def handle_voice_input(self):
        """Handle voice input from the user."""
        user_input = self.listen_to_user()
        if user_input:
            # Enqueue tasks for AI Workers
            self.enqueue_AI_tasks(user_input)

            self.message_queue.put(("user", user_input))
            # Show thinking indicator
            self.start_thinking_animation()
            threading.Thread(target=self.chatbot_interaction, args=(user_input,), daemon=True).start()

    def listen_to_user(self):
        """Capture audio input from the user and convert it to text."""
        with sr.Microphone() as source:
            self.message_queue.put(("system", "listening..."))
            try:
                audio = recognizer.listen(source, timeout=5, phrase_time_limit=10)
                user_input = recognizer.recognize_google(audio)
                logging.info(f"user input via voice: {user_input}")
                return user_input
            except sr.UnknownValueError:
                self.message_queue.put(("system", "sorry, i did not understand that."))
            except sr.RequestError as e:
                self.message_queue.put(("system", f"could not request results; {e}"))
            except sr.WaitTimeoutError:
                self.message_queue.put(("system", "listening timed out while waiting for phrase."))
            return ""

    def enqueue_AI_tasks(self, user_input):
        """Enqueue tasks for AI Workers based on user input."""
        self.sentiment_task_queue.put(user_input)
        self.intent_task_queue.put(user_input)
        self.context_task_queue.put(user_input)

    def display_message_queue(self, sender, message):
        """Display messages from the queue in the GUI."""
        self.chat_display.config(state='normal')
        if sender == "user":
            self.chat_display.insert(tk.END, f"you: {message}\n", "user")
        elif sender == "catgirl":
            self.chat_display.insert(tk.END, f"catgirl: {message}\n", "catgirl")
        elif sender == "system":
            self.chat_display.insert(tk.END, f"{message}\n", "system")
        self.chat_display.see(tk.END)
        self.chat_display.config(state='disabled')
        self.log_conversation(sender, message)
        # Hide the thinking indicator when displaying a new message
        if sender == "catgirl":
            self.stop_thinking_animation()

    def process_queue(self):
        """Process and display messages from the queue."""
        try:
            while not self.message_queue.empty():
                sender, message = self.message_queue.get_nowait()
                if sender == "Thinking":
                    self.thinking_label.config(text=message)
                else:
                    self.display_message_queue(sender, message)
        except queue.Empty:
            pass
        # Schedule the next queue check
        self.window.after(200, self.process_queue)  # Adjusted interval to 200ms

    def process_AI_results(self):
        """Process results from AI Workers."""
        try:
            while not self.AI_result_queue.empty():
                result = self.AI_result_queue.get_nowait()
                if result[0] == 'sentiment':
                    self.user_profile['last_sentiment'] = result[1]
                    logging.info(f"Updated last_sentiment to: {result[1]}")
                elif result[0] == 'intent':
                    self.user_profile['last_intent'] = result[1]
                    logging.info(f"Updated last_intent to: {result[1]}")
                elif result[0] == 'context_updated':
                    logging.info("Context updated successfully.")
                self.AI_result_queue.task_done()
        except queue.Empty:
            pass
        # Schedule the next AI result check
        self.window.after(100, self.process_AI_results)

    def log_conversation(self, sender, message):
        """Log the conversation to a file and update user profile."""
        with open(self.log_file, "a", encoding="utf-8") as f:
            f.write(f"{sender}: {message}\n")
        # Update user profile conversation history
        self.user_profile.setdefault("conversation_history", []).append({"sender": sender, "message": message})
        self.save_user_profile(self.user_id, self.user_profile)
        # Update conversation graph
        self.update_conversation_graph(message, sender)

    def update_conversation_graph(self, text, sender):
        """Update the conversation graph with new words and connections."""
        if not text:
            logging.error("no text provided for graph update.")
            return

        # Tokenize the text
        tokens = word_tokenize(text.lower())

        # Filter out stopwords and punctuation
        filtered_tokens = [word for word in tokens if word.isalnum() and word not in stop_words]

        # Add nodes and edges to the graph
        for i in range(len(filtered_tokens) - 1):
            word1 = filtered_tokens[i]
            word2 = filtered_tokens[i + 1]
            self.conversation_graph.add_node(word1)
            self.conversation_graph.add_node(word2)
            self.conversation_graph.add_edge(word1, word2, sender=sender)

        # If only one token, still add it as a node
        if len(filtered_tokens) == 1:
            self.conversation_graph.add_node(filtered_tokens[0])

    def start_thinking_animation(self):
        """Start the thinking animation in the GUI."""
        if not self.thinking:
            self.thinking = True
            self.thinking_indicator = threading.Thread(target=self.animate_thinking, daemon=True)
            self.thinking_indicator.start()

    def stop_thinking_animation(self):
        """Stop the thinking animation in the GUI."""
        self.thinking = False
        self.message_queue.put(("Thinking", ""))  # Clear thinking label

    def animate_thinking(self):
        """Animate the thinking indicator."""
        for c in itertools.cycle(['|', '/', '-', '\\']):
            if not self.thinking:
                break
            self.message_queue.put(("Thinking", f"catgirl is thinking... {c}"))
            time.sleep(0.5)  # Increased sleep to reduce CPU usage
        self.message_queue.put(("Thinking", ""))  # Clear thinking label

    def chatbot_interaction(self, user_input):
        """Handle the chatbot interaction asynchronously."""
        try:
            response = self.mlrt.process_input(user_input, self.client, main_prompt)
            self.message_queue.put(("catgirl", response))
        except Exception as e:
            logging.error(f"asyncio runtime error: {e}")
            self.message_queue.put(("system", "internal error occurred. please try again."))

    def load_user_profile(self, user_id):
        """
        Load user profile from a JSON file. Create one if it doesn't exist.
        """
        filename = f"user_profiles/{user_id}.json"
        if os.path.exists(filename):
            with open(filename, 'r', encoding='utf-8') as f:
                profile = json.load(f)
            logging.info(f"loaded profile for user: {user_id}")
        else:
            profile = {"preferences": {}, "conversation_history": []}
            os.makedirs(os.path.dirname(filename), exist_ok=True)
            with open(filename, 'w', encoding='utf-8') as f:
                json.dump(profile, f, ensure_ascii=False, indent=4)
            logging.info(f"created new profile for user: {user_id}")
        return profile

    def save_user_profile(self, user_id, profile):
        """
        Save user profile to a JSON file.
        """
        filename = f"user_profiles/{user_id}.json"
        with open(filename, 'w', encoding='utf-8') as f:
            json.dump(profile, f, ensure_ascii=False, indent=4)
        logging.info(f"saved profile for user: {user_id}")

    def get_response(self, user_input):
        """
        Synchronous method to get response for CLI.
        """
        loop = asyncio.new_event_loop()
        asyncio.set_event_loop(loop)
        try:
            response = loop.run_until_complete(self.async_chatbot_interaction(user_input))
            return response
        finally:
            loop.close()

    def main(self):
        """Start the Tkinter main loop."""
        self.window.mainloop()

    def update_conversation(self, sender: str, message: str):
        """
        Update the conversation history and graph.
        """
        # Update conversation memory
        self.conversation_memory.append({'sender': sender, 'message': message})
        logging.debug(f"Conversation updated with {sender}: {message}")

        # Update conversation graph
        self.update_conversation_graph(message, sender)

    # =================== MLRT Integration Methods =====================

    def integrate_mlrt(self, user_input: str) -> str:
        """
        Integrate MLRT into the chatbot interaction flow.
        """
        # Process the input through MLRT to get a refined response
        response = self.mlrt.process_input(user_input, self.client, main_prompt)
        return response

    # =================== End of MLRT Integration Methods =============

# ======================= End of Chatbot Class ======================

# ======================= Main Entry Point ==========================

def main():
    """Main entry point for the chatbot."""
    chatbot = Chatbot()
    chatbot.main()

if __name__ == "__main__":
    main()