prompty 0.1.10__py3-none-any.whl → 0.1.34__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
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 AsyncIterator, 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,9 +357,11 @@ 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
482
367
 
@@ -501,11 +386,13 @@ class AsyncPromptyStream(AsyncIterator):
501
386
  self.items.append(o)
502
387
  return o
503
388
 
504
- except StopIteration:
389
+ except StopAsyncIteration:
505
390
  # StopIteration is raised
506
391
  # contents are exhausted
507
392
  if len(self.items) > 0:
508
- with Tracer.start(f"{self.name}.AsyncPromptyStream") as trace:
509
- trace("items", [to_dict(s) for s in self.items])
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])
510
397
 
511
- raise StopIteration
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)
@@ -0,0 +1,10 @@
1
+ # __init__.py
2
+ from prompty.invoker import InvokerException
3
+
4
+ try:
5
+ from .executor import OpenAIExecutor
6
+ from .processor import OpenAIProcessor
7
+ except ImportError:
8
+ raise InvokerException(
9
+ "Error registering OpenAIExecutor and OpenAIProcessor", "openai"
10
+ )