fix empty content insight bug && modify print memory

This commit is contained in:
jinli.yl 2024-07-22 10:46:26 +08:00
parent 666e78879c
commit 7b09f0bd2b
20 changed files with 217 additions and 170 deletions

View file

@ -5,13 +5,12 @@ global_config:
memory_chat:
cli_memory_chat:
class: chat.cli_memory_chat
memory_service: memory_chat_service
memory_service: chat_memory_service
generation_model: dashscope_generation
memory_service:
memory_chat_service:
chat_memory_service:
class: memory.service.chat_memory_service
contextual_msg_count: 6
memory_operations:
read_message:
class: memory.operation.frontend_operation
@ -47,13 +46,13 @@ memory_service:
class: memory.operation.write_memory_op
workflow: info_filter,[get_observation|get_observation_with_time|load_today_memory],contra_repeat,store_memory
description: "write observation memory of the user"
interval_time: 5
interval_time: 1
summary_memory:
class: memory.operation.backend_operation
workflow: load_obs_and_insight,get_reflection_subject,update_insight,long_contra_repeat,store_memory
description: "summary observation memory of the user"
interval_time: 30
interval_time: 15
worker:
dummy:

View file

@ -1,8 +1,8 @@
system_prompt:
cn: |
你是一个叫MemoryScope的AI小助手善于倾听用户的问题和心声并给出用户建议,回答时使用中文,不要太冗长。
你是一个叫MemoryScope的AI小助手善于倾听用户的问题和心声回答时使用中文,不要太冗长。
en: |
You are an AI assistant named MemoryScope, good at listening to users' questions and feelings, and providing them with advice. Respond in English without being too lengthy.
You are an AI assistant named MemoryScope, good at listening to users' questions and feelings. Respond in English without being too lengthy.
memory_prompt:
cn: |

View file

@ -1,18 +1,15 @@
from memory_scope.constants.common_constants import CHAT_MESSAGES, RESULT, CHAT_KWARGS
from memory_scope.constants.common_constants import CHAT_KWARGS, CHAT_MESSAGES, RESULT
from memory_scope.enumeration.message_role_enum import MessageRoleEnum
from memory_scope.memory.operation.backend_operation import BackendOperation
class WriteMemoryOp(BackendOperation):
def __init__(self,
message_lock=None,
contextual_msg_count: int = 6,
**kwargs):
def __init__(self, **kwargs):
super(WriteMemoryOp, self).__init__(**kwargs)
self.message_lock = message_lock
self.contextual_msg_count: int = contextual_msg_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):
"""
@ -41,15 +38,15 @@ class WriteMemoryOp(BackendOperation):
chat_messages = chat_messages[:-1]
not_memorized_size = sum([not x.memorized for x in chat_messages])
if not_memorized_size < self.contextual_msg_count:
if not_memorized_size < self.contextual_msg_min_count:
self.logger.info(f"not_memorized_size({not_memorized_size}) < "
f"contextual_msg_count({self.contextual_msg_count}), skip.")
f"contextual_msg_min_count({self.contextual_msg_min_count}), skip.")
return
self.context.clear()
# Add additional arguments to the context
kwargs.update({"contextual_msg_count": self.contextual_msg_count, **self.kwargs})
kwargs.update(**self.kwargs)
self.context[CHAT_KWARGS] = kwargs
# Include the most recent messages in the operation context
@ -62,9 +59,8 @@ class WriteMemoryOp(BackendOperation):
result = self.context.get(RESULT)
# set message memorized
if self.message_lock:
with self.message_lock:
for message in chat_messages:
message.memorized = True
with self.message_lock:
for message in chat_messages:
message.memorized = True
return result

View file

@ -7,11 +7,26 @@ from memory_scope.utils.tool_functions import init_instance_by_config
class ChatMemoryService(BaseMemoryService):
def __init__(self, history_msg_count: int = 100, contextual_msg_count: int = 6, **kwargs):
def __init__(self,
history_msg_count: int = 100,
contextual_msg_max_count: int = 20,
contextual_msg_min_count: int = 0,
**kwargs):
"""
init function.
Args:
history_msg_count (int): The conversation history in memory, control the quantity, and reduce memory usage.
contextual_msg_max_count (int): The maximum context length in a conversation. If it exceeds this length,
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.
kwargs (dict): other kwargs
"""
super().__init__(**kwargs)
self.history_msg_count: int = history_msg_count
self.contextual_msg_count: int = contextual_msg_count
assert self.history_msg_count >= self.contextual_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
def add_messages(self, messages: List[Message] | Message):
"""
@ -26,17 +41,18 @@ class ChatMemoryService(BaseMemoryService):
if isinstance(messages, Message):
messages = [messages]
# Sort the messages by their creation time to maintain chronological order
messages = sorted(messages, key=lambda x: x.time_created)
with self.message_lock:
# Append the sorted messages to the chat history
self.chat_messages.extend(messages)
# Append the sorted messages to the chat history
self.chat_messages.extend(messages)
# Sort the messages by their creation time to maintain chronological order
self.chat_messages.sort(key=lambda x: x.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
for _ in range(gap_size):
self.chat_messages.pop(0)
# 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
for _ in range(gap_size):
self.chat_messages.pop(0)
def do_operation(self, op_name: str, **kwargs):
"""
@ -69,13 +85,17 @@ class ChatMemoryService(BaseMemoryService):
name=name,
chat_messages=self.chat_messages,
message_lock=self.message_lock,
contextual_msg_count=self.contextual_msg_count)
contextual_msg_max_count=self.contextual_msg_max_count,
contextual_msg_min_count=self.contextual_msg_min_count)
operation.init_workflow(**kwargs) # Initialize workflow for each operation
self._operation_dict[name] = operation
self.logger.info(f"service={self.__class__.__name__} init operation={name}")
def start_backend_service(self):
"""
Start all backend operations.
"""
for _, operation in self._operation_dict.items():
if operation.operation_type == "backend":
# Run backend operations

View file

@ -7,60 +7,51 @@ from memory_scope.memory.worker.memory_base_worker import MemoryBaseWorker
from memory_scope.scheme.memory_node import MemoryNode
from memory_scope.utils.datetime_handler import DatetimeHandler
PRINT_TEMPLATE = """
The memories of {user_name} about {target_name}.
{obs_content}
{insight_content}
{expired_content}
"""
class PrintMemoryWorker(MemoryBaseWorker):
FILE_PATH: str = __file__
def _run(self):
# get long-term memory
memory_node_list: List[MemoryNode] = self.memory_handler.get_memories(RETRIEVE_MEMORY_NODES)
memory_node_list = sorted(memory_node_list, key=lambda x: x.timestamp, reverse=True)
expired_content_list: List[str] = ["----- expired -----"]
obs_content_list: List[str] = ["----- observation -----"]
insight_content_list: List[str] = ["----- insight -----"]
observation_memory_list: List[str] = []
insight_memory_list: List[str] = []
expired_memory_list: List[str] = []
i = 0
j = 0
k = 0
# remove duplicate content
expired_content_set = set()
for node in memory_node_list:
if not node.content:
continue
dt_handler = DatetimeHandler(node.timestamp)
dt = dt_handler.datetime_format("%Y%m%d-%H:%M:%S")
line = f"{dt} {node.content}"
dt = dt_handler.datetime_format("%Y%m%d %H:%M:%S")
if StoreStatusEnum(node.store_status) is StoreStatusEnum.EXPIRED:
if node.content in expired_content_set:
continue
else:
expired_content_set.add(node.content)
i += 1
expired_content_list.append(f" {i} {line}")
expired_memory_list.append(f"{dt}] {i}. {node.content}")
elif MemoryTypeEnum(node.memory_type) in [MemoryTypeEnum.OBSERVATION, MemoryTypeEnum.OBS_CUSTOMIZED]:
j += 1
obs_content_list.append(f" {j} {line} status={node.obs_reflected}")
observation_memory_list.append(f"{dt}] {j}. {node.content} "
f"status({node.obs_reflected},{node.obs_updated})")
elif MemoryTypeEnum(node.memory_type) is MemoryTypeEnum.INSIGHT:
k += 1
insight_content_list.append(f" {k} {line}")
insight_memory_list.append(f"{dt}] {k}. {node.content}")
obs_content = "\n".join(obs_content_list)
insight_content = "\n".join(insight_content_list)
expired_content = "\n".join(expired_content_list)
result: str = PRINT_TEMPLATE.format(
result: str = self.prompt_handler.print_template.format(
user_name=self.user_name,
target_name=self.target_name,
obs_content=obs_content,
insight_content=insight_content,
expired_content=expired_content,
).strip()
observation_memory="\n".join(observation_memory_list),
insight_memory="\n".join(insight_memory_list),
expired_memory="\n".join(expired_memory_list)).strip()
self.set_context(RESULT, result)

View file

@ -0,0 +1,22 @@
print_template:
cn: |
========== {user_name}关于{target_name}的长期记忆 ==========
----- 观察记忆 -----
{observation_memory}
----- 洞察记忆 -----
{insight_memory}
----- 过期记忆 -----
{expired_memory}
en: |
========== The {user_name}'s long-term memory about {target_name} ==========
----- observation memory -----
{observation_memory}
----- insight memory -----
{insight_memory}
----- expired memory -----
{expired_memory}

View file

@ -6,10 +6,11 @@ from memory_scope.memory.worker.memory_base_worker import MemoryBaseWorker
class ReadMessageWorker(MemoryBaseWorker):
def _run(self):
contextual_msg_count: int = self.chat_kwargs["contextual_msg_count"]
chat_messages = self.chat_messages.copy()
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]
self.set_context(RESULT, chat_messages[-contextual_msg_count:])
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)

View file

@ -35,7 +35,7 @@ class SemanticRankWorker(MemoryBaseWorker):
return
# drop repeated
memory_node_dict: Dict[str, MemoryNode] = {n.content: n for n in memory_node_list}
memory_node_dict: Dict[str, MemoryNode] = {n.content.strip(): n for n in memory_node_list if n.content.strip()}
memory_node_list = list(memory_node_dict.values())
response = self.rank_model.call(query=query, documents=[n.content for n in memory_node_list])

View file

@ -64,8 +64,9 @@ class GetReflectionSubjectWorker(MemoryBaseWorker):
# Count unaudited nodes
not_reflected_count = len(not_reflected_nodes)
if not_reflected_count < self.reflect_obs_cnt_threshold:
self.logger.info(f"not_reflected_count({not_reflected_count}) < is not enough, skip.")
self.continue_run = False
self.logger.info(f"not_reflected_count({not_reflected_count}) < threshold({self.reflect_obs_cnt_threshold})"
f" is not enough, skip.")
# self.continue_run = False
return
# Compile existing insight keys

View file

@ -85,6 +85,10 @@ class LongContraRepeatWorker(MemoryBaseWorker):
self.logger.warning("all_obs_nodes is empty, stop.")
return
if len(all_obs_nodes) == 1:
self.logger.info("all_obs_nodes.size=1, stop.")
return
# gene prompt
user_query_list = []
for i, n in enumerate(all_obs_nodes):

View file

@ -23,7 +23,7 @@ class UpdateInsightWorker(MemoryBaseWorker):
def _parse_params(self, **kwargs):
self.update_insight_threshold: float = kwargs.get("update_insight_threshold", 0.1)
self.generation_model_kwargs: dict = kwargs.get("generation_model_kwargs", {})
self.update_insight_max_count: int = kwargs.get("update_insight_max_count", 10)
self.update_insight_max_count: int = kwargs.get("update_insight_max_count", 5)
def filter_obs_nodes(self,
insight_node: MemoryNode,
@ -165,7 +165,8 @@ class UpdateInsightWorker(MemoryBaseWorker):
"""
insight_nodes: List[MemoryNode] = self.memory_handler.get_memories(INSIGHT_NODES)
not_updated_nodes: List[MemoryNode] = self.memory_handler.get_memories(NOT_UPDATED_NODES)
not_reflected_nodes: List[MemoryNode] = self.memory_handler.get_memories(NOT_REFLECTED_NODES)
not_reflected_nodes: List[MemoryNode] = self.memory_handler.get_memories(keys=[NOT_REFLECTED_NODES,
NOT_UPDATED_NODES])
if not insight_nodes:
self.logger.warning("insight_nodes is empty, stopping processing.")
@ -201,10 +202,14 @@ class UpdateInsightWorker(MemoryBaseWorker):
for _ in self.gather_thread_result():
pass
# delete empty nodes
empty_nodes = [n for n in insight_nodes if not n.content.strip()]
self.memory_handler.delete_memories(empty_nodes)
for node in not_updated_nodes:
node.obs_updated = 1
node.action_status = ActionStatusEnum.MODIFIED
for node in not_reflected_nodes:
node.obs_updated = 1
node.action_status = ActionStatusEnum.MODIFIED
# for node in not_reflected_nodes:
# node.obs_updated = 1
# node.action_status = ActionStatusEnum.MODIFIED

View file

@ -46,7 +46,7 @@ class ContraRepeatWorker(MemoryBaseWorker):
all_obs_nodes: List[MemoryNode] = self.memory_handler.get_memories([NEW_OBS_NODES, NEW_OBS_WITH_TIME_NODES])
if not all_obs_nodes:
self.logger.info("all_obs_nodes is empty!")
self.continue_run = False
# self.continue_run = False
return
today_obs_nodes: List[MemoryNode] = self.memory_handler.get_memories(TODAY_NODES)
@ -55,6 +55,10 @@ class ContraRepeatWorker(MemoryBaseWorker):
all_obs_nodes.extend(today_obs_nodes)
all_obs_nodes = sorted(all_obs_nodes, key=lambda x: x.timestamp, reverse=True)[:self.contra_repeat_max_count]
if len(all_obs_nodes) == 1:
self.logger.info("all_obs_nodes.size=1, stop.")
return
# build prompt
user_query_list = []
for i, n in enumerate(all_obs_nodes):

View file

@ -59,7 +59,6 @@ class InfoFilterWorker(MemoryBaseWorker):
user_query_list = []
for i, msg in enumerate(info_messages):
user_query_list.append(f"{i + 1} {self.target_name}{self.get_language_value(COLON_WORD)} {msg.content}")
self.logger.warning(self.prompt_handler.prompt_dict)
system_prompt = self.prompt_handler.info_filter_system.format(batch_size=len(info_messages),
user_name=self.target_name)
few_shot = self.prompt_handler.info_filter_few_shot.format(user_name=self.target_name)

View file

@ -32,6 +32,7 @@ info_filter_few_shot:
4 {user_name}:我今天心情不好,可以安慰我一下吗?
5 {user_name}:能给我整理一张如何使用大模型的技巧列表吗,要求内容尽量精简。
6 {user_name}记一下明天下午3点提醒我去拿一下文件。
思考从第1句可以确定推断出张三是{user_name}同事这一重要信息。
结果:<1> <3>
思考第2句不包含{user_name}信息。
@ -55,6 +56,7 @@ info_filter_few_shot:
5 {user_name}:假如我要和一个女人准备要孩子,我作为男人,怎么保护女人和孩子以及怎么备孕确保精子质量高对后代好
6 {user_name}:我和你一起出去玩,你会感觉开心吗?
7 {user_name}林浅一位对未来充满好奇的年轻女孩偶然间发现了这家能寄信给未来的邮局。出于对逝去祖父的怀念她决定写下一封信寄给五年后的自己希望能收到祖父生前未说完的故事。五年期限将至当她几乎忘记这段往事时一封泛黄的回信悄然降临不仅带来了祖父未完的冒险故事还藏着一段关于勇气、爱与自我发现的深刻启示。续写成3000字小说。
思考从第1句可以确定得出{user_name}工作单位是阿里巴巴这一重要信息。
结果:<1> <3>
思考从第2句可以猜测{user_name}近期露天睡觉,是不确定的信息。
@ -78,6 +80,8 @@ info_filter_few_shot:
3 {user_name}:我很喜欢打篮球,所以我身体很好
4 {user_name}:篮球明星有哪些?
5 {user_name}:李增杰:这个是星座蛙设,但是我是处女座的,我妈感觉因为我的不正常,我妈不让我看了\n雌猴摸了摸李增杰的头这样啊\n雌猴打开了哔哩哔哩看了看\n雌猴:要不换个设吧我听你未来的你说有一个叫难忘的朱古力232这个人他弄的设是Windows设\n这是剧本1剧本2未完待续
6 {user_name}:我想知道昨天我们聊了什么?
思考第1句是{user_name}假设的信息。
结果:<1> <1>
思考第2句信息不明可能是{user_name}假设的信息。
@ -88,6 +92,8 @@ info_filter_few_shot:
结果:<4> <0>
思考第5句是{user_name}虚构的内容。
结果:<5> <1>
思考第6句是{user_name}的疑问句,没有包含信息。
结果:<6> <0>
en: |
Example 1
@ -98,6 +104,7 @@ info_filter_few_shot:
4 {user_name}: I'm feeling down today. Can you comfort me a bit?
5 {user_name}: Can you compile a list of tips on how to use large models for me, and try to keep the content concise?
6 {user_name}: Note this down: remind me tomorrow at 3 PM to pick up the documents.
Thought: From the first sentence, it can be inferred that Zhang San is a colleague of {user_name}, which is important information.
Result: <1> <3>
Thought: The second sentence does not contain information about {user_name}.
@ -120,6 +127,7 @@ info_filter_few_shot:
5 {user_name}: If I am planning to have a child with a woman, as a man, how can I protect the woman and the baby and how can I prepare to ensure high sperm quality for the benefit of the offspring?
6 {user_name}: If we go out to play together, would you feel happy?
7 {user_name}: Rose, a young girl full of curiosity about the future, accidentally discovered this post office that can send letters to the future. Out of nostalgia for her late grandfather, she decided to write a letter to herself five years in the future, hoping to receive the unfinished stories of her grandfather. As the five-year deadline approached, when she had almost forgotten about this event, a yellowed reply quietly arrived, bringing not only her grandfather's unfinished adventure story but also a profound revelation about courage, love, and self-discovery. Continue writing this into a 3000-word novel.
Thought: From the first sentence, it can be determined that {user_name} works at Alibaba, which is important information.
Result: <1> <3>
Thought: The second sentence suggests that {user_name} might has been sleeping outdoors recently, which is uncertain information.
@ -142,6 +150,8 @@ info_filter_few_shot:
3 {user_name}: I really enjoy playing basketball, so I am in good health.
4 {user_name}: Who are some famous basketball stars?
5 {user_name}: Zack: This is a constellation frog setting, but I am a Virgo. My mom feels I'm abnormal and doesn't let me watch it. \n The female monkey patted Zack's head. "Is that so?" \n The female monkey opened Bilibili and took a look. \n Female monkey: "Why don't you switch the setting? I heard from your future self that there is someone called 'Unforgettable Chocolate 232' who created a Windows setting." \n This is script 1; script 2 is to be continued.
6 {user_name}: I want to know what we talked about yesterday.
Thought: The first sentence contains only hypothetical information from {user_name}.
Result: <1> <1>
Thought: The second sentence is unclear and may contain hypothetical information from {user_name}.
@ -152,6 +162,8 @@ info_filter_few_shot:
Result: <4> <0>
Thought: The fifth sentence contains only fictitious content from {user_name}.
Result: <5> <1>
Thought: Sentence 6 is a question from {user_name} that doesn't include any specific information.
Result: <6> <0>
info_filter_user_query:
cn: |

View file

@ -34,9 +34,7 @@ class BaseModel(metaclass=ABCMeta):
self.raise_exception: bool = raise_exception
self.kwargs: dict = kwargs
self.data = {}
self._model: Any = None
self.logger = Logger.get_logger()
@property
@ -56,44 +54,31 @@ class BaseModel(metaclass=ABCMeta):
return self._model
@abstractmethod
def before_call(self, **kwargs) -> None:
"""prepare data before call
:param kwargs:
:return:
"""
def before_call(self, model_response: ModelResponse, **kwargs):
pass
@abstractmethod
def after_call(self, model_response: ModelResponse | ModelResponseGen,
**kwargs) -> ModelResponse | ModelResponseGen:
"""
:param model_response:
:param kwargs:
:return:
"""
def after_call(self, model_response: ModelResponse, **kwargs) -> ModelResponse | ModelResponseGen:
pass
@abstractmethod
def _call(self, stream: bool = False, **kwargs) -> ModelResponse | ModelResponseGen:
"""
:param kwargs:
:return:
"""
def _call(self, model_response: ModelResponse, stream: bool = False, **kwargs):
pass
def call(self, stream: bool = False, **kwargs) -> ModelResponse | ModelResponseGen:
"""
:param stream: only llm needs stream
:param kwargs:
:return:
"""
with Timer(self.__class__.__name__, time_log_type="none") as t:
self.before_call(stream=stream, **kwargs)
model_response = ModelResponse(m_type=self.m_type)
self.before_call(stream=stream, model_response=model_response, **kwargs)
for i in range(self.max_retries):
if self.raise_exception:
model_response = self._call(stream=stream, **kwargs)
self._call(stream=stream, model_response=model_response, **kwargs)
else:
try:
model_response = self._call(stream=stream, **kwargs)
self._call(stream=stream, model_response=model_response, **kwargs)
except Exception as e:
model_response = ModelResponse(m_type=self.m_type, status=False, details=e.args)
model_response.status = False
model_response.details = e.args
if isinstance(model_response, ModelResponse) and not model_response.status:
self.logger.warning(f"call model={self.model_name} failed! {t.cost_str} retry_cnt={i} "
@ -103,27 +88,23 @@ class BaseModel(metaclass=ABCMeta):
return self.after_call(stream=stream, model_response=model_response, **kwargs)
@abstractmethod
async def _async_call(self, **kwargs) -> ModelResponse:
"""
:param kwargs:
:return:
"""
async def _async_call(self, model_response: ModelResponse, **kwargs) -> ModelResponse:
pass
async def async_call(self, **kwargs) -> ModelResponse:
""" 异步不需要stream
:param kwargs:
:return:
"""
with Timer(self.__class__.__name__, time_log_type="none") as t:
self.before_call(**kwargs)
model_response = ModelResponse(m_type=self.m_type)
self.before_call(model_response=model_response, **kwargs)
for i in range(self.max_retries):
if self.raise_exception:
model_response = self._async_call(**kwargs)
await self._async_call(model_response=model_response, **kwargs)
else:
try:
model_response = self._async_call(**kwargs)
await self._async_call(model_response=model_response, **kwargs)
except Exception as e:
model_response = ModelResponse(m_type=self.m_type, status=False, details=e.args)
model_response.status = False
model_response.details = e.args
if not model_response.status:
self.logger.warning(f"async_call model={self.model_name} failed! {t.cost_str} retry_cnt={i} "

View file

@ -27,11 +27,11 @@ class LlamaIndexEmbeddingModel(BaseModel):
MODEL_REGISTRY.register("dashscope_embedding", DashScopeEmbedding)
def before_call(self, **kwargs):
def before_call(self, model_response: ModelResponse, **kwargs):
text: str | List[str] = kwargs.pop("text", "")
if isinstance(text, str):
text = [text]
self.data = dict(texts=text)
model_response.meta_data["data"] = dict(texts=text)
def after_call(self, model_response: ModelResponse, **kwargs) -> ModelResponse:
embeddings = model_response.raw
@ -47,7 +47,7 @@ class LlamaIndexEmbeddingModel(BaseModel):
model_response.embedding_results = embeddings
return model_response
def _call(self, **kwargs) -> ModelResponse:
def _call(self, model_response: ModelResponse, **kwargs):
"""
Executes a synchronous call to generate embeddings for the input data.
@ -61,9 +61,9 @@ class LlamaIndexEmbeddingModel(BaseModel):
Returns:
ModelResponse: An object containing the embedding results and the model type.
"""
return ModelResponse(m_type=self.m_type, raw=self.model.get_text_embedding_batch(**self.data))
model_response.raw = self.model.get_text_embedding_batch(**model_response.meta_data["data"])
async def _async_call(self, **kwargs) -> ModelResponse:
async def _async_call(self, model_response: ModelResponse, **kwargs):
"""
Executes an asynchronous call to generate embeddings for the input data.
@ -77,4 +77,4 @@ class LlamaIndexEmbeddingModel(BaseModel):
Returns:
ModelResponse: An object encapsulating the embedding output and the model's type.
"""
return ModelResponse(m_type=self.m_type, raw=await self.model.aget_text_embedding_batch(**self.data))
model_response.raw = await self.model.aget_text_embedding_batch(**model_response.meta_data["data"])

View file

@ -23,7 +23,7 @@ class LlamaIndexGenerationModel(BaseModel):
MODEL_REGISTRY.register("dashscope_generation", DashScope)
def before_call(self, **kwargs):
def before_call(self, model_response: ModelResponse, **kwargs):
"""
Prepares the input data before making a call to the language model.
It accepts either a 'prompt' directly or a list of 'messages'.
@ -32,6 +32,7 @@ class LlamaIndexGenerationModel(BaseModel):
Raises an error if neither 'prompt' nor 'messages' are supplied.
Args:
model_response: model_response
**kwargs: Arbitrary keyword arguments including 'prompt' and 'messages'.
Raises:
@ -41,15 +42,16 @@ class LlamaIndexGenerationModel(BaseModel):
messages: List[Message] | List[dict] = kwargs.pop("messages", [])
if prompt:
self.data = {"prompt": prompt}
data = {"prompt": prompt}
elif messages:
if isinstance(messages[0], dict):
self.data = {"messages": [ChatMessage(role=msg["role"], content=msg["content"]) for msg in messages]}
data = {"messages": [ChatMessage(role=msg["role"], content=msg["content"]) for msg in messages]}
else:
self.data = {"messages": [ChatMessage(role=msg.role, content=msg.content) for msg in messages]}
data = {"messages": [ChatMessage(role=msg.role, content=msg.content) for msg in messages]}
else:
raise RuntimeError("prompt and messages are both empty!")
self.data.update(**kwargs)
data.update(**kwargs)
model_response.meta_data["data"] = data
def after_call(self,
model_response: ModelResponse,
@ -76,24 +78,23 @@ class LlamaIndexGenerationModel(BaseModel):
return model_response
def _call(self, stream: bool = False, **kwargs) -> ModelResponse | ModelResponseGen:
assert "prompt" in self.data or "messages" in self.data
results = ModelResponse(m_type=self.m_type)
def _call(self, model_response: ModelResponse, stream: bool = False, **kwargs):
data = model_response.meta_data["data"]
if "prompt" in self.data:
if "prompt" in data:
if stream:
response = self.model.stream_complete(**self.data)
model_response.raw = self.model.stream_complete(**data)
else:
response = self.model.complete(**self.data)
model_response.raw = self.model.complete(**data)
elif "messages" in data:
if stream:
model_response.raw = self.model.stream_chat(**data)
else:
model_response.raw = self.model.chat(**data)
else:
if stream:
response = self.model.stream_chat(**self.data)
else:
response = self.model.chat(**self.data)
results.raw = response
return results
raise RuntimeError("prompt or messages is missing!")
async def _async_call(self, **kwargs) -> ModelResponse:
async def _async_call(self, model_response: ModelResponse, **kwargs):
"""
Asynchronously calls the language model with the provided prompt or message history,
and packages the raw response into a ModelResponse object.
@ -108,13 +109,11 @@ class LlamaIndexGenerationModel(BaseModel):
Returns:
ModelResponse: An object containing the raw response from the language model.
"""
assert "prompt" in self.data or "messages" in self.data
results = ModelResponse(m_type=self.m_type)
data = model_response.meta_data["data"]
if "prompt" in self.data:
response = await self.model.acomplete(**self.data)
if "prompt" in data:
model_response.raw = await self.model.acomplete(**data)
elif "messages" in data:
model_response.raw = await self.model.achat(**data)
else:
response = await self.model.achat(**self.data)
results.raw = response
return results
raise RuntimeError("prompt or messages is missing!")

View file

@ -20,26 +20,29 @@ class LlamaIndexRankModel(BaseModel):
MODEL_REGISTRY.register("dashscope_rank", DashScopeRerank)
def before_call(self, **kwargs) -> None:
def before_call(self, model_response: ModelResponse, **kwargs):
"""
Prepares necessary data before the ranking call by extracting the query and documents,
ensuring they are valid, and initializing nodes with dummy scores.
Args:
model_response: model response
**kwargs: Keyword arguments containing 'query' and 'documents'.
"""
query: str = kwargs.pop("query", "")
documents: List[str] = kwargs.pop("documents", [])
if isinstance(documents, str):
documents = [documents]
assert query and documents, f"query or documents is empty! query={query}, documents={len(documents)}"
assert query and documents and all(documents), \
f"query or documents is empty! query={query}, documents={len(documents)}"
# Using -1.0 as dummy scores
nodes = [NodeWithScore(node=Node(text=doc), score=-1.0) for doc in documents]
self._get_documents_mapping(documents)
self.data = {"nodes": nodes, "query_str": query}
model_response.meta_data.update({
"data": {"nodes": nodes, "query_str": query},
"documents_map": {doc: idx for idx, doc in enumerate(documents)},
})
def after_call(self, model_response: ModelResponse, **kwargs) -> ModelResponse:
"""
@ -56,13 +59,14 @@ class LlamaIndexRankModel(BaseModel):
if not model_response.rank_scores:
model_response.rank_scores = {}
documents_map = model_response.meta_data["documents_map"]
for node in model_response.raw:
text = node.node.text
idx = self.documents_map[text]
idx = documents_map[text]
model_response.rank_scores[idx] = node.score
return model_response
def _call(self, **kwargs) -> ModelResponse:
def _call(self, model_response: ModelResponse, **kwargs):
"""
Executes the ranking process by passing prepared data to the model's postprocessing method.
@ -72,7 +76,7 @@ class LlamaIndexRankModel(BaseModel):
Returns:
ModelResponse: A response object encapsulating the ranked nodes.
"""
return ModelResponse(m_type=self.m_type, raw=self.model.postprocess_nodes(**self.data))
model_response.raw = self.model.postprocess_nodes(**model_response.meta_data["data"])
async def _async_call(self, **kwargs) -> ModelResponse:
"""
@ -84,15 +88,4 @@ class LlamaIndexRankModel(BaseModel):
Returns:
ModelResponse: A response object encapsulating the ranked nodes.
"""
return self._call(**kwargs)
def _get_documents_mapping(self, documents):
"""
Generates a mapping of each document to its index within the provided document list.
Args:
documents (List[str]): The list of documents.
"""
self.documents_map = {}
for idx, doc in enumerate(documents):
self.documents_map[doc] = idx
raise NotImplementedError

View file

@ -26,6 +26,8 @@ class ModelResponse(BaseModel):
raw: Any = Field("", description="Raw response from model call")
meta_data: Dict[str, Any] = Field({}, description="meta data for model response")
def __str__(self, max_size=100, **kwargs):
result = {}
for key, value in self.model_dump().items():

View file

@ -88,10 +88,28 @@ class MemoryHandler(object):
memory_ids: List[str] = self._key_id_dict.get(key.strip())
if memory_ids:
memories.update({x: self._id_memory_dict[x] for x in memory_ids})
memories.update({x: self._id_memory_dict[x] for x in memory_ids if x in self._id_memory_dict})
return list(memories.values())
def delete_memories(self, nodes: MemoryNode | List[MemoryNode], key: str = None):
if isinstance(nodes, MemoryNode):
nodes = [nodes]
for n in nodes:
_id = n.memory_id
if _id in self._id_memory_dict:
self._id_memory_dict.pop(_id, None)
if key is None:
for _, id_list in self._key_id_dict.items():
if _id in id_list:
id_list.remove(_id)
else:
id_list = self._key_id_dict[key]
if _id in id_list:
id_list.remove(_id)
def update_memories(self, keys: str = "", nodes: MemoryNode | List[MemoryNode] = None):
update_memories: Dict[str, MemoryNode] = {n.memory_id: n for n in self.get_memories(keys=keys)}