prompty 0.1.9__py3-none-any.whl → 0.1.33__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/core.py CHANGED
@@ -1,14 +1,19 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import os
4
- import re
5
- import yaml
6
- import json
7
- import abc
8
4
  from pathlib import Path
9
- from .tracer import Tracer, trace, to_dict
5
+
6
+ from .tracer import Tracer, to_dict
10
7
  from pydantic import BaseModel, Field, FilePath
11
- from typing import Iterator, List, Literal, Dict, Callable, Set
8
+ from typing import AsyncIterator, Iterator, List, Literal, Dict, Callable, Set, Tuple
9
+
10
+ from .utils import load_json, load_json_async
11
+
12
+
13
+ class ToolCall(BaseModel):
14
+ id: str
15
+ name: str
16
+ arguments: str
12
17
 
13
18
 
14
19
  class PropertySettings(BaseModel):
@@ -188,33 +193,74 @@ class Prompty(BaseModel):
188
193
  d[k] = v
189
194
  return d
190
195
 
196
+ @staticmethod
197
+ def hoist_base_prompty(top: Prompty, base: Prompty) -> Prompty:
198
+ top.name = base.name if top.name == "" else top.name
199
+ top.description = base.description if top.description == "" else top.description
200
+ top.authors = list(set(base.authors + top.authors))
201
+ top.tags = list(set(base.tags + top.tags))
202
+ top.version = base.version if top.version == "" else top.version
203
+
204
+ top.model.api = base.model.api if top.model.api == "" else top.model.api
205
+ top.model.configuration = param_hoisting(
206
+ top.model.configuration, base.model.configuration
207
+ )
208
+ top.model.parameters = param_hoisting(
209
+ top.model.parameters, base.model.parameters
210
+ )
211
+ top.model.response = param_hoisting(top.model.response, base.model.response)
212
+
213
+ top.sample = param_hoisting(top.sample, base.sample)
214
+
215
+ top.basePrompty = base
216
+
217
+ return top
218
+
191
219
  @staticmethod
192
220
  def _process_file(file: str, parent: Path) -> any:
193
221
  file = Path(parent / Path(file)).resolve().absolute()
194
222
  if file.exists():
195
- with open(str(file), "r") as f:
196
- items = json.load(f)
197
- if isinstance(items, list):
198
- return [Prompty.normalize(value, parent) for value in items]
199
- elif isinstance(items, dict):
200
- return {
201
- key: Prompty.normalize(value, parent)
202
- for key, value in items.items()
203
- }
204
- else:
205
- return items
223
+ items = load_json(file)
224
+ if isinstance(items, list):
225
+ return [Prompty.normalize(value, parent) for value in items]
226
+ elif isinstance(items, dict):
227
+ return {
228
+ key: Prompty.normalize(value, parent)
229
+ for key, value in items.items()
230
+ }
231
+ else:
232
+ return items
233
+ else:
234
+ raise FileNotFoundError(f"File {file} not found")
235
+
236
+ @staticmethod
237
+ async def _process_file_async(file: str, parent: Path) -> any:
238
+ file = Path(parent / Path(file)).resolve().absolute()
239
+ if file.exists():
240
+ items = await load_json_async(file)
241
+ if isinstance(items, list):
242
+ return [Prompty.normalize(value, parent) for value in items]
243
+ elif isinstance(items, dict):
244
+ return {
245
+ key: Prompty.normalize(value, parent)
246
+ for key, value in items.items()
247
+ }
248
+ else:
249
+ return items
206
250
  else:
207
251
  raise FileNotFoundError(f"File {file} not found")
208
252
 
209
253
  @staticmethod
210
- def _process_env(variable: str, env_error=True) -> any:
254
+ def _process_env(variable: str, env_error=True, default: str = None) -> any:
211
255
  if variable in os.environ.keys():
212
256
  return os.environ[variable]
213
257
  else:
258
+ if default:
259
+ return default
214
260
  if env_error:
215
261
  raise ValueError(f"Variable {variable} not found in environment")
216
- else:
217
- return ""
262
+
263
+ return ""
218
264
 
219
265
  @staticmethod
220
266
  def normalize(attribute: any, parent: Path, env_error=True) -> any:
@@ -224,30 +270,15 @@ class Prompty(BaseModel):
224
270
  # check if env or file
225
271
  variable = attribute[2:-1].split(":")
226
272
  if variable[0] == "env" and len(variable) > 1:
227
- return Prompty._process_env(variable[1], env_error)
273
+ return Prompty._process_env(
274
+ variable[1],
275
+ env_error,
276
+ variable[2] if len(variable) > 2 else None,
277
+ )
228
278
  elif variable[0] == "file" and len(variable) > 1:
229
279
  return Prompty._process_file(variable[1], parent)
230
280
  else:
231
- # old way of doing things for back compatibility
232
- v = Prompty._process_env(variable[0], False)
233
- if len(v) == 0:
234
- if len(variable) > 1:
235
- return variable[1]
236
- else:
237
- if env_error:
238
- raise ValueError(
239
- f"Variable {variable[0]} not found in environment"
240
- )
241
- else:
242
- return v
243
- else:
244
- return v
245
- elif (
246
- attribute.startswith("file:")
247
- and Path(parent / attribute.split(":")[1]).exists()
248
- ):
249
- # old way of doing things for back compatibility
250
- return Prompty._process_file(attribute.split(":")[1], parent)
281
+ raise ValueError(f"Invalid attribute format ({attribute})")
251
282
  else:
252
283
  return attribute
253
284
  elif isinstance(attribute, list):
@@ -260,6 +291,35 @@ class Prompty(BaseModel):
260
291
  else:
261
292
  return attribute
262
293
 
294
+ @staticmethod
295
+ async def normalize_async(attribute: any, parent: Path, env_error=True) -> any:
296
+ if isinstance(attribute, str):
297
+ attribute = attribute.strip()
298
+ if attribute.startswith("${") and attribute.endswith("}"):
299
+ # check if env or file
300
+ variable = attribute[2:-1].split(":")
301
+ if variable[0] == "env" and len(variable) > 1:
302
+ return Prompty._process_env(
303
+ variable[1],
304
+ env_error,
305
+ variable[2] if len(variable) > 2 else None,
306
+ )
307
+ elif variable[0] == "file" and len(variable) > 1:
308
+ return await Prompty._process_file_async(variable[1], parent)
309
+ else:
310
+ raise ValueError(f"Invalid attribute format ({attribute})")
311
+ else:
312
+ return attribute
313
+ elif isinstance(attribute, list):
314
+ return [await Prompty.normalize_async(value, parent) for value in attribute]
315
+ elif isinstance(attribute, dict):
316
+ return {
317
+ key: await Prompty.normalize_async(value, parent)
318
+ for key, value in attribute.items()
319
+ }
320
+ else:
321
+ return attribute
322
+
263
323
 
264
324
  def param_hoisting(
265
325
  top: Dict[str, any], bottom: Dict[str, any], top_key: str = None
@@ -274,183 +334,6 @@ def param_hoisting(
274
334
  return new_dict
275
335
 
276
336
 
277
- class Invoker(abc.ABC):
278
- """Abstract class for Invoker
279
-
280
- Attributes
281
- ----------
282
- prompty : Prompty
283
- The prompty object
284
- name : str
285
- The name of the invoker
286
-
287
- """
288
-
289
- def __init__(self, prompty: Prompty) -> None:
290
- self.prompty = prompty
291
- self.name = self.__class__.__name__
292
-
293
- @abc.abstractmethod
294
- def invoke(self, data: any) -> any:
295
- """Abstract method to invoke the invoker
296
-
297
- Parameters
298
- ----------
299
- data : any
300
- The data to be invoked
301
-
302
- Returns
303
- -------
304
- any
305
- The invoked
306
- """
307
- pass
308
-
309
- @trace
310
- def __call__(self, data: any) -> any:
311
- """Method to call the invoker
312
-
313
- Parameters
314
- ----------
315
- data : any
316
- The data to be invoked
317
-
318
- Returns
319
- -------
320
- any
321
- The invoked
322
- """
323
- return self.invoke(data)
324
-
325
-
326
- class InvokerFactory:
327
- """Factory class for Invoker"""
328
-
329
- _renderers: Dict[str, Invoker] = {}
330
- _parsers: Dict[str, Invoker] = {}
331
- _executors: Dict[str, Invoker] = {}
332
- _processors: Dict[str, Invoker] = {}
333
-
334
- @classmethod
335
- def register_renderer(cls, name: str) -> Callable:
336
- def inner_wrapper(wrapped_class: Invoker) -> Callable:
337
- cls._renderers[name] = wrapped_class
338
- return wrapped_class
339
-
340
- return inner_wrapper
341
-
342
- @classmethod
343
- def register_parser(cls, name: str) -> Callable:
344
- def inner_wrapper(wrapped_class: Invoker) -> Callable:
345
- cls._parsers[name] = wrapped_class
346
- return wrapped_class
347
-
348
- return inner_wrapper
349
-
350
- @classmethod
351
- def register_executor(cls, name: str) -> Callable:
352
- def inner_wrapper(wrapped_class: Invoker) -> Callable:
353
- cls._executors[name] = wrapped_class
354
- return wrapped_class
355
-
356
- return inner_wrapper
357
-
358
- @classmethod
359
- def register_processor(cls, name: str) -> Callable:
360
- def inner_wrapper(wrapped_class: Invoker) -> Callable:
361
- cls._processors[name] = wrapped_class
362
- return wrapped_class
363
-
364
- return inner_wrapper
365
-
366
- @classmethod
367
- def create_renderer(cls, name: str, prompty: Prompty) -> Invoker:
368
- if name not in cls._renderers:
369
- raise ValueError(f"Renderer {name} not found")
370
- return cls._renderers[name](prompty)
371
-
372
- @classmethod
373
- def create_parser(cls, name: str, prompty: Prompty) -> Invoker:
374
- if name not in cls._parsers:
375
- raise ValueError(f"Parser {name} not found")
376
- return cls._parsers[name](prompty)
377
-
378
- @classmethod
379
- def create_executor(cls, name: str, prompty: Prompty) -> Invoker:
380
- if name not in cls._executors:
381
- raise ValueError(f"Executor {name} not found")
382
- return cls._executors[name](prompty)
383
-
384
- @classmethod
385
- def create_processor(cls, name: str, prompty: Prompty) -> Invoker:
386
- if name not in cls._processors:
387
- raise ValueError(f"Processor {name} not found")
388
- return cls._processors[name](prompty)
389
-
390
-
391
- @InvokerFactory.register_renderer("NOOP")
392
- @InvokerFactory.register_parser("NOOP")
393
- @InvokerFactory.register_executor("NOOP")
394
- @InvokerFactory.register_processor("NOOP")
395
- @InvokerFactory.register_parser("prompty.embedding")
396
- @InvokerFactory.register_parser("prompty.image")
397
- @InvokerFactory.register_parser("prompty.completion")
398
- class NoOp(Invoker):
399
- def invoke(self, data: any) -> any:
400
- return data
401
-
402
-
403
- class Frontmatter:
404
- """Frontmatter class to extract frontmatter from string."""
405
-
406
- _yaml_delim = r"(?:---|\+\+\+)"
407
- _yaml = r"(.*?)"
408
- _content = r"\s*(.+)$"
409
- _re_pattern = r"^\s*" + _yaml_delim + _yaml + _yaml_delim + _content
410
- _regex = re.compile(_re_pattern, re.S | re.M)
411
-
412
- @classmethod
413
- def read_file(cls, path):
414
- """Returns dict with separated frontmatter from file.
415
-
416
- Parameters
417
- ----------
418
- path : str
419
- The path to the file
420
- """
421
- with open(path, encoding="utf-8") as file:
422
- file_contents = file.read()
423
- return cls.read(file_contents)
424
-
425
- @classmethod
426
- def read(cls, string):
427
- """Returns dict with separated frontmatter from string.
428
-
429
- Parameters
430
- ----------
431
- string : str
432
- The string to extract frontmatter from
433
-
434
-
435
- Returns
436
- -------
437
- dict
438
- The separated frontmatter
439
- """
440
- fmatter = ""
441
- body = ""
442
- result = cls._regex.search(string)
443
-
444
- if result:
445
- fmatter = result.group(1)
446
- body = result.group(2)
447
- return {
448
- "attributes": yaml.load(fmatter, Loader=yaml.FullLoader),
449
- "body": body,
450
- "frontmatter": fmatter,
451
- }
452
-
453
-
454
337
  class PromptyStream(Iterator):
455
338
  """PromptyStream class to iterate over LLM stream.
456
339
  Necessary for Prompty to handle streaming data when tracing."""
@@ -474,8 +357,42 @@ class PromptyStream(Iterator):
474
357
  except StopIteration:
475
358
  # StopIteration is raised
476
359
  # contents are exhausted
477
- if len(self.items) > 0:
478
- with Tracer.start(f"{self.name}.PromptyStream") as trace:
479
- trace("items", [to_dict(s) for s in self.items])
360
+ if len(self.items) > 0:
361
+ with Tracer.start("PromptyStream") as trace:
362
+ trace("signature", f"{self.name}.PromptyStream")
363
+ trace("inputs", "None")
364
+ trace("result", [to_dict(s) for s in self.items])
480
365
 
481
366
  raise StopIteration
367
+
368
+
369
+ class AsyncPromptyStream(AsyncIterator):
370
+ """AsyncPromptyStream class to iterate over LLM stream.
371
+ Necessary for Prompty to handle streaming data when tracing."""
372
+
373
+ def __init__(self, name: str, iterator: AsyncIterator):
374
+ self.name = name
375
+ self.iterator = iterator
376
+ self.items: List[any] = []
377
+ self.__name__ = "AsyncPromptyStream"
378
+
379
+ def __aiter__(self):
380
+ return self
381
+
382
+ async def __anext__(self):
383
+ try:
384
+ # enumerate but add to list
385
+ o = await self.iterator.__anext__()
386
+ self.items.append(o)
387
+ return o
388
+
389
+ except StopAsyncIteration:
390
+ # StopIteration is raised
391
+ # contents are exhausted
392
+ if len(self.items) > 0:
393
+ with Tracer.start("AsyncPromptyStream") as trace:
394
+ trace("signature", f"{self.name}.AsyncPromptyStream")
395
+ trace("inputs", "None")
396
+ trace("result", [to_dict(s) for s in self.items])
397
+
398
+ raise StopAsyncIteration
prompty/invoker.py ADDED
@@ -0,0 +1,297 @@
1
+ import abc
2
+ from .tracer import trace
3
+ from .core import Prompty
4
+ from typing import Callable, Dict, Literal
5
+
6
+
7
+ class Invoker(abc.ABC):
8
+ """Abstract class for Invoker
9
+
10
+ Attributes
11
+ ----------
12
+ prompty : Prompty
13
+ The prompty object
14
+ name : str
15
+ The name of the invoker
16
+
17
+ """
18
+
19
+ def __init__(self, prompty: Prompty) -> None:
20
+ self.prompty = prompty
21
+ self.name = self.__class__.__name__
22
+
23
+ @abc.abstractmethod
24
+ def invoke(self, data: any) -> any:
25
+ """Abstract method to invoke the invoker
26
+
27
+ Parameters
28
+ ----------
29
+ data : any
30
+ The data to be invoked
31
+
32
+ Returns
33
+ -------
34
+ any
35
+ The invoked
36
+ """
37
+ pass
38
+
39
+ @abc.abstractmethod
40
+ async def invoke_async(self, data: any) -> any:
41
+ """Abstract method to invoke the invoker asynchronously
42
+
43
+ Parameters
44
+ ----------
45
+ data : any
46
+ The data to be invoked
47
+
48
+ Returns
49
+ -------
50
+ any
51
+ The invoked
52
+ """
53
+ pass
54
+
55
+ @trace
56
+ def run(self, data: any) -> any:
57
+ """Method to run the invoker
58
+
59
+ Parameters
60
+ ----------
61
+ data : any
62
+ The data to be invoked
63
+
64
+ Returns
65
+ -------
66
+ any
67
+ The invoked
68
+ """
69
+ return self.invoke(data)
70
+
71
+ @trace
72
+ async def run_async(self, data: any) -> any:
73
+ """Method to run the invoker asynchronously
74
+
75
+ Parameters
76
+ ----------
77
+ data : any
78
+ The data to be invoked
79
+
80
+ Returns
81
+ -------
82
+ any
83
+ The invoked
84
+ """
85
+ return await self.invoke_async(data)
86
+
87
+
88
+ class InvokerFactory:
89
+ """Factory class for Invoker"""
90
+
91
+ _renderers: Dict[str, Invoker] = {}
92
+ _parsers: Dict[str, Invoker] = {}
93
+ _executors: Dict[str, Invoker] = {}
94
+ _processors: Dict[str, Invoker] = {}
95
+
96
+ @classmethod
97
+ def add_renderer(cls, name: str, invoker: Invoker) -> None:
98
+ cls._renderers[name] = invoker
99
+
100
+ @classmethod
101
+ def add_parser(cls, name: str, invoker: Invoker) -> None:
102
+ cls._parsers[name] = invoker
103
+
104
+ @classmethod
105
+ def add_executor(cls, name: str, invoker: Invoker) -> None:
106
+ cls._executors[name] = invoker
107
+
108
+ @classmethod
109
+ def add_processor(cls, name: str, invoker: Invoker) -> None:
110
+ cls._processors[name] = invoker
111
+
112
+ @classmethod
113
+ def register_renderer(cls, name: str) -> Callable:
114
+ def inner_wrapper(wrapped_class: Invoker) -> Callable:
115
+ cls._renderers[name] = wrapped_class
116
+ return wrapped_class
117
+
118
+ return inner_wrapper
119
+
120
+ @classmethod
121
+ def register_parser(cls, name: str) -> Callable:
122
+ def inner_wrapper(wrapped_class: Invoker) -> Callable:
123
+ cls._parsers[name] = wrapped_class
124
+ return wrapped_class
125
+
126
+ return inner_wrapper
127
+
128
+ @classmethod
129
+ def register_executor(cls, name: str) -> Callable:
130
+ def inner_wrapper(wrapped_class: Invoker) -> Callable:
131
+ cls._executors[name] = wrapped_class
132
+ return wrapped_class
133
+
134
+ return inner_wrapper
135
+
136
+ @classmethod
137
+ def register_processor(cls, name: str) -> Callable:
138
+ def inner_wrapper(wrapped_class: Invoker) -> Callable:
139
+ cls._processors[name] = wrapped_class
140
+ return wrapped_class
141
+
142
+ return inner_wrapper
143
+
144
+ @classmethod
145
+ def _get_name(
146
+ cls,
147
+ type: Literal["renderer", "parser", "executor", "processor"],
148
+ prompty: Prompty,
149
+ ) -> str:
150
+ if type == "renderer":
151
+ return prompty.template.type
152
+ elif type == "parser":
153
+ return f"{prompty.template.parser}.{prompty.model.api}"
154
+ elif type == "executor":
155
+ return prompty.model.configuration["type"]
156
+ elif type == "processor":
157
+ return prompty.model.configuration["type"]
158
+ else:
159
+ raise ValueError(f"Type {type} not found")
160
+
161
+ @classmethod
162
+ def _get_invoker(
163
+ cls,
164
+ type: Literal["renderer", "parser", "executor", "processor"],
165
+ prompty: Prompty,
166
+ ) -> Invoker:
167
+ if type == "renderer":
168
+ name = prompty.template.type
169
+ if name not in cls._renderers:
170
+ raise ValueError(f"Renderer {name} not found")
171
+
172
+ return cls._renderers[name](prompty)
173
+
174
+ elif type == "parser":
175
+ name = f"{prompty.template.parser}.{prompty.model.api}"
176
+ if name not in cls._parsers:
177
+ raise ValueError(f"Parser {name} not found")
178
+
179
+ return cls._parsers[name](prompty)
180
+
181
+ elif type == "executor":
182
+ name = prompty.model.configuration["type"]
183
+ if name not in cls._executors:
184
+ raise ValueError(f"Executor {name} not found")
185
+
186
+ return cls._executors[name](prompty)
187
+
188
+ elif type == "processor":
189
+ name = prompty.model.configuration["type"]
190
+ if name not in cls._processors:
191
+ raise ValueError(f"Processor {name} not found")
192
+
193
+ return cls._processors[name](prompty)
194
+
195
+ else:
196
+ raise ValueError(f"Type {type} not found")
197
+
198
+ @classmethod
199
+ def run(
200
+ cls,
201
+ type: Literal["renderer", "parser", "executor", "processor"],
202
+ prompty: Prompty,
203
+ data: any,
204
+ default: any = None,
205
+ ):
206
+ name = cls._get_name(type, prompty)
207
+ if name.startswith("NOOP") and default != None:
208
+ return default
209
+ elif name.startswith("NOOP"):
210
+ return data
211
+
212
+ invoker = cls._get_invoker(type, prompty)
213
+ value = invoker.run(data)
214
+ return value
215
+
216
+ @classmethod
217
+ async def run_async(
218
+ cls,
219
+ type: Literal["renderer", "parser", "executor", "processor"],
220
+ prompty: Prompty,
221
+ data: any,
222
+ default: any = None,
223
+ ):
224
+ name = cls._get_name(type, prompty)
225
+ if name.startswith("NOOP") and default != None:
226
+ return default
227
+ elif name.startswith("NOOP"):
228
+ return data
229
+ invoker = cls._get_invoker(type, prompty)
230
+ value = await invoker.run_async(data)
231
+ return value
232
+
233
+ @classmethod
234
+ def run_renderer(cls, prompty: Prompty, data: any, default: any = None) -> any:
235
+ return cls.run("renderer", prompty, data, default)
236
+
237
+ @classmethod
238
+ async def run_renderer_async(
239
+ cls, prompty: Prompty, data: any, default: any = None
240
+ ) -> any:
241
+ return await cls.run_async("renderer", prompty, data, default)
242
+
243
+ @classmethod
244
+ def run_parser(cls, prompty: Prompty, data: any, default: any = None) -> any:
245
+ return cls.run("parser", prompty, data, default)
246
+
247
+ @classmethod
248
+ async def run_parser_async(
249
+ cls, prompty: Prompty, data: any, default: any = None
250
+ ) -> any:
251
+ return await cls.run_async("parser", prompty, data, default)
252
+
253
+ @classmethod
254
+ def run_executor(cls, prompty: Prompty, data: any, default: any = None) -> any:
255
+ return cls.run("executor", prompty, data, default)
256
+
257
+ @classmethod
258
+ async def run_executor_async(
259
+ cls, prompty: Prompty, data: any, default: any = None
260
+ ) -> any:
261
+ return await cls.run_async("executor", prompty, data, default)
262
+
263
+ @classmethod
264
+ def run_processor(cls, prompty: Prompty, data: any, default: any = None) -> any:
265
+ return cls.run("processor", prompty, data, default)
266
+
267
+ @classmethod
268
+ async def run_processor_async(
269
+ cls, prompty: Prompty, data: any, default: any = None
270
+ ) -> any:
271
+ return await cls.run_async("processor", prompty, data, default)
272
+
273
+
274
+ class InvokerException(Exception):
275
+ """Exception class for Invoker"""
276
+
277
+ def __init__(self, message: str, type: str) -> None:
278
+ super().__init__(message)
279
+ self.type = type
280
+
281
+ def __str__(self) -> str:
282
+ return f"{super().__str__()}. Make sure to pip install any necessary package extras (i.e. could be something like `pip install prompty[{self.type}]`) for {self.type} as well as import the appropriate invokers (i.e. could be something like `import prompty.{self.type}`)."
283
+
284
+
285
+ @InvokerFactory.register_renderer("NOOP")
286
+ @InvokerFactory.register_parser("NOOP")
287
+ @InvokerFactory.register_executor("NOOP")
288
+ @InvokerFactory.register_processor("NOOP")
289
+ @InvokerFactory.register_parser("prompty.embedding")
290
+ @InvokerFactory.register_parser("prompty.image")
291
+ @InvokerFactory.register_parser("prompty.completion")
292
+ class NoOp(Invoker):
293
+ def invoke(self, data: any) -> any:
294
+ return data
295
+
296
+ async def invoke_async(self, data: str) -> str:
297
+ return self.invoke(data)