From 6f3d361a3f03f48fc3e353567351defa7ab8e58e Mon Sep 17 00:00:00 2001 From: X-TRON404 Date: Thu, 6 Mar 2025 18:46:26 +0530 Subject: [PATCH 01/20] feat: agent_id associated with each record --- camel/memories/records.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/camel/memories/records.py b/camel/memories/records.py index 2f3be051da..bac344d4a5 100644 --- a/camel/memories/records.py +++ b/camel/memories/records.py @@ -39,6 +39,8 @@ class MemoryRecord(BaseModel): key-value pairs that provide more information. If not given, it will be an empty `Dict`. timestamp (float, optional): The timestamp when the record was created. + agent_id (str): The identifier of the agent associated with this + memory. """ model_config = ConfigDict(arbitrary_types_allowed=True) @@ -50,6 +52,7 @@ class MemoryRecord(BaseModel): timestamp: float = Field( default_factory=lambda: datetime.now(timezone.utc).timestamp() ) + agent_id: str = Field(default="") _MESSAGE_TYPES: ClassVar[dict] = { "BaseMessage": BaseMessage, @@ -73,6 +76,7 @@ def from_dict(cls, record_dict: Dict[str, Any]) -> "MemoryRecord": role_at_backend=record_dict["role_at_backend"], extra_info=record_dict["extra_info"], timestamp=record_dict["timestamp"], + agent_id=record_dict["agent_id"], ) def to_dict(self) -> Dict[str, Any]: @@ -88,6 +92,7 @@ def to_dict(self) -> Dict[str, Any]: "role_at_backend": self.role_at_backend, "extra_info": self.extra_info, "timestamp": self.timestamp, + "agent_id": self.agent_id, } def to_openai_message(self) -> OpenAIMessage: From 115dc4099228fd46a36ae7f5af03b2d77283603d Mon Sep 17 00:00:00 2001 From: X-TRON404 Date: Thu, 6 Mar 2025 18:50:21 +0530 Subject: [PATCH 02/20] feat: agent_id for each chatagent and agent specific memory interface - Each ChatAgent gets a unique agent_id - This agent_id is associated with each memory record - This allows the ChatAgent to save and retrieve agent specific memories - Also added memory persistence that allows the ChatAgent to directly save the memories to a .json file. --- camel/agents/chat_agent.py | 108 ++++++++++++++---- camel/memories/agent_memories.py | 22 +++- camel/memories/base.py | 12 ++ camel/memories/blocks/chat_history_block.py | 9 +- camel/storages/key_value_storages/base.py | 9 +- .../storages/key_value_storages/in_memory.py | 19 ++- camel/storages/key_value_storages/json.py | 18 ++- camel/storages/key_value_storages/redis.py | 16 ++- 8 files changed, 180 insertions(+), 33 deletions(-) diff --git a/camel/agents/chat_agent.py b/camel/agents/chat_agent.py index 3734dfe844..9d82e02480 100644 --- a/camel/agents/chat_agent.py +++ b/camel/agents/chat_agent.py @@ -16,7 +16,10 @@ import json import logging import textwrap +import uuid from collections import defaultdict +from datetime import datetime +from pathlib import Path from typing import ( TYPE_CHECKING, Any, @@ -59,6 +62,7 @@ ) from camel.prompts import TextPrompt from camel.responses import ChatAgentResponse +from camel.storages.key_value_storages.json import JsonStorage from camel.toolkits import FunctionTool from camel.types import ( ChatCompletion, @@ -138,6 +142,8 @@ class ChatAgent(BaseAgent): the next model in ModelManager. (default: :str:`round_robin`) single_iteration (bool): Whether to let the agent perform only one model calling at each step. (default: :obj:`False`) + agent_id (str, optional): The ID of the agent. If not provided, a + random UUID will be generated. (default: :obj:`None`) """ def __init__( @@ -157,6 +163,7 @@ def __init__( response_terminators: Optional[List[ResponseTerminator]] = None, scheduling_strategy: str = "round_robin", single_iteration: bool = False, + agent_id: Optional[str] = None, ) -> None: # Set up model backend self.model_backend = ModelManager( @@ -171,14 +178,21 @@ def __init__( scheduling_strategy=scheduling_strategy, ) self.model_type = self.model_backend.model_type + # Assign unique ID + self.agent_id = agent_id if agent_id else str(uuid.uuid4()) + + self.token_limit = token_limit + self.message_window_size = message_window_size # Set up memory - context_creator = ScoreBasedContextCreator( + self.context_creator = ScoreBasedContextCreator( self.model_backend.token_counter, - token_limit or self.model_backend.token_limit, + self.token_limit or self.model_backend.token_limit, ) self.memory: AgentMemory = memory or ChatHistoryMemory( - context_creator, window_size=message_window_size + self.context_creator, + window_size=self.message_window_size, + agent_id=self.agent_id, ) # Set up system message and initialize messages @@ -321,9 +335,77 @@ def update_memory( role (OpenAIBackendRole): The backend role type. """ self.memory.write_record( - MemoryRecord(message=message, role_at_backend=role) + MemoryRecord( + message=message, + role_at_backend=role, + timestamp=datetime.now().timestamp(), + agent_id=self.agent_id, + ) ) + def load_memory(self, memory: AgentMemory) -> None: + r"""Load the provided memory into the agent. + + Args: + memory (AgentMemory): The memory to load into the agent. + + Returns: + None + """ + + for context_record in memory.retrieve(): + self.memory.write_record(context_record.memory_record) + print(f"Memory loaded from {memory}") + + def load_memory_from_path(self, path: str) -> None: + r""" + Loads memory records from a JSON file filtered by this agent's ID. + + Args: + path (str): The file path to a JSON memory file that uses + JsonStorage. + + Raises: + ValueError: If no matching records for the agent_id are found + (optional check; commented out below). + """ + json_store = JsonStorage(Path(path)) + all_records = json_store.load(agent_id=self.agent_id) + + if not all_records: + raise ValueError( + f"No records found for agent_id={self.agent_id} in {path}" + ) + + for record_dict in all_records: + record = MemoryRecord.from_dict(record_dict) + self.memory.write_records([record]) + print(f"Memory loaded from {path}") + + def save_memory(self, path: str) -> None: + r""" + Retrieves the current conversation data from memory and writes it + into a JSON file using JsonStorage. + + Args: + path (str): Target file path to store JSON data. + """ + json_store = JsonStorage(Path(path)) + context_records = self.memory.retrieve() + to_save = [cr.memory_record.to_dict() for cr in context_records] + json_store.save(to_save) + print(f"Memory saved to {path}") + + def clear_memory(self) -> None: + r"""Clear the agent's memory and reset to initial state. + + Returns: + None + """ + self.memory.clear() + if self.system_message is not None: + self.update_memory(self.system_message, OpenAIBackendRole.SYSTEM) + def _generate_system_message_for_output_language( self, ) -> Optional[BaseMessage]: @@ -694,18 +776,11 @@ def _get_model_response( f"index: {self.model_backend.current_model_index}", exc_info=exc, ) - error_info = str(exc) - - if not response and self.model_backend.num_models > 1: + if not response: raise ModelProcessingError( "Unable to process messages: none of the provided models " "run succesfully." ) - elif not response: - raise ModelProcessingError( - f"Unable to process messages: the only provided model " - f"did not run succesfully. Error: {error_info}" - ) logger.info( f"Model {self.model_backend.model_type}, " @@ -739,18 +814,11 @@ async def _aget_model_response( f"index: {self.model_backend.current_model_index}", exc_info=exc, ) - error_info = str(exc) - - if not response and self.model_backend.num_models > 1: + if not response: raise ModelProcessingError( "Unable to process messages: none of the provided models " "run succesfully." ) - elif not response: - raise ModelProcessingError( - f"Unable to process messages: the only provided model " - f"did not run succesfully. Error: {error_info}" - ) logger.info( f"Model {self.model_backend.model_type}, " diff --git a/camel/memories/agent_memories.py b/camel/memories/agent_memories.py index f6e401a662..d03da58df4 100644 --- a/camel/memories/agent_memories.py +++ b/camel/memories/agent_memories.py @@ -33,6 +33,8 @@ class ChatHistoryMemory(AgentMemory): window_size (int, optional): The number of recent chat messages to retrieve. If not provided, the entire chat history will be retrieved. (default: :obj:`None`) + agent_id (str, optional): The ID of the agent associated with the chat + history. """ def __init__( @@ -40,6 +42,7 @@ def __init__( context_creator: BaseContextCreator, storage: Optional[BaseKeyValueStorage] = None, window_size: Optional[int] = None, + agent_id: Optional[str] = None, ) -> None: if window_size is not None and not isinstance(window_size, int): raise TypeError("`window_size` must be an integer or None.") @@ -47,7 +50,10 @@ def __init__( raise ValueError("`window_size` must be non-negative.") self._context_creator = context_creator self._window_size = window_size - self._chat_history_block = ChatHistoryBlock(storage=storage) + self._chat_history_block = ChatHistoryBlock( + storage=storage, agent_id=agent_id + ) + self._agent_id = agent_id def retrieve(self) -> List[ContextRecord]: records = self._chat_history_block.retrieve(self._window_size) @@ -84,6 +90,8 @@ class VectorDBMemory(AgentMemory): (default: :obj:`None`) retrieve_limit (int, optional): The maximum number of messages to be added into the context. (default: :obj:`3`) + agent_id (str, optional): The ID of the agent associated with + the messages stored in the vector database. """ def __init__( @@ -91,6 +99,7 @@ def __init__( context_creator: BaseContextCreator, storage: Optional[BaseVectorStorage] = None, retrieve_limit: int = 3, + agent_id: Optional[str] = None, ) -> None: self._context_creator = context_creator self._retrieve_limit = retrieve_limit @@ -133,6 +142,8 @@ class LongtermAgentMemory(AgentMemory): (default: :obj:`None`) retrieve_limit (int, optional): The maximum number of messages to be added into the context. (default: :obj:`3`) + agent_id (str, optional): The ID of the agent associated with the chat + history and the messages stored in the vector database. """ def __init__( @@ -141,12 +152,16 @@ def __init__( chat_history_block: Optional[ChatHistoryBlock] = None, vector_db_block: Optional[VectorDBBlock] = None, retrieve_limit: int = 3, + agent_id: Optional[str] = None, ) -> None: - self.chat_history_block = chat_history_block or ChatHistoryBlock() + self.chat_history_block = chat_history_block or ChatHistoryBlock( + agent_id=agent_id + ) self.vector_db_block = vector_db_block or VectorDBBlock() self.retrieve_limit = retrieve_limit self._context_creator = context_creator self._current_topic: str = "" + self._agent_id = agent_id def get_context_creator(self) -> BaseContextCreator: r"""Returns the context creator used by the memory. @@ -166,7 +181,8 @@ def retrieve(self) -> List[ContextRecord]: """ chat_history = self.chat_history_block.retrieve() vector_db_retrieve = self.vector_db_block.retrieve( - self._current_topic, self.retrieve_limit + self._current_topic, + self.retrieve_limit, ) return chat_history[:1] + vector_db_retrieve + chat_history[1:] diff --git a/camel/memories/base.py b/camel/memories/base.py index 57865236c7..e0a5da76dd 100644 --- a/camel/memories/base.py +++ b/camel/memories/base.py @@ -138,3 +138,15 @@ def get_context(self) -> Tuple[List[OpenAIMessage], int]: context in OpenAIMessage format and the total token count. """ return self.get_context_creator().create_context(self.retrieve()) + + def __repr__(self) -> str: + r"""Returns a string representation of the AgentMemory. + + Returns: + str: A string in the format 'ClassName(agent_id=)' + if agent_id exists, otherwise just 'ClassName()'. + """ + agent_id = getattr(self, '_agent_id', None) + if agent_id: + return f"{self.__class__.__name__}(agent_id='{agent_id}')" + return f"{self.__class__.__name__}()" diff --git a/camel/memories/blocks/chat_history_block.py b/camel/memories/blocks/chat_history_block.py index 68218dbb41..646ae1c060 100644 --- a/camel/memories/blocks/chat_history_block.py +++ b/camel/memories/blocks/chat_history_block.py @@ -39,17 +39,21 @@ class ChatHistoryBlock(MemoryBlock): of the message is multiplied by the `keep_rate`. Higher `keep_rate` leads to high possiblity to keep history messages during context creation. + agent_id (str, optional): The ID of the agent associated with the chat + history. (default: :obj:`None`) """ def __init__( self, storage: Optional[BaseKeyValueStorage] = None, keep_rate: float = 0.9, + agent_id: Optional[str] = None, ) -> None: if keep_rate > 1 or keep_rate < 0: raise ValueError("`keep_rate` should be in [0,1]") self.storage = storage or InMemoryKeyValueStorage() self.keep_rate = keep_rate + self.agent_id = agent_id def retrieve( self, @@ -67,7 +71,10 @@ def retrieve( Returns: List[ContextRecord]: A list of retrieved records. """ - record_dicts = self.storage.load() + if self.agent_id is not None: + record_dicts = self.storage.load(self.agent_id) + else: + record_dicts = self.storage.load() if len(record_dicts) == 0: warnings.warn("The `ChatHistoryMemory` is empty.") return list() diff --git a/camel/storages/key_value_storages/base.py b/camel/storages/key_value_storages/base.py index b47d999f70..1d3336d56c 100644 --- a/camel/storages/key_value_storages/base.py +++ b/camel/storages/key_value_storages/base.py @@ -13,7 +13,7 @@ # ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. ========= from abc import ABC, abstractmethod -from typing import Any, Dict, List +from typing import Any, Dict, List, Optional class BaseKeyValueStorage(ABC): @@ -41,9 +41,14 @@ def save(self, records: List[Dict[str, Any]]) -> None: pass @abstractmethod - def load(self) -> List[Dict[str, Any]]: + def load(self, agent_id: Optional[str] = None) -> List[Dict[str, Any]]: r"""Loads all stored records from the key-value storage system. + Args: + agent_id (str, optional): The ID of the agent associated with the + records. If not provided, all records will be loaded. + (default: :obj:`None`) + Returns: List[Dict[str, Any]]: A list of dictionaries, where each dictionary represents a stored record. diff --git a/camel/storages/key_value_storages/in_memory.py b/camel/storages/key_value_storages/in_memory.py index 17c3f75e5a..24847cb4b2 100644 --- a/camel/storages/key_value_storages/in_memory.py +++ b/camel/storages/key_value_storages/in_memory.py @@ -13,7 +13,7 @@ # ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. ========= from copy import deepcopy -from typing import Any, Dict, List +from typing import Any, Dict, List, Optional from camel.storages.key_value_storages import BaseKeyValueStorage @@ -36,14 +36,27 @@ def save(self, records: List[Dict[str, Any]]) -> None: """ self.memory_list.extend(deepcopy(records)) - def load(self) -> List[Dict[str, Any]]: + def load(self, agent_id: Optional[str] = None) -> List[Dict[str, Any]]: r"""Loads all stored records from the key-value storage system. + If agent_id is provided, only records associated with the specified + agent will be returned. + + Args: + agent_id (str, optional): The ID of the agent associated with the + records. If not provided, all records will be loaded. + (default: :obj:`None`) Returns: List[Dict[str, Any]]: A list of dictionaries, where each dictionary represents a stored record. """ - return deepcopy(self.memory_list) + all_records = deepcopy(self.memory_list) + + if agent_id is not None: + all_records = [ + r for r in all_records if r.get("agent_id") == agent_id + ] + return all_records def clear(self) -> None: r"""Removes all records from the key-value storage system.""" diff --git a/camel/storages/key_value_storages/json.py b/camel/storages/key_value_storages/json.py index 50f666029c..7b9900fae5 100644 --- a/camel/storages/key_value_storages/json.py +++ b/camel/storages/key_value_storages/json.py @@ -78,19 +78,33 @@ def save(self, records: List[Dict[str, Any]]) -> None: [json.dumps(r, cls=_CamelJSONEncoder) + "\n" for r in records] ) - def load(self) -> List[Dict[str, Any]]: + def load(self, agent_id: Optional[str] = None) -> List[Dict[str, Any]]: r"""Loads all stored records from the key-value storage system. + If agent_id is provided, only records associated with the specified + agent will be returned. + + Args: + agent_id (str, optional): The ID of the agent associated with the + records. If not provided, all records will be loaded. + (default: :obj:`None`) Returns: List[Dict[str, Any]]: A list of dictionaries, where each dictionary represents a stored record. """ with self.json_path.open("r") as f: - return [ + all_records = [ json.loads(r, object_hook=self._json_object_hook) for r in f.readlines() ] + if agent_id is not None: + all_records = [ + r for r in all_records if r.get("agent_id") == agent_id + ] + + return all_records + def clear(self) -> None: r"""Removes all records from the key-value storage system.""" with self.json_path.open("w"): diff --git a/camel/storages/key_value_storages/redis.py b/camel/storages/key_value_storages/redis.py index 30c5c47a49..5c6f16511d 100644 --- a/camel/storages/key_value_storages/redis.py +++ b/camel/storages/key_value_storages/redis.py @@ -107,15 +107,27 @@ def save( except Exception as e: logger.error(f"Error in save: {e}") - def load(self) -> List[Dict[str, Any]]: + def load(self, agent_id: Optional[str] = None) -> List[Dict[str, Any]]: r"""Loads all stored records from the key-value storage system. + If agent_id is provided, only records associated with the specified + agent will be returned. + + Args: + agent_id (str, optional): The ID of the agent associated with the + records. If not provided, all records will be loaded. + (default: :obj:`None`) Returns: List[Dict[str, Any]]: A list of dictionaries, where each dictionary represents a stored record. """ try: - return self._run_async(self._async_load()) + all_records = self._run_async(self._async_load()) + if agent_id is not None: + all_records = [ + r for r in all_records if r.get("agent_id") == agent_id + ] + return all_records except Exception as e: logger.error(f"Error in load: {e}") return [] From 39907d87c69d34367c076eb2fa80bd598d882e74 Mon Sep 17 00:00:00 2001 From: X-TRON404 Date: Thu, 6 Mar 2025 18:50:45 +0530 Subject: [PATCH 03/20] feat: updated tests for agent_id for each memory record --- test/memories/test_agent_memories.py | 3 +++ test/memories/test_blocks.py | 7 ++++++- test/memories/test_chat_history_memory.py | 16 +++++++++++++--- test/memories/test_vector_db_memory.py | 21 +++++++++++++++++---- 4 files changed, 39 insertions(+), 8 deletions(-) diff --git a/test/memories/test_agent_memories.py b/test/memories/test_agent_memories.py index 4acd18bb8b..161ddc2711 100644 --- a/test/memories/test_agent_memories.py +++ b/test/memories/test_agent_memories.py @@ -11,6 +11,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. ========= +from datetime import datetime from unittest.mock import MagicMock import pytest @@ -95,6 +96,8 @@ def test_write_records( "test message {}".format(i), ), role_at_backend=OpenAIBackendRole.USER, + timestamp=datetime.now().timestamp(), + agent_id=f"test_agent_{i}", ) for i in range(5) ] diff --git a/test/memories/test_blocks.py b/test/memories/test_blocks.py index 45bbcf12d5..92d5ad2d6f 100644 --- a/test/memories/test_blocks.py +++ b/test/memories/test_blocks.py @@ -46,7 +46,8 @@ def generate_mock_records(num_records: int): }, "role_at_backend": "user", "extra_info": {}, - "timestamp": datetime.now().timestamp() + i, + "timestamp": datetime.now().timestamp(), + "agent_id": f"test_agent_{i}", } for i in range(num_records) ] @@ -101,6 +102,8 @@ def test_write_records(self, mock_storage): "test message {}".format(i), ), role_at_backend=OpenAIBackendRole.USER, + timestamp=datetime.now().timestamp(), + agent_id=f"test_agent_{i}", ) for i in range(5) ] @@ -176,6 +179,8 @@ def test_write_records(self, mock_storage, mock_embedding): "test message {}".format(i), ), role_at_backend=OpenAIBackendRole.USER, + timestamp=datetime.now().timestamp(), + agent_id=f"test_agent_{i}", ) for i in range(5) ] diff --git a/test/memories/test_chat_history_memory.py b/test/memories/test_chat_history_memory.py index 1e3c98295c..22daecfe84 100644 --- a/test/memories/test_chat_history_memory.py +++ b/test/memories/test_chat_history_memory.py @@ -13,6 +13,7 @@ # ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. ========= import tempfile +from datetime import datetime from pathlib import Path import pytest @@ -68,13 +69,22 @@ def test_chat_history_memory(memory: ChatHistoryMemory): content="OK", ) system_record = MemoryRecord( - message=system_msg, role_at_backend=OpenAIBackendRole.SYSTEM + message=system_msg, + role_at_backend=OpenAIBackendRole.SYSTEM, + timestamp=datetime.now().timestamp(), + agent_id="system_agent_1", ) user_record = MemoryRecord( - message=user_msg, role_at_backend=OpenAIBackendRole.USER + message=user_msg, + role_at_backend=OpenAIBackendRole.USER, + timestamp=datetime.now().timestamp(), + agent_id="user_agent_1", ) assistant_record = MemoryRecord( - message=assistant_msg, role_at_backend=OpenAIBackendRole.ASSISTANT + message=assistant_msg, + role_at_backend=OpenAIBackendRole.ASSISTANT, + timestamp=datetime.now().timestamp(), + agent_id="assistant_agent_1", ) memory.write_records([system_record, user_record, assistant_record]) output_messages, _ = memory.get_context() diff --git a/test/memories/test_vector_db_memory.py b/test/memories/test_vector_db_memory.py index 24c59484c6..69cc6c60aa 100644 --- a/test/memories/test_vector_db_memory.py +++ b/test/memories/test_vector_db_memory.py @@ -12,6 +12,7 @@ # limitations under the License. # ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. ========= import random +from datetime import datetime from typing import Dict, List, Tuple import pytest @@ -44,6 +45,8 @@ def memory_and_message(request): content="You are a helpful assistant", ), role_at_backend=OpenAIBackendRole.SYSTEM, + timestamp=datetime.now().timestamp(), + agent_id="system_agent_1", ) ] ) @@ -53,7 +56,7 @@ def memory_and_message(request): random.shuffle(messages) # Process each conversation - for conv in messages: + for i, conv in enumerate(messages): records = [ MemoryRecord( message=BaseMessage( @@ -63,6 +66,8 @@ def memory_and_message(request): content=conv["user"], ), role_at_backend=OpenAIBackendRole.USER, + timestamp=datetime.now().timestamp(), + agent_id=f"user_agent_{i}", ), MemoryRecord( message=BaseMessage( @@ -72,6 +77,8 @@ def memory_and_message(request): content=conv["assistant"], ), role_at_backend=OpenAIBackendRole.ASSISTANT, + timestamp=datetime.now().timestamp(), + agent_id=f"assistant_agent_{i}", ), ] memory.write_records(records) @@ -144,7 +151,7 @@ def test_vector_db_memory( ): memory, messages, questions = memory_and_message - for question in questions: + for i, question in enumerate(questions): # Add user question to memory user_msg = BaseMessage( "AI user", RoleType.USER, None, question["user"] @@ -152,15 +159,21 @@ def test_vector_db_memory( memory.write_records( [ MemoryRecord( - message=user_msg, role_at_backend=OpenAIBackendRole.USER + message=user_msg, + role_at_backend=OpenAIBackendRole.USER, + timestamp=datetime.now().timestamp(), + agent_id=f"user_agent_{i}", ) ] ) output_messages, _ = memory.get_context() + print(output_messages) + # Check that context messages are from correct category for msg in output_messages[:-1]: - for conv in messages: + for i, conv in enumerate(messages): + print(i, conv) if ( conv["assistant"] == msg["content"] or conv["user"] == msg["content"] From c43ff3e74021df9c913fe77dfc3c7c6caf9c675e Mon Sep 17 00:00:00 2001 From: X-TRON404 Date: Thu, 6 Mar 2025 19:07:44 +0530 Subject: [PATCH 04/20] fix: message_window_size --- camel/agents/chat_agent.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/camel/agents/chat_agent.py b/camel/agents/chat_agent.py index 9d82e02480..b822321f5d 100644 --- a/camel/agents/chat_agent.py +++ b/camel/agents/chat_agent.py @@ -181,18 +181,14 @@ def __init__( # Assign unique ID self.agent_id = agent_id if agent_id else str(uuid.uuid4()) - self.token_limit = token_limit - self.message_window_size = message_window_size - # Set up memory - self.context_creator = ScoreBasedContextCreator( + context_creator = ScoreBasedContextCreator( self.model_backend.token_counter, - self.token_limit or self.model_backend.token_limit, + token_limit or self.model_backend.token_limit, ) self.memory: AgentMemory = memory or ChatHistoryMemory( - self.context_creator, - window_size=self.message_window_size, - agent_id=self.agent_id, + context_creator, window_size=message_window_size, + agent_id=self.agent_id ) # Set up system message and initialize messages From ddb579302cc562ec6775ab595279bb94bd331f05 Mon Sep 17 00:00:00 2001 From: X-TRON404 Date: Thu, 6 Mar 2025 19:14:56 +0530 Subject: [PATCH 05/20] fix: format --- camel/agents/chat_agent.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/camel/agents/chat_agent.py b/camel/agents/chat_agent.py index b822321f5d..1fdf8469f5 100644 --- a/camel/agents/chat_agent.py +++ b/camel/agents/chat_agent.py @@ -187,8 +187,9 @@ def __init__( token_limit or self.model_backend.token_limit, ) self.memory: AgentMemory = memory or ChatHistoryMemory( - context_creator, window_size=message_window_size, - agent_id=self.agent_id + context_creator, + window_size=message_window_size, + agent_id=self.agent_id, ) # Set up system message and initialize messages From 080fd62f1b719fc4931fbdd5648f2b7728331b28 Mon Sep 17 00:00:00 2001 From: X-TRON404 Date: Thu, 6 Mar 2025 19:56:02 +0530 Subject: [PATCH 06/20] feat: example for ChatAgent memory --- examples/memories/agent_memory_test.py | 95 ++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 examples/memories/agent_memory_test.py diff --git a/examples/memories/agent_memory_test.py b/examples/memories/agent_memory_test.py new file mode 100644 index 0000000000..e4d4a8f25c --- /dev/null +++ b/examples/memories/agent_memory_test.py @@ -0,0 +1,95 @@ +import os +import json +from camel.agents import ChatAgent +from camel.memories import AgentMemory, ChatHistoryMemory, MemoryRecord +from camel.messages import BaseMessage +from camel.memories.context_creators.score_based import ScoreBasedContextCreator +from camel.types import ModelType +from camel.utils import OpenAITokenCounter +from camel.models.model_factory import ModelFactory +from camel.types import ModelPlatformType +from pathlib import Path +from camel.types.enums import OpenAIBackendRole + +def test_chat_agent_memory(): + + context_creator = ScoreBasedContextCreator( + token_counter=OpenAITokenCounter(ModelType.GPT_4O_MINI), + token_limit=1024 + ) + + model = ModelFactory.create( + model_platform=ModelPlatformType.OPENAI, model_type=ModelType.GPT_4O_MINI + ) + + # 1. Instantiate a ChatAgent with system_message & agent_id + # Using ChatHistoryMemory as an example memory store. + agent = ChatAgent( + system_message="You are a helpful assistant", + agent_id="001", + model=model + ) + + # 2. Perform steps so that some MemoryRecords accumulate + # - We'll just send a message and read the response to populate memory + user_input_1 = "Hello, can you remember these instructions?: Banana is a country." + response_1 = agent.step(user_input_1) + print("Assistant response 1:", response_1.msgs[0].content if response_1.msgs else "No response") + + user_input_2 = "Please store and recall this next time: CAMEL lives in Banana." + response_2 = agent.step(user_input_2) + print("Assistant response 2:", response_2.msgs[0].content if response_2.msgs else "No response") + + # 3. Save the agent’s memory to a JSON file + save_path = Path("./chat_agent_memory.json") + agent.save_memory(save_path) + print(f"Agent memory saved to {save_path}.") + + # 4. Create a separate agent that loads memory from the file + # We'll pass no explicit memory, as the loaded data will be used + new_agent = ChatAgent( + system_message="You are a helpful assistant", + agent_id="001", + model=model + ) + + # Load the memory from our file + new_agent.load_memory_from_path(save_path) + + # Test that the memory is loaded by requesting a response + user_input_3 = "What were we talking about?" + response_3 = new_agent.step(user_input_3) + print("New Agent response (after loading memory):", + response_3.msgs[0].content if response_3.msgs else "No response") + + # 5. Demonstrate loading memory from an existing AgentMemory + # Suppose we had another agent with some different or combined memory: + another_agent = ChatAgent( + system_message="Another system message", + agent_id="002", + memory=ChatHistoryMemory(agent_id="002", context_creator=context_creator ) + ) + + # Add some record to the second agent's memory + message = BaseMessage.make_user_message( + role_name="User", content="This is memory from a second agent" + ) + another_agent.update_memory(message, role=OpenAIBackendRole.USER) # role passed for demonstration + + # Extract the memory object from the second agent + second_agent_memory = another_agent.memory + + # Now load that memory into new_agent. We override new_agent’s memory. + new_agent.load_memory(second_agent_memory) + + # Confirm it's loaded by printing some details: + print("After loading another agent's memory, new_agent has records:") + for record in new_agent.memory.retrieve(): + print(record.memory_record.message.content) + + # Clean up: remove the saved file if desired + if os.path.exists(save_path): + os.remove(save_path) + +if __name__ == "__main__": + test_chat_agent_memory() From aa8d89fba200ee60451ed13e34e9eb1edab24876 Mon Sep 17 00:00:00 2001 From: X-TRON404 Date: Thu, 6 Mar 2025 20:01:13 +0530 Subject: [PATCH 07/20] fix: format for agent memory example --- examples/memories/agent_memory_test.py | 76 ++++++++++++++++++-------- 1 file changed, 54 insertions(+), 22 deletions(-) diff --git a/examples/memories/agent_memory_test.py b/examples/memories/agent_memory_test.py index e4d4a8f25c..76199f2e77 100644 --- a/examples/memories/agent_memory_test.py +++ b/examples/memories/agent_memory_test.py @@ -1,25 +1,40 @@ +# ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. ========= +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. ========= import os -import json +from pathlib import Path + from camel.agents import ChatAgent -from camel.memories import AgentMemory, ChatHistoryMemory, MemoryRecord +from camel.memories import ChatHistoryMemory +from camel.memories.context_creators.score_based import ( + ScoreBasedContextCreator, +) from camel.messages import BaseMessage -from camel.memories.context_creators.score_based import ScoreBasedContextCreator -from camel.types import ModelType -from camel.utils import OpenAITokenCounter from camel.models.model_factory import ModelFactory -from camel.types import ModelPlatformType -from pathlib import Path +from camel.types import ModelPlatformType, ModelType from camel.types.enums import OpenAIBackendRole +from camel.utils import OpenAITokenCounter -def test_chat_agent_memory(): +def test_chat_agent_memory(): context_creator = ScoreBasedContextCreator( - token_counter=OpenAITokenCounter(ModelType.GPT_4O_MINI), - token_limit=1024 + token_counter=OpenAITokenCounter(ModelType.GPT_4O_MINI), + token_limit=1024, ) - + model = ModelFactory.create( - model_platform=ModelPlatformType.OPENAI, model_type=ModelType.GPT_4O_MINI + model_platform=ModelPlatformType.OPENAI, + model_type=ModelType.GPT_4O_MINI, ) # 1. Instantiate a ChatAgent with system_message & agent_id @@ -27,18 +42,28 @@ def test_chat_agent_memory(): agent = ChatAgent( system_message="You are a helpful assistant", agent_id="001", - model=model + model=model, ) # 2. Perform steps so that some MemoryRecords accumulate # - We'll just send a message and read the response to populate memory - user_input_1 = "Hello, can you remember these instructions?: Banana is a country." + user_input_1 = ( + "Hello, can you remember these instructions?: Banana is a country." + ) response_1 = agent.step(user_input_1) - print("Assistant response 1:", response_1.msgs[0].content if response_1.msgs else "No response") + print( + "Assistant response 1:", + response_1.msgs[0].content if response_1.msgs else "No response", + ) - user_input_2 = "Please store and recall this next time: CAMEL lives in Banana." + user_input_2 = ( + "Please store and recall this next time: CAMEL lives in Banana." + ) response_2 = agent.step(user_input_2) - print("Assistant response 2:", response_2.msgs[0].content if response_2.msgs else "No response") + print( + "Assistant response 2:", + response_2.msgs[0].content if response_2.msgs else "No response", + ) # 3. Save the agent’s memory to a JSON file save_path = Path("./chat_agent_memory.json") @@ -50,7 +75,7 @@ def test_chat_agent_memory(): new_agent = ChatAgent( system_message="You are a helpful assistant", agent_id="001", - model=model + model=model, ) # Load the memory from our file @@ -59,22 +84,28 @@ def test_chat_agent_memory(): # Test that the memory is loaded by requesting a response user_input_3 = "What were we talking about?" response_3 = new_agent.step(user_input_3) - print("New Agent response (after loading memory):", - response_3.msgs[0].content if response_3.msgs else "No response") + print( + "New Agent response (after loading memory):", + response_3.msgs[0].content if response_3.msgs else "No response", + ) # 5. Demonstrate loading memory from an existing AgentMemory # Suppose we had another agent with some different or combined memory: another_agent = ChatAgent( system_message="Another system message", agent_id="002", - memory=ChatHistoryMemory(agent_id="002", context_creator=context_creator ) + memory=ChatHistoryMemory( + agent_id="002", context_creator=context_creator + ), ) # Add some record to the second agent's memory message = BaseMessage.make_user_message( role_name="User", content="This is memory from a second agent" ) - another_agent.update_memory(message, role=OpenAIBackendRole.USER) # role passed for demonstration + another_agent.update_memory( + message, role=OpenAIBackendRole.USER + ) # role passed for demonstration # Extract the memory object from the second agent second_agent_memory = another_agent.memory @@ -91,5 +122,6 @@ def test_chat_agent_memory(): if os.path.exists(save_path): os.remove(save_path) + if __name__ == "__main__": test_chat_agent_memory() From d021f9d9ab362e8f9b9938489daae7964b767f64 Mon Sep 17 00:00:00 2001 From: X-TRON404 Date: Thu, 6 Mar 2025 20:06:02 +0530 Subject: [PATCH 08/20] fix: format memory example --- examples/memories/agent_memory_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/memories/agent_memory_test.py b/examples/memories/agent_memory_test.py index 76199f2e77..0123e0fd01 100644 --- a/examples/memories/agent_memory_test.py +++ b/examples/memories/agent_memory_test.py @@ -65,7 +65,7 @@ def test_chat_agent_memory(): response_2.msgs[0].content if response_2.msgs else "No response", ) - # 3. Save the agent’s memory to a JSON file + # 3. Save the agent's memory to a JSON file save_path = Path("./chat_agent_memory.json") agent.save_memory(save_path) print(f"Agent memory saved to {save_path}.") @@ -110,7 +110,7 @@ def test_chat_agent_memory(): # Extract the memory object from the second agent second_agent_memory = another_agent.memory - # Now load that memory into new_agent. We override new_agent’s memory. + # Now load that memory into new_agent. We override new_agent's memory. new_agent.load_memory(second_agent_memory) # Confirm it's loaded by printing some details: From 6f78fd02735da08bd74f48203e0dd1025e2790f2 Mon Sep 17 00:00:00 2001 From: X-TRON404 Date: Thu, 6 Mar 2025 20:51:18 +0530 Subject: [PATCH 09/20] fix: updated tests for test_chat_agent - passed the agent_id in the memory record for the test_chat_agent_messages_window() test --- test/agents/test_chat_agent.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test/agents/test_chat_agent.py b/test/agents/test_chat_agent.py index 870a2ba142..1c3b70b02a 100644 --- a/test/agents/test_chat_agent.py +++ b/test/agents/test_chat_agent.py @@ -394,10 +394,12 @@ def test_chat_agent_messages_window(): MemoryRecord( message=user_msg, role_at_backend=OpenAIBackendRole.USER, + agent_id=assistant.agent_id, ) for _ in range(5) ] ) + openai_messages, _ = assistant.memory.get_context() assert len(openai_messages) == 2 @@ -1149,3 +1151,14 @@ def test_chat_agent_vision(step_call_count=3): assert ( agent_response.msgs[0].content == "Yes." ), f"Error in calling round {i+1}" + + +def main(): + """Entry point when running the script with Python.""" + print("Running script directly with Python...") + + test_chat_agent_messages_window() + + +if __name__ == "__main__": + main() From 117d9f7b3f6550d3b0cf5f637463fdcb4ee053c4 Mon Sep 17 00:00:00 2001 From: X-TRON404 Date: Thu, 6 Mar 2025 22:14:43 +0530 Subject: [PATCH 10/20] feat: auto agent_id for each memory in AgentMemory --- camel/memories/agent_memories.py | 8 +++++++- test/agents/test_chat_agent.py | 12 ------------ 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/camel/memories/agent_memories.py b/camel/memories/agent_memories.py index d03da58df4..c34e5d66e8 100644 --- a/camel/memories/agent_memories.py +++ b/camel/memories/agent_memories.py @@ -53,7 +53,7 @@ def __init__( self._chat_history_block = ChatHistoryBlock( storage=storage, agent_id=agent_id ) - self._agent_id = agent_id + self.agent_id = agent_id def retrieve(self) -> List[ContextRecord]: records = self._chat_history_block.retrieve(self._window_size) @@ -69,6 +69,12 @@ def retrieve(self) -> List[ContextRecord]: return records def write_records(self, records: List[MemoryRecord]) -> None: + for record in records: + # assign the agent_id to the record + if record.agent_id is None: + # if the agent memory has an agent_id, use it + if self.agent_id == "": + record.agent_id = self.agent_id self._chat_history_block.write_records(records) def get_context_creator(self) -> BaseContextCreator: diff --git a/test/agents/test_chat_agent.py b/test/agents/test_chat_agent.py index 1c3b70b02a..3e4923138c 100644 --- a/test/agents/test_chat_agent.py +++ b/test/agents/test_chat_agent.py @@ -394,7 +394,6 @@ def test_chat_agent_messages_window(): MemoryRecord( message=user_msg, role_at_backend=OpenAIBackendRole.USER, - agent_id=assistant.agent_id, ) for _ in range(5) ] @@ -1151,14 +1150,3 @@ def test_chat_agent_vision(step_call_count=3): assert ( agent_response.msgs[0].content == "Yes." ), f"Error in calling round {i+1}" - - -def main(): - """Entry point when running the script with Python.""" - print("Running script directly with Python...") - - test_chat_agent_messages_window() - - -if __name__ == "__main__": - main() From 02841fa646a0af0aab94f9fe56b7b8a9e87b541c Mon Sep 17 00:00:00 2001 From: X-TRON404 Date: Thu, 6 Mar 2025 23:43:07 +0530 Subject: [PATCH 11/20] fix: agent_id in record not memory --- camel/memories/agent_memories.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/camel/memories/agent_memories.py b/camel/memories/agent_memories.py index c34e5d66e8..dd1b5138e2 100644 --- a/camel/memories/agent_memories.py +++ b/camel/memories/agent_memories.py @@ -71,9 +71,9 @@ def retrieve(self) -> List[ContextRecord]: def write_records(self, records: List[MemoryRecord]) -> None: for record in records: # assign the agent_id to the record - if record.agent_id is None: + if record.agent_id == "": # if the agent memory has an agent_id, use it - if self.agent_id == "": + if self.agent_id is not None: record.agent_id = self.agent_id self._chat_history_block.write_records(records) From 1efea4559fe7008bae6fa2799c23f3d0daf3a1f5 Mon Sep 17 00:00:00 2001 From: X-TRON404 Date: Fri, 7 Mar 2025 19:26:11 +0530 Subject: [PATCH 12/20] feat: vector_db for ChatAgent memory --- camel/agents/chat_agent.py | 5 ++ camel/memories/agent_memories.py | 8 ++ camel/storages/vectordb_storages/base.py | 6 +- .../memories/agent_memory_vector_db_test.py | 88 +++++++++++++++++++ examples/memories/vector_db_memory.py | 88 +++++++++++++++++++ 5 files changed, 194 insertions(+), 1 deletion(-) create mode 100644 examples/memories/agent_memory_vector_db_test.py create mode 100644 examples/memories/vector_db_memory.py diff --git a/camel/agents/chat_agent.py b/camel/agents/chat_agent.py index 1fdf8469f5..4645245999 100644 --- a/camel/agents/chat_agent.py +++ b/camel/agents/chat_agent.py @@ -186,12 +186,17 @@ def __init__( self.model_backend.token_counter, token_limit or self.model_backend.token_limit, ) + self.memory: AgentMemory = memory or ChatHistoryMemory( context_creator, window_size=message_window_size, agent_id=self.agent_id, ) + # So we don't have to pass agent_id when we define memory + if memory is not None: + memory.agent_id = self.agent_id + # Set up system message and initialize messages self._original_system_message = ( BaseMessage.make_assistant_message( diff --git a/camel/memories/agent_memories.py b/camel/memories/agent_memories.py index dd1b5138e2..fb2d8b7758 100644 --- a/camel/memories/agent_memories.py +++ b/camel/memories/agent_memories.py @@ -110,6 +110,7 @@ def __init__( self._context_creator = context_creator self._retrieve_limit = retrieve_limit self._vectordb_block = VectorDBBlock(storage=storage) + self.agent_id = agent_id self._current_topic: str = "" @@ -124,6 +125,13 @@ def write_records(self, records: List[MemoryRecord]) -> None: for record in records: if record.role_at_backend == OpenAIBackendRole.USER: self._current_topic = record.message.content + + # assign the agent_id to the record + if record.agent_id == "": + # if the agent memory has an agent_id, use it + if self.agent_id is not None: + record.agent_id = self.agent_id + self._vectordb_block.write_records(records) def get_context_creator(self) -> BaseContextCreator: diff --git a/camel/storages/vectordb_storages/base.py b/camel/storages/vectordb_storages/base.py index 6fb32accad..fe55a42e8f 100644 --- a/camel/storages/vectordb_storages/base.py +++ b/camel/storages/vectordb_storages/base.py @@ -87,7 +87,11 @@ def create( ) -> "VectorDBQueryResult": r"""A class method to construct a `VectorDBQueryResult` instance.""" return cls( - record=VectorRecord(vector=vector, id=id, payload=payload), + record=VectorRecord( + vector=vector, + id=id, + payload=payload, + ), similarity=similarity, ) diff --git a/examples/memories/agent_memory_vector_db_test.py b/examples/memories/agent_memory_vector_db_test.py new file mode 100644 index 0000000000..d751dfff4c --- /dev/null +++ b/examples/memories/agent_memory_vector_db_test.py @@ -0,0 +1,88 @@ +import os +from pathlib import Path + +from camel.agents import ChatAgent +from camel.memories import VectorDBMemory +from camel.memories.context_creators.score_based import ScoreBasedContextCreator +from camel.models.model_factory import ModelFactory +from camel.storages.vectordb_storages import QdrantStorage +from camel.types import ModelPlatformType, ModelType +from camel.utils import OpenAITokenCounter + +def test_chat_agent_vectordb_memory_save_and_load(): + # 1) Create a QdrantStorage in-memory instance + # (in production, set path to a real directory or remote) + vector_storage = QdrantStorage( + vector_dim=1536, + path=":memory:", + ) + + # 2) Create a ScoreBasedContextCreator for token limiting + context_creator = ScoreBasedContextCreator( + token_counter=OpenAITokenCounter(ModelType.GPT_4O_MINI), + token_limit=1024, + ) + + # 3) Build a model + model = ModelFactory.create( + model_platform=ModelPlatformType.OPENAI, + model_type=ModelType.GPT_4O_MINI, + ) + + # 4) Create first ChatAgent with VectorDBMemory + agent1 = ChatAgent( + system_message="You are assistant #1 with vector DB memory.", + agent_id="agent_001", + model=model, + ) + agent1_memory = VectorDBMemory( + context_creator=context_creator, + storage=vector_storage, + retrieve_limit=2, + agent_id=agent1.agent_id, # ensure consistent agent_id + ) + agent1.memory = agent1_memory + + # 5) Agent #1 accumulates some conversation + user_input_1 = "Remember that dolphins use echolocation." + response_1 = agent1.step(user_input_1) + print("Agent #1 response:", response_1.msgs[0].content if response_1.msgs else "No response") + + user_input_2 = "And whales are the largest mammals." + response_2 = agent1.step(user_input_2) + print("Agent #1 response:", response_2.msgs[0].content if response_2.msgs else "No response") + + # 6) SAVE agent1's memory to a JSON file (storing the conversation) + save_path = Path("./agent1_vectordb_memory.json") + agent1.save_memory(save_path) + print(f"Agent #1 memory saved to {save_path}.") + + # 7) Create a new agent, load that JSON memory to confirm retrieval + new_agent1 = ChatAgent( + system_message="You are the resurrected assistant #1 with vector DB memory.", + agent_id="agent_001", # same agent_id to match the saved records + model=model, + ) + # Use a new VectorDBMemory pointing to the same underlying storage + # (or a new vector store if you prefer, but that won't have the original embeddings) + new_agent1_memory = VectorDBMemory( + context_creator=context_creator, + storage=vector_storage, + retrieve_limit=2, + agent_id=new_agent1.agent_id, + ) + + # Load memory from JSON, which replays the stored MemoryRecords + new_agent1.load_memory_from_path(save_path) + + # 8) Check if the new agent can recall previous info from loaded conversation + user_input_3 = "What do you remember about marine mammals?" + response_3 = new_agent1.step(user_input_3) + print("New Agent #1 response (after loading memory):", response_3.msgs[0].content if response_3.msgs else "No response") + + # Optionally, remove the JSON file + if os.path.exists(save_path): + os.remove(save_path) + +if __name__ == "__main__": + test_chat_agent_vectordb_memory_save_and_load() diff --git a/examples/memories/vector_db_memory.py b/examples/memories/vector_db_memory.py new file mode 100644 index 0000000000..867dccc97b --- /dev/null +++ b/examples/memories/vector_db_memory.py @@ -0,0 +1,88 @@ +# vector_db_memory_test.py + +import os +from pathlib import Path +from camel.agents import ChatAgent +from camel.memories import VectorDBMemory +from camel.memories.context_creators.score_based import ScoreBasedContextCreator +from camel.models.model_factory import ModelFactory +from camel.storages.vectordb_storages import QdrantStorage +from camel.types import ModelPlatformType, ModelType +from camel.utils import OpenAITokenCounter + +def test_chat_agent_vectordb_memory(): + # Shared vector storage + vector_storage = QdrantStorage( + vector_dim=1536, + path=":memory:", + ) + + context_creator = ScoreBasedContextCreator( + token_counter=OpenAITokenCounter(ModelType.GPT_3_5_TURBO), + token_limit=2048, + ) + + # Memory for agent 1 + vectordb_memory_agent1 = VectorDBMemory( + context_creator=context_creator, + storage=vector_storage, + retrieve_limit=2, + agent_id="vector_agent_007", + ) + + # Memory for agent 2 + vectordb_memory_agent2 = VectorDBMemory( + context_creator=context_creator, + storage=vector_storage, + retrieve_limit=2, + agent_id="vector_agent_008", + ) + + model = ModelFactory.create( + model_platform=ModelPlatformType.OPENAI, + model_type=ModelType.GPT_3_5_TURBO, + ) + + # Agent 1 + agent1 = ChatAgent( + system_message="You are Agent 007.", + model=model, + memory=vectordb_memory_agent1, + agent_id="vector_agent_007", + ) + + # Agent 2 + agent2 = ChatAgent( + system_message="You are Agent 008.", + model=model, + memory=vectordb_memory_agent2, + agent_id="vector_agent_008", + ) + + # Populate agent 1 memory + agent1.step("Elephants are the best swimmers on Earth.") + agent1.step("Whales have eyelashes.") + + # Populate agent 2 with different memory + agent2.step("The sun is a star.") + agent2.step("The moon orbits the Earth.") + + # Query both agents + response_1 = agent1.step("What did I tell you about whales or elephants?") + response_2 = agent2.step("What have I told you about stars and moons?") + + print("Agent 1 response:", response_1.msgs[0].content if response_1.msgs else "No response") + print("Agent 2 response:", response_2.msgs[0].content if response_2.msgs else "No response") + + # Retrieve and print agent-specific records + print("\nAgent 1's memory records:") + for ctx_record in vectordb_memory_agent1.retrieve(): + print(f"Score: {ctx_record.score:.2f} | Content: {ctx_record.memory_record.message.content}") + + print("\nAgent 2's memory records:") + retrieved_context_agent2 = vectordb_memory_agent2.retrieve() + for ctx_record in retrieved_context_agent2: + print(f"Score: {ctx_record.score:.2f} | Content: {ctx_record.memory_record.message.content}") + +if __name__ == "__main__": + test_chat_agent_vectordb_memory() From 70ed47b79838b37971296b187c403cab32f32264 Mon Sep 17 00:00:00 2001 From: X-TRON404 Date: Fri, 7 Mar 2025 19:57:04 +0530 Subject: [PATCH 13/20] fix: dont need to filter cause new memory for each agent --- camel/memories/blocks/chat_history_block.py | 11 ++-------- camel/storages/key_value_storages/base.py | 11 +++------- .../storages/key_value_storages/in_memory.py | 21 ++++--------------- camel/storages/key_value_storages/json.py | 20 +++--------------- camel/storages/key_value_storages/redis.py | 18 +++------------- 5 files changed, 15 insertions(+), 66 deletions(-) diff --git a/camel/memories/blocks/chat_history_block.py b/camel/memories/blocks/chat_history_block.py index 646ae1c060..2ae5ab9ed4 100644 --- a/camel/memories/blocks/chat_history_block.py +++ b/camel/memories/blocks/chat_history_block.py @@ -39,21 +39,17 @@ class ChatHistoryBlock(MemoryBlock): of the message is multiplied by the `keep_rate`. Higher `keep_rate` leads to high possiblity to keep history messages during context creation. - agent_id (str, optional): The ID of the agent associated with the chat - history. (default: :obj:`None`) """ def __init__( self, storage: Optional[BaseKeyValueStorage] = None, keep_rate: float = 0.9, - agent_id: Optional[str] = None, ) -> None: if keep_rate > 1 or keep_rate < 0: raise ValueError("`keep_rate` should be in [0,1]") self.storage = storage or InMemoryKeyValueStorage() self.keep_rate = keep_rate - self.agent_id = agent_id def retrieve( self, @@ -71,10 +67,7 @@ def retrieve( Returns: List[ContextRecord]: A list of retrieved records. """ - if self.agent_id is not None: - record_dicts = self.storage.load(self.agent_id) - else: - record_dicts = self.storage.load() + record_dicts = self.storage.load() if len(record_dicts) == 0: warnings.warn("The `ChatHistoryMemory` is empty.") return list() @@ -127,4 +120,4 @@ def write_records(self, records: List[MemoryRecord]) -> None: def clear(self) -> None: r"""Clears all chat messages from the memory.""" - self.storage.clear() + self.storage.clear() \ No newline at end of file diff --git a/camel/storages/key_value_storages/base.py b/camel/storages/key_value_storages/base.py index 1d3336d56c..ee8b854204 100644 --- a/camel/storages/key_value_storages/base.py +++ b/camel/storages/key_value_storages/base.py @@ -13,7 +13,7 @@ # ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. ========= from abc import ABC, abstractmethod -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List class BaseKeyValueStorage(ABC): @@ -41,14 +41,9 @@ def save(self, records: List[Dict[str, Any]]) -> None: pass @abstractmethod - def load(self, agent_id: Optional[str] = None) -> List[Dict[str, Any]]: + def load(self) -> List[Dict[str, Any]]: r"""Loads all stored records from the key-value storage system. - Args: - agent_id (str, optional): The ID of the agent associated with the - records. If not provided, all records will be loaded. - (default: :obj:`None`) - Returns: List[Dict[str, Any]]: A list of dictionaries, where each dictionary represents a stored record. @@ -58,4 +53,4 @@ def load(self, agent_id: Optional[str] = None) -> List[Dict[str, Any]]: @abstractmethod def clear(self) -> None: r"""Removes all records from the key-value storage system.""" - pass + pass \ No newline at end of file diff --git a/camel/storages/key_value_storages/in_memory.py b/camel/storages/key_value_storages/in_memory.py index 24847cb4b2..ac4aa78c3a 100644 --- a/camel/storages/key_value_storages/in_memory.py +++ b/camel/storages/key_value_storages/in_memory.py @@ -13,7 +13,7 @@ # ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. ========= from copy import deepcopy -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List from camel.storages.key_value_storages import BaseKeyValueStorage @@ -36,28 +36,15 @@ def save(self, records: List[Dict[str, Any]]) -> None: """ self.memory_list.extend(deepcopy(records)) - def load(self, agent_id: Optional[str] = None) -> List[Dict[str, Any]]: + def load(self) -> List[Dict[str, Any]]: r"""Loads all stored records from the key-value storage system. - If agent_id is provided, only records associated with the specified - agent will be returned. - - Args: - agent_id (str, optional): The ID of the agent associated with the - records. If not provided, all records will be loaded. - (default: :obj:`None`) Returns: List[Dict[str, Any]]: A list of dictionaries, where each dictionary represents a stored record. """ - all_records = deepcopy(self.memory_list) - - if agent_id is not None: - all_records = [ - r for r in all_records if r.get("agent_id") == agent_id - ] - return all_records + return deepcopy(self.memory_list) def clear(self) -> None: r"""Removes all records from the key-value storage system.""" - self.memory_list.clear() + self.memory_list.clear() \ No newline at end of file diff --git a/camel/storages/key_value_storages/json.py b/camel/storages/key_value_storages/json.py index 7b9900fae5..094092af3d 100644 --- a/camel/storages/key_value_storages/json.py +++ b/camel/storages/key_value_storages/json.py @@ -78,34 +78,20 @@ def save(self, records: List[Dict[str, Any]]) -> None: [json.dumps(r, cls=_CamelJSONEncoder) + "\n" for r in records] ) - def load(self, agent_id: Optional[str] = None) -> List[Dict[str, Any]]: + def load(self) -> List[Dict[str, Any]]: r"""Loads all stored records from the key-value storage system. - If agent_id is provided, only records associated with the specified - agent will be returned. - - Args: - agent_id (str, optional): The ID of the agent associated with the - records. If not provided, all records will be loaded. - (default: :obj:`None`) Returns: List[Dict[str, Any]]: A list of dictionaries, where each dictionary represents a stored record. """ with self.json_path.open("r") as f: - all_records = [ + return [ json.loads(r, object_hook=self._json_object_hook) for r in f.readlines() ] - if agent_id is not None: - all_records = [ - r for r in all_records if r.get("agent_id") == agent_id - ] - - return all_records - def clear(self) -> None: r"""Removes all records from the key-value storage system.""" with self.json_path.open("w"): - pass + pass \ No newline at end of file diff --git a/camel/storages/key_value_storages/redis.py b/camel/storages/key_value_storages/redis.py index 5c6f16511d..b957cc160a 100644 --- a/camel/storages/key_value_storages/redis.py +++ b/camel/storages/key_value_storages/redis.py @@ -107,27 +107,15 @@ def save( except Exception as e: logger.error(f"Error in save: {e}") - def load(self, agent_id: Optional[str] = None) -> List[Dict[str, Any]]: + def load(self) -> List[Dict[str, Any]]: r"""Loads all stored records from the key-value storage system. - If agent_id is provided, only records associated with the specified - agent will be returned. - - Args: - agent_id (str, optional): The ID of the agent associated with the - records. If not provided, all records will be loaded. - (default: :obj:`None`) Returns: List[Dict[str, Any]]: A list of dictionaries, where each dictionary represents a stored record. """ try: - all_records = self._run_async(self._async_load()) - if agent_id is not None: - all_records = [ - r for r in all_records if r.get("agent_id") == agent_id - ] - return all_records + return self._run_async(self._async_load()) except Exception as e: logger.error(f"Error in load: {e}") return [] @@ -178,4 +166,4 @@ def _run_async(self, coro): return self._loop.run_until_complete(coro) else: future = asyncio.run_coroutine_threadsafe(coro, self._loop) - return future.result() + return future.result() \ No newline at end of file From dbf4a4975caef484c6aa15ae6f480da452df54ad Mon Sep 17 00:00:00 2001 From: X-TRON404 Date: Fri, 7 Mar 2025 20:05:38 +0530 Subject: [PATCH 14/20] fix: no need to pas agent_id in ChatHistoryBlock or load() --- camel/agents/chat_agent.py | 4 ++-- camel/memories/agent_memories.py | 12 ++++-------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/camel/agents/chat_agent.py b/camel/agents/chat_agent.py index 4645245999..9495fd24d3 100644 --- a/camel/agents/chat_agent.py +++ b/camel/agents/chat_agent.py @@ -186,7 +186,7 @@ def __init__( self.model_backend.token_counter, token_limit or self.model_backend.token_limit, ) - + self.memory: AgentMemory = memory or ChatHistoryMemory( context_creator, window_size=message_window_size, @@ -372,7 +372,7 @@ def load_memory_from_path(self, path: str) -> None: (optional check; commented out below). """ json_store = JsonStorage(Path(path)) - all_records = json_store.load(agent_id=self.agent_id) + all_records = json_store.load() if not all_records: raise ValueError( diff --git a/camel/memories/agent_memories.py b/camel/memories/agent_memories.py index fb2d8b7758..cfade1dfaa 100644 --- a/camel/memories/agent_memories.py +++ b/camel/memories/agent_memories.py @@ -50,9 +50,7 @@ def __init__( raise ValueError("`window_size` must be non-negative.") self._context_creator = context_creator self._window_size = window_size - self._chat_history_block = ChatHistoryBlock( - storage=storage, agent_id=agent_id - ) + self._chat_history_block = ChatHistoryBlock(storage=storage) self.agent_id = agent_id def retrieve(self) -> List[ContextRecord]: @@ -125,13 +123,13 @@ def write_records(self, records: List[MemoryRecord]) -> None: for record in records: if record.role_at_backend == OpenAIBackendRole.USER: self._current_topic = record.message.content - + # assign the agent_id to the record if record.agent_id == "": # if the agent memory has an agent_id, use it if self.agent_id is not None: record.agent_id = self.agent_id - + self._vectordb_block.write_records(records) def get_context_creator(self) -> BaseContextCreator: @@ -168,9 +166,7 @@ def __init__( retrieve_limit: int = 3, agent_id: Optional[str] = None, ) -> None: - self.chat_history_block = chat_history_block or ChatHistoryBlock( - agent_id=agent_id - ) + self.chat_history_block = chat_history_block or ChatHistoryBlock() self.vector_db_block = vector_db_block or VectorDBBlock() self.retrieve_limit = retrieve_limit self._context_creator = context_creator From 2e241ea2a2296d0b7e9dc0a37aed3d467d9b15ac Mon Sep 17 00:00:00 2001 From: X-TRON404 Date: Fri, 7 Mar 2025 20:26:08 +0530 Subject: [PATCH 15/20] fix: precommit checks --- camel/memories/agent_memories.py | 28 ++++++++++- camel/memories/base.py | 12 ++++- camel/memories/blocks/chat_history_block.py | 2 +- camel/storages/key_value_storages/base.py | 2 +- .../storages/key_value_storages/in_memory.py | 2 +- camel/storages/key_value_storages/json.py | 2 +- camel/storages/key_value_storages/redis.py | 2 +- camel/toolkits/memory_toolkit.py | 13 +++++ .../memories/agent_memory_vector_db_test.py | 47 +++++++++++++++---- examples/memories/vector_db_memory.py | 43 +++++++++++++---- 10 files changed, 128 insertions(+), 25 deletions(-) create mode 100644 camel/toolkits/memory_toolkit.py diff --git a/camel/memories/agent_memories.py b/camel/memories/agent_memories.py index cfade1dfaa..8bf35d1a7d 100644 --- a/camel/memories/agent_memories.py +++ b/camel/memories/agent_memories.py @@ -51,7 +51,15 @@ def __init__( self._context_creator = context_creator self._window_size = window_size self._chat_history_block = ChatHistoryBlock(storage=storage) - self.agent_id = agent_id + self._agent_id = agent_id + + @property + def agent_id(self) -> Optional[str]: + return self._agent_id + + @agent_id.setter + def agent_id(self, val: Optional[str]) -> None: + self._agent_id = val def retrieve(self) -> List[ContextRecord]: records = self._chat_history_block.retrieve(self._window_size) @@ -108,10 +116,18 @@ def __init__( self._context_creator = context_creator self._retrieve_limit = retrieve_limit self._vectordb_block = VectorDBBlock(storage=storage) - self.agent_id = agent_id + self._agent_id = agent_id self._current_topic: str = "" + @property + def agent_id(self) -> Optional[str]: + return self._agent_id + + @agent_id.setter + def agent_id(self, val: Optional[str]) -> None: + self._agent_id = val + def retrieve(self) -> List[ContextRecord]: return self._vectordb_block.retrieve( self._current_topic, @@ -173,6 +189,14 @@ def __init__( self._current_topic: str = "" self._agent_id = agent_id + @property + def agent_id(self) -> Optional[str]: + return self._agent_id + + @agent_id.setter + def agent_id(self, val: Optional[str]) -> None: + self._agent_id = val + def get_context_creator(self) -> BaseContextCreator: r"""Returns the context creator used by the memory. diff --git a/camel/memories/base.py b/camel/memories/base.py index e0a5da76dd..140a720097 100644 --- a/camel/memories/base.py +++ b/camel/memories/base.py @@ -13,7 +13,7 @@ # ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. ========= from abc import ABC, abstractmethod -from typing import List, Tuple +from typing import List, Optional, Tuple from camel.memories.records import ContextRecord, MemoryRecord from camel.messages import OpenAIMessage @@ -112,6 +112,16 @@ class AgentMemory(MemoryBlock, ABC): the memory records stored within the AgentMemory. """ + @property + @abstractmethod + def agent_id(self) -> Optional[str]: + pass + + @agent_id.setter + @abstractmethod + def agent_id(self, val: Optional[str]) -> None: + pass + @abstractmethod def retrieve(self) -> List[ContextRecord]: r"""Get a record list from the memory for creating model context. diff --git a/camel/memories/blocks/chat_history_block.py b/camel/memories/blocks/chat_history_block.py index 2ae5ab9ed4..68218dbb41 100644 --- a/camel/memories/blocks/chat_history_block.py +++ b/camel/memories/blocks/chat_history_block.py @@ -120,4 +120,4 @@ def write_records(self, records: List[MemoryRecord]) -> None: def clear(self) -> None: r"""Clears all chat messages from the memory.""" - self.storage.clear() \ No newline at end of file + self.storage.clear() diff --git a/camel/storages/key_value_storages/base.py b/camel/storages/key_value_storages/base.py index ee8b854204..b47d999f70 100644 --- a/camel/storages/key_value_storages/base.py +++ b/camel/storages/key_value_storages/base.py @@ -53,4 +53,4 @@ def load(self) -> List[Dict[str, Any]]: @abstractmethod def clear(self) -> None: r"""Removes all records from the key-value storage system.""" - pass \ No newline at end of file + pass diff --git a/camel/storages/key_value_storages/in_memory.py b/camel/storages/key_value_storages/in_memory.py index ac4aa78c3a..17c3f75e5a 100644 --- a/camel/storages/key_value_storages/in_memory.py +++ b/camel/storages/key_value_storages/in_memory.py @@ -47,4 +47,4 @@ def load(self) -> List[Dict[str, Any]]: def clear(self) -> None: r"""Removes all records from the key-value storage system.""" - self.memory_list.clear() \ No newline at end of file + self.memory_list.clear() diff --git a/camel/storages/key_value_storages/json.py b/camel/storages/key_value_storages/json.py index 094092af3d..50f666029c 100644 --- a/camel/storages/key_value_storages/json.py +++ b/camel/storages/key_value_storages/json.py @@ -94,4 +94,4 @@ def load(self) -> List[Dict[str, Any]]: def clear(self) -> None: r"""Removes all records from the key-value storage system.""" with self.json_path.open("w"): - pass \ No newline at end of file + pass diff --git a/camel/storages/key_value_storages/redis.py b/camel/storages/key_value_storages/redis.py index b957cc160a..30c5c47a49 100644 --- a/camel/storages/key_value_storages/redis.py +++ b/camel/storages/key_value_storages/redis.py @@ -166,4 +166,4 @@ def _run_async(self, coro): return self._loop.run_until_complete(coro) else: future = asyncio.run_coroutine_threadsafe(coro, self._loop) - return future.result() \ No newline at end of file + return future.result() diff --git a/camel/toolkits/memory_toolkit.py b/camel/toolkits/memory_toolkit.py new file mode 100644 index 0000000000..0f91e59f50 --- /dev/null +++ b/camel/toolkits/memory_toolkit.py @@ -0,0 +1,13 @@ +# ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. ========= +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. ========= diff --git a/examples/memories/agent_memory_vector_db_test.py b/examples/memories/agent_memory_vector_db_test.py index d751dfff4c..e997af0fdc 100644 --- a/examples/memories/agent_memory_vector_db_test.py +++ b/examples/memories/agent_memory_vector_db_test.py @@ -1,14 +1,30 @@ +# ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. ========= +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. ========= import os from pathlib import Path from camel.agents import ChatAgent from camel.memories import VectorDBMemory -from camel.memories.context_creators.score_based import ScoreBasedContextCreator +from camel.memories.context_creators.score_based import ( + ScoreBasedContextCreator, +) from camel.models.model_factory import ModelFactory from camel.storages.vectordb_storages import QdrantStorage from camel.types import ModelPlatformType, ModelType from camel.utils import OpenAITokenCounter + def test_chat_agent_vectordb_memory_save_and_load(): # 1) Create a QdrantStorage in-memory instance # (in production, set path to a real directory or remote) @@ -46,11 +62,17 @@ def test_chat_agent_vectordb_memory_save_and_load(): # 5) Agent #1 accumulates some conversation user_input_1 = "Remember that dolphins use echolocation." response_1 = agent1.step(user_input_1) - print("Agent #1 response:", response_1.msgs[0].content if response_1.msgs else "No response") + print( + "Agent #1 response:", + response_1.msgs[0].content if response_1.msgs else "No response", + ) user_input_2 = "And whales are the largest mammals." response_2 = agent1.step(user_input_2) - print("Agent #1 response:", response_2.msgs[0].content if response_2.msgs else "No response") + print( + "Agent #1 response:", + response_2.msgs[0].content if response_2.msgs else "No response", + ) # 6) SAVE agent1's memory to a JSON file (storing the conversation) save_path = Path("./agent1_vectordb_memory.json") @@ -59,12 +81,14 @@ def test_chat_agent_vectordb_memory_save_and_load(): # 7) Create a new agent, load that JSON memory to confirm retrieval new_agent1 = ChatAgent( - system_message="You are the resurrected assistant #1 with vector DB memory.", - agent_id="agent_001", # same agent_id to match the saved records + system_message="""You are the resurrected assistant #1 with + vector DB memory.""", + agent_id="agent_001", # same agent_id to match the saved records model=model, ) # Use a new VectorDBMemory pointing to the same underlying storage - # (or a new vector store if you prefer, but that won't have the original embeddings) + # (or a new vector store if you prefer, but that won't have the + # original embeddings) new_agent1_memory = VectorDBMemory( context_creator=context_creator, storage=vector_storage, @@ -72,17 +96,24 @@ def test_chat_agent_vectordb_memory_save_and_load(): agent_id=new_agent1.agent_id, ) + new_agent1.memory = new_agent1_memory + # Load memory from JSON, which replays the stored MemoryRecords new_agent1.load_memory_from_path(save_path) - # 8) Check if the new agent can recall previous info from loaded conversation + # 8) Check if the new agent can recall previous info from loaded + # conversation user_input_3 = "What do you remember about marine mammals?" response_3 = new_agent1.step(user_input_3) - print("New Agent #1 response (after loading memory):", response_3.msgs[0].content if response_3.msgs else "No response") + print( + "New Agent #1 response (after loading memory):", + response_3.msgs[0].content if response_3.msgs else "No response", + ) # Optionally, remove the JSON file if os.path.exists(save_path): os.remove(save_path) + if __name__ == "__main__": test_chat_agent_vectordb_memory_save_and_load() diff --git a/examples/memories/vector_db_memory.py b/examples/memories/vector_db_memory.py index 867dccc97b..f25acf25c8 100644 --- a/examples/memories/vector_db_memory.py +++ b/examples/memories/vector_db_memory.py @@ -1,15 +1,27 @@ -# vector_db_memory_test.py - -import os -from pathlib import Path +# ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. ========= +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. ========= from camel.agents import ChatAgent from camel.memories import VectorDBMemory -from camel.memories.context_creators.score_based import ScoreBasedContextCreator +from camel.memories.context_creators.score_based import ( + ScoreBasedContextCreator, +) from camel.models.model_factory import ModelFactory from camel.storages.vectordb_storages import QdrantStorage from camel.types import ModelPlatformType, ModelType from camel.utils import OpenAITokenCounter + def test_chat_agent_vectordb_memory(): # Shared vector storage vector_storage = QdrantStorage( @@ -71,18 +83,31 @@ def test_chat_agent_vectordb_memory(): response_1 = agent1.step("What did I tell you about whales or elephants?") response_2 = agent2.step("What have I told you about stars and moons?") - print("Agent 1 response:", response_1.msgs[0].content if response_1.msgs else "No response") - print("Agent 2 response:", response_2.msgs[0].content if response_2.msgs else "No response") + print( + "Agent 1 response:", + response_1.msgs[0].content if response_1.msgs else "No response", + ) + print( + "Agent 2 response:", + response_2.msgs[0].content if response_2.msgs else "No response", + ) # Retrieve and print agent-specific records print("\nAgent 1's memory records:") for ctx_record in vectordb_memory_agent1.retrieve(): - print(f"Score: {ctx_record.score:.2f} | Content: {ctx_record.memory_record.message.content}") + print( + f"""Score: {ctx_record.score:.2f} | + Content: {ctx_record.memory_record.message.content}""" + ) print("\nAgent 2's memory records:") retrieved_context_agent2 = vectordb_memory_agent2.retrieve() for ctx_record in retrieved_context_agent2: - print(f"Score: {ctx_record.score:.2f} | Content: {ctx_record.memory_record.message.content}") + print( + f"""Score: {ctx_record.score:.2f} | + Content: {ctx_record.memory_record.message.content}""" + ) + if __name__ == "__main__": test_chat_agent_vectordb_memory() From f73c7aa7743d34c6194121a084519bd5cd87529a Mon Sep 17 00:00:00 2001 From: X-TRON404 Date: Mon, 10 Mar 2025 19:58:18 +0530 Subject: [PATCH 16/20] feat: added memory toolkit --- camel/toolkits/__init__.py | 5 +- camel/toolkits/memory_toolkit.py | 124 +++++++++++++++++++++++++--- examples/toolkits/memory_toolkit.py | 74 +++++++++++++++++ 3 files changed, 188 insertions(+), 15 deletions(-) create mode 100644 examples/toolkits/memory_toolkit.py diff --git a/camel/toolkits/__init__.py b/camel/toolkits/__init__.py index 475f3aaa59..1d5dcd9727 100644 --- a/camel/toolkits/__init__.py +++ b/camel/toolkits/__init__.py @@ -50,7 +50,7 @@ from .zapier_toolkit import ZapierToolkit from .sympy_toolkit import SymPyToolkit from .mineru_toolkit import MinerUToolkit - +from .memory_toolkit import MemoryToolkit __all__ = [ 'BaseToolkit', @@ -88,4 +88,5 @@ 'ZapierToolkit', 'SymPyToolkit', 'MinerUToolkit', -] + 'MemoryToolkit' +] \ No newline at end of file diff --git a/camel/toolkits/memory_toolkit.py b/camel/toolkits/memory_toolkit.py index 0f91e59f50..9cd73de1ad 100644 --- a/camel/toolkits/memory_toolkit.py +++ b/camel/toolkits/memory_toolkit.py @@ -1,13 +1,111 @@ -# ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. ========= -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. ========= +# memory_toolkit.py +import json +from typing import Optional + +from camel.toolkits.base import BaseToolkit +from camel.toolkits.function_tool import FunctionTool +from camel.memories import ChatHistoryMemory, MemoryRecord, ScoreBasedContextCreator + +class MemoryToolkit(BaseToolkit): + r""" + A toolkit that provides methods for saving, loading, and clearing a ChatAgent's memory. + These methods are exposed as FunctionTool objects for function calling. + Internally, it calls: + - agent.save_memory(path) + - agent.load_memory(new_memory_obj) + - agent.load_memory_from_path(path) + - agent.clear_memory() + """ + + def __init__(self, agent: 'ChatAgent', timeout: Optional[float] = None): + super().__init__(timeout=timeout) + self.agent = agent + + def save(self, path: str) -> str: + r""" + Saves the agent's current memory to a JSON file by calling agent.save_memory(path). + + Args: + path (str): The file path to save the memory to. + + Returns: + str: Confirmation message. + """ + self.agent.save_memory(path) + return f"Memory saved to {path}" + + def load(self, memory_json: str) -> str: + r""" + Loads memory into the agent from a JSON string: + 1) Parse `memory_json` as a list of memory records + 2) Create a fresh ChatHistoryMemory + 3) Write records into that memory + 4) Call agent.load_memory(...) with that new memory + + Args: + memory_json (str): A JSON string containing memory records. + + Returns: + str: Confirmation or error message. + """ + try: + data = json.loads(memory_json.strip()) + if not isinstance(data, list): + return "[ERROR] Memory data should be a list of records." + + # Build a fresh ChatHistoryMemory + context_creator = ScoreBasedContextCreator( + token_counter=self.agent.model_backend.token_counter, + token_limit=self.agent.model_backend.token_limit, + ) + new_memory = ChatHistoryMemory(context_creator) + + # Convert each record dict -> MemoryRecord + for record_dict in data: + record = MemoryRecord.from_dict(record_dict) + new_memory.write_record(record) + + # Load into the agent + self.agent.load_memory(new_memory) + return "Loaded memory from provided JSON string." + except json.JSONDecodeError: + return "[ERROR] Invalid JSON string provided." + except Exception as e: + return f"[ERROR] Failed to load memory: {str(e)}" + + def load_from_path(self, path: str) -> str: + r""" + Loads the agent's memory from a JSON file by calling agent.load_memory_from_path(path). + + Args: + path (str): The file path to load the memory from. + + Returns: + str: Confirmation message. + """ + self.agent.load_memory_from_path(path) + return f"Memory loaded from {path}" + + def clear_memory(self) -> str: + r""" + Clears the agent's memory by calling agent.clear_memory(). + + Returns: + str: Confirmation message. + """ + self.agent.clear_memory() + return "Memory has been cleared." + + def get_tools(self) -> list[FunctionTool]: + r""" + Expose the memory management methods as function tools for the ChatAgent. + + Returns: + list[FunctionTool]: List of FunctionTool objects. + """ + return [ + FunctionTool(self.save), + FunctionTool(self.load), + FunctionTool(self.load_from_path), + FunctionTool(self.clear_memory), + ] \ No newline at end of file diff --git a/examples/toolkits/memory_toolkit.py b/examples/toolkits/memory_toolkit.py new file mode 100644 index 0000000000..04375d0172 --- /dev/null +++ b/examples/toolkits/memory_toolkit.py @@ -0,0 +1,74 @@ +# memory_toolkit_example.py +import json +from camel.agents import ChatAgent +from camel.models import ModelFactory +from camel.configs import ChatGPTConfig +from camel.types import ModelPlatformType, ModelType +from camel.toolkits.memory_toolkit import MemoryToolkit + +def run_memory_toolkit_example(): + """ + Demonstrates a ChatAgent using the MemoryToolkit for function calling to manage memory. + """ + + # Create a Model + model_config_dict = ChatGPTConfig(temperature=0.0).as_dict() + model = ModelFactory.create( + model_platform=ModelPlatformType.DEFAULT, + model_type=ModelType.DEFAULT, + model_config_dict=model_config_dict, + ) + + # Create a ChatAgent + agent = ChatAgent( + system_message="You are an assistant that can manage conversation memory using tools.", + model=model, + ) + + # Add MemoryToolkit to the Agent + memory_toolkit = MemoryToolkit(agent=agent) + for tool in memory_toolkit.get_tools(): + agent.add_tool(tool) + + # Have a conversation to populate memory + print("\n--- Starting a Conversation ---") + user_msg_1 = "Tell me about the moon." + print(f"[User] {user_msg_1}") + response_1 = agent.step(user_msg_1) + print(f"[Agent] {response_1.msgs[0].content}") + + # Save the memory to a file via function calling + print("\n--- Saving Memory ---") + save_msg = "Please save the current memory to 'conversation_memory.json'." + response_save = agent.step(save_msg) + print(f"[Agent] {response_save.msgs[0].content}") + print(f"[Tool Call Info] {response_save.info['tool_calls']}") + + # Clear the memory via function calling + print("\n--- Clearing Memory ---") + clear_msg = "Please clear the memory." + response_clear = agent.step(clear_msg) + print(f"[Agent] {response_clear.msgs[0].content}") + print(f"[Tool Call Info] {response_clear.info['tool_calls']}") + + # Verify memory is cleared + print("\n--- Checking Memory After Clear ---") + check_msg = "What do you remember about the moon?" + response_check = agent.step(check_msg) + print(f"[Agent] {response_check.msgs[0].content}") + + # Load memory from the saved file via function calling + print("\n--- Loading Memory from File ---") + load_msg = "Please load the memory from 'conversation_memory.json'." + response_load = agent.step(load_msg) + print(f"[Agent] {response_load.msgs[0].content}") + print(f"[Tool Call Info] {response_load.info['tool_calls']}") + + # Verify memory is restored + print("\n--- Checking Memory After Load ---") + check_msg = "What do you remember about the moon?" + response_restored = agent.step(check_msg) + print(f"[Agent] {response_restored.msgs[0].content}") + +if __name__ == "__main__": + run_memory_toolkit_example() \ No newline at end of file From 4c8ed1b5439b1a58645ae265a7bc41d88d770f88 Mon Sep 17 00:00:00 2001 From: X-TRON404 Date: Tue, 11 Mar 2025 19:36:45 +0530 Subject: [PATCH 17/20] feat: precommit checks and tests --- camel/agents/base.py | 22 ++++++++ camel/toolkits/memory_toolkit.py | 63 +++++++++++++++------- examples/toolkits/memory_toolkit.py | 30 ++++++++--- test/toolkits/test_memory_toolkit.py | 78 ++++++++++++++++++++++++++++ 4 files changed, 167 insertions(+), 26 deletions(-) create mode 100644 test/toolkits/test_memory_toolkit.py diff --git a/camel/agents/base.py b/camel/agents/base.py index f6af3d4743..91f8931c6e 100644 --- a/camel/agents/base.py +++ b/camel/agents/base.py @@ -27,3 +27,25 @@ def reset(self, *args: Any, **kwargs: Any) -> Any: def step(self, *args: Any, **kwargs: Any) -> Any: r"""Performs a single step of the agent.""" pass + + @abstractmethod + def save_memory(self, path: str) -> None: + r"""Retrieves the current conversation data from memory and writes it + into a JSON file.""" + pass + + @abstractmethod + def load_memory(self, memory: Any) -> None: + r"""Load the provided memory into the agent.""" + pass + + @abstractmethod + def load_memory_from_path(self, path: str) -> None: + r"""Loads memory records from a JSON file filtered by this + agent's ID.""" + pass + + @abstractmethod + def clear_memory(self) -> None: + r"""Clear the agent's memory and reset to initial state.""" + pass diff --git a/camel/toolkits/memory_toolkit.py b/camel/toolkits/memory_toolkit.py index 9cd73de1ad..0bfe43c996 100644 --- a/camel/toolkits/memory_toolkit.py +++ b/camel/toolkits/memory_toolkit.py @@ -1,33 +1,56 @@ +# ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. ========= +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. ========= # memory_toolkit.py import json -from typing import Optional +from typing import TYPE_CHECKING, Optional +from camel.memories import ( + ChatHistoryMemory, + MemoryRecord, + ScoreBasedContextCreator, +) from camel.toolkits.base import BaseToolkit from camel.toolkits.function_tool import FunctionTool -from camel.memories import ChatHistoryMemory, MemoryRecord, ScoreBasedContextCreator + +if TYPE_CHECKING: + from camel.agents import ChatAgent + class MemoryToolkit(BaseToolkit): r""" - A toolkit that provides methods for saving, loading, and clearing a ChatAgent's memory. - These methods are exposed as FunctionTool objects for function calling. - Internally, it calls: + A toolkit that provides methods for saving, loading, and clearing a + ChatAgent's memory. + These methods are exposed as FunctionTool objects for + function calling. Internally, it calls: - agent.save_memory(path) - agent.load_memory(new_memory_obj) - agent.load_memory_from_path(path) - agent.clear_memory() """ - + def __init__(self, agent: 'ChatAgent', timeout: Optional[float] = None): super().__init__(timeout=timeout) self.agent = agent def save(self, path: str) -> str: r""" - Saves the agent's current memory to a JSON file by calling agent.save_memory(path). - + Saves the agent's current memory to a JSON file by calling + agent.save_memory(path). + Args: path (str): The file path to save the memory to. - + Returns: str: Confirmation message. """ @@ -41,10 +64,10 @@ def load(self, memory_json: str) -> str: 2) Create a fresh ChatHistoryMemory 3) Write records into that memory 4) Call agent.load_memory(...) with that new memory - + Args: memory_json (str): A JSON string containing memory records. - + Returns: str: Confirmation or error message. """ @@ -71,15 +94,16 @@ def load(self, memory_json: str) -> str: except json.JSONDecodeError: return "[ERROR] Invalid JSON string provided." except Exception as e: - return f"[ERROR] Failed to load memory: {str(e)}" + return f"[ERROR] Failed to load memory: {e!s}" def load_from_path(self, path: str) -> str: r""" - Loads the agent's memory from a JSON file by calling agent.load_memory_from_path(path). - + Loads the agent's memory from a JSON file by calling + agent.load_memory_from_path(path). + Args: path (str): The file path to load the memory from. - + Returns: str: Confirmation message. """ @@ -89,7 +113,7 @@ def load_from_path(self, path: str) -> str: def clear_memory(self) -> str: r""" Clears the agent's memory by calling agent.clear_memory(). - + Returns: str: Confirmation message. """ @@ -98,8 +122,9 @@ def clear_memory(self) -> str: def get_tools(self) -> list[FunctionTool]: r""" - Expose the memory management methods as function tools for the ChatAgent. - + Expose the memory management methods as function tools + for the ChatAgent. + Returns: list[FunctionTool]: List of FunctionTool objects. """ @@ -108,4 +133,4 @@ def get_tools(self) -> list[FunctionTool]: FunctionTool(self.load), FunctionTool(self.load_from_path), FunctionTool(self.clear_memory), - ] \ No newline at end of file + ] diff --git a/examples/toolkits/memory_toolkit.py b/examples/toolkits/memory_toolkit.py index 04375d0172..1560e13119 100644 --- a/examples/toolkits/memory_toolkit.py +++ b/examples/toolkits/memory_toolkit.py @@ -1,14 +1,28 @@ -# memory_toolkit_example.py -import json +# ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. ========= +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. ========= + from camel.agents import ChatAgent -from camel.models import ModelFactory from camel.configs import ChatGPTConfig -from camel.types import ModelPlatformType, ModelType +from camel.models import ModelFactory from camel.toolkits.memory_toolkit import MemoryToolkit +from camel.types import ModelPlatformType, ModelType + def run_memory_toolkit_example(): """ - Demonstrates a ChatAgent using the MemoryToolkit for function calling to manage memory. + Demonstrates a ChatAgent using the MemoryToolkit for + function calling to manage memory. """ # Create a Model @@ -21,7 +35,8 @@ def run_memory_toolkit_example(): # Create a ChatAgent agent = ChatAgent( - system_message="You are an assistant that can manage conversation memory using tools.", + system_message="""You are an assistant that can manage + conversation memory using tools.""", model=model, ) @@ -70,5 +85,6 @@ def run_memory_toolkit_example(): response_restored = agent.step(check_msg) print(f"[Agent] {response_restored.msgs[0].content}") + if __name__ == "__main__": - run_memory_toolkit_example() \ No newline at end of file + run_memory_toolkit_example() diff --git a/test/toolkits/test_memory_toolkit.py b/test/toolkits/test_memory_toolkit.py new file mode 100644 index 0000000000..18b4e408ab --- /dev/null +++ b/test/toolkits/test_memory_toolkit.py @@ -0,0 +1,78 @@ +# ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. ========= +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. ========= +import json +import pytest +from unittest.mock import MagicMock + +from camel.toolkits.memory_toolkit import MemoryToolkit + +@pytest.fixture(scope="function") +def memory_toolkit_fixture(): + mock_agent = MagicMock() + mock_agent.model_backend.token_counter = MagicMock() + mock_agent.model_backend.token_limit = 1000 + toolkit = MemoryToolkit(agent=mock_agent) + return toolkit + +def test_save_memory(memory_toolkit_fixture): + toolkit = memory_toolkit_fixture + path = "test_memory.json" + + toolkit.agent.save_memory.return_value = f"Memory saved to {path}" + result = toolkit.save(path) + + assert "Memory saved to" in result + +def test_load_memory_valid_json(memory_toolkit_fixture): + toolkit = memory_toolkit_fixture + memory_json = json.dumps([{"message": {"content": "hello", "role": "user"}, + "role_at_backend": "user", "agent_id": "test_agent"}]) + + # Mocking the memory loading process properly + mock_memory = MagicMock() + toolkit.agent.load_memory.return_value = None # Simulate a successful load + result = toolkit.load(memory_json) + +def test_load_memory_invalid_json(memory_toolkit_fixture): + toolkit = memory_toolkit_fixture + invalid_json = "{invalid json}" + + result = toolkit.load(invalid_json) + + assert "[ERROR] Invalid JSON string provided." in result + +def test_load_memory_wrong_format(memory_toolkit_fixture): + toolkit = memory_toolkit_fixture + incorrect_json = json.dumps({"not": "a list"}) + + result = toolkit.load(incorrect_json) + + assert "[ERROR] Memory data should be a list of records." in result + +def test_load_from_path(memory_toolkit_fixture): + toolkit = memory_toolkit_fixture + path = "memory.json" + + toolkit.agent.load_memory_from_path.return_value = f"Memory loaded from {path}" + result = toolkit.load_from_path(path) + + assert "Memory loaded from" in result + +def test_clear_memory(memory_toolkit_fixture): + toolkit = memory_toolkit_fixture + + toolkit.agent.clear_memory.return_value = "Memory has been cleared." + result = toolkit.clear_memory() + + assert "Memory has been cleared." in result From 358c4d43cacd0e540b47611977ca49bdbe6b737e Mon Sep 17 00:00:00 2001 From: X-TRON404 Date: Tue, 11 Mar 2025 20:09:31 +0530 Subject: [PATCH 18/20] feat: not needed --- camel/agents/base.py | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/camel/agents/base.py b/camel/agents/base.py index 91f8931c6e..f6af3d4743 100644 --- a/camel/agents/base.py +++ b/camel/agents/base.py @@ -27,25 +27,3 @@ def reset(self, *args: Any, **kwargs: Any) -> Any: def step(self, *args: Any, **kwargs: Any) -> Any: r"""Performs a single step of the agent.""" pass - - @abstractmethod - def save_memory(self, path: str) -> None: - r"""Retrieves the current conversation data from memory and writes it - into a JSON file.""" - pass - - @abstractmethod - def load_memory(self, memory: Any) -> None: - r"""Load the provided memory into the agent.""" - pass - - @abstractmethod - def load_memory_from_path(self, path: str) -> None: - r"""Loads memory records from a JSON file filtered by this - agent's ID.""" - pass - - @abstractmethod - def clear_memory(self) -> None: - r"""Clear the agent's memory and reset to initial state.""" - pass From d0229c1f52119d65c57e8ec0d7c9e4d458e1c13d Mon Sep 17 00:00:00 2001 From: X-TRON404 Date: Wed, 12 Mar 2025 11:24:26 +0530 Subject: [PATCH 19/20] fix: fixed tests for mem toolkit --- camel/storages/key_value_storages/__init__.py | 3 +- camel/storages/key_value_storages/json.py | 6 +-- test/toolkits/test_memory_toolkit.py | 52 ++++++++++++++++--- 3 files changed, 50 insertions(+), 11 deletions(-) diff --git a/camel/storages/key_value_storages/__init__.py b/camel/storages/key_value_storages/__init__.py index 014a692885..5428c836f9 100644 --- a/camel/storages/key_value_storages/__init__.py +++ b/camel/storages/key_value_storages/__init__.py @@ -14,7 +14,7 @@ from .base import BaseKeyValueStorage from .in_memory import InMemoryKeyValueStorage -from .json import JsonStorage +from .json import CamelJSONEncoder, JsonStorage from .redis import RedisStorage __all__ = [ @@ -22,4 +22,5 @@ 'InMemoryKeyValueStorage', 'JsonStorage', 'RedisStorage', + 'CamelJSONEncoder', ] diff --git a/camel/storages/key_value_storages/json.py b/camel/storages/key_value_storages/json.py index 50f666029c..8dd36d6b4b 100644 --- a/camel/storages/key_value_storages/json.py +++ b/camel/storages/key_value_storages/json.py @@ -26,7 +26,7 @@ ) -class _CamelJSONEncoder(json.JSONEncoder): +class CamelJSONEncoder(json.JSONEncoder): r"""A custom JSON encoder for serializing specifically enumerated types. Ensures enumerated types can be stored in and retrieved from JSON format. """ @@ -62,7 +62,7 @@ def __init__(self, path: Optional[Path] = None) -> None: def _json_object_hook(self, d) -> Any: if "__enum__" in d: name, member = d["__enum__"].split(".") - return getattr(_CamelJSONEncoder.CAMEL_ENUMS[name], member) + return getattr(CamelJSONEncoder.CAMEL_ENUMS[name], member) else: return d @@ -75,7 +75,7 @@ def save(self, records: List[Dict[str, Any]]) -> None: """ with self.json_path.open("a") as f: f.writelines( - [json.dumps(r, cls=_CamelJSONEncoder) + "\n" for r in records] + [json.dumps(r, cls=CamelJSONEncoder) + "\n" for r in records] ) def load(self) -> List[Dict[str, Any]]: diff --git a/test/toolkits/test_memory_toolkit.py b/test/toolkits/test_memory_toolkit.py index 18b4e408ab..cf6412eb69 100644 --- a/test/toolkits/test_memory_toolkit.py +++ b/test/toolkits/test_memory_toolkit.py @@ -12,10 +12,25 @@ # limitations under the License. # ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. ========= import json -import pytest +from enum import Enum from unittest.mock import MagicMock +import pytest + +from camel.memories import MemoryRecord +from camel.messages.base import BaseMessage +from camel.storages.key_value_storages import CamelJSONEncoder from camel.toolkits.memory_toolkit import MemoryToolkit +from camel.types import RoleType +from camel.types.enums import OpenAIBackendRole + + +class TestCamelJSONEncoder(CamelJSONEncoder): + def default(self, obj): + if isinstance(obj, Enum): + return obj.value # Serialize enum to its string value + return super().default(obj) + @pytest.fixture(scope="function") def memory_toolkit_fixture(): @@ -25,6 +40,7 @@ def memory_toolkit_fixture(): toolkit = MemoryToolkit(agent=mock_agent) return toolkit + def test_save_memory(memory_toolkit_fixture): toolkit = memory_toolkit_fixture path = "test_memory.json" @@ -34,16 +50,27 @@ def test_save_memory(memory_toolkit_fixture): assert "Memory saved to" in result + def test_load_memory_valid_json(memory_toolkit_fixture): toolkit = memory_toolkit_fixture - memory_json = json.dumps([{"message": {"content": "hello", "role": "user"}, - "role_at_backend": "user", "agent_id": "test_agent"}]) + mock_message = BaseMessage( + role_name="User", + role_type=RoleType.USER, + meta_dict={}, + content="test message", + ) + mock_record = MemoryRecord( + message=mock_message, + role_at_backend=OpenAIBackendRole.USER, + agent_id="test_agent", + ) + memory_json = json.dumps([mock_record.to_dict()], cls=TestCamelJSONEncoder) - # Mocking the memory loading process properly - mock_memory = MagicMock() - toolkit.agent.load_memory.return_value = None # Simulate a successful load result = toolkit.load(memory_json) + assert "Loaded memory from provided JSON string." in result + + def test_load_memory_invalid_json(memory_toolkit_fixture): toolkit = memory_toolkit_fixture invalid_json = "{invalid json}" @@ -52,6 +79,7 @@ def test_load_memory_invalid_json(memory_toolkit_fixture): assert "[ERROR] Invalid JSON string provided." in result + def test_load_memory_wrong_format(memory_toolkit_fixture): toolkit = memory_toolkit_fixture incorrect_json = json.dumps({"not": "a list"}) @@ -60,15 +88,19 @@ def test_load_memory_wrong_format(memory_toolkit_fixture): assert "[ERROR] Memory data should be a list of records." in result + def test_load_from_path(memory_toolkit_fixture): toolkit = memory_toolkit_fixture path = "memory.json" - toolkit.agent.load_memory_from_path.return_value = f"Memory loaded from {path}" + toolkit.agent.load_memory_from_path.return_value = ( + f"Memory loaded from {path}" + ) result = toolkit.load_from_path(path) assert "Memory loaded from" in result + def test_clear_memory(memory_toolkit_fixture): toolkit = memory_toolkit_fixture @@ -76,3 +108,9 @@ def test_clear_memory(memory_toolkit_fixture): result = toolkit.clear_memory() assert "Memory has been cleared." in result + + +if __name__ == "__main__": + import sys + + pytest.main([sys.argv[0]]) From f672ea9d84c58c2709ee1d4597428f21715ea3db Mon Sep 17 00:00:00 2001 From: X-TRON404 Date: Wed, 12 Mar 2025 11:33:08 +0530 Subject: [PATCH 20/20] fix: merge conflict error_info --- camel/agents/chat_agent.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/camel/agents/chat_agent.py b/camel/agents/chat_agent.py index 0d9601c7fc..de453d92a4 100644 --- a/camel/agents/chat_agent.py +++ b/camel/agents/chat_agent.py @@ -778,12 +778,13 @@ def _get_model_response( f"index: {self.model_backend.current_model_index}", exc_info=exc, ) - if not response: + error_info = str(exc) + + if not response and self.model_backend.num_models > 1: raise ModelProcessingError( "Unable to process messages: none of the provided models " "run successfully." ) - elif not response: raise ModelProcessingError( f"Unable to process messages: the only provided model " @@ -822,12 +823,13 @@ async def _aget_model_response( f"index: {self.model_backend.current_model_index}", exc_info=exc, ) - if not response: + error_info = str(exc) + + if not response and self.model_backend.num_models > 1: raise ModelProcessingError( "Unable to process messages: none of the provided models " "run successfully." ) - elif not response: raise ModelProcessingError( f"Unable to process messages: the only provided model "