freeplay 0.3.11__py3-none-any.whl → 0.3.14__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/freeplay.py CHANGED
@@ -5,6 +5,7 @@ from freeplay.resources.customer_feedback import CustomerFeedback
5
5
  from freeplay.resources.prompts import Prompts, APITemplateResolver, TemplateResolver
6
6
  from freeplay.resources.recordings import Recordings
7
7
  from freeplay.resources.sessions import Sessions
8
+ from freeplay.resources.test_cases import TestCases
8
9
  from freeplay.resources.test_runs import TestRuns
9
10
  from freeplay.support import CallSupport
10
11
 
@@ -38,3 +39,4 @@ class Freeplay:
38
39
  self.recordings = Recordings(self.call_support)
39
40
  self.sessions = Sessions(self.call_support)
40
41
  self.test_runs = TestRuns(self.call_support)
42
+ self.test_cases = TestCases(self.call_support)
freeplay/model.py CHANGED
@@ -1,5 +1,5 @@
1
1
  from dataclasses import dataclass
2
- from typing import List, Union, Any, Dict, Mapping, TypedDict
2
+ from typing import List, Union, Any, Dict, Mapping, TypedDict, Literal
3
3
 
4
4
  InputValue = Union[str, int, bool, float, Dict[str, Any], List[Any]]
5
5
  InputVariables = Mapping[str, InputValue]
@@ -16,3 +16,49 @@ class TestRun:
16
16
  class OpenAIFunctionCall(TypedDict):
17
17
  name: str
18
18
  arguments: str
19
+
20
+
21
+ @dataclass
22
+ class TextBlock:
23
+ text: str
24
+ type: Literal["text"] = "text"
25
+
26
+
27
+ @dataclass
28
+ class ToolResultBlock:
29
+ # AKA tool_use_id -- the ID of the tool call that this message is responding to.
30
+ tool_call_id: str
31
+ content: Union[str, List[TextBlock]]
32
+ type: Literal["tool_result"] = "tool_result"
33
+
34
+
35
+ @dataclass
36
+ class ToolCallBlock:
37
+ id: str
38
+ name: str
39
+ arguments: Any
40
+ type: Literal["tool_call"] = "tool_call"
41
+
42
+
43
+ ContentBlock = Union[TextBlock, ToolResultBlock, ToolCallBlock]
44
+
45
+
46
+ @dataclass
47
+ class UserMessage:
48
+ content: Union[str, List[ContentBlock]]
49
+ role: Literal["user"] = "user"
50
+
51
+
52
+ @dataclass
53
+ class SystemMessage:
54
+ content: str
55
+ role: Literal["system"] = "system"
56
+
57
+
58
+ @dataclass
59
+ class AssistantMessage:
60
+ content: Union[str, List[ContentBlock]]
61
+ role: Literal["assistant"] = "assistant"
62
+
63
+ # Largely used for history in dataset test cases presently
64
+ NormalizedMessage = Union[UserMessage, SystemMessage, AssistantMessage]
@@ -35,14 +35,14 @@ class UnsupportedToolSchemaError(FreeplayConfigurationError):
35
35
 
36
36
  # A content block a la OpenAI or Anthropic. Intentionally over-permissive to allow schema evolution by the providers.
37
37
  @runtime_checkable
38
- class ContentBlock(Protocol):
38
+ class ProviderMessageContentBlock(Protocol):
39
39
  def model_dump(self) -> Dict[str, Any]:
40
40
  pass
41
41
 
42
42
 
43
43
  # A content/role pair with a type-safe content for common provider recording. If not using a common provider,
44
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]
45
+ GenericProviderMessage = Union[Dict[str, Any], ProviderMessageContentBlock]
46
46
 
47
47
 
48
48
  # SDK-Exposed Classes
@@ -2,6 +2,7 @@ import json
2
2
  import logging
3
3
  from dataclasses import dataclass
4
4
  from typing import Any, Dict, List, Optional, Union
5
+ from uuid import UUID
5
6
 
6
7
  from requests import HTTPError
7
8
 
@@ -64,6 +65,15 @@ class RecordPayload:
64
65
  test_run_info: Optional[TestRunInfo] = None
65
66
  eval_results: Optional[Dict[str, Union[bool, float]]] = None
66
67
  trace_info: Optional[TraceInfo] = None
68
+ completion_id: Optional[UUID] = None
69
+
70
+
71
+ @dataclass
72
+ class RecordUpdatePayload:
73
+ project_id: str
74
+ completion_id: str
75
+ new_messages: Optional[List[Dict[str, Any]]] = None
76
+ eval_results: Optional[Dict[str, Union[bool, float]]] = None
67
77
 
68
78
 
69
79
  @dataclass
@@ -75,12 +85,12 @@ class Recordings:
75
85
  def __init__(self, call_support: CallSupport):
76
86
  self.call_support = call_support
77
87
 
78
- def create(self, record_payload: RecordPayload) -> RecordResponse:
88
+ def create(self, record_payload: RecordPayload) -> RecordResponse: # type: ignore
79
89
  if len(record_payload.all_messages) < 1:
80
90
  raise FreeplayClientError("Messages list must have at least one message. "
81
91
  "The last message should be the current response.")
82
92
 
83
- record_api_payload = {
93
+ record_api_payload: Dict[str, Any] = {
84
94
  "messages": record_payload.all_messages,
85
95
  "inputs": record_payload.inputs,
86
96
  "tool_schema": record_payload.tool_schema,
@@ -99,6 +109,9 @@ class Recordings:
99
109
  }
100
110
  }
101
111
 
112
+ if record_payload.completion_id is not None:
113
+ record_api_payload['completion_id'] = str(record_payload.completion_id)
114
+
102
115
  if record_payload.session_info.custom_metadata is not None:
103
116
  record_api_payload['custom_metadata'] = record_payload.session_info.custom_metadata
104
117
 
@@ -138,18 +151,7 @@ class Recordings:
138
151
  message = f'There was an error recording to Freeplay. Call will not be logged. ' \
139
152
  f'Status: {e.response.status_code}. '
140
153
 
141
- if e.response.content:
142
- try:
143
- content = e.response.content
144
- json_body = json.loads(content)
145
- if 'message' in json_body:
146
- message += json_body['message']
147
- except:
148
- pass
149
- else:
150
- message += f'{e.__class__}'
151
-
152
- raise FreeplayError(message) from e
154
+ self.__handle_and_raise_api_error(e, message)
153
155
 
154
156
  except Exception as e:
155
157
  status_code = -1
@@ -160,3 +162,37 @@ class Recordings:
160
162
  f'Status: {status_code}. {e.__class__}'
161
163
 
162
164
  raise FreeplayError(message) from e
165
+
166
+
167
+ def update(self, record_update_payload: RecordUpdatePayload) -> RecordResponse: # type: ignore
168
+ record_update_api_payload: Dict[str, Any] = {
169
+ "new_messages": record_update_payload.new_messages,
170
+ "eval_results": record_update_payload.eval_results,
171
+ }
172
+
173
+ try:
174
+ record_update_response = api_support.post_raw(
175
+ api_key=self.call_support.freeplay_api_key,
176
+ url=f'{self.call_support.api_base}/v2/projects/{record_update_payload.project_id}/completions/{record_update_payload.completion_id}',
177
+ payload=record_update_api_payload
178
+ )
179
+ record_update_response.raise_for_status()
180
+ json_dom = record_update_response.json()
181
+ return RecordResponse(completion_id=str(json_dom['completion_id']))
182
+ except HTTPError as e:
183
+ message = f'There was an error updating the completion. Status: {e.response.status_code}.'
184
+ self.__handle_and_raise_api_error(e, message)
185
+
186
+ @staticmethod
187
+ def __handle_and_raise_api_error(e: HTTPError, messages: str) -> None:
188
+ if e.response.content:
189
+ try:
190
+ content = e.response.content
191
+ json_body = json.loads(content)
192
+ if 'message' in json_body:
193
+ messages += json_body['message']
194
+ except:
195
+ pass
196
+ else:
197
+ messages += f'{e.__class__}'
198
+ raise FreeplayError(messages) from e
@@ -0,0 +1,55 @@
1
+ from dataclasses import dataclass
2
+ from typing import List, Optional, Dict, Any
3
+
4
+ from freeplay.model import InputVariables, NormalizedMessage
5
+ from freeplay.support import CallSupport, DatasetTestCaseRequest, DatasetTestCasesRetrievalResponse
6
+
7
+
8
+ @dataclass
9
+ class DatasetTestCase:
10
+ def __init__(
11
+ self,
12
+ inputs: InputVariables,
13
+ output: Optional[str],
14
+ history: Optional[List[NormalizedMessage]] = None,
15
+ metadata: Optional[Dict[str, str]] = None,
16
+ id: Optional[str] = None, # Only set on retrieval
17
+ ):
18
+ self.inputs = inputs
19
+ self.output = output
20
+ self.history = history
21
+ self.metadata = metadata
22
+ self.id = id
23
+
24
+
25
+
26
+ @dataclass
27
+ class Dataset:
28
+ def __init__(self, dataset_id: str, test_cases: List[DatasetTestCase]):
29
+ self.dataset_id = dataset_id
30
+ self.test_cases = test_cases
31
+
32
+
33
+ @dataclass
34
+ class DatasetResults:
35
+ def __init__(self, dataset_id: str, test_cases: List[DatasetTestCase]) -> None:
36
+ self.dataset_id = dataset_id
37
+ self.test_cases = test_cases
38
+
39
+ class TestCases:
40
+ def __init__(self, call_support: CallSupport) -> None:
41
+ self.call_support = call_support
42
+
43
+ def create(self, project_id: str, dataset_id: str, test_case: DatasetTestCase) -> Dataset:
44
+ return self.create_many(project_id, dataset_id, [test_case])
45
+
46
+ def create_many(self, project_id: str, dataset_id: str, test_cases: List[DatasetTestCase]) -> Dataset:
47
+ dataset_test_cases = [DatasetTestCaseRequest(test_case.history, test_case.inputs, test_case.metadata, test_case.output) for test_case in test_cases]
48
+ self.call_support.create_test_cases(project_id, dataset_id, dataset_test_cases)
49
+ return Dataset(dataset_id, test_cases)
50
+
51
+ def get(self, project_id: str, dataset_id: str) -> DatasetResults:
52
+ test_case_results: DatasetTestCasesRetrievalResponse = self.call_support.get_test_cases(project_id, dataset_id)
53
+ dataset_test_cases = test_case_results.test_cases
54
+
55
+ return DatasetResults(dataset_id, [DatasetTestCase(id=test_case.id, history=test_case.history, output=test_case.output, inputs=test_case.values, metadata=test_case.metadata) for test_case in dataset_test_cases])
freeplay/support.py CHANGED
@@ -1,3 +1,4 @@
1
+ import json
1
2
  from dataclasses import dataclass
2
3
  from json import JSONEncoder
3
4
  from typing import Optional, Dict, Any, List, Union
@@ -5,7 +6,7 @@ from typing import Optional, Dict, Any, List, Union
5
6
  from freeplay import api_support
6
7
  from freeplay.api_support import try_decode
7
8
  from freeplay.errors import freeplay_response_error, FreeplayServerError
8
- from freeplay.model import InputVariables, FeedbackValue
9
+ from freeplay.model import InputVariables, FeedbackValue, NormalizedMessage
9
10
 
10
11
 
11
12
  @dataclass
@@ -87,6 +88,28 @@ class TestRunRetrievalResponse:
87
88
  human_evaluation=summary_statistics['human_evaluation']
88
89
  )
89
90
 
91
+ class DatasetTestCaseRequest:
92
+ def __init__(self, history: Optional[List[NormalizedMessage]], inputs: InputVariables, metadata: Optional[Dict[str, str]], output: Optional[str]) -> None:
93
+ self.history: Optional[List[NormalizedMessage]] = history
94
+ self.inputs: InputVariables = inputs
95
+ self.metadata: Optional[Dict[str, str]] = metadata
96
+ self.output: Optional[str] = output
97
+
98
+
99
+ class DatasetTestCaseResponse:
100
+ def __init__(self, test_case: Dict[str, Any]):
101
+ self.values: InputVariables = test_case['values']
102
+ self.id: str = test_case['id']
103
+ self.output: Optional[str] = test_case.get('output')
104
+ self.history: Optional[List[NormalizedMessage]] = test_case.get('history')
105
+ self.metadata: Optional[Dict[str, str]] = test_case.get('metadata')
106
+
107
+ class DatasetTestCasesRetrievalResponse:
108
+ def __init__(self, test_cases: List[Dict[str, Any]]) -> None:
109
+ self.test_cases = [
110
+ DatasetTestCaseResponse(test_case)
111
+ for test_case in test_cases
112
+ ]
90
113
 
91
114
  class CallSupport:
92
115
  def __init__(
@@ -253,3 +276,24 @@ class CallSupport:
253
276
  if response.status_code != 201:
254
277
  raise freeplay_response_error('Error while deleting session.', response)
255
278
 
279
+ def create_test_cases(self, project_id: str, dataset_id: str, test_cases: List[DatasetTestCaseRequest]) -> None:
280
+ examples = [{"history": test_case.history, "output": test_case.output, "metadata": test_case.metadata, "inputs": test_case.inputs} for test_case in test_cases]
281
+ payload: Dict[str, Any] = {"examples": examples}
282
+ url = f'{self.api_base}/v2/projects/{project_id}/datasets/id/{dataset_id}/test-cases'
283
+
284
+ response = api_support.post_raw(self.freeplay_api_key, url, payload)
285
+ if response.status_code != 201:
286
+ raise freeplay_response_error('Error while creating test cases.', response)
287
+
288
+ def get_test_cases(self, project_id: str, dataset_id: str) -> DatasetTestCasesRetrievalResponse:
289
+ url = f'{self.api_base}/v2/projects/{project_id}/datasets/id/{dataset_id}/test-cases'
290
+ response = api_support.get_raw(self.freeplay_api_key, url)
291
+
292
+ if response.status_code != 200:
293
+ raise freeplay_response_error('Error while getting test cases.', response)
294
+
295
+ json_dom = response.json()
296
+
297
+ return DatasetTestCasesRetrievalResponse(
298
+ test_cases=[{"history": jsn["history"], "id": jsn["id"], "output": jsn["output"], "values": jsn["values"], "metadata": jsn["metadata"] if 'metadata' in jsn.keys() else None} for jsn in json_dom]
299
+ )
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: freeplay
3
- Version: 0.3.11
3
+ Version: 0.3.14
4
4
  Summary:
5
5
  License: MIT
6
6
  Author: FreePlay Engineering
@@ -1,21 +1,22 @@
1
1
  freeplay/__init__.py,sha256=oseuUqIVAi-2a_ns4ZbbFqkZez6KGGwI6fPkA0AKt6I,374
2
2
  freeplay/api_support.py,sha256=Kn2x3g6yloHQl3NwFRjbZE9BnIh7d1sgwGwC0mHuvw4,2483
3
3
  freeplay/errors.py,sha256=vwotUBldxDzREZOmLUeoiDoZjcvDwgH1AMwKBLhLooE,807
4
- freeplay/freeplay.py,sha256=cj0TGxIziS5tEL12czMJrrKrCKRoYR_Qxsipg3ClpsU,1496
4
+ freeplay/freeplay.py,sha256=J04-erDD6rI2SAje_Nsf3x5Qx-Z6p8gQvGrMRHFWoD4,1602
5
5
  freeplay/freeplay_cli.py,sha256=lmdsYwzdpWmUKHz_ieCzB-e6j1EnDHlVw3XIEyP_NEk,3460
6
6
  freeplay/llm_parameters.py,sha256=bQbfuC8EICF0XMZQa5pwI3FkQqxmCUVqHO3gYHy3Tg8,898
7
- freeplay/model.py,sha256=bh3TmINOxvKFxeVO8Uz7ybX28eD1tmO0XLewwLOtS7I,436
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
10
  freeplay/resources/customer_feedback.py,sha256=bw8MfEOKbGgn4FOyvcADrcs9GhcpNXNTgxKjBjIzywE,899
11
- freeplay/resources/prompts.py,sha256=-N8djt8VzqGqGNZbG23a9e_dFQfK1RTd6oDyt7Lfgn0,22155
12
- freeplay/resources/recordings.py,sha256=nECoZb159POpOm-pZnJuFrmvFFWSrea665I5YXEYMFY,6048
11
+ freeplay/resources/prompts.py,sha256=NZi4K6oGnbSgw_i0NFssSqRNonl6Ov8eGPFFbZ6O5aI,22185
12
+ freeplay/resources/recordings.py,sha256=k_ZQ-9YYeIcaSkdRFaELJF1dAkomdtNEcxbqDpeLXZU,7615
13
13
  freeplay/resources/sessions.py,sha256=Qz5v7VOf1DmQTd1wCOFXnrizlW5WFJT5V8-pq22Ifvg,2793
14
+ freeplay/resources/test_cases.py,sha256=nXL_976RwSJDT6OWDM4GEzbcOzcGkJ9ulvb0XOzCRDM,2240
14
15
  freeplay/resources/test_runs.py,sha256=Tp2N-odInT5XEEWrEsVhdgfnsclOE8n92_C8gTwO2MI,2623
15
- freeplay/support.py,sha256=RgC-EDMdxKu7iQEHQ16gxt9VGmjHLUbaKi_k0U5YR1I,8686
16
+ freeplay/support.py,sha256=we_FEtxcqc-8R0uOWy8p0nX0pHUbs-ulw7TC5NarlX4,11091
16
17
  freeplay/utils.py,sha256=Xvt4mNLXLL7E6MI2hTuDLV5cl5Y83DgdjCZSyDGMjR0,3187
17
- freeplay-0.3.11.dist-info/LICENSE,sha256=_jzIw45hB1XHGxiQ8leZ0GH_X7bR_a8qgxaqnHbCUOo,1064
18
- freeplay-0.3.11.dist-info/METADATA,sha256=cpOGXpBPm-uUj0bPLGDrbGzHxnGsSaVtvyQT_ak8Ihg,1654
19
- freeplay-0.3.11.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
20
- freeplay-0.3.11.dist-info/entry_points.txt,sha256=32s3rf2UUCqiJT4jnClEXZhdXlvl30uwpcxz-Gsy4UU,54
21
- freeplay-0.3.11.dist-info/RECORD,,
18
+ freeplay-0.3.14.dist-info/LICENSE,sha256=_jzIw45hB1XHGxiQ8leZ0GH_X7bR_a8qgxaqnHbCUOo,1064
19
+ freeplay-0.3.14.dist-info/METADATA,sha256=attHahy983-M4iFtaU-00Uzf_dsVVTSOAhgny3lpxk4,1654
20
+ freeplay-0.3.14.dist-info/WHEEL,sha256=XbeZDeTWKc1w7CSIyre5aMDU_-PohRwTQceYnisIYYY,88
21
+ freeplay-0.3.14.dist-info/entry_points.txt,sha256=32s3rf2UUCqiJT4jnClEXZhdXlvl30uwpcxz-Gsy4UU,54
22
+ freeplay-0.3.14.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 1.9.1
2
+ Generator: poetry-core 2.1.1
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any