textprompts 0.0.1__tar.gz → 0.0.3__tar.gz
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.
- {textprompts-0.0.1 → textprompts-0.0.3}/PKG-INFO +32 -19
- {textprompts-0.0.1 → textprompts-0.0.3}/README.md +31 -18
- {textprompts-0.0.1 → textprompts-0.0.3}/pyproject.toml +5 -1
- {textprompts-0.0.1 → textprompts-0.0.3}/src/textprompts/__init__.py +11 -2
- textprompts-0.0.3/src/textprompts/__main__.py +8 -0
- {textprompts-0.0.1 → textprompts-0.0.3}/src/textprompts/_parser.py +31 -8
- {textprompts-0.0.1 → textprompts-0.0.3}/src/textprompts/cli.py +1 -1
- {textprompts-0.0.1 → textprompts-0.0.3}/src/textprompts/config.py +23 -1
- {textprompts-0.0.1 → textprompts-0.0.3}/src/textprompts/loaders.py +12 -4
- textprompts-0.0.3/src/textprompts/models.py +85 -0
- {textprompts-0.0.1 → textprompts-0.0.3}/src/textprompts/placeholder_utils.py +5 -2
- textprompts-0.0.3/src/textprompts/prompt_string.py +60 -0
- textprompts-0.0.3/src/textprompts/safe_string.py +5 -0
- {textprompts-0.0.1 → textprompts-0.0.3}/src/textprompts/savers.py +2 -2
- textprompts-0.0.1/src/textprompts/models.py +0 -42
- textprompts-0.0.1/src/textprompts/safe_string.py +0 -110
- {textprompts-0.0.1 → textprompts-0.0.3}/src/textprompts/errors.py +0 -0
- {textprompts-0.0.1 → textprompts-0.0.3}/src/textprompts/py.typed +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: textprompts
|
3
|
-
Version: 0.0.
|
3
|
+
Version: 0.0.3
|
4
4
|
Summary: Minimal text-based prompt-loader with TOML front-matter
|
5
5
|
Keywords: prompts,toml,frontmatter,template
|
6
6
|
Author: Jan Siml
|
@@ -24,6 +24,13 @@ Description-Content-Type: text/markdown
|
|
24
24
|
|
25
25
|
# textprompts
|
26
26
|
|
27
|
+
[](https://pypi.org/project/textprompts/)
|
28
|
+
[](https://pypi.org/project/textprompts/)
|
29
|
+
[](https://github.com/svilupp/textprompts/actions)
|
30
|
+
[](https://codecov.io/gh/svilupp/textprompts)
|
31
|
+
[](https://github.com/svilupp/textprompts/blob/main/LICENSE)
|
32
|
+
|
33
|
+
|
27
34
|
> **So simple, it's not even worth vibing about coding yet it just makes so much sense.**
|
28
35
|
|
29
36
|
Are you tired of vendors trying to sell you fancy UIs for prompt management that just make your system more confusing and harder to debug? Isn't it nice to just have your prompts **next to your code**?
|
@@ -59,7 +66,6 @@ title = "Customer Greeting"
|
|
59
66
|
version = "1.0.0"
|
60
67
|
description = "Friendly greeting for customer support"
|
61
68
|
---
|
62
|
-
|
63
69
|
Hello {customer_name}!
|
64
70
|
|
65
71
|
Welcome to {company_name}. We're here to help you with {issue_type}.
|
@@ -74,9 +80,11 @@ import textprompts
|
|
74
80
|
|
75
81
|
# Just load it - works with or without metadata
|
76
82
|
prompt = textprompts.load_prompt("greeting.txt")
|
83
|
+
# Or simply
|
84
|
+
alt = textprompts.Prompt("greeting.txt")
|
77
85
|
|
78
86
|
# Use it safely - all placeholders must be provided
|
79
|
-
message = prompt.
|
87
|
+
message = prompt.prompt.format(
|
80
88
|
customer_name="Alice",
|
81
89
|
company_name="ACME Corp",
|
82
90
|
issue_type="billing question",
|
@@ -86,19 +94,22 @@ message = prompt.body.format(
|
|
86
94
|
print(message)
|
87
95
|
|
88
96
|
# Or use partial formatting when needed
|
89
|
-
partial = prompt.
|
97
|
+
partial = prompt.prompt.format(
|
90
98
|
customer_name="Alice",
|
91
99
|
company_name="ACME Corp",
|
92
100
|
skip_validation=True
|
93
101
|
)
|
94
102
|
# Result: "Hello Alice!\n\nWelcome to ACME Corp. We're here to help you with {issue_type}.\n\nBest regards,\n{agent_name}"
|
103
|
+
|
104
|
+
# Prompt objects expose `.meta` and `.prompt`.
|
105
|
+
# Use `prompt.prompt.format()` for safe formatting or `str(prompt)` for raw text.
|
95
106
|
```
|
96
107
|
|
97
108
|
**Even simpler** - no metadata required:
|
98
109
|
```python
|
99
110
|
# simple_prompt.txt contains just: "Analyze this data: {data}"
|
100
111
|
prompt = textprompts.load_prompt("simple_prompt.txt") # Just works!
|
101
|
-
result = prompt.
|
112
|
+
result = prompt.prompt.format(data="sales figures")
|
102
113
|
```
|
103
114
|
|
104
115
|
## Core Features
|
@@ -108,9 +119,9 @@ result = prompt.body.format(data="sales figures")
|
|
108
119
|
Never ship a prompt with missing variables again:
|
109
120
|
|
110
121
|
```python
|
111
|
-
from textprompts import
|
122
|
+
from textprompts import PromptString
|
112
123
|
|
113
|
-
template =
|
124
|
+
template = PromptString("Hello {name}, your order {order_id} is {status}")
|
114
125
|
|
115
126
|
# ✅ Strict formatting - all placeholders must be provided
|
116
127
|
result = template.format(name="Alice", order_id="12345", status="shipped")
|
@@ -190,14 +201,14 @@ response = openai.chat.completions.create(
|
|
190
201
|
messages=[
|
191
202
|
{
|
192
203
|
"role": "system",
|
193
|
-
"content": system_prompt.
|
204
|
+
"content": system_prompt.prompt.format(
|
194
205
|
company_name="ACME Corp",
|
195
206
|
support_level="premium"
|
196
207
|
)
|
197
208
|
},
|
198
209
|
{
|
199
210
|
"role": "user",
|
200
|
-
"content": user_prompt.
|
211
|
+
"content": user_prompt.prompt.format(
|
201
212
|
query="How do I return an item?",
|
202
213
|
customer_tier="premium"
|
203
214
|
)
|
@@ -208,7 +219,7 @@ response = openai.chat.completions.create(
|
|
208
219
|
|
209
220
|
### Function Calling (Tool Definitions)
|
210
221
|
|
211
|
-
Yes, you can version control your
|
222
|
+
Yes, you can version control your whole tool schemas too:
|
212
223
|
|
213
224
|
```python
|
214
225
|
# tools/search_products.txt
|
@@ -217,7 +228,6 @@ title = "Product Search Tool"
|
|
217
228
|
version = "2.1.0"
|
218
229
|
description = "Search our product catalog"
|
219
230
|
---
|
220
|
-
|
221
231
|
{
|
222
232
|
"type": "function",
|
223
233
|
"function": {
|
@@ -253,7 +263,7 @@ from textprompts import load_prompt
|
|
253
263
|
|
254
264
|
# Load and parse the tool definition
|
255
265
|
tool_prompt = load_prompt("tools/search_products.txt")
|
256
|
-
tool_schema = json.loads(tool_prompt.
|
266
|
+
tool_schema = json.loads(tool_prompt.prompt)
|
257
267
|
|
258
268
|
# Use with OpenAI
|
259
269
|
response = openai.chat.completions.create(
|
@@ -303,7 +313,6 @@ description = "What this prompt does"
|
|
303
313
|
created = "2024-01-15"
|
304
314
|
tags = ["customer-support", "greeting"]
|
305
315
|
---
|
306
|
-
|
307
316
|
Your prompt content goes here.
|
308
317
|
|
309
318
|
Use {variables} for templating.
|
@@ -317,6 +326,10 @@ Choose the right level of strictness for your use case:
|
|
317
326
|
2. **ALLOW** - Load metadata if present, don't worry about completeness
|
318
327
|
3. **STRICT** - Require complete metadata (title, description, version) for production safety
|
319
328
|
|
329
|
+
You can also set the environment variable `TEXTPROMPTS_METADATA_MODE` to one of
|
330
|
+
`strict`, `allow`, or `ignore` before importing the library to configure the
|
331
|
+
default mode.
|
332
|
+
|
320
333
|
```python
|
321
334
|
# Set globally
|
322
335
|
textprompts.set_metadata("ignore") # Default: simple file loading
|
@@ -338,7 +351,7 @@ Load a single prompt file.
|
|
338
351
|
|
339
352
|
Returns a `Prompt` object with:
|
340
353
|
- `prompt.meta`: Metadata from TOML front-matter (always present)
|
341
|
-
- `prompt.
|
354
|
+
- `prompt.prompt`: The prompt content as a `PromptString`
|
342
355
|
- `prompt.path`: Path to the original file
|
343
356
|
|
344
357
|
### `load_prompts(*paths, recursive=False, glob="*.txt", meta=None, max_files=1000)`
|
@@ -385,14 +398,14 @@ save_prompt("my_prompt.txt", "You are a helpful assistant.")
|
|
385
398
|
save_prompt("my_prompt.txt", prompt_object)
|
386
399
|
```
|
387
400
|
|
388
|
-
### `
|
401
|
+
### `PromptString`
|
389
402
|
|
390
403
|
A string subclass that validates `format()` calls:
|
391
404
|
|
392
405
|
```python
|
393
|
-
from textprompts import
|
406
|
+
from textprompts import PromptString
|
394
407
|
|
395
|
-
template =
|
408
|
+
template = PromptString("Hello {name}, you are {role}")
|
396
409
|
|
397
410
|
# Strict formatting (default) - all placeholders required
|
398
411
|
result = template.format(name="Alice", role="admin") # ✅ Works
|
@@ -460,8 +473,8 @@ textprompts validate prompts/
|
|
460
473
|
4. **Test your prompts**: Write unit tests for critical prompts
|
461
474
|
```python
|
462
475
|
def test_greeting_prompt():
|
463
|
-
|
464
|
-
|
476
|
+
prompt = load_prompt("greeting.txt")
|
477
|
+
result = prompt.prompt.format(customer_name="Test")
|
465
478
|
assert "Test" in result
|
466
479
|
```
|
467
480
|
|
@@ -1,5 +1,12 @@
|
|
1
1
|
# textprompts
|
2
2
|
|
3
|
+
[](https://pypi.org/project/textprompts/)
|
4
|
+
[](https://pypi.org/project/textprompts/)
|
5
|
+
[](https://github.com/svilupp/textprompts/actions)
|
6
|
+
[](https://codecov.io/gh/svilupp/textprompts)
|
7
|
+
[](https://github.com/svilupp/textprompts/blob/main/LICENSE)
|
8
|
+
|
9
|
+
|
3
10
|
> **So simple, it's not even worth vibing about coding yet it just makes so much sense.**
|
4
11
|
|
5
12
|
Are you tired of vendors trying to sell you fancy UIs for prompt management that just make your system more confusing and harder to debug? Isn't it nice to just have your prompts **next to your code**?
|
@@ -35,7 +42,6 @@ title = "Customer Greeting"
|
|
35
42
|
version = "1.0.0"
|
36
43
|
description = "Friendly greeting for customer support"
|
37
44
|
---
|
38
|
-
|
39
45
|
Hello {customer_name}!
|
40
46
|
|
41
47
|
Welcome to {company_name}. We're here to help you with {issue_type}.
|
@@ -50,9 +56,11 @@ import textprompts
|
|
50
56
|
|
51
57
|
# Just load it - works with or without metadata
|
52
58
|
prompt = textprompts.load_prompt("greeting.txt")
|
59
|
+
# Or simply
|
60
|
+
alt = textprompts.Prompt("greeting.txt")
|
53
61
|
|
54
62
|
# Use it safely - all placeholders must be provided
|
55
|
-
message = prompt.
|
63
|
+
message = prompt.prompt.format(
|
56
64
|
customer_name="Alice",
|
57
65
|
company_name="ACME Corp",
|
58
66
|
issue_type="billing question",
|
@@ -62,19 +70,22 @@ message = prompt.body.format(
|
|
62
70
|
print(message)
|
63
71
|
|
64
72
|
# Or use partial formatting when needed
|
65
|
-
partial = prompt.
|
73
|
+
partial = prompt.prompt.format(
|
66
74
|
customer_name="Alice",
|
67
75
|
company_name="ACME Corp",
|
68
76
|
skip_validation=True
|
69
77
|
)
|
70
78
|
# Result: "Hello Alice!\n\nWelcome to ACME Corp. We're here to help you with {issue_type}.\n\nBest regards,\n{agent_name}"
|
79
|
+
|
80
|
+
# Prompt objects expose `.meta` and `.prompt`.
|
81
|
+
# Use `prompt.prompt.format()` for safe formatting or `str(prompt)` for raw text.
|
71
82
|
```
|
72
83
|
|
73
84
|
**Even simpler** - no metadata required:
|
74
85
|
```python
|
75
86
|
# simple_prompt.txt contains just: "Analyze this data: {data}"
|
76
87
|
prompt = textprompts.load_prompt("simple_prompt.txt") # Just works!
|
77
|
-
result = prompt.
|
88
|
+
result = prompt.prompt.format(data="sales figures")
|
78
89
|
```
|
79
90
|
|
80
91
|
## Core Features
|
@@ -84,9 +95,9 @@ result = prompt.body.format(data="sales figures")
|
|
84
95
|
Never ship a prompt with missing variables again:
|
85
96
|
|
86
97
|
```python
|
87
|
-
from textprompts import
|
98
|
+
from textprompts import PromptString
|
88
99
|
|
89
|
-
template =
|
100
|
+
template = PromptString("Hello {name}, your order {order_id} is {status}")
|
90
101
|
|
91
102
|
# ✅ Strict formatting - all placeholders must be provided
|
92
103
|
result = template.format(name="Alice", order_id="12345", status="shipped")
|
@@ -166,14 +177,14 @@ response = openai.chat.completions.create(
|
|
166
177
|
messages=[
|
167
178
|
{
|
168
179
|
"role": "system",
|
169
|
-
"content": system_prompt.
|
180
|
+
"content": system_prompt.prompt.format(
|
170
181
|
company_name="ACME Corp",
|
171
182
|
support_level="premium"
|
172
183
|
)
|
173
184
|
},
|
174
185
|
{
|
175
186
|
"role": "user",
|
176
|
-
"content": user_prompt.
|
187
|
+
"content": user_prompt.prompt.format(
|
177
188
|
query="How do I return an item?",
|
178
189
|
customer_tier="premium"
|
179
190
|
)
|
@@ -184,7 +195,7 @@ response = openai.chat.completions.create(
|
|
184
195
|
|
185
196
|
### Function Calling (Tool Definitions)
|
186
197
|
|
187
|
-
Yes, you can version control your
|
198
|
+
Yes, you can version control your whole tool schemas too:
|
188
199
|
|
189
200
|
```python
|
190
201
|
# tools/search_products.txt
|
@@ -193,7 +204,6 @@ title = "Product Search Tool"
|
|
193
204
|
version = "2.1.0"
|
194
205
|
description = "Search our product catalog"
|
195
206
|
---
|
196
|
-
|
197
207
|
{
|
198
208
|
"type": "function",
|
199
209
|
"function": {
|
@@ -229,7 +239,7 @@ from textprompts import load_prompt
|
|
229
239
|
|
230
240
|
# Load and parse the tool definition
|
231
241
|
tool_prompt = load_prompt("tools/search_products.txt")
|
232
|
-
tool_schema = json.loads(tool_prompt.
|
242
|
+
tool_schema = json.loads(tool_prompt.prompt)
|
233
243
|
|
234
244
|
# Use with OpenAI
|
235
245
|
response = openai.chat.completions.create(
|
@@ -279,7 +289,6 @@ description = "What this prompt does"
|
|
279
289
|
created = "2024-01-15"
|
280
290
|
tags = ["customer-support", "greeting"]
|
281
291
|
---
|
282
|
-
|
283
292
|
Your prompt content goes here.
|
284
293
|
|
285
294
|
Use {variables} for templating.
|
@@ -293,6 +302,10 @@ Choose the right level of strictness for your use case:
|
|
293
302
|
2. **ALLOW** - Load metadata if present, don't worry about completeness
|
294
303
|
3. **STRICT** - Require complete metadata (title, description, version) for production safety
|
295
304
|
|
305
|
+
You can also set the environment variable `TEXTPROMPTS_METADATA_MODE` to one of
|
306
|
+
`strict`, `allow`, or `ignore` before importing the library to configure the
|
307
|
+
default mode.
|
308
|
+
|
296
309
|
```python
|
297
310
|
# Set globally
|
298
311
|
textprompts.set_metadata("ignore") # Default: simple file loading
|
@@ -314,7 +327,7 @@ Load a single prompt file.
|
|
314
327
|
|
315
328
|
Returns a `Prompt` object with:
|
316
329
|
- `prompt.meta`: Metadata from TOML front-matter (always present)
|
317
|
-
- `prompt.
|
330
|
+
- `prompt.prompt`: The prompt content as a `PromptString`
|
318
331
|
- `prompt.path`: Path to the original file
|
319
332
|
|
320
333
|
### `load_prompts(*paths, recursive=False, glob="*.txt", meta=None, max_files=1000)`
|
@@ -361,14 +374,14 @@ save_prompt("my_prompt.txt", "You are a helpful assistant.")
|
|
361
374
|
save_prompt("my_prompt.txt", prompt_object)
|
362
375
|
```
|
363
376
|
|
364
|
-
### `
|
377
|
+
### `PromptString`
|
365
378
|
|
366
379
|
A string subclass that validates `format()` calls:
|
367
380
|
|
368
381
|
```python
|
369
|
-
from textprompts import
|
382
|
+
from textprompts import PromptString
|
370
383
|
|
371
|
-
template =
|
384
|
+
template = PromptString("Hello {name}, you are {role}")
|
372
385
|
|
373
386
|
# Strict formatting (default) - all placeholders required
|
374
387
|
result = template.format(name="Alice", role="admin") # ✅ Works
|
@@ -436,8 +449,8 @@ textprompts validate prompts/
|
|
436
449
|
4. **Test your prompts**: Write unit tests for critical prompts
|
437
450
|
```python
|
438
451
|
def test_greeting_prompt():
|
439
|
-
|
440
|
-
|
452
|
+
prompt = load_prompt("greeting.txt")
|
453
|
+
result = prompt.prompt.format(customer_name="Test")
|
441
454
|
assert "Test" in result
|
442
455
|
```
|
443
456
|
|
@@ -1,9 +1,10 @@
|
|
1
1
|
[project]
|
2
2
|
name = "textprompts"
|
3
|
-
version = "0.0.
|
3
|
+
version = "0.0.3"
|
4
4
|
description = "Minimal text-based prompt-loader with TOML front-matter"
|
5
5
|
readme = "README.md"
|
6
6
|
license = "MIT"
|
7
|
+
license-file = "LICENSE"
|
7
8
|
authors = [
|
8
9
|
{name = "Jan Siml", email = "49557684+svilupp@users.noreply.github.com"},
|
9
10
|
]
|
@@ -37,6 +38,9 @@ dev = [
|
|
37
38
|
"ruff>=0.12.2",
|
38
39
|
"pre-commit>=3.0.0",
|
39
40
|
]
|
41
|
+
test = [
|
42
|
+
"pydantic-ai>=0.4.5",
|
43
|
+
]
|
40
44
|
|
41
45
|
[build-system]
|
42
46
|
requires = ["uv_build>=0.7.19,<0.8.0"]
|
@@ -1,4 +1,10 @@
|
|
1
|
-
from .config import
|
1
|
+
from .config import (
|
2
|
+
MetadataMode,
|
3
|
+
get_metadata,
|
4
|
+
set_metadata,
|
5
|
+
skip_metadata,
|
6
|
+
warn_on_ignored_metadata,
|
7
|
+
)
|
2
8
|
from .errors import (
|
3
9
|
FileMissingError,
|
4
10
|
InvalidMetadataError,
|
@@ -8,7 +14,7 @@ from .errors import (
|
|
8
14
|
)
|
9
15
|
from .loaders import load_prompt, load_prompts
|
10
16
|
from .models import Prompt, PromptMeta
|
11
|
-
from .
|
17
|
+
from .prompt_string import PromptString, SafeString
|
12
18
|
from .savers import save_prompt
|
13
19
|
|
14
20
|
__all__ = [
|
@@ -17,10 +23,13 @@ __all__ = [
|
|
17
23
|
"save_prompt",
|
18
24
|
"Prompt",
|
19
25
|
"PromptMeta",
|
26
|
+
"PromptString",
|
20
27
|
"SafeString",
|
21
28
|
"MetadataMode",
|
22
29
|
"set_metadata",
|
23
30
|
"get_metadata",
|
31
|
+
"skip_metadata",
|
32
|
+
"warn_on_ignored_metadata",
|
24
33
|
"TextPromptsError",
|
25
34
|
"FileMissingError",
|
26
35
|
"MissingMetadataError",
|
@@ -0,0 +1,8 @@
|
|
1
|
+
"""Command-line entry point for ``python -m textprompts``.""" # pragma: no cover
|
2
|
+
|
3
|
+
from textprompts.cli import main # pragma: no cover
|
4
|
+
|
5
|
+
__all__ = ["main"] # pragma: no cover
|
6
|
+
|
7
|
+
if __name__ == "__main__": # pragma: no cover - small entrypoint wrapper
|
8
|
+
main() # pragma: no cover
|
@@ -4,13 +4,13 @@ from typing import Optional
|
|
4
4
|
|
5
5
|
try:
|
6
6
|
import tomllib
|
7
|
-
except ImportError:
|
7
|
+
except ImportError: # pragma: no cover - Python <3.11 fallback
|
8
8
|
import tomli as tomllib # type: ignore[import-not-found, no-redef]
|
9
9
|
|
10
|
-
from .config import MetadataMode
|
10
|
+
from .config import MetadataMode, warn_on_ignored_metadata
|
11
11
|
from .errors import InvalidMetadataError, MalformedHeaderError, MissingMetadataError
|
12
12
|
from .models import Prompt, PromptMeta
|
13
|
-
from .
|
13
|
+
from .prompt_string import PromptString
|
14
14
|
|
15
15
|
DELIM = "---"
|
16
16
|
|
@@ -59,8 +59,25 @@ def parse_file(path: Path, *, metadata_mode: MetadataMode) -> Prompt:
|
|
59
59
|
|
60
60
|
# Handle IGNORE mode - treat entire file as body
|
61
61
|
if metadata_mode == MetadataMode.IGNORE:
|
62
|
+
if (
|
63
|
+
warn_on_ignored_metadata()
|
64
|
+
and raw.startswith(DELIM)
|
65
|
+
and raw.find(DELIM, len(DELIM)) != -1
|
66
|
+
):
|
67
|
+
import warnings
|
68
|
+
|
69
|
+
warnings.warn(
|
70
|
+
"Metadata detected but ignored; use set_metadata('allow') or skip_metadata(skip_warning=True) to silence",
|
71
|
+
stacklevel=2,
|
72
|
+
)
|
62
73
|
ignore_meta = PromptMeta(title=path.stem)
|
63
|
-
return Prompt
|
74
|
+
return Prompt.model_validate(
|
75
|
+
{
|
76
|
+
"path": path,
|
77
|
+
"meta": ignore_meta,
|
78
|
+
"prompt": PromptString(textwrap.dedent(raw)),
|
79
|
+
}
|
80
|
+
)
|
64
81
|
|
65
82
|
# For STRICT and ALLOW modes, try to parse front matter
|
66
83
|
try:
|
@@ -72,7 +89,7 @@ def parse_file(path: Path, *, metadata_mode: MetadataMode) -> Prompt:
|
|
72
89
|
f"{e}. If this file has no metadata and starts with '---', "
|
73
90
|
f"use meta=MetadataMode.IGNORE to skip metadata parsing."
|
74
91
|
) from e
|
75
|
-
raise
|
92
|
+
raise # pragma: no cover - reraised to preserve stack
|
76
93
|
|
77
94
|
meta: Optional[PromptMeta] = None
|
78
95
|
if header_txt is not None:
|
@@ -114,7 +131,7 @@ def parse_file(path: Path, *, metadata_mode: MetadataMode) -> Prompt:
|
|
114
131
|
) from e
|
115
132
|
except InvalidMetadataError:
|
116
133
|
raise
|
117
|
-
except Exception as e:
|
134
|
+
except Exception as e: # pragma: no cover - unlikely generic error
|
118
135
|
raise InvalidMetadataError(f"Invalid metadata: {e}") from e
|
119
136
|
|
120
137
|
else:
|
@@ -129,11 +146,17 @@ def parse_file(path: Path, *, metadata_mode: MetadataMode) -> Prompt:
|
|
129
146
|
meta = PromptMeta()
|
130
147
|
|
131
148
|
# Always ensure we have metadata with a title
|
132
|
-
if not meta:
|
149
|
+
if not meta: # pragma: no cover - meta is never falsy but kept for safety
|
133
150
|
meta = PromptMeta()
|
134
151
|
|
135
152
|
# Use filename as title if not provided
|
136
153
|
if meta.title is None:
|
137
154
|
meta.title = path.stem
|
138
155
|
|
139
|
-
return Prompt
|
156
|
+
return Prompt.model_validate(
|
157
|
+
{
|
158
|
+
"path": path,
|
159
|
+
"meta": meta,
|
160
|
+
"prompt": PromptString(textwrap.dedent(body)),
|
161
|
+
}
|
162
|
+
)
|
@@ -2,6 +2,7 @@
|
|
2
2
|
Global configuration for textprompts metadata handling.
|
3
3
|
"""
|
4
4
|
|
5
|
+
import os
|
5
6
|
from enum import Enum
|
6
7
|
from typing import Union
|
7
8
|
|
@@ -29,7 +30,14 @@ class MetadataMode(Enum):
|
|
29
30
|
|
30
31
|
|
31
32
|
# Global configuration variable
|
32
|
-
|
33
|
+
_env_mode = os.getenv("TEXTPROMPTS_METADATA_MODE")
|
34
|
+
try:
|
35
|
+
_METADATA_MODE: MetadataMode = (
|
36
|
+
MetadataMode(_env_mode.lower()) if _env_mode else MetadataMode.IGNORE
|
37
|
+
)
|
38
|
+
except ValueError:
|
39
|
+
_METADATA_MODE = MetadataMode.IGNORE
|
40
|
+
_WARN_ON_IGNORED_META: bool = True
|
33
41
|
|
34
42
|
|
35
43
|
def set_metadata(mode: Union[MetadataMode, str]) -> None:
|
@@ -82,6 +90,20 @@ def get_metadata() -> MetadataMode:
|
|
82
90
|
return _METADATA_MODE
|
83
91
|
|
84
92
|
|
93
|
+
def skip_metadata(*, skip_warning: bool = False) -> None:
|
94
|
+
"""Convenience setter for ignoring metadata with optional warnings."""
|
95
|
+
|
96
|
+
global _WARN_ON_IGNORED_META
|
97
|
+
_WARN_ON_IGNORED_META = not skip_warning # pragma: no cover
|
98
|
+
set_metadata(MetadataMode.IGNORE) # pragma: no cover - simple wrapper
|
99
|
+
|
100
|
+
|
101
|
+
def warn_on_ignored_metadata() -> bool:
|
102
|
+
"""Return whether warnings for ignored metadata are enabled."""
|
103
|
+
|
104
|
+
return _WARN_ON_IGNORED_META
|
105
|
+
|
106
|
+
|
85
107
|
def _resolve_metadata_mode(meta: Union[MetadataMode, str, None]) -> MetadataMode:
|
86
108
|
"""
|
87
109
|
Resolve the metadata mode from parameters and global config.
|
@@ -96,17 +96,25 @@ def load_prompts(
|
|
96
96
|
if pth.is_dir():
|
97
97
|
itr: Iterable[Path] = pth.rglob(glob) if recursive else pth.glob(glob)
|
98
98
|
for f in itr:
|
99
|
-
if
|
99
|
+
if (
|
100
|
+
max_files and file_count >= max_files
|
101
|
+
): # pragma: no cover - boundary check
|
100
102
|
from .errors import TextPromptsError
|
101
103
|
|
102
|
-
raise TextPromptsError(
|
104
|
+
raise TextPromptsError(
|
105
|
+
f"Exceeded max_files limit of {max_files}"
|
106
|
+
) # pragma: no cover - boundary check
|
103
107
|
collected.append(load_prompt(f, meta=meta))
|
104
108
|
file_count += 1
|
105
109
|
else:
|
106
|
-
if
|
110
|
+
if (
|
111
|
+
max_files and file_count >= max_files
|
112
|
+
): # pragma: no cover - boundary check
|
107
113
|
from .errors import TextPromptsError
|
108
114
|
|
109
|
-
raise TextPromptsError(
|
115
|
+
raise TextPromptsError(
|
116
|
+
f"Exceeded max_files limit of {max_files}"
|
117
|
+
) # pragma: no cover - boundary check
|
110
118
|
collected.append(load_prompt(pth, meta=meta))
|
111
119
|
file_count += 1
|
112
120
|
|
@@ -0,0 +1,85 @@
|
|
1
|
+
from datetime import date
|
2
|
+
from pathlib import Path
|
3
|
+
from typing import Any, Union
|
4
|
+
|
5
|
+
from pydantic import BaseModel, Field, field_validator
|
6
|
+
|
7
|
+
from .config import MetadataMode
|
8
|
+
from .prompt_string import PromptString
|
9
|
+
|
10
|
+
|
11
|
+
class PromptMeta(BaseModel):
|
12
|
+
title: Union[str, None] = Field(default=None, description="Human-readable name")
|
13
|
+
version: Union[str, None] = Field(default=None)
|
14
|
+
author: Union[str, None] = Field(default=None)
|
15
|
+
created: Union[date, None] = Field(default=None)
|
16
|
+
description: Union[str, None] = Field(default=None)
|
17
|
+
|
18
|
+
|
19
|
+
class Prompt(BaseModel):
|
20
|
+
path: Path
|
21
|
+
meta: Union[PromptMeta, None]
|
22
|
+
prompt: PromptString
|
23
|
+
|
24
|
+
def __init__(
|
25
|
+
self,
|
26
|
+
path: Union[str, Path],
|
27
|
+
meta: Union[PromptMeta, MetadataMode, str, None] = None,
|
28
|
+
prompt: Union[str, PromptString, None] = None,
|
29
|
+
) -> None:
|
30
|
+
"""Initialize Prompt from fields or load from file."""
|
31
|
+
if prompt is None:
|
32
|
+
from .loaders import load_prompt
|
33
|
+
|
34
|
+
loaded = load_prompt(path, meta=meta)
|
35
|
+
super().__init__(**loaded.model_dump())
|
36
|
+
else:
|
37
|
+
if isinstance(prompt, str):
|
38
|
+
prompt = PromptString(prompt)
|
39
|
+
super().__init__(path=Path(path), meta=meta, prompt=prompt)
|
40
|
+
|
41
|
+
@field_validator("prompt")
|
42
|
+
@classmethod
|
43
|
+
def prompt_not_empty(cls, v: str) -> PromptString:
|
44
|
+
if not v.strip():
|
45
|
+
raise ValueError("Prompt body is empty")
|
46
|
+
return PromptString(v)
|
47
|
+
|
48
|
+
def __repr__(self) -> str:
|
49
|
+
if self.meta and self.meta.title:
|
50
|
+
if self.meta.version:
|
51
|
+
return (
|
52
|
+
f"Prompt(title='{self.meta.title}', version='{self.meta.version}')"
|
53
|
+
" # use .format() or str()"
|
54
|
+
)
|
55
|
+
return f"Prompt(title='{self.meta.title}') # use .format() or str()"
|
56
|
+
return f"Prompt(path='{self.path}') # use .format() or str()"
|
57
|
+
|
58
|
+
def __str__(self) -> str:
|
59
|
+
return str(self.prompt)
|
60
|
+
|
61
|
+
@property
|
62
|
+
def body(self) -> PromptString:
|
63
|
+
import warnings
|
64
|
+
|
65
|
+
warnings.warn(
|
66
|
+
"Prompt.body is deprecated; use .prompt instead",
|
67
|
+
DeprecationWarning,
|
68
|
+
stacklevel=2,
|
69
|
+
)
|
70
|
+
return self.prompt
|
71
|
+
|
72
|
+
def __len__(self) -> int:
|
73
|
+
return len(self.prompt)
|
74
|
+
|
75
|
+
def __getitem__(self, item: int | slice) -> str:
|
76
|
+
return self.prompt[item]
|
77
|
+
|
78
|
+
def __add__(self, other: str) -> str:
|
79
|
+
return str(self.prompt) + str(other)
|
80
|
+
|
81
|
+
def strip(self, *args: Any, **kwargs: Any) -> str:
|
82
|
+
return self.prompt.strip(*args, **kwargs)
|
83
|
+
|
84
|
+
def format(self, *args: Any, **kwargs: Any) -> str:
|
85
|
+
return self.prompt.format(*args, **kwargs)
|
@@ -1,7 +1,7 @@
|
|
1
1
|
"""
|
2
2
|
Utility functions for extracting and validating placeholders in format strings.
|
3
3
|
|
4
|
-
This module provides robust placeholder extraction and validation for
|
4
|
+
This module provides robust placeholder extraction and validation for PromptString
|
5
5
|
formatting operations.
|
6
6
|
"""
|
7
7
|
|
@@ -53,7 +53,10 @@ def extract_placeholders(text: str) -> Set[str]:
|
|
53
53
|
|
54
54
|
|
55
55
|
def validate_format_args(
|
56
|
-
placeholders: Set[str],
|
56
|
+
placeholders: Set[str],
|
57
|
+
args: Tuple[Any, ...],
|
58
|
+
kwargs: Dict[str, Any],
|
59
|
+
skip_validation: bool = False,
|
57
60
|
) -> None:
|
58
61
|
"""
|
59
62
|
Validate that format arguments match the placeholders in the template.
|
@@ -0,0 +1,60 @@
|
|
1
|
+
from typing import Any, Set
|
2
|
+
|
3
|
+
from pydantic import GetCoreSchemaHandler
|
4
|
+
from pydantic_core import core_schema
|
5
|
+
|
6
|
+
from .placeholder_utils import extract_placeholders, validate_format_args
|
7
|
+
|
8
|
+
|
9
|
+
class PromptString(str):
|
10
|
+
"""String subclass that validates ``format()`` calls."""
|
11
|
+
|
12
|
+
placeholders: Set[str]
|
13
|
+
|
14
|
+
def __new__(cls, value: str) -> "PromptString":
|
15
|
+
instance = str.__new__(cls, value)
|
16
|
+
instance.placeholders = extract_placeholders(value)
|
17
|
+
return instance
|
18
|
+
|
19
|
+
def format(self, *args: Any, **kwargs: Any) -> str:
|
20
|
+
"""Format with validation and optional partial formatting."""
|
21
|
+
skip_validation = kwargs.pop("skip_validation", False)
|
22
|
+
source = str(self).strip()
|
23
|
+
if skip_validation:
|
24
|
+
return self._partial_format(*args, source=source, **kwargs)
|
25
|
+
validate_format_args(self.placeholders, args, kwargs, skip_validation=False)
|
26
|
+
return str.format(source, *args, **kwargs)
|
27
|
+
|
28
|
+
def _partial_format(
|
29
|
+
self, *args: Any, source: str | None = None, **kwargs: Any
|
30
|
+
) -> str:
|
31
|
+
"""Partial formatting - replace placeholders that have values."""
|
32
|
+
all_kwargs = kwargs.copy()
|
33
|
+
for i, arg in enumerate(args):
|
34
|
+
all_kwargs[str(i)] = arg
|
35
|
+
|
36
|
+
result = source if source is not None else str(self)
|
37
|
+
for placeholder in self.placeholders:
|
38
|
+
if placeholder in all_kwargs:
|
39
|
+
pattern = f"{{{placeholder}}}"
|
40
|
+
if pattern in result:
|
41
|
+
try:
|
42
|
+
result = result.replace(pattern, str(all_kwargs[placeholder]))
|
43
|
+
except (KeyError, ValueError): # pragma: no cover - defensive
|
44
|
+
pass
|
45
|
+
return result
|
46
|
+
|
47
|
+
def __repr__(self) -> str:
|
48
|
+
return f"PromptString({str.__repr__(self)}, placeholders={self.placeholders})"
|
49
|
+
|
50
|
+
@classmethod
|
51
|
+
def __get_pydantic_core_schema__(
|
52
|
+
cls, source_type: Any, handler: GetCoreSchemaHandler
|
53
|
+
) -> core_schema.CoreSchema:
|
54
|
+
return core_schema.no_info_after_validator_function(
|
55
|
+
cls, core_schema.str_schema()
|
56
|
+
)
|
57
|
+
|
58
|
+
|
59
|
+
# Backwards compatibility alias
|
60
|
+
SafeString = PromptString
|
@@ -25,7 +25,7 @@ def save_prompt(path: Union[str, Path], content: Union[str, Prompt]) -> None:
|
|
25
25
|
>>> prompt = Prompt(
|
26
26
|
... path=Path("my_prompt.txt"),
|
27
27
|
... meta=PromptMeta(title="Assistant", version="1.0.0", description="A helpful AI"),
|
28
|
-
...
|
28
|
+
... prompt="You are a helpful assistant."
|
29
29
|
... )
|
30
30
|
>>> save_prompt("my_prompt.txt", prompt)
|
31
31
|
"""
|
@@ -59,7 +59,7 @@ version = ""
|
|
59
59
|
|
60
60
|
lines.append("---")
|
61
61
|
lines.append("")
|
62
|
-
lines.append(str(content.
|
62
|
+
lines.append(str(content.prompt))
|
63
63
|
|
64
64
|
path.write_text("\n".join(lines), encoding="utf-8")
|
65
65
|
else:
|
@@ -1,42 +0,0 @@
|
|
1
|
-
from datetime import date
|
2
|
-
from pathlib import Path
|
3
|
-
from typing import Union
|
4
|
-
|
5
|
-
from pydantic import BaseModel, Field, field_validator
|
6
|
-
|
7
|
-
from .safe_string import SafeString
|
8
|
-
|
9
|
-
|
10
|
-
class PromptMeta(BaseModel):
|
11
|
-
title: Union[str, None] = Field(default=None, description="Human-readable name")
|
12
|
-
version: Union[str, None] = Field(default=None)
|
13
|
-
author: Union[str, None] = Field(default=None)
|
14
|
-
created: Union[date, None] = Field(default=None)
|
15
|
-
description: Union[str, None] = Field(default=None)
|
16
|
-
|
17
|
-
|
18
|
-
class Prompt(BaseModel):
|
19
|
-
path: Path
|
20
|
-
meta: Union[PromptMeta, None]
|
21
|
-
body: SafeString
|
22
|
-
|
23
|
-
@field_validator("body")
|
24
|
-
@classmethod
|
25
|
-
def body_not_empty(cls, v: str) -> SafeString:
|
26
|
-
if not v.strip():
|
27
|
-
raise ValueError("Prompt body is empty")
|
28
|
-
return SafeString(v)
|
29
|
-
|
30
|
-
def __repr__(self) -> str:
|
31
|
-
if self.meta and self.meta.title:
|
32
|
-
if self.meta.version:
|
33
|
-
return (
|
34
|
-
f"Prompt(title='{self.meta.title}', version='{self.meta.version}')"
|
35
|
-
)
|
36
|
-
else:
|
37
|
-
return f"Prompt(title='{self.meta.title}')"
|
38
|
-
else:
|
39
|
-
return f"Prompt(path='{self.path}')"
|
40
|
-
|
41
|
-
def __str__(self) -> str:
|
42
|
-
return str(self.body)
|
@@ -1,110 +0,0 @@
|
|
1
|
-
from typing import Any, Set
|
2
|
-
|
3
|
-
from pydantic import GetCoreSchemaHandler
|
4
|
-
from pydantic_core import core_schema
|
5
|
-
|
6
|
-
from .placeholder_utils import extract_placeholders, validate_format_args
|
7
|
-
|
8
|
-
|
9
|
-
class SafeString(str):
|
10
|
-
"""
|
11
|
-
A string subclass that validates format() calls to ensure all placeholders are provided.
|
12
|
-
|
13
|
-
This prevents common errors where format variables are missing, making prompt templates
|
14
|
-
more reliable and easier to debug.
|
15
|
-
|
16
|
-
Attributes:
|
17
|
-
placeholders: Set of placeholder names found in the string
|
18
|
-
"""
|
19
|
-
|
20
|
-
placeholders: Set[str]
|
21
|
-
|
22
|
-
def __new__(cls, value: str) -> "SafeString":
|
23
|
-
"""Create a new SafeString instance with extracted placeholders."""
|
24
|
-
instance = str.__new__(cls, value)
|
25
|
-
instance.placeholders = extract_placeholders(value)
|
26
|
-
return instance
|
27
|
-
|
28
|
-
def format(self, *args: Any, **kwargs: Any) -> str:
|
29
|
-
"""
|
30
|
-
Format the string with configurable validation behavior.
|
31
|
-
|
32
|
-
By default (skip_validation=False), this method validates that all placeholders
|
33
|
-
have corresponding values and raises ValueError if any are missing.
|
34
|
-
|
35
|
-
When skip_validation=True, it performs partial formatting, replacing only
|
36
|
-
the placeholders for which values are provided.
|
37
|
-
|
38
|
-
Args:
|
39
|
-
*args: Positional arguments for formatting
|
40
|
-
skip_validation: If True, perform partial formatting without validation
|
41
|
-
**kwargs: Keyword arguments for formatting
|
42
|
-
|
43
|
-
Returns:
|
44
|
-
The formatted string
|
45
|
-
|
46
|
-
Raises:
|
47
|
-
ValueError: If skip_validation=False and any placeholder is missing
|
48
|
-
"""
|
49
|
-
skip_validation = kwargs.pop('skip_validation', False)
|
50
|
-
if skip_validation:
|
51
|
-
# Partial formatting - replace only available placeholders
|
52
|
-
return self._partial_format(*args, **kwargs)
|
53
|
-
else:
|
54
|
-
# Strict formatting - validate all placeholders are provided
|
55
|
-
validate_format_args(self.placeholders, args, kwargs, skip_validation=False)
|
56
|
-
return str.format(self, *args, **kwargs)
|
57
|
-
|
58
|
-
def _partial_format(self, *args: Any, **kwargs: Any) -> str:
|
59
|
-
"""
|
60
|
-
Perform partial formatting, replacing only the placeholders that have values.
|
61
|
-
|
62
|
-
Args:
|
63
|
-
*args: Positional arguments for formatting
|
64
|
-
**kwargs: Keyword arguments for formatting
|
65
|
-
|
66
|
-
Returns:
|
67
|
-
The partially formatted string
|
68
|
-
"""
|
69
|
-
# Convert positional args to keyword args
|
70
|
-
all_kwargs = kwargs.copy()
|
71
|
-
for i, arg in enumerate(args):
|
72
|
-
all_kwargs[str(i)] = arg
|
73
|
-
|
74
|
-
# Build a format string with only available placeholders
|
75
|
-
result = str(self)
|
76
|
-
|
77
|
-
# Replace placeholders one by one if they have values
|
78
|
-
for placeholder in self.placeholders:
|
79
|
-
if placeholder in all_kwargs:
|
80
|
-
# Create a single-placeholder format string
|
81
|
-
placeholder_pattern = f"{{{placeholder}}}"
|
82
|
-
if placeholder_pattern in result:
|
83
|
-
try:
|
84
|
-
# Replace this specific placeholder
|
85
|
-
result = result.replace(
|
86
|
-
placeholder_pattern, str(all_kwargs[placeholder])
|
87
|
-
)
|
88
|
-
except (KeyError, ValueError):
|
89
|
-
# If replacement fails, leave the placeholder as is
|
90
|
-
pass
|
91
|
-
|
92
|
-
return result
|
93
|
-
|
94
|
-
def __str__(self) -> str:
|
95
|
-
"""Return the string representation."""
|
96
|
-
return str.__str__(self)
|
97
|
-
|
98
|
-
def __repr__(self) -> str:
|
99
|
-
"""Return the string representation for debugging."""
|
100
|
-
return f"SafeString({str.__repr__(self)}, placeholders={self.placeholders})"
|
101
|
-
|
102
|
-
@classmethod
|
103
|
-
def __get_pydantic_core_schema__(
|
104
|
-
cls, source_type: Any, handler: GetCoreSchemaHandler
|
105
|
-
) -> core_schema.CoreSchema:
|
106
|
-
"""Support for Pydantic v2 schema generation."""
|
107
|
-
return core_schema.no_info_after_validator_function(
|
108
|
-
cls,
|
109
|
-
core_schema.str_schema(),
|
110
|
-
)
|
File without changes
|
File without changes
|