freeplay 0.2.25__tar.gz → 0.2.30__tar.gz
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.
- freeplay-0.2.30/LICENSE +21 -0
- {freeplay-0.2.25 → freeplay-0.2.30}/PKG-INFO +4 -3
- {freeplay-0.2.25 → freeplay-0.2.30}/pyproject.toml +14 -2
- {freeplay-0.2.25 → freeplay-0.2.30/src}/freeplay/api_support.py +3 -2
- {freeplay-0.2.25 → freeplay-0.2.30/src}/freeplay/completions.py +16 -9
- {freeplay-0.2.25 → freeplay-0.2.30/src}/freeplay/flavors.py +132 -103
- {freeplay-0.2.25 → freeplay-0.2.30/src}/freeplay/freeplay.py +39 -32
- freeplay-0.2.30/src/freeplay/freeplay_cli.py +106 -0
- freeplay-0.2.30/src/freeplay/freeplay_thin.py +21 -0
- {freeplay-0.2.25 → freeplay-0.2.30/src}/freeplay/llm_parameters.py +3 -5
- {freeplay-0.2.25 → freeplay-0.2.30/src}/freeplay/provider_config.py +4 -23
- {freeplay-0.2.25 → freeplay-0.2.30/src}/freeplay/record.py +3 -3
- freeplay-0.2.30/src/freeplay/utils.py +21 -0
- freeplay-0.2.25/freeplay/utils.py +0 -11
- {freeplay-0.2.25 → freeplay-0.2.30}/README.md +0 -0
- {freeplay-0.2.25 → freeplay-0.2.30/src}/freeplay/__init__.py +0 -0
- {freeplay-0.2.25 → freeplay-0.2.30/src}/freeplay/errors.py +0 -0
freeplay-0.2.30/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2023 228Labs
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: freeplay
|
3
|
-
Version: 0.2.
|
3
|
+
Version: 0.2.30
|
4
4
|
Summary:
|
5
5
|
License: MIT
|
6
6
|
Author: FreePlay Engineering
|
@@ -12,10 +12,11 @@ Classifier: Programming Language :: Python :: 3.8
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.9
|
13
13
|
Classifier: Programming Language :: Python :: 3.10
|
14
14
|
Classifier: Programming Language :: Python :: 3.11
|
15
|
-
Classifier: Programming Language :: Python :: 3.12
|
16
15
|
Requires-Dist: anthropic (>=0.7.7,<0.8.0)
|
16
|
+
Requires-Dist: click (==8.1.7)
|
17
17
|
Requires-Dist: dacite (>=1.8.0,<2.0.0)
|
18
|
-
Requires-Dist: openai (>=
|
18
|
+
Requires-Dist: openai (>=1,<2)
|
19
|
+
Requires-Dist: pystache (>=0.6.5,<0.7.0)
|
19
20
|
Requires-Dist: requests (>=2.20.0,<3.0.0dev)
|
20
21
|
Description-Content-Type: text/markdown
|
21
22
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
[tool.poetry]
|
2
2
|
name = "freeplay"
|
3
|
-
version = "0.2.
|
3
|
+
version = "0.2.30"
|
4
4
|
description = ""
|
5
5
|
authors = ["FreePlay Engineering <engineering@freeplay.ai>"]
|
6
6
|
license = "MIT"
|
@@ -11,9 +11,21 @@ python = ">=3.8, <4"
|
|
11
11
|
requests = ">=2.20.0,<3.0.0dev"
|
12
12
|
dacite = "^1.8.0"
|
13
13
|
anthropic = "^0.7.7"
|
14
|
-
openai = "^
|
14
|
+
openai = "^1"
|
15
|
+
click = "8.1.7"
|
16
|
+
pystache = "^0.6.5"
|
15
17
|
|
18
|
+
[tool.poetry.group.dev.dependencies]
|
19
|
+
mypy = "^1"
|
20
|
+
types-requests = "^2.31"
|
21
|
+
|
22
|
+
[tool.poetry.group.test.dependencies]
|
23
|
+
responses = "^0.23.1"
|
24
|
+
respx = "^0.20.2"
|
16
25
|
|
17
26
|
[build-system]
|
18
27
|
requires = ["poetry-core"]
|
19
28
|
build-backend = "poetry.core.masonry.api"
|
29
|
+
|
30
|
+
[tool.poetry.scripts]
|
31
|
+
freeplay = "freeplay.freeplay_cli:cli"
|
@@ -1,6 +1,7 @@
|
|
1
1
|
import json
|
2
2
|
import logging
|
3
3
|
import typing as t
|
4
|
+
from typing import Dict
|
4
5
|
|
5
6
|
import dacite
|
6
7
|
import requests
|
@@ -24,7 +25,7 @@ def try_decode(target_type: t.Type[T], data: bytes) -> t.Optional[T]:
|
|
24
25
|
return None
|
25
26
|
|
26
27
|
|
27
|
-
def post(target_type: t.Type[T], api_key: str, url: str, payload: t.Optional[
|
28
|
+
def post(target_type: t.Type[T], api_key: str, url: str, payload: t.Optional[Dict[str, str]] = None) -> T:
|
28
29
|
response = requests.post(
|
29
30
|
url=url,
|
30
31
|
headers={'Authorization': f'Bearer {api_key}'},
|
@@ -41,7 +42,7 @@ def post(target_type: t.Type[T], api_key: str, url: str, payload: t.Optional[dic
|
|
41
42
|
return maybe_object
|
42
43
|
|
43
44
|
|
44
|
-
def post_raw(api_key: str, url: str, payload: t.Optional[
|
45
|
+
def post_raw(api_key: str, url: str, payload: t.Optional[Dict[str, t.Any]] = None) -> Response:
|
45
46
|
return requests.post(
|
46
47
|
url=url,
|
47
48
|
headers={'Authorization': f'Bearer {api_key}'},
|
@@ -1,33 +1,40 @@
|
|
1
1
|
from dataclasses import dataclass
|
2
|
-
from typing import Optional,
|
2
|
+
from typing import Any, Dict, List, Optional, TypedDict
|
3
|
+
|
4
|
+
from openai.types.chat.chat_completion_chunk import ChoiceDeltaFunctionCall
|
5
|
+
from openai.types.chat.chat_completion_message import FunctionCall
|
3
6
|
|
4
7
|
from .llm_parameters import LLMParameters
|
5
8
|
|
6
|
-
|
7
|
-
|
9
|
+
|
10
|
+
class ChatMessage(TypedDict):
|
11
|
+
role: str
|
12
|
+
content: str
|
8
13
|
|
9
14
|
|
10
15
|
@dataclass
|
11
16
|
class CompletionResponse:
|
12
17
|
content: str
|
13
18
|
is_complete: bool
|
14
|
-
openai_function_call: Optional[
|
19
|
+
openai_function_call: Optional[FunctionCall] = None
|
20
|
+
|
15
21
|
|
16
22
|
@dataclass
|
17
23
|
class ChatCompletionResponse:
|
18
24
|
content: str
|
19
25
|
is_complete: bool
|
20
|
-
message_history:
|
26
|
+
message_history: List[ChatMessage]
|
21
27
|
|
22
28
|
|
23
29
|
@dataclass
|
24
30
|
class PromptTemplateWithMetadata:
|
25
|
-
project_version_id: str
|
26
31
|
prompt_template_id: str
|
32
|
+
prompt_template_version_id: str
|
33
|
+
|
27
34
|
name: str
|
28
35
|
content: str
|
29
36
|
flavor_name: Optional[str]
|
30
|
-
params: Optional[
|
37
|
+
params: Optional[Dict[str, Any]]
|
31
38
|
|
32
39
|
def get_params(self) -> LLMParameters:
|
33
40
|
return LLMParameters.empty() if self.params is None else LLMParameters(self.params)
|
@@ -35,11 +42,11 @@ class PromptTemplateWithMetadata:
|
|
35
42
|
|
36
43
|
@dataclass
|
37
44
|
class PromptTemplates:
|
38
|
-
templates:
|
45
|
+
templates: List[PromptTemplateWithMetadata]
|
39
46
|
|
40
47
|
|
41
48
|
@dataclass
|
42
49
|
class CompletionChunk:
|
43
50
|
text: str
|
44
51
|
is_complete: bool
|
45
|
-
openai_function_call: Optional[
|
52
|
+
openai_function_call: Optional[ChoiceDeltaFunctionCall] = None
|
@@ -1,34 +1,34 @@
|
|
1
1
|
import json
|
2
2
|
from abc import abstractmethod, ABC
|
3
3
|
from copy import copy
|
4
|
-
from typing import
|
4
|
+
from typing import cast, Any, Dict, Generator, List, Optional, Union
|
5
5
|
|
6
|
-
import anthropic
|
6
|
+
import anthropic
|
7
7
|
import openai
|
8
|
-
from openai
|
8
|
+
from openai import AuthenticationError, BadRequestError, Stream
|
9
|
+
from openai.types.chat import ChatCompletion, ChatCompletionChunk, ChatCompletionMessageParam
|
9
10
|
|
10
11
|
from .completions import CompletionChunk, PromptTemplateWithMetadata, CompletionResponse, ChatCompletionResponse, \
|
11
12
|
ChatMessage
|
12
13
|
from .errors import FreeplayConfigurationError, LLMClientError, LLMServerError, FreeplayError
|
13
14
|
from .llm_parameters import LLMParameters
|
14
|
-
from .provider_config import
|
15
|
+
from .provider_config import AnthropicConfig, AzureConfig, OpenAIConfig, ProviderConfig
|
15
16
|
from .utils import format_template_variables
|
16
17
|
|
17
18
|
|
18
19
|
class Flavor(ABC):
|
19
20
|
@classmethod
|
20
21
|
def get_by_name(cls, flavor_name: str) -> 'Flavor':
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
'a different model in the Freeplay UI.')
|
22
|
+
if flavor_name == OpenAIChat.record_format_type:
|
23
|
+
return OpenAIChat()
|
24
|
+
elif flavor_name == AzureOpenAIChat.record_format_type:
|
25
|
+
return AzureOpenAIChat()
|
26
|
+
elif flavor_name == AnthropicClaudeChat.record_format_type:
|
27
|
+
return AnthropicClaudeChat()
|
28
|
+
else:
|
29
|
+
raise FreeplayConfigurationError(
|
30
|
+
'Configured flavor not found in SDK. Please update your SDK version or configure '
|
31
|
+
'a different model in the Freeplay UI.')
|
32
32
|
|
33
33
|
@property
|
34
34
|
@abstractmethod
|
@@ -45,7 +45,7 @@ class Flavor(ABC):
|
|
45
45
|
return LLMParameters.empty()
|
46
46
|
|
47
47
|
@abstractmethod
|
48
|
-
def format(self, prompt_template: PromptTemplateWithMetadata, variables:
|
48
|
+
def format(self, prompt_template: PromptTemplateWithMetadata, variables: Dict[str, str]) -> str:
|
49
49
|
pass
|
50
50
|
|
51
51
|
@abstractmethod
|
@@ -74,7 +74,7 @@ class ChatFlavor(Flavor, ABC):
|
|
74
74
|
@abstractmethod
|
75
75
|
def continue_chat(
|
76
76
|
self,
|
77
|
-
messages:
|
77
|
+
messages: List[ChatMessage],
|
78
78
|
provider_config: ProviderConfig,
|
79
79
|
llm_parameters: LLMParameters
|
80
80
|
) -> ChatCompletionResponse:
|
@@ -83,57 +83,29 @@ class ChatFlavor(Flavor, ABC):
|
|
83
83
|
@abstractmethod
|
84
84
|
def continue_chat_stream(
|
85
85
|
self,
|
86
|
-
messages:
|
86
|
+
messages: List[ChatMessage],
|
87
87
|
provider_config: ProviderConfig,
|
88
88
|
llm_parameters: LLMParameters
|
89
89
|
) -> Generator[CompletionChunk, None, None]:
|
90
90
|
pass
|
91
91
|
|
92
92
|
|
93
|
-
class
|
94
|
-
def configure_openai(
|
95
|
-
self,
|
96
|
-
openai_config: Optional[OpenAIConfig],
|
97
|
-
api_base: Optional[str] = None,
|
98
|
-
api_version: Optional[str] = None,
|
99
|
-
api_type: Optional[str] = None,
|
100
|
-
) -> None:
|
101
|
-
super().__init__()
|
102
|
-
if not openai_config:
|
103
|
-
raise FreeplayConfigurationError(
|
104
|
-
"Missing OpenAI key. Use a ProviderConfig to specify keys prior to getting completion.")
|
105
|
-
|
106
|
-
if api_base:
|
107
|
-
openai.api_base = api_base
|
108
|
-
elif openai_config.api_base:
|
109
|
-
openai.api_base = openai_config.api_base
|
110
|
-
|
111
|
-
if api_type:
|
112
|
-
openai.api_type = api_type
|
113
|
-
|
114
|
-
if api_version:
|
115
|
-
openai.api_version = api_version
|
93
|
+
class OpenAIChatFlavor(ChatFlavor, ABC):
|
116
94
|
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
class OpenAIChat(OpenAI, ChatFlavor):
|
128
|
-
record_format_type = "openai_chat"
|
129
|
-
_model_params_with_defaults = LLMParameters({
|
130
|
-
"model": "gpt-3.5-turbo"
|
131
|
-
})
|
95
|
+
@abstractmethod
|
96
|
+
def _call_openai(
|
97
|
+
self,
|
98
|
+
messages: List[ChatMessage],
|
99
|
+
provider_config: ProviderConfig,
|
100
|
+
llm_parameters: LLMParameters,
|
101
|
+
stream: bool
|
102
|
+
) -> Union[ChatCompletion, openai.Stream[ChatCompletionChunk]]:
|
103
|
+
pass
|
132
104
|
|
133
|
-
def format(self, prompt_template: PromptTemplateWithMetadata, variables:
|
105
|
+
def format(self, prompt_template: PromptTemplateWithMetadata, variables: Dict[str, str]) -> str:
|
134
106
|
# Extract messages JSON to enable formatting of individual content fields of each message. If we do not
|
135
107
|
# extract the JSON, current variable interpolation will fail on JSON curly braces.
|
136
|
-
messages_as_json:
|
108
|
+
messages_as_json: List[Dict[str, str]] = json.loads(prompt_template.content)
|
137
109
|
formatted_messages = [
|
138
110
|
{
|
139
111
|
"content": format_template_variables(message['content'], variables), "role": message['role']
|
@@ -147,11 +119,12 @@ class OpenAIChat(OpenAI, ChatFlavor):
|
|
147
119
|
llm_parameters: LLMParameters
|
148
120
|
) -> CompletionResponse:
|
149
121
|
messages = json.loads(formatted_prompt)
|
150
|
-
completion = self._call_openai(messages, provider_config, llm_parameters, stream=False)
|
122
|
+
completion = cast(ChatCompletion, self._call_openai(messages, provider_config, llm_parameters, stream=False))
|
123
|
+
|
151
124
|
return CompletionResponse(
|
152
125
|
content=completion.choices[0].message.content or '',
|
153
126
|
is_complete=completion.choices[0].finish_reason == 'stop',
|
154
|
-
openai_function_call=completion.choices[0].message.
|
127
|
+
openai_function_call=completion.choices[0].message.function_call,
|
155
128
|
)
|
156
129
|
|
157
130
|
def call_service_stream(
|
@@ -161,70 +134,129 @@ class OpenAIChat(OpenAI, ChatFlavor):
|
|
161
134
|
llm_parameters: LLMParameters
|
162
135
|
) -> Generator[CompletionChunk, None, None]:
|
163
136
|
messages = json.loads(formatted_prompt)
|
164
|
-
completion_stream =
|
137
|
+
completion_stream = cast(Stream[ChatCompletionChunk],
|
138
|
+
self._call_openai(messages, provider_config, llm_parameters, stream=True))
|
165
139
|
for chunk in completion_stream:
|
166
140
|
yield CompletionChunk(
|
167
|
-
text=chunk.choices[0].delta.
|
141
|
+
text=chunk.choices[0].delta.content or '',
|
168
142
|
is_complete=chunk.choices[0].finish_reason == 'stop',
|
169
|
-
openai_function_call=chunk.choices[0].delta.
|
143
|
+
openai_function_call=chunk.choices[0].delta.function_call
|
170
144
|
)
|
171
145
|
|
172
146
|
def continue_chat(
|
173
147
|
self,
|
174
|
-
messages:
|
148
|
+
messages: List[ChatMessage],
|
175
149
|
provider_config: ProviderConfig,
|
176
150
|
llm_parameters: LLMParameters
|
177
151
|
) -> ChatCompletionResponse:
|
178
|
-
completion = self._call_openai(messages, provider_config, llm_parameters, stream=False)
|
152
|
+
completion = cast(ChatCompletion, self._call_openai(messages, provider_config, llm_parameters, stream=False))
|
179
153
|
|
180
154
|
message_history = copy(messages)
|
181
|
-
|
155
|
+
message = completion.choices[0].message
|
156
|
+
message_history.append({
|
157
|
+
"role": message.role or '',
|
158
|
+
"content": message.content or ''
|
159
|
+
})
|
182
160
|
return ChatCompletionResponse(
|
183
|
-
content=
|
161
|
+
content=message.content or '',
|
184
162
|
message_history=message_history,
|
185
163
|
is_complete=completion.choices[0].finish_reason == "stop"
|
186
164
|
)
|
187
165
|
|
188
166
|
def continue_chat_stream(
|
189
167
|
self,
|
190
|
-
messages:
|
168
|
+
messages: List[ChatMessage],
|
191
169
|
provider_config: ProviderConfig,
|
192
170
|
llm_parameters: LLMParameters
|
193
171
|
) -> Generator[CompletionChunk, None, None]:
|
194
|
-
completion_stream =
|
172
|
+
completion_stream = cast(Stream[ChatCompletionChunk],
|
173
|
+
self._call_openai(messages, provider_config, llm_parameters, stream=True))
|
195
174
|
for chunk in completion_stream:
|
196
175
|
yield CompletionChunk(
|
197
|
-
text=chunk.choices[0].delta.
|
176
|
+
text=chunk.choices[0].delta.content or '',
|
198
177
|
is_complete=chunk.choices[0].finish_reason == "stop"
|
199
178
|
)
|
200
179
|
|
180
|
+
|
181
|
+
class OpenAIChat(OpenAIChatFlavor):
|
182
|
+
record_format_type = "openai_chat"
|
183
|
+
_model_params_with_defaults = LLMParameters({
|
184
|
+
"model": "gpt-3.5-turbo"
|
185
|
+
})
|
186
|
+
|
187
|
+
def __init__(self) -> None:
|
188
|
+
self.client: Optional[openai.OpenAI] = None
|
189
|
+
|
190
|
+
@property
|
191
|
+
def provider(self) -> str:
|
192
|
+
return "openai"
|
193
|
+
|
194
|
+
def get_openai_client(self, openai_config: Optional[OpenAIConfig]) -> openai.OpenAI:
|
195
|
+
if self.client:
|
196
|
+
return self.client
|
197
|
+
|
198
|
+
if not openai_config:
|
199
|
+
raise FreeplayConfigurationError(
|
200
|
+
"Missing OpenAI key. Use a ProviderConfig to specify keys prior to getting completion.")
|
201
|
+
|
202
|
+
self.client = openai.OpenAI(api_key=openai_config.api_key, base_url=openai_config.base_url)
|
203
|
+
return self.client
|
204
|
+
|
201
205
|
def _call_openai(
|
202
206
|
self,
|
203
|
-
messages:
|
207
|
+
messages: List[ChatMessage],
|
204
208
|
provider_config: ProviderConfig,
|
205
209
|
llm_parameters: LLMParameters,
|
206
210
|
stream: bool
|
207
|
-
) ->
|
208
|
-
self.
|
209
|
-
llm_parameters.pop('endpoint')
|
211
|
+
) -> Union[ChatCompletion, openai.Stream[ChatCompletionChunk]]:
|
212
|
+
client = self.get_openai_client(provider_config.openai)
|
210
213
|
try:
|
211
|
-
return
|
212
|
-
messages=messages,
|
214
|
+
return client.chat.completions.create(
|
215
|
+
messages=cast(List[ChatCompletionMessageParam], messages),
|
213
216
|
**self.get_model_params(llm_parameters),
|
214
217
|
stream=stream,
|
215
|
-
)
|
216
|
-
except (
|
218
|
+
)
|
219
|
+
except (BadRequestError, AuthenticationError) as e:
|
217
220
|
raise LLMClientError("Unable to call OpenAI") from e
|
218
221
|
except Exception as e:
|
219
222
|
raise LLMServerError("Unable to call OpenAI") from e
|
220
223
|
|
221
224
|
|
222
|
-
class AzureOpenAIChat(
|
225
|
+
class AzureOpenAIChat(OpenAIChatFlavor):
|
223
226
|
record_format_type = "azure_openai_chat"
|
224
227
|
|
228
|
+
def __init__(self) -> None:
|
229
|
+
self.client: Optional[openai.AzureOpenAI] = None
|
230
|
+
|
231
|
+
@property
|
232
|
+
def provider(self) -> str:
|
233
|
+
return "azure"
|
234
|
+
|
235
|
+
def get_azure_client(
|
236
|
+
self,
|
237
|
+
azure_config: Optional[AzureConfig],
|
238
|
+
api_version: Optional[str] = None,
|
239
|
+
endpoint: Optional[str] = None,
|
240
|
+
deployment: Optional[str] = None,
|
241
|
+
) -> openai.AzureOpenAI:
|
242
|
+
if self.client:
|
243
|
+
return self.client
|
244
|
+
|
245
|
+
if not azure_config:
|
246
|
+
raise FreeplayConfigurationError(
|
247
|
+
"Missing Azure key. Use a ProviderConfig to specify keys prior to getting completion.")
|
248
|
+
|
249
|
+
self.client = openai.AzureOpenAI(
|
250
|
+
api_key=azure_config.api_key,
|
251
|
+
api_version=api_version,
|
252
|
+
azure_endpoint=endpoint or '',
|
253
|
+
azure_deployment=deployment,
|
254
|
+
)
|
255
|
+
return self.client
|
256
|
+
|
225
257
|
def _call_openai(
|
226
258
|
self,
|
227
|
-
messages:
|
259
|
+
messages: List[ChatMessage],
|
228
260
|
provider_config: ProviderConfig,
|
229
261
|
llm_parameters: LLMParameters,
|
230
262
|
stream: bool
|
@@ -233,28 +265,25 @@ class AzureOpenAIChat(OpenAIChat):
|
|
233
265
|
deployment_id = llm_parameters.get('deployment_id')
|
234
266
|
resource_name = llm_parameters.get('resource_name')
|
235
267
|
endpoint = f'https://{resource_name}.openai.azure.com'
|
236
|
-
self.configure_openai(
|
237
|
-
provider_config.azure,
|
238
|
-
api_base=endpoint,
|
239
|
-
api_type='azure',
|
240
|
-
api_version=api_version
|
241
|
-
)
|
242
268
|
llm_parameters.pop('resource_name')
|
269
|
+
|
270
|
+
client = self.get_azure_client(
|
271
|
+
azure_config=provider_config.azure,
|
272
|
+
api_version=api_version,
|
273
|
+
endpoint=endpoint,
|
274
|
+
deployment=deployment_id,
|
275
|
+
)
|
276
|
+
|
243
277
|
try:
|
244
|
-
return
|
245
|
-
messages=messages,
|
278
|
+
return client.chat.completions.create(
|
279
|
+
messages=cast(List[ChatCompletionMessageParam], messages),
|
246
280
|
**self.get_model_params(llm_parameters),
|
247
|
-
engine=deployment_id,
|
248
281
|
stream=stream,
|
249
|
-
)
|
250
|
-
except (
|
251
|
-
raise LLMClientError("Unable to call
|
282
|
+
)
|
283
|
+
except (BadRequestError, AuthenticationError) as e:
|
284
|
+
raise LLMClientError("Unable to call Azure") from e
|
252
285
|
except Exception as e:
|
253
|
-
raise LLMServerError("Unable to call
|
254
|
-
|
255
|
-
@property
|
256
|
-
def provider(self) -> str:
|
257
|
-
return "azure"
|
286
|
+
raise LLMServerError("Unable to call Azure") from e
|
258
287
|
|
259
288
|
|
260
289
|
class AnthropicClaudeText(Flavor):
|
@@ -282,7 +311,7 @@ class AnthropicClaudeText(Flavor):
|
|
282
311
|
self.client = anthropic.Client(api_key=anthropic_config.api_key)
|
283
312
|
return self.client
|
284
313
|
|
285
|
-
def format(self, prompt_template: PromptTemplateWithMetadata, variables:
|
314
|
+
def format(self, prompt_template: PromptTemplateWithMetadata, variables: Dict[str, str]) -> str:
|
286
315
|
interpolated_prompt = format_template_variables(prompt_template.content, variables)
|
287
316
|
# Anthropic expects a specific Chat format "Human: $PROMPT_TEXT\n\nAssistant:". We add the wrapping for Text.
|
288
317
|
chat_formatted_prompt = f"{anthropic.HUMAN_PROMPT} {interpolated_prompt} {anthropic.AI_PROMPT}"
|
@@ -361,10 +390,10 @@ class AnthropicClaudeChat(ChatFlavor):
|
|
361
390
|
|
362
391
|
# This just formats the prompt for uploading to the record endpoint.
|
363
392
|
# TODO: Move this to a base class.
|
364
|
-
def format(self, prompt_template: PromptTemplateWithMetadata, variables:
|
393
|
+
def format(self, prompt_template: PromptTemplateWithMetadata, variables: Dict[str, str]) -> str:
|
365
394
|
# Extract messages JSON to enable formatting of individual content fields of each message. If we do not
|
366
395
|
# extract the JSON, current variable interpolation will fail on JSON curly braces.
|
367
|
-
messages_as_json:
|
396
|
+
messages_as_json: List[Dict[str, str]] = json.loads(prompt_template.content)
|
368
397
|
formatted_messages = [
|
369
398
|
{
|
370
399
|
"content": format_template_variables(message['content'], variables),
|
@@ -383,7 +412,7 @@ class AnthropicClaudeChat(ChatFlavor):
|
|
383
412
|
return 'Human'
|
384
413
|
|
385
414
|
@staticmethod
|
386
|
-
def __to_anthropic_chat_format(messages:
|
415
|
+
def __to_anthropic_chat_format(messages: List[ChatMessage]) -> str:
|
387
416
|
formatted_messages = []
|
388
417
|
for message in messages:
|
389
418
|
formatted_messages.append(f"{message['role']}: {message['content']}")
|
@@ -393,7 +422,7 @@ class AnthropicClaudeChat(ChatFlavor):
|
|
393
422
|
|
394
423
|
def continue_chat(
|
395
424
|
self,
|
396
|
-
messages:
|
425
|
+
messages: List[ChatMessage],
|
397
426
|
provider_config: ProviderConfig,
|
398
427
|
llm_parameters: LLMParameters
|
399
428
|
) -> ChatCompletionResponse:
|
@@ -416,7 +445,7 @@ class AnthropicClaudeChat(ChatFlavor):
|
|
416
445
|
|
417
446
|
def continue_chat_stream(
|
418
447
|
self,
|
419
|
-
messages:
|
448
|
+
messages: List[ChatMessage],
|
420
449
|
provider_config: ProviderConfig,
|
421
450
|
llm_parameters: LLMParameters
|
422
451
|
) -> Generator[CompletionChunk, None, None]:
|
@@ -3,12 +3,18 @@ import logging
|
|
3
3
|
import time
|
4
4
|
from copy import copy
|
5
5
|
from dataclasses import dataclass
|
6
|
-
from typing import
|
6
|
+
from typing import cast, Any, Dict, Generator, List, Optional, Tuple, Union
|
7
7
|
|
8
8
|
from . import api_support
|
9
9
|
from .api_support import try_decode
|
10
|
-
from .completions import
|
11
|
-
|
10
|
+
from .completions import (
|
11
|
+
PromptTemplates,
|
12
|
+
CompletionResponse,
|
13
|
+
CompletionChunk,
|
14
|
+
PromptTemplateWithMetadata,
|
15
|
+
ChatCompletionResponse,
|
16
|
+
ChatMessage
|
17
|
+
)
|
12
18
|
from .errors import FreeplayConfigurationError, freeplay_response_error, FreeplayServerError
|
13
19
|
from .flavors import Flavor, ChatFlavor
|
14
20
|
from .llm_parameters import LLMParameters
|
@@ -19,8 +25,8 @@ from .record import (
|
|
19
25
|
RecordCallFields
|
20
26
|
)
|
21
27
|
|
22
|
-
JsonDom =
|
23
|
-
Variables =
|
28
|
+
JsonDom = Dict[str, Any]
|
29
|
+
Variables = Dict[str, str]
|
24
30
|
|
25
31
|
logger = logging.getLogger(__name__)
|
26
32
|
default_tag = 'latest'
|
@@ -51,9 +57,9 @@ class CallSupport:
|
|
51
57
|
project_id: str,
|
52
58
|
tag: str,
|
53
59
|
test_run_id: Optional[str] = None,
|
54
|
-
metadata: Optional[
|
60
|
+
metadata: Optional[Dict[str, Union[str,int,float]]] = None
|
55
61
|
) -> JsonDom:
|
56
|
-
request_body:
|
62
|
+
request_body: Dict[str, Any] = {}
|
57
63
|
if test_run_id is not None:
|
58
64
|
request_body['test_run_id'] = test_run_id
|
59
65
|
if metadata is not None:
|
@@ -65,7 +71,7 @@ class CallSupport:
|
|
65
71
|
payload=request_body)
|
66
72
|
|
67
73
|
if response.status_code == 201:
|
68
|
-
return cast(
|
74
|
+
return cast(Dict[str, Any], json.loads(response.content))
|
69
75
|
else:
|
70
76
|
raise freeplay_response_error('Error while creating a session.', response)
|
71
77
|
|
@@ -93,8 +99,8 @@ class CallSupport:
|
|
93
99
|
tag: str,
|
94
100
|
target_template: PromptTemplateWithMetadata,
|
95
101
|
variables: Variables,
|
96
|
-
message_history:
|
97
|
-
new_messages: Optional[
|
102
|
+
message_history: List[ChatMessage],
|
103
|
+
new_messages: Optional[List[ChatMessage]],
|
98
104
|
test_run_id: Optional[str] = None,
|
99
105
|
completion_parameters: Optional[LLMParameters] = None) -> ChatCompletionResponse:
|
100
106
|
# make call
|
@@ -142,7 +148,7 @@ class CallSupport:
|
|
142
148
|
tag: str,
|
143
149
|
target_template: PromptTemplateWithMetadata,
|
144
150
|
variables: Variables,
|
145
|
-
message_history:
|
151
|
+
message_history: List[ChatMessage],
|
146
152
|
test_run_id: Optional[str] = None,
|
147
153
|
completion_parameters: Optional[LLMParameters] = None
|
148
154
|
) -> Generator[CompletionChunk, None, None]:
|
@@ -189,7 +195,7 @@ class CallSupport:
|
|
189
195
|
session_id: str,
|
190
196
|
prompts: PromptTemplates,
|
191
197
|
template_name: str,
|
192
|
-
variables:
|
198
|
+
variables: Dict[str, str],
|
193
199
|
flavor: Optional[Flavor],
|
194
200
|
provider_config: ProviderConfig,
|
195
201
|
tag: str,
|
@@ -239,7 +245,7 @@ class CallSupport:
|
|
239
245
|
session_id: str,
|
240
246
|
prompts: PromptTemplates,
|
241
247
|
template_name: str,
|
242
|
-
variables:
|
248
|
+
variables: Dict[str, str],
|
243
249
|
flavor: Optional[Flavor],
|
244
250
|
provider_config: ProviderConfig,
|
245
251
|
tag: str,
|
@@ -310,7 +316,7 @@ class Session:
|
|
310
316
|
def get_completion(
|
311
317
|
self,
|
312
318
|
template_name: str,
|
313
|
-
variables:
|
319
|
+
variables: Dict[str, str],
|
314
320
|
flavor: Optional[Flavor] = None,
|
315
321
|
**kwargs: Any
|
316
322
|
) -> CompletionResponse:
|
@@ -328,7 +334,7 @@ class Session:
|
|
328
334
|
def get_completion_stream(
|
329
335
|
self,
|
330
336
|
template_name: str,
|
331
|
-
variables:
|
337
|
+
variables: Dict[str, str],
|
332
338
|
flavor: Optional[Flavor] = None,
|
333
339
|
**kwargs: Any
|
334
340
|
) -> Generator[CompletionChunk, None, None]:
|
@@ -356,7 +362,7 @@ class ChatSession(Session):
|
|
356
362
|
variables: Variables,
|
357
363
|
tag: str = default_tag,
|
358
364
|
test_run_id: Optional[str] = None,
|
359
|
-
messages: Optional[
|
365
|
+
messages: Optional[List[ChatMessage]] = None
|
360
366
|
) -> None:
|
361
367
|
super().__init__(call_support, session_id, prompts, flavor, provider_config, tag, test_run_id)
|
362
368
|
# A Chat Session tracks the template_name and variables for a set of chat completions.
|
@@ -370,7 +376,7 @@ class ChatSession(Session):
|
|
370
376
|
def last_message(self) -> Optional[ChatMessage]:
|
371
377
|
return self.message_history[len(self.message_history) - 1]
|
372
378
|
|
373
|
-
def store_new_messages(self, new_messages:
|
379
|
+
def store_new_messages(self, new_messages: List[ChatMessage]) -> None:
|
374
380
|
for message in new_messages:
|
375
381
|
self.message_history.append({
|
376
382
|
"role": message["role"],
|
@@ -412,7 +418,7 @@ class ChatSession(Session):
|
|
412
418
|
|
413
419
|
def continue_chat(
|
414
420
|
self,
|
415
|
-
new_messages: Optional[
|
421
|
+
new_messages: Optional[List[ChatMessage]] = None,
|
416
422
|
**kwargs: Any
|
417
423
|
) -> ChatCompletionResponse:
|
418
424
|
|
@@ -438,7 +444,7 @@ class ChatSession(Session):
|
|
438
444
|
|
439
445
|
def continue_chat_stream(
|
440
446
|
self,
|
441
|
-
new_messages: Optional[
|
447
|
+
new_messages: Optional[List[ChatMessage]] = None,
|
442
448
|
**kwargs: Any
|
443
449
|
) -> Generator[CompletionChunk, None, None]:
|
444
450
|
new_messages = new_messages or []
|
@@ -468,7 +474,7 @@ class FreeplayTestRun:
|
|
468
474
|
flavor: Optional[Flavor],
|
469
475
|
provider_config: ProviderConfig,
|
470
476
|
test_run_id: str,
|
471
|
-
inputs:
|
477
|
+
inputs: List[Dict[str, str]]
|
472
478
|
):
|
473
479
|
self.call_support = call_support
|
474
480
|
self.flavor = flavor
|
@@ -476,7 +482,7 @@ class FreeplayTestRun:
|
|
476
482
|
self.test_run_id = test_run_id
|
477
483
|
self.inputs = inputs
|
478
484
|
|
479
|
-
def get_inputs(self) ->
|
485
|
+
def get_inputs(self) -> List[Dict[str, str]]:
|
480
486
|
return self.inputs
|
481
487
|
|
482
488
|
def create_session(self, project_id: str, tag: str = default_tag) -> Session:
|
@@ -501,7 +507,7 @@ class Freeplay:
|
|
501
507
|
**kwargs: Any
|
502
508
|
) -> None:
|
503
509
|
if not freeplay_api_key or not freeplay_api_key.strip():
|
504
|
-
raise FreeplayConfigurationError("Freeplay API key not set. It must be set to the Freeplay API.")
|
510
|
+
raise FreeplayConfigurationError("Freeplay API key not set. It must be set to use the Freeplay API.")
|
505
511
|
provider_config.validate()
|
506
512
|
|
507
513
|
self.__record_processor = record_processor or DefaultRecordProcessor(freeplay_api_key, api_base)
|
@@ -522,7 +528,7 @@ class Freeplay:
|
|
522
528
|
project_id: str,
|
523
529
|
session_id: str,
|
524
530
|
template_name: str,
|
525
|
-
variables:
|
531
|
+
variables: Dict[str, str],
|
526
532
|
tag: str = default_tag,
|
527
533
|
flavor: Optional[Flavor] = None,
|
528
534
|
**kwargs: Any
|
@@ -544,10 +550,10 @@ class Freeplay:
|
|
544
550
|
self,
|
545
551
|
project_id: str,
|
546
552
|
template_name: str,
|
547
|
-
variables:
|
553
|
+
variables: Dict[str, str],
|
548
554
|
tag: str = default_tag,
|
549
555
|
flavor: Optional[Flavor] = None,
|
550
|
-
metadata: Optional[
|
556
|
+
metadata: Optional[Dict[str, Union[str,int,float]]] = None,
|
551
557
|
**kwargs: Any
|
552
558
|
) -> CompletionResponse:
|
553
559
|
project_session = self.call_support.create_session(project_id, tag, None, metadata)
|
@@ -567,10 +573,10 @@ class Freeplay:
|
|
567
573
|
self,
|
568
574
|
project_id: str,
|
569
575
|
template_name: str,
|
570
|
-
variables:
|
576
|
+
variables: Dict[str, str],
|
571
577
|
tag: str = default_tag,
|
572
578
|
flavor: Optional[Flavor] = None,
|
573
|
-
metadata: Optional[
|
579
|
+
metadata: Optional[Dict[str, Union[str,int,float]]] = None,
|
574
580
|
**kwargs: Any
|
575
581
|
) -> Generator[CompletionChunk, None, None]:
|
576
582
|
project_session = self.call_support.create_session(project_id, tag, None, metadata)
|
@@ -611,7 +617,7 @@ class Freeplay:
|
|
611
617
|
template_name: str,
|
612
618
|
variables: Variables,
|
613
619
|
tag: str = default_tag,
|
614
|
-
metadata: Optional[
|
620
|
+
metadata: Optional[Dict[str, Union[str,int,float]]] = None,
|
615
621
|
**kwargs: Any
|
616
622
|
) -> Tuple[ChatSession, ChatCompletionResponse]:
|
617
623
|
session = self.__create_chat_session(project_id, tag, template_name, variables, metadata)
|
@@ -625,7 +631,7 @@ class Freeplay:
|
|
625
631
|
session_id: str,
|
626
632
|
variables: Variables,
|
627
633
|
tag: str = default_tag,
|
628
|
-
messages: Optional[
|
634
|
+
messages: Optional[List[ChatMessage]] = None,
|
629
635
|
flavor: Optional[ChatFlavor] = None) -> ChatSession:
|
630
636
|
prompts = self.call_support.get_prompts(project_id, tag)
|
631
637
|
chat_flavor = flavor or require_chat_flavor(self.client_flavor) if self.client_flavor else None
|
@@ -647,7 +653,7 @@ class Freeplay:
|
|
647
653
|
template_name: str,
|
648
654
|
variables: Variables,
|
649
655
|
tag: str = default_tag,
|
650
|
-
metadata: Optional[
|
656
|
+
metadata: Optional[Dict[str, Union[str,int,float]]] = None,
|
651
657
|
**kwargs: Any
|
652
658
|
) -> Tuple[ChatSession, Generator[CompletionChunk, None, None]]:
|
653
659
|
"""Returns a chat session, the base prompt template messages, and a streamed response from the LLM."""
|
@@ -661,7 +667,7 @@ class Freeplay:
|
|
661
667
|
tag: str,
|
662
668
|
template_name: str,
|
663
669
|
variables: Variables,
|
664
|
-
metadata: Optional[
|
670
|
+
metadata: Optional[Dict[str, Union[str,int,float]]] = None) -> ChatSession:
|
665
671
|
chat_flavor = require_chat_flavor(self.client_flavor) if self.client_flavor else None
|
666
672
|
|
667
673
|
project_session = self.call_support.create_session(project_id, tag, None, metadata)
|
@@ -700,7 +706,8 @@ def require_chat_flavor(flavor: Flavor) -> ChatFlavor:
|
|
700
706
|
|
701
707
|
return flavor
|
702
708
|
|
703
|
-
|
709
|
+
|
710
|
+
def check_all_values_string_or_number(metadata: Optional[Dict[str, Union[str,int,float]]]) -> None:
|
704
711
|
if metadata:
|
705
712
|
for key, value in metadata.items():
|
706
713
|
if not isinstance(value, (str, int, float)):
|
@@ -0,0 +1,106 @@
|
|
1
|
+
import dataclasses
|
2
|
+
import json
|
3
|
+
import os
|
4
|
+
import sys
|
5
|
+
from pathlib import Path
|
6
|
+
from stat import S_IREAD, S_IRGRP, S_IROTH, S_IWUSR
|
7
|
+
|
8
|
+
import click
|
9
|
+
|
10
|
+
from .completions import PromptTemplates, PromptTemplateWithMetadata
|
11
|
+
from .errors import FreeplayClientError, FreeplayServerError
|
12
|
+
from .freeplay_thin import FreeplayThin
|
13
|
+
|
14
|
+
|
15
|
+
@click.group()
|
16
|
+
def cli() -> None:
|
17
|
+
pass
|
18
|
+
|
19
|
+
|
20
|
+
@cli.command()
|
21
|
+
@click.option("--project-id", required=True, help="The Freeplay project ID.")
|
22
|
+
@click.option("--environment", required=True, help="The environment from which the prompts will be pulled.")
|
23
|
+
@click.option("--output-dir", required=True, help="The directory where the prompts will be saved.")
|
24
|
+
def download(project_id: str, environment: str, output_dir: str) -> None:
|
25
|
+
if "FREEPLAY_API_KEY" not in os.environ:
|
26
|
+
print("FREEPLAY_API_KEY is not set. It is required to run the freeplay command.", file=sys.stderr)
|
27
|
+
exit(4)
|
28
|
+
|
29
|
+
if "FREEPLAY_SUBDOMAIN" not in os.environ:
|
30
|
+
print("FREEPLAY_SUBDOMAIN is not set. It is required to run the freeplay command.", file=sys.stderr)
|
31
|
+
exit(4)
|
32
|
+
|
33
|
+
FREEPLAY_API_KEY = os.environ["FREEPLAY_API_KEY"]
|
34
|
+
freeplay_api_url = f'https://{os.environ["FREEPLAY_SUBDOMAIN"]}.freeplay.ai/api'
|
35
|
+
|
36
|
+
if "FREEPLAY_API_URL" in os.environ:
|
37
|
+
freeplay_api_url = f'{os.environ["FREEPLAY_API_URL"]}/api'
|
38
|
+
click.echo("Using URL override for Freeplay specified in the FREEPLAY_API_URL environment variable")
|
39
|
+
|
40
|
+
click.echo("Downloading prompts for project %s, environment %s, to directory %s from %s" %
|
41
|
+
(project_id, environment, output_dir, freeplay_api_url))
|
42
|
+
|
43
|
+
fp_client = FreeplayThin(
|
44
|
+
freeplay_api_key=FREEPLAY_API_KEY,
|
45
|
+
api_base=freeplay_api_url
|
46
|
+
)
|
47
|
+
|
48
|
+
try:
|
49
|
+
prompts: PromptTemplates = fp_client.get_prompts(project_id, tag=environment)
|
50
|
+
click.echo("Found %s prompt templates" % len(prompts.templates))
|
51
|
+
|
52
|
+
for prompt in prompts.templates:
|
53
|
+
__write_single_file(environment, output_dir, project_id, prompt)
|
54
|
+
except FreeplayClientError as e:
|
55
|
+
print("Error downloading templates: %s.\nIs your project ID correct?" % e, file=sys.stderr)
|
56
|
+
exit(1)
|
57
|
+
except FreeplayServerError as e:
|
58
|
+
print("Error on Freeplay's servers downloading templates: %s.\nTry again after a short wait." % e,
|
59
|
+
file=sys.stderr)
|
60
|
+
exit(2)
|
61
|
+
except Exception as e:
|
62
|
+
print("Error downloading templates: %s" % e, file=sys.stderr)
|
63
|
+
exit(3)
|
64
|
+
|
65
|
+
|
66
|
+
def __write_single_file(
|
67
|
+
environment: str,
|
68
|
+
output_dir: str,
|
69
|
+
project_id: str,
|
70
|
+
prompt: PromptTemplateWithMetadata
|
71
|
+
) -> None:
|
72
|
+
directory = __root_dir(environment, output_dir, project_id)
|
73
|
+
basename = f'{prompt.name}'
|
74
|
+
prompt_path = directory / f'{basename}.json'
|
75
|
+
click.echo("Writing prompt file: %s" % prompt_path)
|
76
|
+
|
77
|
+
full_dict = dataclasses.asdict(prompt)
|
78
|
+
del full_dict['prompt_template_id']
|
79
|
+
del full_dict['prompt_template_version_id']
|
80
|
+
del full_dict['name']
|
81
|
+
del full_dict['content']
|
82
|
+
|
83
|
+
output_dict = {
|
84
|
+
'prompt_template_id': prompt.prompt_template_id,
|
85
|
+
'prompt_template_version_id': prompt.prompt_template_version_id,
|
86
|
+
'name': prompt.name,
|
87
|
+
'content': prompt.content,
|
88
|
+
'metadata': full_dict
|
89
|
+
}
|
90
|
+
|
91
|
+
# Make sure it's owner writable if it already exists
|
92
|
+
if prompt_path.is_file():
|
93
|
+
os.chmod(prompt_path, S_IWUSR | S_IREAD)
|
94
|
+
|
95
|
+
with prompt_path.open(mode='w') as f:
|
96
|
+
f.write(json.dumps(output_dict, sort_keys=True, indent=4))
|
97
|
+
f.write('\n')
|
98
|
+
|
99
|
+
# Make the file read-only to discourage local changes
|
100
|
+
os.chmod(prompt_path, S_IREAD | S_IRGRP | S_IROTH)
|
101
|
+
|
102
|
+
|
103
|
+
def __root_dir(environment: str, output_dir: str, project_id: str) -> Path:
|
104
|
+
directory = Path(output_dir) / "freeplay" / "prompts" / project_id / environment
|
105
|
+
os.makedirs(directory, exist_ok=True)
|
106
|
+
return directory
|
@@ -0,0 +1,21 @@
|
|
1
|
+
from .completions import PromptTemplates
|
2
|
+
from .errors import FreeplayConfigurationError
|
3
|
+
from .freeplay import CallSupport
|
4
|
+
from .record import DefaultRecordProcessor
|
5
|
+
|
6
|
+
|
7
|
+
class FreeplayThin:
|
8
|
+
def __init__(
|
9
|
+
self,
|
10
|
+
freeplay_api_key: str,
|
11
|
+
api_base: str
|
12
|
+
) -> None:
|
13
|
+
if not freeplay_api_key or not freeplay_api_key.strip():
|
14
|
+
raise FreeplayConfigurationError("Freeplay API key not set. It must be set to the Freeplay API.")
|
15
|
+
|
16
|
+
self.call_support = CallSupport(freeplay_api_key, api_base, DefaultRecordProcessor(freeplay_api_key, api_base))
|
17
|
+
self.freeplay_api_key = freeplay_api_key
|
18
|
+
self.api_base = api_base
|
19
|
+
|
20
|
+
def get_prompts(self, project_id: str, tag: str) -> PromptTemplates:
|
21
|
+
return self.call_support.get_prompts(project_id=project_id, tag=tag)
|
@@ -1,14 +1,12 @@
|
|
1
1
|
import copy
|
2
2
|
import logging
|
3
|
-
from
|
4
|
-
from typing import Any, Optional
|
3
|
+
from typing import Any, Dict, Optional
|
5
4
|
|
6
5
|
logger = logging.getLogger(__name__)
|
7
6
|
|
8
7
|
|
9
|
-
|
10
|
-
|
11
|
-
def __init__(self, members: dict[str, Any]) -> None:
|
8
|
+
class LLMParameters(Dict[str, Any]):
|
9
|
+
def __init__(self, members: Dict[str, Any]) -> None:
|
12
10
|
super().__init__(members)
|
13
11
|
|
14
12
|
@classmethod
|
@@ -7,15 +7,7 @@ from .errors import FreeplayConfigurationError
|
|
7
7
|
@dataclass
|
8
8
|
class OpenAIConfig:
|
9
9
|
api_key: str
|
10
|
-
|
11
|
-
|
12
|
-
def __init__(
|
13
|
-
self,
|
14
|
-
api_key: str,
|
15
|
-
api_base: Optional[str] = None,
|
16
|
-
) -> None:
|
17
|
-
self.api_key = api_key
|
18
|
-
self.api_base = api_base
|
10
|
+
base_url: Optional[str] = None
|
19
11
|
|
20
12
|
def validate(self) -> None:
|
21
13
|
if not self.api_key or not self.api_key.strip():
|
@@ -24,19 +16,8 @@ class OpenAIConfig:
|
|
24
16
|
|
25
17
|
@dataclass
|
26
18
|
class AzureConfig(OpenAIConfig):
|
27
|
-
engine: Optional[str]
|
28
|
-
api_version: Optional[str]
|
29
|
-
|
30
|
-
def __init__(
|
31
|
-
self,
|
32
|
-
api_key: str,
|
33
|
-
api_base: Optional[str] = None,
|
34
|
-
engine: Optional[str] = None,
|
35
|
-
api_version: Optional[str] = None
|
36
|
-
):
|
37
|
-
super().__init__(api_key, api_base)
|
38
|
-
self.api_version = api_version
|
39
|
-
self.engine = engine
|
19
|
+
engine: Optional[str] = None
|
20
|
+
api_version: Optional[str] = None
|
40
21
|
|
41
22
|
def validate(self) -> None:
|
42
23
|
super().validate()
|
@@ -61,7 +42,7 @@ class ProviderConfig:
|
|
61
42
|
azure: Optional[AzureConfig] = None
|
62
43
|
|
63
44
|
def validate(self) -> None:
|
64
|
-
if
|
45
|
+
if all(config is None for config in [self.anthropic, self.openai, self.azure]):
|
65
46
|
FreeplayConfigurationError("At least one provider key must be set in ProviderConfig.")
|
66
47
|
|
67
48
|
if self.openai is not None:
|
@@ -1,6 +1,6 @@
|
|
1
1
|
import logging
|
2
2
|
from dataclasses import dataclass
|
3
|
-
from typing import
|
3
|
+
from typing import Any, Dict, Optional
|
4
4
|
from abc import abstractmethod, ABC
|
5
5
|
|
6
6
|
from .llm_parameters import LLMParameters
|
@@ -18,7 +18,7 @@ class RecordCallFields:
|
|
18
18
|
session_id: str
|
19
19
|
start: float
|
20
20
|
target_template: PromptTemplateWithMetadata
|
21
|
-
variables:
|
21
|
+
variables: Dict[str, str]
|
22
22
|
tag: str
|
23
23
|
test_run_id: Optional[str]
|
24
24
|
record_format_type: Optional[str]
|
@@ -60,7 +60,7 @@ class DefaultRecordProcessor(RecordProcessor):
|
|
60
60
|
) -> None:
|
61
61
|
record_payload = {
|
62
62
|
"session_id": record_call.session_id,
|
63
|
-
"project_version_id": record_call.target_template.
|
63
|
+
"project_version_id": record_call.target_template.prompt_template_version_id,
|
64
64
|
"prompt_template_id": record_call.target_template.prompt_template_id,
|
65
65
|
"start_time": record_call.start,
|
66
66
|
"end_time": record_call.end,
|
@@ -0,0 +1,21 @@
|
|
1
|
+
import pystache # type: ignore
|
2
|
+
from pydantic import RootModel, ValidationError
|
3
|
+
from typing import Any, Dict, List, Union
|
4
|
+
|
5
|
+
from .errors import FreeplayError
|
6
|
+
|
7
|
+
InputVariable = RootModel[Union[Dict[str, "InputVariable"], List["InputVariable"], str, int, bool, float]]
|
8
|
+
InputVariable.model_rebuild()
|
9
|
+
InputVariables = Dict[str, Union[str, int, bool, Dict[str, Any], List[Any]]]
|
10
|
+
PydanticInputVariables = RootModel[Dict[str, InputVariable]]
|
11
|
+
|
12
|
+
def format_template_variables(template: str, variables: Any) -> str:
|
13
|
+
# Validate that the variables are of the correct type, and do not include functions or None values.
|
14
|
+
try:
|
15
|
+
PydanticInputVariables.model_validate(variables)
|
16
|
+
except ValidationError as err:
|
17
|
+
raise FreeplayError('Variables must be a string, number, bool, or a possibly nested list or dict of strings, numbers and booleans.')
|
18
|
+
|
19
|
+
# When rendering mustache, do not escape HTML special characters.
|
20
|
+
rendered: str = pystache.Renderer(escape=lambda s: s).render(template, variables)
|
21
|
+
return rendered
|
@@ -1,11 +0,0 @@
|
|
1
|
-
import re
|
2
|
-
|
3
|
-
from .errors import FreeplayError
|
4
|
-
|
5
|
-
variable_regex = re.compile(r"{{(\w+)}}")
|
6
|
-
|
7
|
-
def format_template_variables(template_content: str, variables: dict[str, str]) -> str:
|
8
|
-
try:
|
9
|
-
return variable_regex.sub(lambda match: variables[match.group(1)], template_content)
|
10
|
-
except KeyError as e:
|
11
|
-
raise FreeplayError(f"Missing variable with key: {e}.")
|
File without changes
|
File without changes
|
File without changes
|