mirror of
https://github.com/agentscope-ai/ReMe.git
synced 2026-09-06 08:16:00 +00:00
finish memory chat
This commit is contained in:
parent
3c423118a5
commit
5d6e6c0f21
5 changed files with 84 additions and 227 deletions
|
|
@ -1,20 +1,14 @@
|
|||
import os
|
||||
import time
|
||||
from typing import List
|
||||
|
||||
import questionary
|
||||
|
||||
from memoryscope.chat.base_memory_chat import BaseMemoryChat
|
||||
from memoryscope.constants.language_constants import DEFAULT_HUMAN_NAME
|
||||
from memoryscope.enumeration.message_role_enum import MessageRoleEnum
|
||||
from memoryscope.memory.service.base_memory_service import BaseMemoryService
|
||||
from memoryscope.memoryscope_context import MemoryscopeContext
|
||||
from memoryscope.models.base_model import BaseModel
|
||||
from memoryscope.scheme.message import Message
|
||||
from memoryscope.scheme.model_response import ModelResponse, ModelResponseGen
|
||||
from memoryscope.utils.global_context import G_CONTEXT
|
||||
from memoryscope.utils.logger import Logger
|
||||
from memoryscope.utils.prompt_handler import PromptHandler
|
||||
from memoryscope.utils.tool_functions import char_logo
|
||||
|
||||
|
||||
class ApiMemoryChat(BaseMemoryChat):
|
||||
|
|
@ -22,28 +16,27 @@ class ApiMemoryChat(BaseMemoryChat):
|
|||
def __init__(self,
|
||||
memory_service: str,
|
||||
generation_model: str,
|
||||
stream: bool = True,
|
||||
human_name: str = DEFAULT_HUMAN_NAME[G_CONTEXT.language],
|
||||
assistant_name: str = "AI",
|
||||
context: MemoryscopeContext,
|
||||
human_name: str = None,
|
||||
assistant_name: str = None,
|
||||
**kwargs):
|
||||
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self._memory_service: BaseMemoryService | str = memory_service
|
||||
self._generation_model: BaseModel | str = generation_model
|
||||
self.context: MemoryscopeContext = context
|
||||
self.generation_model_kwargs: dict = kwargs.pop("generation_model_kwargs", {})
|
||||
|
||||
self.stream: bool = stream
|
||||
self.human_name: str = human_name
|
||||
if not self.human_name:
|
||||
self.human_name = DEFAULT_HUMAN_NAME[self.context.language]
|
||||
|
||||
self.assistant_name: str = assistant_name
|
||||
self.kwargs: dict = kwargs
|
||||
if not self.assistant_name:
|
||||
self.assistant_name = "AI"
|
||||
|
||||
self._logo = char_logo("MemoryScope")
|
||||
self._prompt_handler: PromptHandler | None = None
|
||||
G_CONTEXT.meta_data.update({
|
||||
"human_name": human_name,
|
||||
"assistant_name": assistant_name,
|
||||
})
|
||||
|
||||
self.logger = Logger.get_logger()
|
||||
|
||||
@property
|
||||
def prompt_handler(self) -> PromptHandler:
|
||||
|
|
@ -57,24 +50,14 @@ class ApiMemoryChat(BaseMemoryChat):
|
|||
PromptHandler: An instance of the PromptHandler configured for this CLI session.
|
||||
"""
|
||||
if self._prompt_handler is None:
|
||||
self._prompt_handler = PromptHandler(__file__, **self.kwargs)
|
||||
self._prompt_handler = PromptHandler(__file__, prompt_file="memory_chat_prompt", **self.kwargs)
|
||||
return self._prompt_handler
|
||||
|
||||
def print_logo(self):
|
||||
"""
|
||||
Prints the logo of the CLI application to the console.
|
||||
|
||||
The logo is composed of multiple lines, which are iterated through
|
||||
and printed one by one to provide a visual identity for the chat interface.
|
||||
"""
|
||||
for line in self._logo:
|
||||
print(line)
|
||||
|
||||
@property
|
||||
def memory_service(self) -> BaseMemoryService:
|
||||
"""
|
||||
Property to access the memory service. If the service is initially set as a string,
|
||||
it will be looked up in the memory service dictionary of global context, initialized,
|
||||
it will be looked up in the memory service dictionary of context, initialized,
|
||||
and then returned as an instance of `BaseMemoryService`. Ensures the memory service
|
||||
is properly started before use.
|
||||
|
||||
|
|
@ -82,13 +65,15 @@ class ApiMemoryChat(BaseMemoryChat):
|
|||
BaseMemoryService: An active memory service instance.
|
||||
|
||||
Raises:
|
||||
ValueError: If the declaration of memory service is not found in the memory service dictionary of global context.
|
||||
ValueError: If the declaration of memory service is not found in the memory service dictionary of context.
|
||||
"""
|
||||
if isinstance(self._memory_service, str):
|
||||
if self._memory_service not in G_CONTEXT.memory_service_conf_dict:
|
||||
raise ValueError("Missing declaration of memory_service in yaml configuration: " + self._memory_service)
|
||||
self._memory_service = G_CONTEXT.memory_service_conf_dict[self._memory_service]
|
||||
self._memory_service.init_service()
|
||||
if self._memory_service not in self.context.memory_service_dict:
|
||||
raise ValueError(f"Missing declaration of memory_service in context: {self._memory_service}")
|
||||
|
||||
self._memory_service: BaseMemoryService = self.context.memory_service_dict[self._memory_service]
|
||||
# init service & update kwargs
|
||||
self._memory_service.init_service(human_name=self.human_name, assistant_name=self.assistant_name)
|
||||
self._memory_service.start_backend_service()
|
||||
return self._memory_service
|
||||
|
||||
|
|
@ -99,18 +84,18 @@ class ApiMemoryChat(BaseMemoryChat):
|
|||
context's model dictionary.
|
||||
|
||||
Raises:
|
||||
ValueError: If the declaration of generation model is not found in the model dictionary of global context .
|
||||
ValueError: If the declaration of generation model is not found in the model dictionary of context .
|
||||
|
||||
Returns:
|
||||
BaseModel: An actual generation model instance.
|
||||
"""
|
||||
if isinstance(self._generation_model, str):
|
||||
if self._generation_model not in G_CONTEXT.model_conf_dict:
|
||||
if self._generation_model not in self.context.model_dict:
|
||||
raise ValueError(f"Missing declaration of generation model in yaml config: {self._generation_model}")
|
||||
self._generation_model = G_CONTEXT.model_conf_dict[self._generation_model]
|
||||
self._generation_model = self.context.model_dict[self._generation_model]
|
||||
return self._generation_model
|
||||
|
||||
def chat_with_memory(self, query: str, remember_response: bool = False) -> ModelResponse | ModelResponseGen:
|
||||
def chat_with_memory(self, query: str, role_name: str = "") -> ModelResponse | ModelResponseGen:
|
||||
"""
|
||||
Engages in a conversation with the AI model, utilizing conversation memory.
|
||||
The function sends the user's query, incorporates conversation history and memory,
|
||||
|
|
@ -118,8 +103,7 @@ class ApiMemoryChat(BaseMemoryChat):
|
|||
|
||||
Args:
|
||||
query (str): The user's input or query for the AI.
|
||||
remember_response (bool, optional): Flag indicating whether to save the AI's response to memory.
|
||||
Defaults to False.
|
||||
role_name (str, optional): The user's name, default value is human_name.
|
||||
|
||||
Returns:
|
||||
- ModelResponse: In non-streaming mode, returns a complete AI response.
|
||||
|
|
@ -129,8 +113,10 @@ class ApiMemoryChat(BaseMemoryChat):
|
|||
- Updates the conversation memory with the query of user and (optionally) the response of AI.
|
||||
- Retrieves and includes historical messages and memory content in the context of conversation.
|
||||
"""
|
||||
new_message: Message = Message(role=MessageRoleEnum.USER.value, role_name=self.human_name, content=query)
|
||||
self.memory_service.add_messages(new_message)
|
||||
if not role_name:
|
||||
role_name = self.human_name
|
||||
new_message: Message = Message(role=MessageRoleEnum.USER.value, role_name=role_name, content=query)
|
||||
self.add_messages(new_message)
|
||||
|
||||
messages: List[Message] = []
|
||||
|
||||
|
|
@ -151,172 +137,20 @@ class ApiMemoryChat(BaseMemoryChat):
|
|||
messages.append(new_message)
|
||||
self.logger.info(f"messages={messages}")
|
||||
|
||||
# Invoke the Language Model with the constructed message context, respecting streaming setting
|
||||
generated = self.generation_model.call(messages=messages, stream=self.stream, **self.generation_model_kwargs)
|
||||
result = self.generation_model.call(messages=messages, stream=self.stream, **self.generation_model_kwargs)
|
||||
|
||||
# In non-streaming interactions, explicitly save the AI's reply to memory if instructed
|
||||
if remember_response:
|
||||
assert not self.stream # Ensure we're not in streaming mode when remembering responses
|
||||
generated.message.role_name = self.assistant_name
|
||||
self.memory_service.add_messages(generated.message)
|
||||
if self.stream:
|
||||
assert isinstance(result, ModelResponseGen)
|
||||
model_response: ModelResponse | None = None
|
||||
for model_response in result:
|
||||
yield model_response
|
||||
|
||||
# Return the AI's response directly or as a generator based on the streaming mode
|
||||
return generated
|
||||
|
||||
@staticmethod
|
||||
def parse_query_command(query: str):
|
||||
"""
|
||||
Parses the user's input query command, separating it into the command and its associated keyword arguments.
|
||||
|
||||
Args:
|
||||
query (str): The raw input string from the user which includes the command and its arguments.
|
||||
|
||||
Returns:
|
||||
tuple: A tuple containing the command (str) as the first element and a dictionary (kwargs) of keyword
|
||||
arguments as the second element.
|
||||
"""
|
||||
query_split = query.lstrip("/").lower().split(" ") # Split and preprocess the input command
|
||||
command = query_split[0] # Extract the command
|
||||
args = query_split[1:] # Extract the arguments following the command
|
||||
kwargs = {} # Initialize dictionary to hold keyword arguments
|
||||
|
||||
for arg in args:
|
||||
# Skip if no arguments exist (unnecessary check due to prior assignment, but retained as per original)
|
||||
if not args:
|
||||
continue
|
||||
arg_split = arg.split("=") # Split argument into key-value pair
|
||||
if len(arg_split) >= 2: # Ensure there's both a key and value
|
||||
k = arg_split[0] # Extract key
|
||||
v = arg_split[1] # Extract value
|
||||
if k and v: # Only add to kwargs if both key and value are non-empty
|
||||
kwargs[k] = v
|
||||
|
||||
return command, kwargs # Return the parsed command and keyword arguments
|
||||
|
||||
def process_commands(self, query: str) -> bool:
|
||||
"""
|
||||
Parses and executes commands from user input in the CLI chat interface.
|
||||
Supports operations like exiting, clearing screen, showing help, toggling stream mode,
|
||||
executing predefined memory operations, and handling unknown commands.
|
||||
|
||||
Args:
|
||||
query (str): The user's input command string.
|
||||
|
||||
Returns:
|
||||
bool: Indicates whether to continue running the CLI after processing the command.
|
||||
"""
|
||||
continue_run = True
|
||||
command, kwargs = self.parse_query_command(query)
|
||||
|
||||
# Print prompt for AI's response
|
||||
questionary.print("> ", end="", style="fg:yellow")
|
||||
questionary.print(f"{self.assistant_name}: ", end="", style="bold")
|
||||
|
||||
if command == "exit":
|
||||
self.memory_service.stop_backend_service()
|
||||
continue_run = False
|
||||
|
||||
elif command == "clear":
|
||||
os.system("clear")
|
||||
|
||||
elif command == "help":
|
||||
questionary.print("CLI commands", "bold")
|
||||
for cmd, desc in self.USER_COMMANDS.items():
|
||||
questionary.print(text=f" /{cmd}:", style="bold")
|
||||
questionary.print(text=f" {desc}")
|
||||
|
||||
elif command == "stream":
|
||||
self.stream = not self.stream
|
||||
questionary.print(f"set stream: {self.stream}")
|
||||
|
||||
elif command in self.memory_service.op_description_dict:
|
||||
refresh_time = kwargs.pop("refresh_time", "")
|
||||
if refresh_time and refresh_time.isdigit():
|
||||
refresh_time = int(refresh_time)
|
||||
self.memory_service.stop_backend_service()
|
||||
while True:
|
||||
result = self.memory_service.do_operation(op_name=command, **kwargs)
|
||||
os.system("clear")
|
||||
self.print_logo()
|
||||
if result:
|
||||
if isinstance(result, list):
|
||||
result = "\n".join([str(x) for x in result])
|
||||
questionary.print(result)
|
||||
else:
|
||||
questionary.print(f"command={command} result is empty! kwargs={kwargs}")
|
||||
time.sleep(refresh_time)
|
||||
|
||||
else:
|
||||
result = self.memory_service.do_operation(op_name=command, **kwargs)
|
||||
if result:
|
||||
if isinstance(result, list):
|
||||
result = "\n".join([str(x) for x in result])
|
||||
questionary.print(result)
|
||||
else:
|
||||
questionary.print(f"command={command} result is empty! kwargs={kwargs}")
|
||||
if model_response and model_response.message:
|
||||
self.add_messages(model_response.message)
|
||||
|
||||
else:
|
||||
questionary.print(f"Unknown command={command} received.")
|
||||
|
||||
return continue_run
|
||||
|
||||
def run(self):
|
||||
"""
|
||||
Runs the CLI chat loop, which handles user input, processes commands,
|
||||
communicates with the AI model, manages conversation memory, and controls
|
||||
the chat session including streaming responses, command execution, and error handling.
|
||||
|
||||
The loop continues until the user explicitly chooses to exit.
|
||||
"""
|
||||
self.print_logo()
|
||||
self.USER_COMMANDS.update(self.memory_service.op_description_dict)
|
||||
|
||||
while True:
|
||||
try:
|
||||
query = questionary.text(message=f"{self.human_name}:", multiline=False, qmark=">").unsafe_ask()
|
||||
if not query:
|
||||
continue
|
||||
|
||||
query: str = query.strip()
|
||||
|
||||
# Handle special commands prefixed with '/'
|
||||
if query.startswith("/"):
|
||||
if self.process_commands(query=query):
|
||||
continue
|
||||
else:
|
||||
break
|
||||
|
||||
# Print prompt for AI's response
|
||||
questionary.print("> ", end="", style="fg:yellow")
|
||||
questionary.print(f"{self.assistant_name}: ", end="", style="bold")
|
||||
|
||||
# Fetch and display AI's response, with support for streaming
|
||||
self.memory_service.start_backend_service()
|
||||
if self.stream:
|
||||
model_response = None
|
||||
for model_response in self.chat_with_memory(query=query):
|
||||
questionary.print(model_response.delta, end="")
|
||||
questionary.print("")
|
||||
|
||||
else:
|
||||
model_response = self.chat_with_memory(query=query)
|
||||
questionary.print(model_response.message.content)
|
||||
|
||||
# Append AI's response to the conversation memory
|
||||
model_response.message.role_name = self.assistant_name
|
||||
self.memory_service.add_messages(model_response.message)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
# Handle user interruption and confirm exit
|
||||
questionary.print("User interrupt occurred.")
|
||||
is_exit = questionary.confirm("Continue exit?").unsafe_ask()
|
||||
if is_exit:
|
||||
self.memory_service.stop_backend_service()
|
||||
break
|
||||
|
||||
except Exception as e:
|
||||
# Log and handle any unanticipated exceptions
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
self.logger.exception(f"An exception occurred when running cli memory chat. args={e.args}.")
|
||||
continue
|
||||
assert isinstance(result, ModelResponse)
|
||||
model_response: ModelResponse = result
|
||||
if model_response and model_response.message:
|
||||
self.add_messages(model_response.message)
|
||||
return model_response
|
||||
|
|
|
|||
|
|
@ -12,8 +12,8 @@ class BaseMemoryChat(metaclass=ABCMeta):
|
|||
It outlines the method to initiate a chat session leveraging memory data, which concrete subclasses must implement.
|
||||
"""
|
||||
|
||||
def __init__(self, generation_stream: bool = True, **kwargs):
|
||||
self.generation_stream: bool = generation_stream
|
||||
def __init__(self, stream: bool = True, **kwargs):
|
||||
self.stream: bool = stream
|
||||
self.kwargs: dict = kwargs
|
||||
self.logger = Logger.get_logger()
|
||||
|
||||
|
|
@ -32,9 +32,6 @@ class BaseMemoryChat(metaclass=ABCMeta):
|
|||
subclass.
|
||||
"""
|
||||
|
||||
def add_message(self, messages: List[Message] | Message):
|
||||
self.memory_service.add_messages(messages)
|
||||
|
||||
@property
|
||||
def memory_service(self) -> BaseMemoryService:
|
||||
"""
|
||||
|
|
@ -45,6 +42,12 @@ class BaseMemoryChat(metaclass=ABCMeta):
|
|||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def add_messages(self, messages: List[Message] | Message):
|
||||
self.memory_service.add_messages(messages)
|
||||
|
||||
def do_memory_operation(self, op_name: str, **kwargs):
|
||||
return self.memory_service.do_operation(op_name=op_name, **kwargs)
|
||||
|
||||
def run(self):
|
||||
"""
|
||||
Abstract method to run the chat system.
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ class CliMemoryChat(BaseMemoryChat):
|
|||
"exit": "Exit the CLI.",
|
||||
"clear": "Clear the command history.",
|
||||
"help": "Display available CLI commands and their descriptions.",
|
||||
"stream": "Toggle between getting streamed responses from the model."
|
||||
}
|
||||
|
||||
def __init__(self,
|
||||
|
|
@ -121,10 +122,27 @@ class CliMemoryChat(BaseMemoryChat):
|
|||
return self._generation_model
|
||||
|
||||
def chat_with_memory(self, query: str, role_name: str = "") -> ModelResponse | ModelResponseGen:
|
||||
"""
|
||||
Engages in a conversation with the AI model, utilizing conversation memory.
|
||||
The function sends the user's query, incorporates conversation history and memory,
|
||||
and optionally remembers the AI's response based on the user's preference.
|
||||
|
||||
Args:
|
||||
query (str): The user's input or query for the AI.
|
||||
role_name (str, optional): The user's name, default value is human_name.
|
||||
|
||||
Returns:
|
||||
- ModelResponse: In non-streaming mode, returns a complete AI response.
|
||||
- ModelResponseGen: In streaming mode, returns a generator yielding AI response parts.
|
||||
|
||||
Side Effects:
|
||||
- Updates the conversation memory with the query of user and (optionally) the response of AI.
|
||||
- Retrieves and includes historical messages and memory content in the context of conversation.
|
||||
"""
|
||||
if not role_name:
|
||||
role_name = self.human_name
|
||||
new_message: Message = Message(role=MessageRoleEnum.USER.value, role_name=role_name, content=query)
|
||||
self.memory_service.add_messages(new_message)
|
||||
self.add_messages(new_message)
|
||||
|
||||
messages: List[Message] = []
|
||||
|
||||
|
|
@ -147,7 +165,7 @@ class CliMemoryChat(BaseMemoryChat):
|
|||
|
||||
# Invoke the Language Model with the constructed message context, respecting streaming setting
|
||||
return self.generation_model.call(messages=messages,
|
||||
stream=self.generation_stream,
|
||||
stream=self.stream,
|
||||
**self.generation_model_kwargs)
|
||||
|
||||
@staticmethod
|
||||
|
|
@ -212,6 +230,10 @@ class CliMemoryChat(BaseMemoryChat):
|
|||
questionary.print(text=f" /{cmd}:", style="bold")
|
||||
questionary.print(text=f" {desc}")
|
||||
|
||||
elif command == "stream":
|
||||
self.stream = not self.stream
|
||||
questionary.print(f"set stream: {self.stream}")
|
||||
|
||||
elif command in self.memory_service.op_description_dict:
|
||||
refresh_time = kwargs.pop("refresh_time", "")
|
||||
if refresh_time and refresh_time.isdigit():
|
||||
|
|
@ -275,7 +297,7 @@ class CliMemoryChat(BaseMemoryChat):
|
|||
|
||||
# Fetch and display AI's response
|
||||
self.memory_service.start_backend_service()
|
||||
if self.generation_stream:
|
||||
if self.stream:
|
||||
model_response = None
|
||||
for model_response in self.chat_with_memory(query=query):
|
||||
questionary.print(model_response.delta, end="")
|
||||
|
|
@ -286,7 +308,7 @@ class CliMemoryChat(BaseMemoryChat):
|
|||
|
||||
# Append AI's response to the conversation memory
|
||||
model_response.message.role_name = self.assistant_name
|
||||
self.memory_service.add_messages(model_response.message)
|
||||
self.add_messages(model_response.message)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
# Handle user interruption and confirm exit
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import time
|
||||
from typing import List
|
||||
|
||||
|
||||
from memoryscope.constants.common_constants import CHAT_KWARGS, RESULT, CHAT_MESSAGES
|
||||
from memoryscope.memory.operation.base_operation import BaseOperation, OPERATION_TYPE
|
||||
from memoryscope.memory.operation.base_workflow import BaseWorkflow
|
||||
from memoryscope.scheme.message import Message
|
||||
from memoryscope.utils.global_context import G_CONTEXT
|
||||
from memoryscope.utils.logger import Logger
|
||||
|
||||
|
||||
|
|
@ -30,7 +30,7 @@ class BackendOperation(BaseWorkflow, BaseOperation):
|
|||
|
||||
self._operation_status_run: bool = False
|
||||
self._loop_switch: bool = False
|
||||
self._run_thread = None
|
||||
self._backend_task = None
|
||||
|
||||
self.logger = Logger.get_logger()
|
||||
|
||||
|
|
@ -114,10 +114,12 @@ class BackendOperation(BaseWorkflow, BaseOperation):
|
|||
"""
|
||||
if not self._loop_switch:
|
||||
self._loop_switch = True
|
||||
self._run_thread = G_CONTEXT.thread_pool.submit(self._loop_operation)
|
||||
self._backend_task = G_CONTEXT.thread_pool.submit(self._loop_operation)
|
||||
|
||||
def stop_operation_backend(self):
|
||||
def stop_operation_backend(self, wait_task_end: bool = False):
|
||||
"""
|
||||
Stops the background operation loop by setting the _loop_switch to False.
|
||||
"""
|
||||
self._loop_switch = False
|
||||
if wait_task_end and self._backend_task:
|
||||
self._backend_task.result()
|
||||
|
|
|
|||
|
|
@ -25,15 +25,11 @@ class BaseMemoryService(metaclass=ABCMeta):
|
|||
"""
|
||||
self.memory_operations_conf: Dict[str, dict] = memory_operations
|
||||
self.context: MemoryscopeContext = context
|
||||
self.kwargs = kwargs
|
||||
|
||||
self._operation_dict: Dict[str, BaseOperation] = {}
|
||||
self._op_description_dict: Dict[str, str] = {}
|
||||
|
||||
self.logger = Logger.get_logger()
|
||||
self.kwargs = kwargs
|
||||
|
||||
def update_kwargs(self, **kwargs):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def add_messages(self, messages: List[Message] | Message):
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue