fix(memory): skip messages without a content key in message parsers (#5575)
This commit is contained in:
+26
-13
@@ -61,12 +61,18 @@ def ensure_json_instruction(system_prompt, user_prompt):
|
|||||||
def parse_messages(messages):
|
def parse_messages(messages):
|
||||||
response = ""
|
response = ""
|
||||||
for msg in messages:
|
for msg in messages:
|
||||||
if msg["role"] == "system":
|
role = msg.get("role")
|
||||||
response += f"system: {msg['content']}\n"
|
content = msg.get("content")
|
||||||
if msg["role"] == "user":
|
# Skip messages without textual content (e.g. assistant tool-call
|
||||||
response += f"user: {msg['content']}\n"
|
# messages that carry `tool_calls` but no `content` key).
|
||||||
if msg["role"] == "assistant":
|
if content is None:
|
||||||
response += f"assistant: {msg['content']}\n"
|
continue
|
||||||
|
if role == "system":
|
||||||
|
response += f"system: {content}\n"
|
||||||
|
elif role == "user":
|
||||||
|
response += f"user: {content}\n"
|
||||||
|
elif role == "assistant":
|
||||||
|
response += f"assistant: {content}\n"
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
@@ -173,12 +179,19 @@ def parse_vision_messages(messages, llm=None, vision_details="auto"):
|
|||||||
"""
|
"""
|
||||||
returned_messages = []
|
returned_messages = []
|
||||||
for msg in messages:
|
for msg in messages:
|
||||||
if msg["role"] == "system":
|
role = msg.get("role")
|
||||||
|
content = msg.get("content")
|
||||||
|
if role == "system":
|
||||||
returned_messages.append(msg)
|
returned_messages.append(msg)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# Skip messages without content (e.g. assistant tool-call messages
|
||||||
|
# that carry `tool_calls` but no `content` key).
|
||||||
|
if content is None:
|
||||||
|
continue
|
||||||
|
|
||||||
# Handle message content
|
# Handle message content
|
||||||
if isinstance(msg["content"], list):
|
if isinstance(content, list):
|
||||||
if llm is None:
|
if llm is None:
|
||||||
text_parts = [
|
text_parts = [
|
||||||
part["text"] for part in msg["content"]
|
part["text"] for part in msg["content"]
|
||||||
@@ -186,17 +199,17 @@ def parse_vision_messages(messages, llm=None, vision_details="auto"):
|
|||||||
]
|
]
|
||||||
if not text_parts:
|
if not text_parts:
|
||||||
continue
|
continue
|
||||||
returned_messages.append({"role": msg["role"], "content": " ".join(text_parts)})
|
returned_messages.append({"role": role, "content": " ".join(text_parts)})
|
||||||
else:
|
else:
|
||||||
description = get_image_description(msg, llm, vision_details)
|
description = get_image_description(msg, llm, vision_details)
|
||||||
returned_messages.append({"role": msg["role"], "content": description})
|
returned_messages.append({"role": role, "content": description})
|
||||||
elif isinstance(msg["content"], dict) and msg["content"].get("type") == "image_url":
|
elif isinstance(content, dict) and content.get("type") == "image_url":
|
||||||
if llm is None:
|
if llm is None:
|
||||||
continue
|
continue
|
||||||
image_url = msg["content"]["image_url"]["url"]
|
image_url = content["image_url"]["url"]
|
||||||
try:
|
try:
|
||||||
description = get_image_description(image_url, llm, vision_details)
|
description = get_image_description(image_url, llm, vision_details)
|
||||||
returned_messages.append({"role": msg["role"], "content": description})
|
returned_messages.append({"role": role, "content": description})
|
||||||
except Exception:
|
except Exception:
|
||||||
raise Exception(f"Error while downloading {image_url}.")
|
raise Exception(f"Error while downloading {image_url}.")
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -1,10 +1,50 @@
|
|||||||
import pytest
|
import pytest
|
||||||
from unittest.mock import Mock
|
from unittest.mock import Mock
|
||||||
|
|
||||||
from mem0.memory.utils import parse_vision_messages, remove_spaces_from_entities, sanitize_relationship_for_cypher
|
from mem0.memory.utils import (
|
||||||
|
parse_messages,
|
||||||
|
parse_vision_messages,
|
||||||
|
remove_spaces_from_entities,
|
||||||
|
sanitize_relationship_for_cypher,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestParseMessages:
|
||||||
|
def test_skips_message_without_content_key(self):
|
||||||
|
# Reproduces #5067: a FunctionCalling assistant message carries
|
||||||
|
# `tool_calls` but no `content` key -> used to raise KeyError.
|
||||||
|
messages = [
|
||||||
|
{"role": "user", "content": "hi"},
|
||||||
|
{"role": "assistant", "tool_calls": [{"id": "1", "function": {"name": "search"}}]},
|
||||||
|
{"role": "assistant", "content": "done"},
|
||||||
|
]
|
||||||
|
result = parse_messages(messages)
|
||||||
|
assert result == "user: hi\nassistant: done\n"
|
||||||
|
|
||||||
|
def test_skips_explicit_none_content(self):
|
||||||
|
messages = [{"role": "assistant", "content": None}, {"role": "user", "content": "ok"}]
|
||||||
|
assert parse_messages(messages) == "user: ok\n"
|
||||||
|
|
||||||
|
def test_plain_roles_pass_through(self):
|
||||||
|
messages = [
|
||||||
|
{"role": "system", "content": "sys"},
|
||||||
|
{"role": "user", "content": "u"},
|
||||||
|
{"role": "assistant", "content": "a"},
|
||||||
|
]
|
||||||
|
assert parse_messages(messages) == "system: sys\nuser: u\nassistant: a\n"
|
||||||
|
|
||||||
|
|
||||||
class TestParseVisionMessages:
|
class TestParseVisionMessages:
|
||||||
|
def test_skips_message_without_content_key(self):
|
||||||
|
# Reproduces #5067 for the vision parser path.
|
||||||
|
messages = [
|
||||||
|
{"role": "user", "content": "hi"},
|
||||||
|
{"role": "assistant", "tool_calls": [{"id": "1", "function": {"name": "search"}}]},
|
||||||
|
]
|
||||||
|
result = parse_vision_messages(messages, llm=None)
|
||||||
|
assert len(result) == 1
|
||||||
|
assert result[0] == {"role": "user", "content": "hi"}
|
||||||
|
|
||||||
def test_multimodal_list_without_llm_extracts_text(self):
|
def test_multimodal_list_without_llm_extracts_text(self):
|
||||||
messages = [
|
messages = [
|
||||||
{"role": "user", "content": [
|
{"role": "user", "content": [
|
||||||
|
|||||||
Reference in New Issue
Block a user