backend.memory

from typing import Any, Dict, List, Union
from loguru import logger
import json

from backend.app import app
from backend.utils.running_time_storage import get_running_time_storage
from backend.utils.user_conversation_storage import get_user_conversation_storage
from real_agents.adapters.memory import BaseChatMemory

HUMAN_MESSAGE_KEY = "human_message"
AI_MESSAGE_KEY = "ai_message"

LOCAL = "local"
DATABASE = "database"

class UserMemoryManager:
    """A class to manage the global memory including messages, grounding_sources,
    etc. on user level"""

    # api_key_pool:
    # {
    #   "user_id": [{
    #       "tool_id": the id of the tool,
    #       "tool_name": the name of the tool,
    #       "api_key": the api_key of the tool,
    # }]
    # }

    def __init__(self, name: str = None, backend: str = LOCAL, memory_pool: Dict = None):
        self.backend = backend
        self.name = name
        if self.backend == LOCAL:
            if memory_pool is None:
                memory_pool = {}
            self.memory_pool = memory_pool
        elif self.backend == DATABASE:
            with app.app_context():
                self.redis_client = get_running_time_storage()
                self.db_client = get_user_conversation_storage()
        else:
            raise ValueError("Unknown backend option: {}".format(self.backend))

    def get_pool_info_with_id(
        self,
        user_id: str,
        default_value: Union[List, Dict],
    ) -> Any:
        """Gets the information with user_id and chat_id from the pool."""
        if self.backend == LOCAL:
            pool = self.memory_pool
            if user_id in pool:
                return pool[user_id]
            else:
                return default_value
        elif self.backend == DATABASE:
            memory_pool_name = f"{self.name}:{user_id}"
            if self.redis_client.exists(memory_pool_name):
                # In cache
                info = json.loads(self.redis_client.get(memory_pool_name))
            else:
                # Cache miss
                try:
                    # api_keys are not stored in database
                    if self.name == "api_key_pool":
                        info = default_value
                    else:
                        raise NotImplementedError(f"Currently only support message pool in database, not {self.name}")
                except Exception as e:
                    # Not in database
                    logger.bind(user_id=user_id, msg_head="Cache miss but not in database").warning(
                        "Failed to get pool info from database: {}".format(e)
                    )
                    info = default_value
            return info

    def set_pool_info_with_id(self, user_id: str, info: Any) -> None:
        """Sets the information with user_id and chat_id to the pool."""
        if self.backend == LOCAL:
            pool = self.memory_pool
            if user_id not in pool:
                pool[user_id] = info
        elif self.backend == DATABASE:
            # As db has its own updating logic, we only need to update the cache here (write-through).
            memory_pool_name = f"{self.name}:{user_id}"
            self.redis_client.set(memory_pool_name, json.dumps(info))

    def __iter__(self):
        """Iterates over the memory pool."""
        if self.backend == LOCAL:
            for user_id, info in self.memory_pool.items():
                yield user_id, info
        elif self.backend == DATABASE:
            raise NotImplementedError("Currently not support UserMemoryManager iteration in database mode.")

class ChatMemoryManager:
    """A class to manage the global memory including messages, grounding_sources, etc. on chat level"""

    # memory_pool:
    # {user_id: {chat_id: [
    #                           {"message_id": the id of this pair of messages,
    #                            "parent_message_id": the message of the parent message,
    #                            "message_type": type of the message, possible values: human_message / ai_message
    #                            "message_content": content of the message
    #                           }
    #                     ]
    #           }
    # }
    # grounding_source_pool:
    # {user_id: {chat_id: {"filenames": List of filenames,
    #                      "activated_filenames": List of user-selected activated names}}

    def __init__(self, name: str = None, backend: str = LOCAL, memory_pool: Dict = None):
        """
        This ChatMemoryManager can not be applied to grounding_source_pool in database mode.
        """
        self.backend = backend
        self.name = name
        if self.backend == LOCAL:
            if memory_pool is None:
                memory_pool = {}
            self.memory_pool = memory_pool
        elif self.backend == DATABASE:
            with app.app_context():
                self.redis_client = get_running_time_storage()
                self.db_client = get_user_conversation_storage()
        else:
            raise ValueError("Unknown backend option: {}".format(self.backend))

    def get_pool_info_with_id(
        self,
        user_id: str,
        chat_id: str,
        default_value: Union[List, Dict],
    ) -> Any:
        """Gets the information with user_id and chat_id from the pool."""
        if self.backend == LOCAL:
            pool = self.memory_pool
            if user_id in pool and chat_id in pool[user_id]:
                return pool[user_id][chat_id]
            else:
                return default_value
        elif self.backend == DATABASE:
            memory_pool_name = f"{self.name}:{user_id}:{chat_id}"
            if self.redis_client.exists(memory_pool_name):
                # In cache
                info = json.loads(self.redis_client.get(memory_pool_name))
            else:
                # Cache miss
                try:
                    # Found in database
                    if self.name == "message_pool":
                        info = []
                        response = self.db_client.message.find({"conversation_id": chat_id})
                        if response is None:
                            # Not in database (new chat)
                            info = default_value
                        else:
                            # In database
                            for message in response:
                                if message["role"] == "user":
                                    message_type = HUMAN_MESSAGE_KEY
                                elif message["role"] == "assistant":
                                    message_type = AI_MESSAGE_KEY
                                else:
                                    raise ValueError("Unknown role: {}".format(message["role"]))
                                info.append(
                                    {
                                        "message_id": message["message_id"],
                                        "parent_message_id": message["parent_message_id"],
                                        "message_type": message_type,
                                        "message_content": message["data_for_llm"],
                                    }
                                )
                            self.redis_client.set(memory_pool_name, json.dumps(info))
                    elif self.name == "jupyter_kernel_pool":
                        info = default_value
                    else:
                        raise NotImplementedError(f"Currently only support message pool in database, not {self.name}")
                except Exception as e:
                    # Not in database
                    logger.bind(user_id=user_id, chat_id=chat_id, msg_head="Cache miss but not in database").warning(
                        "Failed to get pool info from database: {}".format(e)
                    )
                    info = default_value
            return info

    def set_pool_info_with_id(self, user_id: str, chat_id: str, info: Any) -> None:
        """Sets the information with user_id and chat_id to the pool."""
        if self.backend == LOCAL:
            pool = self.memory_pool
            if user_id not in pool:
                pool[user_id] = {}
            pool[user_id][chat_id] = info
        elif self.backend == DATABASE:
            # As db has its own updating logic, we only need to update the cache here (write-through).
            memory_pool_name = f"{self.name}:{user_id}:{chat_id}"
            self.redis_client.set(memory_pool_name, json.dumps(info))

    def __iter__(self):
        """Iterates over the memory pool."""
        if self.backend == LOCAL:
            for user_id, chat_id_info in self.memory_pool.items():
                for chat_id, info in chat_id_info.items():
                    yield user_id, chat_id, info
        elif self.backend == DATABASE:
            if self.name == "jupyter_kernel_pool":
                iterator = self.redis_client.scan_iter("jupyter_kernel_pool:*")
                for key in iterator:
                    user_id, chat_id = key.split(":")[1:]
                    yield user_id, chat_id, self.get_pool_info_with_id(user_id, chat_id, {})
            else:
                raise NotImplementedError("Currently only support jupyter kernel pool iteration in database mode.")

    def drop_item_with_id(self, user_id: str, chat_id: str):
        if self.backend == LOCAL:
            # drop item under one user
            if user_id in self.memory_pool:
                self.memory_pool[user_id].pop([chat_id], None)
        elif self.backend == DATABASE:
            if self.name == "jupyter_kernel_pool":
                self.redis_client.delete(f"{self.name}:{user_id}:{chat_id}")
            else:
                raise NotImplementedError("Currently only support jupyter kernel pool delete in database mode.")

class MessageMemoryManager(ChatMemoryManager):
    """A class to manage the memory of messages."""

    @staticmethod
    def load_agent_memory_from_list(agent_memory: BaseChatMemory, message_list: List[Dict[str, str]]) -> None:
        """Load agent's memory from a list."""
        agent_memory.clear()
        for message in message_list:
            if message.get("message_type", None) == HUMAN_MESSAGE_KEY:
                agent_memory.chat_memory.add_user_message(message["message_content"])
            elif message.get("message_type", None) == AI_MESSAGE_KEY:
                agent_memory.chat_memory.add_ai_message(message["message_content"])
        try:
            agent_memory.fit_max_token_limit()
        except Exception as e:
            import traceback

            traceback.print_exc()
            pass

    @staticmethod
    def save_agent_memory_to_list(agent_memory: BaseChatMemory) -> List[Dict[str, str]]:
        """Saves agent's memory to a list"""
        messages = agent_memory.chat_memory.messages
        message_list = []
        for message in messages:
            if message.type == "human":
                message_list.append(
                    {
                        "message_type": "human_message",
                        "message_content": message.content,
                    }
                )
            elif message.type == "ai":
                message_list.append(
                    {
                        "message_type": "ai_message",
                        "message_content": message.content,
                    }
                )
        return message_list

    def get_activated_message_list(
        self,
        user_id: str,
        chat_id: str,
        default_value: Union[List, Dict],
        parent_message_id: Union[int, str],
    ) -> List:
        """Gets the activated message list from leaf to root."""
        # ONLY work for messages
        message_list = self.get_pool_info_with_id(user_id, chat_id, default_value)
        activated_message_list = []
        end_point = parent_message_id
        while len(message_list) > 0 and end_point != -1:
            flag = False
            for msg in message_list:
                if msg["message_id"] == end_point:
                    if end_point == msg["parent_message_id"]:
                        flag = False
                        break
                    activated_message_list = [msg] + activated_message_list
                    end_point = msg["parent_message_id"]
                    flag = True
                    break
            if not flag:
                break
        logger.bind(msg_head=f"get_activated_message_list").debug(activated_message_list)
        return activated_message_list


UserMemoryManager

from typing import Any, Dict, List, Union
from loguru import logger
import json

from backend.app import app
from backend.utils.running_time_storage import get_running_time_storage
from backend.utils.user_conversation_storage import get_user_conversation_storage
from real_agents.adapters.memory import BaseChatMemory

HUMAN_MESSAGE_KEY = "human_message"
AI_MESSAGE_KEY = "ai_message"

LOCAL = "local"
DATABASE = "database"

class UserMemoryManager:
    """A class to manage the global memory including messages, grounding_sources,
    etc. on user level"""

    # api_key_pool:
    # {
    #   "user_id": [{
    #       "tool_id": the id of the tool,
    #       "tool_name": the name of the tool,
    #       "api_key": the api_key of the tool,
    # }]
    # }

    def __init__(self, name: str = None, backend: str = LOCAL, memory_pool: Dict = None):
        self.backend = backend
        self.name = name
        if self.backend == LOCAL:
            if memory_pool is None:
                memory_pool = {}
            self.memory_pool = memory_pool
        elif self.backend == DATABASE:
            with app.app_context():
                self.redis_client = get_running_time_storage()
                self.db_client = get_user_conversation_storage()
        else:
            raise ValueError("Unknown backend option: {}".format(self.backend))

    def get_pool_info_with_id(
        self,
        user_id: str,
        default_value: Union[List, Dict],
    ) -> Any:
        """Gets the information with user_id and chat_id from the pool."""
        if self.backend == LOCAL:
            pool = self.memory_pool
            if user_id in pool:
                return pool[user_id]
            else:
                return default_value
        elif self.backend == DATABASE:
            memory_pool_name = f"{self.name}:{user_id}"
            if self.redis_client.exists(memory_pool_name):
                # In cache
                info = json.loads(self.redis_client.get(memory_pool_name))
            else:
                # Cache miss
                try:
                    # api_keys are not stored in database
                    if self.name == "api_key_pool":
                        info = default_value
                    else:
                        raise NotImplementedError(f"Currently only support message pool in database, not {self.name}")
                except Exception as e:
                    # Not in database
                    logger.bind(user_id=user_id, msg_head="Cache miss but not in database").warning(
                        "Failed to get pool info from database: {}".format(e)
                    )
                    info = default_value
            return info

    def set_pool_info_with_id(self, user_id: str, info: Any) -> None:
        """Sets the information with user_id and chat_id to the pool."""
        if self.backend == LOCAL:
            pool = self.memory_pool
            if user_id not in pool:
                pool[user_id] = info
        elif self.backend == DATABASE:
            # As db has its own updating logic, we only need to update the cache here (write-through).
            memory_pool_name = f"{self.name}:{user_id}"
            self.redis_client.set(memory_pool_name, json.dumps(info))

    def __iter__(self):
        """Iterates over the memory pool."""
        if self.backend == LOCAL:
            for user_id, info in self.memory_pool.items():
                yield user_id, info
        elif self.backend == DATABASE:
            raise NotImplementedError("Currently not support UserMemoryManager iteration in database mode.")

class ChatMemoryManager:
    """A class to manage the global memory including messages, grounding_sources, etc. on chat level"""

    # memory_pool:
    # {user_id: {chat_id: [
    #                           {"message_id": the id of this pair of messages,
    #                            "parent_message_id": the message of the parent message,
    #                            "message_type": type of the message, possible values: human_message / ai_message
    #                            "message_content": content of the message
    #                           }
    #                     ]
    #           }
    # }
    # grounding_source_pool:
    # {user_id: {chat_id: {"filenames": List of filenames,
    #                      "activated_filenames": List of user-selected activated names}}

    def __init__(self, name: str = None, backend: str = LOCAL, memory_pool: Dict = None):


ChatMemoryManager

    def set_pool_info_with_id(self, user_id: str, info: Any) -> None:
        """Sets the information with user_id and chat_id to the pool."""
        if self.backend == LOCAL:
            pool = self.memory_pool
            if user_id not in pool:
                pool[user_id] = info
        elif self.backend == DATABASE:
            # As db has its own updating logic, we only need to update the cache here (write-through).
            memory_pool_name = f"{self.name}:{user_id}"
            self.redis_client.set(memory_pool_name, json.dumps(info))

    def __iter__(self):
        """Iterates over the memory pool."""
        if self.backend == LOCAL:
            for user_id, info in self.memory_pool.items():
                yield user_id, info
        elif self.backend == DATABASE:
            raise NotImplementedError("Currently not support UserMemoryManager iteration in database mode.")

class ChatMemoryManager:
    """A class to manage the global memory including messages, grounding_sources, etc. on chat level"""

    # memory_pool:
    # {user_id: {chat_id: [
    #                           {"message_id": the id of this pair of messages,
    #                            "parent_message_id": the message of the parent message,
    #                            "message_type": type of the message, possible values: human_message / ai_message
    #                            "message_content": content of the message
    #                           }
    #                     ]
    #           }
    # }
    # grounding_source_pool:
    # {user_id: {chat_id: {"filenames": List of filenames,
    #                      "activated_filenames": List of user-selected activated names}}

    def __init__(self, name: str = None, backend: str = LOCAL, memory_pool: Dict = None):
        """
        This ChatMemoryManager can not be applied to grounding_source_pool in database mode.
        """
        self.backend = backend
        self.name = name
        if self.backend == LOCAL:
            if memory_pool is None:
                memory_pool = {}
            self.memory_pool = memory_pool
        elif self.backend == DATABASE:
            with app.app_context():
                self.redis_client = get_running_time_storage()
                self.db_client = get_user_conversation_storage()
        else:
            raise ValueError("Unknown backend option: {}".format(self.backend))

    def get_pool_info_with_id(
        self,
        user_id: str,
        chat_id: str,
        default_value: Union[List, Dict],
    ) -> Any:
        """Gets the information with user_id and chat_id from the pool."""
        if self.backend == LOCAL:
            pool = self.memory_pool
            if user_id in pool and chat_id in pool[user_id]:
                return pool[user_id][chat_id]
            else:
                return default_value
        elif self.backend == DATABASE:
            memory_pool_name = f"{self.name}:{user_id}:{chat_id}"
            if self.redis_client.exists(memory_pool_name):
                # In cache
                info = json.loads(self.redis_client.get(memory_pool_name))
            else:
                # Cache miss
                try:
                    # Found in database
                    if self.name == "message_pool":
                        info = []
                        response = self.db_client.message.find({"conversation_id": chat_id})
                        if response is None:
                            # Not in database (new chat)
                            info = default_value
                        else:
                            # In database
                            for message in response:
                                if message["role"] == "user":
                                    message_type = HUMAN_MESSAGE_KEY
                                elif message["role"] == "assistant":
                                    message_type = AI_MESSAGE_KEY
                                else:
                                    raise ValueError("Unknown role: {}".format(message["role"]))
                                info.append(
                                    {
                                        "message_id": message["message_id"],
                                        "parent_message_id": message["parent_message_id"],
                                        "message_type": message_type,
                                        "message_content": message["data_for_llm"],
                                    }
                                )
                            self.redis_client.set(memory_pool_name, json.dumps(info))
                    elif self.name == "jupyter_kernel_pool":
                        info = default_value
                    else:
                        raise NotImplementedError(f"Currently only support message pool in database, not {self.name}")
                except Exception as e:
                    # Not in database
                    logger.bind(user_id=user_id, chat_id=chat_id, msg_head="Cache miss but not in database").warning(
                        "Failed to get pool info from database: {}".format(e)
                    )
                    info = default_value
            return info

    def set_pool_info_with_id(self, user_id: str, chat_id: str, info: Any) -> None:
        """Sets the information with user_id and chat_id to the pool."""
        if self.backend == LOCAL:
            pool = self.memory_pool
            if user_id not in pool:
                pool[user_id] = {}
            pool[user_id][chat_id] = info
        elif self.backend == DATABASE:
            # As db has its own updating logic, we only need to update the cache here (write-through).
            memory_pool_name = f"{self.name}:{user_id}:{chat_id}"
            self.redis_client.set(memory_pool_name, json.dumps(info))

    def __iter__(self):
        """Iterates over the memory pool."""
        if self.backend == LOCAL:
            for user_id, chat_id_info in self.memory_pool.items():
                for chat_id, info in chat_id_info.items():
                    yield user_id, chat_id, info
        elif self.backend == DATABASE:
            if self.name == "jupyter_kernel_pool":
                iterator = self.redis_client.scan_iter("jupyter_kernel_pool:*")
                for key in iterator:
                    user_id, chat_id = key.split(":")[1:]
                    yield user_id, chat_id, self.get_pool_info_with_id(user_id, chat_id, {})
            else:
                raise NotImplementedError("Currently only support jupyter kernel pool iteration in database mode.")

    def drop_item_with_id(self, user_id: str, chat_id: str):
        if self.backend == LOCAL:
            # drop item under one user
            if user_id in self.memory_pool:
                self.memory_pool[user_id].pop([chat_id], None)
        elif self.backend == DATABASE:
            if self.name == "jupyter_kernel_pool":
                self.redis_client.delete(f"{self.name}:{user_id}:{chat_id}")
            else:
                raise NotImplementedError("Currently only support jupyter kernel pool delete in database mode.")

class MessageMemoryManager(ChatMemoryManager):
    """A class to manage the memory of messages."""

    @staticmethod
    def load_agent_memory_from_list(agent_memory: BaseChatMemory, message_list: List[Dict[str, str]]) -> None:
        """Load agent's memory from a list."""
        agent_memory.clear()
        for message in message_list:
            if message.get("message_type", None) == HUMAN_MESSAGE_KEY:
                agent_memory.chat_memory.add_user_message(message["message_content"])
            elif message.get("message_type", None) == AI_MESSAGE_KEY:
                agent_memory.chat_memory.add_ai_message(message["message_content"])
        try:
            agent_memory.fit_max_token_limit()
        except Exception as e:
            import traceback

            traceback.print_exc()


MessageMemoryManager

            if self.name == "jupyter_kernel_pool":
                iterator = self.redis_client.scan_iter("jupyter_kernel_pool:*")
                for key in iterator:
                    user_id, chat_id = key.split(":")[1:]
                    yield user_id, chat_id, self.get_pool_info_with_id(user_id, chat_id, {})
            else:
                raise NotImplementedError("Currently only support jupyter kernel pool iteration in database mode.")

    def drop_item_with_id(self, user_id: str, chat_id: str):
        if self.backend == LOCAL:
            # drop item under one user
            if user_id in self.memory_pool:
                self.memory_pool[user_id].pop([chat_id], None)
        elif self.backend == DATABASE:
            if self.name == "jupyter_kernel_pool":
                self.redis_client.delete(f"{self.name}:{user_id}:{chat_id}")
            else:
                raise NotImplementedError("Currently only support jupyter kernel pool delete in database mode.")

class MessageMemoryManager(ChatMemoryManager):
    """A class to manage the memory of messages."""

    @staticmethod
    def load_agent_memory_from_list(agent_memory: BaseChatMemory, message_list: List[Dict[str, str]]) -> None:
        """Load agent's memory from a list."""
        agent_memory.clear()
        for message in message_list:
            if message.get("message_type", None) == HUMAN_MESSAGE_KEY:
                agent_memory.chat_memory.add_user_message(message["message_content"])
            elif message.get("message_type", None) == AI_MESSAGE_KEY:
                agent_memory.chat_memory.add_ai_message(message["message_content"])
        try:
            agent_memory.fit_max_token_limit()
        except Exception as e:
            import traceback

            traceback.print_exc()
            pass

    @staticmethod
    def save_agent_memory_to_list(agent_memory: BaseChatMemory) -> List[Dict[str, str]]:
        """Saves agent's memory to a list"""
        messages = agent_memory.chat_memory.messages
        message_list = []
        for message in messages:
            if message.type == "human":
                message_list.append(
                    {
                        "message_type": "human_message",
                        "message_content": message.content,
                    }
                )
            elif message.type == "ai":
                message_list.append(
                    {
                        "message_type": "ai_message",
                        "message_content": message.content,
                    }
                )
        return message_list

    def get_activated_message_list(
        self,
        user_id: str,
        chat_id: str,
        default_value: Union[List, Dict],
        parent_message_id: Union[int, str],
    ) -> List:
        """Gets the activated message list from leaf to root."""
        # ONLY work for messages
        message_list = self.get_pool_info_with_id(user_id, chat_id, default_value)
        activated_message_list = []
        end_point = parent_message_id
        while len(message_list) > 0 and end_point != -1:
            flag = False
            for msg in message_list:
                if msg["message_id"] == end_point:
                    if end_point == msg["parent_message_id"]:
                        flag = False
                        break
                    activated_message_list = [msg] + activated_message_list
                    end_point = msg["parent_message_id"]
                    flag = True
                    break
            if not flag:
                break
        logger.bind(msg_head=f"get_activated_message_list").debug(activated_message_list)
        return activated_message_list


            raise NotImplementedError("Currently not support UserMemoryManager iteration in database mode.")

class ChatMemoryManager:
    """A class to manage the global memory including messages, grounding_sources, etc. on chat level"""

    # memory_pool:
    # {user_id: {chat_id: [
    #                           {"message_id": the id of this pair of messages,
    #                            "parent_message_id": the message of the parent message,
    #                            "message_type": type of the message, possible values: human_message / ai_message
    #                            "message_content": content of the message
    #                           }
    #                     ]
    #           }
    # }
    # grounding_source_pool:
    # {user_id: {chat_id: {"filenames": List of filenames,
    #                      "activated_filenames": List of user-selected activated names}}

    def __init__(self, name: str = None, backend: str = LOCAL, memory_pool: Dict = None):
        """
        This ChatMemoryManager can not be applied to grounding_source_pool in database mode.
        """
        self.backend = backend
        self.name = name
        if self.backend == LOCAL:
            if memory_pool is None:
                memory_pool = {}
            self.memory_pool = memory_pool
        elif self.backend == DATABASE:
            with app.app_context():
                self.redis_client = get_running_time_storage()
                self.db_client = get_user_conversation_storage()
        else:
            raise ValueError("Unknown backend option: {}".format(self.backend))

    def get_pool_info_with_id(
        self,
        user_id: str,
        chat_id: str,
        default_value: Union[List, Dict],
    ) -> Any:
        """Gets the information with user_id and chat_id from the pool."""
        if self.backend == LOCAL:
            pool = self.memory_pool
            if user_id in pool and chat_id in pool[user_id]:
                return pool[user_id][chat_id]
            else:
                return default_value
        elif self.backend == DATABASE:
            memory_pool_name = f"{self.name}:{user_id}:{chat_id}"
            if self.redis_client.exists(memory_pool_name):
                # In cache
                info = json.loads(self.redis_client.get(memory_pool_name))
            else:


    # {user_id: {chat_id: {"filenames": List of filenames,
    #                      "activated_filenames": List of user-selected activated names}}

    def __init__(self, name: str = None, backend: str = LOCAL, memory_pool: Dict = None):
        """
        This ChatMemoryManager can not be applied to grounding_source_pool in database mode.
        """
        self.backend = backend
        self.name = name
        if self.backend == LOCAL:
            if memory_pool is None:
                memory_pool = {}
            self.memory_pool = memory_pool
        elif self.backend == DATABASE:
            with app.app_context():
                self.redis_client = get_running_time_storage()
                self.db_client = get_user_conversation_storage()
        else:
            raise ValueError("Unknown backend option: {}".format(self.backend))

    def get_pool_info_with_id(
        self,
        user_id: str,
        chat_id: str,
        default_value: Union[List, Dict],
    ) -> Any:
        """Gets the information with user_id and chat_id from the pool."""
        if self.backend == LOCAL:
            pool = self.memory_pool
            if user_id in pool and chat_id in pool[user_id]:
                return pool[user_id][chat_id]
            else:
                return default_value
        elif self.backend == DATABASE:
            memory_pool_name = f"{self.name}:{user_id}:{chat_id}"
            if self.redis_client.exists(memory_pool_name):
                # In cache
                info = json.loads(self.redis_client.get(memory_pool_name))
            else:
                # Cache miss
                try:
                    # Found in database
                    if self.name == "message_pool":
                        info = []
                        response = self.db_client.message.find({"conversation_id": chat_id})
                        if response is None:
                            # Not in database (new chat)
                            info = default_value
                        else:
                            # In database
                            for message in response:
                                if message["role"] == "user":
                                    message_type = HUMAN_MESSAGE_KEY
                                elif message["role"] == "assistant":
                                    message_type = AI_MESSAGE_KEY
                                else:
                                    raise ValueError("Unknown role: {}".format(message["role"]))
                                info.append(
                                    {
                                        "message_id": message["message_id"],
                                        "parent_message_id": message["parent_message_id"],
                                        "message_type": message_type,
                                        "message_content": message["data_for_llm"],
                                    }
                                )
                            self.redis_client.set(memory_pool_name, json.dumps(info))
                    elif self.name == "jupyter_kernel_pool":
                        info = default_value
                    else:
                        raise NotImplementedError(f"Currently only support message pool in database, not {self.name}")
                except Exception as e:
                    # Not in database
                    logger.bind(user_id=user_id, chat_id=chat_id, msg_head="Cache miss but not in database").warning(
                        "Failed to get pool info from database: {}".format(e)
                    )
                    info = default_value
            return info

    def set_pool_info_with_id(self, user_id: str, chat_id: str, info: Any) -> None:
        """Sets the information with user_id and chat_id to the pool."""
        if self.backend == LOCAL:
            pool = self.memory_pool
            if user_id not in pool:
                pool[user_id] = {}
            pool[user_id][chat_id] = info
        elif self.backend == DATABASE:
            # As db has its own updating logic, we only need to update the cache here (write-through).
            memory_pool_name = f"{self.name}:{user_id}:{chat_id}"
            self.redis_client.set(memory_pool_name, json.dumps(info))

    def __iter__(self):
        """Iterates over the memory pool."""
        if self.backend == LOCAL:
            for user_id, chat_id_info in self.memory_pool.items():
                for chat_id, info in chat_id_info.items():
                    yield user_id, chat_id, info
        elif self.backend == DATABASE:


                                    {
                                        "message_id": message["message_id"],
                                        "parent_message_id": message["parent_message_id"],
                                        "message_type": message_type,
                                        "message_content": message["data_for_llm"],
                                    }
                                )
                            self.redis_client.set(memory_pool_name, json.dumps(info))
                    elif self.name == "jupyter_kernel_pool":
                        info = default_value
                    else:
                        raise NotImplementedError(f"Currently only support message pool in database, not {self.name}")
                except Exception as e:
                    # Not in database
                    logger.bind(user_id=user_id, chat_id=chat_id, msg_head="Cache miss but not in database").warning(
                        "Failed to get pool info from database: {}".format(e)
                    )
                    info = default_value
            return info

    def set_pool_info_with_id(self, user_id: str, chat_id: str, info: Any) -> None:
        """Sets the information with user_id and chat_id to the pool."""
        if self.backend == LOCAL:
            pool = self.memory_pool
            if user_id not in pool:
                pool[user_id] = {}
            pool[user_id][chat_id] = info
        elif self.backend == DATABASE:
            # As db has its own updating logic, we only need to update the cache here (write-through).
            memory_pool_name = f"{self.name}:{user_id}:{chat_id}"
            self.redis_client.set(memory_pool_name, json.dumps(info))

    def __iter__(self):
        """Iterates over the memory pool."""
        if self.backend == LOCAL:
            for user_id, chat_id_info in self.memory_pool.items():
                for chat_id, info in chat_id_info.items():
                    yield user_id, chat_id, info
        elif self.backend == DATABASE:
            if self.name == "jupyter_kernel_pool":
                iterator = self.redis_client.scan_iter("jupyter_kernel_pool:*")
                for key in iterator:
                    user_id, chat_id = key.split(":")[1:]
                    yield user_id, chat_id, self.get_pool_info_with_id(user_id, chat_id, {})
            else:
                raise NotImplementedError("Currently only support jupyter kernel pool iteration in database mode.")

    def drop_item_with_id(self, user_id: str, chat_id: str):
        if self.backend == LOCAL:
            # drop item under one user
            if user_id in self.memory_pool:


                except Exception as e:
                    # Not in database
                    logger.bind(user_id=user_id, chat_id=chat_id, msg_head="Cache miss but not in database").warning(
                        "Failed to get pool info from database: {}".format(e)
                    )
                    info = default_value
            return info

    def set_pool_info_with_id(self, user_id: str, chat_id: str, info: Any) -> None:
        """Sets the information with user_id and chat_id to the pool."""
        if self.backend == LOCAL:
            pool = self.memory_pool
            if user_id not in pool:
                pool[user_id] = {}
            pool[user_id][chat_id] = info
        elif self.backend == DATABASE:
            # As db has its own updating logic, we only need to update the cache here (write-through).
            memory_pool_name = f"{self.name}:{user_id}:{chat_id}"
            self.redis_client.set(memory_pool_name, json.dumps(info))

    def __iter__(self):
        """Iterates over the memory pool."""
        if self.backend == LOCAL:
            for user_id, chat_id_info in self.memory_pool.items():
                for chat_id, info in chat_id_info.items():
                    yield user_id, chat_id, info
        elif self.backend == DATABASE:
            if self.name == "jupyter_kernel_pool":
                iterator = self.redis_client.scan_iter("jupyter_kernel_pool:*")
                for key in iterator:
                    user_id, chat_id = key.split(":")[1:]
                    yield user_id, chat_id, self.get_pool_info_with_id(user_id, chat_id, {})
            else:
                raise NotImplementedError("Currently only support jupyter kernel pool iteration in database mode.")

    def drop_item_with_id(self, user_id: str, chat_id: str):
        if self.backend == LOCAL:
            # drop item under one user
            if user_id in self.memory_pool:
                self.memory_pool[user_id].pop([chat_id], None)
        elif self.backend == DATABASE:
            if self.name == "jupyter_kernel_pool":
                self.redis_client.delete(f"{self.name}:{user_id}:{chat_id}")
            else:
                raise NotImplementedError("Currently only support jupyter kernel pool delete in database mode.")

class MessageMemoryManager(ChatMemoryManager):
    """A class to manage the memory of messages."""

    @staticmethod
    def load_agent_memory_from_list(agent_memory: BaseChatMemory, message_list: List[Dict[str, str]]) -> None:
        """Load agent's memory from a list."""
        agent_memory.clear()


        elif self.backend == DATABASE:
            # As db has its own updating logic, we only need to update the cache here (write-through).
            memory_pool_name = f"{self.name}:{user_id}:{chat_id}"
            self.redis_client.set(memory_pool_name, json.dumps(info))

    def __iter__(self):
        """Iterates over the memory pool."""
        if self.backend == LOCAL:
            for user_id, chat_id_info in self.memory_pool.items():
                for chat_id, info in chat_id_info.items():
                    yield user_id, chat_id, info
        elif self.backend == DATABASE:
            if self.name == "jupyter_kernel_pool":
                iterator = self.redis_client.scan_iter("jupyter_kernel_pool:*")
                for key in iterator:
                    user_id, chat_id = key.split(":")[1:]
                    yield user_id, chat_id, self.get_pool_info_with_id(user_id, chat_id, {})
            else:
                raise NotImplementedError("Currently only support jupyter kernel pool iteration in database mode.")

    def drop_item_with_id(self, user_id: str, chat_id: str):
        if self.backend == LOCAL:
            # drop item under one user
            if user_id in self.memory_pool:
                self.memory_pool[user_id].pop([chat_id], None)
        elif self.backend == DATABASE:
            if self.name == "jupyter_kernel_pool":
                self.redis_client.delete(f"{self.name}:{user_id}:{chat_id}")
            else:
                raise NotImplementedError("Currently only support jupyter kernel pool delete in database mode.")

class MessageMemoryManager(ChatMemoryManager):
    """A class to manage the memory of messages."""

    @staticmethod
    def load_agent_memory_from_list(agent_memory: BaseChatMemory, message_list: List[Dict[str, str]]) -> None:
        """Load agent's memory from a list."""
        agent_memory.clear()
        for message in message_list:
            if message.get("message_type", None) == HUMAN_MESSAGE_KEY:
                agent_memory.chat_memory.add_user_message(message["message_content"])
            elif message.get("message_type", None) == AI_MESSAGE_KEY:
                agent_memory.chat_memory.add_ai_message(message["message_content"])
        try:
            agent_memory.fit_max_token_limit()
        except Exception as e:
            import traceback

            traceback.print_exc()


                    yield user_id, chat_id, self.get_pool_info_with_id(user_id, chat_id, {})
            else:
                raise NotImplementedError("Currently only support jupyter kernel pool iteration in database mode.")

    def drop_item_with_id(self, user_id: str, chat_id: str):
        if self.backend == LOCAL:
            # drop item under one user
            if user_id in self.memory_pool:
                self.memory_pool[user_id].pop([chat_id], None)
        elif self.backend == DATABASE:
            if self.name == "jupyter_kernel_pool":
                self.redis_client.delete(f"{self.name}:{user_id}:{chat_id}")
            else:
                raise NotImplementedError("Currently only support jupyter kernel pool delete in database mode.")

class MessageMemoryManager(ChatMemoryManager):
    """A class to manage the memory of messages."""

    @staticmethod
    def load_agent_memory_from_list(agent_memory: BaseChatMemory, message_list: List[Dict[str, str]]) -> None:
        """Load agent's memory from a list."""
        agent_memory.clear()
        for message in message_list:
            if message.get("message_type", None) == HUMAN_MESSAGE_KEY:
                agent_memory.chat_memory.add_user_message(message["message_content"])
            elif message.get("message_type", None) == AI_MESSAGE_KEY:
                agent_memory.chat_memory.add_ai_message(message["message_content"])
        try:
            agent_memory.fit_max_token_limit()
        except Exception as e:
            import traceback

            traceback.print_exc()
            pass

    @staticmethod
    def save_agent_memory_to_list(agent_memory: BaseChatMemory) -> List[Dict[str, str]]:
        """Saves agent's memory to a list"""
        messages = agent_memory.chat_memory.messages
        message_list = []
        for message in messages:
            if message.type == "human":
                message_list.append(
                    {
                        "message_type": "human_message",
                        "message_content": message.content,
                    }
                )
            elif message.type == "ai":
                message_list.append(
                    {
                        "message_type": "ai_message",
                        "message_content": message.content,
                    }


    """A class to manage the memory of messages."""

    @staticmethod
    def load_agent_memory_from_list(agent_memory: BaseChatMemory, message_list: List[Dict[str, str]]) -> None:
        """Load agent's memory from a list."""
        agent_memory.clear()
        for message in message_list:
            if message.get("message_type", None) == HUMAN_MESSAGE_KEY:
                agent_memory.chat_memory.add_user_message(message["message_content"])
            elif message.get("message_type", None) == AI_MESSAGE_KEY:
                agent_memory.chat_memory.add_ai_message(message["message_content"])
        try:
            agent_memory.fit_max_token_limit()
        except Exception as e:
            import traceback

            traceback.print_exc()
            pass

    @staticmethod
    def save_agent_memory_to_list(agent_memory: BaseChatMemory) -> List[Dict[str, str]]:
        """Saves agent's memory to a list"""
        messages = agent_memory.chat_memory.messages
        message_list = []
        for message in messages:
            if message.type == "human":
                message_list.append(
                    {
                        "message_type": "human_message",
                        "message_content": message.content,
                    }
                )
            elif message.type == "ai":
                message_list.append(
                    {
                        "message_type": "ai_message",
                        "message_content": message.content,
                    }
                )
        return message_list

    def get_activated_message_list(
        self,
        user_id: str,
        chat_id: str,
        default_value: Union[List, Dict],
        parent_message_id: Union[int, str],
    ) -> List:
        """Gets the activated message list from leaf to root."""
        # ONLY work for messages
        message_list = self.get_pool_info_with_id(user_id, chat_id, default_value)
        activated_message_list = []
        end_point = parent_message_id
        while len(message_list) > 0 and end_point != -1:
            flag = False
            for msg in message_list:
                if msg["message_id"] == end_point:
                    if end_point == msg["parent_message_id"]:
                        flag = False
                        break


        """Saves agent's memory to a list"""
        messages = agent_memory.chat_memory.messages
        message_list = []
        for message in messages:
            if message.type == "human":
                message_list.append(
                    {
                        "message_type": "human_message",
                        "message_content": message.content,
                    }
                )
            elif message.type == "ai":
                message_list.append(
                    {
                        "message_type": "ai_message",
                        "message_content": message.content,
                    }
                )
        return message_list

    def get_activated_message_list(
        self,
        user_id: str,
        chat_id: str,
        default_value: Union[List, Dict],
        parent_message_id: Union[int, str],
    ) -> List:
        """Gets the activated message list from leaf to root."""
        # ONLY work for messages
        message_list = self.get_pool_info_with_id(user_id, chat_id, default_value)
        activated_message_list = []
        end_point = parent_message_id
        while len(message_list) > 0 and end_point != -1:
            flag = False
            for msg in message_list:
                if msg["message_id"] == end_point:
                    if end_point == msg["parent_message_id"]:
                        flag = False
                        break
                    activated_message_list = [msg] + activated_message_list
                    end_point = msg["parent_message_id"]
                    flag = True
                    break
            if not flag:
                break
        logger.bind(msg_head=f"get_activated_message_list").debug(activated_message_list)
        return activated_message_list


backend.app

import os

from flask import Flask
from flask_cors import CORS

app = Flask(__name__)
current_path = os.path.abspath(__file__)
app.config["UPLOAD_FOLDER"] = os.path.dirname(current_path) + "/data"
os.makedirs(app.config["UPLOAD_FOLDER"], exist_ok=True)
# Execute code locally or remotely on docker
app.config["CODE_EXECUTION_MODE"] = os.getenv("CODE_EXECUTION_MODE", "local")
CORS(app)


backend.schemas

APP_TYPES = ["copilot", "plugins", "webot"]
TIME_STEP = 0.035
TIME_OUT_MAP = {"copilot": 90, "plugins": 300, "webot": 600}
STREAM_BLOCK_TYPES = ["image", "echarts"]
STREAM_TOKEN_TYPES = ["tool", "transition", "execution_result", "error", "kaggle_search", "kaggle_connect", "plain"]
EXECUTION_RESULT_MAX_TOKENS_MAP = {"copilot": 1000, "plugins": 2000, "webot": 20000}

HEARTBEAT_INTERVAL = 10

# define error code
UNAUTH = 401
UNFOUND = 404
OVERLOAD = 503
INTERNAL = 500
UNSUPPORTED = 403

# define models which need extra continue flag
NEED_CONTINUE_MODEL = {"claude-v1", "claude-2"}
DEFAULT_USER_ID = "DefaultUser"


backend.main

import os
import warnings
import threading

from backend.app import app
from backend.kernel_publisher import start_kernel_publisher
from backend.utils.threading import ThreadManager
from backend.utils.utils import VariableRegister, init_log
from backend.memory import (
    ChatMemoryManager,
    MessageMemoryManager,
    UserMemoryManager,
)

warnings.filterwarnings("ignore", category=UserWarning)

logger = init_log(
    error=os.path.join(".logging", "error.log"),
    runtime=os.path.join(".logging", "runtime.log"),
    serialize=os.path.join(".logging", "serialize.log"),
    trace=os.path.join(".logging", "trace.log"),
)

VARIABLE_REGISTER_BACKEND = os.environ.get("VARIABLE_REGISTER_BACKEND", "local")
MESSAGE_MEMORY_MANAGER_BACKEND = os.environ.get("MESSAGE_MEMORY_MANAGER_BACKEND", "local")
API_KEY_MEMORY_MANAGER_BACKEND = os.environ.get("API_KEY_MEMORY_MANAGER_BACKEND", "local")
JUPYTER_KERNEL_MEMORY_MANAGER_BACKEND = os.environ.get("JUPYTER_KERNEL_MEMORY_MANAGER_BACKEND", "local")

message_pool: MessageMemoryManager = MessageMemoryManager(name="message_pool", backend=MESSAGE_MEMORY_MANAGER_BACKEND)
grounding_source_pool: ChatMemoryManager = ChatMemoryManager()
api_key_pool: UserMemoryManager = UserMemoryManager(name="api_key_pool", backend=API_KEY_MEMORY_MANAGER_BACKEND)
jupyter_kernel_pool: ChatMemoryManager = ChatMemoryManager(
    name="jupyter_kernel_pool", backend=JUPYTER_KERNEL_MEMORY_MANAGER_BACKEND
)
threading_pool: ThreadManager = ThreadManager()

message_id_register = VariableRegister(name="message_id_register", backend=VARIABLE_REGISTER_BACKEND)

# Monitor kernel and kill long running kernels
if app.config["CODE_EXECUTION_MODE"] == "docker":
    threading.Thread(target=start_kernel_publisher, args=(), daemon=True).start()

if __name__ == "__main__":
    import multiprocess

    multiprocess.set_start_method("spawn", True)
    app.run()


import redis
from typing import Any

from backend.utils.utils import logger
import os

r = redis.Redis(host=os.getenv("REDIS_SERVER"), port=6379, decode_responses=True)

# Set the queue and pending key
QUEUE_RUNNING = "kernel_running_queue"
QUEUE_PENDING = "kernel_pending_queue"
SUBMIT_EVENT = "job_submitted"
RUNNING_EVENT = "job_started"
COMPLETE_EVENT = "job_completed"

MAX_CONCURRENT_KERNELS = 300

def add_job_to_pending(job: Any) -> None:
    # always add the jobn to pending
    r.rpush(QUEUE_PENDING, job)

def move_pending_to_running() -> None:
    """Move pending jobs to the running queue."""
    # Get the first pending job
    job = r.lindex(QUEUE_PENDING, 0)
    if job is not None:
        logger.bind(msg_head="Job running").debug(job)
        # Move the job from pending queue to running queue
        r.rpush(QUEUE_RUNNING, job)
        # Notify the running channel
        r.publish(RUNNING_EVENT, job)
        # Remove the job from pending queue
        r.lpop(QUEUE_PENDING)

# Subscribe to job completion events
def handle_job_completion(message: dict) -> None:
    # the data should be the chat id
    chat_id = message["data"]
    logger.bind(msg_head="Job completed").debug(chat_id)
    # here we only care about the capacity, not caring about which one is poped now
    logger.bind(msg_head="Queue running").debug(r.lrange(QUEUE_RUNNING, 0, -1))
    r.lrem(QUEUE_RUNNING, 0, chat_id)
    move_pending_to_running()

def handle_new_job(message: dict) -> None:
    # the data should be the chat id
    chat_id = message["data"]
    logger.bind(msg_head="Job submitted").debug(chat_id)
    # all submitted jobs into pending queue
    add_job_to_pending(chat_id)
    # push the id to the pending queue
    logger.bind(msg_head="Queue pending").debug(r.lrange(QUEUE_PENDING, 0, -1))
    if r.llen(QUEUE_RUNNING) < MAX_CONCURRENT_KERNELS:
        move_pending_to_running()

def start_kernel_publisher() -> None:
    # Connect to Redis
    r.delete(QUEUE_RUNNING)
    r.delete(QUEUE_PENDING)
    # Start the publisher & subscriber
    p = r.pubsub()
    p.subscribe(**{COMPLETE_EVENT: handle_job_completion, SUBMIT_EVENT: handle_new_job})

    p.run_in_thread(sleep_time=0.1)


import redis
from typing import Any

from backend.utils.utils import logger
import os

r = redis.Redis(host=os.getenv("REDIS_SERVER"), port=6379, decode_responses=True)

# Set the queue and pending key
QUEUE_RUNNING = "kernel_running_queue"
QUEUE_PENDING = "kernel_pending_queue"
SUBMIT_EVENT = "job_submitted"
RUNNING_EVENT = "job_started"
COMPLETE_EVENT = "job_completed"

MAX_CONCURRENT_KERNELS = 300

def add_job_to_pending(job: Any) -> None:
    # always add the jobn to pending
    r.rpush(QUEUE_PENDING, job)

def move_pending_to_running() -> None:
    """Move pending jobs to the running queue."""
    # Get the first pending job
    job = r.lindex(QUEUE_PENDING, 0)
    if job is not None:
        logger.bind(msg_head="Job running").debug(job)
        # Move the job from pending queue to running queue
        r.rpush(QUEUE_RUNNING, job)
        # Notify the running channel
        r.publish(RUNNING_EVENT, job)
        # Remove the job from pending queue
        r.lpop(QUEUE_PENDING)

# Subscribe to job completion events
def handle_job_completion(message: dict) -> None:
    # the data should be the chat id
    chat_id = message["data"]


import os

r = redis.Redis(host=os.getenv("REDIS_SERVER"), port=6379, decode_responses=True)

# Set the queue and pending key
QUEUE_RUNNING = "kernel_running_queue"
QUEUE_PENDING = "kernel_pending_queue"
SUBMIT_EVENT = "job_submitted"
RUNNING_EVENT = "job_started"
COMPLETE_EVENT = "job_completed"

MAX_CONCURRENT_KERNELS = 300

def add_job_to_pending(job: Any) -> None:
    # always add the jobn to pending
    r.rpush(QUEUE_PENDING, job)

def move_pending_to_running() -> None:
    """Move pending jobs to the running queue."""
    # Get the first pending job
    job = r.lindex(QUEUE_PENDING, 0)
    if job is not None:
        logger.bind(msg_head="Job running").debug(job)
        # Move the job from pending queue to running queue
        r.rpush(QUEUE_RUNNING, job)
        # Notify the running channel
        r.publish(RUNNING_EVENT, job)
        # Remove the job from pending queue
        r.lpop(QUEUE_PENDING)

# Subscribe to job completion events
def handle_job_completion(message: dict) -> None:
    # the data should be the chat id
    chat_id = message["data"]
    logger.bind(msg_head="Job completed").debug(chat_id)
    # here we only care about the capacity, not caring about which one is poped now
    logger.bind(msg_head="Queue running").debug(r.lrange(QUEUE_RUNNING, 0, -1))
    r.lrem(QUEUE_RUNNING, 0, chat_id)
    move_pending_to_running()

def handle_new_job(message: dict) -> None:
    # the data should be the chat id
    chat_id = message["data"]
    logger.bind(msg_head="Job submitted").debug(chat_id)
    # all submitted jobs into pending queue
    add_job_to_pending(chat_id)
    # push the id to the pending queue


def add_job_to_pending(job: Any) -> None:
    # always add the jobn to pending
    r.rpush(QUEUE_PENDING, job)

def move_pending_to_running() -> None:
    """Move pending jobs to the running queue."""
    # Get the first pending job
    job = r.lindex(QUEUE_PENDING, 0)
    if job is not None:
        logger.bind(msg_head="Job running").debug(job)
        # Move the job from pending queue to running queue
        r.rpush(QUEUE_RUNNING, job)
        # Notify the running channel
        r.publish(RUNNING_EVENT, job)
        # Remove the job from pending queue
        r.lpop(QUEUE_PENDING)

# Subscribe to job completion events
def handle_job_completion(message: dict) -> None:
    # the data should be the chat id
    chat_id = message["data"]
    logger.bind(msg_head="Job completed").debug(chat_id)
    # here we only care about the capacity, not caring about which one is poped now
    logger.bind(msg_head="Queue running").debug(r.lrange(QUEUE_RUNNING, 0, -1))
    r.lrem(QUEUE_RUNNING, 0, chat_id)
    move_pending_to_running()

def handle_new_job(message: dict) -> None:
    # the data should be the chat id
    chat_id = message["data"]
    logger.bind(msg_head="Job submitted").debug(chat_id)
    # all submitted jobs into pending queue
    add_job_to_pending(chat_id)
    # push the id to the pending queue
    logger.bind(msg_head="Queue pending").debug(r.lrange(QUEUE_PENDING, 0, -1))
    if r.llen(QUEUE_RUNNING) < MAX_CONCURRENT_KERNELS:
        move_pending_to_running()

def start_kernel_publisher() -> None:
    # Connect to Redis
    r.delete(QUEUE_RUNNING)
    r.delete(QUEUE_PENDING)
    # Start the publisher & subscriber
    p = r.pubsub()


        logger.bind(msg_head="Job running").debug(job)
        # Move the job from pending queue to running queue
        r.rpush(QUEUE_RUNNING, job)
        # Notify the running channel
        r.publish(RUNNING_EVENT, job)
        # Remove the job from pending queue
        r.lpop(QUEUE_PENDING)

# Subscribe to job completion events
def handle_job_completion(message: dict) -> None:
    # the data should be the chat id
    chat_id = message["data"]
    logger.bind(msg_head="Job completed").debug(chat_id)
    # here we only care about the capacity, not caring about which one is poped now
    logger.bind(msg_head="Queue running").debug(r.lrange(QUEUE_RUNNING, 0, -1))
    r.lrem(QUEUE_RUNNING, 0, chat_id)
    move_pending_to_running()

def handle_new_job(message: dict) -> None:
    # the data should be the chat id
    chat_id = message["data"]
    logger.bind(msg_head="Job submitted").debug(chat_id)
    # all submitted jobs into pending queue
    add_job_to_pending(chat_id)
    # push the id to the pending queue
    logger.bind(msg_head="Queue pending").debug(r.lrange(QUEUE_PENDING, 0, -1))
    if r.llen(QUEUE_RUNNING) < MAX_CONCURRENT_KERNELS:
        move_pending_to_running()

def start_kernel_publisher() -> None:
    # Connect to Redis
    r.delete(QUEUE_RUNNING)
    r.delete(QUEUE_PENDING)
    # Start the publisher & subscriber
    p = r.pubsub()
    p.subscribe(**{COMPLETE_EVENT: handle_job_completion, SUBMIT_EVENT: handle_new_job})

    p.run_in_thread(sleep_time=0.1)


    chat_id = message["data"]
    logger.bind(msg_head="Job completed").debug(chat_id)
    # here we only care about the capacity, not caring about which one is poped now
    logger.bind(msg_head="Queue running").debug(r.lrange(QUEUE_RUNNING, 0, -1))
    r.lrem(QUEUE_RUNNING, 0, chat_id)
    move_pending_to_running()

def handle_new_job(message: dict) -> None:
    # the data should be the chat id
    chat_id = message["data"]
    logger.bind(msg_head="Job submitted").debug(chat_id)
    # all submitted jobs into pending queue
    add_job_to_pending(chat_id)
    # push the id to the pending queue
    logger.bind(msg_head="Queue pending").debug(r.lrange(QUEUE_PENDING, 0, -1))
    if r.llen(QUEUE_RUNNING) < MAX_CONCURRENT_KERNELS:
        move_pending_to_running()

def start_kernel_publisher() -> None:
    # Connect to Redis
    r.delete(QUEUE_RUNNING)
    r.delete(QUEUE_PENDING)
    # Start the publisher & subscriber
    p = r.pubsub()
    p.subscribe(**{COMPLETE_EVENT: handle_job_completion, SUBMIT_EVENT: handle_new_job})

    p.run_in_thread(sleep_time=0.1)


from typing import Dict, Optional, List
import json
import base64
import re
import ast

import mo_sql_parsing
from pydantic import BaseModel

from real_agents.adapters.data_model import MessageDataModel, DataModel

def is_json(text: str) -> bool:
    try:
        json.loads(text)
        return True
    except json.JSONDecodeError:
        return False

def split_text_and_code(text: str) -> List:
    pattern = r"(```[\s\S]+?```)"
    result = [x for x in re.split(pattern, text) if x.strip()]

    return result

def detect_code_type(code) -> str:
    # Attempt Python parsing
    try:
        ast.parse(code)
        return "python"
    except SyntaxError:
        pass

    # Attempt SQL parsing
    try:
        mo_sql_parsing.parse(code)
        return "sql"
    except:
        pass

    # If all else fails, it's probably plain text
    return "text"

def add_backticks(text: str) -> str:
    """Add backticks to code blocks."""
    text_type = detect_code_type(text)
    if is_json(text):
        text = "```json\n" + text + "\n```"
    elif text_type == "python":
        if not text.startswith("```") and not text.endswith("```"):
            text = "```python\n" + text + "\n```"
    elif text_type == "sql":
        if not text.startswith("```") and not text.endswith("```"):
            text = "```sql\n" + text + "\n```"
    return text

class DisplayStream(BaseModel):
    """The display stream to parse and render tokens and blocks"""

    streaming_mode: str = "plain"
    action: str = ""
    action_cache: str = ""
    execution_result_max_tokens: int = 1000
    escape: bool = False
    escape_cache: str = ""
    llm_call_id: int = -1

    def reset(self):
        self.streaming_mode = "plain"
        self.action = ""
        self.action_cache = ""
        self.escape = False
        self.escape_cache = ""

    def display(self, token: Dict) -> Optional[List[Dict]]:
        # Reset if the llm_call_id has changed
        if token["llm_call_id"] != self.llm_call_id:
            self.llm_call_id = token["llm_call_id"]
            self.reset()
        # Handle escape characters
        import codecs

        if token["text"] == "\\":
            self.escape = True
            self.escape_cache = "\\"
            return None
        else:
            if self.escape:
                try:
                    token["text"] = codecs.decode(self.escape_cache + token["text"], "unicode_escape")
                    self.escape = False
                    self.escape_cache = ""
                except Exception as e:
                    self.escape_cache += token["text"]
        # Tool selection
        if self.action != "" and token["type"] != "action":
            # An action has been generated
            if self.action != "Final Answer":
                _pretty_name = self.action
                self.action_cache = self.action
                self.action = ""
                return [{"text": _pretty_name, "type": "tool", "final": False}]
        if token["type"] == "plain":
            # Display plain text
            if self.streaming_mode == "identifier":
                return None
            else:
                self.streaming_mode = "plain"
                return [{"text": token["text"], "type": "transition", "final": False}]
        elif token["type"] == "identifier":
            self.streaming_mode = "identifier"
            return None
        elif token["type"] == "key":
            self.streaming_mode = "key"
            return None
        elif token["type"] == "action":
            self.streaming_mode = "action"
            self.action += token["text"]
            return None
        elif token["type"] == "action_input":
            self.streaming_mode = "action_input"
            if self.action == "Final Answer":
                return [{"text": token["text"], "type": "plain", "final": True}]
        elif token["type"] == "block":
            observation = token["text"]
            result = self._display_observation(observation=observation)
            return result
        else:
            raise ValueError("Unknown token type: {}".format(token["type"]))

    def _display_observation(self, observation: Dict) -> Optional[List]:
        """Display the observation, i.e., the response from the tool

        Args:
            observation: Tool response block

        Returns:
            A list of display blocks to the frontend
        """
        tool_response_list = []
        if isinstance(observation, str):
            # Observation is a plain text (not used)
            tool_response_list.append({"text": observation, "type": "plain", "final": False})
            return tool_response_list

        assert isinstance(observation, DataModel), "Observation must be a DataModel object"
        observation = observation.get_human_side_data()

        assert isinstance(observation, Dict), "Observation must be a Dict object"

        result = observation.get("result", "")
        result_str = str(result)
        # Code & Plugin block
        if "intermediate_steps" in observation:
            intermediate_steps = observation["intermediate_steps"]
            if self.action_cache == "PythonCodeBuilder":
                intermediate_steps = "```python\n" + intermediate_steps + "\n```"
            elif self.action_cache == "SQLCodeBuilder":
                intermediate_steps = "```sql\n" + intermediate_steps + "\n```"
            else:
                intermediate_steps = add_backticks(intermediate_steps)
            tool_response_list.append({"text": intermediate_steps, "type": "plain", "final": False})

        # Execution result
        if not observation["success"]:
            tool_response_list.append({"text": result_str, "type": "error", "final": False})
        else:
            result_str = MessageDataModel.truncate_text(result_str, max_token=self.execution_result_max_tokens)

            tool_response_list.append(
                {
                    "text": f"""```console\n{result_str.strip(' ').strip("```")}\n```"""
                    if result_str.strip("```")
                    else "",
                    "type": "execution_result",
                    "final": False,
                }
            )
        # Kaggle search and connect
        if "kaggle_action" in observation:
            kaggle_action = observation["kaggle_action"]
            tool_response_list.append(
                {
                    "text": json.dumps(observation["kaggle_output_info"]),
                    "type": f"kaggle_{kaggle_action}",
                    "final": False,
                }
            )
        # Image result, e.g., matplotlib
        if "images" in observation:
            try:
                for image in observation["images"]:
                    if isinstance(image, str):
                        continue
                    image_data_64 = "data:image/png;base64," + base64.b64encode(
                        base64.b64decode(image.data["image/png"])
                    ).decode("utf-8")
                    tool_response_list.append({"text": image_data_64, "type": "image", "final": False})
            except:
                tool_response_list.append(
                    {"text": "[ERROR]: error rendering image/png", "type": "error", "final": False}
                )
        # Echarts
        if "echarts" in observation:
            chart_json = observation["echarts"]

            if is_json(chart_json):
                tool_response_list.append({"text": chart_json, "type": "echarts", "final": False})
            else:
                tool_response_list.append({"text": f"""```json{chart_json}```""", "type": "plain", "final": False})

        return tool_response_list


from typing import Dict, Optional, List
import json
import base64
import re
import ast

import mo_sql_parsing
from pydantic import BaseModel

from real_agents.adapters.data_model import MessageDataModel, DataModel

def is_json(text: str) -> bool:
    try:
        json.loads(text)
        return True
    except json.JSONDecodeError:
        return False

def split_text_and_code(text: str) -> List:
    pattern = r"(```[\s\S]+?```)"
    result = [x for x in re.split(pattern, text) if x.strip()]

    return result

def detect_code_type(code) -> str:
    # Attempt Python parsing
    try:
        ast.parse(code)
        return "python"
    except SyntaxError:
        pass

    # Attempt SQL parsing
    try:
        mo_sql_parsing.parse(code)


from typing import Dict, Optional, List
import json
import base64
import re
import ast

import mo_sql_parsing
from pydantic import BaseModel

from real_agents.adapters.data_model import MessageDataModel, DataModel

def is_json(text: str) -> bool:
    try:
        json.loads(text)
        return True
    except json.JSONDecodeError:
        return False

def split_text_and_code(text: str) -> List:
    pattern = r"(```[\s\S]+?```)"
    result = [x for x in re.split(pattern, text) if x.strip()]

    return result

def detect_code_type(code) -> str:
    # Attempt Python parsing
    try:
        ast.parse(code)
        return "python"
    except SyntaxError:
        pass

    # Attempt SQL parsing
    try:
        mo_sql_parsing.parse(code)
        return "sql"
    except:
        pass

    # If all else fails, it's probably plain text
    return "text"


from pydantic import BaseModel

from real_agents.adapters.data_model import MessageDataModel, DataModel

def is_json(text: str) -> bool:
    try:
        json.loads(text)
        return True
    except json.JSONDecodeError:
        return False

def split_text_and_code(text: str) -> List:
    pattern = r"(```[\s\S]+?```)"
    result = [x for x in re.split(pattern, text) if x.strip()]

    return result

def detect_code_type(code) -> str:
    # Attempt Python parsing
    try:
        ast.parse(code)
        return "python"
    except SyntaxError:
        pass

    # Attempt SQL parsing
    try:
        mo_sql_parsing.parse(code)
        return "sql"
    except:
        pass

    # If all else fails, it's probably plain text
    return "text"

def add_backticks(text: str) -> str:
    """Add backticks to code blocks."""
    text_type = detect_code_type(text)
    if is_json(text):
        text = "```json\n" + text + "\n```"
    elif text_type == "python":
        if not text.startswith("```") and not text.endswith("```"):
            text = "```python\n" + text + "\n```"
    elif text_type == "sql":
        if not text.startswith("```") and not text.endswith("```"):
            text = "```sql\n" + text + "\n```"
    return text

class DisplayStream(BaseModel):
    """The display stream to parse and render tokens and blocks"""

    streaming_mode: str = "plain"


def detect_code_type(code) -> str:
    # Attempt Python parsing
    try:
        ast.parse(code)
        return "python"
    except SyntaxError:
        pass

    # Attempt SQL parsing
    try:
        mo_sql_parsing.parse(code)
        return "sql"
    except:
        pass

    # If all else fails, it's probably plain text
    return "text"

def add_backticks(text: str) -> str:
    """Add backticks to code blocks."""
    text_type = detect_code_type(text)
    if is_json(text):
        text = "```json\n" + text + "\n```"
    elif text_type == "python":
        if not text.startswith("```") and not text.endswith("```"):
            text = "```python\n" + text + "\n```"
    elif text_type == "sql":
        if not text.startswith("```") and not text.endswith("```"):
            text = "```sql\n" + text + "\n```"
    return text

class DisplayStream(BaseModel):
    """The display stream to parse and render tokens and blocks"""

    streaming_mode: str = "plain"
    action: str = ""
    action_cache: str = ""
    execution_result_max_tokens: int = 1000
    escape: bool = False
    escape_cache: str = ""
    llm_call_id: int = -1

    def reset(self):
        self.streaming_mode = "plain"
        self.action = ""
        self.action_cache = ""
        self.escape = False
        self.escape_cache = ""


DisplayStream

        pass

    # If all else fails, it's probably plain text
    return "text"

def add_backticks(text: str) -> str:
    """Add backticks to code blocks."""
    text_type = detect_code_type(text)
    if is_json(text):
        text = "```json\n" + text + "\n```"
    elif text_type == "python":
        if not text.startswith("```") and not text.endswith("```"):
            text = "```python\n" + text + "\n```"
    elif text_type == "sql":
        if not text.startswith("```") and not text.endswith("```"):
            text = "```sql\n" + text + "\n```"
    return text

class DisplayStream(BaseModel):
    """The display stream to parse and render tokens and blocks"""

    streaming_mode: str = "plain"
    action: str = ""
    action_cache: str = ""
    execution_result_max_tokens: int = 1000
    escape: bool = False
    escape_cache: str = ""
    llm_call_id: int = -1

    def reset(self):
        self.streaming_mode = "plain"
        self.action = ""
        self.action_cache = ""
        self.escape = False
        self.escape_cache = ""

    def display(self, token: Dict) -> Optional[List[Dict]]:
        # Reset if the llm_call_id has changed
        if token["llm_call_id"] != self.llm_call_id:
            self.llm_call_id = token["llm_call_id"]
            self.reset()
        # Handle escape characters
        import codecs

        if token["text"] == "\\":
            self.escape = True
            self.escape_cache = "\\"
            return None
        else:
            if self.escape:
                try:
                    token["text"] = codecs.decode(self.escape_cache + token["text"], "unicode_escape")
                    self.escape = False
                    self.escape_cache = ""
                except Exception as e:
                    self.escape_cache += token["text"]
        # Tool selection
        if self.action != "" and token["type"] != "action":
            # An action has been generated
            if self.action != "Final Answer":
                _pretty_name = self.action
                self.action_cache = self.action
                self.action = ""
                return [{"text": _pretty_name, "type": "tool", "final": False}]
        if token["type"] == "plain":
            # Display plain text
            if self.streaming_mode == "identifier":
                return None
            else:
                self.streaming_mode = "plain"
                return [{"text": token["text"], "type": "transition", "final": False}]
        elif token["type"] == "identifier":
            self.streaming_mode = "identifier"
            return None
        elif token["type"] == "key":
            self.streaming_mode = "key"
            return None
        elif token["type"] == "action":
            self.streaming_mode = "action"
            self.action += token["text"]
            return None
        elif token["type"] == "action_input":
            self.streaming_mode = "action_input"
            if self.action == "Final Answer":
                return [{"text": token["text"], "type": "plain", "final": True}]
        elif token["type"] == "block":
            observation = token["text"]
            result = self._display_observation(observation=observation)
            return result
        else:
            raise ValueError("Unknown token type: {}".format(token["type"]))

    def _display_observation(self, observation: Dict) -> Optional[List]:
        """Display the observation, i.e., the response from the tool

        Args:
            observation: Tool response block

        Returns:
            A list of display blocks to the frontend
        """
        tool_response_list = []
        if isinstance(observation, str):
            # Observation is a plain text (not used)
            tool_response_list.append({"text": observation, "type": "plain", "final": False})
            return tool_response_list

        assert isinstance(observation, DataModel), "Observation must be a DataModel object"
        observation = observation.get_human_side_data()

        assert isinstance(observation, Dict), "Observation must be a Dict object"

        result = observation.get("result", "")
        result_str = str(result)
        # Code & Plugin block
        if "intermediate_steps" in observation:
            intermediate_steps = observation["intermediate_steps"]
            if self.action_cache == "PythonCodeBuilder":
                intermediate_steps = "```python\n" + intermediate_steps + "\n```"
            elif self.action_cache == "SQLCodeBuilder":
                intermediate_steps = "```sql\n" + intermediate_steps + "\n```"
            else:
                intermediate_steps = add_backticks(intermediate_steps)
            tool_response_list.append({"text": intermediate_steps, "type": "plain", "final": False})

        # Execution result
        if not observation["success"]:
            tool_response_list.append({"text": result_str, "type": "error", "final": False})
        else:
            result_str = MessageDataModel.truncate_text(result_str, max_token=self.execution_result_max_tokens)

            tool_response_list.append(
                {
                    "text": f"""```console\n{result_str.strip(' ').strip("```")}\n```"""
                    if result_str.strip("```")
                    else "",
                    "type": "execution_result",
                    "final": False,
                }
            )
        # Kaggle search and connect
        if "kaggle_action" in observation:
            kaggle_action = observation["kaggle_action"]
            tool_response_list.append(
                {
                    "text": json.dumps(observation["kaggle_output_info"]),
                    "type": f"kaggle_{kaggle_action}",
                    "final": False,
                }
            )
        # Image result, e.g., matplotlib
        if "images" in observation:
            try:
                for image in observation["images"]:
                    if isinstance(image, str):
                        continue
                    image_data_64 = "data:image/png;base64," + base64.b64encode(
                        base64.b64decode(image.data["image/png"])
                    ).decode("utf-8")
                    tool_response_list.append({"text": image_data_64, "type": "image", "final": False})
            except:
                tool_response_list.append(
                    {"text": "[ERROR]: error rendering image/png", "type": "error", "final": False}
                )
        # Echarts
        if "echarts" in observation:
            chart_json = observation["echarts"]

            if is_json(chart_json):
                tool_response_list.append({"text": chart_json, "type": "echarts", "final": False})
            else:
                tool_response_list.append({"text": f"""```json{chart_json}```""", "type": "plain", "final": False})

        return tool_response_list


reset

    elif text_type == "python":
        if not text.startswith("```") and not text.endswith("```"):
            text = "```python\n" + text + "\n```"
    elif text_type == "sql":
        if not text.startswith("```") and not text.endswith("```"):
            text = "```sql\n" + text + "\n```"
    return text

class DisplayStream(BaseModel):
    """The display stream to parse and render tokens and blocks"""

    streaming_mode: str = "plain"
    action: str = ""
    action_cache: str = ""
    execution_result_max_tokens: int = 1000
    escape: bool = False
    escape_cache: str = ""
    llm_call_id: int = -1

    def reset(self):
        self.streaming_mode = "plain"
        self.action = ""
        self.action_cache = ""
        self.escape = False
        self.escape_cache = ""

    def display(self, token: Dict) -> Optional[List[Dict]]:
        # Reset if the llm_call_id has changed
        if token["llm_call_id"] != self.llm_call_id:
            self.llm_call_id = token["llm_call_id"]
            self.reset()
        # Handle escape characters
        import codecs

        if token["text"] == "\\":
            self.escape = True
            self.escape_cache = "\\"
            return None
        else:
            if self.escape:
                try:
                    token["text"] = codecs.decode(self.escape_cache + token["text"], "unicode_escape")
                    self.escape = False
                    self.escape_cache = ""
                except Exception as e:


display

class DisplayStream(BaseModel):
    """The display stream to parse and render tokens and blocks"""

    streaming_mode: str = "plain"
    action: str = ""
    action_cache: str = ""
    execution_result_max_tokens: int = 1000
    escape: bool = False
    escape_cache: str = ""
    llm_call_id: int = -1

    def reset(self):
        self.streaming_mode = "plain"
        self.action = ""
        self.action_cache = ""
        self.escape = False
        self.escape_cache = ""

    def display(self, token: Dict) -> Optional[List[Dict]]:
        # Reset if the llm_call_id has changed
        if token["llm_call_id"] != self.llm_call_id:
            self.llm_call_id = token["llm_call_id"]
            self.reset()
        # Handle escape characters
        import codecs

        if token["text"] == "\\":
            self.escape = True
            self.escape_cache = "\\"
            return None
        else:
            if self.escape:
                try:
                    token["text"] = codecs.decode(self.escape_cache + token["text"], "unicode_escape")
                    self.escape = False
                    self.escape_cache = ""
                except Exception as e:
                    self.escape_cache += token["text"]
        # Tool selection
        if self.action != "" and token["type"] != "action":
            # An action has been generated
            if self.action != "Final Answer":
                _pretty_name = self.action
                self.action_cache = self.action
                self.action = ""
                return [{"text": _pretty_name, "type": "tool", "final": False}]
        if token["type"] == "plain":
            # Display plain text
            if self.streaming_mode == "identifier":
                return None
            else:
                self.streaming_mode = "plain"
                return [{"text": token["text"], "type": "transition", "final": False}]
        elif token["type"] == "identifier":
            self.streaming_mode = "identifier"
            return None
        elif token["type"] == "key":
            self.streaming_mode = "key"
            return None
        elif token["type"] == "action":
            self.streaming_mode = "action"
            self.action += token["text"]
            return None
        elif token["type"] == "action_input":
            self.streaming_mode = "action_input"
            if self.action == "Final Answer":
                return [{"text": token["text"], "type": "plain", "final": True}]
        elif token["type"] == "block":
            observation = token["text"]
            result = self._display_observation(observation=observation)
            return result
        else:
            raise ValueError("Unknown token type: {}".format(token["type"]))

    def _display_observation(self, observation: Dict) -> Optional[List]:
        """Display the observation, i.e., the response from the tool

        Args:
            observation: Tool response block

        Returns:
            A list of display blocks to the frontend
        """
        tool_response_list = []
        if isinstance(observation, str):
            # Observation is a plain text (not used)
            tool_response_list.append({"text": observation, "type": "plain", "final": False})
            return tool_response_list

        assert isinstance(observation, DataModel), "Observation must be a DataModel object"
        observation = observation.get_human_side_data()

        assert isinstance(observation, Dict), "Observation must be a Dict object"


            self.streaming_mode = "identifier"
            return None
        elif token["type"] == "key":
            self.streaming_mode = "key"
            return None
        elif token["type"] == "action":
            self.streaming_mode = "action"
            self.action += token["text"]
            return None
        elif token["type"] == "action_input":
            self.streaming_mode = "action_input"
            if self.action == "Final Answer":
                return [{"text": token["text"], "type": "plain", "final": True}]
        elif token["type"] == "block":
            observation = token["text"]
            result = self._display_observation(observation=observation)
            return result
        else:
            raise ValueError("Unknown token type: {}".format(token["type"]))

    def _display_observation(self, observation: Dict) -> Optional[List]:
        """Display the observation, i.e., the response from the tool

        Args:
            observation: Tool response block

        Returns:
            A list of display blocks to the frontend
        """
        tool_response_list = []
        if isinstance(observation, str):
            # Observation is a plain text (not used)
            tool_response_list.append({"text": observation, "type": "plain", "final": False})
            return tool_response_list

        assert isinstance(observation, DataModel), "Observation must be a DataModel object"
        observation = observation.get_human_side_data()

        assert isinstance(observation, Dict), "Observation must be a Dict object"

        result = observation.get("result", "")
        result_str = str(result)
        # Code & Plugin block
        if "intermediate_steps" in observation:
            intermediate_steps = observation["intermediate_steps"]
            if self.action_cache == "PythonCodeBuilder":
                intermediate_steps = "```python\n" + intermediate_steps + "\n```"
            elif self.action_cache == "SQLCodeBuilder":
                intermediate_steps = "```sql\n" + intermediate_steps + "\n```"
            else:
                intermediate_steps = add_backticks(intermediate_steps)
            tool_response_list.append({"text": intermediate_steps, "type": "plain", "final": False})

        # Execution result
        if not observation["success"]:
            tool_response_list.append({"text": result_str, "type": "error", "final": False})
        else:
            result_str = MessageDataModel.truncate_text(result_str, max_token=self.execution_result_max_tokens)

            tool_response_list.append(
                {
                    "text": f"""```console\n{result_str.strip(' ').strip("```")}\n```"""
                    if result_str.strip("```")
                    else "",
                    "type": "execution_result",
                    "final": False,
                }
            )
        # Kaggle search and connect
        if "kaggle_action" in observation:
            kaggle_action = observation["kaggle_action"]
            tool_response_list.append(
                {
                    "text": json.dumps(observation["kaggle_output_info"]),
                    "type": f"kaggle_{kaggle_action}",
                    "final": False,
                }
            )
        # Image result, e.g., matplotlib
        if "images" in observation:
            try:
                for image in observation["images"]:
                    if isinstance(image, str):
                        continue
                    image_data_64 = "data:image/png;base64," + base64.b64encode(
                        base64.b64decode(image.data["image/png"])
                    ).decode("utf-8")
                    tool_response_list.append({"text": image_data_64, "type": "image", "final": False})
            except:
                tool_response_list.append(
                    {"text": "[ERROR]: error rendering image/png", "type": "error", "final": False}
                )
        # Echarts
        if "echarts" in observation:
            chart_json = observation["echarts"]

            if is_json(chart_json):
                tool_response_list.append({"text": chart_json, "type": "echarts", "final": False})
            else:
                tool_response_list.append({"text": f"""```json{chart_json}```""", "type": "plain", "final": False})

        return tool_response_list


import redis
from flask import g
import os

def get_running_time_storage():
    """Connects to redis."""
    if "running_time_storage" not in g:
        g.running_time_storage = redis.Redis(host=os.getenv("REDIS_SERVER"), port=6379, decode_responses=True)
        # Set maxmemory to 200MB (value is in bytes)
        g.running_time_storage.config_set("maxmemory", "500000000")
        # Set maxmemory policy to allkeys-lru (Least Recently Used)
        g.running_time_storage.config_set("maxmemory-policy", "allkeys-lru")
    return g.running_time_storage


import pymongo
from flask import g
import os

def get_user_conversation_storage():
    """Connects to mongodb."""
    if "user_conversation_storage" not in g:
        g.user_conversation_storage = pymongo.MongoClient("mongodb://{0}:27017/".format(os.getenv("MONGO_SERVER")))
    return g.user_conversation_storage["xlang"]

def close_user_conversation_storage():
    """Closes mongodb."""
    user_conversation_storage = g.pop("user_conversation_storage", None)
    if user_conversation_storage is not None:
        user_conversation_storage["xlang"].close()