From 8e8096b82038eda5f9c7ebd2424f75a9d210ce36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=94=B5=E5=A4=A7=E8=80=81baby?= <3231811510@qq.com> Date: Mon, 9 Nov 2026 11:56:00 +0800 Subject: [PATCH] FIX: Deep-copy seed metadata when converting SeedGroup prompts to messages --- pyrit/models/seeds/seed_group.py | 3 ++- tests/unit/models/test_seed_group.py | 23 +++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/pyrit/models/seeds/seed_group.py b/pyrit/models/seeds/seed_group.py index 820108a9a9..ccb313c231 100644 --- a/pyrit/models/seeds/seed_group.py +++ b/pyrit/models/seeds/seed_group.py @@ -13,6 +13,7 @@ import logging import uuid from collections import defaultdict +from copy import deepcopy from typing import TYPE_CHECKING, Annotated, Any from pydantic import BaseModel, ConfigDict, Field, model_validator @@ -460,7 +461,7 @@ def _prompts_to_messages(self, prompts: Sequence[SeedPrompt], *, is_history: boo original_value_data_type=prompt.data_type or "text", conversation_id=str(prompt.prompt_group_id), sequence=sequence, - prompt_metadata=prompt.metadata or {}, + prompt_metadata=deepcopy(prompt.metadata) if prompt.metadata else {}, ) if is_history or piece.api_role in {"assistant", "tool"}: piece.set_simulated_role() diff --git a/tests/unit/models/test_seed_group.py b/tests/unit/models/test_seed_group.py index 527ddad546..48b3b7ee3b 100644 --- a/tests/unit/models/test_seed_group.py +++ b/tests/unit/models/test_seed_group.py @@ -102,6 +102,29 @@ def test_seed_prompts_without_metadata_convert_to_messages() -> None: assert [message.get_piece().prompt_metadata for message in group.user_messages] == [{}, {}] +def test_derived_message_metadata_is_independent_of_seed_metadata() -> None: + """Mutating a derived message's nested metadata must not change the source seed.""" + group = SeedGroup( + seeds=[ + SeedPrompt(value="Earlier request", role="user", sequence=0, metadata={"nested": {"labels": ["initial"]}}), + SeedPrompt(value="Next request", role="user", sequence=1, metadata={"nested": {"labels": ["next"]}}), + ] + ) + + history = group.prepended_conversation + assert history is not None + history[0].get_piece().prompt_metadata["nested"]["labels"].append("history") + next_message = group.next_message + assert next_message is not None + next_message.get_piece().prompt_metadata["nested"]["labels"].append("next-turn") + + assert group.seeds[0].metadata == {"nested": {"labels": ["initial"]}} + assert group.seeds[1].metadata == {"nested": {"labels": ["next"]}} + later_history = group.prepended_conversation + assert later_history is not None + assert later_history[0].get_piece().prompt_metadata["nested"] == {"labels": ["initial"]} + + class TestSeedGroupInit: """Tests for SeedGroup initialization."""