forked from CopilotKit/CopilotKit
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtest_before_model_modifier.py
More file actions
222 lines (167 loc) · 8.45 KB
/
Copy pathtest_before_model_modifier.py
File metadata and controls
222 lines (167 loc) · 8.45 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
"""Unit tests for before_model_modifier.
Verifies that sales-todo state is serialized into the LLM system prompt and
that missing / malformed state degrades gracefully (no crash, no leaking
internal errors into the prompt).
"""
from __future__ import annotations
from types import SimpleNamespace
from google.genai import types
from agents.main import before_model_modifier
class FakeCallbackContext:
def __init__(
self,
*,
agent_name: str = "SalesPipelineAgent",
state: dict | None = None,
) -> None:
self.agent_name = agent_name
self.state = {} if state is None else state
def _make_request() -> SimpleNamespace:
"""Build a minimal LlmRequest-like object with an empty system_instruction config."""
config = SimpleNamespace(system_instruction=None)
return SimpleNamespace(config=config)
def _system_text(request) -> str:
"""Concatenate all system_instruction part texts for assertion."""
si = request.config.system_instruction
if si is None:
return ""
if not isinstance(si, types.Content):
return str(si)
return "".join((p.text or "") for p in (si.parts or []))
# ---------------------------------------------------------------------------
# Happy path: serialized todos appear in the prompt.
# ---------------------------------------------------------------------------
def test_todos_serialized_into_system_prompt():
todos = [
{"id": "1", "title": "Call Acme", "status": "open"},
{"id": "2", "title": "Send proposal", "status": "done"},
]
ctx = FakeCallbackContext(state={"todos": todos})
request = _make_request()
before_model_modifier(ctx, request)
text = _system_text(request)
assert "Call Acme" in text
assert "Send proposal" in text
assert "manage_sales_todos" in text
# ---------------------------------------------------------------------------
# Missing state: must not crash and must fall back to neutral placeholder.
# ---------------------------------------------------------------------------
def test_missing_todos_state_does_not_crash_and_uses_placeholder():
ctx = FakeCallbackContext(state={}) # no "todos" key at all
request = _make_request()
# Must not raise.
before_model_modifier(ctx, request)
text = _system_text(request)
assert "No sales todos yet" in text
def test_none_todos_state_uses_placeholder():
ctx = FakeCallbackContext(state={"todos": None})
request = _make_request()
before_model_modifier(ctx, request)
text = _system_text(request)
assert "No sales todos yet" in text
# ---------------------------------------------------------------------------
# Non-serializable todos: error must NOT leak into the prompt.
# ---------------------------------------------------------------------------
def test_non_serializable_todos_do_not_leak_error_into_prompt():
# Sets are not JSON-serializable → json.dumps raises TypeError.
ctx = FakeCallbackContext(state={"todos": {"bad": {1, 2, 3}}})
request = _make_request()
before_model_modifier(ctx, request)
text = _system_text(request)
# Error text must not bleed into the LLM prompt (that was the prior bug).
assert "Error serializing todos" not in text
# Neutral placeholder should be used instead.
assert "No sales todos yet" in text
# ---------------------------------------------------------------------------
# Different agent name: callback should be a no-op.
# ---------------------------------------------------------------------------
def test_non_sales_pipeline_agent_is_noop():
ctx = FakeCallbackContext(
agent_name="SomeOtherAgent", state={"todos": [{"id": "x"}]}
)
request = _make_request()
before_model_modifier(ctx, request)
# system_instruction should be untouched (still None).
assert request.config.system_instruction is None
# ---------------------------------------------------------------------------
# Prefix stacking regression (CR round 3 finding #2): calling the callback
# twice with the SAME request MUST NOT stack the prefix twice. The fix builds
# a fresh types.Content each call and assigns it, rather than mutating the
# previous parts[0].text in place.
# ---------------------------------------------------------------------------
def test_before_model_modifier_does_not_stack_prefix_on_repeated_calls():
todos = [{"id": "1", "title": "Call Acme", "status": "open"}]
ctx = FakeCallbackContext(state={"todos": todos})
request = _make_request()
# Call twice with the SAME request object.
before_model_modifier(ctx, request)
first_text = _system_text(request)
before_model_modifier(ctx, request)
second_text = _system_text(request)
# Prefix signature must appear exactly once after each call, not twice
# after the second.
prefix_signature = (
"You are a helpful sales assistant for managing a sales pipeline."
)
assert first_text.count(prefix_signature) == 1, (
f"expected prefix once on first call, got {first_text.count(prefix_signature)}: {first_text!r}"
)
assert second_text.count(prefix_signature) == 1, (
f"expected prefix once on second call (no stacking), got "
f"{second_text.count(prefix_signature)}: {second_text!r}"
)
def test_before_model_modifier_does_not_mutate_shared_original_instruction():
"""If system_instruction is a pre-existing shared Content, the callback
must NOT mutate it in place. Mutation would cause N requests sharing the
same base instruction to stack the prefix N times across requests."""
shared = types.Content(role="system", parts=[types.Part(text="BASE")])
ctx = FakeCallbackContext(state={"todos": []})
request = SimpleNamespace(config=SimpleNamespace(system_instruction=shared))
before_model_modifier(ctx, request)
# Shared instance must be untouched — callback should have assigned a
# fresh Content to request.config.system_instruction.
assert shared.parts[0].text == "BASE", (
f"shared system_instruction was mutated in place: {shared.parts[0].text!r}"
)
# And the request got a new Content, not the shared one.
assert request.config.system_instruction is not shared
# The new instruction still contains BASE appended to the prefix.
new_text = _system_text(request)
assert "BASE" in new_text
assert "You are a helpful sales assistant" in new_text
# ---------------------------------------------------------------------------
# Suffix preservation when prefix signature is present but end_marker is
# missing (mangled / drifted prefix) (CR round 4 finding #1).
#
# Prior behavior chopped `original_text[:sig_idx]` when the end_marker was
# absent, discarding ALL content after the signature — including legitimate
# user content that happened to follow a corrupted prefix. The fix leaves
# `original_text` as-is in that case; at worst we get one duplicated
# signature (single-shot, non-stacking) instead of data loss.
# ---------------------------------------------------------------------------
def test_before_model_modifier_preserves_suffix_when_end_marker_missing():
"""When system_instruction starts with the PREFIX_SIGNATURE but does NOT
contain the end_marker, the callback must NOT chop the user suffix.
Instead it should leave `original_text` untouched and prepend a fresh
prefix. Worst case is a single duplicated signature — that is explicitly
preferred over data loss."""
prefix_signature = (
"You are a helpful sales assistant for managing a sales pipeline."
)
# Build a mangled system_instruction: starts with the signature (so the
# `find(PREFIX_SIGNATURE)` branch fires), but DOES NOT contain the end
# marker sentence. A real-world user suffix follows that should survive.
user_suffix = "IMPORTANT: Always respond in French."
mangled_base = f"{prefix_signature} (but the end marker is missing!) {user_suffix}"
shared = types.Content(role="system", parts=[types.Part(text=mangled_base)])
ctx = FakeCallbackContext(state={"todos": []})
request = SimpleNamespace(config=SimpleNamespace(system_instruction=shared))
before_model_modifier(ctx, request)
new_text = _system_text(request)
# User suffix MUST survive — it was legitimate content after a
# corrupted prefix.
assert user_suffix in new_text, (
f"user suffix was dropped when end_marker was missing: {new_text!r}"
)
# A fresh prefix is prepended, so the signature appears at least once.
assert prefix_signature in new_text