|
22 | 22 | from pyrit.common.singleton import Singleton |
23 | 23 | from pyrit.converter.base64_converter import Base64Converter |
24 | 24 | from pyrit.memory.alembic.versions.ab8f2c1a9d07_pre_alembic_release_schema import INITIAL_METADATA |
| 25 | +from pyrit.memory.memory_embedding import MemoryEmbedding |
25 | 26 | from pyrit.memory.memory_models import EmbeddingDataEntry, PromptMemoryEntry |
26 | 27 | from pyrit.memory.migration import run_schema_migrations |
27 | 28 | from pyrit.memory.sqlite_memory import SQLiteMemory |
28 | 29 | 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 | +) |
30 | 39 | from pyrit.prompt_target.text_target import TextTarget |
31 | 40 | from unit.mocks import get_sample_conversation_entries |
32 | 41 |
|
@@ -456,6 +465,79 @@ def test_insert_embedding_entry(sqlite_instance): |
456 | 465 | assert persisted_embedding_entry.embedding_type_name == "test_type" |
457 | 466 |
|
458 | 467 |
|
| 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 | + |
459 | 541 | def test_disable_embedding(sqlite_instance): |
460 | 542 | sqlite_instance.disable_embedding() |
461 | 543 |
|
|
0 commit comments