freeplay 0.3.18__tar.gz → 0.3.20__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.3.18 → freeplay-0.3.20}/PKG-INFO +1 -1
- {freeplay-0.3.18 → freeplay-0.3.20}/pyproject.toml +1 -1
- freeplay-0.3.20/src/freeplay/resources/adapters.py +236 -0
- {freeplay-0.3.18 → freeplay-0.3.20}/src/freeplay/resources/prompts.py +97 -96
- {freeplay-0.3.18 → freeplay-0.3.20}/src/freeplay/resources/recordings.py +21 -1
- {freeplay-0.3.18 → freeplay-0.3.20}/src/freeplay/support.py +29 -3
- {freeplay-0.3.18 → freeplay-0.3.20}/LICENSE +0 -0
- {freeplay-0.3.18 → freeplay-0.3.20}/README.md +0 -0
- {freeplay-0.3.18 → freeplay-0.3.20}/src/freeplay/__init__.py +0 -0
- {freeplay-0.3.18 → freeplay-0.3.20}/src/freeplay/api_support.py +0 -0
- {freeplay-0.3.18 → freeplay-0.3.20}/src/freeplay/errors.py +0 -0
- {freeplay-0.3.18 → freeplay-0.3.20}/src/freeplay/freeplay.py +0 -0
- {freeplay-0.3.18 → freeplay-0.3.20}/src/freeplay/freeplay_cli.py +0 -0
- {freeplay-0.3.18 → freeplay-0.3.20}/src/freeplay/llm_parameters.py +0 -0
- {freeplay-0.3.18 → freeplay-0.3.20}/src/freeplay/model.py +0 -0
- {freeplay-0.3.18 → freeplay-0.3.20}/src/freeplay/py.typed +0 -0
- {freeplay-0.3.18 → freeplay-0.3.20}/src/freeplay/resources/__init__.py +0 -0
- {freeplay-0.3.18 → freeplay-0.3.20}/src/freeplay/resources/customer_feedback.py +0 -0
- {freeplay-0.3.18 → freeplay-0.3.20}/src/freeplay/resources/sessions.py +0 -0
- {freeplay-0.3.18 → freeplay-0.3.20}/src/freeplay/resources/test_cases.py +0 -0
- {freeplay-0.3.18 → freeplay-0.3.20}/src/freeplay/resources/test_runs.py +0 -0
- {freeplay-0.3.18 → freeplay-0.3.20}/src/freeplay/utils.py +0 -0
@@ -0,0 +1,236 @@
|
|
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
|
+
from freeplay.support import MediaType
|
7
|
+
|
8
|
+
|
9
|
+
@dataclass
|
10
|
+
class TextContent:
|
11
|
+
text: str
|
12
|
+
|
13
|
+
|
14
|
+
@dataclass
|
15
|
+
class MediaContentUrl:
|
16
|
+
slot_name: str
|
17
|
+
type: MediaType
|
18
|
+
url: str
|
19
|
+
|
20
|
+
|
21
|
+
@dataclass
|
22
|
+
class MediaContentBase64:
|
23
|
+
slot_name: str
|
24
|
+
type: MediaType
|
25
|
+
content_type: str
|
26
|
+
data: str
|
27
|
+
|
28
|
+
|
29
|
+
class MissingFlavorError(FreeplayConfigurationError):
|
30
|
+
def __init__(self, flavor_name: str):
|
31
|
+
super().__init__(
|
32
|
+
f'Configured flavor ({flavor_name}) not found in SDK. Please update your SDK version or configure '
|
33
|
+
'a different model in the Freeplay UI.'
|
34
|
+
)
|
35
|
+
|
36
|
+
|
37
|
+
class LLMAdapter(Protocol):
|
38
|
+
def to_llm_syntax(self, messages: List[Dict[str, Any]]) -> Union[str, List[Dict[str, Any]]]:
|
39
|
+
pass
|
40
|
+
|
41
|
+
|
42
|
+
class PassthroughAdapter(LLMAdapter):
|
43
|
+
def to_llm_syntax(self, messages: List[Dict[str, Any]]) -> Union[str, List[Dict[str, Any]]]:
|
44
|
+
# We need a deepcopy here to avoid referential equality with the llm_prompt
|
45
|
+
return copy.deepcopy(messages)
|
46
|
+
|
47
|
+
|
48
|
+
class AnthropicAdapter(LLMAdapter):
|
49
|
+
def to_llm_syntax(self, messages: List[Dict[str, Any]]) -> Union[str, List[Dict[str, Any]]]:
|
50
|
+
anthropic_messages = []
|
51
|
+
|
52
|
+
for message in messages:
|
53
|
+
if message['role'] == 'system':
|
54
|
+
continue
|
55
|
+
if "has_media" in message and message["has_media"]:
|
56
|
+
anthropic_messages.append({
|
57
|
+
'role': message['role'],
|
58
|
+
'content': [self.__map_content(content) for content in message['content']]
|
59
|
+
})
|
60
|
+
else:
|
61
|
+
anthropic_messages.append(copy.deepcopy(message))
|
62
|
+
|
63
|
+
return anthropic_messages
|
64
|
+
|
65
|
+
@staticmethod
|
66
|
+
def __map_content(content: Union[TextContent, MediaContentBase64, MediaContentUrl]) -> Dict[str, Any]:
|
67
|
+
if isinstance(content, TextContent):
|
68
|
+
return {
|
69
|
+
"type": "text",
|
70
|
+
"text": content.text
|
71
|
+
}
|
72
|
+
if content.type == "audio" or content.type == "video":
|
73
|
+
raise ValueError("Anthropic does not support audio or video content")
|
74
|
+
|
75
|
+
media_type = "image" if content.type == "image" else "document"
|
76
|
+
if isinstance(content, MediaContentBase64):
|
77
|
+
return {
|
78
|
+
"type": media_type,
|
79
|
+
"source": {
|
80
|
+
"type": "base64",
|
81
|
+
"media_type": content.content_type,
|
82
|
+
"data": content.data,
|
83
|
+
}
|
84
|
+
}
|
85
|
+
elif isinstance(content, MediaContentUrl):
|
86
|
+
return {
|
87
|
+
"type": media_type,
|
88
|
+
"source": {
|
89
|
+
"type": "url",
|
90
|
+
"url": content.url,
|
91
|
+
}
|
92
|
+
}
|
93
|
+
else:
|
94
|
+
raise ValueError(f"Unexpected content type {type(content)}")
|
95
|
+
|
96
|
+
|
97
|
+
class OpenAIAdapter(LLMAdapter):
|
98
|
+
def to_llm_syntax(self, messages: List[Dict[str, Any]]) -> Union[str, List[Dict[str, Any]]]:
|
99
|
+
openai_messages = []
|
100
|
+
|
101
|
+
for message in messages:
|
102
|
+
if "has_media" in message and message["has_media"]:
|
103
|
+
openai_messages.append({
|
104
|
+
'role': message['role'],
|
105
|
+
'content': [self.__map_content(content) for content in message['content']]
|
106
|
+
})
|
107
|
+
else:
|
108
|
+
openai_messages.append(copy.deepcopy(message))
|
109
|
+
|
110
|
+
return openai_messages
|
111
|
+
|
112
|
+
@staticmethod
|
113
|
+
def __map_content(content: Union[TextContent, MediaContentBase64, MediaContentUrl]) -> Dict[str, Any]:
|
114
|
+
if isinstance(content, TextContent):
|
115
|
+
return {
|
116
|
+
"type": "text",
|
117
|
+
"text": content.text
|
118
|
+
}
|
119
|
+
elif isinstance(content, MediaContentBase64):
|
120
|
+
return OpenAIAdapter.__format_base64_content(content)
|
121
|
+
elif isinstance(content, MediaContentUrl):
|
122
|
+
if content.type != "image":
|
123
|
+
raise ValueError("Message contains a non-image URL, but OpenAI only supports image URLs.")
|
124
|
+
|
125
|
+
return {
|
126
|
+
"type": "image_url",
|
127
|
+
"image_url": {
|
128
|
+
"url": content.url
|
129
|
+
}
|
130
|
+
}
|
131
|
+
else:
|
132
|
+
raise ValueError(f"Unexpected content type {type(content)}")
|
133
|
+
|
134
|
+
@staticmethod
|
135
|
+
def __format_base64_content(content: MediaContentBase64) -> Dict[str, Any]:
|
136
|
+
if content.type == "audio":
|
137
|
+
return {
|
138
|
+
"type": "input_audio",
|
139
|
+
"input_audio": {
|
140
|
+
"data": content.data,
|
141
|
+
"format": content.content_type.split("/")[-1].replace("mpeg", "mp3")
|
142
|
+
}
|
143
|
+
}
|
144
|
+
elif content.type == "file":
|
145
|
+
return {
|
146
|
+
"type": "file",
|
147
|
+
"file": {
|
148
|
+
"filename": f"{content.slot_name}.{content.content_type.split('/')[-1]}",
|
149
|
+
"file_data": f"data:{content.content_type};base64,{content.data}"
|
150
|
+
}
|
151
|
+
}
|
152
|
+
else:
|
153
|
+
return {
|
154
|
+
"type": "image_url",
|
155
|
+
"image_url": {
|
156
|
+
"url": f"data:{content.content_type};base64,{content.data}"
|
157
|
+
}
|
158
|
+
}
|
159
|
+
|
160
|
+
|
161
|
+
class Llama3Adapter(LLMAdapter):
|
162
|
+
def to_llm_syntax(self, messages: List[Dict[str, Any]]) -> Union[str, List[Dict[str, Any]]]:
|
163
|
+
if len(messages) < 1:
|
164
|
+
raise ValueError("Must have at least one message to format")
|
165
|
+
|
166
|
+
formatted = "<|begin_of_text|>"
|
167
|
+
for message in messages:
|
168
|
+
formatted += f"<|start_header_id|>{message['role']}<|end_header_id|>\n{message['content']}<|eot_id|>"
|
169
|
+
formatted += "<|start_header_id|>assistant<|end_header_id|>"
|
170
|
+
|
171
|
+
return formatted
|
172
|
+
|
173
|
+
|
174
|
+
class GeminiAdapter(LLMAdapter):
|
175
|
+
def to_llm_syntax(self, messages: List[Dict[str, Any]]) -> Union[str, List[Dict[str, Any]]]:
|
176
|
+
if len(messages) < 1:
|
177
|
+
raise ValueError("Must have at least one message to format")
|
178
|
+
|
179
|
+
gemini_messages = []
|
180
|
+
|
181
|
+
for message in messages:
|
182
|
+
if message['role'] == 'system':
|
183
|
+
continue
|
184
|
+
|
185
|
+
if "has_media" in message and message["has_media"]:
|
186
|
+
gemini_messages.append({
|
187
|
+
"role": self.__translate_role(message["role"]),
|
188
|
+
"parts": [self.__map_content(content) for content in message['content']]
|
189
|
+
})
|
190
|
+
else:
|
191
|
+
gemini_messages.append({
|
192
|
+
"role": self.__translate_role(message["role"]),
|
193
|
+
"parts": [{"text": message["content"]}]
|
194
|
+
})
|
195
|
+
|
196
|
+
return gemini_messages
|
197
|
+
|
198
|
+
@staticmethod
|
199
|
+
def __map_content(content: Union[TextContent, MediaContentBase64, MediaContentUrl]) -> Dict[str, Any]:
|
200
|
+
if isinstance(content, TextContent):
|
201
|
+
return {"text": content.text}
|
202
|
+
elif isinstance(content, MediaContentBase64):
|
203
|
+
return {
|
204
|
+
"inline_data": {
|
205
|
+
"data": content.data,
|
206
|
+
"mime_type": content.content_type,
|
207
|
+
}
|
208
|
+
}
|
209
|
+
elif isinstance(content, MediaContentUrl):
|
210
|
+
raise ValueError("Message contains an image URL, but image URLs are not supported by Gemini")
|
211
|
+
else:
|
212
|
+
raise ValueError(f"Unexpected content type {type(content)}")
|
213
|
+
|
214
|
+
@staticmethod
|
215
|
+
def __translate_role(role: str) -> str:
|
216
|
+
if role == "user":
|
217
|
+
return "user"
|
218
|
+
elif role == "assistant":
|
219
|
+
return "model"
|
220
|
+
else:
|
221
|
+
raise ValueError(f"Gemini formatting found unexpected role {role}")
|
222
|
+
|
223
|
+
|
224
|
+
def adaptor_for_flavor(flavor_name: str) -> LLMAdapter:
|
225
|
+
if flavor_name in ["baseten_mistral_chat", "mistral_chat", "perplexity_chat"]:
|
226
|
+
return PassthroughAdapter()
|
227
|
+
elif flavor_name in ["azure_openai_chat", "openai_chat"]:
|
228
|
+
return OpenAIAdapter()
|
229
|
+
elif flavor_name == "anthropic_chat":
|
230
|
+
return AnthropicAdapter()
|
231
|
+
elif flavor_name == "llama_3_chat":
|
232
|
+
return Llama3Adapter()
|
233
|
+
elif flavor_name == "gemini_chat":
|
234
|
+
return GeminiAdapter()
|
235
|
+
else:
|
236
|
+
raise MissingFlavorError(flavor_name)
|
@@ -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, MediaContentBase64, MediaContentUrl, \
|
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,45 @@ 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[MediaContentBase64, MediaContentUrl]]:
|
216
|
+
media_content: List[Union[MediaContentBase64, MediaContentUrl]] = []
|
217
|
+
for slot in media_slots:
|
218
|
+
file = media_inputs.get(slot.placeholder_name, None)
|
219
|
+
if file is None:
|
220
|
+
continue
|
221
|
+
if isinstance(file, MediaInputUrl):
|
222
|
+
media_content.append(MediaContentUrl(type=slot.type, url=file.url, slot_name=slot.placeholder_name))
|
223
|
+
else:
|
224
|
+
media_content.append(MediaContentBase64(type=slot.type, content_type=file.content_type, data=file.data, slot_name=slot.placeholder_name))
|
225
|
+
|
226
|
+
return media_content
|
227
|
+
|
228
|
+
|
248
229
|
class TemplatePrompt:
|
249
230
|
def __init__(
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
231
|
+
self,
|
232
|
+
prompt_info: PromptInfo,
|
233
|
+
messages: List[TemplateMessage],
|
234
|
+
tool_schema: Optional[List[ToolSchema]] = None
|
254
235
|
):
|
255
236
|
self.prompt_info = prompt_info
|
256
237
|
self.tool_schema = tool_schema
|
@@ -260,11 +241,13 @@ class TemplatePrompt:
|
|
260
241
|
self,
|
261
242
|
variables: InputVariables,
|
262
243
|
history: Optional[Sequence[ProviderMessage]] = None,
|
244
|
+
media_inputs: Optional[MediaInputMap] = None
|
263
245
|
) -> BoundPrompt:
|
264
246
|
# check history for a system message
|
265
247
|
history_clean = []
|
266
248
|
if history:
|
267
|
-
template_messages_contain_system = any(
|
249
|
+
template_messages_contain_system = any(
|
250
|
+
message.role == 'system' for message in self.messages if isinstance(message, TemplateChatMessage))
|
268
251
|
history_dict = [convert_provider_message_to_dict(msg) for msg in history]
|
269
252
|
for msg in history_dict:
|
270
253
|
history_has_system = msg.get('role', None) == 'system'
|
@@ -274,22 +257,37 @@ class TemplatePrompt:
|
|
274
257
|
else:
|
275
258
|
history_clean.append(msg)
|
276
259
|
|
277
|
-
has_history_placeholder =
|
260
|
+
has_history_placeholder = any(isinstance(message, HistoryTemplateMessage) for message in self.messages)
|
278
261
|
if history and not has_history_placeholder:
|
279
262
|
raise FreeplayClientError(
|
280
263
|
"History provided for prompt that does not expect history")
|
281
264
|
if has_history_placeholder and not history:
|
282
265
|
log_freeplay_client_warning("History missing for prompt that expects history")
|
283
266
|
|
284
|
-
bound_messages = []
|
267
|
+
bound_messages: List[Dict[str, Any]] = []
|
268
|
+
if not media_inputs:
|
269
|
+
media_inputs = {}
|
285
270
|
for msg in self.messages:
|
286
|
-
if msg
|
271
|
+
if isinstance(msg, HistoryTemplateMessage):
|
287
272
|
bound_messages.extend(history_clean)
|
288
273
|
else:
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
274
|
+
media_content = extract_media_content(media_inputs, msg.media_slots)
|
275
|
+
content = bind_template_variables(msg.content, variables)
|
276
|
+
|
277
|
+
if media_content:
|
278
|
+
bound_messages.append({
|
279
|
+
'role': msg.role,
|
280
|
+
'content': [
|
281
|
+
TextContent(text=content),
|
282
|
+
*media_content
|
283
|
+
],
|
284
|
+
'has_media': True,
|
285
|
+
})
|
286
|
+
else:
|
287
|
+
bound_messages.append({
|
288
|
+
'role': msg.role,
|
289
|
+
'content': content},
|
290
|
+
)
|
293
291
|
|
294
292
|
return BoundPrompt(self.prompt_info, bound_messages, self.tool_schema)
|
295
293
|
|
@@ -385,7 +383,7 @@ class FilesystemTemplateResolver(TemplateResolver):
|
|
385
383
|
prompt_template_id=json_dom.get('prompt_template_id'), # type: ignore
|
386
384
|
prompt_template_version_id=json_dom.get('prompt_template_version_id'), # type: ignore
|
387
385
|
prompt_template_name=json_dom.get('prompt_template_name'), # type: ignore
|
388
|
-
content=FilesystemTemplateResolver.
|
386
|
+
content=FilesystemTemplateResolver.__normalize_messages(json_dom['content']),
|
389
387
|
metadata=PromptTemplateMetadata(
|
390
388
|
provider=FilesystemTemplateResolver.__flavor_to_provider(flavor_name),
|
391
389
|
flavor=flavor_name,
|
@@ -412,7 +410,7 @@ class FilesystemTemplateResolver(TemplateResolver):
|
|
412
410
|
prompt_template_id=json_dom.get('prompt_template_id'), # type: ignore
|
413
411
|
prompt_template_version_id=json_dom.get('prompt_template_version_id'), # type: ignore
|
414
412
|
prompt_template_name=json_dom.get('name'), # type: ignore
|
415
|
-
content=FilesystemTemplateResolver.
|
413
|
+
content=FilesystemTemplateResolver.__normalize_messages(json.loads(str(json_dom['content']))),
|
416
414
|
metadata=PromptTemplateMetadata(
|
417
415
|
provider=FilesystemTemplateResolver.__flavor_to_provider(flavor_name),
|
418
416
|
flavor=flavor_name,
|
@@ -424,14 +422,16 @@ class FilesystemTemplateResolver(TemplateResolver):
|
|
424
422
|
)
|
425
423
|
|
426
424
|
@staticmethod
|
427
|
-
def
|
428
|
-
normalized = []
|
425
|
+
def __normalize_messages(messages: List[Dict[str, Any]]) -> List[TemplateMessage]:
|
426
|
+
normalized: List[TemplateMessage] = []
|
429
427
|
for message in messages:
|
430
428
|
if 'kind' in message:
|
431
|
-
normalized.append(
|
429
|
+
normalized.append(HistoryTemplateMessage(kind="history"))
|
432
430
|
else:
|
433
431
|
role = FilesystemTemplateResolver.__role_translations.get(message['role']) or message['role']
|
434
|
-
|
432
|
+
media_slots: List[MediaSlot] = cast(List[MediaSlot], message.get('media_slots', []))
|
433
|
+
normalized.append(
|
434
|
+
TemplateChatMessage(role=cast(Role, role), content=message['content'], media_slots=media_slots))
|
435
435
|
return normalized
|
436
436
|
|
437
437
|
@staticmethod
|
@@ -577,22 +577,23 @@ class Prompts:
|
|
577
577
|
variables: InputVariables,
|
578
578
|
history: Optional[Sequence[ProviderMessage]] = None,
|
579
579
|
flavor_name: Optional[str] = None,
|
580
|
+
media_inputs: Optional[MediaInputMap] = None,
|
580
581
|
) -> FormattedPrompt:
|
581
582
|
bound_prompt = self.get(
|
582
583
|
project_id=project_id,
|
583
584
|
template_name=template_name,
|
584
585
|
environment=environment
|
585
|
-
).bind(variables=variables, history=history)
|
586
|
+
).bind(variables=variables, history=history, media_inputs=media_inputs)
|
586
587
|
|
587
588
|
return bound_prompt.format(flavor_name)
|
588
589
|
|
589
590
|
def get_formatted_by_version_id(
|
590
|
-
|
591
|
-
|
592
|
-
|
593
|
-
|
594
|
-
|
595
|
-
|
591
|
+
self,
|
592
|
+
project_id: str,
|
593
|
+
template_id: str,
|
594
|
+
version_id: str,
|
595
|
+
variables: InputVariables,
|
596
|
+
flavor_name: Optional[str] = None,
|
596
597
|
) -> FormattedPrompt:
|
597
598
|
bound_prompt = self.get_by_version_id(
|
598
599
|
project_id=project_id,
|
@@ -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,
|
@@ -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,38 @@ class ToolSchema:
|
|
26
26
|
parameters: Dict[str, Any]
|
27
27
|
|
28
28
|
|
29
|
+
Role = Literal['system', 'user', 'assistant']
|
30
|
+
|
31
|
+
|
32
|
+
MediaType = Literal["image", "audio", "video", "file"]
|
33
|
+
|
34
|
+
|
35
|
+
@dataclass
|
36
|
+
class MediaSlot:
|
37
|
+
type: MediaType
|
38
|
+
placeholder_name: str
|
39
|
+
|
40
|
+
|
41
|
+
@dataclass
|
42
|
+
class TemplateChatMessage:
|
43
|
+
role: Role
|
44
|
+
content: str
|
45
|
+
media_slots: List[MediaSlot] = field(default_factory=list)
|
46
|
+
|
47
|
+
|
48
|
+
@dataclass
|
49
|
+
class HistoryTemplateMessage:
|
50
|
+
kind: Literal["history"]
|
51
|
+
|
52
|
+
TemplateMessage = Union[HistoryTemplateMessage, TemplateChatMessage]
|
53
|
+
|
54
|
+
|
29
55
|
@dataclass
|
30
56
|
class PromptTemplate:
|
31
57
|
prompt_template_id: str
|
32
58
|
prompt_template_version_id: str
|
33
59
|
prompt_template_name: str
|
34
|
-
content: List[
|
60
|
+
content: List[TemplateMessage]
|
35
61
|
metadata: PromptTemplateMetadata
|
36
62
|
project_id: str
|
37
63
|
format_version: int
|
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
|