[features] memory service support multi-role_name!

This commit is contained in:
jinli.yl 2024-07-30 17:03:19 +08:00
parent 81d60b840c
commit f1398ad262
20 changed files with 339 additions and 280 deletions

View file

@ -1,5 +1,5 @@
[flake8]
exclude = tests/models/test_models_lli_embedding.py,tests/*,examples/*,memoryscope/core/storage/llama_index_sync_elasticsearch.py
exclude = tests/*,examples/*,memoryscope/core/storage/llama_index_sync_elasticsearch.py
max-line-length = 120
inline-quotes = "
avoid-escape = no

View file

@ -1,7 +1,3 @@
import sys
sys.path.append(".")
from memoryscope import MemoryScope, Arguments
arguments = Arguments(
@ -31,7 +27,6 @@ def chat_example1():
def chat_example2():
with MemoryScope(arguments=arguments) as ms:
memory_chat = ms.default_memory_chat
response = memory_chat.chat_with_memory(query="我的爱好是弹琴。")
print("回答:\n" + response.message.content)
print("记忆:\n" + response.meta_data["memories"])
@ -40,7 +35,6 @@ def chat_example2():
def chat_example3():
with MemoryScope(arguments=arguments) as ms:
memory_chat = ms.default_memory_chat
response = memory_chat.chat_with_memory(
query="你知道我消灭了几个星系吗?",
temporary_memories="用户是一只哥斯拉,这是真是的,不是虚构的。\n前天用户消灭了星系0x4be\n大前天用户消灭了星系0x5be")
@ -50,18 +44,14 @@ def chat_example3():
def chat_example4():
with MemoryScope(arguments=arguments) as ms:
memory_chat = ms.default_memory_chat
result = memory_chat.run_service_operation("delete_all")
memory_chat.run_service_operation("delete_all")
response = memory_chat.chat_with_memory(query="我的爱好是弹琴。")
print("回答1\n" + response.message.content)
# memory_chat.start_backend_service()
result = memory_chat.run_service_operation("consolidate_memory")
print("记忆更新变化:\n" + result)
response = memory_chat.chat_with_memory(query="你知道我的乐器爱好是什么?",
history_message_strategy=None)
response = memory_chat.chat_with_memory(query="你知道我的乐器爱好是什么?", history_message_strategy=None)
print("回答2\n" + response.message.content)
print("记忆2\n" + response.meta_data["memories"])
@ -72,14 +62,46 @@ def chat_example5():
memory_service.init_service()
result = memory_service.list_memory()
print(f"list_memory result={result}")
result = memory_service.retrieve_memory()
print(f"retrieve_memory result={result}")
result = memory_service.consolidate_memory()
print(result)
print(f"consolidate_memory result={result}")
def chat_example6():
with MemoryScope(arguments=arguments) as ms:
memory_chat = ms.default_memory_chat
memory_chat.run_service_operation("delete_all", "锦鲤")
memory_chat.run_service_operation("delete_all", "浩然")
print("浩然=========================")
response = memory_chat.chat_with_memory(query="我的爱好是弹琴。", role_name="浩然")
print("回答1\n" + response.message.content)
result = memory_chat.run_service_operation("consolidate_memory", role_name="浩然")
print("记忆更新变化:\n" + result)
response = memory_chat.chat_with_memory(query="你知道我的乐器爱好是什么?", role_name="浩然",
history_message_strategy=None)
print("回答2\n" + response.message.content)
print("记忆2\n" + response.meta_data["memories"])
print("锦鲤=========================")
response = memory_chat.chat_with_memory(query="我的爱好是打羽毛球。", role_name="锦鲤")
print("回答1\n" + response.message.content)
result = memory_chat.run_service_operation("consolidate_memory", role_name="锦鲤")
print("记忆更新变化:\n" + result)
response = memory_chat.chat_with_memory(query="你知道我的运动爱好是什么?", role_name="锦鲤",
history_message_strategy=None)
print("回答2\n" + response.message.content)
print("记忆2\n" + response.meta_data["memories"])
if __name__ == "__main__":
# chat_example1()
# chat_example2()
# chat_example3()
chat_example4()
# chat_example4()
# chat_example5()
chat_example6()

View file

@ -1 +1 @@
python memoryscope/cli.py --config_path=memoryscope/core/config/demo_config.yaml
memoryscope --config_path=memoryscope/core/config/demo_config.yaml

View file

@ -13,10 +13,16 @@ MEMORIES = "memories"
CHAT_MESSAGES = "chat_messages"
MEMORY_MANAGER = "memory_manager"
CHAT_MESSAGES_SCATTER = "chat_messages_scatter"
CHAT_KWARGS = "chat_kwargs"
USER_NAME = "user_name"
TARGET_NAME = "target_name"
MEMORY_MANAGER = "memory_manager"
QUERY_WITH_TS = "query_with_ts"
RETRIEVE_MEMORY_NODES = "retrieve_memory_nodes"

View file

@ -20,8 +20,6 @@ class ApiMemoryChat(BaseMemoryChat):
generation_model: str,
context: MemoryscopeContext,
stream: bool = False,
human_name: str = None,
assistant_name: str = None,
**kwargs):
super().__init__(**kwargs)
@ -32,16 +30,6 @@ class ApiMemoryChat(BaseMemoryChat):
self.stream: bool = stream
self.generation_model_kwargs: dict = kwargs.pop("generation_model_kwargs", {})
self.human_name: str = human_name
if not self.human_name:
self.human_name = DEFAULT_HUMAN_NAME[self.context.language]
self.context.meta_data["human_name"] = self.human_name
self.assistant_name: str = assistant_name
if not self.assistant_name:
self.assistant_name = "AI"
self.context.meta_data["assistant_name"] = self.assistant_name
self._prompt_handler: PromptHandler | None = None
@property
@ -85,6 +73,14 @@ class ApiMemoryChat(BaseMemoryChat):
self._memory_service.init_service()
return self._memory_service
@property
def human_name(self):
return self.memory_service.human_name
@property
def assistant_name(self):
return self.memory_service.assistant_name
@property
def generation_model(self) -> BaseModel:
"""
@ -117,7 +113,7 @@ class ApiMemoryChat(BaseMemoryChat):
if model_response and model_response.message:
model_response.message.role_name = self.assistant_name
model_response.meta_data[MEMORIES] = memories
self.memory_service.add_messages([query_message, model_response.message])
self.memory_service.add_messages_pair([query_message, model_response.message])
else:
self.logger.warning("model_response or model_response.message is empty!")
@ -179,9 +175,10 @@ class ApiMemoryChat(BaseMemoryChat):
history_messages = self.memory_service.read_message()
elif isinstance(history_message_strategy, int):
history_messages = self.memory_service.chat_messages[-history_message_strategy:]
history_messages = self.memory_service.get_chat_messages_scatter(history_message_strategy)
if history_messages:
assert isinstance(history_messages[0], Message)
chat_messages.extend(history_messages)
# Append the current user's message to the conversation context
@ -198,7 +195,7 @@ class ApiMemoryChat(BaseMemoryChat):
if model_response and model_response.message:
model_response.message.role_name = self.assistant_name
model_response.meta_data[MEMORIES] = memories
self.memory_service.add_messages([query_message, model_response.message])
self.memory_service.add_messages_pair([query_message, model_response.message])
else:
self.logger.warning("model_response or model_response.message is empty!")
return model_response

View file

@ -49,7 +49,7 @@ class BaseMemoryChat(metaclass=ABCMeta):
- If it is set to "auto" the history messages in the conversation will retain those that have not
yet been summarized. Default to "auto".
- If it is set to None no conversation history will be saved.
- If it is set to an integer value "n", the most recent "n" messages will be retained.
- If it is set to an integer value "n", recent "n" message-pair[user, assistant] will be retained.
remember_response (bool, optional): Flag indicating whether to save the AI's response to memory.
Defaults to False.
Returns:
@ -59,11 +59,11 @@ class BaseMemoryChat(metaclass=ABCMeta):
"""
raise NotImplementedError
def start_backend_service(self):
self.memory_service.start_backend_service()
def start_backend_service(self, **kwargs):
self.memory_service.start_backend_service(**kwargs)
def run_service_operation(self, name: str, **kwargs):
return self.memory_service.run_operation(name, **kwargs)
def run_service_operation(self, name: str, role_name: Optional[str] = None, **kwargs):
return self.memory_service.run_operation(name, role_name=role_name, **kwargs)
def run(self):
"""

View file

@ -17,6 +17,8 @@ class Arguments(object):
memory_chat_class: str = field(default="cli_memory_chat", metadata={
"help": "cli_memory_chat(Command-line interaction), api_memory_chat(API interface interaction), etc."})
chat_stream: bool | None = field(default=None)
human_name: str = field(default="user")
assistant_name: str = field(default="AI")

View file

@ -87,11 +87,11 @@ class ConfigManager(object):
@staticmethod
def update_memory_chat_by_arguments(config: dict, arguments: Arguments):
memory_chat_class_split = config["class"].split(".")
stream = arguments.memory_chat_class in ["cli_memory_chat", ]
stream = arguments.chat_stream
if stream is None:
stream = arguments.memory_chat_class in ["cli_memory_chat", ]
config.update({
"class": ".".join(memory_chat_class_split[:-1] + [arguments.memory_chat_class]),
"human_name": arguments.human_name if arguments.human_name else "",
"assistant_name": arguments.assistant_name if arguments.assistant_name else "",
"stream": stream,
})

View file

@ -12,12 +12,12 @@ memory_chat:
memory_service: memoryscope_service
generation_model: generation_model
stream: true
human_name: user
assistant_name: AI
memory_service:
memoryscope_service:
class: core.service.memory_scope_service
human_name: user
assistant_name: AI
memory_operations:
read_message:
class: core.operation.frontend_operation

View file

@ -59,7 +59,7 @@ class MemoryScope(ConfigManager):
def close(self):
# wait service to stop
for _, service in self.context.memory_service_dict.items():
service.stop_backend_service(wait_service_end=True)
service.stop_backend_service(wait_service=True)
self.context.thread_pool.shutdown()

View file

@ -1,38 +1,25 @@
import time
from typing import List
from memoryscope.constants.common_constants import CHAT_KWARGS, RESULT, CHAT_MESSAGES
from memoryscope.core.operation.base_operation import BaseOperation, OPERATION_TYPE
from memoryscope.core.operation.base_workflow import BaseWorkflow
from memoryscope.core.utils.logger import Logger
from memoryscope.scheme.message import Message
from memoryscope.core.operation.base_operation import OPERATION_TYPE
from memoryscope.core.operation.frontend_operation import FrontendOperation
class BackendOperation(BaseWorkflow, BaseOperation):
class BackendOperation(FrontendOperation):
"""
BaseBackendOperation serves as an abstract base class for defining backend operations.
It manages operation status, loop control, and integrates with a global context for thread management.
"""
operation_type: OPERATION_TYPE = "backend"
def __init__(self,
name: str,
description: str,
chat_messages: List[Message],
interval_time: int,
**kwargs):
super().__init__(name=name, **kwargs)
BaseOperation.__init__(self, name=name, description=description)
def __init__(self, interval_time: int, **kwargs):
super().__init__(**kwargs)
self.chat_messages: List[Message] = chat_messages
self.interval_time: int = interval_time
self._interval_time: int = interval_time
self._operation_status_run: bool = False
self._loop_switch: bool = False
self._backend_task = None
self.logger = Logger.get_logger()
def init_workflow(self, **kwargs):
"""
Initializes the workflow by setting up workers with provided keyword arguments.
@ -42,84 +29,53 @@ class BackendOperation(BaseWorkflow, BaseOperation):
"""
self.init_workers(is_backend=True, **kwargs)
def _run_operation(self, **kwargs):
"""
Executes an operation within the workflow by clearing the context,
setting chat arguments, running the workflow, and returning the result.
Args:
**kwargs: Keyword arguments necessary for the operation, including chat parameters.
Returns:
Any: The result obtained after executing the workflow.
"""
# prepare kwargs
workflow_kwargs = {
CHAT_MESSAGES: self.chat_messages,
CHAT_KWARGS: {**kwargs, **self.kwargs},
}
# Execute the workflow with the prepared context
self.run_workflow(**workflow_kwargs)
# Retrieve the result from the context after workflow execution
return self.context.get(RESULT)
def run_operation(self, **kwargs):
"""
Executes the operation defined by `_run_operation` method with given keyword arguments,
while managing the operation status and exception handling.
Args:
**kwargs: Arbitrary keyword arguments to be passed to `_run_operation`.
Returns:
The result of the `_run_operation` method if no exception occurs, otherwise None.
"""
if self._operation_status_run:
return
self._operation_status_run = True
result = None
try:
result = self._run_operation(**kwargs)
except Exception as e:
self.logger.exception(f"{self.name} encounter exception. args={e.args}")
self._operation_status_run = False
return result
def _loop_operation(self):
def _loop_operation(self, **kwargs):
"""
Loops until _loop_switch is False, sleeping for 1 second in each interval.
At each interval, it checks if _loop_switch is still True, and if so, executes the operation.
"""
while self._loop_switch:
for _ in range(self.interval_time):
for _ in range(self._interval_time):
if self._loop_switch:
time.sleep(1)
else:
break
if self._loop_switch:
self.run_operation()
def start_operation_backend(self):
if self._loop_switch:
if self._operation_status_run:
continue
self._operation_status_run = True
if len(self.target_names) > 1:
self.logger.warning("current version is not stable under target_names.size > 1!")
for target_name in self.target_names:
try:
self.run_operation(target_name=target_name, **kwargs)
except Exception as e:
self.logger.exception(f"op_name={self.name} target_name={target_name} encounter exception. "
f"args={e.args}")
self._operation_status_run = False
def start_operation_backend(self, **kwargs):
"""
Initiates the background operation loop if it's not already running.
Sets the _loop_switch to True and submits the _loop_operation to a thread from the global thread pool.
"""
if not self._loop_switch:
self._loop_switch = True
self._backend_task = self.thread_pool.submit(self._loop_operation)
self._backend_task = self.thread_pool.submit(self._loop_operation, **kwargs)
self.logger.info(f"start operation={self.name}...")
def stop_operation_backend(self, wait_task_end: bool = False):
def stop_operation_backend(self, wait_operation: bool = False):
"""
Stops the background operation loop by setting the _loop_switch to False.
"""
self._loop_switch = False
if self._backend_task:
if wait_task_end:
if wait_operation:
self._backend_task.result()
self.logger.info(f"stop operation={self.name}...")
else:

View file

@ -1,5 +1,7 @@
from abc import ABCMeta, abstractmethod
from typing import Literal
from typing import Literal, List
from memoryscope.scheme.message import Message
OPERATION_TYPE = Literal["frontend", "backend"]
@ -16,15 +18,19 @@ class BaseOperation(metaclass=ABCMeta):
operation_type: OPERATION_TYPE = "frontend"
def __init__(self, name: str, description: str = ""):
def __init__(self,
name: str,
user_name: str,
target_names: List[str],
chat_messages: List[List[Message]],
description: str):
"""
Initializes a new instance of the BaseOperation.
Args:
name (str): The name identifying the operation.
description (str): An optional description detailing the operation's purpose or behavior.
"""
self.name: str = name
self.user_name: str = user_name
self.target_names: List[str] = target_names
self.chat_messages: List[List[Message]] = chat_messages
self.description: str = description
def init_workflow(self, **kwargs):
@ -37,12 +43,13 @@ class BaseOperation(metaclass=ABCMeta):
pass
@abstractmethod
def run_operation(self, **kwargs):
def run_operation(self, target_name: str, **kwargs):
"""
Abstract method to define the operation to be run.
Subclasses must implement this method.
Args:
target_name (str): target_name(human name).
**kwargs: Keyword arguments for running the operation.
Raises:
@ -50,14 +57,14 @@ class BaseOperation(metaclass=ABCMeta):
"""
raise NotImplementedError
def start_operation_backend(self):
def start_operation_backend(self, **kwargs):
"""
Placeholder method for running an operation specific to the backend.
Intended to be overridden by subclasses if backend operations are required.
"""
pass
def stop_operation_backend(self, wait_task_end: bool = False):
def stop_operation_backend(self, wait_operation: bool = False):
"""
Placeholder method to stop any ongoing backend operations.
Should be implemented in subclasses where backend operations are managed.

View file

@ -1,17 +1,21 @@
from memoryscope.constants.common_constants import CHAT_KWARGS, CHAT_MESSAGES, RESULT
from typing import List
from memoryscope.constants.common_constants import CHAT_KWARGS, CHAT_MESSAGES, RESULT, TARGET_NAME, USER_NAME
from memoryscope.core.operation.backend_operation import BackendOperation
from memoryscope.enumeration.message_role_enum import MessageRoleEnum
from memoryscope.scheme.message import Message
class ConsolidateMemoryOp(BackendOperation):
def __init__(self, **kwargs):
super(ConsolidateMemoryOp, self).__init__(**kwargs)
def __init__(self,
message_lock,
contextual_msg_min_count: int = 0,
**kwargs):
super().__init__(**kwargs)
self.message_lock = message_lock
self.contextual_msg_min_count: int = contextual_msg_min_count
self.message_lock = kwargs.get("message_lock", None)
self.contextual_msg_min_count: int = kwargs.get("contextual_msg_min_count", 0)
def _run_operation(self, **kwargs):
def run_operation(self, target_name: str, **kwargs):
"""
Executes an operation after preparing the chat context, checking message memory status,
and updating workflow status accordingly.
@ -21,32 +25,45 @@ class ConsolidateMemoryOp(BackendOperation):
captures the result, and updates the memory status.
Args:
target_name (str): target_name(human name).
**kwargs: Keyword arguments for chat operation configuration.
Returns:
Any: The result obtained from running the workflow.
"""
if not self.chat_messages:
chat_messages: List[List[Message]] = []
for messages in self.chat_messages:
if not messages:
continue
if messages[0].memorized:
continue
contain_flag = False
for msg in messages:
if msg.role_name == target_name:
contain_flag = True
break
if contain_flag:
chat_messages.append(messages)
if not chat_messages:
self.logger.info(f"empty not_memorized chat_messages for target_name={target_name}.")
return
# Use shallow copy to prevent adding new messages.
chat_messages = self.chat_messages.copy()
# filter for user-assistant pair
if chat_messages[-1].role == MessageRoleEnum.USER.value:
chat_messages = chat_messages[:-1]
not_memorized_size = sum([not x.memorized for x in chat_messages])
if not_memorized_size < self.contextual_msg_min_count:
self.logger.info(f"not_memorized_size({not_memorized_size}) < "
f"contextual_msg_min_count({self.contextual_msg_min_count}), skip.")
if len(chat_messages) < self.contextual_msg_min_count:
self.logger.info(f"not_memorized_size={len(chat_messages)} < {self.contextual_msg_min_count}, skip.")
return
# prepare kwargs
workflow_kwargs = {
CHAT_MESSAGES: chat_messages,
CHAT_KWARGS: {**kwargs, **self.kwargs},
TARGET_NAME: target_name,
USER_NAME: self.user_name,
}
# Execute the workflow with the prepared context
@ -57,7 +74,8 @@ class ConsolidateMemoryOp(BackendOperation):
# set message memorized
with self.message_lock:
for message in chat_messages:
message.memorized = True
for messages in chat_messages:
for msg in messages:
msg.memorized = True
return result

View file

@ -1,6 +1,6 @@
from typing import List
from memoryscope.constants.common_constants import RESULT, CHAT_MESSAGES, CHAT_KWARGS
from memoryscope.constants.common_constants import RESULT, CHAT_MESSAGES, CHAT_KWARGS, TARGET_NAME, USER_NAME
from memoryscope.core.operation.base_operation import BaseOperation, OPERATION_TYPE
from memoryscope.core.operation.base_workflow import BaseWorkflow
from memoryscope.scheme.message import Message
@ -11,13 +11,18 @@ class FrontendOperation(BaseWorkflow, BaseOperation):
def __init__(self,
name: str,
user_name: str,
target_names: List[str],
chat_messages: List[List[Message]],
description: str,
chat_messages: List[Message],
**kwargs):
super().__init__(name=name, **kwargs)
BaseOperation.__init__(self, name=name, description=description)
self.chat_messages: List[Message] = chat_messages
BaseOperation.__init__(self,
name=name,
user_name=user_name,
target_names=target_names,
chat_messages=chat_messages,
description=description)
def init_workflow(self, **kwargs):
"""
@ -28,12 +33,13 @@ class FrontendOperation(BaseWorkflow, BaseOperation):
"""
self.init_workers(**kwargs)
def run_operation(self, **kwargs):
def run_operation(self, target_name: str, **kwargs):
"""
Executes the main operation of reading recent chat messages, initializing workflow,
and returning the result of the workflow execution.
Args:
target_name (str): target_name(human name).
**kwargs: Additional keyword arguments used in the operation context.
Returns:
@ -44,6 +50,8 @@ class FrontendOperation(BaseWorkflow, BaseOperation):
workflow_kwargs = {
CHAT_MESSAGES: self.chat_messages,
CHAT_KWARGS: {**kwargs, **self.kwargs},
TARGET_NAME: target_name,
USER_NAME: self.user_name,
}
# Execute the workflow with the prepared context

View file

@ -1,6 +1,7 @@
from abc import ABCMeta, abstractmethod
from typing import List, Dict
from memoryscope.constants.language_constants import DEFAULT_HUMAN_NAME
from memoryscope.core.memoryscope_context import MemoryscopeContext
from memoryscope.core.operation.base_operation import BaseOperation
from memoryscope.core.utils.logger import Logger
@ -14,23 +15,54 @@ class BaseMemoryService(metaclass=ABCMeta):
along with logging capabilities and customizable configurations.
"""
def __init__(self, memory_operations: Dict[str, dict], context: MemoryscopeContext, **kwargs):
def __init__(self,
memory_operations: Dict[str, dict],
context: MemoryscopeContext,
assistant_name: str = None,
human_name: str = None,
**kwargs):
"""
Initializes the BaseMemoryService with operation definitions, keys for memory access,
and additional keyword arguments for flexibility.
Args:
memory_operations (Dict[str, dict]): A dictionary defining available memory operations.
context (MemoryscopeContext): runtime context.
human_name (str): human name.
assistant_name (str): assistant name.
**kwargs: Additional parameters to customize service behavior.
"""
self.memory_operations_conf: Dict[str, dict] = memory_operations
self.context: MemoryscopeContext = context
self.kwargs = kwargs
self._operations_conf: Dict[str, dict] = memory_operations
self._context: MemoryscopeContext = context
self._human_name: str = human_name
self._assistant_name: str = assistant_name
self._kwargs = kwargs
if not self._human_name:
self._human_name = DEFAULT_HUMAN_NAME[self._context.language]
if not self._assistant_name:
self._assistant_name = "AI"
self._operation_dict: Dict[str, BaseOperation] = {}
self.chat_messages: List[Message] = []
self._chat_messages: List[List[Message]] = []
self._role_names: List[str] = []
self.logger = Logger.get_logger()
@property
def human_name(self) -> str:
return self._human_name
@property
def assistant_name(self) -> str:
return self._assistant_name
def get_chat_messages_scatter(self, recent_n_pair: int) -> List[Message]:
chat_messages_scatter: List[Message] = []
for messages in self._chat_messages[-recent_n_pair:]:
chat_messages_scatter.extend(messages)
return chat_messages_scatter
@property
def op_description_dict(self) -> Dict[str, str]:
"""
@ -41,7 +73,7 @@ class BaseMemoryService(metaclass=ABCMeta):
return {k: v.description for k, v in self._operation_dict.items()}
@abstractmethod
def add_messages(self, messages: List[Message] | Message):
def add_messages_pair(self, messages: List[Message]):
raise NotImplementedError
@abstractmethod
@ -52,30 +84,15 @@ class BaseMemoryService(metaclass=ABCMeta):
def init_service(self, **kwargs):
raise NotImplementedError
def start_backend_service(self, name: str = None):
def start_backend_service(self, name: str = None, **kwargs):
pass
def stop_backend_service(self, wait_service_end: bool = False):
def stop_backend_service(self, wait_service: bool = False):
pass
def run_operation(self, name: str, **kwargs):
"""
Executes a specific operation by its name with provided keyword arguments.
Args:
name (str): The name of the operation to execute.
**kwargs: Keyword arguments for the operation's execution.
Returns:
The result of the operation execution, if any. Otherwise, None.
Raises:
Warning: If the operation name is not initialized in `_operation_dict`.
"""
if name not in self._operation_dict:
self.logger.warning(f"operation={name} is not registered!")
return
return self._operation_dict[name].run_operation(**kwargs)
@abstractmethod
def run_operation(self, name: str, role_name: str = "", **kwargs):
raise NotImplementedError
def __getattr__(self, name: str):
assert name in self._operation_dict, f"operation={name} is not registered!"

View file

@ -10,10 +10,8 @@ from memoryscope.scheme.message import Message
class MemoryScopeService(BaseMemoryService):
def __init__(self,
history_msg_count: int = 100,
contextual_msg_max_count: int = 20,
contextual_msg_max_count: int = 10,
contextual_msg_min_count: int = 0,
human_name: str = None,
assistant_name: str = None,
**kwargs):
"""
init function.
@ -23,47 +21,45 @@ class MemoryScopeService(BaseMemoryService):
it will not be included in the context to prevent token overflow.
contextual_msg_min_count (int): The minimum context length in a conversation. If it is shorter than this
length, no conversation summary will be made and no long-term memory will be generated.
human_name (str): human name.
assistant_name (str): assistant name.
kwargs (dict): other kwargs.
kwargs (dict): Additional parameters to customize service behavior.
"""
super().__init__(**kwargs)
self.history_msg_count: int = history_msg_count
self.contextual_msg_max_count: int = contextual_msg_max_count
self.contextual_msg_min_count: int = contextual_msg_min_count
assert history_msg_count >= contextual_msg_max_count >= contextual_msg_min_count
if human_name:
self.context.meta_data["human_name"] = human_name
if assistant_name:
self.context.meta_data["assistant_name"] = assistant_name
self._history_msg_count: int = history_msg_count
self._contextual_msg_max_count: int = contextual_msg_max_count
self._contextual_msg_min_count: int = contextual_msg_min_count
self.message_lock = threading.Lock()
self._message_lock = threading.Lock()
def add_messages(self, messages: List[Message] | Message):
def add_messages_pair(self, messages: List[Message]):
"""
Adds a single message or a list of messages to the chat history, ensuring the message list
remains sorted by creation time and does not exceed the maximum history message count.
Adds a list of messages to the chat history, it can be a pair [user_message, assistant_message].
Ensuring the message list remains sorted by creation time and does not exceed the maximum history message count.
Args:
messages (List[Message] | Message): A single message instance or a list of message instances
to be added to the chat history.
"""
# If a single message is provided, convert it into a list for uniform processing
if isinstance(messages, Message):
messages = [messages]
assert messages, "messages should not be empty!"
with self.message_lock:
with self._message_lock:
# Append the sorted messages to the chat history
self.chat_messages.extend(messages)
self._chat_messages.append(messages)
# Sort the messages by their creation time to maintain chronological order
self.chat_messages.sort(key=lambda x: x.time_created)
self._chat_messages.sort(key=lambda x: x[0].time_created)
# If the chat history exceeds the allowed message count, remove the oldest messages
if len(self.chat_messages) > self.history_msg_count:
gap_size = len(self.chat_messages) - self.history_msg_count
if len(self._chat_messages) > self._history_msg_count:
gap_size = len(self._chat_messages) - self._history_msg_count
for _ in range(gap_size):
self.chat_messages.pop(0)
self._chat_messages.pop(0)
for message in messages:
if message.role_name and message.role_name != self.assistant_name \
and message.role_name not in self._role_names:
self._role_names.append(message.role_name)
def register_operation(self, name: str, operation_config: dict, **kwargs):
if name in self._operation_dict:
@ -73,11 +69,13 @@ class MemoryScopeService(BaseMemoryService):
operation: BaseOperation = init_instance_by_config(
config=operation_config,
name=name,
chat_messages=self.chat_messages,
message_lock=self.message_lock,
memoryscope_context=self.context,
contextual_msg_max_count=self.contextual_msg_max_count,
contextual_msg_min_count=self.contextual_msg_min_count)
user_name=self._assistant_name,
target_names=self._role_names,
chat_messages=self._chat_messages,
message_lock=self._message_lock,
memoryscope_context=self._context,
contextual_msg_max_count=self._contextual_msg_max_count,
contextual_msg_min_count=self._contextual_msg_min_count)
# Initialize workflow for each operation
operation.init_workflow(**kwargs)
@ -85,25 +83,52 @@ class MemoryScopeService(BaseMemoryService):
self.logger.info(f"service={self.__class__.__name__} init operation={name}")
def init_service(self, **kwargs):
for name, operation_config in self.memory_operations_conf.items():
for name, operation_config in self._operations_conf.items():
self.register_operation(name, operation_config, **kwargs)
def start_backend_service(self, name: str = None):
def run_operation(self, name: str, role_name: str = "", **kwargs):
"""
Executes a specific operation by its name with provided keyword arguments.
Args:
name (str): The name of the operation to execute.
role_name (str): The name of the operation to execute.
**kwargs: Keyword arguments for the operation's execution.
Returns:
The result of the operation execution, if any. Otherwise, None.
Raises:
Warning: If the operation name is not initialized in `_operation_dict`.
"""
if name not in self._operation_dict:
self.logger.warning(f"operation={name} is not registered!")
return
target_name = self._human_name
if role_name:
target_name = role_name
if role_name not in self._role_names:
self._role_names.append(role_name)
return self._operation_dict[name].run_operation(target_name=target_name, **kwargs)
def start_backend_service(self, name: str = None, **kwargs):
"""
Start all backend operations.
"""
for op_name, operation in self._operation_dict.items():
if name:
if op_name == name:
operation.start_operation_backend()
operation.start_operation_backend(**kwargs)
else:
if operation.operation_type == "backend":
operation.start_operation_backend()
operation.start_operation_backend(**kwargs)
def stop_backend_service(self, wait_service_end: bool = False):
def stop_backend_service(self, wait_service: bool = False):
"""
Stops all backend operations that are currently running.
"""
for _, operation in self._operation_dict.items():
if operation.operation_type == "backend":
operation.stop_operation_backend(wait_task_end=wait_service_end)
operation.stop_operation_backend(wait_operation=wait_service)

View file

@ -3,7 +3,6 @@ from typing import List
from memoryscope.constants.language_constants import COLON_WORD
from memoryscope.core.utils.response_text_parser import ResponseTextParser
from memoryscope.core.worker.memory_base_worker import MemoryBaseWorker
from memoryscope.enumeration.message_role_enum import MessageRoleEnum
from memoryscope.scheme.message import Message
@ -40,8 +39,8 @@ class InfoFilterWorker(MemoryBaseWorker):
if msg.memorized:
continue
# TODO: add memory for assistant
if msg.role != MessageRoleEnum.USER.value:
# TODO: add memory for all messages
if msg.role_name != self.target_name:
continue
if len(msg.content) >= self.info_filter_msg_max_size:

View file

@ -1,6 +1,8 @@
from typing import List
from memoryscope.constants.common_constants import RESULT
from memoryscope.core.worker.memory_base_worker import MemoryBaseWorker
from memoryscope.enumeration.message_role_enum import MessageRoleEnum
from memoryscope.scheme.message import Message
class ReadMessageWorker(MemoryBaseWorker):
@ -12,11 +14,27 @@ class ReadMessageWorker(MemoryBaseWorker):
"""
Executes the primary function to fetch unmemorized chat messages.
"""
chat_messages = [x for x in self.chat_messages if not x.memorized]
if len(chat_messages) > 0 and chat_messages[-1].role == MessageRoleEnum.USER.value:
chat_messages = chat_messages[:-1]
chat_messages_not_memorized: List[List[Message]] = []
for messages in self.chat_messages_origin:
if not messages:
continue
if messages[0].memorized:
continue
contain_flag = False
for msg in messages:
if msg.role_name == self.target_name:
contain_flag = True
break
if contain_flag:
chat_messages_not_memorized.append(messages)
contextual_msg_max_count: int = self.chat_kwargs["contextual_msg_max_count"]
chat_messages = chat_messages[-contextual_msg_max_count:]
self.set_context(RESULT, chat_messages)
chat_message_scatter = []
for messages in chat_messages_not_memorized[-contextual_msg_max_count:]:
chat_message_scatter.extend(messages)
chat_message_scatter.sort(key=lambda _: _.time_created)
self.set_context(RESULT, chat_message_scatter)

View file

@ -2,7 +2,6 @@ import datetime
from memoryscope.constants.common_constants import QUERY_WITH_TS
from memoryscope.core.worker.memory_base_worker import MemoryBaseWorker
from memoryscope.enumeration.message_role_enum import MessageRoleEnum
class SetQueryWorker(MemoryBaseWorker):
@ -36,25 +35,5 @@ class SetQueryWorker(MemoryBaseWorker):
if _timestamp and isinstance(_timestamp, int):
timestamp = _timestamp
# check role_name
role_name = self.chat_kwargs.get("role_name")
if role_name:
assert role_name == self.target_name, (f"role_name={role_name} <> target_name={self.target_name} "
f"is not supported in human/assistant memory workflow!")
elif self.chat_messages:
# If no explicit query is given, use the content of the latest chat message
chat_messages = [msg for msg in self.chat_messages if msg.role == MessageRoleEnum.USER.value]
if chat_messages:
message = chat_messages[-1]
query = message.content
timestamp = message.time_created
# check role_name
role_name = message.role_name
if role_name:
assert role_name == self.target_name, \
f"role_name={role_name} is not supported in human/assistant memory workflow!"
# Store the determined query and its timestamp in the context
self.set_context(QUERY_WITH_TS, (query, timestamp))

View file

@ -2,7 +2,7 @@ from abc import ABCMeta
from typing import List, Dict, Any
from memoryscope.constants.common_constants import CHAT_MESSAGES, CHAT_KWARGS, MEMORYSCOPE_CONTEXT, \
WORKFLOW_NAME, MEMORY_MANAGER
WORKFLOW_NAME, MEMORY_MANAGER, USER_NAME, TARGET_NAME, CHAT_MESSAGES_SCATTER
from memoryscope.constants.language_constants import DEFAULT_HUMAN_NAME, USER_NAME_EXPRESSION
from memoryscope.core.memoryscope_context import MemoryscopeContext
from memoryscope.core.models.base_model import BaseModel
@ -45,11 +45,18 @@ class MemoryBaseWorker(BaseWorker, metaclass=ABCMeta):
self._memory_store: BaseMemoryStore | None = None
self._monitor: BaseMonitor | None = None
self._user_name: str | None = None
self._target_name: str | None = None
self._prompt_handler: PromptHandler | None = None
@property
def chat_messages_origin(self) -> List[List[Message]]:
"""
Property to get the chat messages.
Returns:
List[Message]: List of chat messages.
"""
return self.get_context(CHAT_MESSAGES)
@property
def chat_messages(self) -> List[Message]:
"""
@ -58,15 +65,30 @@ class MemoryBaseWorker(BaseWorker, metaclass=ABCMeta):
Returns:
List[Message]: List of chat messages.
"""
return self.get_context(CHAT_MESSAGES)
result = self.get_context(CHAT_MESSAGES_SCATTER)
if not result:
if isinstance(self.chat_messages_origin[0], list):
chat_messages: List[Message] = []
for messages in self.chat_messages_origin:
if messages:
chat_messages.extend(messages)
chat_messages.sort(key=lambda _: _.time_created)
self.set_context(CHAT_MESSAGES_SCATTER, chat_messages)
else:
assert isinstance(self.chat_messages_origin[0], Message)
self.set_context(CHAT_MESSAGES_SCATTER, self.chat_messages_origin)
return self.get_context(CHAT_MESSAGES_SCATTER)
@chat_messages.setter
def chat_messages(self, value):
def chat_messages(self, value: List[Message]):
"""
Set the chat messages with the new value.
"""
self.set_context(CHAT_MESSAGES, value)
self.set_context(CHAT_MESSAGES_SCATTER, value)
@property
def chat_kwargs(self) -> Dict[str, Any]:
@ -81,6 +103,14 @@ class MemoryBaseWorker(BaseWorker, metaclass=ABCMeta):
"""
return self.get_context(CHAT_KWARGS)
@property
def user_name(self) -> str:
return self.get_context(USER_NAME)
@property
def target_name(self) -> str:
return self.get_context(TARGET_NAME)
@property
def workflow_name(self) -> str:
return self.get_context(WORKFLOW_NAME)
@ -158,31 +188,6 @@ class MemoryBaseWorker(BaseWorker, metaclass=ABCMeta):
self._monitor = self.memoryscope_context.monitor
return self._monitor
@property
def user_name(self) -> str:
"""
Property to get the username from the meta_data of the global context.
If not set initially, it retrieves the 'assistant_name' as the username.
Returns:
str: The name of the assistant.
"""
if self._user_name is None:
self._user_name = self.memoryscope_context.meta_data["assistant_name"]
return self._user_name
@property
def target_name(self) -> str:
"""
Retrieves the target name, initializing it from meta_data if not set.
Returns:
str: The readable name of the human.
"""
if self._target_name is None:
self._target_name = self.memoryscope_context.meta_data["human_name"]
return self._target_name
@property
def prompt_handler(self) -> PromptHandler:
"""