ai-prompter 0.1.1__py3-none-any.whl → 0.2.1__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.
- ai_prompter/__init__.py +127 -61
- ai_prompter/py.typed +1 -0
- {ai_prompter-0.1.1.dist-info → ai_prompter-0.2.1.dist-info}/METADATA +78 -18
- ai_prompter-0.2.1.dist-info/RECORD +6 -0
- ai_prompter-0.1.1.dist-info/RECORD +0 -5
- {ai_prompter-0.1.1.dist-info → ai_prompter-0.2.1.dist-info}/WHEEL +0 -0
- {ai_prompter-0.1.1.dist-info → ai_prompter-0.2.1.dist-info}/licenses/LICENSE +0 -0
ai_prompter/__init__.py
CHANGED
@@ -13,7 +13,7 @@ from pydantic import BaseModel
|
|
13
13
|
prompt_path_default = os.path.join(
|
14
14
|
os.path.dirname(os.path.abspath(__file__)), "prompts"
|
15
15
|
)
|
16
|
-
prompt_path_custom = os.getenv("
|
16
|
+
prompt_path_custom = os.getenv("PROMPTS_PATH")
|
17
17
|
|
18
18
|
env_default = Environment(loader=FileSystemLoader(prompt_path_default))
|
19
19
|
|
@@ -34,95 +34,161 @@ class Prompter:
|
|
34
34
|
prompt_variation: Optional[str] = "default"
|
35
35
|
prompt_text: Optional[str] = None
|
36
36
|
template: Optional[Union[str, Template]] = None
|
37
|
+
template_text: Optional[str] = None
|
37
38
|
parser: Optional[Any] = None
|
38
39
|
|
39
|
-
def __init__(
|
40
|
-
|
41
|
-
|
40
|
+
def __init__(
|
41
|
+
self,
|
42
|
+
prompt_template: Optional[str] = None,
|
43
|
+
model: Optional[Union[str, Any]] = None,
|
44
|
+
template_text: Optional[str] = None,
|
45
|
+
prompt_dir: Optional[str] = None,
|
46
|
+
) -> None:
|
47
|
+
"""Initialize the Prompter with a template name, model, and optional custom directory.
|
42
48
|
|
43
49
|
Args:
|
44
|
-
prompt_template (str, optional): The name of the prompt template
|
45
|
-
|
50
|
+
prompt_template (str, optional): The name of the prompt template (without .jinja extension).
|
51
|
+
model (Union[str, Any], optional): The model to use for generation.
|
52
|
+
template_text (str, optional): The raw text of the template.
|
53
|
+
prompt_dir (str, optional): Custom directory to search for templates.
|
46
54
|
"""
|
47
55
|
self.prompt_template = prompt_template
|
48
|
-
self.
|
49
|
-
self.
|
50
|
-
self.
|
56
|
+
self.template = None
|
57
|
+
self.template_text = template_text
|
58
|
+
self.model = model or os.getenv("OPENAI_MODEL", "gpt-4-turbo")
|
59
|
+
self.prompt_dir = prompt_dir
|
60
|
+
self._setup_template(template_text, prompt_dir)
|
51
61
|
|
52
|
-
def
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
62
|
+
def _setup_template(
|
63
|
+
self, template_text: Optional[str] = None, prompt_dir: Optional[str] = None
|
64
|
+
) -> None:
|
65
|
+
"""Set up the Jinja2 template based on the provided template file or text.
|
66
|
+
|
67
|
+
Args:
|
68
|
+
template_text (str, optional): The raw text of the template.
|
69
|
+
prompt_dir (str, optional): Custom directory to search for templates.
|
57
70
|
"""
|
58
|
-
if
|
71
|
+
if template_text is None:
|
72
|
+
if self.prompt_template is None:
|
73
|
+
raise ValueError(
|
74
|
+
"Either prompt_template or template_text must be provided"
|
75
|
+
)
|
59
76
|
if not self.prompt_template:
|
60
77
|
raise ValueError("Template name cannot be empty")
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
except Exception as e:
|
75
|
-
raise ValueError(f"Template {self.prompt_template} not found in default folder: {e}")
|
76
|
-
elif self.prompt_text is not None:
|
77
|
-
self.template = Template(self.prompt_text)
|
78
|
+
prompt_dirs = []
|
79
|
+
if prompt_dir:
|
80
|
+
prompt_dirs.append(prompt_dir)
|
81
|
+
prompts_path = os.getenv("PROMPTS_PATH")
|
82
|
+
if prompts_path is not None:
|
83
|
+
prompt_dirs.extend(prompts_path.split(":"))
|
84
|
+
# Fallback to local folder and ~/ai-prompter
|
85
|
+
prompt_dirs.extend([os.getcwd(), os.path.expanduser("~/ai-prompter")])
|
86
|
+
# Default package prompts folder
|
87
|
+
if os.path.exists(prompt_path_default):
|
88
|
+
prompt_dirs.append(prompt_path_default)
|
89
|
+
env = Environment(loader=FileSystemLoader(prompt_dirs))
|
90
|
+
self.template = env.get_template(f"{self.prompt_template}.jinja")
|
78
91
|
else:
|
79
|
-
|
80
|
-
|
81
|
-
# Removed assertion as it's redundant with the checks above
|
82
|
-
# assert self.prompt_template or self.prompt_text, "Prompt is required"
|
92
|
+
self.template_text = template_text
|
93
|
+
self.template = Template(template_text)
|
83
94
|
|
84
95
|
def to_langchain(self):
|
85
|
-
#
|
86
|
-
if self.prompt_text is not None:
|
87
|
-
raise ImportError(
|
88
|
-
"langchain-core integration only supports file-based templates; install with `pip install .[langchain]`"
|
89
|
-
)
|
96
|
+
# Support for both text-based and file-based templates with LangChain
|
90
97
|
try:
|
91
98
|
from langchain_core.prompts import ChatPromptTemplate
|
92
99
|
except ImportError:
|
93
100
|
raise ImportError(
|
94
101
|
"langchain-core is required for to_langchain; install with `pip install .[langchain]`"
|
95
102
|
)
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
103
|
+
from jinja2 import Template, Environment, FileSystemLoader
|
104
|
+
import os
|
105
|
+
import re
|
106
|
+
if self.template_text is not None:
|
107
|
+
template_content = self.template_text
|
108
|
+
return ChatPromptTemplate.from_template(
|
109
|
+
template_content, template_format="jinja2"
|
110
|
+
)
|
111
|
+
elif self.prompt_template is not None and self.template is not None:
|
112
|
+
# For file-based templates, we need to get the raw string content with includes resolved
|
113
|
+
if isinstance(self.template, Template):
|
114
|
+
try:
|
115
|
+
# Use the same logic as Prompter initialization for finding prompt_dir
|
116
|
+
if self.prompt_dir is None:
|
117
|
+
# Check for PROMPTS_PATH environment variable
|
118
|
+
prompts_path = os.environ.get("PROMPTS_PATH")
|
119
|
+
if prompts_path:
|
120
|
+
self.prompt_dir = prompts_path
|
121
|
+
else:
|
122
|
+
# Check a series of default directories
|
123
|
+
potential_dirs = [
|
124
|
+
os.path.join(os.getcwd(), "prompts"),
|
125
|
+
os.path.join(os.path.dirname(os.path.abspath(__file__)), "prompts"),
|
126
|
+
os.path.join(os.path.expanduser("~"), ".prompts"),
|
127
|
+
]
|
128
|
+
for dir_path in potential_dirs:
|
129
|
+
if os.path.exists(dir_path):
|
130
|
+
self.prompt_dir = dir_path
|
131
|
+
break
|
132
|
+
if self.prompt_dir is None:
|
133
|
+
raise ValueError(
|
134
|
+
"No prompt directory found. Please set PROMPTS_PATH environment variable "
|
135
|
+
"or specify prompt_dir when initializing Prompter with a prompt_template."
|
136
|
+
)
|
137
|
+
# Function to manually resolve includes while preserving variables
|
138
|
+
def resolve_includes(template_name, base_dir, visited=None):
|
139
|
+
if visited is None:
|
140
|
+
visited = set()
|
141
|
+
if template_name in visited:
|
142
|
+
raise ValueError(f"Circular include detected for {template_name}")
|
143
|
+
visited.add(template_name)
|
144
|
+
# Ensure we don't add .jinja if it's already in the name
|
145
|
+
if template_name.endswith('.jinja'):
|
146
|
+
template_file = os.path.join(base_dir, template_name)
|
147
|
+
else:
|
148
|
+
template_file = os.path.join(base_dir, f"{template_name}.jinja")
|
149
|
+
if not os.path.exists(template_file):
|
150
|
+
raise ValueError(f"Template file {template_file} not found")
|
151
|
+
with open(template_file, 'r', encoding='utf-8') as f:
|
152
|
+
content = f.read()
|
153
|
+
# Find all include statements
|
154
|
+
include_pattern = r"{%\s*include\s*['\"]([^'\"]+)['\"]\s*%}"
|
155
|
+
matches = re.findall(include_pattern, content)
|
156
|
+
for included_template in matches:
|
157
|
+
included_content = resolve_includes(included_template, base_dir, visited)
|
158
|
+
placeholder = "{% include '" + included_template + "' %}"
|
159
|
+
content = content.replace(placeholder, included_content)
|
160
|
+
visited.remove(template_name)
|
161
|
+
return content
|
162
|
+
# Resolve includes for the main template
|
163
|
+
template_content = resolve_includes(self.prompt_template, self.prompt_dir)
|
164
|
+
return ChatPromptTemplate.from_template(
|
165
|
+
template_content, template_format="jinja2"
|
166
|
+
)
|
167
|
+
except Exception as e:
|
168
|
+
raise ValueError(f"Error processing template for LangChain: {str(e)}")
|
169
|
+
else:
|
170
|
+
raise ValueError(
|
171
|
+
"Template is not properly initialized for LangChain conversion"
|
172
|
+
)
|
101
173
|
else:
|
102
|
-
|
103
|
-
|
104
|
-
prompt_path_custom
|
105
|
-
if prompt_path_custom and os.path.exists(prompt_path_custom)
|
106
|
-
else prompt_path_default
|
174
|
+
raise ValueError(
|
175
|
+
"Either prompt_template with a valid template or template_text must be provided for LangChain conversion"
|
107
176
|
)
|
108
|
-
template_file = os.path.join(prompt_dir, f"{self.prompt_template}.jinja")
|
109
|
-
with open(template_file, "r") as f:
|
110
|
-
template_text = f.read()
|
111
|
-
return ChatPromptTemplate.from_template(template_text, template_format="jinja2")
|
112
177
|
|
113
178
|
@classmethod
|
114
|
-
def from_text(
|
115
|
-
|
116
|
-
|
179
|
+
def from_text(
|
180
|
+
cls, text: str, model: Optional[Union[str, Any]] = None
|
181
|
+
) -> "Prompter":
|
182
|
+
"""Create a Prompter instance from raw text, which can contain Jinja code.
|
117
183
|
|
118
184
|
Args:
|
119
|
-
text (str): The raw
|
185
|
+
text (str): The raw template text.
|
186
|
+
model (Union[str, Any], optional): The model to use for generation.
|
120
187
|
|
121
188
|
Returns:
|
122
189
|
Prompter: A new Prompter instance.
|
123
190
|
"""
|
124
|
-
|
125
|
-
return cls(prompt_text=text)
|
191
|
+
return cls(template_text=text, model=model)
|
126
192
|
|
127
193
|
def render(self, data: Optional[Union[Dict, BaseModel]] = None) -> str:
|
128
194
|
"""
|
ai_prompter/py.typed
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: ai-prompter
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.2.1
|
4
4
|
Summary: A prompt management library using Jinja2 templates to build complex prompts easily.
|
5
5
|
Author-email: LUIS NOVO <lfnovo@gmail.com>
|
6
6
|
License: MIT
|
@@ -21,7 +21,7 @@ A prompt management library using Jinja2 templates to build complex prompts easi
|
|
21
21
|
|
22
22
|
- Define prompts as Jinja templates.
|
23
23
|
- Load default templates from `src/ai_prompter/prompts`.
|
24
|
-
- Override templates via `
|
24
|
+
- Override templates via `PROMPTS_PATH` environment variable.
|
25
25
|
- Render prompts with arbitrary data or Pydantic models.
|
26
26
|
- Export to LangChain `ChatPromptTemplate`.
|
27
27
|
|
@@ -51,34 +51,104 @@ uv add langchain_core
|
|
51
51
|
Configure a custom template path by creating a `.env` file in the project root:
|
52
52
|
|
53
53
|
```dotenv
|
54
|
-
|
54
|
+
PROMPTS_PATH=path/to/custom/templates
|
55
55
|
```
|
56
56
|
|
57
57
|
## Usage
|
58
58
|
|
59
|
+
### Basic Usage
|
60
|
+
|
61
|
+
```python
|
62
|
+
from ai_prompter import Prompter
|
63
|
+
|
64
|
+
# Initialize with a template name
|
65
|
+
prompter = Prompter('my_template')
|
66
|
+
|
67
|
+
# Render a prompt with variables
|
68
|
+
prompt = prompter.render({'variable': 'value'})
|
69
|
+
print(prompt)
|
70
|
+
```
|
71
|
+
|
72
|
+
### Custom Prompt Directory
|
73
|
+
|
74
|
+
You can specify a custom directory for your prompt templates using the `prompt_dir` parameter:
|
75
|
+
|
76
|
+
```python
|
77
|
+
prompter = Prompter(template_text='Hello {{ name }}!', prompt_dir='/path/to/your/prompts')
|
78
|
+
```
|
79
|
+
|
80
|
+
### Using Environment Variable for Prompt Path
|
81
|
+
|
82
|
+
Set the `PROMPTS_PATH` environment variable to point to your custom prompts directory:
|
83
|
+
|
84
|
+
```bash
|
85
|
+
export PROMPTS_PATH=/path/to/your/prompts
|
86
|
+
```
|
87
|
+
|
88
|
+
The `Prompter` class will check this path if no custom directory is provided in the constructor. If not set, it will also look in the current working directory and `~/ai-prompter/` as fallback options before using the default package prompts.
|
89
|
+
|
59
90
|
### Raw text template
|
60
91
|
|
61
92
|
```python
|
62
93
|
from ai_prompter import Prompter
|
63
94
|
|
64
95
|
template = """Write an article about {{ topic }}."""
|
65
|
-
prompter = Prompter(
|
96
|
+
prompter = Prompter(template_text=template)
|
66
97
|
prompt = prompter.render({"topic": "AI"})
|
67
98
|
print(prompt) # Write an article about AI.
|
68
99
|
```
|
69
100
|
|
70
101
|
### Using File-based Templates
|
71
102
|
|
72
|
-
You can store your templates in files and reference them by name (without the `.jinja` extension). The library looks for templates in the `prompts` directory by default, or you can set a custom directory with the `
|
103
|
+
You can store your templates in files and reference them by name (without the `.jinja` extension). The library looks for templates in the `prompts` directory by default, or you can set a custom directory with the `PROMPTS_PATH` environment variable. You can specify multiple directories separated by `:` (colon), and the library will search through them in order until a matching template is found.
|
73
104
|
|
74
105
|
```python
|
75
106
|
from ai_prompter import Prompter
|
76
107
|
|
108
|
+
# Set multiple search paths
|
109
|
+
os.environ["PROMPTS_PATH"] = "/path/to/templates1:/path/to/templates2"
|
110
|
+
|
77
111
|
prompter = Prompter(prompt_template="greet")
|
78
|
-
|
79
|
-
print(
|
112
|
+
result = prompter.render({"name": "World"})
|
113
|
+
print(result) # Output depends on the content of greet.jinja in the first found path
|
114
|
+
```
|
115
|
+
|
116
|
+
### Using Raw Text Templates
|
117
|
+
|
118
|
+
Alternatively, you can provide the template content directly as raw text using the `template_text` parameter or the `from_text` class method.
|
119
|
+
|
120
|
+
```python
|
121
|
+
from ai_prompter import Prompter
|
122
|
+
|
123
|
+
# Using template_text parameter
|
124
|
+
prompter = Prompter(template_text="Hello, {{ name }}!")
|
125
|
+
result = prompter.render({"name": "World"})
|
126
|
+
print(result) # Output: Hello, World!
|
127
|
+
|
128
|
+
# Using from_text class method
|
129
|
+
prompter = Prompter.from_text("Hi, {{ person }}!", model="gpt-4")
|
130
|
+
result = prompter.render({"person": "Alice"})
|
131
|
+
print(result) # Output: Hi, Alice!
|
132
|
+
```
|
133
|
+
|
134
|
+
### LangChain Integration
|
135
|
+
|
136
|
+
You can convert your prompts to LangChain's `ChatPromptTemplate` format for use in LangChain workflows. This works for both text-based and file-based templates.
|
137
|
+
|
138
|
+
```python
|
139
|
+
from ai_prompter import Prompter
|
140
|
+
|
141
|
+
# With text-based template
|
142
|
+
text_prompter = Prompter(template_text="Hello, {{ name }}!")
|
143
|
+
lc_text_prompt = text_prompter.to_langchain()
|
144
|
+
|
145
|
+
# With file-based template
|
146
|
+
file_prompter = Prompter(prompt_template="greet")
|
147
|
+
lc_file_prompt = file_prompter.to_langchain()
|
80
148
|
```
|
81
149
|
|
150
|
+
**Note**: LangChain integration requires the `langchain-core` package. Install it with `pip install .[langchain]`.
|
151
|
+
|
82
152
|
### Including Other Templates
|
83
153
|
|
84
154
|
You can include other template files within a template using Jinja2's `{% include %}` directive. This allows you to build modular templates.
|
@@ -138,7 +208,7 @@ The library also automatically provides a `current_time` variable with the curre
|
|
138
208
|
```python
|
139
209
|
from ai_prompter import Prompter
|
140
210
|
|
141
|
-
prompter = Prompter(
|
211
|
+
prompter = Prompter(template_text="Current time: {{current_time}}")
|
142
212
|
prompt = prompter.render()
|
143
213
|
print(prompt) # Current time: 2025-04-19 23:28:00
|
144
214
|
```
|
@@ -159,16 +229,6 @@ prompt = prompter.render({"topic": "AI"})
|
|
159
229
|
print(prompt)
|
160
230
|
```
|
161
231
|
|
162
|
-
### LangChain integration
|
163
|
-
|
164
|
-
```python
|
165
|
-
from ai_prompter import Prompter
|
166
|
-
|
167
|
-
prompter = Prompter(prompt_template="article")
|
168
|
-
lc_template = prompter.to_langchain()
|
169
|
-
# use lc_template in LangChain chains
|
170
|
-
```
|
171
|
-
|
172
232
|
### Jupyter Notebook
|
173
233
|
|
174
234
|
See `notebooks/prompter_usage.ipynb` for interactive examples.
|
@@ -0,0 +1,6 @@
|
|
1
|
+
ai_prompter/__init__.py,sha256=ehtuhOKP0ze3sW1cYO4OyNhoB8CY926TmkPqvzz4ilA,10124
|
2
|
+
ai_prompter/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
3
|
+
ai_prompter-0.2.1.dist-info/METADATA,sha256=c4Wh6qs7JVcBwWf1FCe2MZtsw5HjygYuXyJzhuKFTUc,6950
|
4
|
+
ai_prompter-0.2.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
5
|
+
ai_prompter-0.2.1.dist-info/licenses/LICENSE,sha256=cS0_fa_8BoP0PvVG8D19pn_HDJrG96hd4PyEm9nkRo8,1066
|
6
|
+
ai_prompter-0.2.1.dist-info/RECORD,,
|
@@ -1,5 +0,0 @@
|
|
1
|
-
ai_prompter/__init__.py,sha256=4Zzy7drJRTmQZGJCEcqnf1-RkvDvZiLIYXLDeyFXGJw,5913
|
2
|
-
ai_prompter-0.1.1.dist-info/METADATA,sha256=PgRjdlAIemIiqe5ANnoM52qK1FWxW4frN2SxS7MTsPs,4755
|
3
|
-
ai_prompter-0.1.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
4
|
-
ai_prompter-0.1.1.dist-info/licenses/LICENSE,sha256=cS0_fa_8BoP0PvVG8D19pn_HDJrG96hd4PyEm9nkRo8,1066
|
5
|
-
ai_prompter-0.1.1.dist-info/RECORD,,
|
File without changes
|
File without changes
|