prompty 0.1.12__py2.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.
- prompty/__init__.py +391 -0
- prompty/azure/__init__.py +3 -0
- prompty/azure/executor.py +95 -0
- prompty/azure/processor.py +66 -0
- prompty/cli.py +117 -0
- prompty/core.py +539 -0
- prompty/openai/__init__.py +3 -0
- prompty/openai/executor.py +74 -0
- prompty/openai/processor.py +65 -0
- prompty/parsers.py +139 -0
- prompty/renderers.py +23 -0
- prompty/serverless/__init__.py +3 -0
- prompty/serverless/executor.py +82 -0
- prompty/serverless/processor.py +62 -0
- prompty/tracer.py +260 -0
- prompty-0.1.12.dist-info/METADATA +17 -0
- prompty-0.1.12.dist-info/RECORD +19 -0
- prompty-0.1.12.dist-info/WHEEL +4 -0
- prompty-0.1.12.dist-info/licenses/LICENSE +7 -0
prompty/__init__.py
ADDED
@@ -0,0 +1,391 @@
|
|
1
|
+
import json
|
2
|
+
import traceback
|
3
|
+
from pathlib import Path
|
4
|
+
from typing import Dict, List, Union
|
5
|
+
|
6
|
+
from prompty.tracer import trace
|
7
|
+
from prompty.core import (
|
8
|
+
Frontmatter,
|
9
|
+
InvokerFactory,
|
10
|
+
ModelSettings,
|
11
|
+
Prompty,
|
12
|
+
PropertySettings,
|
13
|
+
TemplateSettings,
|
14
|
+
param_hoisting,
|
15
|
+
)
|
16
|
+
|
17
|
+
from .renderers import *
|
18
|
+
from .parsers import *
|
19
|
+
|
20
|
+
|
21
|
+
def load_global_config(
|
22
|
+
prompty_path: Path = Path.cwd(), configuration: str = "default"
|
23
|
+
) -> Dict[str, any]:
|
24
|
+
# prompty.config laying around?
|
25
|
+
prompty_config = list(Path.cwd().glob("**/prompty.json"))
|
26
|
+
|
27
|
+
# if there is one load it
|
28
|
+
if len(prompty_config) > 0:
|
29
|
+
# pick the nearest prompty.json
|
30
|
+
config = sorted(
|
31
|
+
[
|
32
|
+
c
|
33
|
+
for c in prompty_config
|
34
|
+
if len(c.parent.parts) <= len(prompty_path.parts)
|
35
|
+
],
|
36
|
+
key=lambda p: len(p.parts),
|
37
|
+
)[-1]
|
38
|
+
|
39
|
+
with open(config, "r") as f:
|
40
|
+
c = json.load(f)
|
41
|
+
if configuration in c:
|
42
|
+
return c[configuration]
|
43
|
+
else:
|
44
|
+
raise ValueError(f'Item "{configuration}" not found in "{config}"')
|
45
|
+
|
46
|
+
return {}
|
47
|
+
|
48
|
+
|
49
|
+
@trace(description="Create a headless prompty object for programmatic use.")
|
50
|
+
def headless(
|
51
|
+
api: str,
|
52
|
+
content: str | List[str] | dict,
|
53
|
+
configuration: Dict[str, any] = {},
|
54
|
+
parameters: Dict[str, any] = {},
|
55
|
+
connection: str = "default",
|
56
|
+
) -> Prompty:
|
57
|
+
"""Create a headless prompty object for programmatic use.
|
58
|
+
|
59
|
+
Parameters
|
60
|
+
----------
|
61
|
+
api : str
|
62
|
+
The API to use for the model
|
63
|
+
content : str | List[str] | dict
|
64
|
+
The content to process
|
65
|
+
configuration : Dict[str, any], optional
|
66
|
+
The configuration to use, by default {}
|
67
|
+
parameters : Dict[str, any], optional
|
68
|
+
The parameters to use, by default {}
|
69
|
+
connection : str, optional
|
70
|
+
The connection to use, by default "default"
|
71
|
+
|
72
|
+
Returns
|
73
|
+
-------
|
74
|
+
Prompty
|
75
|
+
The headless prompty object
|
76
|
+
|
77
|
+
Example
|
78
|
+
-------
|
79
|
+
>>> import prompty
|
80
|
+
>>> p = prompty.headless(
|
81
|
+
api="embedding",
|
82
|
+
configuration={"type": "azure", "azure_deployment": "text-embedding-ada-002"},
|
83
|
+
content="hello world",
|
84
|
+
)
|
85
|
+
>>> emb = prompty.execute(p)
|
86
|
+
|
87
|
+
"""
|
88
|
+
|
89
|
+
# get caller's path (to get relative path for prompty.json)
|
90
|
+
caller = Path(traceback.extract_stack()[-2].filename)
|
91
|
+
templateSettings = TemplateSettings(type="NOOP", parser="NOOP")
|
92
|
+
modelSettings = ModelSettings(
|
93
|
+
api=api,
|
94
|
+
configuration=Prompty.normalize(
|
95
|
+
param_hoisting(
|
96
|
+
configuration, load_global_config(caller.parent, connection)
|
97
|
+
),
|
98
|
+
caller.parent,
|
99
|
+
),
|
100
|
+
parameters=parameters,
|
101
|
+
)
|
102
|
+
|
103
|
+
return Prompty(model=modelSettings, template=templateSettings, content=content)
|
104
|
+
|
105
|
+
|
106
|
+
@trace(description="Load a prompty file.")
|
107
|
+
def load(prompty_file: str, configuration: str = "default") -> Prompty:
|
108
|
+
"""Load a prompty file.
|
109
|
+
|
110
|
+
Parameters
|
111
|
+
----------
|
112
|
+
prompty_file : str
|
113
|
+
The path to the prompty file
|
114
|
+
configuration : str, optional
|
115
|
+
The configuration to use, by default "default"
|
116
|
+
|
117
|
+
Returns
|
118
|
+
-------
|
119
|
+
Prompty
|
120
|
+
The loaded prompty object
|
121
|
+
|
122
|
+
Example
|
123
|
+
-------
|
124
|
+
>>> import prompty
|
125
|
+
>>> p = prompty.load("prompts/basic.prompty")
|
126
|
+
>>> print(p)
|
127
|
+
"""
|
128
|
+
|
129
|
+
p = Path(prompty_file)
|
130
|
+
if not p.is_absolute():
|
131
|
+
# get caller's path (take into account trace frame)
|
132
|
+
caller = Path(traceback.extract_stack()[-3].filename)
|
133
|
+
p = Path(caller.parent / p).resolve().absolute()
|
134
|
+
|
135
|
+
# load dictionary from prompty file
|
136
|
+
matter = Frontmatter.read_file(p)
|
137
|
+
attributes = matter["attributes"]
|
138
|
+
content = matter["body"]
|
139
|
+
|
140
|
+
# normalize attribute dictionary resolve keys and files
|
141
|
+
attributes = Prompty.normalize(attributes, p.parent)
|
142
|
+
|
143
|
+
# load global configuration
|
144
|
+
global_config = Prompty.normalize(
|
145
|
+
load_global_config(p.parent, configuration), p.parent
|
146
|
+
)
|
147
|
+
if "model" not in attributes:
|
148
|
+
attributes["model"] = {}
|
149
|
+
|
150
|
+
if "configuration" not in attributes["model"]:
|
151
|
+
attributes["model"]["configuration"] = global_config
|
152
|
+
else:
|
153
|
+
attributes["model"]["configuration"] = param_hoisting(
|
154
|
+
attributes["model"]["configuration"],
|
155
|
+
global_config,
|
156
|
+
)
|
157
|
+
|
158
|
+
# pull model settings out of attributes
|
159
|
+
try:
|
160
|
+
model = ModelSettings(**attributes.pop("model"))
|
161
|
+
except Exception as e:
|
162
|
+
raise ValueError(f"Error in model settings: {e}")
|
163
|
+
|
164
|
+
# pull template settings
|
165
|
+
try:
|
166
|
+
if "template" in attributes:
|
167
|
+
t = attributes.pop("template")
|
168
|
+
if isinstance(t, dict):
|
169
|
+
template = TemplateSettings(**t)
|
170
|
+
# has to be a string denoting the type
|
171
|
+
else:
|
172
|
+
template = TemplateSettings(type=t, parser="prompty")
|
173
|
+
else:
|
174
|
+
template = TemplateSettings(type="jinja2", parser="prompty")
|
175
|
+
except Exception as e:
|
176
|
+
raise ValueError(f"Error in template loader: {e}")
|
177
|
+
|
178
|
+
# formalize inputs and outputs
|
179
|
+
if "inputs" in attributes:
|
180
|
+
try:
|
181
|
+
inputs = {
|
182
|
+
k: PropertySettings(**v) for (k, v) in attributes.pop("inputs").items()
|
183
|
+
}
|
184
|
+
except Exception as e:
|
185
|
+
raise ValueError(f"Error in inputs: {e}")
|
186
|
+
else:
|
187
|
+
inputs = {}
|
188
|
+
if "outputs" in attributes:
|
189
|
+
try:
|
190
|
+
outputs = {
|
191
|
+
k: PropertySettings(**v) for (k, v) in attributes.pop("outputs").items()
|
192
|
+
}
|
193
|
+
except Exception as e:
|
194
|
+
raise ValueError(f"Error in outputs: {e}")
|
195
|
+
else:
|
196
|
+
outputs = {}
|
197
|
+
|
198
|
+
# recursive loading of base prompty
|
199
|
+
if "base" in attributes:
|
200
|
+
# load the base prompty from the same directory as the current prompty
|
201
|
+
base = load(p.parent / attributes["base"])
|
202
|
+
# hoist the base prompty's attributes to the current prompty
|
203
|
+
model.api = base.model.api if model.api == "" else model.api
|
204
|
+
model.configuration = param_hoisting(
|
205
|
+
model.configuration, base.model.configuration
|
206
|
+
)
|
207
|
+
model.parameters = param_hoisting(model.parameters, base.model.parameters)
|
208
|
+
model.response = param_hoisting(model.response, base.model.response)
|
209
|
+
attributes["sample"] = param_hoisting(attributes, base.sample, "sample")
|
210
|
+
|
211
|
+
p = Prompty(
|
212
|
+
**attributes,
|
213
|
+
model=model,
|
214
|
+
inputs=inputs,
|
215
|
+
outputs=outputs,
|
216
|
+
template=template,
|
217
|
+
content=content,
|
218
|
+
file=p,
|
219
|
+
basePrompty=base,
|
220
|
+
)
|
221
|
+
else:
|
222
|
+
p = Prompty(
|
223
|
+
**attributes,
|
224
|
+
model=model,
|
225
|
+
inputs=inputs,
|
226
|
+
outputs=outputs,
|
227
|
+
template=template,
|
228
|
+
content=content,
|
229
|
+
file=p,
|
230
|
+
)
|
231
|
+
return p
|
232
|
+
|
233
|
+
@trace(description="Prepare the inputs for the prompt.")
|
234
|
+
def prepare(
|
235
|
+
prompt: Prompty,
|
236
|
+
inputs: Dict[str, any] = {},
|
237
|
+
):
|
238
|
+
""" Prepare the inputs for the prompt.
|
239
|
+
|
240
|
+
Parameters
|
241
|
+
----------
|
242
|
+
prompt : Prompty
|
243
|
+
The prompty object
|
244
|
+
inputs : Dict[str, any], optional
|
245
|
+
The inputs to the prompt, by default {}
|
246
|
+
|
247
|
+
Returns
|
248
|
+
-------
|
249
|
+
dict
|
250
|
+
The prepared and hidrated template shaped to the LLM model
|
251
|
+
|
252
|
+
Example
|
253
|
+
-------
|
254
|
+
>>> import prompty
|
255
|
+
>>> p = prompty.load("prompts/basic.prompty")
|
256
|
+
>>> inputs = {"name": "John Doe"}
|
257
|
+
>>> content = prompty.prepare(p, inputs)
|
258
|
+
"""
|
259
|
+
inputs = param_hoisting(inputs, prompt.sample)
|
260
|
+
|
261
|
+
if prompt.template.type == "NOOP":
|
262
|
+
render = prompt.content
|
263
|
+
else:
|
264
|
+
# render
|
265
|
+
renderer = InvokerFactory.create_renderer(prompt.template.type, prompt)
|
266
|
+
render = renderer(inputs)
|
267
|
+
|
268
|
+
if prompt.template.parser == "NOOP":
|
269
|
+
result = render
|
270
|
+
else:
|
271
|
+
# parse [parser].[api]
|
272
|
+
parser = InvokerFactory.create_parser(
|
273
|
+
f"{prompt.template.parser}.{prompt.model.api}", prompt
|
274
|
+
)
|
275
|
+
result = parser(render)
|
276
|
+
|
277
|
+
return result
|
278
|
+
|
279
|
+
@trace(description="Run the prepared Prompty content against the model.")
|
280
|
+
def run(
|
281
|
+
prompt: Prompty,
|
282
|
+
content: dict | list | str,
|
283
|
+
configuration: Dict[str, any] = {},
|
284
|
+
parameters: Dict[str, any] = {},
|
285
|
+
raw: bool = False,
|
286
|
+
):
|
287
|
+
"""Run the prepared Prompty content.
|
288
|
+
|
289
|
+
Parameters
|
290
|
+
----------
|
291
|
+
prompt : Prompty
|
292
|
+
The prompty object
|
293
|
+
content : dict | list | str
|
294
|
+
The content to process
|
295
|
+
configuration : Dict[str, any], optional
|
296
|
+
The configuration to use, by default {}
|
297
|
+
parameters : Dict[str, any], optional
|
298
|
+
The parameters to use, by default {}
|
299
|
+
raw : bool, optional
|
300
|
+
Whether to skip processing, by default False
|
301
|
+
|
302
|
+
Returns
|
303
|
+
-------
|
304
|
+
any
|
305
|
+
The result of the prompt
|
306
|
+
|
307
|
+
Example
|
308
|
+
-------
|
309
|
+
>>> import prompty
|
310
|
+
>>> p = prompty.load("prompts/basic.prompty")
|
311
|
+
>>> inputs = {"name": "John Doe"}
|
312
|
+
>>> content = prompty.prepare(p, inputs)
|
313
|
+
>>> result = prompty.run(p, content)
|
314
|
+
"""
|
315
|
+
|
316
|
+
if configuration != {}:
|
317
|
+
prompt.model.configuration = param_hoisting(
|
318
|
+
configuration, prompt.model.configuration
|
319
|
+
)
|
320
|
+
|
321
|
+
if parameters != {}:
|
322
|
+
prompt.model.parameters = param_hoisting(parameters, prompt.model.parameters)
|
323
|
+
|
324
|
+
# execute
|
325
|
+
executor = InvokerFactory.create_executor(
|
326
|
+
prompt.model.configuration["type"], prompt
|
327
|
+
)
|
328
|
+
result = executor(content)
|
329
|
+
|
330
|
+
# skip?
|
331
|
+
if not raw:
|
332
|
+
# process
|
333
|
+
processor = InvokerFactory.create_processor(
|
334
|
+
prompt.model.configuration["type"], prompt
|
335
|
+
)
|
336
|
+
result = processor(result)
|
337
|
+
|
338
|
+
return result
|
339
|
+
|
340
|
+
@trace(description="Execute a prompty")
|
341
|
+
def execute(
|
342
|
+
prompt: Union[str, Prompty],
|
343
|
+
configuration: Dict[str, any] = {},
|
344
|
+
parameters: Dict[str, any] = {},
|
345
|
+
inputs: Dict[str, any] = {},
|
346
|
+
raw: bool = False,
|
347
|
+
connection: str = "default",
|
348
|
+
):
|
349
|
+
"""Execute a prompty.
|
350
|
+
|
351
|
+
Parameters
|
352
|
+
----------
|
353
|
+
prompt : Union[str, Prompty]
|
354
|
+
The prompty object or path to the prompty file
|
355
|
+
configuration : Dict[str, any], optional
|
356
|
+
The configuration to use, by default {}
|
357
|
+
parameters : Dict[str, any], optional
|
358
|
+
The parameters to use, by default {}
|
359
|
+
inputs : Dict[str, any], optional
|
360
|
+
The inputs to the prompt, by default {}
|
361
|
+
raw : bool, optional
|
362
|
+
Whether to skip processing, by default False
|
363
|
+
connection : str, optional
|
364
|
+
The connection to use, by default "default"
|
365
|
+
|
366
|
+
Returns
|
367
|
+
-------
|
368
|
+
any
|
369
|
+
The result of the prompt
|
370
|
+
|
371
|
+
Example
|
372
|
+
-------
|
373
|
+
>>> import prompty
|
374
|
+
>>> inputs = {"name": "John Doe"}
|
375
|
+
>>> result = prompty.execute("prompts/basic.prompty", inputs=inputs)
|
376
|
+
"""
|
377
|
+
if isinstance(prompt, str):
|
378
|
+
path = Path(prompt)
|
379
|
+
if not path.is_absolute():
|
380
|
+
# get caller's path (take into account trace frame)
|
381
|
+
caller = Path(traceback.extract_stack()[-3].filename)
|
382
|
+
path = Path(caller.parent / path).resolve().absolute()
|
383
|
+
prompt = load(path, connection)
|
384
|
+
|
385
|
+
# prepare content
|
386
|
+
content = prepare(prompt, inputs)
|
387
|
+
|
388
|
+
# run LLM model
|
389
|
+
result = run(prompt, content, configuration, parameters, raw)
|
390
|
+
|
391
|
+
return result
|
@@ -0,0 +1,95 @@
|
|
1
|
+
import azure.identity
|
2
|
+
import importlib.metadata
|
3
|
+
from typing import Iterator
|
4
|
+
from openai import AzureOpenAI
|
5
|
+
from ..core import Invoker, InvokerFactory, Prompty, PromptyStream
|
6
|
+
|
7
|
+
VERSION = importlib.metadata.version("prompty")
|
8
|
+
|
9
|
+
|
10
|
+
@InvokerFactory.register_executor("azure")
|
11
|
+
@InvokerFactory.register_executor("azure_openai")
|
12
|
+
class AzureOpenAIExecutor(Invoker):
|
13
|
+
"""Azure OpenAI Executor"""
|
14
|
+
|
15
|
+
def __init__(self, prompty: Prompty) -> None:
|
16
|
+
super().__init__(prompty)
|
17
|
+
kwargs = {
|
18
|
+
key: value
|
19
|
+
for key, value in self.prompty.model.configuration.items()
|
20
|
+
if key != "type"
|
21
|
+
}
|
22
|
+
|
23
|
+
# no key, use default credentials
|
24
|
+
if "api_key" not in kwargs:
|
25
|
+
# managed identity if client id
|
26
|
+
if "client_id" in kwargs:
|
27
|
+
default_credential = azure.identity.ManagedIdentityCredential(
|
28
|
+
client_id=kwargs.pop("client_id"),
|
29
|
+
)
|
30
|
+
# default credential
|
31
|
+
else:
|
32
|
+
default_credential = azure.identity.DefaultAzureCredential(
|
33
|
+
exclude_shared_token_cache_credential=True
|
34
|
+
)
|
35
|
+
|
36
|
+
kwargs["azure_ad_token_provider"] = (
|
37
|
+
azure.identity.get_bearer_token_provider(
|
38
|
+
default_credential, "https://cognitiveservices.azure.com/.default"
|
39
|
+
)
|
40
|
+
)
|
41
|
+
|
42
|
+
self.client = AzureOpenAI(
|
43
|
+
default_headers={
|
44
|
+
"User-Agent": f"prompty/{VERSION}",
|
45
|
+
"x-ms-useragent": f"prompty/{VERSION}",
|
46
|
+
},
|
47
|
+
**kwargs,
|
48
|
+
)
|
49
|
+
|
50
|
+
self.api = self.prompty.model.api
|
51
|
+
self.deployment = self.prompty.model.configuration["azure_deployment"]
|
52
|
+
self.parameters = self.prompty.model.parameters
|
53
|
+
|
54
|
+
def invoke(self, data: any) -> any:
|
55
|
+
"""Invoke the Azure OpenAI API
|
56
|
+
|
57
|
+
Parameters
|
58
|
+
----------
|
59
|
+
data : any
|
60
|
+
The data to send to the Azure OpenAI API
|
61
|
+
|
62
|
+
Returns
|
63
|
+
-------
|
64
|
+
any
|
65
|
+
The response from the Azure OpenAI API
|
66
|
+
"""
|
67
|
+
if self.api == "chat":
|
68
|
+
response = self.client.chat.completions.create(
|
69
|
+
model=self.deployment,
|
70
|
+
messages=data if isinstance(data, list) else [data],
|
71
|
+
**self.parameters,
|
72
|
+
)
|
73
|
+
|
74
|
+
elif self.api == "completion":
|
75
|
+
response = self.client.completions.create(
|
76
|
+
prompt=data.item,
|
77
|
+
model=self.deployment,
|
78
|
+
**self.parameters,
|
79
|
+
)
|
80
|
+
|
81
|
+
elif self.api == "embedding":
|
82
|
+
response = self.client.embeddings.create(
|
83
|
+
input=data if isinstance(data, list) else [data],
|
84
|
+
model=self.deployment,
|
85
|
+
**self.parameters,
|
86
|
+
)
|
87
|
+
|
88
|
+
elif self.api == "image":
|
89
|
+
raise NotImplementedError("Azure OpenAI Image API is not implemented yet")
|
90
|
+
|
91
|
+
# stream response
|
92
|
+
if isinstance(response, Iterator):
|
93
|
+
return PromptyStream("AzureOpenAIExecutor", response)
|
94
|
+
else:
|
95
|
+
return response
|
@@ -0,0 +1,66 @@
|
|
1
|
+
from typing import Iterator
|
2
|
+
from openai.types.completion import Completion
|
3
|
+
from openai.types.chat.chat_completion import ChatCompletion
|
4
|
+
from ..core import Invoker, InvokerFactory, Prompty, PromptyStream, ToolCall
|
5
|
+
from openai.types.create_embedding_response import CreateEmbeddingResponse
|
6
|
+
|
7
|
+
|
8
|
+
@InvokerFactory.register_processor("azure")
|
9
|
+
@InvokerFactory.register_processor("azure_openai")
|
10
|
+
class AzureOpenAIProcessor(Invoker):
|
11
|
+
"""Azure OpenAI Processor"""
|
12
|
+
|
13
|
+
def __init__(self, prompty: Prompty) -> None:
|
14
|
+
super().__init__(prompty)
|
15
|
+
|
16
|
+
def invoke(self, data: any) -> any:
|
17
|
+
"""Invoke the OpenAI/Azure API
|
18
|
+
|
19
|
+
Parameters
|
20
|
+
----------
|
21
|
+
data : any
|
22
|
+
The data to send to the OpenAI/Azure API
|
23
|
+
|
24
|
+
Returns
|
25
|
+
-------
|
26
|
+
any
|
27
|
+
The response from the OpenAI/Azure API
|
28
|
+
"""
|
29
|
+
if isinstance(data, ChatCompletion):
|
30
|
+
response = data.choices[0].message
|
31
|
+
# tool calls available in response
|
32
|
+
if response.tool_calls:
|
33
|
+
return [
|
34
|
+
ToolCall(
|
35
|
+
id=tool_call.id,
|
36
|
+
name=tool_call.function.name,
|
37
|
+
arguments=tool_call.function.arguments,
|
38
|
+
)
|
39
|
+
for tool_call in response.tool_calls
|
40
|
+
]
|
41
|
+
else:
|
42
|
+
return response.content
|
43
|
+
|
44
|
+
elif isinstance(data, Completion):
|
45
|
+
return data.choices[0].text
|
46
|
+
elif isinstance(data, CreateEmbeddingResponse):
|
47
|
+
if len(data.data) == 0:
|
48
|
+
raise ValueError("Invalid data")
|
49
|
+
elif len(data.data) == 1:
|
50
|
+
return data.data[0].embedding
|
51
|
+
else:
|
52
|
+
return [item.embedding for item in data.data]
|
53
|
+
elif isinstance(data, Iterator):
|
54
|
+
|
55
|
+
def generator():
|
56
|
+
for chunk in data:
|
57
|
+
if (
|
58
|
+
len(chunk.choices) == 1
|
59
|
+
and chunk.choices[0].delta.content != None
|
60
|
+
):
|
61
|
+
content = chunk.choices[0].delta.content
|
62
|
+
yield content
|
63
|
+
|
64
|
+
return PromptyStream("AzureOpenAIProcessor", generator())
|
65
|
+
else:
|
66
|
+
return data
|
prompty/cli.py
ADDED
@@ -0,0 +1,117 @@
|
|
1
|
+
import os
|
2
|
+
import json
|
3
|
+
import click
|
4
|
+
import importlib
|
5
|
+
|
6
|
+
from pathlib import Path
|
7
|
+
from pydantic import BaseModel
|
8
|
+
|
9
|
+
import prompty
|
10
|
+
from prompty.tracer import trace, PromptyTracer, console_tracer, Tracer
|
11
|
+
from dotenv import load_dotenv
|
12
|
+
|
13
|
+
load_dotenv()
|
14
|
+
|
15
|
+
|
16
|
+
def normalize_path(p, create_dir=False) -> Path:
|
17
|
+
path = Path(p)
|
18
|
+
if not path.is_absolute():
|
19
|
+
path = Path(os.getcwd()).joinpath(path).absolute().resolve()
|
20
|
+
else:
|
21
|
+
path = path.absolute().resolve()
|
22
|
+
|
23
|
+
if create_dir:
|
24
|
+
if not path.exists():
|
25
|
+
print(f"Creating directory {str(path)}")
|
26
|
+
os.makedirs(str(path))
|
27
|
+
|
28
|
+
return path
|
29
|
+
|
30
|
+
def dynamic_import(module: str):
|
31
|
+
t = module if "." in module else f"prompty.{module}"
|
32
|
+
print(f"Loading invokers from {t}")
|
33
|
+
importlib.import_module(t)
|
34
|
+
|
35
|
+
|
36
|
+
@trace
|
37
|
+
def chat_mode(prompt_path: str):
|
38
|
+
W = "\033[0m" # white (normal)
|
39
|
+
R = "\033[31m" # red
|
40
|
+
G = "\033[32m" # green
|
41
|
+
O = "\033[33m" # orange
|
42
|
+
B = "\033[34m" # blue
|
43
|
+
P = "\033[35m" # purple
|
44
|
+
print(f"Executing {str(prompt_path)} in chat mode...")
|
45
|
+
p = prompty.load(str(prompt_path))
|
46
|
+
if "chat_history" not in p.sample:
|
47
|
+
print(
|
48
|
+
f"{R}{str(prompt_path)} needs to have a chat_history input to work in chat mode{W}"
|
49
|
+
)
|
50
|
+
return
|
51
|
+
else:
|
52
|
+
|
53
|
+
try:
|
54
|
+
# load executor / processor types
|
55
|
+
dynamic_import(p.model.configuration["type"])
|
56
|
+
chat_history = p.sample["chat_history"]
|
57
|
+
while True:
|
58
|
+
user_input = input(f"\n{B}User:{W} ")
|
59
|
+
if user_input == "exit":
|
60
|
+
break
|
61
|
+
# reloadable prompty file
|
62
|
+
chat_history.append({"role": "user", "content": user_input})
|
63
|
+
result = prompty.execute(prompt_path, inputs={"chat_history": chat_history})
|
64
|
+
print(f"\n{G}Assistant:{W} {result}")
|
65
|
+
chat_history.append({"role": "assistant", "content": result})
|
66
|
+
except Exception as e:
|
67
|
+
print(f"{type(e).__qualname__}: {e}")
|
68
|
+
|
69
|
+
print(f"\n{R}Goodbye!{W}\n")
|
70
|
+
|
71
|
+
|
72
|
+
@trace
|
73
|
+
def execute(prompt_path: str, raw=False):
|
74
|
+
p = prompty.load(prompt_path)
|
75
|
+
|
76
|
+
try:
|
77
|
+
# load executor / processor types
|
78
|
+
dynamic_import(p.model.configuration["type"])
|
79
|
+
|
80
|
+
result = prompty.execute(p, raw=raw)
|
81
|
+
if issubclass(type(result), BaseModel):
|
82
|
+
print("\n", json.dumps(result.model_dump(), indent=4), "\n")
|
83
|
+
elif isinstance(result, list):
|
84
|
+
print(
|
85
|
+
"\n", json.dumps([item.model_dump() for item in result], indent=4), "\n"
|
86
|
+
)
|
87
|
+
else:
|
88
|
+
print("\n", result, "\n")
|
89
|
+
except Exception as e:
|
90
|
+
print(f"{type(e).__qualname__}: {e}", "\n")
|
91
|
+
|
92
|
+
|
93
|
+
@click.command()
|
94
|
+
@click.option("--source", "-s", required=True)
|
95
|
+
@click.option("--verbose", "-v", is_flag=True)
|
96
|
+
@click.option("--chat", "-c", is_flag=True)
|
97
|
+
@click.version_option()
|
98
|
+
def run(source, verbose, chat):
|
99
|
+
prompt_path = normalize_path(source)
|
100
|
+
if not prompt_path.exists():
|
101
|
+
print(f"{str(prompt_path)} does not exist")
|
102
|
+
return
|
103
|
+
|
104
|
+
if verbose:
|
105
|
+
Tracer.add("console", console_tracer)
|
106
|
+
|
107
|
+
ptrace = PromptyTracer()
|
108
|
+
Tracer.add("prompty", ptrace.tracer)
|
109
|
+
|
110
|
+
if chat:
|
111
|
+
chat_mode(str(prompt_path))
|
112
|
+
else:
|
113
|
+
execute(str(prompt_path), raw=verbose)
|
114
|
+
|
115
|
+
|
116
|
+
if __name__ == "__main__":
|
117
|
+
chat_mode(source="./tests/prompts/basic.prompt")
|