ERROR:root:asyncio runtime error: 'Chatbot' object has no attribute 'update_conversation'

And also improve the MLRT.

```
# Required modules:
# pip install g4f langchain nltk speechrecognition pyaudio networkx pydantic tk requests rich asyncio scikit-learn

import os
import random
import re
import json
import logging
import time
import itertools
import queue  # For thread-safe communication
import requests
import threading
import asyncio
from typing import Optional, List, Dict, Tuple
from collections import deque

import g4f
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

# 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 Rich Console for CLI
from rich.console import Console
from rich.progress import Progress, SpinnerColumn, TextColumn

console = Console()

# Set up logging for debugging and monitoring
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):
        memory_entry = {'input': input_text, 'response': response, 'score': score}
        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]):
        """
        weights: Dictionary defining the importance of each metric.
                 e.g., {'correctness': 0.4, 'conciseness': 0.2, 'relevance': 0.2, 'fluency':0.1, 'clarity':0.1}
        """
        self.weights = weights

    def evaluate(self, response: str, input_text: str) -> float:
        """
        Evaluates the response based on multiple metrics.
        """
        metrics = {
            'correctness': self.evaluate_correctness(response),
            '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) -> 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]) -> str:
        # Apply specific refinements based on identified weaknesses
        for weakness in weaknesses:
            action = self.refinement_actions.get(weakness, "")
            response += action
            logging.debug(f"Applied refinement for {weakness}: {action}")
        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) -> str:
        logging.info(f"MLRT processing input: {input_text}")
        iteration = 0
        response = self.generate_response(input_text)
        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.refine_response(response, weaknesses)
            score, weaknesses = self.reward_evaluator_with_weaknesses(response, input_text)
            logging.info(f"MLRT Refined response score: {score:.2f}")

        # 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) -> str:
        # Layered processing
        response = self.base_layer.process(input_text)
        response = self.middle_layer.process(response)
        response = self.top_layer.process(response)
        logging.debug(f"MLRT Generated response after all layers: {response}")
        return response

    def refine_response(self, response: str, weaknesses: List[str]) -> str:
        # Use RefinementEngine to apply specific refinements
        response = self.refinement_engine.refine(response, weaknesses)
        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),
            '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
        # You can integrate an actual sentiment analysis API or model here
        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.save_context({"user_input": text}, {"response": ""})

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

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

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

    @property
    def _llm_type(self) -> str:
        return "gptfree"

    def _call(self, prompt: str, stop: Optional[List[str]] = None, **kwargs) -> str:
        messages = [{"role": "user", "content": prompt}]
        response = self.get_gpt_response(messages)
        return response

    def get_gpt_response(self, messages):
        try:
            # Add the main sensory system prompt to the messages
            messages_with_prompt = [{"role": "system", "content": main_prompt}] + messages

            # Send the conversation history to the provider
            response = g4f.ChatCompletion.create(
                messages=messages_with_prompt,
                model="gpt-4o-mini",  # Ensure the model name is correct for the provider
                provider=provider,
            )
            # 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
            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~"

    class Config:
        extra = "allow"  # Updated to use literal value as per Pydantic v2.0

# ================== 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 LLM and Prompt Template
        self.llm = GPTFreeLLM()
        self.template = PromptTemplate.from_template("{conversation_history}\nuser says: {user_input}")

        # Initialize memory using LangChain's ConversationBufferMemory
        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, self.memory)

        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, desired_attributes, memory_capacity=200, max_iterations=7, score_threshold=0.85)

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

        # Add a variable to control the maximum reasoning steps
        self.max_reasoning_steps = 5  # You can adjust this number as needed

        # 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"]:
                    console.print("exiting chatbot. goodbye!", style="bold red")
                    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)
                        progress.remove_task(task)
                    console.print(f"catgirl: {response}")
                    # Enqueue the response to be displayed in the GUI
                    self.message_queue.put(("catgirl", response))
            except (EOFError, KeyboardInterrupt):
                console.print("\nexiting chatbot. goodbye!", style="bold red")
                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["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 = asyncio.run(self.async_chatbot_interaction(user_input))
            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."))

    async def async_chatbot_interaction(self, user_input):
        """
        Handle the asynchronous chatbot interaction with enhanced MLRT.
        """
        # Add the user input to the conversation history
        self.update_conversation("user", user_input)

        # Update memory
        self.memory.save_context({"user_input": user_input}, {"response": ""})

        # Retrieve conversation history from memory
        try:
            memory_vars = self.memory.load_memory_variables({"user_input": user_input})
            conversation_history = memory_vars["conversation_history"]
        except AttributeError as e:
            logging.error(f"memory loading error: {e}")
            conversation_history = ""

        # Prepare prompt with conversation history
        prompt = self.template.format(conversation_history=conversation_history, user_input=user_input)

        # Use MLRT to generate refined response
        assistant_response = self.mlrt.process_input(user_input)

        # Add the bot's response to the conversation memory
        self.update_conversation("assistant", assistant_response)

        # Perform self-reflection and improvement (if needed)
        self.self_reflection(assistant_response)

        return assistant_response

    def self_reflection(self, assistant_response):
        """
        Internal self-reflection to improve future responses.
        """
        reflection_prompt = f"""
assistant, internally reflect on the previous response: "{assistant_response}". consider:

- was the response coherent and in character?
- did it adhere to the style guidelines?
- how can you improve in future responses?

do not share this reflection with the user.
"""
        messages = [{"role": "assistant", "content": reflection_prompt}]
        # The assistant performs the reflection internally
        _ = self.llm.get_gpt_response(messages)
        # No action needed; this adjusts internal state
        logging.info("self-reflection completed.")

    def critique_response(self, response):
        """
        Critique the assistant's response based on predefined criteria.
        """
        # Define criteria for evaluation
        criteria = {
            "in_character": "does the response include cat sounds like 'nya' or 'meow'?",
            "coherent": "is the response coherent and understandable?",
            "style": "is the response in all lowercase letters and whimsical in nature?",
            "ethical": "does the response maintain ethical boundaries and avoid inappropriate content?"
        }

        # Simple checks for criteria
        in_character = any(cat_sound in response for cat_sound in ["nya", "meow", "nyah", "nyan"])
        coherent = len(response.split()) > 2  # Simple check for minimal coherence
        style = response.islower()  # Check if the response is in lowercase
        ethical = self.check_ethics(response)  # Implement ethical checks

        # Compile critique
        critique = ""
        if in_character and coherent and style and ethical:
            critique = "good response."
        else:
            critique = "needs improvement:"
            if not in_character:
                critique += " response is not in character. "
            if not coherent:
                critique += " response is not coherent. "
            if not style:
                critique += " response does not adhere to style guidelines. "
            if not ethical:
                critique += " response contains unethical content. "

        logging.info(f"critique: {critique}")
        return critique

    def check_ethics(self, response):
        """
        Check if the response maintains ethical boundaries.
        """
        # Define a list of forbidden topics or phrases
        forbidden = ["violence", "hate", "illegal", "adult content", "self-harm", "attack", "abuse"]
        response_lower = response.lower()
        for word in forbidden:
            if word in response_lower:
                return False
        return True

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

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