prompty 0.1.33__tar.gz → 0.1.36__tar.gz

Sign up to get free protection for your applications and to get access to all the features.
Files changed (82) hide show
  1. {prompty-0.1.33 → prompty-0.1.36}/PKG-INFO +4 -3
  2. {prompty-0.1.33 → prompty-0.1.36}/prompty/azure/processor.py +2 -0
  3. prompty-0.1.36/prompty/azure_beta/__init__.py +11 -0
  4. prompty-0.1.36/prompty/azure_beta/executor.py +281 -0
  5. {prompty-0.1.33 → prompty-0.1.36}/prompty/serverless/executor.py +91 -4
  6. {prompty-0.1.33 → prompty-0.1.36}/prompty/serverless/processor.py +38 -3
  7. {prompty-0.1.33 → prompty-0.1.36}/prompty/tracer.py +16 -20
  8. {prompty-0.1.33 → prompty-0.1.36}/pyproject.toml +5 -5
  9. prompty-0.1.36/tests/prompts/structured_output.prompty +24 -0
  10. prompty-0.1.36/tests/prompts/structured_output.prompty.execution.json +68 -0
  11. prompty-0.1.36/tests/prompts/structured_output_schema.json +29 -0
  12. prompty-0.1.36/tests/prompts/sub/sub/__init__.py +0 -0
  13. {prompty-0.1.33 → prompty-0.1.36}/tests/test_common.py +1 -0
  14. {prompty-0.1.33 → prompty-0.1.36}/tests/test_execute.py +10 -0
  15. {prompty-0.1.33 → prompty-0.1.36}/tests/test_tracing.py +36 -2
  16. {prompty-0.1.33 → prompty-0.1.36}/LICENSE +0 -0
  17. {prompty-0.1.33 → prompty-0.1.36}/README.md +0 -0
  18. {prompty-0.1.33 → prompty-0.1.36}/prompty/__init__.py +0 -0
  19. {prompty-0.1.33 → prompty-0.1.36}/prompty/azure/__init__.py +0 -0
  20. {prompty-0.1.33 → prompty-0.1.36}/prompty/azure/executor.py +0 -0
  21. {prompty-0.1.33 → prompty-0.1.36}/prompty/cli.py +0 -0
  22. {prompty-0.1.33 → prompty-0.1.36}/prompty/core.py +0 -0
  23. {prompty-0.1.33 → prompty-0.1.36}/prompty/invoker.py +0 -0
  24. {prompty-0.1.33 → prompty-0.1.36}/prompty/openai/__init__.py +0 -0
  25. {prompty-0.1.33 → prompty-0.1.36}/prompty/openai/executor.py +0 -0
  26. {prompty-0.1.33 → prompty-0.1.36}/prompty/openai/processor.py +0 -0
  27. {prompty-0.1.33 → prompty-0.1.36}/prompty/parsers.py +0 -0
  28. {prompty-0.1.33 → prompty-0.1.36}/prompty/renderers.py +0 -0
  29. {prompty-0.1.33 → prompty-0.1.36}/prompty/serverless/__init__.py +0 -0
  30. {prompty-0.1.33 → prompty-0.1.36}/prompty/utils.py +0 -0
  31. {prompty-0.1.33/tests/prompts → prompty-0.1.36/tests}/__init__.py +0 -0
  32. {prompty-0.1.33 → prompty-0.1.36}/tests/fake_azure_executor.py +0 -0
  33. {prompty-0.1.33 → prompty-0.1.36}/tests/fake_serverless_executor.py +0 -0
  34. {prompty-0.1.33 → prompty-0.1.36}/tests/generated/1contoso.md +0 -0
  35. {prompty-0.1.33 → prompty-0.1.36}/tests/generated/2contoso.md +0 -0
  36. {prompty-0.1.33 → prompty-0.1.36}/tests/generated/3contoso.md +0 -0
  37. {prompty-0.1.33 → prompty-0.1.36}/tests/generated/4contoso.md +0 -0
  38. {prompty-0.1.33 → prompty-0.1.36}/tests/generated/basic.prompty.md +0 -0
  39. {prompty-0.1.33 → prompty-0.1.36}/tests/generated/camping.jpg +0 -0
  40. {prompty-0.1.33 → prompty-0.1.36}/tests/generated/context.prompty.md +0 -0
  41. {prompty-0.1.33 → prompty-0.1.36}/tests/generated/contoso_multi.md +0 -0
  42. {prompty-0.1.33 → prompty-0.1.36}/tests/generated/faithfulness.prompty.md +0 -0
  43. {prompty-0.1.33 → prompty-0.1.36}/tests/generated/groundedness.prompty.md +0 -0
  44. {prompty-0.1.33 → prompty-0.1.36}/tests/hello_world-goodbye_world-hello_again.embedding.json +0 -0
  45. {prompty-0.1.33 → prompty-0.1.36}/tests/hello_world.embedding.json +0 -0
  46. {prompty-0.1.33/tests/prompts/sub → prompty-0.1.36/tests/prompts}/__init__.py +0 -0
  47. {prompty-0.1.33 → prompty-0.1.36}/tests/prompts/basic.prompty +0 -0
  48. {prompty-0.1.33 → prompty-0.1.36}/tests/prompts/basic.prompty.execution.json +0 -0
  49. {prompty-0.1.33 → prompty-0.1.36}/tests/prompts/basic_json_output.prompty +0 -0
  50. {prompty-0.1.33 → prompty-0.1.36}/tests/prompts/camping.jpg +0 -0
  51. {prompty-0.1.33 → prompty-0.1.36}/tests/prompts/chat.prompty +0 -0
  52. {prompty-0.1.33 → prompty-0.1.36}/tests/prompts/context.json +0 -0
  53. {prompty-0.1.33 → prompty-0.1.36}/tests/prompts/context.prompty +0 -0
  54. {prompty-0.1.33 → prompty-0.1.36}/tests/prompts/context.prompty.execution.json +0 -0
  55. {prompty-0.1.33 → prompty-0.1.36}/tests/prompts/embedding.prompty +0 -0
  56. {prompty-0.1.33 → prompty-0.1.36}/tests/prompts/embedding.prompty.execution.json +0 -0
  57. {prompty-0.1.33 → prompty-0.1.36}/tests/prompts/evaluation.prompty +0 -0
  58. {prompty-0.1.33 → prompty-0.1.36}/tests/prompts/faithfulness.prompty +0 -0
  59. {prompty-0.1.33 → prompty-0.1.36}/tests/prompts/faithfulness.prompty.execution.json +0 -0
  60. {prompty-0.1.33 → prompty-0.1.36}/tests/prompts/fake.prompty +0 -0
  61. {prompty-0.1.33 → prompty-0.1.36}/tests/prompts/funcfile.json +0 -0
  62. {prompty-0.1.33 → prompty-0.1.36}/tests/prompts/funcfile.prompty +0 -0
  63. {prompty-0.1.33 → prompty-0.1.36}/tests/prompts/functions.prompty +0 -0
  64. {prompty-0.1.33 → prompty-0.1.36}/tests/prompts/functions.prompty.execution.json +0 -0
  65. {prompty-0.1.33 → prompty-0.1.36}/tests/prompts/groundedness.prompty +0 -0
  66. {prompty-0.1.33 → prompty-0.1.36}/tests/prompts/groundedness.prompty.execution.json +0 -0
  67. {prompty-0.1.33 → prompty-0.1.36}/tests/prompts/prompty.json +0 -0
  68. {prompty-0.1.33 → prompty-0.1.36}/tests/prompts/serverless.prompty +0 -0
  69. {prompty-0.1.33 → prompty-0.1.36}/tests/prompts/serverless.prompty.execution.json +0 -0
  70. {prompty-0.1.33 → prompty-0.1.36}/tests/prompts/serverless_stream.prompty +0 -0
  71. {prompty-0.1.33 → prompty-0.1.36}/tests/prompts/serverless_stream.prompty.execution.json +0 -0
  72. {prompty-0.1.33 → prompty-0.1.36}/tests/prompts/streaming.prompty +0 -0
  73. {prompty-0.1.33 → prompty-0.1.36}/tests/prompts/streaming.prompty.execution.json +0 -0
  74. {prompty-0.1.33/tests/prompts/sub → prompty-0.1.36/tests/prompts}/sub/__init__.py +0 -0
  75. {prompty-0.1.33 → prompty-0.1.36}/tests/prompts/sub/basic.prompty +0 -0
  76. {prompty-0.1.33 → prompty-0.1.36}/tests/prompts/sub/sub/basic.prompty +0 -0
  77. {prompty-0.1.33 → prompty-0.1.36}/tests/prompts/sub/sub/prompty.json +0 -0
  78. {prompty-0.1.33 → prompty-0.1.36}/tests/prompts/sub/sub/test.py +0 -0
  79. {prompty-0.1.33 → prompty-0.1.36}/tests/prompts/test.py +0 -0
  80. {prompty-0.1.33 → prompty-0.1.36}/tests/prompty.json +0 -0
  81. {prompty-0.1.33 → prompty-0.1.36}/tests/test_factory_invoker.py +0 -0
  82. {prompty-0.1.33 → prompty-0.1.36}/tests/test_path_exec.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: prompty
3
- Version: 0.1.33
3
+ Version: 0.1.36
4
4
  Summary: Prompty is a new asset class and format for LLM prompts that aims to provide observability, understandability, and portability for developers. It includes spec, tooling, and a runtime. This Prompty runtime supports Python
5
5
  Author-Email: Seth Juarez <seth.juarez@microsoft.com>
6
6
  License: MIT
@@ -13,10 +13,11 @@ Requires-Dist: click>=8.1.7
13
13
  Requires-Dist: aiofiles>=24.1.0
14
14
  Provides-Extra: azure
15
15
  Requires-Dist: azure-identity>=1.17.1; extra == "azure"
16
- Requires-Dist: openai>=1.35.10; extra == "azure"
16
+ Requires-Dist: openai>=1.43.0; extra == "azure"
17
17
  Provides-Extra: openai
18
- Requires-Dist: openai>=1.35.10; extra == "openai"
18
+ Requires-Dist: openai>=1.43.0; extra == "openai"
19
19
  Provides-Extra: serverless
20
+ Requires-Dist: azure-identity>=1.17.1; extra == "serverless"
20
21
  Requires-Dist: azure-ai-inference>=1.0.0b3; extra == "serverless"
21
22
  Description-Content-Type: text/markdown
22
23
 
@@ -9,6 +9,8 @@ from openai.types.create_embedding_response import CreateEmbeddingResponse
9
9
 
10
10
  @InvokerFactory.register_processor("azure")
11
11
  @InvokerFactory.register_processor("azure_openai")
12
+ @InvokerFactory.register_processor("azure_beta")
13
+ @InvokerFactory.register_processor("azure_openai_beta")
12
14
  class AzureOpenAIProcessor(Invoker):
13
15
  """Azure OpenAI Processor"""
14
16
 
@@ -0,0 +1,11 @@
1
+ # __init__.py
2
+ from prompty.invoker import InvokerException
3
+
4
+ try:
5
+ from .executor import AzureOpenAIBetaExecutor
6
+ # Reuse the common Azure OpenAI Processor
7
+ from ..azure.processor import AzureOpenAIProcessor
8
+ except ImportError:
9
+ raise InvokerException(
10
+ "Error registering AzureOpenAIBetaExecutor and AzureOpenAIProcessor", "azure_beta"
11
+ )
@@ -0,0 +1,281 @@
1
+ import azure.identity
2
+ import importlib.metadata
3
+ from typing import AsyncIterator, Iterator
4
+ from openai import AzureOpenAI, AsyncAzureOpenAI
5
+
6
+ from prompty.tracer import Tracer
7
+ from ..core import AsyncPromptyStream, Prompty, PromptyStream
8
+ from ..invoker import Invoker, InvokerFactory
9
+ import re
10
+ from datetime import datetime
11
+
12
+ def extract_date(data: str) -> datetime:
13
+ """Extract date from a string
14
+
15
+ Parameters
16
+ ----------
17
+ data : str
18
+ The string containing the date
19
+
20
+ Returns
21
+ -------
22
+ datetime
23
+ The extracted date as a datetime object
24
+ """
25
+
26
+ # Regular expression to find dates in the format YYYY-MM-DD
27
+ date_pattern = re.compile(r'\b\d{4}-\d{2}-\d{2}\b')
28
+ match = date_pattern.search(data)
29
+ if match:
30
+ date_str = match.group(0)
31
+ # Validate the date format
32
+ try:
33
+ return datetime.strptime(date_str, '%Y-%m-%d')
34
+ except ValueError:
35
+ pass
36
+ return None
37
+
38
+ def is_structured_output_available(api_version: str) -> bool:
39
+ """Check if the structured output API is available for the given API version
40
+
41
+ Parameters
42
+ ----------
43
+ api_version : datetime
44
+ The API version
45
+
46
+ Returns
47
+ -------
48
+ bool
49
+ True if the structured output API is available, False otherwise
50
+ """
51
+
52
+ # Define the threshold date
53
+ threshold_api_version_date = datetime(2024, 8, 1)
54
+
55
+ api_version_date = extract_date(api_version)
56
+
57
+ # Check if the API version are on or after the threshold date
58
+ if api_version_date >= threshold_api_version_date:
59
+ return True
60
+ return False
61
+
62
+ VERSION = importlib.metadata.version("prompty")
63
+
64
+
65
+ @InvokerFactory.register_executor("azure_beta")
66
+ @InvokerFactory.register_executor("azure_openai_beta")
67
+ class AzureOpenAIBetaExecutor(Invoker):
68
+ """Azure OpenAI Beta Executor"""
69
+
70
+ def __init__(self, prompty: Prompty) -> None:
71
+ super().__init__(prompty)
72
+ self.kwargs = {
73
+ key: value
74
+ for key, value in self.prompty.model.configuration.items()
75
+ if key != "type"
76
+ }
77
+
78
+ # no key, use default credentials
79
+ if "api_key" not in self.kwargs:
80
+ # managed identity if client id
81
+ if "client_id" in self.kwargs:
82
+ default_credential = azure.identity.ManagedIdentityCredential(
83
+ client_id=self.kwargs.pop("client_id"),
84
+ )
85
+ # default credential
86
+ else:
87
+ default_credential = azure.identity.DefaultAzureCredential(
88
+ exclude_shared_token_cache_credential=True
89
+ )
90
+
91
+ self.kwargs["azure_ad_token_provider"] = (
92
+ azure.identity.get_bearer_token_provider(
93
+ default_credential, "https://cognitiveservices.azure.com/.default"
94
+ )
95
+ )
96
+
97
+ self.api = self.prompty.model.api
98
+ self.api_version = self.prompty.model.configuration["api_version"]
99
+ self.deployment = self.prompty.model.configuration["azure_deployment"]
100
+ self.parameters = self.prompty.model.parameters
101
+
102
+ def invoke(self, data: any) -> any:
103
+ """Invoke the Azure OpenAI API
104
+
105
+ Parameters
106
+ ----------
107
+ data : any
108
+ The data to send to the Azure OpenAI API
109
+
110
+ Returns
111
+ -------
112
+ any
113
+ The response from the Azure OpenAI API
114
+ """
115
+
116
+ with Tracer.start("AzureOpenAI") as trace:
117
+ trace("type", "LLM")
118
+ trace("signature", "AzureOpenAI.ctor")
119
+ trace("description", "Azure OpenAI Constructor")
120
+ trace("inputs", self.kwargs)
121
+ client = AzureOpenAI(
122
+ default_headers={
123
+ "User-Agent": f"prompty/{VERSION}",
124
+ "x-ms-useragent": f"prompty/{VERSION}",
125
+ },
126
+ **self.kwargs,
127
+ )
128
+ trace("result", client)
129
+
130
+ with Tracer.start("create") as trace:
131
+ trace("type", "LLM")
132
+ trace("description", "Azure OpenAI Client")
133
+
134
+ if self.api == "chat":
135
+ # We can only verify the API version as the model and its version are not part of prompty configuration
136
+ # Should be gpt-4o and 2024-08-06 or later
137
+ choose_beta = is_structured_output_available(self.api_version)
138
+ if choose_beta:
139
+ trace("signature", "AzureOpenAI.beta.chat.completions.parse")
140
+ else:
141
+ trace("signature", "AzureOpenAI.chat.completions.create")
142
+
143
+ args = {
144
+ "model": self.deployment,
145
+ "messages": data if isinstance(data, list) else [data],
146
+ **self.parameters,
147
+ }
148
+ trace("inputs", args)
149
+ if choose_beta:
150
+ response = client.beta.chat.completions.parse(**args)
151
+ else:
152
+ response = client.chat.completions.create(**args)
153
+ trace("result", response)
154
+
155
+ elif self.api == "completion":
156
+ trace("signature", "AzureOpenAI.completions.create")
157
+ args = {
158
+ "prompt": data,
159
+ "model": self.deployment,
160
+ **self.parameters,
161
+ }
162
+ trace("inputs", args)
163
+ response = client.completions.create(**args)
164
+ trace("result", response)
165
+
166
+ elif self.api == "embedding":
167
+ trace("signature", "AzureOpenAI.embeddings.create")
168
+ args = {
169
+ "input": data if isinstance(data, list) else [data],
170
+ "model": self.deployment,
171
+ **self.parameters,
172
+ }
173
+ trace("inputs", args)
174
+ response = client.embeddings.create(**args)
175
+ trace("result", response)
176
+
177
+ elif self.api == "image":
178
+ trace("signature", "AzureOpenAI.images.generate")
179
+ args = {
180
+ "prompt": data,
181
+ "model": self.deployment,
182
+ **self.parameters,
183
+ }
184
+ trace("inputs", args)
185
+ response = client.images.generate.create(**args)
186
+ trace("result", response)
187
+
188
+ # stream response
189
+ if isinstance(response, Iterator):
190
+ if self.api == "chat":
191
+ # TODO: handle the case where there might be no usage in the stream
192
+ return PromptyStream("AzureOpenAIBetaExecutor", response)
193
+ else:
194
+ return PromptyStream("AzureOpenAIBetaExecutor", response)
195
+ else:
196
+ return response
197
+
198
+ async def invoke_async(self, data: str) -> str:
199
+ """Invoke the Prompty Chat Parser (Async)
200
+
201
+ Parameters
202
+ ----------
203
+ data : str
204
+ The data to parse
205
+
206
+ Returns
207
+ -------
208
+ str
209
+ The parsed data
210
+ """
211
+ with Tracer.start("AzureOpenAIAsync") as trace:
212
+ trace("type", "LLM")
213
+ trace("signature", "AzureOpenAIAsync.ctor")
214
+ trace("description", "Async Azure OpenAI Constructor")
215
+ trace("inputs", self.kwargs)
216
+ client = AsyncAzureOpenAI(
217
+ default_headers={
218
+ "User-Agent": f"prompty/{VERSION}",
219
+ "x-ms-useragent": f"prompty/{VERSION}",
220
+ },
221
+ **self.kwargs,
222
+ )
223
+ trace("result", client)
224
+
225
+ with Tracer.start("create") as trace:
226
+ trace("type", "LLM")
227
+ trace("description", "Azure OpenAI Client")
228
+
229
+ if self.api == "chat":
230
+ trace("signature", "AzureOpenAIAsync.chat.completions.create")
231
+ args = {
232
+ "model": self.deployment,
233
+ "messages": data if isinstance(data, list) else [data],
234
+ **self.parameters,
235
+ }
236
+ trace("inputs", args)
237
+ response = await client.chat.completions.create(**args)
238
+ trace("result", response)
239
+
240
+ elif self.api == "completion":
241
+ trace("signature", "AzureOpenAIAsync.completions.create")
242
+ args = {
243
+ "prompt": data,
244
+ "model": self.deployment,
245
+ **self.parameters,
246
+ }
247
+ trace("inputs", args)
248
+ response = await client.completions.create(**args)
249
+ trace("result", response)
250
+
251
+ elif self.api == "embedding":
252
+ trace("signature", "AzureOpenAIAsync.embeddings.create")
253
+ args = {
254
+ "input": data if isinstance(data, list) else [data],
255
+ "model": self.deployment,
256
+ **self.parameters,
257
+ }
258
+ trace("inputs", args)
259
+ response = await client.embeddings.create(**args)
260
+ trace("result", response)
261
+
262
+ elif self.api == "image":
263
+ trace("signature", "AzureOpenAIAsync.images.generate")
264
+ args = {
265
+ "prompt": data,
266
+ "model": self.deployment,
267
+ **self.parameters,
268
+ }
269
+ trace("inputs", args)
270
+ response = await client.images.generate.create(**args)
271
+ trace("result", response)
272
+
273
+ # stream response
274
+ if isinstance(response, AsyncIterator):
275
+ if self.api == "chat":
276
+ # TODO: handle the case where there might be no usage in the stream
277
+ return AsyncPromptyStream("AzureOpenAIBetaExecutorAsync", response)
278
+ else:
279
+ return AsyncPromptyStream("AzureOpenAIBetaExecutorAsync", response)
280
+ else:
281
+ return response
@@ -1,3 +1,4 @@
1
+ import azure.identity
1
2
  import importlib.metadata
2
3
  from typing import Iterator
3
4
  from azure.core.credentials import AzureKeyCredential
@@ -5,6 +6,11 @@ from azure.ai.inference import (
5
6
  ChatCompletionsClient,
6
7
  EmbeddingsClient,
7
8
  )
9
+
10
+ from azure.ai.inference.aio import (
11
+ ChatCompletionsClient as AsyncChatCompletionsClient,
12
+ EmbeddingsClient as AsyncEmbeddingsClient,
13
+ )
8
14
  from azure.ai.inference.models import (
9
15
  StreamingChatCompletions,
10
16
  AsyncStreamingChatCompletions,
@@ -24,10 +30,18 @@ class ServerlessExecutor(Invoker):
24
30
  def __init__(self, prompty: Prompty) -> None:
25
31
  super().__init__(prompty)
26
32
 
27
- # serverless configuration
28
33
  self.endpoint = self.prompty.model.configuration["endpoint"]
29
34
  self.model = self.prompty.model.configuration["model"]
30
- self.key = self.prompty.model.configuration["key"]
35
+
36
+ # no key, use default credentials
37
+ if "key" not in self.kwargs:
38
+ self.credential = azure.identity.DefaultAzureCredential(
39
+ exclude_shared_token_cache_credential=True
40
+ )
41
+ else:
42
+ self.credential = AzureKeyCredential(
43
+ self.prompty.model.configuration["key"]
44
+ )
31
45
 
32
46
  # api type
33
47
  self.api = self.prompty.model.api
@@ -64,7 +78,7 @@ class ServerlessExecutor(Invoker):
64
78
 
65
79
  cargs = {
66
80
  "endpoint": self.endpoint,
67
- "credential": AzureKeyCredential(self.key),
81
+ "credential": self.credential,
68
82
  }
69
83
 
70
84
  if self.api == "chat":
@@ -150,4 +164,77 @@ class ServerlessExecutor(Invoker):
150
164
  str
151
165
  The parsed data
152
166
  """
153
- return self.invoke(data)
167
+ cargs = {
168
+ "endpoint": self.endpoint,
169
+ "credential": self.credential,
170
+ }
171
+
172
+ if self.api == "chat":
173
+ with Tracer.start("ChatCompletionsClient") as trace:
174
+ trace("type", "LLM")
175
+ trace("signature", "azure.ai.inference.aio.ChatCompletionsClient.ctor")
176
+ trace(
177
+ "description", "Azure Unified Inference SDK Async Chat Completions Client"
178
+ )
179
+ trace("inputs", cargs)
180
+ client = AsyncChatCompletionsClient(
181
+ user_agent=f"prompty/{VERSION}",
182
+ **cargs,
183
+ )
184
+ trace("result", client)
185
+
186
+ with Tracer.start("complete") as trace:
187
+ trace("type", "LLM")
188
+ trace("signature", "azure.ai.inference.ChatCompletionsClient.complete")
189
+ trace(
190
+ "description", "Azure Unified Inference SDK Async Chat Completions Client"
191
+ )
192
+ eargs = {
193
+ "model": self.model,
194
+ "messages": data if isinstance(data, list) else [data],
195
+ **self.prompty.model.parameters,
196
+ }
197
+ trace("inputs", eargs)
198
+ r = await client.complete(**eargs)
199
+ trace("result", r)
200
+
201
+ response = self._response(r)
202
+
203
+ elif self.api == "completion":
204
+ raise NotImplementedError(
205
+ "Serverless Completions API is not implemented yet"
206
+ )
207
+
208
+ elif self.api == "embedding":
209
+ with Tracer.start("EmbeddingsClient") as trace:
210
+ trace("type", "LLM")
211
+ trace("signature", "azure.ai.inference.aio.EmbeddingsClient.ctor")
212
+ trace("description", "Azure Unified Inference SDK Async Embeddings Client")
213
+ trace("inputs", cargs)
214
+ client = AsyncEmbeddingsClient(
215
+ user_agent=f"prompty/{VERSION}",
216
+ **cargs,
217
+ )
218
+ trace("result", client)
219
+
220
+ with Tracer.start("complete") as trace:
221
+ trace("type", "LLM")
222
+ trace("signature", "azure.ai.inference.ChatCompletionsClient.complete")
223
+ trace(
224
+ "description", "Azure Unified Inference SDK Chat Completions Client"
225
+ )
226
+ eargs = {
227
+ "model": self.model,
228
+ "input": data if isinstance(data, list) else [data],
229
+ **self.prompty.model.parameters,
230
+ }
231
+ trace("inputs", eargs)
232
+ r = await client.complete(**eargs)
233
+ trace("result", r)
234
+
235
+ response = self._response(r)
236
+
237
+ elif self.api == "image":
238
+ raise NotImplementedError("Azure OpenAI Image API is not implemented yet")
239
+
240
+ return response
@@ -1,6 +1,6 @@
1
- from typing import Iterator
1
+ from typing import AsyncIterator, Iterator
2
2
  from ..invoker import Invoker, InvokerFactory
3
- from ..core import Prompty, PromptyStream, ToolCall
3
+ from ..core import AsyncPromptyStream, Prompty, PromptyStream, ToolCall
4
4
 
5
5
  from azure.ai.inference.models import ChatCompletions, EmbeddingsResult
6
6
 
@@ -75,4 +75,39 @@ class ServerlessProcessor(Invoker):
75
75
  str
76
76
  The parsed data
77
77
  """
78
- return self.invoke(data)
78
+ if isinstance(data, ChatCompletions):
79
+ response = data.choices[0].message
80
+ # tool calls available in response
81
+ if response.tool_calls:
82
+ return [
83
+ ToolCall(
84
+ id=tool_call.id,
85
+ name=tool_call.function.name,
86
+ arguments=tool_call.function.arguments,
87
+ )
88
+ for tool_call in response.tool_calls
89
+ ]
90
+ else:
91
+ return response.content
92
+
93
+ elif isinstance(data, EmbeddingsResult):
94
+ if len(data.data) == 0:
95
+ raise ValueError("Invalid data")
96
+ elif len(data.data) == 1:
97
+ return data.data[0].embedding
98
+ else:
99
+ return [item.embedding for item in data.data]
100
+ elif isinstance(data, AsyncIterator):
101
+
102
+ async def generator():
103
+ async for chunk in data:
104
+ if (
105
+ len(chunk.choices) == 1
106
+ and chunk.choices[0].delta.content != None
107
+ ):
108
+ content = chunk.choices[0].delta.content
109
+ yield content
110
+
111
+ return AsyncPromptyStream("ServerlessProcessor", generator())
112
+ else:
113
+ return data
@@ -93,7 +93,9 @@ def _name(func: Callable, args):
93
93
  if core_invoker:
94
94
  name = type(args[0]).__name__
95
95
  if signature.endswith("async"):
96
- signature = f"{args[0].__module__}.{args[0].__class__.__name__}.invoke_async"
96
+ signature = (
97
+ f"{args[0].__module__}.{args[0].__class__.__name__}.invoke_async"
98
+ )
97
99
  else:
98
100
  signature = f"{args[0].__module__}.{args[0].__class__.__name__}.invoke"
99
101
  else:
@@ -116,20 +118,19 @@ def _results(result: Any) -> dict:
116
118
 
117
119
 
118
120
  def _trace_sync(
119
- func: Callable = None, *, description: str = None, itemtype: str = None
121
+ func: Callable = None, **okwargs: Any
120
122
  ) -> Callable:
121
- description = description or ""
122
123
 
123
124
  @wraps(func)
124
125
  def wrapper(*args, **kwargs):
125
126
  name, signature = _name(func, args)
126
127
  with Tracer.start(name) as trace:
127
128
  trace("signature", signature)
128
- if description and description != "":
129
- trace("description", description)
130
129
 
131
- if itemtype and itemtype != "":
132
- trace("type", itemtype)
130
+ # support arbitrary keyword
131
+ # arguments for trace decorator
132
+ for k, v in okwargs.items():
133
+ trace(k, to_dict(v))
133
134
 
134
135
  inputs = _inputs(func, args, kwargs)
135
136
  trace("inputs", inputs)
@@ -161,20 +162,19 @@ def _trace_sync(
161
162
 
162
163
 
163
164
  def _trace_async(
164
- func: Callable = None, *, description: str = None, itemtype: str = None
165
+ func: Callable = None, **okwargs: Any
165
166
  ) -> Callable:
166
- description = description or ""
167
167
 
168
168
  @wraps(func)
169
169
  async def wrapper(*args, **kwargs):
170
170
  name, signature = _name(func, args)
171
171
  with Tracer.start(name) as trace:
172
172
  trace("signature", signature)
173
- if description and description != "":
174
- trace("description", description)
175
173
 
176
- if itemtype and itemtype != "":
177
- trace("type", itemtype)
174
+ # support arbitrary keyword
175
+ # arguments for trace decorator
176
+ for k, v in okwargs.items():
177
+ trace(k, to_dict(v))
178
178
 
179
179
  inputs = _inputs(func, args, kwargs)
180
180
  trace("inputs", inputs)
@@ -204,15 +204,11 @@ def _trace_async(
204
204
  return wrapper
205
205
 
206
206
 
207
- def trace(
208
- func: Callable = None, *, description: str = None, itemtype: str = None
209
- ) -> Callable:
207
+ def trace(func: Callable = None, **kwargs: Any) -> Callable:
210
208
  if func is None:
211
- return partial(trace, description=description, itemtype=itemtype)
212
-
209
+ return partial(trace, **kwargs)
213
210
  wrapped_method = _trace_async if inspect.iscoroutinefunction(func) else _trace_sync
214
-
215
- return wrapped_method(func, description=description, itemtype=itemtype)
211
+ return wrapped_method(func, **kwargs)
216
212
 
217
213
 
218
214
  class PromptyTracer:
@@ -15,7 +15,7 @@ dependencies = [
15
15
  "click>=8.1.7",
16
16
  "aiofiles>=24.1.0",
17
17
  ]
18
- version = "0.1.33"
18
+ version = "0.1.36"
19
19
 
20
20
  [project.license]
21
21
  text = "MIT"
@@ -23,12 +23,13 @@ text = "MIT"
23
23
  [project.optional-dependencies]
24
24
  azure = [
25
25
  "azure-identity>=1.17.1",
26
- "openai>=1.35.10",
26
+ "openai>=1.43.0",
27
27
  ]
28
28
  openai = [
29
- "openai>=1.35.10",
29
+ "openai>=1.43.0",
30
30
  ]
31
31
  serverless = [
32
+ "azure-identity>=1.17.1",
32
33
  "azure-ai-inference>=1.0.0b3",
33
34
  ]
34
35
 
@@ -41,11 +42,10 @@ distribution = true
41
42
  [tool.pdm.dev-dependencies]
42
43
  dev = [
43
44
  "pytest>=8.2.2",
44
- "openai>=1.35.10",
45
+ "openai>=1.43.0",
45
46
  "azure-ai-inference>=1.0.0b3",
46
47
  "pytest-asyncio>=0.24.0",
47
48
  "azure-identity>=1.17.1",
48
- "openai>=1.35.10",
49
49
  ]
50
50
 
51
51
  [tool.pdm.version]
@@ -0,0 +1,24 @@
1
+ ---
2
+ name: Structured Output Prompt
3
+ description: A prompt that uses the GPT-4o chat API to answer questions in a structured format.
4
+ authors:
5
+ - vgiraud
6
+ model:
7
+ api: chat
8
+ configuration:
9
+ # Minimal model version required for structured output
10
+ azure_deployment: gpt-4o-2024-08-06
11
+ # Minimal API version required for structured output
12
+ api_version: 2024-08-01-preview
13
+ # OpenAI beta API required for structured output
14
+ type: azure_openai_beta
15
+ parameters:
16
+ response_format: ${file:structured_output_schema.json}
17
+ sample:
18
+ statement: Alice and Bob are going to a science fair on Friday.
19
+ ---
20
+ system:
21
+ Extract the event information.
22
+
23
+ user:
24
+ {{statement}}
@@ -0,0 +1,68 @@
1
+ {
2
+ "choices": [
3
+ {
4
+ "content_filter_results": {
5
+ "hate": {
6
+ "filtered": false,
7
+ "severity": "safe"
8
+ },
9
+ "self_harm": {
10
+ "filtered": false,
11
+ "severity": "safe"
12
+ },
13
+ "sexual": {
14
+ "filtered": false,
15
+ "severity": "safe"
16
+ },
17
+ "violence": {
18
+ "filtered": false,
19
+ "severity": "safe"
20
+ }
21
+ },
22
+ "finish_reason": "stop",
23
+ "index": 0,
24
+ "logprobs": null,
25
+ "message": {
26
+ "content": "{\"name\":\"Science Fair\",\"date\":\"Friday\",\"participants\":[\"Alice\",\"Bob\"]}",
27
+ "role": "assistant"
28
+ }
29
+ }
30
+ ],
31
+ "created": 1728500899,
32
+ "id": "chatcmpl-AGWKhgl9bSoQffPMuWjT6JojCKVVs",
33
+ "model": "gpt-4o-2024-08-06",
34
+ "object": "chat.completion",
35
+ "prompt_filter_results": [
36
+ {
37
+ "prompt_index": 0,
38
+ "content_filter_results": {
39
+ "hate": {
40
+ "filtered": false,
41
+ "severity": "safe"
42
+ },
43
+ "jailbreak": {
44
+ "filtered": false,
45
+ "detected": false
46
+ },
47
+ "self_harm": {
48
+ "filtered": false,
49
+ "severity": "safe"
50
+ },
51
+ "sexual": {
52
+ "filtered": false,
53
+ "severity": "safe"
54
+ },
55
+ "violence": {
56
+ "filtered": false,
57
+ "severity": "safe"
58
+ }
59
+ }
60
+ }
61
+ ],
62
+ "system_fingerprint": "fp_67802d9a6d",
63
+ "usage": {
64
+ "completion_tokens": 17,
65
+ "prompt_tokens": 74,
66
+ "total_tokens": 91
67
+ }
68
+ }
@@ -0,0 +1,29 @@
1
+ {
2
+ "type": "json_schema",
3
+ "json_schema": {
4
+ "name": "calendar_event",
5
+ "schema": {
6
+ "type": "object",
7
+ "properties": {
8
+ "name": {
9
+ "type": "string"
10
+ },
11
+ "date": {
12
+ "type": "string",
13
+ "format": "date-time"
14
+ },
15
+ "participants": {
16
+ "type": "array",
17
+ "items": {
18
+ "type": "string"
19
+ }
20
+ }
21
+ },
22
+ "required": [
23
+ "name",
24
+ "date",
25
+ "participants"
26
+ ]
27
+ }
28
+ }
29
+ }
File without changes
@@ -15,6 +15,7 @@ import prompty
15
15
  "prompts/faithfulness.prompty",
16
16
  "prompts/funcfile.prompty",
17
17
  "prompts/functions.prompty",
18
+ "prompts/structured_output.prompty",
18
19
  "prompts/groundedness.prompty",
19
20
  "prompts/sub/basic.prompty",
20
21
  "prompts/sub/sub/basic.prompty",
@@ -13,12 +13,17 @@ from dotenv import load_dotenv
13
13
  load_dotenv()
14
14
 
15
15
 
16
+
16
17
  @pytest.fixture(scope="module", autouse=True)
17
18
  def fake_azure_executor():
18
19
  InvokerFactory.add_executor("azure", FakeAzureExecutor)
19
20
  InvokerFactory.add_executor("azure_openai", FakeAzureExecutor)
21
+ InvokerFactory.add_executor("azure_beta", FakeAzureExecutor)
22
+ InvokerFactory.add_executor("azure_openai_beta", FakeAzureExecutor)
20
23
  InvokerFactory.add_processor("azure", AzureOpenAIProcessor)
21
24
  InvokerFactory.add_processor("azure_openai", AzureOpenAIProcessor)
25
+ InvokerFactory.add_executor("azure_beta", AzureOpenAIProcessor)
26
+ InvokerFactory.add_executor("azure_openai_beta", AzureOpenAIProcessor)
22
27
  InvokerFactory.add_executor("serverless", FakeServerlessExecutor)
23
28
  InvokerFactory.add_processor("serverless", ServerlessProcessor)
24
29
 
@@ -217,6 +222,11 @@ def test_function_calling():
217
222
  )
218
223
  print(result)
219
224
 
225
+ def test_structured_output():
226
+ result = prompty.execute(
227
+ "prompts/structured_output.prompty",
228
+ )
229
+ print(result)
220
230
 
221
231
  @pytest.mark.asyncio
222
232
  async def test_function_calling_async():
@@ -1,19 +1,27 @@
1
1
  from typing import AsyncIterator
2
2
  import pytest
3
3
  import prompty
4
+ from prompty.serverless.processor import ServerlessProcessor
4
5
  from prompty.tracer import trace, Tracer, console_tracer, PromptyTracer
5
6
 
6
7
  from prompty.invoker import InvokerFactory
7
8
  from tests.fake_azure_executor import FakeAzureExecutor
8
9
  from prompty.azure import AzureOpenAIProcessor
10
+ from tests.fake_serverless_executor import FakeServerlessExecutor
9
11
 
10
12
 
11
13
  @pytest.fixture(scope="module", autouse=True)
12
14
  def setup_module():
13
15
  InvokerFactory.add_executor("azure", FakeAzureExecutor)
14
16
  InvokerFactory.add_executor("azure_openai", FakeAzureExecutor)
17
+ InvokerFactory.add_executor("azure_beta", FakeAzureExecutor)
18
+ InvokerFactory.add_executor("azure_openai_beta", FakeAzureExecutor)
15
19
  InvokerFactory.add_processor("azure", AzureOpenAIProcessor)
16
20
  InvokerFactory.add_processor("azure_openai", AzureOpenAIProcessor)
21
+ InvokerFactory.add_executor("azure_beta", AzureOpenAIProcessor)
22
+ InvokerFactory.add_executor("azure_openai_beta", AzureOpenAIProcessor)
23
+ InvokerFactory.add_executor("serverless", FakeServerlessExecutor)
24
+ InvokerFactory.add_processor("serverless", ServerlessProcessor)
17
25
 
18
26
  Tracer.add("console", console_tracer)
19
27
  json_tracer = PromptyTracer()
@@ -238,10 +246,36 @@ async def test_function_calling_async():
238
246
  print(result)
239
247
 
240
248
 
249
+ @trace
250
+ def test_structured_output():
251
+ result = prompty.execute(
252
+ "prompts/structured_output.prompty",
253
+ )
254
+ print(result)
255
+
256
+
257
+ @pytest.mark.asyncio
258
+ @trace
259
+ async def test_structured_output_async():
260
+ result = await prompty.execute_async(
261
+ "prompts/structured_output.prompty",
262
+ )
263
+ print(result)
264
+
265
+
266
+ @pytest.mark.asyncio
267
+ @trace
268
+ async def test_function_calling_async():
269
+ result = await prompty.execute_async(
270
+ "prompts/functions.prompty",
271
+ )
272
+ print(result)
273
+
274
+
241
275
  # need to add trace attribute to
242
276
  # materialize stream into the function
243
277
  # trace decorator
244
- @trace
278
+ @trace(streaming=True, other="test")
245
279
  def test_streaming():
246
280
  result = prompty.execute(
247
281
  "prompts/streaming.prompty",
@@ -254,7 +288,7 @@ def test_streaming():
254
288
 
255
289
 
256
290
  @pytest.mark.asyncio
257
- @trace
291
+ @trace(streaming=True)
258
292
  async def test_streaming_async():
259
293
  result = await prompty.execute_async(
260
294
  "prompts/streaming.prompty",
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes