pixie-prompts 0.0.0__py3-none-any.whl → 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pixie/prompts/__init__.py +19 -0
- pixie/prompts/file_watcher.py +320 -0
- pixie/prompts/graphql.py +17 -10
- pixie/prompts/prompt.py +4 -4
- pixie/prompts/server.py +16 -48
- pixie/prompts/storage.py +206 -35
- {pixie_prompts-0.0.0.dist-info → pixie_prompts-0.1.0.dist-info}/METADATA +2 -1
- pixie_prompts-0.1.0.dist-info/RECORD +12 -0
- pixie_prompts-0.1.0.dist-info/entry_points.txt +3 -0
- pixie/tests/__init__.py +0 -0
- pixie/tests/test_prompt.py +0 -1321
- pixie/tests/test_prompt_management.py +0 -117
- pixie/tests/test_prompt_storage.py +0 -1453
- pixie_prompts-0.0.0.dist-info/RECORD +0 -15
- pixie_prompts-0.0.0.dist-info/entry_points.txt +0 -3
- {pixie_prompts-0.0.0.dist-info → pixie_prompts-0.1.0.dist-info}/WHEEL +0 -0
- {pixie_prompts-0.0.0.dist-info → pixie_prompts-0.1.0.dist-info}/licenses/LICENSE +0 -0
pixie/tests/test_prompt.py
DELETED
|
@@ -1,1321 +0,0 @@
|
|
|
1
|
-
"""Comprehensive unit tests for pixie.prompts.prompt module."""
|
|
2
|
-
|
|
3
|
-
import os
|
|
4
|
-
import json
|
|
5
|
-
import pytest
|
|
6
|
-
import tempfile
|
|
7
|
-
from types import NoneType
|
|
8
|
-
from jinja2 import UndefinedError
|
|
9
|
-
|
|
10
|
-
from pixie.prompts.storage import _FilePromptStorage
|
|
11
|
-
from pixie.prompts.prompt import (
|
|
12
|
-
DEFAULT_VERSION_ID,
|
|
13
|
-
BasePrompt,
|
|
14
|
-
PromptVariables,
|
|
15
|
-
BaseUntypedPrompt,
|
|
16
|
-
OutdatedPrompt,
|
|
17
|
-
_prompt_registry, # Import the registry explicitly
|
|
18
|
-
_compiled_prompt_registry,
|
|
19
|
-
)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
class SamplePromptVariables(PromptVariables):
|
|
23
|
-
"""Sample subclass of PromptVariables for testing."""
|
|
24
|
-
|
|
25
|
-
name: str
|
|
26
|
-
age: int
|
|
27
|
-
city: str = "Unknown"
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
class AnotherPromptVariables(PromptVariables):
|
|
31
|
-
"""Another sample subclass with different fields."""
|
|
32
|
-
|
|
33
|
-
greeting: str
|
|
34
|
-
topic: str
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
class NestedPromptVariables(PromptVariables):
|
|
38
|
-
"""Sample with nested model."""
|
|
39
|
-
|
|
40
|
-
user: SamplePromptVariables
|
|
41
|
-
message: str
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
class TestPromptInitialization:
|
|
45
|
-
"""Tests for Prompt initialization."""
|
|
46
|
-
|
|
47
|
-
def test_init_with_string_versions(self):
|
|
48
|
-
"""Test that a string version is converted to a dict with 'default' key."""
|
|
49
|
-
prompt = BasePrompt(versions="Hello, world!")
|
|
50
|
-
|
|
51
|
-
assert isinstance(prompt._versions, dict)
|
|
52
|
-
assert DEFAULT_VERSION_ID in prompt._versions
|
|
53
|
-
assert prompt._versions[DEFAULT_VERSION_ID] == "Hello, world!"
|
|
54
|
-
|
|
55
|
-
@pytest.mark.asyncio
|
|
56
|
-
async def test_init_with_dict_versions(self):
|
|
57
|
-
"""Test initialization with a dictionary of versions."""
|
|
58
|
-
versions = {"v1": "Version 1", "v2": "Version 2"}
|
|
59
|
-
prompt = BasePrompt(versions=versions)
|
|
60
|
-
|
|
61
|
-
assert prompt._versions == versions
|
|
62
|
-
assert set(prompt._versions.keys()) == {"v1", "v2"}
|
|
63
|
-
|
|
64
|
-
def test_init_with_dict_versions_creates_copy(self):
|
|
65
|
-
"""Test that the versions dict is deep copied to prevent external mutations."""
|
|
66
|
-
original_versions = {"v1": "Version 1"}
|
|
67
|
-
prompt = BasePrompt(versions=original_versions)
|
|
68
|
-
|
|
69
|
-
# Modify the original dict
|
|
70
|
-
original_versions["v1"] = "Modified"
|
|
71
|
-
original_versions["v2"] = "New version"
|
|
72
|
-
|
|
73
|
-
# Prompt should still have the original value
|
|
74
|
-
assert prompt._versions["v1"] == "Version 1"
|
|
75
|
-
assert "v2" not in prompt._versions
|
|
76
|
-
|
|
77
|
-
@pytest.mark.asyncio
|
|
78
|
-
async def test_init_with_explicit_default_version(self):
|
|
79
|
-
"""Test setting an explicit default version."""
|
|
80
|
-
versions = {"v1": "Version 1", "v2": "Version 2", "v3": "Version 3"}
|
|
81
|
-
prompt = BasePrompt(versions=versions, default_version_id="v2")
|
|
82
|
-
|
|
83
|
-
assert prompt.get_default_version_id() == "v2"
|
|
84
|
-
|
|
85
|
-
@pytest.mark.asyncio
|
|
86
|
-
async def test_init_default_version_is_first_key_when_not_specified(self):
|
|
87
|
-
"""Test that default version is the first key when not explicitly set."""
|
|
88
|
-
# Note: dict order is preserved in Python 3.7+
|
|
89
|
-
versions = {"first": "First version", "second": "Second version"}
|
|
90
|
-
prompt = BasePrompt(versions=versions)
|
|
91
|
-
|
|
92
|
-
assert prompt.get_default_version_id() == "first"
|
|
93
|
-
|
|
94
|
-
@pytest.mark.asyncio
|
|
95
|
-
async def test_init_with_string_and_default_version(self):
|
|
96
|
-
"""Test that default_version_id works with string versions."""
|
|
97
|
-
prompt = BasePrompt(versions="Hello!", default_version_id="default")
|
|
98
|
-
|
|
99
|
-
assert prompt.get_default_version_id() == "default"
|
|
100
|
-
|
|
101
|
-
def test_init_with_variables_definition(self):
|
|
102
|
-
"""Test initialization with variable definitions."""
|
|
103
|
-
prompt = BasePrompt(
|
|
104
|
-
versions="Hello, {name}!",
|
|
105
|
-
variables_definition=SamplePromptVariables,
|
|
106
|
-
)
|
|
107
|
-
|
|
108
|
-
assert prompt._variables_definition == SamplePromptVariables
|
|
109
|
-
|
|
110
|
-
def test_init_with_none_variables_definition(self):
|
|
111
|
-
"""Test initialization with NoneType variable definitions (default)."""
|
|
112
|
-
|
|
113
|
-
prompt = BasePrompt(versions="Hello!")
|
|
114
|
-
|
|
115
|
-
assert prompt._variables_definition == NoneType
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
class TestPromptProperties:
|
|
119
|
-
"""Tests for Prompt properties."""
|
|
120
|
-
|
|
121
|
-
@pytest.mark.asyncio
|
|
122
|
-
async def test_version_ids_property(self):
|
|
123
|
-
"""Test the version_ids property returns all version keys."""
|
|
124
|
-
versions = {"v1": "Version 1", "v2": "Version 2", "v3": "Version 3"}
|
|
125
|
-
prompt = BasePrompt(versions=versions)
|
|
126
|
-
|
|
127
|
-
version_ids = set(prompt._versions.keys())
|
|
128
|
-
|
|
129
|
-
assert isinstance(version_ids, set)
|
|
130
|
-
assert version_ids == {"v1", "v2", "v3"}
|
|
131
|
-
|
|
132
|
-
@pytest.mark.asyncio
|
|
133
|
-
async def test_version_ids_with_single_version(self):
|
|
134
|
-
"""Test version_ids with a single version."""
|
|
135
|
-
prompt = BasePrompt(versions="Single version")
|
|
136
|
-
|
|
137
|
-
assert set(prompt._versions.keys()) == {DEFAULT_VERSION_ID}
|
|
138
|
-
|
|
139
|
-
@pytest.mark.asyncio
|
|
140
|
-
async def test_default_version_id_property(self):
|
|
141
|
-
"""Test the default_version_id property."""
|
|
142
|
-
versions = {"v1": "Version 1", "v2": "Version 2"}
|
|
143
|
-
prompt = BasePrompt(versions=versions, default_version_id="v2")
|
|
144
|
-
|
|
145
|
-
assert prompt.get_default_version_id() == "v2"
|
|
146
|
-
|
|
147
|
-
@pytest.mark.asyncio
|
|
148
|
-
async def test_default_version_id_property_with_string_init(self):
|
|
149
|
-
"""Test default_version_id when initialized with string."""
|
|
150
|
-
prompt = BasePrompt(versions="Test prompt")
|
|
151
|
-
|
|
152
|
-
assert prompt.get_default_version_id() == DEFAULT_VERSION_ID
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
class TestPromptCompileWithoutVariables:
|
|
156
|
-
"""Tests for Prompt.compile() without variables (NoneType case)."""
|
|
157
|
-
|
|
158
|
-
def test_compile_without_variables_default_version(self):
|
|
159
|
-
"""Test compiling a prompt without variables using default version."""
|
|
160
|
-
prompt = BasePrompt(versions="Hello, world!")
|
|
161
|
-
|
|
162
|
-
result = prompt.compile()
|
|
163
|
-
|
|
164
|
-
assert result == "Hello, world!"
|
|
165
|
-
|
|
166
|
-
def test_compile_without_variables_specific_version(self):
|
|
167
|
-
"""Test compiling a prompt without variables using a specific version."""
|
|
168
|
-
versions = {
|
|
169
|
-
"formal": "Good day, esteemed user.",
|
|
170
|
-
"casual": "Hey there!",
|
|
171
|
-
"excited": "Hello!!!",
|
|
172
|
-
}
|
|
173
|
-
prompt = BasePrompt(versions=versions, default_version_id="casual")
|
|
174
|
-
|
|
175
|
-
result_formal = prompt.compile(version_id="formal")
|
|
176
|
-
result_casual = prompt.compile(version_id="casual")
|
|
177
|
-
result_excited = prompt.compile(version_id="excited")
|
|
178
|
-
|
|
179
|
-
assert result_formal == "Good day, esteemed user."
|
|
180
|
-
assert result_casual == "Hey there!"
|
|
181
|
-
assert result_excited == "Hello!!!"
|
|
182
|
-
|
|
183
|
-
def test_compile_without_variables_uses_default_when_no_version_specified(self):
|
|
184
|
-
"""Test that compile uses default version when version_id is None."""
|
|
185
|
-
versions = {"v1": "Version 1", "v2": "Version 2"}
|
|
186
|
-
prompt = BasePrompt(versions=versions, default_version_id="v2")
|
|
187
|
-
|
|
188
|
-
result = prompt.compile()
|
|
189
|
-
|
|
190
|
-
assert result == "Version 2"
|
|
191
|
-
|
|
192
|
-
def test_compile_plain_text_no_formatting(self):
|
|
193
|
-
"""Test compiling plain text without any formatting placeholders."""
|
|
194
|
-
text = "This is a plain text prompt with no variables."
|
|
195
|
-
prompt = BasePrompt(versions=text)
|
|
196
|
-
|
|
197
|
-
result = prompt.compile()
|
|
198
|
-
|
|
199
|
-
assert result == text
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
class TestPromptCompileWithVariables:
|
|
203
|
-
"""Tests for Prompt.compile() with variables."""
|
|
204
|
-
|
|
205
|
-
def test_compile_with_variables(self):
|
|
206
|
-
"""Test compiling a prompt with variable substitution."""
|
|
207
|
-
template = "Hello, {{name}}! You are {{age}} years old."
|
|
208
|
-
prompt = BasePrompt(
|
|
209
|
-
versions=template,
|
|
210
|
-
variables_definition=SamplePromptVariables,
|
|
211
|
-
)
|
|
212
|
-
|
|
213
|
-
variables = SamplePromptVariables(name="Alice", age=30)
|
|
214
|
-
result = prompt.compile(variables)
|
|
215
|
-
|
|
216
|
-
assert result == "Hello, Alice! You are 30 years old."
|
|
217
|
-
|
|
218
|
-
def test_compile_with_variables_and_default_values(self):
|
|
219
|
-
"""Test that default values in Pydantic model work correctly."""
|
|
220
|
-
template = "Hello, {{name}} from {{city}}!"
|
|
221
|
-
prompt = BasePrompt(
|
|
222
|
-
versions=template,
|
|
223
|
-
variables_definition=SamplePromptVariables,
|
|
224
|
-
)
|
|
225
|
-
|
|
226
|
-
variables = SamplePromptVariables(name="Bob", age=25)
|
|
227
|
-
result = prompt.compile(variables)
|
|
228
|
-
|
|
229
|
-
assert result == "Hello, Bob from Unknown!"
|
|
230
|
-
|
|
231
|
-
def test_compile_with_variables_specific_version(self):
|
|
232
|
-
"""Test compiling with variables using a specific version."""
|
|
233
|
-
versions = {
|
|
234
|
-
"greeting": "Hello, {{name}}!",
|
|
235
|
-
"farewell": "Goodbye, {{name}}!",
|
|
236
|
-
"question": "How are you, {{name}}?",
|
|
237
|
-
}
|
|
238
|
-
prompt = BasePrompt(
|
|
239
|
-
versions=versions,
|
|
240
|
-
variables_definition=SamplePromptVariables,
|
|
241
|
-
default_version_id="greeting",
|
|
242
|
-
)
|
|
243
|
-
|
|
244
|
-
variables = SamplePromptVariables(name="Charlie", age=35)
|
|
245
|
-
|
|
246
|
-
greeting = prompt.compile(variables, version_id="greeting")
|
|
247
|
-
farewell = prompt.compile(variables, version_id="farewell")
|
|
248
|
-
question = prompt.compile(variables, version_id="question")
|
|
249
|
-
|
|
250
|
-
assert greeting == "Hello, Charlie!"
|
|
251
|
-
assert farewell == "Goodbye, Charlie!"
|
|
252
|
-
assert question == "How are you, Charlie?"
|
|
253
|
-
|
|
254
|
-
def test_compile_with_multiple_variables(self):
|
|
255
|
-
"""Test compiling with multiple variable substitutions."""
|
|
256
|
-
template = "{{greeting}}, {{topic}} is fascinating!"
|
|
257
|
-
prompt = BasePrompt(
|
|
258
|
-
versions=template,
|
|
259
|
-
variables_definition=AnotherPromptVariables,
|
|
260
|
-
)
|
|
261
|
-
|
|
262
|
-
variables = AnotherPromptVariables(greeting="Hello", topic="Python")
|
|
263
|
-
result = prompt.compile(variables)
|
|
264
|
-
|
|
265
|
-
assert result == "Hello, Python is fascinating!"
|
|
266
|
-
|
|
267
|
-
def test_compile_with_variables_complex_template(self):
|
|
268
|
-
"""Test compiling with a more complex template."""
|
|
269
|
-
template = """
|
|
270
|
-
Name: {{name}}
|
|
271
|
-
Age: {{age}}
|
|
272
|
-
City: {{city}}
|
|
273
|
-
Status: Active
|
|
274
|
-
"""
|
|
275
|
-
prompt = BasePrompt(
|
|
276
|
-
versions=template,
|
|
277
|
-
variables_definition=SamplePromptVariables,
|
|
278
|
-
)
|
|
279
|
-
|
|
280
|
-
variables = SamplePromptVariables(name="Diana", age=28, city="Paris")
|
|
281
|
-
result = prompt.compile(variables)
|
|
282
|
-
|
|
283
|
-
expected = """
|
|
284
|
-
Name: Diana
|
|
285
|
-
Age: 28
|
|
286
|
-
City: Paris
|
|
287
|
-
Status: Active"""
|
|
288
|
-
assert result == expected
|
|
289
|
-
|
|
290
|
-
def test_compile_with_variables_uses_default_version(self):
|
|
291
|
-
"""Test that compile with variables uses default version when not specified."""
|
|
292
|
-
versions = {
|
|
293
|
-
"v1": "Version 1: {{name}}",
|
|
294
|
-
"v2": "Version 2: {{name}}",
|
|
295
|
-
}
|
|
296
|
-
prompt = BasePrompt(
|
|
297
|
-
versions=versions,
|
|
298
|
-
variables_definition=SamplePromptVariables,
|
|
299
|
-
default_version_id="v2",
|
|
300
|
-
)
|
|
301
|
-
|
|
302
|
-
variables = SamplePromptVariables(name="Eve", age=40)
|
|
303
|
-
result = prompt.compile(variables)
|
|
304
|
-
|
|
305
|
-
assert result == "Version 2: Eve"
|
|
306
|
-
|
|
307
|
-
def test_compile_with_nested_variables(self):
|
|
308
|
-
"""Test compiling with nested property access."""
|
|
309
|
-
template = (
|
|
310
|
-
"Hello {{user.name}}, you are {{user.age}} years old. Message: {{message}}"
|
|
311
|
-
)
|
|
312
|
-
prompt = BasePrompt(
|
|
313
|
-
versions=template,
|
|
314
|
-
variables_definition=NestedPromptVariables,
|
|
315
|
-
)
|
|
316
|
-
|
|
317
|
-
variables = NestedPromptVariables(
|
|
318
|
-
user=SamplePromptVariables(name="Alice", age=30, city="NYC"),
|
|
319
|
-
message="Welcome!",
|
|
320
|
-
)
|
|
321
|
-
result = prompt.compile(variables)
|
|
322
|
-
|
|
323
|
-
assert result == "Hello Alice, you are 30 years old. Message: Welcome!"
|
|
324
|
-
|
|
325
|
-
def test_compile_variables_required_when_definitions_exist(self):
|
|
326
|
-
"""Test that ValueError is raised when variables are required but not provided."""
|
|
327
|
-
prompt = BasePrompt(
|
|
328
|
-
versions="Hello, {name}!",
|
|
329
|
-
variables_definition=SamplePromptVariables,
|
|
330
|
-
)
|
|
331
|
-
|
|
332
|
-
with pytest.raises(ValueError):
|
|
333
|
-
prompt.compile() # type: ignore[call-arg]
|
|
334
|
-
|
|
335
|
-
def test_compile_variables_required_with_none_passed(self):
|
|
336
|
-
"""Test that ValueError is raised when None is explicitly passed."""
|
|
337
|
-
prompt = BasePrompt(
|
|
338
|
-
versions="Hello, {name}!",
|
|
339
|
-
variables_definition=SamplePromptVariables,
|
|
340
|
-
)
|
|
341
|
-
|
|
342
|
-
with pytest.raises(ValueError):
|
|
343
|
-
prompt.compile(None) # type: ignore[arg-type]
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
class TestPromptEdgeCases:
|
|
347
|
-
"""Tests for edge cases and special scenarios."""
|
|
348
|
-
|
|
349
|
-
def test_empty_string_version(self):
|
|
350
|
-
"""Test with an empty string raises ValueError."""
|
|
351
|
-
# Empty string is falsy, so it should fail validation
|
|
352
|
-
with pytest.raises(ValueError, match="No versions provided"):
|
|
353
|
-
BasePrompt(versions="")
|
|
354
|
-
|
|
355
|
-
def test_empty_dict_versions(self):
|
|
356
|
-
"""Test with an empty dict raises ValueError (no versions available)."""
|
|
357
|
-
# This creates a prompt with no versions, which fails validation
|
|
358
|
-
with pytest.raises(ValueError, match="No versions provided"):
|
|
359
|
-
BasePrompt(versions={})
|
|
360
|
-
|
|
361
|
-
def test_version_with_special_characters(self):
|
|
362
|
-
"""Test version content with special characters."""
|
|
363
|
-
special_text = "Hello! @#$%^&*() {{name}} [brackets] 'quotes' \"double\""
|
|
364
|
-
prompt = BasePrompt(
|
|
365
|
-
versions=special_text,
|
|
366
|
-
variables_definition=SamplePromptVariables,
|
|
367
|
-
)
|
|
368
|
-
|
|
369
|
-
variables = SamplePromptVariables(name="Test", age=20)
|
|
370
|
-
result = prompt.compile(variables)
|
|
371
|
-
|
|
372
|
-
assert result == "Hello! @#$%^&*() Test [brackets] 'quotes' \"double\""
|
|
373
|
-
|
|
374
|
-
def test_version_with_curly_braces_not_variables(self):
|
|
375
|
-
"""Test that literal curly braces (doubled) are preserved."""
|
|
376
|
-
template = "This {is} not {{name}} a variable"
|
|
377
|
-
prompt = BasePrompt(
|
|
378
|
-
versions=template,
|
|
379
|
-
variables_definition=SamplePromptVariables,
|
|
380
|
-
)
|
|
381
|
-
|
|
382
|
-
variables = SamplePromptVariables(name="formatted", age=10)
|
|
383
|
-
result = prompt.compile(variables)
|
|
384
|
-
|
|
385
|
-
# Python's .format() handles {{}} as escaped braces
|
|
386
|
-
assert result == "This {is} not formatted a variable"
|
|
387
|
-
|
|
388
|
-
def test_unicode_content(self):
|
|
389
|
-
"""Test with Unicode characters in version content."""
|
|
390
|
-
unicode_text = "Hello, {{name}}! 你好 🎉 Привет"
|
|
391
|
-
prompt = BasePrompt(
|
|
392
|
-
versions=unicode_text,
|
|
393
|
-
variables_definition=SamplePromptVariables,
|
|
394
|
-
)
|
|
395
|
-
|
|
396
|
-
variables = SamplePromptVariables(name="World", age=1)
|
|
397
|
-
result = prompt.compile(variables)
|
|
398
|
-
|
|
399
|
-
assert result == "Hello, World! 你好 🎉 Привет"
|
|
400
|
-
|
|
401
|
-
def test_multiline_template(self):
|
|
402
|
-
"""Test with a multiline template."""
|
|
403
|
-
template = """Line 1: {{name}}
|
|
404
|
-
Line 2: {{age}}
|
|
405
|
-
Line 3: {{city}}"""
|
|
406
|
-
prompt = BasePrompt(
|
|
407
|
-
versions=template,
|
|
408
|
-
variables_definition=SamplePromptVariables,
|
|
409
|
-
)
|
|
410
|
-
|
|
411
|
-
variables = SamplePromptVariables(name="Frank", age=50, city="London")
|
|
412
|
-
result = prompt.compile(variables)
|
|
413
|
-
|
|
414
|
-
expected = """Line 1: Frank
|
|
415
|
-
Line 2: 50
|
|
416
|
-
Line 3: London"""
|
|
417
|
-
assert result == expected
|
|
418
|
-
|
|
419
|
-
def test_version_id_lookup_key_error_propagates(self):
|
|
420
|
-
"""Test that KeyError is raised when version_id doesn't exist."""
|
|
421
|
-
prompt = BasePrompt(versions={"v1": "Version 1"})
|
|
422
|
-
|
|
423
|
-
with pytest.raises(KeyError):
|
|
424
|
-
prompt.compile(version_id="nonexistent")
|
|
425
|
-
|
|
426
|
-
def test_missing_variable_in_template_raises_key_error(self):
|
|
427
|
-
"""Test that KeyError is raised when template variable is not in model."""
|
|
428
|
-
template = "Hello, {{name}} and {{missing_var}}!"
|
|
429
|
-
prompt = BasePrompt(
|
|
430
|
-
versions=template,
|
|
431
|
-
variables_definition=SamplePromptVariables,
|
|
432
|
-
)
|
|
433
|
-
|
|
434
|
-
variables = SamplePromptVariables(name="Test", age=25)
|
|
435
|
-
|
|
436
|
-
with pytest.raises(UndefinedError):
|
|
437
|
-
prompt.compile(variables)
|
|
438
|
-
|
|
439
|
-
def test_extra_variables_in_model_dont_affect_compile(self):
|
|
440
|
-
"""Test that extra variables in the model that aren't in template are ignored."""
|
|
441
|
-
template = "Hello, {{name}}!"
|
|
442
|
-
prompt = BasePrompt(
|
|
443
|
-
versions=template,
|
|
444
|
-
variables_definition=SamplePromptVariables,
|
|
445
|
-
)
|
|
446
|
-
|
|
447
|
-
# Model has age and city, but template only uses name
|
|
448
|
-
variables = SamplePromptVariables(name="Grace", age=60, city="Tokyo")
|
|
449
|
-
result = prompt.compile(variables)
|
|
450
|
-
|
|
451
|
-
assert result == "Hello, Grace!"
|
|
452
|
-
|
|
453
|
-
def test_numeric_values_in_template(self):
|
|
454
|
-
"""Test that numeric values are properly converted to strings."""
|
|
455
|
-
template = "Count: {{age}}, Double: {{age}}"
|
|
456
|
-
prompt = BasePrompt(
|
|
457
|
-
versions=template,
|
|
458
|
-
variables_definition=SamplePromptVariables,
|
|
459
|
-
)
|
|
460
|
-
|
|
461
|
-
variables = SamplePromptVariables(name="Test", age=42)
|
|
462
|
-
result = prompt.compile(variables)
|
|
463
|
-
|
|
464
|
-
assert result == "Count: 42, Double: 42"
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
class TestPromptTypeAnnotations:
|
|
468
|
-
"""Tests related to type annotations and generic behavior."""
|
|
469
|
-
|
|
470
|
-
def test_prompt_with_nonetype_generic(self):
|
|
471
|
-
"""Test Prompt with NoneType explicitly."""
|
|
472
|
-
|
|
473
|
-
prompt: BasePrompt[NoneType] = BasePrompt(versions="No variables here")
|
|
474
|
-
|
|
475
|
-
result = prompt.compile()
|
|
476
|
-
|
|
477
|
-
assert result == "No variables here"
|
|
478
|
-
|
|
479
|
-
def test_prompt_with_custom_type_generic(self):
|
|
480
|
-
"""Test Prompt with custom PromptVariables type."""
|
|
481
|
-
prompt: BasePrompt[SamplePromptVariables] = BasePrompt(
|
|
482
|
-
versions="Name: {{name}}",
|
|
483
|
-
variables_definition=SamplePromptVariables,
|
|
484
|
-
)
|
|
485
|
-
|
|
486
|
-
variables = SamplePromptVariables(name="Type Test", age=99)
|
|
487
|
-
result = prompt.compile(variables)
|
|
488
|
-
|
|
489
|
-
assert result == "Name: Type Test"
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
class TestPromptIntegration:
|
|
493
|
-
"""Integration tests combining multiple features."""
|
|
494
|
-
|
|
495
|
-
def test_multiple_versions_with_variables(self):
|
|
496
|
-
"""Test using multiple versions with the same variable definitions."""
|
|
497
|
-
versions = {
|
|
498
|
-
"short": "{{greeting}}!",
|
|
499
|
-
"medium": "{{greeting}}, let's talk about {{topic}}.",
|
|
500
|
-
"long": "{{greeting}}! Today we'll discuss {{topic}} in detail.",
|
|
501
|
-
}
|
|
502
|
-
prompt = BasePrompt(
|
|
503
|
-
versions=versions,
|
|
504
|
-
variables_definition=AnotherPromptVariables,
|
|
505
|
-
default_version_id="medium",
|
|
506
|
-
)
|
|
507
|
-
|
|
508
|
-
variables = AnotherPromptVariables(greeting="Hi", topic="AI")
|
|
509
|
-
|
|
510
|
-
short = prompt.compile(variables, version_id="short")
|
|
511
|
-
medium = prompt.compile(variables) # Uses default
|
|
512
|
-
long = prompt.compile(variables, version_id="long")
|
|
513
|
-
|
|
514
|
-
assert short == "Hi!"
|
|
515
|
-
assert medium == "Hi, let's talk about AI."
|
|
516
|
-
assert long == "Hi! Today we'll discuss AI in detail."
|
|
517
|
-
|
|
518
|
-
@pytest.mark.asyncio
|
|
519
|
-
async def test_switching_versions_maintains_state(self):
|
|
520
|
-
"""Test that switching versions doesn't affect internal state."""
|
|
521
|
-
versions = {"v1": "Version 1", "v2": "Version 2"}
|
|
522
|
-
prompt = BasePrompt(versions=versions, default_version_id="v1")
|
|
523
|
-
|
|
524
|
-
result1 = prompt.compile(version_id="v2")
|
|
525
|
-
result2 = prompt.compile() # Should still use v1 as default
|
|
526
|
-
|
|
527
|
-
assert result1 == "Version 2"
|
|
528
|
-
assert result2 == "Version 1"
|
|
529
|
-
assert prompt.get_default_version_id() == "v1"
|
|
530
|
-
|
|
531
|
-
def test_same_prompt_compiled_multiple_times(self):
|
|
532
|
-
"""Test that compiling the same prompt multiple times produces consistent results."""
|
|
533
|
-
template = "Hello, {{name}}!"
|
|
534
|
-
prompt = BasePrompt(
|
|
535
|
-
versions=template,
|
|
536
|
-
variables_definition=SamplePromptVariables,
|
|
537
|
-
)
|
|
538
|
-
|
|
539
|
-
variables = SamplePromptVariables(name="Harry", age=45)
|
|
540
|
-
|
|
541
|
-
results = [prompt.compile(variables) for _ in range(5)]
|
|
542
|
-
|
|
543
|
-
assert all(r == "Hello, Harry!" for r in results)
|
|
544
|
-
|
|
545
|
-
def test_different_variable_instances_same_values(self):
|
|
546
|
-
"""Test that different variable instances with same values produce same output."""
|
|
547
|
-
prompt = BasePrompt(
|
|
548
|
-
versions="{{name}} is {{age}}",
|
|
549
|
-
variables_definition=SamplePromptVariables,
|
|
550
|
-
)
|
|
551
|
-
|
|
552
|
-
vars1 = SamplePromptVariables(name="Ivy", age=33)
|
|
553
|
-
vars2 = SamplePromptVariables(name="Ivy", age=33)
|
|
554
|
-
|
|
555
|
-
result1 = prompt.compile(vars1)
|
|
556
|
-
result2 = prompt.compile(vars2)
|
|
557
|
-
|
|
558
|
-
assert result1 == result2
|
|
559
|
-
assert result1 == "Ivy is 33"
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
class TestPromptUpdateAndOutdated:
|
|
563
|
-
"""Tests for Prompt.update() and OutdatedPrompt behavior."""
|
|
564
|
-
|
|
565
|
-
@pytest.mark.asyncio
|
|
566
|
-
async def test_prompt_update_returns_outdated_prompt(self):
|
|
567
|
-
"""Test that Prompt.update() returns an OutdatedPrompt."""
|
|
568
|
-
prompt = BasePrompt(versions={"v1": "Original"})
|
|
569
|
-
current, outdated = prompt._update(versions={"v1": "Updated"})
|
|
570
|
-
|
|
571
|
-
assert isinstance(outdated, OutdatedPrompt)
|
|
572
|
-
assert outdated.id == prompt.id
|
|
573
|
-
assert outdated.get_versions() == {"v1": "Original"}
|
|
574
|
-
|
|
575
|
-
assert current == prompt # Current prompt remains the same instance
|
|
576
|
-
assert current.get_versions() == {"v1": "Updated"}
|
|
577
|
-
|
|
578
|
-
@pytest.mark.asyncio
|
|
579
|
-
async def test_prompt_update_modifies_prompt_in_place(self):
|
|
580
|
-
"""Test that Prompt.update() modifies the prompt object in place."""
|
|
581
|
-
prompt = BasePrompt(versions={"v1": "Original"}, default_version_id="v1")
|
|
582
|
-
|
|
583
|
-
prompt._update(
|
|
584
|
-
versions={"v1": "Updated", "v2": "New version"}, default_version_id="v2"
|
|
585
|
-
)
|
|
586
|
-
|
|
587
|
-
versions = prompt.get_versions()
|
|
588
|
-
assert versions["v1"] == "Updated"
|
|
589
|
-
assert versions["v2"] == "New version"
|
|
590
|
-
assert prompt.get_default_version_id() == "v2"
|
|
591
|
-
|
|
592
|
-
@pytest.mark.asyncio
|
|
593
|
-
async def test_prompt_remains_usable_after_update(self):
|
|
594
|
-
"""Test that Prompt remains usable after update."""
|
|
595
|
-
prompt = BasePrompt(versions={"v1": "Original {name}"})
|
|
596
|
-
|
|
597
|
-
# Compile before update
|
|
598
|
-
result_before = prompt.compile()
|
|
599
|
-
|
|
600
|
-
# Update
|
|
601
|
-
prompt._update(versions={"v1": "Updated {name}"})
|
|
602
|
-
|
|
603
|
-
# Compile after update
|
|
604
|
-
result_after = prompt.compile()
|
|
605
|
-
|
|
606
|
-
assert result_before == "Original {name}"
|
|
607
|
-
assert result_after == "Updated {name}"
|
|
608
|
-
|
|
609
|
-
def test_outdated_prompt_compile_raises_error(self):
|
|
610
|
-
"""Test that OutdatedPrompt.compile() raises ValueError."""
|
|
611
|
-
outdated = OutdatedPrompt(
|
|
612
|
-
versions={"v1": "Test"},
|
|
613
|
-
default_version_id="v1",
|
|
614
|
-
id="test_id",
|
|
615
|
-
variables_definition=NoneType,
|
|
616
|
-
)
|
|
617
|
-
|
|
618
|
-
with pytest.raises(ValueError, match="This prompt is outdated"):
|
|
619
|
-
outdated.compile()
|
|
620
|
-
|
|
621
|
-
@pytest.mark.asyncio
|
|
622
|
-
async def test_outdated_prompt_update_does_nothing(self):
|
|
623
|
-
"""Test that OutdatedPrompt.update() returns self without changes."""
|
|
624
|
-
outdated = OutdatedPrompt(
|
|
625
|
-
versions={"v1": "Original"},
|
|
626
|
-
default_version_id="v1",
|
|
627
|
-
id="test_id",
|
|
628
|
-
variables_definition=NoneType,
|
|
629
|
-
)
|
|
630
|
-
|
|
631
|
-
with pytest.raises(ValueError):
|
|
632
|
-
outdated._update(versions={"v1": "Updated"})
|
|
633
|
-
|
|
634
|
-
@pytest.mark.asyncio
|
|
635
|
-
async def test_compiled_prompts_become_outdated_on_update(self):
|
|
636
|
-
"""Test that compiled prompts reference OutdatedPrompt after prompt update."""
|
|
637
|
-
prompt = BasePrompt(
|
|
638
|
-
versions={"v1": "Version {name}"},
|
|
639
|
-
variables_definition=SamplePromptVariables,
|
|
640
|
-
)
|
|
641
|
-
variables = SamplePromptVariables(name="Test", age=25)
|
|
642
|
-
|
|
643
|
-
# Compile a prompt
|
|
644
|
-
compiled_result = prompt.compile(variables)
|
|
645
|
-
|
|
646
|
-
# Find the compiled prompt in registry
|
|
647
|
-
compiled_prompt = None
|
|
648
|
-
for cp in _compiled_prompt_registry.values():
|
|
649
|
-
if cp.value == compiled_result:
|
|
650
|
-
compiled_prompt = cp
|
|
651
|
-
break
|
|
652
|
-
|
|
653
|
-
assert compiled_prompt is not None
|
|
654
|
-
assert isinstance(compiled_prompt.prompt, BasePrompt)
|
|
655
|
-
|
|
656
|
-
# Update the prompt
|
|
657
|
-
prompt._update(versions={"v1": "Updated {name}"})
|
|
658
|
-
|
|
659
|
-
# Check that the same compiled prompt now references OutdatedPrompt
|
|
660
|
-
updated_compiled = None
|
|
661
|
-
for cp in _compiled_prompt_registry.values():
|
|
662
|
-
if cp.value == compiled_result:
|
|
663
|
-
updated_compiled = cp
|
|
664
|
-
break
|
|
665
|
-
|
|
666
|
-
assert updated_compiled is not None
|
|
667
|
-
assert isinstance(updated_compiled.prompt, OutdatedPrompt)
|
|
668
|
-
versions = updated_compiled.prompt.get_versions()
|
|
669
|
-
assert versions["v1"] == "Version {name}"
|
|
670
|
-
|
|
671
|
-
@pytest.mark.asyncio
|
|
672
|
-
async def test_outdated_compiled_prompt_cannot_compile(self):
|
|
673
|
-
"""Test that trying to compile an outdated compiled prompt raises error."""
|
|
674
|
-
prompt = BasePrompt(
|
|
675
|
-
versions={"v1": "Version {name}"},
|
|
676
|
-
variables_definition=SamplePromptVariables,
|
|
677
|
-
)
|
|
678
|
-
variables = SamplePromptVariables(name="Test", age=25)
|
|
679
|
-
|
|
680
|
-
# Compile a prompt
|
|
681
|
-
compiled_result = prompt.compile(variables)
|
|
682
|
-
|
|
683
|
-
# Update the prompt, making compiled prompt outdated
|
|
684
|
-
prompt._update(versions={"v1": "Updated {name}"})
|
|
685
|
-
|
|
686
|
-
# Find the outdated compiled prompt
|
|
687
|
-
outdated_cp = None
|
|
688
|
-
for cp in _compiled_prompt_registry.values():
|
|
689
|
-
if cp.value == compiled_result:
|
|
690
|
-
outdated_cp = cp
|
|
691
|
-
break
|
|
692
|
-
|
|
693
|
-
assert outdated_cp is not None
|
|
694
|
-
assert isinstance(outdated_cp.prompt, OutdatedPrompt)
|
|
695
|
-
|
|
696
|
-
with pytest.raises(ValueError, match="This prompt is outdated"):
|
|
697
|
-
outdated_cp.prompt.compile(variables)
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
@pytest.mark.asyncio
|
|
701
|
-
class TestBasePromptNewMethods:
|
|
702
|
-
"""Tests for newly added methods on BasePrompt: append_version and update_default_version_id."""
|
|
703
|
-
|
|
704
|
-
@pytest.fixture(autouse=True)
|
|
705
|
-
def clear_compiled_registry(self):
|
|
706
|
-
"""Clear the compiled prompt registry before each test."""
|
|
707
|
-
_compiled_prompt_registry.clear()
|
|
708
|
-
|
|
709
|
-
async def test_append_version_adds_new_version(self):
|
|
710
|
-
"""Test that append_version adds a new version to the prompt."""
|
|
711
|
-
prompt = BasePrompt(versions={"v1": "Version 1"})
|
|
712
|
-
|
|
713
|
-
prompt.append_version(version_id="v2", content="Version 2")
|
|
714
|
-
|
|
715
|
-
versions = prompt.get_versions()
|
|
716
|
-
assert "v2" in versions
|
|
717
|
-
assert versions["v2"] == "Version 2"
|
|
718
|
-
assert versions["v1"] == "Version 1" # Original version still exists
|
|
719
|
-
|
|
720
|
-
async def test_append_version_sets_as_default_when_requested(self):
|
|
721
|
-
"""Test that append_version can set the new version as default."""
|
|
722
|
-
prompt = BasePrompt(versions={"v1": "Version 1"}, default_version_id="v1")
|
|
723
|
-
|
|
724
|
-
prompt.append_version(version_id="v2", content="Version 2", set_as_default=True)
|
|
725
|
-
|
|
726
|
-
assert prompt.get_default_version_id() == "v2"
|
|
727
|
-
|
|
728
|
-
async def test_append_version_keeps_existing_default_when_not_requested(self):
|
|
729
|
-
"""Test that append_version keeps existing default when set_as_default=False."""
|
|
730
|
-
prompt = BasePrompt(versions={"v1": "Version 1"}, default_version_id="v1")
|
|
731
|
-
|
|
732
|
-
prompt.append_version(
|
|
733
|
-
version_id="v2", content="Version 2", set_as_default=False
|
|
734
|
-
)
|
|
735
|
-
|
|
736
|
-
assert prompt.get_default_version_id() == "v1"
|
|
737
|
-
|
|
738
|
-
async def test_append_version_raises_error_for_existing_version_id(self):
|
|
739
|
-
"""Test that append_version raises ValueError for existing version ID."""
|
|
740
|
-
prompt = BasePrompt(versions={"v1": "Version 1", "v2": "Version 2"})
|
|
741
|
-
|
|
742
|
-
with pytest.raises(ValueError, match="Version ID 'v1' already exists"):
|
|
743
|
-
prompt.append_version(version_id="v1", content="Duplicate")
|
|
744
|
-
|
|
745
|
-
async def test_append_version_creates_outdated_prompt(self):
|
|
746
|
-
"""Test that append_version creates an OutdatedPrompt for compiled prompts."""
|
|
747
|
-
prompt = BasePrompt(
|
|
748
|
-
versions={"v1": "Version {name}"},
|
|
749
|
-
variables_definition=SamplePromptVariables,
|
|
750
|
-
)
|
|
751
|
-
variables = SamplePromptVariables(name="Test", age=25)
|
|
752
|
-
|
|
753
|
-
# Compile to create a compiled prompt entry
|
|
754
|
-
compiled_result = prompt.compile(variables)
|
|
755
|
-
|
|
756
|
-
# Append new version
|
|
757
|
-
prompt.append_version(version_id="v2", content="New {name}")
|
|
758
|
-
|
|
759
|
-
# Check that compiled prompt now references OutdatedPrompt
|
|
760
|
-
compiled_prompt = None
|
|
761
|
-
for cp in _compiled_prompt_registry.values():
|
|
762
|
-
if cp.value == compiled_result:
|
|
763
|
-
compiled_prompt = cp
|
|
764
|
-
break
|
|
765
|
-
|
|
766
|
-
assert compiled_prompt is not None
|
|
767
|
-
assert isinstance(compiled_prompt.prompt, OutdatedPrompt)
|
|
768
|
-
|
|
769
|
-
async def test_update_default_version_id_changes_default(self):
|
|
770
|
-
"""Test that update_default_version_id changes the default version."""
|
|
771
|
-
prompt = BasePrompt(
|
|
772
|
-
versions={"v1": "Version 1", "v2": "Version 2", "v3": "Version 3"},
|
|
773
|
-
default_version_id="v1",
|
|
774
|
-
)
|
|
775
|
-
|
|
776
|
-
prompt.update_default_version_id("v2")
|
|
777
|
-
|
|
778
|
-
assert prompt.get_default_version_id() == "v2"
|
|
779
|
-
|
|
780
|
-
async def test_update_default_version_id_raises_error_for_nonexistent_version(self):
|
|
781
|
-
"""Test that update_default_version_id raises ValueError for nonexistent version."""
|
|
782
|
-
prompt = BasePrompt(versions={"v1": "Version 1"}, default_version_id="v1")
|
|
783
|
-
|
|
784
|
-
with pytest.raises(ValueError, match="Version ID 'nonexistent' does not exist"):
|
|
785
|
-
prompt.update_default_version_id("nonexistent")
|
|
786
|
-
|
|
787
|
-
async def test_update_default_version_id_noop_when_same_version(self):
|
|
788
|
-
"""Test that update_default_version_id does nothing when setting to same version."""
|
|
789
|
-
prompt = BasePrompt(
|
|
790
|
-
versions={"v1": "Version 1", "v2": "Version 2"}, default_version_id="v1"
|
|
791
|
-
)
|
|
792
|
-
|
|
793
|
-
# This should not raise an error or change anything
|
|
794
|
-
prompt.update_default_version_id("v1")
|
|
795
|
-
|
|
796
|
-
assert prompt.get_default_version_id() == "v1"
|
|
797
|
-
|
|
798
|
-
async def test_update_default_version_id_creates_outdated_prompt(self):
|
|
799
|
-
"""Test that update_default_version_id creates OutdatedPrompt for compiled prompts."""
|
|
800
|
-
prompt = BasePrompt(
|
|
801
|
-
versions={"v1": "Version {name}", "v2": "Other {name}"},
|
|
802
|
-
variables_definition=SamplePromptVariables,
|
|
803
|
-
default_version_id="v1",
|
|
804
|
-
)
|
|
805
|
-
variables = SamplePromptVariables(name="Test", age=25)
|
|
806
|
-
|
|
807
|
-
# Compile to create a compiled prompt entry
|
|
808
|
-
compiled_result = prompt.compile(variables)
|
|
809
|
-
|
|
810
|
-
# Update default version
|
|
811
|
-
prompt.update_default_version_id("v2")
|
|
812
|
-
|
|
813
|
-
# Check that compiled prompt now references OutdatedPrompt
|
|
814
|
-
compiled_prompt = None
|
|
815
|
-
for cp in _compiled_prompt_registry.values():
|
|
816
|
-
if cp.value == compiled_result:
|
|
817
|
-
compiled_prompt = cp
|
|
818
|
-
break
|
|
819
|
-
|
|
820
|
-
assert compiled_prompt is not None
|
|
821
|
-
assert isinstance(compiled_prompt.prompt, OutdatedPrompt)
|
|
822
|
-
|
|
823
|
-
async def test_append_version_with_variables_compiles_correctly(self):
|
|
824
|
-
"""Test that appended versions compile correctly with variables."""
|
|
825
|
-
prompt = BasePrompt(
|
|
826
|
-
versions={"v1": "Hello {{name}}"},
|
|
827
|
-
variables_definition=SamplePromptVariables,
|
|
828
|
-
)
|
|
829
|
-
|
|
830
|
-
prompt.append_version(version_id="v2", content="Hi {{name}}, you are {{age}}")
|
|
831
|
-
|
|
832
|
-
variables = SamplePromptVariables(name="Alice", age=30)
|
|
833
|
-
|
|
834
|
-
result_v1 = prompt.compile(variables, version_id="v1")
|
|
835
|
-
result_v2 = prompt.compile(variables, version_id="v2")
|
|
836
|
-
|
|
837
|
-
assert result_v1 == "Hello Alice"
|
|
838
|
-
assert result_v2 == "Hi Alice, you are 30"
|
|
839
|
-
|
|
840
|
-
async def test_update_default_version_id_affects_compile_without_version_id(self):
|
|
841
|
-
"""Test that updating default version affects compile() without version_id parameter."""
|
|
842
|
-
prompt = BasePrompt(
|
|
843
|
-
versions={"v1": "Version 1", "v2": "Version 2"},
|
|
844
|
-
variables_definition=SamplePromptVariables,
|
|
845
|
-
default_version_id="v1",
|
|
846
|
-
)
|
|
847
|
-
|
|
848
|
-
variables = SamplePromptVariables(name="Test", age=25)
|
|
849
|
-
|
|
850
|
-
# Initially uses v1
|
|
851
|
-
result_before = prompt.compile(variables)
|
|
852
|
-
assert result_before == "Version 1"
|
|
853
|
-
|
|
854
|
-
# Update default to v2
|
|
855
|
-
prompt.update_default_version_id("v2")
|
|
856
|
-
|
|
857
|
-
# Now uses v2 as default
|
|
858
|
-
result_after = prompt.compile(variables)
|
|
859
|
-
assert result_after == "Version 2"
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
class TestUpdatePromptRegistry:
|
|
863
|
-
"""Tests for the update_prompt_registry function."""
|
|
864
|
-
|
|
865
|
-
@pytest.mark.asyncio
|
|
866
|
-
async def test_update_prompt_registry_new_prompt_raises_error(self):
|
|
867
|
-
"""Test that update_prompt_registry raises KeyError for new prompts."""
|
|
868
|
-
new_prompt = BaseUntypedPrompt(versions={"v1": "New version"})
|
|
869
|
-
|
|
870
|
-
with pytest.raises(KeyError):
|
|
871
|
-
BasePrompt.update_prompt_registry(new_prompt)
|
|
872
|
-
|
|
873
|
-
@pytest.mark.asyncio
|
|
874
|
-
async def test_update_prompt_registry_existing_prompt(self):
|
|
875
|
-
"""Test that update_prompt_registry updates existing prompts."""
|
|
876
|
-
original_prompt = BaseUntypedPrompt(versions={"v1": "Original version"})
|
|
877
|
-
# Manually add to registry with variable definitions
|
|
878
|
-
_prompt_registry[original_prompt.id] = BasePrompt.from_untyped(
|
|
879
|
-
original_prompt, variables_definition=SamplePromptVariables
|
|
880
|
-
)
|
|
881
|
-
|
|
882
|
-
# Temporarily remove from registry to create updated UntypedPrompt
|
|
883
|
-
del _prompt_registry[original_prompt.id]
|
|
884
|
-
|
|
885
|
-
updated_prompt = BaseUntypedPrompt(
|
|
886
|
-
id=original_prompt.id, versions={"v1": "Updated version"}
|
|
887
|
-
)
|
|
888
|
-
|
|
889
|
-
# Add back to registry to simulate existing
|
|
890
|
-
_prompt_registry[original_prompt.id] = BasePrompt.from_untyped(
|
|
891
|
-
original_prompt, variables_definition=SamplePromptVariables
|
|
892
|
-
)
|
|
893
|
-
|
|
894
|
-
result = BasePrompt.update_prompt_registry(updated_prompt)
|
|
895
|
-
|
|
896
|
-
assert result.id == original_prompt.id
|
|
897
|
-
versions = result.get_versions()
|
|
898
|
-
assert versions["v1"] == "Updated version"
|
|
899
|
-
assert (
|
|
900
|
-
result.variables_definition == SamplePromptVariables
|
|
901
|
-
) # Should retain original var_def
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
@pytest.mark.asyncio
|
|
905
|
-
class TestFilePromptStorage:
|
|
906
|
-
"""Tests for the FilePromptStorage class."""
|
|
907
|
-
|
|
908
|
-
async def test_save_existing_prompt(self):
|
|
909
|
-
"""Test saving an existing prompt updates its data."""
|
|
910
|
-
with tempfile.TemporaryDirectory() as temp_dir:
|
|
911
|
-
storage = _FilePromptStorage(directory=temp_dir)
|
|
912
|
-
|
|
913
|
-
# Create the prompt in registry first
|
|
914
|
-
prompt = BaseUntypedPrompt(versions={"v1": "Initial version"})
|
|
915
|
-
BasePrompt.from_untyped(prompt)
|
|
916
|
-
|
|
917
|
-
storage.save(prompt) # Use await for async call
|
|
918
|
-
|
|
919
|
-
# Update the prompt
|
|
920
|
-
updated_prompt = BaseUntypedPrompt(
|
|
921
|
-
id=prompt.id, versions={"v1": "Updated version"}
|
|
922
|
-
)
|
|
923
|
-
|
|
924
|
-
is_new = storage.save(updated_prompt) # Use await for async call
|
|
925
|
-
|
|
926
|
-
assert not is_new
|
|
927
|
-
|
|
928
|
-
# Verify the file content
|
|
929
|
-
filepath = os.path.join(temp_dir, f"{prompt.id}.json")
|
|
930
|
-
with open(filepath, "r") as f:
|
|
931
|
-
data = json.load(f)
|
|
932
|
-
|
|
933
|
-
assert data["versions"]["v1"] == "Updated version"
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
class TestVariablesDefinitionToSchema:
|
|
937
|
-
"""Tests for variables_definition_to_schema function."""
|
|
938
|
-
|
|
939
|
-
def test_nonetype_returns_empty_schema(self):
|
|
940
|
-
"""Test that NoneType returns an empty schema."""
|
|
941
|
-
from pixie.prompts.prompt import variables_definition_to_schema
|
|
942
|
-
|
|
943
|
-
result = variables_definition_to_schema(NoneType)
|
|
944
|
-
assert result == {"type": "object", "properties": {}}
|
|
945
|
-
|
|
946
|
-
def test_prompt_variables_returns_json_schema(self):
|
|
947
|
-
"""Test that PromptVariables subclass returns proper JSON schema."""
|
|
948
|
-
from pixie.prompts.prompt import variables_definition_to_schema
|
|
949
|
-
|
|
950
|
-
result = variables_definition_to_schema(SamplePromptVariables)
|
|
951
|
-
|
|
952
|
-
assert result["type"] == "object"
|
|
953
|
-
assert "properties" in result
|
|
954
|
-
assert "name" in result["properties"]
|
|
955
|
-
assert "age" in result["properties"]
|
|
956
|
-
assert "city" in result["properties"]
|
|
957
|
-
|
|
958
|
-
# Check required fields
|
|
959
|
-
assert "required" in result
|
|
960
|
-
assert "name" in result["required"]
|
|
961
|
-
assert "age" in result["required"]
|
|
962
|
-
# city has default, so not required
|
|
963
|
-
|
|
964
|
-
def test_different_variable_classes_have_different_schemas(self):
|
|
965
|
-
"""Test that different PromptVariables classes produce different schemas."""
|
|
966
|
-
from pixie.prompts.prompt import variables_definition_to_schema
|
|
967
|
-
|
|
968
|
-
schema1 = variables_definition_to_schema(SamplePromptVariables)
|
|
969
|
-
schema2 = variables_definition_to_schema(AnotherPromptVariables)
|
|
970
|
-
|
|
971
|
-
assert schema1 != schema2
|
|
972
|
-
assert "name" in schema1["properties"]
|
|
973
|
-
assert "greeting" in schema2["properties"]
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
@pytest.mark.asyncio
|
|
977
|
-
class TestBasePromptFromUntyped:
|
|
978
|
-
"""Tests for BasePrompt.from_untyped with schema validation."""
|
|
979
|
-
|
|
980
|
-
async def test_from_untyped_with_compatible_schema(self):
|
|
981
|
-
"""Test that from_untyped works with compatible schema."""
|
|
982
|
-
untyped = BaseUntypedPrompt(
|
|
983
|
-
versions={"v1": "Hello {name}"},
|
|
984
|
-
default_version_id="v1",
|
|
985
|
-
variables_schema={
|
|
986
|
-
"type": "object",
|
|
987
|
-
"properties": {
|
|
988
|
-
"name": {"type": "string"},
|
|
989
|
-
"age": {"type": "integer"},
|
|
990
|
-
"city": {"type": "string"},
|
|
991
|
-
},
|
|
992
|
-
"required": ["name", "age"],
|
|
993
|
-
},
|
|
994
|
-
)
|
|
995
|
-
|
|
996
|
-
# SamplePromptVariables has name, age, city (with default)
|
|
997
|
-
# This should be compatible
|
|
998
|
-
typed = BasePrompt.from_untyped(untyped, SamplePromptVariables)
|
|
999
|
-
|
|
1000
|
-
assert typed.variables_definition == SamplePromptVariables
|
|
1001
|
-
assert typed.get_versions() == {"v1": "Hello {name}"}
|
|
1002
|
-
|
|
1003
|
-
async def test_from_untyped_with_incompatible_schema_raises_error(self):
|
|
1004
|
-
"""Test that from_untyped raises ValueError for incompatible schema."""
|
|
1005
|
-
# Base schema requires 'greeting' and 'topic'
|
|
1006
|
-
untyped = BaseUntypedPrompt(
|
|
1007
|
-
versions={"v1": "Hello"},
|
|
1008
|
-
default_version_id="v1",
|
|
1009
|
-
variables_schema={
|
|
1010
|
-
"type": "object",
|
|
1011
|
-
"properties": {
|
|
1012
|
-
"greeting": {"type": "string"},
|
|
1013
|
-
"topic": {"type": "string"},
|
|
1014
|
-
},
|
|
1015
|
-
"required": ["greeting", "topic"],
|
|
1016
|
-
},
|
|
1017
|
-
)
|
|
1018
|
-
|
|
1019
|
-
# SamplePromptVariables has name, age, city - incompatible
|
|
1020
|
-
with pytest.raises(TypeError):
|
|
1021
|
-
BasePrompt.from_untyped(untyped, SamplePromptVariables)
|
|
1022
|
-
|
|
1023
|
-
async def test_from_untyped_with_nonetype(self):
|
|
1024
|
-
"""Test that from_untyped works with NoneType."""
|
|
1025
|
-
untyped = BaseUntypedPrompt(
|
|
1026
|
-
versions={"v1": "Hello"},
|
|
1027
|
-
default_version_id="v1",
|
|
1028
|
-
)
|
|
1029
|
-
|
|
1030
|
-
typed = BasePrompt.from_untyped(untyped, NoneType)
|
|
1031
|
-
|
|
1032
|
-
assert typed.variables_definition == NoneType
|
|
1033
|
-
result = typed.compile()
|
|
1034
|
-
assert result == "Hello"
|
|
1035
|
-
|
|
1036
|
-
async def test_from_untyped_preserves_id(self):
|
|
1037
|
-
"""Test that from_untyped preserves the prompt ID."""
|
|
1038
|
-
untyped = BaseUntypedPrompt(
|
|
1039
|
-
versions={"v1": "Hello"},
|
|
1040
|
-
default_version_id="v1",
|
|
1041
|
-
id="test_id_123",
|
|
1042
|
-
)
|
|
1043
|
-
|
|
1044
|
-
typed = BasePrompt.from_untyped(untyped, NoneType)
|
|
1045
|
-
|
|
1046
|
-
assert typed.id == "test_id_123"
|
|
1047
|
-
|
|
1048
|
-
async def test_from_untyped_with_empty_schema_accepts_any_variables(self):
|
|
1049
|
-
"""Test that empty schema accepts any variables definition."""
|
|
1050
|
-
untyped = BaseUntypedPrompt(
|
|
1051
|
-
versions={"v1": "Hello {name}"},
|
|
1052
|
-
default_version_id="v1",
|
|
1053
|
-
variables_schema={"type": "object", "properties": {}},
|
|
1054
|
-
)
|
|
1055
|
-
|
|
1056
|
-
# Should accept any PromptVariables subclass
|
|
1057
|
-
typed1 = BasePrompt.from_untyped(untyped, SamplePromptVariables)
|
|
1058
|
-
typed2 = BasePrompt.from_untyped(untyped, AnotherPromptVariables)
|
|
1059
|
-
|
|
1060
|
-
assert typed1.variables_definition == SamplePromptVariables
|
|
1061
|
-
assert typed2.variables_definition == AnotherPromptVariables
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
@pytest.mark.asyncio
|
|
1065
|
-
class TestBaseUntypedPromptWithSchema:
|
|
1066
|
-
"""Tests for BaseUntypedPrompt with variables_schema parameter."""
|
|
1067
|
-
|
|
1068
|
-
async def test_init_with_variables_schema(self):
|
|
1069
|
-
"""Test initialization with explicit variables_schema."""
|
|
1070
|
-
schema = {
|
|
1071
|
-
"type": "object",
|
|
1072
|
-
"properties": {"name": {"type": "string"}},
|
|
1073
|
-
"required": ["name"],
|
|
1074
|
-
}
|
|
1075
|
-
|
|
1076
|
-
prompt = BaseUntypedPrompt(
|
|
1077
|
-
versions={"v1": "Hello"},
|
|
1078
|
-
variables_schema=schema,
|
|
1079
|
-
)
|
|
1080
|
-
|
|
1081
|
-
result_schema = prompt.get_variables_schema()
|
|
1082
|
-
assert result_schema == schema
|
|
1083
|
-
|
|
1084
|
-
async def test_init_without_variables_schema_uses_empty(self):
|
|
1085
|
-
"""Test that missing variables_schema defaults to empty schema."""
|
|
1086
|
-
prompt = BaseUntypedPrompt(versions={"v1": "Hello"})
|
|
1087
|
-
|
|
1088
|
-
result_schema = prompt.get_variables_schema()
|
|
1089
|
-
assert result_schema == {"type": "object", "properties": {}}
|
|
1090
|
-
|
|
1091
|
-
async def test_get_variables_schema_returns_copy(self):
|
|
1092
|
-
"""Test that get_variables_schema returns a deep copy."""
|
|
1093
|
-
original_schema = {
|
|
1094
|
-
"type": "object",
|
|
1095
|
-
"properties": {"name": {"type": "string"}},
|
|
1096
|
-
}
|
|
1097
|
-
|
|
1098
|
-
prompt = BaseUntypedPrompt(
|
|
1099
|
-
versions={"v1": "Hello"},
|
|
1100
|
-
variables_schema=original_schema,
|
|
1101
|
-
)
|
|
1102
|
-
|
|
1103
|
-
result_schema = prompt.get_variables_schema()
|
|
1104
|
-
result_schema["properties"]["name"]["type"] = "integer"
|
|
1105
|
-
|
|
1106
|
-
# Original should be unchanged
|
|
1107
|
-
check_schema = prompt.get_variables_schema()
|
|
1108
|
-
assert check_schema["properties"]["name"]["type"] == "string"
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
@pytest.mark.asyncio
|
|
1112
|
-
class TestOutdatedPromptGetMethods:
|
|
1113
|
-
"""Tests for OutdatedPrompt async get methods."""
|
|
1114
|
-
|
|
1115
|
-
async def test_outdated_prompt_get_versions(self):
|
|
1116
|
-
"""Test that OutdatedPrompt.get_versions works."""
|
|
1117
|
-
outdated = OutdatedPrompt(
|
|
1118
|
-
versions={"v1": "Test", "v2": "Test2"},
|
|
1119
|
-
default_version_id="v1",
|
|
1120
|
-
id="test_id",
|
|
1121
|
-
variables_definition=NoneType,
|
|
1122
|
-
)
|
|
1123
|
-
|
|
1124
|
-
versions = outdated.get_versions()
|
|
1125
|
-
assert versions == {"v1": "Test", "v2": "Test2"}
|
|
1126
|
-
|
|
1127
|
-
async def test_outdated_prompt_get_default_version_id(self):
|
|
1128
|
-
"""Test that OutdatedPrompt.get_default_version_id works."""
|
|
1129
|
-
outdated = OutdatedPrompt(
|
|
1130
|
-
versions={"v1": "Test"},
|
|
1131
|
-
default_version_id="v1",
|
|
1132
|
-
id="test_id",
|
|
1133
|
-
variables_definition=NoneType,
|
|
1134
|
-
)
|
|
1135
|
-
|
|
1136
|
-
default_id = outdated.get_default_version_id()
|
|
1137
|
-
assert default_id == "v1"
|
|
1138
|
-
|
|
1139
|
-
async def test_outdated_prompt_preserves_variables_definition(self):
|
|
1140
|
-
"""Test that OutdatedPrompt preserves variables_definition."""
|
|
1141
|
-
prompt = BasePrompt(
|
|
1142
|
-
versions={"v1": "Hello {name}"},
|
|
1143
|
-
variables_definition=SamplePromptVariables,
|
|
1144
|
-
)
|
|
1145
|
-
|
|
1146
|
-
outdated = OutdatedPrompt.from_prompt(prompt)
|
|
1147
|
-
|
|
1148
|
-
assert outdated.variables_definition == SamplePromptVariables
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
class TestGetCompiledPrompt:
|
|
1152
|
-
"""Tests for get_compiled_prompt function."""
|
|
1153
|
-
|
|
1154
|
-
def setup_method(self):
|
|
1155
|
-
"""Clear registries before each test."""
|
|
1156
|
-
_compiled_prompt_registry.clear()
|
|
1157
|
-
_prompt_registry.clear()
|
|
1158
|
-
|
|
1159
|
-
def teardown_method(self):
|
|
1160
|
-
"""Clear registries after each test."""
|
|
1161
|
-
_compiled_prompt_registry.clear()
|
|
1162
|
-
_prompt_registry.clear()
|
|
1163
|
-
|
|
1164
|
-
def test_get_compiled_prompt_empty_registry(self):
|
|
1165
|
-
"""Test get_compiled_prompt returns None when registry is empty."""
|
|
1166
|
-
from pixie.prompts.prompt import get_compiled_prompt
|
|
1167
|
-
|
|
1168
|
-
result = get_compiled_prompt("any text")
|
|
1169
|
-
assert result is None
|
|
1170
|
-
|
|
1171
|
-
def test_get_compiled_prompt_direct_match(self):
|
|
1172
|
-
"""Test get_compiled_prompt finds prompt by direct id match."""
|
|
1173
|
-
from pixie.prompts.prompt import get_compiled_prompt
|
|
1174
|
-
|
|
1175
|
-
prompt = BasePrompt(versions="Test prompt")
|
|
1176
|
-
compiled_text = prompt.compile()
|
|
1177
|
-
|
|
1178
|
-
# The compiled text should be in the registry with its id as key
|
|
1179
|
-
result = get_compiled_prompt(compiled_text)
|
|
1180
|
-
assert result is not None
|
|
1181
|
-
assert result.value == "Test prompt"
|
|
1182
|
-
assert result.prompt == prompt
|
|
1183
|
-
assert result.version_id == DEFAULT_VERSION_ID
|
|
1184
|
-
|
|
1185
|
-
def test_get_compiled_prompt_value_match(self):
|
|
1186
|
-
"""Test get_compiled_prompt finds prompt by value when direct match fails."""
|
|
1187
|
-
from pixie.prompts.prompt import get_compiled_prompt
|
|
1188
|
-
|
|
1189
|
-
prompt = BasePrompt(versions="Test prompt")
|
|
1190
|
-
prompt.compile()
|
|
1191
|
-
|
|
1192
|
-
# Create a new string with same content but different id
|
|
1193
|
-
same_text = "Test prompt"
|
|
1194
|
-
|
|
1195
|
-
result = get_compiled_prompt(same_text)
|
|
1196
|
-
assert result is not None
|
|
1197
|
-
assert result.value == "Test prompt"
|
|
1198
|
-
assert result.prompt == prompt
|
|
1199
|
-
|
|
1200
|
-
def test_get_compiled_prompt_with_variables(self):
|
|
1201
|
-
"""Test get_compiled_prompt works with variable-substituted prompts."""
|
|
1202
|
-
from pixie.prompts.prompt import get_compiled_prompt
|
|
1203
|
-
|
|
1204
|
-
prompt = BasePrompt(
|
|
1205
|
-
versions="Hello, {{name}}!", variables_definition=SamplePromptVariables
|
|
1206
|
-
)
|
|
1207
|
-
variables = SamplePromptVariables(name="Alice", age=25)
|
|
1208
|
-
compiled_text = prompt.compile(variables)
|
|
1209
|
-
|
|
1210
|
-
result = get_compiled_prompt(compiled_text)
|
|
1211
|
-
assert result is not None
|
|
1212
|
-
assert result.value == "Hello, Alice!"
|
|
1213
|
-
assert result.prompt == prompt
|
|
1214
|
-
assert result.variables == variables
|
|
1215
|
-
|
|
1216
|
-
def test_get_compiled_prompt_json_string(self):
|
|
1217
|
-
"""Test get_compiled_prompt can find prompts in JSON strings."""
|
|
1218
|
-
from pixie.prompts.prompt import get_compiled_prompt
|
|
1219
|
-
|
|
1220
|
-
prompt = BasePrompt(versions="Test prompt")
|
|
1221
|
-
compiled_text = prompt.compile()
|
|
1222
|
-
|
|
1223
|
-
# Embed the compiled text in JSON
|
|
1224
|
-
json_text = f'{{"message": "{compiled_text}", "other": "data"}}'
|
|
1225
|
-
|
|
1226
|
-
result = get_compiled_prompt(json_text)
|
|
1227
|
-
assert result is not None
|
|
1228
|
-
assert result.value == "Test prompt"
|
|
1229
|
-
assert result.prompt == prompt
|
|
1230
|
-
|
|
1231
|
-
def test_get_compiled_prompt_json_array(self):
|
|
1232
|
-
"""Test get_compiled_prompt can find prompts in JSON arrays."""
|
|
1233
|
-
from pixie.prompts.prompt import get_compiled_prompt
|
|
1234
|
-
|
|
1235
|
-
prompt = BasePrompt(versions="Test prompt")
|
|
1236
|
-
compiled_text = prompt.compile()
|
|
1237
|
-
|
|
1238
|
-
# Embed the compiled text in JSON array
|
|
1239
|
-
json_text = f'["start", "{compiled_text}", "end"]'
|
|
1240
|
-
|
|
1241
|
-
result = get_compiled_prompt(json_text)
|
|
1242
|
-
assert result is not None
|
|
1243
|
-
assert result.value == "Test prompt"
|
|
1244
|
-
assert result.prompt == prompt
|
|
1245
|
-
|
|
1246
|
-
def test_get_compiled_prompt_nested_json(self):
|
|
1247
|
-
"""Test get_compiled_prompt can find prompts in nested JSON structures."""
|
|
1248
|
-
from pixie.prompts.prompt import get_compiled_prompt
|
|
1249
|
-
|
|
1250
|
-
prompt = BasePrompt(versions="Test prompt")
|
|
1251
|
-
compiled_text = prompt.compile()
|
|
1252
|
-
|
|
1253
|
-
# Embed in nested structure
|
|
1254
|
-
json_text = f'{{"data": {{"messages": ["{compiled_text}"]}}}}'
|
|
1255
|
-
|
|
1256
|
-
result = get_compiled_prompt(json_text)
|
|
1257
|
-
assert result is not None
|
|
1258
|
-
assert result.value == "Test prompt"
|
|
1259
|
-
assert result.prompt == prompt
|
|
1260
|
-
|
|
1261
|
-
def test_get_compiled_prompt_no_match(self):
|
|
1262
|
-
"""Test get_compiled_prompt returns None when no match is found."""
|
|
1263
|
-
from pixie.prompts.prompt import get_compiled_prompt
|
|
1264
|
-
|
|
1265
|
-
prompt = BasePrompt(versions="Test prompt")
|
|
1266
|
-
prompt.compile() # Add to registry
|
|
1267
|
-
|
|
1268
|
-
result = get_compiled_prompt("Completely different text")
|
|
1269
|
-
assert result is None
|
|
1270
|
-
|
|
1271
|
-
def test_get_compiled_prompt_invalid_json(self):
|
|
1272
|
-
"""Test get_compiled_prompt handles invalid JSON gracefully."""
|
|
1273
|
-
from pixie.prompts.prompt import get_compiled_prompt
|
|
1274
|
-
|
|
1275
|
-
prompt = BasePrompt(versions="Test prompt")
|
|
1276
|
-
prompt.compile() # Add to registry
|
|
1277
|
-
|
|
1278
|
-
# Invalid JSON should not crash
|
|
1279
|
-
result = get_compiled_prompt("{invalid json")
|
|
1280
|
-
assert result is None
|
|
1281
|
-
|
|
1282
|
-
def test_get_compiled_prompt_empty_string(self):
|
|
1283
|
-
"""Test get_compiled_prompt with empty string."""
|
|
1284
|
-
from pixie.prompts.prompt import get_compiled_prompt
|
|
1285
|
-
|
|
1286
|
-
result = get_compiled_prompt("")
|
|
1287
|
-
assert result is None
|
|
1288
|
-
|
|
1289
|
-
def test_get_compiled_prompt_multiple_prompts(self):
|
|
1290
|
-
"""Test get_compiled_prompt with multiple prompts in registry."""
|
|
1291
|
-
from pixie.prompts.prompt import get_compiled_prompt
|
|
1292
|
-
|
|
1293
|
-
prompt1 = BasePrompt(versions="First prompt")
|
|
1294
|
-
prompt2 = BasePrompt(versions="Second prompt")
|
|
1295
|
-
|
|
1296
|
-
compiled1 = prompt1.compile()
|
|
1297
|
-
compiled2 = prompt2.compile()
|
|
1298
|
-
|
|
1299
|
-
result1 = get_compiled_prompt(compiled1)
|
|
1300
|
-
result2 = get_compiled_prompt(compiled2)
|
|
1301
|
-
|
|
1302
|
-
assert result1 is not None
|
|
1303
|
-
assert result1.prompt == prompt1
|
|
1304
|
-
assert result2 is not None
|
|
1305
|
-
assert result2.prompt == prompt2
|
|
1306
|
-
|
|
1307
|
-
@pytest.mark.asyncio
|
|
1308
|
-
async def test_get_compiled_prompt_outdated_prompt(self):
|
|
1309
|
-
"""Test get_compiled_prompt handles outdated prompts correctly."""
|
|
1310
|
-
from pixie.prompts.prompt import get_compiled_prompt
|
|
1311
|
-
|
|
1312
|
-
prompt = BasePrompt(versions={"v1": "Original"})
|
|
1313
|
-
compiled_text = prompt.compile()
|
|
1314
|
-
|
|
1315
|
-
# Update the prompt to create an outdated version
|
|
1316
|
-
prompt._update(versions={"v1": "Updated"})
|
|
1317
|
-
|
|
1318
|
-
result = get_compiled_prompt(compiled_text)
|
|
1319
|
-
assert result is not None
|
|
1320
|
-
assert isinstance(result.prompt, OutdatedPrompt)
|
|
1321
|
-
assert result.value == "Original"
|