freeplay 0.3.18__py3-none-any.whl → 0.3.19__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- freeplay/resources/adapters.py +203 -0
- freeplay/resources/prompts.py +99 -96
- freeplay/resources/recordings.py +21 -1
- freeplay/support.py +26 -3
- {freeplay-0.3.18.dist-info → freeplay-0.3.19.dist-info}/METADATA +1 -1
- {freeplay-0.3.18.dist-info → freeplay-0.3.19.dist-info}/RECORD +9 -8
- {freeplay-0.3.18.dist-info → freeplay-0.3.19.dist-info}/LICENSE +0 -0
- {freeplay-0.3.18.dist-info → freeplay-0.3.19.dist-info}/WHEEL +0 -0
- {freeplay-0.3.18.dist-info → freeplay-0.3.19.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,203 @@
|
|
1
|
+
import copy
|
2
|
+
from dataclasses import dataclass
|
3
|
+
from typing import Protocol, Dict, List, Union, Any
|
4
|
+
|
5
|
+
from freeplay.errors import FreeplayConfigurationError
|
6
|
+
|
7
|
+
|
8
|
+
@dataclass
|
9
|
+
class TextContent:
|
10
|
+
text: str
|
11
|
+
|
12
|
+
|
13
|
+
@dataclass
|
14
|
+
class ImageContentUrl:
|
15
|
+
url: str
|
16
|
+
|
17
|
+
|
18
|
+
@dataclass
|
19
|
+
class ImageContentBase64:
|
20
|
+
content_type: str
|
21
|
+
data: str
|
22
|
+
|
23
|
+
|
24
|
+
class MissingFlavorError(FreeplayConfigurationError):
|
25
|
+
def __init__(self, flavor_name: str):
|
26
|
+
super().__init__(
|
27
|
+
f'Configured flavor ({flavor_name}) not found in SDK. Please update your SDK version or configure '
|
28
|
+
'a different model in the Freeplay UI.'
|
29
|
+
)
|
30
|
+
|
31
|
+
|
32
|
+
class LLMAdapter(Protocol):
|
33
|
+
def to_llm_syntax(self, messages: List[Dict[str, Any]]) -> Union[str, List[Dict[str, Any]]]:
|
34
|
+
pass
|
35
|
+
|
36
|
+
|
37
|
+
class PassthroughAdapter(LLMAdapter):
|
38
|
+
def to_llm_syntax(self, messages: List[Dict[str, Any]]) -> Union[str, List[Dict[str, Any]]]:
|
39
|
+
# We need a deepcopy here to avoid referential equality with the llm_prompt
|
40
|
+
return copy.deepcopy(messages)
|
41
|
+
|
42
|
+
|
43
|
+
class AnthropicAdapter(LLMAdapter):
|
44
|
+
def to_llm_syntax(self, messages: List[Dict[str, Any]]) -> Union[str, List[Dict[str, Any]]]:
|
45
|
+
anthropic_messages = []
|
46
|
+
|
47
|
+
for message in messages:
|
48
|
+
if message['role'] == 'system':
|
49
|
+
continue
|
50
|
+
if "has_media" in message and message["has_media"]:
|
51
|
+
anthropic_messages.append({
|
52
|
+
'role': message['role'],
|
53
|
+
'content': [self.__map_content(content) for content in message['content']]
|
54
|
+
})
|
55
|
+
else:
|
56
|
+
anthropic_messages.append(copy.deepcopy(message))
|
57
|
+
|
58
|
+
return anthropic_messages
|
59
|
+
|
60
|
+
@staticmethod
|
61
|
+
def __map_content(content: Union[TextContent, ImageContentBase64, ImageContentUrl]) -> Dict[str, Any]:
|
62
|
+
if isinstance(content, TextContent):
|
63
|
+
return {
|
64
|
+
"type": "text",
|
65
|
+
"text": content.text
|
66
|
+
}
|
67
|
+
elif isinstance(content, ImageContentBase64):
|
68
|
+
return {
|
69
|
+
"type": "image",
|
70
|
+
"source": {
|
71
|
+
"type": "base64",
|
72
|
+
"media_type": content.content_type,
|
73
|
+
"data": content.data,
|
74
|
+
}
|
75
|
+
}
|
76
|
+
elif isinstance(content, ImageContentUrl):
|
77
|
+
return {
|
78
|
+
"type": "image",
|
79
|
+
"source": {
|
80
|
+
"type": "url",
|
81
|
+
"url": content.url,
|
82
|
+
}
|
83
|
+
}
|
84
|
+
else:
|
85
|
+
raise ValueError(f"Unexpected content type {type(content)}")
|
86
|
+
|
87
|
+
|
88
|
+
class OpenAIAdapter(LLMAdapter):
|
89
|
+
def to_llm_syntax(self, messages: List[Dict[str, Any]]) -> Union[str, List[Dict[str, Any]]]:
|
90
|
+
openai_messages = []
|
91
|
+
|
92
|
+
for message in messages:
|
93
|
+
if "has_media" in message and message["has_media"]:
|
94
|
+
openai_messages.append({
|
95
|
+
'role': message['role'],
|
96
|
+
'content': [self.__map_content(content) for content in message['content']]
|
97
|
+
})
|
98
|
+
else:
|
99
|
+
openai_messages.append(copy.deepcopy(message))
|
100
|
+
|
101
|
+
return openai_messages
|
102
|
+
|
103
|
+
@staticmethod
|
104
|
+
def __map_content(content: Union[TextContent, ImageContentBase64, ImageContentUrl]) -> Dict[str, Any]:
|
105
|
+
if isinstance(content, TextContent):
|
106
|
+
return {
|
107
|
+
"type": "text",
|
108
|
+
"text": content.text
|
109
|
+
}
|
110
|
+
elif isinstance(content, ImageContentBase64):
|
111
|
+
return {
|
112
|
+
"type": "image_url",
|
113
|
+
"image_url": {
|
114
|
+
"url": f"data:{content.content_type};base64,{content.data}"
|
115
|
+
}
|
116
|
+
}
|
117
|
+
elif isinstance(content, ImageContentUrl):
|
118
|
+
return {
|
119
|
+
"type": "image_url",
|
120
|
+
"image_url": {
|
121
|
+
"url": content.url
|
122
|
+
}
|
123
|
+
}
|
124
|
+
else:
|
125
|
+
raise ValueError(f"Unexpected content type {type(content)}")
|
126
|
+
|
127
|
+
|
128
|
+
class Llama3Adapter(LLMAdapter):
|
129
|
+
def to_llm_syntax(self, messages: List[Dict[str, Any]]) -> Union[str, List[Dict[str, Any]]]:
|
130
|
+
if len(messages) < 1:
|
131
|
+
raise ValueError("Must have at least one message to format")
|
132
|
+
|
133
|
+
formatted = "<|begin_of_text|>"
|
134
|
+
for message in messages:
|
135
|
+
formatted += f"<|start_header_id|>{message['role']}<|end_header_id|>\n{message['content']}<|eot_id|>"
|
136
|
+
formatted += "<|start_header_id|>assistant<|end_header_id|>"
|
137
|
+
|
138
|
+
return formatted
|
139
|
+
|
140
|
+
|
141
|
+
class GeminiAdapter(LLMAdapter):
|
142
|
+
def to_llm_syntax(self, messages: List[Dict[str, Any]]) -> Union[str, List[Dict[str, Any]]]:
|
143
|
+
if len(messages) < 1:
|
144
|
+
raise ValueError("Must have at least one message to format")
|
145
|
+
|
146
|
+
gemini_messages = []
|
147
|
+
|
148
|
+
for message in messages:
|
149
|
+
if message['role'] == 'system':
|
150
|
+
continue
|
151
|
+
|
152
|
+
if "has_media" in message and message["has_media"]:
|
153
|
+
gemini_messages.append({
|
154
|
+
"role": self.__translate_role(message["role"]),
|
155
|
+
"parts": [self.__map_content(content) for content in message['content']]
|
156
|
+
})
|
157
|
+
else:
|
158
|
+
gemini_messages.append({
|
159
|
+
"role": self.__translate_role(message["role"]),
|
160
|
+
"parts": [{"text": message["content"]}]
|
161
|
+
})
|
162
|
+
|
163
|
+
return gemini_messages
|
164
|
+
|
165
|
+
@staticmethod
|
166
|
+
def __map_content(content: Union[TextContent, ImageContentBase64, ImageContentUrl]) -> Dict[str, Any]:
|
167
|
+
if isinstance(content, TextContent):
|
168
|
+
return {"text": content.text}
|
169
|
+
elif isinstance(content, ImageContentBase64):
|
170
|
+
return {
|
171
|
+
"inline_data": {
|
172
|
+
"data": content.data,
|
173
|
+
"mime_type": content.content_type,
|
174
|
+
}
|
175
|
+
}
|
176
|
+
elif isinstance(content, ImageContentUrl):
|
177
|
+
raise ValueError("Message contains an image URL, but image URLs are not supported by Gemini")
|
178
|
+
else:
|
179
|
+
raise ValueError(f"Unexpected content type {type(content)}")
|
180
|
+
|
181
|
+
@staticmethod
|
182
|
+
def __translate_role(role: str) -> str:
|
183
|
+
if role == "user":
|
184
|
+
return "user"
|
185
|
+
elif role == "assistant":
|
186
|
+
return "model"
|
187
|
+
else:
|
188
|
+
raise ValueError(f"Gemini formatting found unexpected role {role}")
|
189
|
+
|
190
|
+
|
191
|
+
def adaptor_for_flavor(flavor_name: str) -> LLMAdapter:
|
192
|
+
if flavor_name in ["baseten_mistral_chat", "mistral_chat", "perplexity_chat"]:
|
193
|
+
return PassthroughAdapter()
|
194
|
+
elif flavor_name in ["azure_openai_chat", "openai_chat"]:
|
195
|
+
return OpenAIAdapter()
|
196
|
+
elif flavor_name == "anthropic_chat":
|
197
|
+
return AnthropicAdapter()
|
198
|
+
elif flavor_name == "llama_3_chat":
|
199
|
+
return Llama3Adapter()
|
200
|
+
elif flavor_name == "gemini_chat":
|
201
|
+
return GeminiAdapter()
|
202
|
+
else:
|
203
|
+
raise MissingFlavorError(flavor_name)
|
freeplay/resources/prompts.py
CHANGED
@@ -1,4 +1,3 @@
|
|
1
|
-
import copy
|
2
1
|
import json
|
3
2
|
import logging
|
4
3
|
import warnings
|
@@ -16,6 +15,7 @@ from typing import (
|
|
16
15
|
Union,
|
17
16
|
cast,
|
18
17
|
runtime_checkable,
|
18
|
+
Literal,
|
19
19
|
)
|
20
20
|
|
21
21
|
from freeplay.errors import (
|
@@ -25,26 +25,21 @@ from freeplay.errors import (
|
|
25
25
|
)
|
26
26
|
from freeplay.llm_parameters import LLMParameters
|
27
27
|
from freeplay.model import InputVariables
|
28
|
+
from freeplay.resources.adapters import MissingFlavorError, adaptor_for_flavor, ImageContentBase64, ImageContentUrl, \
|
29
|
+
TextContent
|
28
30
|
from freeplay.support import (
|
29
31
|
CallSupport,
|
30
32
|
PromptTemplate,
|
31
33
|
PromptTemplateMetadata,
|
32
34
|
PromptTemplates,
|
33
|
-
|
35
|
+
TemplateMessage,
|
36
|
+
ToolSchema, TemplateChatMessage, HistoryTemplateMessage, MediaSlot, Role,
|
34
37
|
)
|
35
38
|
from freeplay.utils import bind_template_variables, convert_provider_message_to_dict
|
36
39
|
|
37
40
|
logger = logging.getLogger(__name__)
|
38
41
|
|
39
42
|
|
40
|
-
class MissingFlavorError(FreeplayConfigurationError):
|
41
|
-
def __init__(self, flavor_name: str):
|
42
|
-
super().__init__(
|
43
|
-
f'Configured flavor ({flavor_name}) not found in SDK. Please update your SDK version or configure '
|
44
|
-
'a different model in the Freeplay UI.'
|
45
|
-
)
|
46
|
-
|
47
|
-
|
48
43
|
class UnsupportedToolSchemaError(FreeplayConfigurationError):
|
49
44
|
def __init__(self) -> None:
|
50
45
|
super().__init__(
|
@@ -97,12 +92,12 @@ class PromptInfo:
|
|
97
92
|
|
98
93
|
class FormattedPrompt:
|
99
94
|
def __init__(
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
95
|
+
self,
|
96
|
+
prompt_info: PromptInfo,
|
97
|
+
messages: List[Dict[str, str]],
|
98
|
+
formatted_prompt: Optional[List[Dict[str, str]]] = None,
|
99
|
+
formatted_prompt_text: Optional[str] = None,
|
100
|
+
tool_schema: Optional[List[Dict[str, Any]]] = None
|
106
101
|
):
|
107
102
|
# These two definitions allow us to operate on typed fields until we expose them as Any for client use.
|
108
103
|
self._llm_prompt = formatted_prompt
|
@@ -142,63 +137,15 @@ class FormattedPrompt:
|
|
142
137
|
|
143
138
|
class BoundPrompt:
|
144
139
|
def __init__(
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
140
|
+
self,
|
141
|
+
prompt_info: PromptInfo,
|
142
|
+
messages: List[Dict[str, Any]],
|
143
|
+
tool_schema: Optional[List[ToolSchema]] = None
|
149
144
|
):
|
150
145
|
self.prompt_info = prompt_info
|
151
146
|
self.messages = messages
|
152
147
|
self.tool_schema = tool_schema
|
153
148
|
|
154
|
-
@staticmethod
|
155
|
-
def __format_messages_for_flavor(
|
156
|
-
flavor_name: str,
|
157
|
-
messages: List[Dict[str, str]]
|
158
|
-
) -> Union[str, List[Dict[str, str]]]:
|
159
|
-
if flavor_name in [
|
160
|
-
'azure_openai_chat',
|
161
|
-
'openai_chat',
|
162
|
-
'baseten_mistral_chat',
|
163
|
-
'mistral_chat',
|
164
|
-
'perplexity_chat'
|
165
|
-
]:
|
166
|
-
# We need a deepcopy here to avoid referential equality with the llm_prompt
|
167
|
-
return copy.deepcopy(messages)
|
168
|
-
elif flavor_name == 'anthropic_chat':
|
169
|
-
messages_without_system = [message for message in messages if message['role'] != 'system']
|
170
|
-
return messages_without_system
|
171
|
-
elif flavor_name == 'llama_3_chat':
|
172
|
-
if len(messages) < 1:
|
173
|
-
raise ValueError("Must have at least one message to format")
|
174
|
-
|
175
|
-
formatted = "<|begin_of_text|>"
|
176
|
-
for message in messages:
|
177
|
-
formatted += f"<|start_header_id|>{message['role']}<|end_header_id|>\n{message['content']}<|eot_id|>"
|
178
|
-
formatted += "<|start_header_id|>assistant<|end_header_id|>"
|
179
|
-
|
180
|
-
return formatted
|
181
|
-
elif flavor_name == 'gemini_chat':
|
182
|
-
if len(messages) < 1:
|
183
|
-
raise ValueError("Must have at least one message to format")
|
184
|
-
|
185
|
-
def translate_role(role: str) -> str:
|
186
|
-
if role == "user":
|
187
|
-
return "user"
|
188
|
-
elif role == "assistant":
|
189
|
-
return "model"
|
190
|
-
else:
|
191
|
-
raise ValueError(f"Gemini formatting found unexpected role {role}")
|
192
|
-
|
193
|
-
formatted = [ # type: ignore
|
194
|
-
{'role': translate_role(message['role']), 'parts': [{'text': message['content']}]}
|
195
|
-
for message in messages if message['role'] != 'system'
|
196
|
-
]
|
197
|
-
|
198
|
-
return formatted
|
199
|
-
|
200
|
-
raise MissingFlavorError(flavor_name)
|
201
|
-
|
202
149
|
@staticmethod
|
203
150
|
def __format_tool_schema(flavor_name: str, tool_schema: List[ToolSchema]) -> List[Dict[str, Any]]:
|
204
151
|
if flavor_name == 'anthropic_chat':
|
@@ -218,11 +165,12 @@ class BoundPrompt:
|
|
218
165
|
raise UnsupportedToolSchemaError()
|
219
166
|
|
220
167
|
def format(
|
221
|
-
|
222
|
-
|
168
|
+
self,
|
169
|
+
flavor_name: Optional[str] = None
|
223
170
|
) -> FormattedPrompt:
|
224
171
|
final_flavor = flavor_name or self.prompt_info.flavor_name
|
225
|
-
|
172
|
+
adapter = adaptor_for_flavor(final_flavor)
|
173
|
+
formatted_prompt = adapter.to_llm_syntax(self.messages)
|
226
174
|
|
227
175
|
formatted_tool_schema = BoundPrompt.__format_tool_schema(
|
228
176
|
final_flavor,
|
@@ -245,12 +193,47 @@ class BoundPrompt:
|
|
245
193
|
)
|
246
194
|
|
247
195
|
|
196
|
+
@dataclass
|
197
|
+
class MediaInputUrl:
|
198
|
+
type: Literal["url"]
|
199
|
+
url: str
|
200
|
+
|
201
|
+
|
202
|
+
@dataclass
|
203
|
+
class MediaInputBase64:
|
204
|
+
type: Literal["base64"]
|
205
|
+
data: str
|
206
|
+
content_type: str
|
207
|
+
|
208
|
+
|
209
|
+
MediaInput = Union[MediaInputUrl, MediaInputBase64]
|
210
|
+
|
211
|
+
MediaInputMap = Dict[str, MediaInput]
|
212
|
+
|
213
|
+
|
214
|
+
def extract_media_content(media_inputs: MediaInputMap, media_slots: List[MediaSlot]) -> List[
|
215
|
+
Union[ImageContentBase64, ImageContentUrl]]:
|
216
|
+
media_content: List[Union[ImageContentBase64, ImageContentUrl]] = []
|
217
|
+
for slot in media_slots:
|
218
|
+
if slot.type != "image":
|
219
|
+
continue
|
220
|
+
file = media_inputs.get(slot.placeholder_name, None)
|
221
|
+
if file is None:
|
222
|
+
continue
|
223
|
+
if isinstance(file, MediaInputUrl):
|
224
|
+
media_content.append(ImageContentUrl(url=file.url))
|
225
|
+
else:
|
226
|
+
media_content.append(ImageContentBase64(content_type=file.content_type, data=file.data))
|
227
|
+
|
228
|
+
return media_content
|
229
|
+
|
230
|
+
|
248
231
|
class TemplatePrompt:
|
249
232
|
def __init__(
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
233
|
+
self,
|
234
|
+
prompt_info: PromptInfo,
|
235
|
+
messages: List[TemplateMessage],
|
236
|
+
tool_schema: Optional[List[ToolSchema]] = None
|
254
237
|
):
|
255
238
|
self.prompt_info = prompt_info
|
256
239
|
self.tool_schema = tool_schema
|
@@ -260,11 +243,13 @@ class TemplatePrompt:
|
|
260
243
|
self,
|
261
244
|
variables: InputVariables,
|
262
245
|
history: Optional[Sequence[ProviderMessage]] = None,
|
246
|
+
media_inputs: Optional[MediaInputMap] = None
|
263
247
|
) -> BoundPrompt:
|
264
248
|
# check history for a system message
|
265
249
|
history_clean = []
|
266
250
|
if history:
|
267
|
-
template_messages_contain_system = any(
|
251
|
+
template_messages_contain_system = any(
|
252
|
+
message.role == 'system' for message in self.messages if isinstance(message, TemplateChatMessage))
|
268
253
|
history_dict = [convert_provider_message_to_dict(msg) for msg in history]
|
269
254
|
for msg in history_dict:
|
270
255
|
history_has_system = msg.get('role', None) == 'system'
|
@@ -274,22 +259,37 @@ class TemplatePrompt:
|
|
274
259
|
else:
|
275
260
|
history_clean.append(msg)
|
276
261
|
|
277
|
-
has_history_placeholder =
|
262
|
+
has_history_placeholder = any(isinstance(message, HistoryTemplateMessage) for message in self.messages)
|
278
263
|
if history and not has_history_placeholder:
|
279
264
|
raise FreeplayClientError(
|
280
265
|
"History provided for prompt that does not expect history")
|
281
266
|
if has_history_placeholder and not history:
|
282
267
|
log_freeplay_client_warning("History missing for prompt that expects history")
|
283
268
|
|
284
|
-
bound_messages = []
|
269
|
+
bound_messages: List[Dict[str, Any]] = []
|
270
|
+
if not media_inputs:
|
271
|
+
media_inputs = {}
|
285
272
|
for msg in self.messages:
|
286
|
-
if msg
|
273
|
+
if isinstance(msg, HistoryTemplateMessage):
|
287
274
|
bound_messages.extend(history_clean)
|
288
275
|
else:
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
276
|
+
media_content = extract_media_content(media_inputs, msg.media_slots)
|
277
|
+
content = bind_template_variables(msg.content, variables)
|
278
|
+
|
279
|
+
if media_content:
|
280
|
+
bound_messages.append({
|
281
|
+
'role': msg.role,
|
282
|
+
'content': [
|
283
|
+
TextContent(text=content),
|
284
|
+
*media_content
|
285
|
+
],
|
286
|
+
'has_media': True,
|
287
|
+
})
|
288
|
+
else:
|
289
|
+
bound_messages.append({
|
290
|
+
'role': msg.role,
|
291
|
+
'content': content},
|
292
|
+
)
|
293
293
|
|
294
294
|
return BoundPrompt(self.prompt_info, bound_messages, self.tool_schema)
|
295
295
|
|
@@ -385,7 +385,7 @@ class FilesystemTemplateResolver(TemplateResolver):
|
|
385
385
|
prompt_template_id=json_dom.get('prompt_template_id'), # type: ignore
|
386
386
|
prompt_template_version_id=json_dom.get('prompt_template_version_id'), # type: ignore
|
387
387
|
prompt_template_name=json_dom.get('prompt_template_name'), # type: ignore
|
388
|
-
content=FilesystemTemplateResolver.
|
388
|
+
content=FilesystemTemplateResolver.__normalize_messages(json_dom['content']),
|
389
389
|
metadata=PromptTemplateMetadata(
|
390
390
|
provider=FilesystemTemplateResolver.__flavor_to_provider(flavor_name),
|
391
391
|
flavor=flavor_name,
|
@@ -412,7 +412,7 @@ class FilesystemTemplateResolver(TemplateResolver):
|
|
412
412
|
prompt_template_id=json_dom.get('prompt_template_id'), # type: ignore
|
413
413
|
prompt_template_version_id=json_dom.get('prompt_template_version_id'), # type: ignore
|
414
414
|
prompt_template_name=json_dom.get('name'), # type: ignore
|
415
|
-
content=FilesystemTemplateResolver.
|
415
|
+
content=FilesystemTemplateResolver.__normalize_messages(json.loads(str(json_dom['content']))),
|
416
416
|
metadata=PromptTemplateMetadata(
|
417
417
|
provider=FilesystemTemplateResolver.__flavor_to_provider(flavor_name),
|
418
418
|
flavor=flavor_name,
|
@@ -424,14 +424,16 @@ class FilesystemTemplateResolver(TemplateResolver):
|
|
424
424
|
)
|
425
425
|
|
426
426
|
@staticmethod
|
427
|
-
def
|
428
|
-
normalized = []
|
427
|
+
def __normalize_messages(messages: List[Dict[str, Any]]) -> List[TemplateMessage]:
|
428
|
+
normalized: List[TemplateMessage] = []
|
429
429
|
for message in messages:
|
430
430
|
if 'kind' in message:
|
431
|
-
normalized.append(
|
431
|
+
normalized.append(HistoryTemplateMessage(kind="history"))
|
432
432
|
else:
|
433
433
|
role = FilesystemTemplateResolver.__role_translations.get(message['role']) or message['role']
|
434
|
-
|
434
|
+
media_slots: List[MediaSlot] = cast(List[MediaSlot], message.get('media_slots', []))
|
435
|
+
normalized.append(
|
436
|
+
TemplateChatMessage(role=cast(Role, role), content=message['content'], media_slots=media_slots))
|
435
437
|
return normalized
|
436
438
|
|
437
439
|
@staticmethod
|
@@ -577,22 +579,23 @@ class Prompts:
|
|
577
579
|
variables: InputVariables,
|
578
580
|
history: Optional[Sequence[ProviderMessage]] = None,
|
579
581
|
flavor_name: Optional[str] = None,
|
582
|
+
media_inputs: Optional[MediaInputMap] = None,
|
580
583
|
) -> FormattedPrompt:
|
581
584
|
bound_prompt = self.get(
|
582
585
|
project_id=project_id,
|
583
586
|
template_name=template_name,
|
584
587
|
environment=environment
|
585
|
-
).bind(variables=variables, history=history)
|
588
|
+
).bind(variables=variables, history=history, media_inputs=media_inputs)
|
586
589
|
|
587
590
|
return bound_prompt.format(flavor_name)
|
588
591
|
|
589
592
|
def get_formatted_by_version_id(
|
590
|
-
|
591
|
-
|
592
|
-
|
593
|
-
|
594
|
-
|
595
|
-
|
593
|
+
self,
|
594
|
+
project_id: str,
|
595
|
+
template_id: str,
|
596
|
+
version_id: str,
|
597
|
+
variables: InputVariables,
|
598
|
+
flavor_name: Optional[str] = None,
|
596
599
|
) -> FormattedPrompt:
|
597
600
|
bound_prompt = self.get_by_version_id(
|
598
601
|
project_id=project_id,
|
freeplay/resources/recordings.py
CHANGED
@@ -10,7 +10,7 @@ from freeplay import api_support
|
|
10
10
|
from freeplay.errors import FreeplayClientError, FreeplayError
|
11
11
|
from freeplay.llm_parameters import LLMParameters
|
12
12
|
from freeplay.model import InputVariables, OpenAIFunctionCall
|
13
|
-
from freeplay.resources.prompts import PromptInfo
|
13
|
+
from freeplay.resources.prompts import PromptInfo, MediaInputMap, MediaInput, MediaInputUrl
|
14
14
|
from freeplay.resources.sessions import SessionInfo, TraceInfo
|
15
15
|
from freeplay.support import CallSupport
|
16
16
|
|
@@ -79,6 +79,7 @@ class RecordPayload:
|
|
79
79
|
session_info: SessionInfo
|
80
80
|
prompt_info: PromptInfo
|
81
81
|
call_info: CallInfo
|
82
|
+
media_inputs: Optional[MediaInputMap] = None
|
82
83
|
tool_schema: Optional[List[Dict[str, Any]]] = None
|
83
84
|
response_info: Optional[ResponseInfo] = None
|
84
85
|
test_run_info: Optional[TestRunInfo] = None
|
@@ -100,6 +101,19 @@ class RecordResponse:
|
|
100
101
|
completion_id: str
|
101
102
|
|
102
103
|
|
104
|
+
def media_inputs_to_json(media_input: MediaInput) -> Dict[str, Any]:
|
105
|
+
if isinstance(media_input, MediaInputUrl):
|
106
|
+
return {
|
107
|
+
"type": media_input.type,
|
108
|
+
"url": media_input.url
|
109
|
+
}
|
110
|
+
else:
|
111
|
+
return {
|
112
|
+
"type": media_input.type,
|
113
|
+
"data": media_input.data,
|
114
|
+
"content_type": media_input.content_type
|
115
|
+
}
|
116
|
+
|
103
117
|
class Recordings:
|
104
118
|
def __init__(self, call_support: CallSupport):
|
105
119
|
self.call_support = call_support
|
@@ -166,6 +180,12 @@ class Recordings:
|
|
166
180
|
if record_payload.call_info.api_style is not None:
|
167
181
|
record_api_payload['call_info']['api_style'] = record_payload.call_info.api_style
|
168
182
|
|
183
|
+
if record_payload.media_inputs is not None:
|
184
|
+
record_api_payload['media_inputs'] = {
|
185
|
+
name: media_inputs_to_json(media_input)
|
186
|
+
for name, media_input in record_payload.media_inputs.items()
|
187
|
+
}
|
188
|
+
|
169
189
|
try:
|
170
190
|
recorded_response = api_support.post_raw(
|
171
191
|
api_key=self.call_support.freeplay_api_key,
|
freeplay/support.py
CHANGED
@@ -1,6 +1,6 @@
|
|
1
|
-
from dataclasses import dataclass
|
1
|
+
from dataclasses import dataclass, field
|
2
2
|
from json import JSONEncoder
|
3
|
-
from typing import Optional, Dict, Any, List, Union
|
3
|
+
from typing import Optional, Dict, Any, List, Union, Literal
|
4
4
|
|
5
5
|
from freeplay import api_support
|
6
6
|
from freeplay.api_support import try_decode
|
@@ -26,12 +26,35 @@ class ToolSchema:
|
|
26
26
|
parameters: Dict[str, Any]
|
27
27
|
|
28
28
|
|
29
|
+
Role = Literal['system', 'user', 'assistant']
|
30
|
+
|
31
|
+
|
32
|
+
@dataclass
|
33
|
+
class MediaSlot:
|
34
|
+
type: Literal["image", "audio", "video", "file"]
|
35
|
+
placeholder_name: str
|
36
|
+
|
37
|
+
|
38
|
+
@dataclass
|
39
|
+
class TemplateChatMessage:
|
40
|
+
role: Role
|
41
|
+
content: str
|
42
|
+
media_slots: List[MediaSlot] = field(default_factory=list)
|
43
|
+
|
44
|
+
|
45
|
+
@dataclass
|
46
|
+
class HistoryTemplateMessage:
|
47
|
+
kind: Literal["history"]
|
48
|
+
|
49
|
+
TemplateMessage = Union[HistoryTemplateMessage, TemplateChatMessage]
|
50
|
+
|
51
|
+
|
29
52
|
@dataclass
|
30
53
|
class PromptTemplate:
|
31
54
|
prompt_template_id: str
|
32
55
|
prompt_template_version_id: str
|
33
56
|
prompt_template_name: str
|
34
|
-
content: List[
|
57
|
+
content: List[TemplateMessage]
|
35
58
|
metadata: PromptTemplateMetadata
|
36
59
|
project_id: str
|
37
60
|
format_version: int
|
@@ -7,16 +7,17 @@ freeplay/llm_parameters.py,sha256=bQbfuC8EICF0XMZQa5pwI3FkQqxmCUVqHO3gYHy3Tg8,89
|
|
7
7
|
freeplay/model.py,sha256=o0de_RZ2WTJ4m5OJw1ZVfC2xG6zBq_XShBrRt1laEjc,1405
|
8
8
|
freeplay/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
9
9
|
freeplay/resources/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
10
|
+
freeplay/resources/adapters.py,sha256=zq-cDTBgFziFHyAJX18rEM4Fv2NHrLg3PuuxPqmg6Vw,6902
|
10
11
|
freeplay/resources/customer_feedback.py,sha256=bw8MfEOKbGgn4FOyvcADrcs9GhcpNXNTgxKjBjIzywE,899
|
11
|
-
freeplay/resources/prompts.py,sha256=
|
12
|
-
freeplay/resources/recordings.py,sha256=
|
12
|
+
freeplay/resources/prompts.py,sha256=i1Lck6UTF0WhW5miK5CAIJl19BFoI760rSyjFdju6H4,22799
|
13
|
+
freeplay/resources/recordings.py,sha256=z2ARII1jCnmNh1GU3hGnXZUz5IF_KhyayQum71k-h9c,9213
|
13
14
|
freeplay/resources/sessions.py,sha256=J5A3CjiV2MFqQyxN3TWTvJaa9jmMza58mRFRq2v9iAk,3746
|
14
15
|
freeplay/resources/test_cases.py,sha256=nXL_976RwSJDT6OWDM4GEzbcOzcGkJ9ulvb0XOzCRDM,2240
|
15
16
|
freeplay/resources/test_runs.py,sha256=Tp2N-odInT5XEEWrEsVhdgfnsclOE8n92_C8gTwO2MI,2623
|
16
|
-
freeplay/support.py,sha256=
|
17
|
+
freeplay/support.py,sha256=sj3JLD2syBxDyzWVWthWWwYWCUy4GzLNMSLSHegVvkk,12251
|
17
18
|
freeplay/utils.py,sha256=Xvt4mNLXLL7E6MI2hTuDLV5cl5Y83DgdjCZSyDGMjR0,3187
|
18
|
-
freeplay-0.3.
|
19
|
-
freeplay-0.3.
|
20
|
-
freeplay-0.3.
|
21
|
-
freeplay-0.3.
|
22
|
-
freeplay-0.3.
|
19
|
+
freeplay-0.3.19.dist-info/LICENSE,sha256=_jzIw45hB1XHGxiQ8leZ0GH_X7bR_a8qgxaqnHbCUOo,1064
|
20
|
+
freeplay-0.3.19.dist-info/METADATA,sha256=-RBwb7sFV4g4yNM1bREAO8mCF0cIFcWrnKmcPv9FJTI,1654
|
21
|
+
freeplay-0.3.19.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
|
22
|
+
freeplay-0.3.19.dist-info/entry_points.txt,sha256=32s3rf2UUCqiJT4jnClEXZhdXlvl30uwpcxz-Gsy4UU,54
|
23
|
+
freeplay-0.3.19.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|