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.
- {prompty-0.1.33 → prompty-0.1.36}/PKG-INFO +4 -3
- {prompty-0.1.33 → prompty-0.1.36}/prompty/azure/processor.py +2 -0
- prompty-0.1.36/prompty/azure_beta/__init__.py +11 -0
- prompty-0.1.36/prompty/azure_beta/executor.py +281 -0
- {prompty-0.1.33 → prompty-0.1.36}/prompty/serverless/executor.py +91 -4
- {prompty-0.1.33 → prompty-0.1.36}/prompty/serverless/processor.py +38 -3
- {prompty-0.1.33 → prompty-0.1.36}/prompty/tracer.py +16 -20
- {prompty-0.1.33 → prompty-0.1.36}/pyproject.toml +5 -5
- prompty-0.1.36/tests/prompts/structured_output.prompty +24 -0
- prompty-0.1.36/tests/prompts/structured_output.prompty.execution.json +68 -0
- prompty-0.1.36/tests/prompts/structured_output_schema.json +29 -0
- prompty-0.1.36/tests/prompts/sub/sub/__init__.py +0 -0
- {prompty-0.1.33 → prompty-0.1.36}/tests/test_common.py +1 -0
- {prompty-0.1.33 → prompty-0.1.36}/tests/test_execute.py +10 -0
- {prompty-0.1.33 → prompty-0.1.36}/tests/test_tracing.py +36 -2
- {prompty-0.1.33 → prompty-0.1.36}/LICENSE +0 -0
- {prompty-0.1.33 → prompty-0.1.36}/README.md +0 -0
- {prompty-0.1.33 → prompty-0.1.36}/prompty/__init__.py +0 -0
- {prompty-0.1.33 → prompty-0.1.36}/prompty/azure/__init__.py +0 -0
- {prompty-0.1.33 → prompty-0.1.36}/prompty/azure/executor.py +0 -0
- {prompty-0.1.33 → prompty-0.1.36}/prompty/cli.py +0 -0
- {prompty-0.1.33 → prompty-0.1.36}/prompty/core.py +0 -0
- {prompty-0.1.33 → prompty-0.1.36}/prompty/invoker.py +0 -0
- {prompty-0.1.33 → prompty-0.1.36}/prompty/openai/__init__.py +0 -0
- {prompty-0.1.33 → prompty-0.1.36}/prompty/openai/executor.py +0 -0
- {prompty-0.1.33 → prompty-0.1.36}/prompty/openai/processor.py +0 -0
- {prompty-0.1.33 → prompty-0.1.36}/prompty/parsers.py +0 -0
- {prompty-0.1.33 → prompty-0.1.36}/prompty/renderers.py +0 -0
- {prompty-0.1.33 → prompty-0.1.36}/prompty/serverless/__init__.py +0 -0
- {prompty-0.1.33 → prompty-0.1.36}/prompty/utils.py +0 -0
- {prompty-0.1.33/tests/prompts → prompty-0.1.36/tests}/__init__.py +0 -0
- {prompty-0.1.33 → prompty-0.1.36}/tests/fake_azure_executor.py +0 -0
- {prompty-0.1.33 → prompty-0.1.36}/tests/fake_serverless_executor.py +0 -0
- {prompty-0.1.33 → prompty-0.1.36}/tests/generated/1contoso.md +0 -0
- {prompty-0.1.33 → prompty-0.1.36}/tests/generated/2contoso.md +0 -0
- {prompty-0.1.33 → prompty-0.1.36}/tests/generated/3contoso.md +0 -0
- {prompty-0.1.33 → prompty-0.1.36}/tests/generated/4contoso.md +0 -0
- {prompty-0.1.33 → prompty-0.1.36}/tests/generated/basic.prompty.md +0 -0
- {prompty-0.1.33 → prompty-0.1.36}/tests/generated/camping.jpg +0 -0
- {prompty-0.1.33 → prompty-0.1.36}/tests/generated/context.prompty.md +0 -0
- {prompty-0.1.33 → prompty-0.1.36}/tests/generated/contoso_multi.md +0 -0
- {prompty-0.1.33 → prompty-0.1.36}/tests/generated/faithfulness.prompty.md +0 -0
- {prompty-0.1.33 → prompty-0.1.36}/tests/generated/groundedness.prompty.md +0 -0
- {prompty-0.1.33 → prompty-0.1.36}/tests/hello_world-goodbye_world-hello_again.embedding.json +0 -0
- {prompty-0.1.33 → prompty-0.1.36}/tests/hello_world.embedding.json +0 -0
- {prompty-0.1.33/tests/prompts/sub → prompty-0.1.36/tests/prompts}/__init__.py +0 -0
- {prompty-0.1.33 → prompty-0.1.36}/tests/prompts/basic.prompty +0 -0
- {prompty-0.1.33 → prompty-0.1.36}/tests/prompts/basic.prompty.execution.json +0 -0
- {prompty-0.1.33 → prompty-0.1.36}/tests/prompts/basic_json_output.prompty +0 -0
- {prompty-0.1.33 → prompty-0.1.36}/tests/prompts/camping.jpg +0 -0
- {prompty-0.1.33 → prompty-0.1.36}/tests/prompts/chat.prompty +0 -0
- {prompty-0.1.33 → prompty-0.1.36}/tests/prompts/context.json +0 -0
- {prompty-0.1.33 → prompty-0.1.36}/tests/prompts/context.prompty +0 -0
- {prompty-0.1.33 → prompty-0.1.36}/tests/prompts/context.prompty.execution.json +0 -0
- {prompty-0.1.33 → prompty-0.1.36}/tests/prompts/embedding.prompty +0 -0
- {prompty-0.1.33 → prompty-0.1.36}/tests/prompts/embedding.prompty.execution.json +0 -0
- {prompty-0.1.33 → prompty-0.1.36}/tests/prompts/evaluation.prompty +0 -0
- {prompty-0.1.33 → prompty-0.1.36}/tests/prompts/faithfulness.prompty +0 -0
- {prompty-0.1.33 → prompty-0.1.36}/tests/prompts/faithfulness.prompty.execution.json +0 -0
- {prompty-0.1.33 → prompty-0.1.36}/tests/prompts/fake.prompty +0 -0
- {prompty-0.1.33 → prompty-0.1.36}/tests/prompts/funcfile.json +0 -0
- {prompty-0.1.33 → prompty-0.1.36}/tests/prompts/funcfile.prompty +0 -0
- {prompty-0.1.33 → prompty-0.1.36}/tests/prompts/functions.prompty +0 -0
- {prompty-0.1.33 → prompty-0.1.36}/tests/prompts/functions.prompty.execution.json +0 -0
- {prompty-0.1.33 → prompty-0.1.36}/tests/prompts/groundedness.prompty +0 -0
- {prompty-0.1.33 → prompty-0.1.36}/tests/prompts/groundedness.prompty.execution.json +0 -0
- {prompty-0.1.33 → prompty-0.1.36}/tests/prompts/prompty.json +0 -0
- {prompty-0.1.33 → prompty-0.1.36}/tests/prompts/serverless.prompty +0 -0
- {prompty-0.1.33 → prompty-0.1.36}/tests/prompts/serverless.prompty.execution.json +0 -0
- {prompty-0.1.33 → prompty-0.1.36}/tests/prompts/serverless_stream.prompty +0 -0
- {prompty-0.1.33 → prompty-0.1.36}/tests/prompts/serverless_stream.prompty.execution.json +0 -0
- {prompty-0.1.33 → prompty-0.1.36}/tests/prompts/streaming.prompty +0 -0
- {prompty-0.1.33 → prompty-0.1.36}/tests/prompts/streaming.prompty.execution.json +0 -0
- {prompty-0.1.33/tests/prompts/sub → prompty-0.1.36/tests/prompts}/sub/__init__.py +0 -0
- {prompty-0.1.33 → prompty-0.1.36}/tests/prompts/sub/basic.prompty +0 -0
- {prompty-0.1.33 → prompty-0.1.36}/tests/prompts/sub/sub/basic.prompty +0 -0
- {prompty-0.1.33 → prompty-0.1.36}/tests/prompts/sub/sub/prompty.json +0 -0
- {prompty-0.1.33 → prompty-0.1.36}/tests/prompts/sub/sub/test.py +0 -0
- {prompty-0.1.33 → prompty-0.1.36}/tests/prompts/test.py +0 -0
- {prompty-0.1.33 → prompty-0.1.36}/tests/prompty.json +0 -0
- {prompty-0.1.33 → prompty-0.1.36}/tests/test_factory_invoker.py +0 -0
- {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.
|
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.
|
16
|
+
Requires-Dist: openai>=1.43.0; extra == "azure"
|
17
17
|
Provides-Extra: openai
|
18
|
-
Requires-Dist: openai>=1.
|
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
|
-
|
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":
|
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
|
-
|
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
|
-
|
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 =
|
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,
|
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
|
-
|
132
|
-
|
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,
|
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
|
-
|
177
|
-
|
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,
|
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.
|
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.
|
26
|
+
"openai>=1.43.0",
|
27
27
|
]
|
28
28
|
openai = [
|
29
|
-
"openai>=1.
|
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.
|
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
|
@@ -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
|
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
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{prompty-0.1.33 → prompty-0.1.36}/tests/hello_world-goodbye_world-hello_again.embedding.json
RENAMED
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
|
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
|
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
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|