Skip to content

Commit 3b151b3

Browse files
committed
FIX: embed only text pieces in multimodal writes instead of failing them
Signed-off-by: Manohar Paturi <186662190+ManoharPaturi@users.noreply.github.com>
1 parent 5503ecb commit 3b151b3

2 files changed

Lines changed: 90 additions & 2 deletions

File tree

‎pyrit/memory/memory_interface.py‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2819,7 +2819,13 @@ def add_message_to_memory(self, *, request: Message) -> None:
28192819
self.add_message_pieces_to_memory(message_pieces=message_pieces)
28202820

28212821
if self.memory_embedding:
2822-
for piece in message_pieces:
2822+
# Embeddings power text similarity search and only describe text pieces.
2823+
# A multimodal message (e.g. text plus an image_path piece) must still
2824+
# persist instead of failing the whole write, and pieces flagged
2825+
# not_in_memory are never persisted, so no embedding row may reference them.
2826+
for piece in pieces_to_persist:
2827+
if piece.converted_value_data_type != "text":
2828+
continue
28232829
embedding_entry = self.memory_embedding.generate_embedding_memory_data(message_piece=piece)
28242830
embedding_entries.append(embedding_entry)
28252831

‎tests/unit/memory/test_sqlite_memory.py‎

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,20 @@
2222
from pyrit.common.singleton import Singleton
2323
from pyrit.converter.base64_converter import Base64Converter
2424
from pyrit.memory.alembic.versions.ab8f2c1a9d07_pre_alembic_release_schema import INITIAL_METADATA
25+
from pyrit.memory.memory_embedding import MemoryEmbedding
2526
from pyrit.memory.memory_models import EmbeddingDataEntry, PromptMemoryEntry
2627
from pyrit.memory.migration import run_schema_migrations
2728
from pyrit.memory.sqlite_memory import SQLiteMemory
2829
from pyrit.memory.storage.serializers import set_message_piece_sha256_async
29-
from pyrit.models import Conversation, MessagePiece, flatten_to_message_pieces
30+
from pyrit.models import (
31+
Conversation,
32+
EmbeddingData,
33+
EmbeddingResponse,
34+
EmbeddingUsageInformation,
35+
Message,
36+
MessagePiece,
37+
flatten_to_message_pieces,
38+
)
3039
from pyrit.prompt_target.text_target import TextTarget
3140
from unit.mocks import get_sample_conversation_entries
3241

@@ -456,6 +465,79 @@ def test_insert_embedding_entry(sqlite_instance):
456465
assert persisted_embedding_entry.embedding_type_name == "test_type"
457466

458467

468+
class _MockEmbeddingGenerator:
469+
"""Duck-typed ``EmbeddingSupport`` stand-in so no network call is needed."""
470+
471+
def generate_text_embedding(self, text: str, **kwargs) -> EmbeddingResponse:
472+
return EmbeddingResponse(
473+
model="mock_model",
474+
object="mock_object",
475+
usage=EmbeddingUsageInformation(prompt_tokens=0, total_tokens=0),
476+
data=[EmbeddingData(embedding=[0.5], index=0, object="mock_object")],
477+
)
478+
479+
480+
def test_add_multimodal_message_with_embedding_persists_and_skips_non_text_pieces(sqlite_instance):
481+
"""A multimodal message must persist when embeddings are enabled.
482+
483+
``MemoryEmbedding.generate_embedding_memory_data`` raises for non-text pieces, so
484+
embedding only the text pieces (instead of failing the whole write) keeps multimodal
485+
sends working while text similarity search still covers the embeddable content.
486+
"""
487+
sqlite_instance.memory_embedding = MemoryEmbedding(embedding_model=_MockEmbeddingGenerator())
488+
sqlite_instance.add_conversation_to_memory(conversation=Conversation(conversation_id="multimodal-conversation"))
489+
490+
text_piece = MessagePiece(
491+
role="user",
492+
original_value="describe this image",
493+
conversation_id="multimodal-conversation",
494+
)
495+
image_piece = MessagePiece(
496+
role="user",
497+
original_value="/tmp/image.png",
498+
original_value_data_type="image_path",
499+
converted_value="/tmp/image.png",
500+
converted_value_data_type="image_path",
501+
conversation_id="multimodal-conversation",
502+
)
503+
504+
sqlite_instance.add_message_to_memory(request=Message(message_pieces=[text_piece, image_piece]))
505+
506+
persisted = sqlite_instance.get_message_pieces(conversation_id="multimodal-conversation")
507+
assert len(persisted) == 2
508+
with sqlite_instance.get_session() as session:
509+
embedding_entries = session.query(EmbeddingDataEntry).all()
510+
assert len(embedding_entries) == 1
511+
assert embedding_entries[0].id == text_piece.id
512+
513+
514+
def test_add_message_with_embedding_skips_pieces_not_in_memory(sqlite_instance):
515+
"""Pieces flagged ``not_in_memory`` are never persisted, so they get no embedding row."""
516+
sqlite_instance.memory_embedding = MemoryEmbedding(embedding_model=_MockEmbeddingGenerator())
517+
sqlite_instance.add_conversation_to_memory(conversation=Conversation(conversation_id="ephemeral-conversation"))
518+
519+
persisted_piece = MessagePiece(
520+
role="user",
521+
original_value="persisted text",
522+
conversation_id="ephemeral-conversation",
523+
)
524+
ephemeral_piece = MessagePiece(
525+
role="user",
526+
original_value="ephemeral text",
527+
conversation_id="ephemeral-conversation",
528+
not_in_memory=True,
529+
)
530+
531+
sqlite_instance.add_message_to_memory(request=Message(message_pieces=[persisted_piece, ephemeral_piece]))
532+
533+
persisted = sqlite_instance.get_message_pieces(conversation_id="ephemeral-conversation")
534+
assert [piece.original_value for piece in persisted] == ["persisted text"]
535+
with sqlite_instance.get_session() as session:
536+
embedding_entries = session.query(EmbeddingDataEntry).all()
537+
assert len(embedding_entries) == 1
538+
assert embedding_entries[0].id == persisted_piece.id
539+
540+
459541
def test_disable_embedding(sqlite_instance):
460542
sqlite_instance.disable_embedding()
461543

0 commit comments

Comments
 (0)