freeplay 0.3.9__tar.gz → 0.3.11__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: freeplay
3
- Version: 0.3.9
3
+ Version: 0.3.11
4
4
  Summary:
5
5
  License: MIT
6
6
  Author: FreePlay Engineering
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "freeplay"
3
- version = "0.3.9"
3
+ version = "0.3.11"
4
4
  description = ""
5
5
  authors = ["FreePlay Engineering <engineering@freeplay.ai>"]
6
6
  license = "MIT"
@@ -16,10 +16,11 @@ pystache = "^0.6.5"
16
16
  [tool.poetry.group.dev.dependencies]
17
17
  mypy = "^1"
18
18
  types-requests = "^2.31"
19
- anthropic = {extras = ["bedrock"], version = "^0.39.0"}
19
+ anthropic = { extras = ["bedrock"], version = "^0.39.0" }
20
20
  openai = "^1"
21
21
  boto3 = "^1.34.97"
22
22
  google-cloud-aiplatform = "1.51.0"
23
+ httpx = "0.27.2"
23
24
 
24
25
  [tool.poetry.group.test.dependencies]
25
26
  responses = "^0.23.1"
@@ -1,19 +1,21 @@
1
1
  import copy
2
2
  import json
3
+ import logging
3
4
  from abc import ABC, abstractmethod
4
5
  from dataclasses import asdict, dataclass
5
- import logging
6
6
  from pathlib import Path
7
- from typing import Dict, Optional, List, Protocol, cast, Any, Union, runtime_checkable
7
+ from typing import Dict, Optional, List, Sequence, cast, Any, Union, runtime_checkable, Protocol
8
8
 
9
9
  from freeplay.errors import FreeplayConfigurationError, FreeplayClientError, log_freeplay_client_warning
10
10
  from freeplay.llm_parameters import LLMParameters
11
11
  from freeplay.model import InputVariables
12
12
  from freeplay.support import CallSupport, ToolSchema
13
13
  from freeplay.support import PromptTemplate, PromptTemplates, PromptTemplateMetadata
14
- from freeplay.utils import bind_template_variables
14
+ from freeplay.utils import bind_template_variables, convert_provider_message_to_dict
15
15
 
16
16
  logger = logging.getLogger(__name__)
17
+
18
+
17
19
  class MissingFlavorError(FreeplayConfigurationError):
18
20
  def __init__(self, flavor_name: str):
19
21
  super().__init__(
@@ -21,6 +23,7 @@ class MissingFlavorError(FreeplayConfigurationError):
21
23
  'a different model in the Freeplay UI.'
22
24
  )
23
25
 
26
+
24
27
  class UnsupportedToolSchemaError(FreeplayConfigurationError):
25
28
  def __init__(self) -> None:
26
29
  super().__init__(
@@ -28,6 +31,20 @@ class UnsupportedToolSchemaError(FreeplayConfigurationError):
28
31
  )
29
32
 
30
33
 
34
+ # Models ==
35
+
36
+ # A content block a la OpenAI or Anthropic. Intentionally over-permissive to allow schema evolution by the providers.
37
+ @runtime_checkable
38
+ class ContentBlock(Protocol):
39
+ def model_dump(self) -> Dict[str, Any]:
40
+ pass
41
+
42
+
43
+ # A content/role pair with a type-safe content for common provider recording. If not using a common provider,
44
+ # use {'content': str, 'role': str} to record. If using a common provider, this is usually the `.content` field.
45
+ GenericProviderMessage = Union[Dict[str, Any], ContentBlock]
46
+
47
+
31
48
  # SDK-Exposed Classes
32
49
  @dataclass
33
50
  class PromptInfo:
@@ -42,13 +59,6 @@ class PromptInfo:
42
59
  flavor_name: str
43
60
  project_id: str
44
61
 
45
- # Client SDKs (Anthropic and OpenAI) have pydantic types for messages that require strict structures.
46
- # We want to avoid taking a dependency on the client SDKs, so we instead just ensure they can be converted to a dict,
47
- # this will ultimately be validated at runtime by the server for any incompatibility.
48
- @runtime_checkable
49
- class GenericProviderMessage(Protocol):
50
- def to_dict(self) -> Dict[str, Any]:
51
- pass
52
62
 
53
63
  class FormattedPrompt:
54
64
  def __init__(
@@ -78,20 +88,18 @@ class FormattedPrompt:
78
88
  # We know this is a list of dict[str,str], but we use Any to avoid typing issues with client SDK libraries, which require strict TypedDict.
79
89
  def llm_prompt(self) -> Any:
80
90
  return self._llm_prompt
81
-
91
+
82
92
  @property
83
93
  def tool_schema(self) -> Any:
84
94
  return self._tool_schema
85
95
 
86
96
  def all_messages(
87
97
  self,
88
- new_message: Union[Dict[str, str], GenericProviderMessage]
98
+ new_message: GenericProviderMessage
89
99
  ) -> List[Dict[str, Any]]:
90
- # Check if it's a OpenAI or Anthropic message: if it's a provider message object (has to_dict method)
91
- if isinstance(new_message, GenericProviderMessage):
92
- return self.messages + [new_message.to_dict()]
93
- elif isinstance(new_message, dict):
94
- return self.messages + [new_message]
100
+ converted_message = convert_provider_message_to_dict(new_message)
101
+ return self.messages + [converted_message]
102
+
95
103
 
96
104
  class BoundPrompt:
97
105
  def __init__(
@@ -145,7 +153,7 @@ class BoundPrompt:
145
153
  return formatted
146
154
 
147
155
  raise MissingFlavorError(flavor_name)
148
-
156
+
149
157
  @staticmethod
150
158
  def __format_tool_schema(flavor_name: str, tool_schema: List[ToolSchema]) -> List[Dict[str, Any]]:
151
159
  if flavor_name == 'anthropic_chat':
@@ -161,9 +169,8 @@ class BoundPrompt:
161
169
  'type': 'function'
162
170
  } for tool_schema in tool_schema
163
171
  ]
164
-
165
- raise UnsupportedToolSchemaError()
166
172
 
173
+ raise UnsupportedToolSchemaError()
167
174
 
168
175
  def format(
169
176
  self,
@@ -173,7 +180,7 @@ class BoundPrompt:
173
180
  formatted_prompt = BoundPrompt.__format_messages_for_flavor(final_flavor, self.messages)
174
181
 
175
182
  formatted_tool_schema = BoundPrompt.__format_tool_schema(
176
- final_flavor,
183
+ final_flavor,
177
184
  self.tool_schema
178
185
  ) if self.tool_schema else None
179
186
 
@@ -204,12 +211,15 @@ class TemplatePrompt:
204
211
  self.tool_schema = tool_schema
205
212
  self.messages = messages
206
213
 
207
- def bind(self, variables: InputVariables, history: Optional[List[Dict[str, str]]] = None) -> BoundPrompt:
214
+ def bind(self, variables: InputVariables, history: Optional[Sequence[GenericProviderMessage]] = None) -> BoundPrompt:
208
215
  # check history for a system message
209
216
  history_clean = []
210
217
  if history:
211
- for msg in history:
212
- if (msg.get('role') == 'system') and ('system' in [message.get('role') for message in self.messages]):
218
+ template_messages_contain_system = any(message.get('role') == 'system' for message in self.messages)
219
+ history_dict = [convert_provider_message_to_dict(msg) for msg in history]
220
+ for msg in history_dict:
221
+ history_has_system = msg.get('role', None) == 'system'
222
+ if history_has_system and template_messages_contain_system:
213
223
  log_freeplay_client_warning("System message found in history, and prompt template."
214
224
  "Removing system message from the history")
215
225
  else:
@@ -334,7 +344,12 @@ class FilesystemTemplateResolver(TemplateResolver):
334
344
  params=metadata.get('params'),
335
345
  provider_info=metadata.get('provider_info')
336
346
  ),
337
- project_id=str(json_dom.get('project_id'))
347
+ project_id=str(json_dom.get('project_id')),
348
+ tool_schema=[ToolSchema(
349
+ name=schema.get('name'),
350
+ description=schema.get('description'),
351
+ parameters=schema.get('parameters')
352
+ ) for schema in json_dom.get('tool_schema', [])] if json_dom.get('tool_schema') else None
338
353
  )
339
354
  else:
340
355
  metadata = json_dom['metadata']
@@ -511,7 +526,7 @@ class Prompts:
511
526
  template_name: str,
512
527
  environment: str,
513
528
  variables: InputVariables,
514
- history: Optional[List[Dict[str, str]]] = None,
529
+ history: Optional[Sequence[GenericProviderMessage]] = None,
515
530
  flavor_name: Optional[str] = None
516
531
  ) -> FormattedPrompt:
517
532
  bound_prompt = self.get(
@@ -9,7 +9,7 @@ from freeplay import api_support
9
9
  from freeplay.errors import FreeplayClientError, FreeplayError
10
10
  from freeplay.llm_parameters import LLMParameters
11
11
  from freeplay.model import InputVariables, OpenAIFunctionCall
12
- from freeplay.resources.prompts import FormattedPrompt, PromptInfo
12
+ from freeplay.resources.prompts import PromptInfo
13
13
  from freeplay.resources.sessions import SessionInfo, TraceInfo
14
14
  from freeplay.support import CallSupport
15
15
 
@@ -53,7 +53,7 @@ class TestRunInfo:
53
53
 
54
54
  @dataclass
55
55
  class RecordPayload:
56
- all_messages: List[Dict[str, str]]
56
+ all_messages: List[Dict[str, Any]]
57
57
  inputs: InputVariables
58
58
 
59
59
  session_info: SessionInfo
@@ -37,6 +37,7 @@ class TestRun:
37
37
  def get_test_run_info(self, test_case_id: str) -> TestRunInfo:
38
38
  return TestRunInfo(self.test_run_id, test_case_id)
39
39
 
40
+
40
41
  @dataclass
41
42
  class TestRunResults:
42
43
  def __init__(
@@ -62,9 +63,11 @@ class TestRuns:
62
63
  testlist: str,
63
64
  include_outputs: bool = False,
64
65
  name: Optional[str] = None,
65
- description: Optional[str] = None
66
+ description: Optional[str] = None,
67
+ flavor_name: Optional[str] = None
66
68
  ) -> TestRun:
67
- test_run = self.call_support.create_test_run(project_id, testlist, include_outputs, name, description)
69
+ test_run = self.call_support.create_test_run(
70
+ project_id, testlist, include_outputs, name, description, flavor_name)
68
71
  test_cases = [
69
72
  TestCase(test_case_id=test_case.id,
70
73
  variables=test_case.variables,
@@ -55,7 +55,7 @@ class TestCaseTestRunResponse:
55
55
  self.variables: InputVariables = test_case['variables']
56
56
  self.id: str = test_case['test_case_id']
57
57
  self.output: Optional[str] = test_case.get('output')
58
- self.history: Optional[List[Dict[str, str]]] = test_case.get('history')
58
+ self.history: Optional[List[Dict[str, Any]]] = test_case.get('history')
59
59
 
60
60
 
61
61
  class TestRunResponse:
@@ -190,7 +190,8 @@ class CallSupport:
190
190
  testlist: str,
191
191
  include_outputs: bool = False,
192
192
  name: Optional[str] = None,
193
- description: Optional[str] = None
193
+ description: Optional[str] = None,
194
+ flavor_name: Optional[str] = None
194
195
  ) -> TestRunResponse:
195
196
  response = api_support.post_raw(
196
197
  api_key=self.freeplay_api_key,
@@ -199,7 +200,8 @@ class CallSupport:
199
200
  'dataset_name': testlist,
200
201
  'include_outputs': include_outputs,
201
202
  'test_run_name': name,
202
- 'test_run_description': description
203
+ 'test_run_description': description,
204
+ 'flavor_name': flavor_name
203
205
  },
204
206
  )
205
207
 
@@ -1,7 +1,7 @@
1
- import json
2
- from typing import Dict, Union, Optional, Any
3
1
  import importlib.metadata
2
+ import json
4
3
  import platform
4
+ from typing import Dict, Union, Optional, Any
5
5
 
6
6
  import pystache # type: ignore
7
7
 
@@ -70,3 +70,19 @@ def get_user_agent() -> str:
70
70
  # Output format
71
71
  # Freeplay/0.2.30 (Python/3.11.4; Darwin/23.2.0)
72
72
  return f"{sdk_name}/{sdk_version} ({language}/{language_version}; {os_name}/{os_version})"
73
+
74
+
75
+ # Recursively convert Pydantic models, lists, and dicts to dict compatible format -- used to allow us to accept
76
+ # provider message shapes (usually generated types) or the default {'content': ..., 'role': ...} shape.
77
+ def convert_provider_message_to_dict(obj: Any) -> Any:
78
+ if hasattr(obj, 'model_dump'):
79
+ # Pydantic v2
80
+ return obj.model_dump(mode='json')
81
+ elif hasattr(obj, 'dict'):
82
+ # Pydantic v1
83
+ return obj.dict(encode_json=True)
84
+ elif isinstance(obj, dict):
85
+ return {k: convert_provider_message_to_dict(v) for k, v in obj.items()}
86
+ elif isinstance(obj, list):
87
+ return [convert_provider_message_to_dict(item) for item in obj]
88
+ return obj
File without changes
File without changes