hyperstudy 0.4.0__tar.gz → 0.5.0__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.
- {hyperstudy-0.4.0 → hyperstudy-0.5.0}/CHANGELOG.md +14 -0
- {hyperstudy-0.4.0 → hyperstudy-0.5.0}/PKG-INFO +17 -3
- {hyperstudy-0.4.0 → hyperstudy-0.5.0}/README.md +16 -2
- {hyperstudy-0.4.0 → hyperstudy-0.5.0}/pyproject.toml +1 -1
- {hyperstudy-0.4.0 → hyperstudy-0.5.0}/src/hyperstudy/__init__.py +1 -1
- {hyperstudy-0.4.0 → hyperstudy-0.5.0}/src/hyperstudy/_types.py +1 -0
- {hyperstudy-0.4.0 → hyperstudy-0.5.0}/src/hyperstudy/client.py +91 -0
- {hyperstudy-0.4.0 → hyperstudy-0.5.0}/src/hyperstudy/experiments.py +11 -0
- {hyperstudy-0.4.0 → hyperstudy-0.5.0}/src/hyperstudy/models.py +3 -3
- {hyperstudy-0.4.0 → hyperstudy-0.5.0}/src/hyperstudy/personas.py +18 -0
- {hyperstudy-0.4.0 → hyperstudy-0.5.0}/tests/test_client.py +123 -2
- {hyperstudy-0.4.0 → hyperstudy-0.5.0}/tests/test_experiments.py +32 -0
- {hyperstudy-0.4.0 → hyperstudy-0.5.0}/tests/test_personas.py +70 -0
- {hyperstudy-0.4.0 → hyperstudy-0.5.0}/.github/workflows/publish.yml +0 -0
- {hyperstudy-0.4.0 → hyperstudy-0.5.0}/.github/workflows/sync-release-notes.yml +0 -0
- {hyperstudy-0.4.0 → hyperstudy-0.5.0}/.github/workflows/test.yml +0 -0
- {hyperstudy-0.4.0 → hyperstudy-0.5.0}/.gitignore +0 -0
- {hyperstudy-0.4.0 → hyperstudy-0.5.0}/LICENSE +0 -0
- {hyperstudy-0.4.0 → hyperstudy-0.5.0}/docs/superpowers/specs/2026-04-10-recording-downloads-design.md +0 -0
- {hyperstudy-0.4.0 → hyperstudy-0.5.0}/src/hyperstudy/_dataframe.py +0 -0
- {hyperstudy-0.4.0 → hyperstudy-0.5.0}/src/hyperstudy/_display.py +0 -0
- {hyperstudy-0.4.0 → hyperstudy-0.5.0}/src/hyperstudy/_downloads.py +0 -0
- {hyperstudy-0.4.0 → hyperstudy-0.5.0}/src/hyperstudy/_http.py +0 -0
- {hyperstudy-0.4.0 → hyperstudy-0.5.0}/src/hyperstudy/_pagination.py +0 -0
- {hyperstudy-0.4.0 → hyperstudy-0.5.0}/src/hyperstudy/exceptions.py +0 -0
- {hyperstudy-0.4.0 → hyperstudy-0.5.0}/tests/__init__.py +0 -0
- {hyperstudy-0.4.0 → hyperstudy-0.5.0}/tests/conftest.py +0 -0
- {hyperstudy-0.4.0 → hyperstudy-0.5.0}/tests/fixtures/agent_decision_detail_response.json +0 -0
- {hyperstudy-0.4.0 → hyperstudy-0.5.0}/tests/fixtures/agent_decisions_response.json +0 -0
- {hyperstudy-0.4.0 → hyperstudy-0.5.0}/tests/fixtures/agent_decisions_truncated_response.json +0 -0
- {hyperstudy-0.4.0 → hyperstudy-0.5.0}/tests/fixtures/agent_runs_response.json +0 -0
- {hyperstudy-0.4.0 → hyperstudy-0.5.0}/tests/fixtures/deployment_sessions_response.json +0 -0
- {hyperstudy-0.4.0 → hyperstudy-0.5.0}/tests/fixtures/deployment_single_response.json +0 -0
- {hyperstudy-0.4.0 → hyperstudy-0.5.0}/tests/fixtures/deployments_list_response.json +0 -0
- {hyperstudy-0.4.0 → hyperstudy-0.5.0}/tests/fixtures/error_401.json +0 -0
- {hyperstudy-0.4.0 → hyperstudy-0.5.0}/tests/fixtures/error_403.json +0 -0
- {hyperstudy-0.4.0 → hyperstudy-0.5.0}/tests/fixtures/events_response.json +0 -0
- {hyperstudy-0.4.0 → hyperstudy-0.5.0}/tests/fixtures/experiment.schema.json +0 -0
- {hyperstudy-0.4.0 → hyperstudy-0.5.0}/tests/fixtures/experiment_single_response.json +0 -0
- {hyperstudy-0.4.0 → hyperstudy-0.5.0}/tests/fixtures/experiments_list_response.json +0 -0
- {hyperstudy-0.4.0 → hyperstudy-0.5.0}/tests/fixtures/paginated_page1.json +0 -0
- {hyperstudy-0.4.0 → hyperstudy-0.5.0}/tests/fixtures/paginated_page2.json +0 -0
- {hyperstudy-0.4.0 → hyperstudy-0.5.0}/tests/fixtures/pre_experiment_response.json +0 -0
- {hyperstudy-0.4.0 → hyperstudy-0.5.0}/tests/fixtures/recordings_response.json +0 -0
- {hyperstudy-0.4.0 → hyperstudy-0.5.0}/tests/fixtures/sparse_ratings_response.json +0 -0
- {hyperstudy-0.4.0 → hyperstudy-0.5.0}/tests/fixtures/warnings_response.json +0 -0
- {hyperstudy-0.4.0 → hyperstudy-0.5.0}/tests/test_agent_data.py +0 -0
- {hyperstudy-0.4.0 → hyperstudy-0.5.0}/tests/test_dataframe.py +0 -0
- {hyperstudy-0.4.0 → hyperstudy-0.5.0}/tests/test_deployments_write.py +0 -0
- {hyperstudy-0.4.0 → hyperstudy-0.5.0}/tests/test_downloads.py +0 -0
- {hyperstudy-0.4.0 → hyperstudy-0.5.0}/tests/test_http.py +0 -0
- {hyperstudy-0.4.0 → hyperstudy-0.5.0}/tests/test_models.py +0 -0
- {hyperstudy-0.4.0 → hyperstudy-0.5.0}/tests/test_pagination.py +0 -0
- {hyperstudy-0.4.0 → hyperstudy-0.5.0}/tests/test_recordings_download.py +0 -0
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v0.5.0
|
|
4
|
+
|
|
5
|
+
### Breaking Changes
|
|
6
|
+
|
|
7
|
+
- **Removed `Persona.offline_cognition`.** The server now hard-discards this field (`offlineCognition` is always null server-side); offline loops live inside `cognition.config.contexts.<name>.offline`. The SDK also strips `offlineCognition` from create/update payloads so personas rebuilt from older API responses never send it.
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
- **Eye-tracking data access**: `get_eyetracking(scope_id, scope="experiment"|"room"|"participant", room_id=)` with the standard pagination/output options, plus `DataType.EYETRACKING`. `get_all_data` now includes an `eyetracking` key.
|
|
12
|
+
- **Room variables**: `get_variables(room_id)` — the reconstructed shared-variable timeline for a room. Returns `{writes, timeline, variable_names, dropped_writes}`: the write log (each write tagged with source and persisted flag), the per-state forward-filled snapshot matrix, the ordered variable set, and any writes that failed the server's ground-truth cross-checks (a bug signal).
|
|
13
|
+
- **Participant counts**: `get_counts(participant_id, room_id=)` — cheap per-data-type document counts (`counts` / `hasData`) for a participant in a room.
|
|
14
|
+
- **Experiment export**: `export_experiment(experiment_id)` — portable experiment JSON (definition plus media-asset manifest), suitable for re-import.
|
|
15
|
+
- **Cognition catalog**: `get_cognition_catalog()` — the valid building blocks for persona `cognition` configs (`abilities`, `recipes`, `offlineRecipes`). Requires the `read:personas` scope.
|
|
16
|
+
|
|
3
17
|
## v0.4.0
|
|
4
18
|
|
|
5
19
|
### Features
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hyperstudy
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Python SDK for the HyperStudy experiment platform API
|
|
5
5
|
Project-URL: Homepage, https://hyperstudy.io
|
|
6
6
|
Project-URL: Documentation, https://docs.hyperstudy.io/developers/python-sdk
|
|
@@ -78,6 +78,7 @@ chat = hs.get_chat("exp_id")
|
|
|
78
78
|
videochat = hs.get_videochat("exp_id")
|
|
79
79
|
sync = hs.get_sync("exp_id")
|
|
80
80
|
ratings = hs.get_ratings("exp_id", kind="continuous")
|
|
81
|
+
eyetracking = hs.get_eyetracking("exp_id")
|
|
81
82
|
components = hs.get_components("exp_id")
|
|
82
83
|
participants = hs.get_participants("exp_id")
|
|
83
84
|
rooms = hs.get_rooms("exp_id")
|
|
@@ -86,6 +87,13 @@ rooms = hs.get_rooms("exp_id")
|
|
|
86
87
|
questionnaire = hs.get_questionnaire("exp_id")
|
|
87
88
|
instructions = hs.get_instructions("exp_id")
|
|
88
89
|
consent = hs.get_consent("exp_id")
|
|
90
|
+
|
|
91
|
+
# Room-scoped: reconstructed shared-variable timeline
|
|
92
|
+
# -> {"writes", "timeline", "variable_names", "dropped_writes"}
|
|
93
|
+
variables = hs.get_variables("room_id")
|
|
94
|
+
|
|
95
|
+
# Per-data-type document counts for one participant in a room
|
|
96
|
+
counts = hs.get_counts("participant_id", room_id="room_id")
|
|
89
97
|
```
|
|
90
98
|
|
|
91
99
|
### Output Formats
|
|
@@ -130,6 +138,9 @@ exp = hs.get_experiment("exp_id")
|
|
|
130
138
|
new_exp = hs.create_experiment(name="My Study", description="...")
|
|
131
139
|
hs.update_experiment("exp_id", name="Updated Name")
|
|
132
140
|
hs.delete_experiment("exp_id")
|
|
141
|
+
|
|
142
|
+
# Portable experiment JSON (definition + media-asset manifest)
|
|
143
|
+
export = hs.export_experiment("exp_id")
|
|
133
144
|
```
|
|
134
145
|
|
|
135
146
|
### Building experiments programmatically (0.3.0+)
|
|
@@ -215,6 +226,9 @@ hs.run_more(dep["id"], rooms=5, budget_usd=2.5) # additive batch
|
|
|
215
226
|
decisions = hs.get_agent_decisions(exp["id"]) # per-turn logs
|
|
216
227
|
runs = hs.get_agent_runs(exp["id"]) # run manifests
|
|
217
228
|
detail = hs.get_agent_decision("room_id", "participantId_3") # full blobs
|
|
229
|
+
|
|
230
|
+
# --- Cognition authoring catalog ---
|
|
231
|
+
catalog = hs.get_cognition_catalog() # {"abilities": ..., "recipes": ..., "offlineRecipes": ...}
|
|
218
232
|
```
|
|
219
233
|
|
|
220
234
|
Persona methods need the `read:personas` / `write:personas` API-key scopes; deployment launch/control needs `write:deployments`; agent data reads need `read:events`.
|
|
@@ -224,8 +238,8 @@ Persona methods need the `read:personas` / `write:personas` API-key scopes; depl
|
|
|
224
238
|
```python
|
|
225
239
|
data = hs.get_all_data("participant_id", room_id="room_id")
|
|
226
240
|
# Returns dict with keys: events, recordings, chat, videochat, sync,
|
|
227
|
-
# ratings_continuous, ratings_sparse, components,
|
|
228
|
-
# instructions, consent, agent_decisions
|
|
241
|
+
# ratings_continuous, ratings_sparse, components, eyetracking,
|
|
242
|
+
# questionnaire, instructions, consent, agent_decisions
|
|
229
243
|
```
|
|
230
244
|
|
|
231
245
|
## API Key
|
|
@@ -43,6 +43,7 @@ chat = hs.get_chat("exp_id")
|
|
|
43
43
|
videochat = hs.get_videochat("exp_id")
|
|
44
44
|
sync = hs.get_sync("exp_id")
|
|
45
45
|
ratings = hs.get_ratings("exp_id", kind="continuous")
|
|
46
|
+
eyetracking = hs.get_eyetracking("exp_id")
|
|
46
47
|
components = hs.get_components("exp_id")
|
|
47
48
|
participants = hs.get_participants("exp_id")
|
|
48
49
|
rooms = hs.get_rooms("exp_id")
|
|
@@ -51,6 +52,13 @@ rooms = hs.get_rooms("exp_id")
|
|
|
51
52
|
questionnaire = hs.get_questionnaire("exp_id")
|
|
52
53
|
instructions = hs.get_instructions("exp_id")
|
|
53
54
|
consent = hs.get_consent("exp_id")
|
|
55
|
+
|
|
56
|
+
# Room-scoped: reconstructed shared-variable timeline
|
|
57
|
+
# -> {"writes", "timeline", "variable_names", "dropped_writes"}
|
|
58
|
+
variables = hs.get_variables("room_id")
|
|
59
|
+
|
|
60
|
+
# Per-data-type document counts for one participant in a room
|
|
61
|
+
counts = hs.get_counts("participant_id", room_id="room_id")
|
|
54
62
|
```
|
|
55
63
|
|
|
56
64
|
### Output Formats
|
|
@@ -95,6 +103,9 @@ exp = hs.get_experiment("exp_id")
|
|
|
95
103
|
new_exp = hs.create_experiment(name="My Study", description="...")
|
|
96
104
|
hs.update_experiment("exp_id", name="Updated Name")
|
|
97
105
|
hs.delete_experiment("exp_id")
|
|
106
|
+
|
|
107
|
+
# Portable experiment JSON (definition + media-asset manifest)
|
|
108
|
+
export = hs.export_experiment("exp_id")
|
|
98
109
|
```
|
|
99
110
|
|
|
100
111
|
### Building experiments programmatically (0.3.0+)
|
|
@@ -180,6 +191,9 @@ hs.run_more(dep["id"], rooms=5, budget_usd=2.5) # additive batch
|
|
|
180
191
|
decisions = hs.get_agent_decisions(exp["id"]) # per-turn logs
|
|
181
192
|
runs = hs.get_agent_runs(exp["id"]) # run manifests
|
|
182
193
|
detail = hs.get_agent_decision("room_id", "participantId_3") # full blobs
|
|
194
|
+
|
|
195
|
+
# --- Cognition authoring catalog ---
|
|
196
|
+
catalog = hs.get_cognition_catalog() # {"abilities": ..., "recipes": ..., "offlineRecipes": ...}
|
|
183
197
|
```
|
|
184
198
|
|
|
185
199
|
Persona methods need the `read:personas` / `write:personas` API-key scopes; deployment launch/control needs `write:deployments`; agent data reads need `read:events`.
|
|
@@ -189,8 +203,8 @@ Persona methods need the `read:personas` / `write:personas` API-key scopes; depl
|
|
|
189
203
|
```python
|
|
190
204
|
data = hs.get_all_data("participant_id", room_id="room_id")
|
|
191
205
|
# Returns dict with keys: events, recordings, chat, videochat, sync,
|
|
192
|
-
# ratings_continuous, ratings_sparse, components,
|
|
193
|
-
# instructions, consent, agent_decisions
|
|
206
|
+
# ratings_continuous, ratings_sparse, components, eyetracking,
|
|
207
|
+
# questionnaire, instructions, consent, agent_decisions
|
|
194
208
|
```
|
|
195
209
|
|
|
196
210
|
## API Key
|
|
@@ -235,6 +235,35 @@ class HyperStudy(ExperimentMixin, PersonaMixin):
|
|
|
235
235
|
output=output, progress=progress,
|
|
236
236
|
)
|
|
237
237
|
|
|
238
|
+
def get_eyetracking(
|
|
239
|
+
self,
|
|
240
|
+
scope_id: str,
|
|
241
|
+
*,
|
|
242
|
+
scope: str = "experiment",
|
|
243
|
+
room_id: str | None = None,
|
|
244
|
+
limit: int | None = None,
|
|
245
|
+
offset: int = 0,
|
|
246
|
+
output: str = "pandas",
|
|
247
|
+
progress: bool = True,
|
|
248
|
+
):
|
|
249
|
+
"""Fetch eye-tracking gaze data.
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
scope_id: ID of the experiment, room, or participant.
|
|
253
|
+
scope: ``"experiment"``, ``"room"``, or ``"participant"``.
|
|
254
|
+
room_id: Required when ``scope="participant"``.
|
|
255
|
+
limit: Max records. ``None`` fetches all pages.
|
|
256
|
+
offset: Starting offset.
|
|
257
|
+
output: ``"pandas"`` (default), ``"polars"``, or ``"dict"``.
|
|
258
|
+
progress: Show progress bar when paginating.
|
|
259
|
+
"""
|
|
260
|
+
return self._fetch_data(
|
|
261
|
+
"eyetracking", scope_id,
|
|
262
|
+
scope=scope, room_id=room_id,
|
|
263
|
+
limit=limit, offset=offset,
|
|
264
|
+
output=output, progress=progress,
|
|
265
|
+
)
|
|
266
|
+
|
|
238
267
|
def get_components(
|
|
239
268
|
self,
|
|
240
269
|
scope_id: str,
|
|
@@ -294,6 +323,67 @@ class HyperStudy(ExperimentMixin, PersonaMixin):
|
|
|
294
323
|
output=output, progress=progress,
|
|
295
324
|
)
|
|
296
325
|
|
|
326
|
+
def get_variables(
|
|
327
|
+
self,
|
|
328
|
+
room_id: str,
|
|
329
|
+
*,
|
|
330
|
+
output: str = "pandas",
|
|
331
|
+
):
|
|
332
|
+
"""Fetch the reconstructed shared-variable timeline for a room.
|
|
333
|
+
|
|
334
|
+
Returns a dict with four keys:
|
|
335
|
+
|
|
336
|
+
- ``writes`` — the time-ordered write log (one row per variable
|
|
337
|
+
write: seeded constant, human component response, or agent
|
|
338
|
+
submit-response, tagged with source and persisted flag), converted
|
|
339
|
+
per ``output``
|
|
340
|
+
- ``timeline`` — the derived per-state forward-filled variable
|
|
341
|
+
snapshot matrix, converted per ``output``
|
|
342
|
+
- ``variable_names`` — ordered list of every variable seen
|
|
343
|
+
- ``matrix_columns`` — ordered column set of the timeline matrix
|
|
344
|
+
- ``dropped_writes`` — writes that failed the server's ground-truth
|
|
345
|
+
cross-checks (a bug signal; empty is the healthy state)
|
|
346
|
+
- ``mode`` — the reconstruction mode the server processor ran in
|
|
347
|
+
|
|
348
|
+
Args:
|
|
349
|
+
room_id: Room ID.
|
|
350
|
+
output: ``"pandas"`` (default), ``"polars"``, or ``"dict"``
|
|
351
|
+
for the tabular values.
|
|
352
|
+
"""
|
|
353
|
+
body = self._transport.get(f"data/variables/room/{room_id}")
|
|
354
|
+
metadata = body.get("metadata") or {}
|
|
355
|
+
return {
|
|
356
|
+
"writes": self._convert_output(body.get("data", []), output),
|
|
357
|
+
"timeline": self._convert_output(metadata.get("timeline") or [], output),
|
|
358
|
+
"variable_names": metadata.get("variableNames") or [],
|
|
359
|
+
"matrix_columns": metadata.get("matrixColumns") or [],
|
|
360
|
+
"dropped_writes": metadata.get("droppedWrites") or [],
|
|
361
|
+
"mode": metadata.get("mode"),
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
def get_counts(
|
|
365
|
+
self,
|
|
366
|
+
participant_id: str,
|
|
367
|
+
room_id: str,
|
|
368
|
+
) -> dict[str, Any]:
|
|
369
|
+
"""Fetch per-data-type document counts for a participant in a room.
|
|
370
|
+
|
|
371
|
+
Cheap count queries grouped by data type — useful for checking
|
|
372
|
+
which data types exist before fetching them.
|
|
373
|
+
|
|
374
|
+
Args:
|
|
375
|
+
participant_id: Participant ID.
|
|
376
|
+
room_id: Room ID.
|
|
377
|
+
|
|
378
|
+
Returns:
|
|
379
|
+
Dict with ``counts`` and ``hasData`` keyed by data type.
|
|
380
|
+
"""
|
|
381
|
+
body = self._transport.get(
|
|
382
|
+
f"data/counts/participant/{participant_id}", params={"roomId": room_id}
|
|
383
|
+
)
|
|
384
|
+
data = body.get("data", [])
|
|
385
|
+
return data[0] if isinstance(data, list) and data else data
|
|
386
|
+
|
|
297
387
|
# ------------------------------------------------------------------
|
|
298
388
|
# Convenience: category-filtered events
|
|
299
389
|
# ------------------------------------------------------------------
|
|
@@ -639,6 +729,7 @@ class HyperStudy(ExperimentMixin, PersonaMixin):
|
|
|
639
729
|
"ratings_continuous": self.get_ratings(participant_id, kind="continuous", **common),
|
|
640
730
|
"ratings_sparse": self.get_ratings(participant_id, kind="sparse", **common),
|
|
641
731
|
"components": self.get_components(participant_id, **common),
|
|
732
|
+
"eyetracking": self.get_eyetracking(participant_id, **common),
|
|
642
733
|
"questionnaire": self.get_questionnaire(participant_id, **common),
|
|
643
734
|
"instructions": self.get_instructions(participant_id, **common),
|
|
644
735
|
"consent": self.get_consent(participant_id, **common),
|
|
@@ -115,6 +115,17 @@ class ExperimentMixin:
|
|
|
115
115
|
data = body.get("data", [])
|
|
116
116
|
return data[0] if isinstance(data, list) and data else data
|
|
117
117
|
|
|
118
|
+
def export_experiment(self, experiment_id: str) -> dict[str, Any]:
|
|
119
|
+
"""Export an experiment as portable JSON.
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
The export payload (experiment definition plus media-asset
|
|
123
|
+
manifest), suitable for re-import.
|
|
124
|
+
"""
|
|
125
|
+
body = self._transport.get(f"experiments/{experiment_id}/export")
|
|
126
|
+
data = body.get("data", [])
|
|
127
|
+
return data[0] if isinstance(data, list) and data else data
|
|
128
|
+
|
|
118
129
|
# ------------------------------------------------------------------
|
|
119
130
|
# Write
|
|
120
131
|
# ------------------------------------------------------------------
|
|
@@ -186,8 +186,9 @@ class Persona(_Model):
|
|
|
186
186
|
"""A reusable AI-agent definition (agent library entry).
|
|
187
187
|
|
|
188
188
|
The persona owns the agent's identity, model settings, guardrails and
|
|
189
|
-
(optionally) cognition. ``cognition``
|
|
190
|
-
|
|
189
|
+
(optionally) cognition. ``cognition`` is accepted as a plain dict — its
|
|
190
|
+
schema is experimental and still evolving. Offline loops live inside
|
|
191
|
+
``cognition["config"]["contexts"][<name>]["offline"]``.
|
|
191
192
|
"""
|
|
192
193
|
|
|
193
194
|
name: str = Field(min_length=1)
|
|
@@ -204,7 +205,6 @@ class Persona(_Model):
|
|
|
204
205
|
disable_thinking: Optional[bool] = None
|
|
205
206
|
seed: Optional[int] = None
|
|
206
207
|
cognition: Optional[dict[str, Any]] = None
|
|
207
|
-
offline_cognition: Optional[dict[str, Any]] = None
|
|
208
208
|
memory_persistence: Optional[str] = None # "none" (default) | "cross-experiment"
|
|
209
209
|
language: Optional[str] = None
|
|
210
210
|
seed_memories: Optional[list[dict[str, Any]]] = None
|
|
@@ -25,6 +25,11 @@ def _persona_payload(persona: "Persona | None", kwargs: dict[str, Any]) -> dict[
|
|
|
25
25
|
payload.update(persona.model_dump(by_alias=True, exclude_none=True))
|
|
26
26
|
if kwargs:
|
|
27
27
|
payload.update(camelize_wire(kwargs))
|
|
28
|
+
# The server hard-discards offlineCognition (offline loops now live inside
|
|
29
|
+
# cognition.config.contexts.<name>.offline), so never send it — including
|
|
30
|
+
# when a Persona was rebuilt from an older API response via extra fields.
|
|
31
|
+
payload.pop("offlineCognition", None)
|
|
32
|
+
payload.pop("offline_cognition", None)
|
|
28
33
|
return payload
|
|
29
34
|
|
|
30
35
|
|
|
@@ -101,3 +106,16 @@ class PersonaMixin:
|
|
|
101
106
|
"""
|
|
102
107
|
body = self._transport.post(f"personas/{persona_id}/duplicate")
|
|
103
108
|
return body.get("data", {})
|
|
109
|
+
|
|
110
|
+
def get_cognition_catalog(self) -> dict[str, Any]:
|
|
111
|
+
"""Fetch the cognition authoring catalog.
|
|
112
|
+
|
|
113
|
+
Lists the valid building blocks for persona ``cognition`` configs.
|
|
114
|
+
Requires the ``read:personas`` scope.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
Dict with ``abilities``, ``recipes``, and ``offlineRecipes``.
|
|
118
|
+
"""
|
|
119
|
+
body = self._transport.get("agent-cognition/catalog")
|
|
120
|
+
data = body.get("data", [])
|
|
121
|
+
return data[0] if isinstance(data, list) and data else data
|
|
@@ -242,6 +242,126 @@ def test_get_ratings_sparse_flattens_data(api_key, sparse_ratings_response):
|
|
|
242
242
|
assert df["sparseRatingData_mediaPauseOnset"].iloc[0] == 8200
|
|
243
243
|
|
|
244
244
|
|
|
245
|
+
@responses.activate
|
|
246
|
+
def test_get_eyetracking(api_key, events_response):
|
|
247
|
+
"""get_eyetracking hits the correct endpoint."""
|
|
248
|
+
responses.get(
|
|
249
|
+
f"{BASE_URL}/data/eyetracking/experiment/exp_abc123",
|
|
250
|
+
json=events_response,
|
|
251
|
+
status=200,
|
|
252
|
+
)
|
|
253
|
+
client = HyperStudy(api_key=api_key, base_url=BASE_URL)
|
|
254
|
+
df = client.get_eyetracking("exp_abc123", limit=1000)
|
|
255
|
+
assert isinstance(df, pd.DataFrame)
|
|
256
|
+
assert len(df) == 3
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
@responses.activate
|
|
260
|
+
def test_get_eyetracking_participant_scope(api_key, events_response):
|
|
261
|
+
"""get_eyetracking with scope='participant' passes roomId query param."""
|
|
262
|
+
responses.get(
|
|
263
|
+
f"{BASE_URL}/data/eyetracking/participant/user_1",
|
|
264
|
+
json=events_response,
|
|
265
|
+
status=200,
|
|
266
|
+
)
|
|
267
|
+
client = HyperStudy(api_key=api_key, base_url=BASE_URL)
|
|
268
|
+
df = client.get_eyetracking(
|
|
269
|
+
"user_1", scope="participant", room_id="room_xyz", limit=1000
|
|
270
|
+
)
|
|
271
|
+
assert len(df) == 3
|
|
272
|
+
assert "roomId=room_xyz" in responses.calls[0].request.url
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
@responses.activate
|
|
276
|
+
def test_get_variables(api_key):
|
|
277
|
+
"""get_variables returns writes, timeline, variable names, and dropped writes."""
|
|
278
|
+
responses.get(
|
|
279
|
+
f"{BASE_URL}/data/variables/room/room_xyz",
|
|
280
|
+
json={
|
|
281
|
+
"status": "success",
|
|
282
|
+
"data": [
|
|
283
|
+
{"variable": "rating", "value": 7, "source": "human", "persisted": True},
|
|
284
|
+
{"variable": "condition", "value": "A", "source": "seed", "persisted": True},
|
|
285
|
+
],
|
|
286
|
+
"metadata": {
|
|
287
|
+
"variableNames": ["condition", "rating"],
|
|
288
|
+
"timeline": [{"stateId": "s1", "condition": "A", "rating": 7}],
|
|
289
|
+
"matrixColumns": ["stateId", "condition", "rating"],
|
|
290
|
+
"droppedWrites": [{"variable": "rating", "reason": "mismatch"}],
|
|
291
|
+
"mode": "replay",
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
status=200,
|
|
295
|
+
)
|
|
296
|
+
client = HyperStudy(api_key=api_key, base_url=BASE_URL)
|
|
297
|
+
result = client.get_variables("room_xyz")
|
|
298
|
+
|
|
299
|
+
assert isinstance(result["writes"], pd.DataFrame)
|
|
300
|
+
assert len(result["writes"]) == 2
|
|
301
|
+
assert set(result["writes"]["variable"]) == {"rating", "condition"}
|
|
302
|
+
assert isinstance(result["timeline"], pd.DataFrame)
|
|
303
|
+
assert len(result["timeline"]) == 1
|
|
304
|
+
assert result["variable_names"] == ["condition", "rating"]
|
|
305
|
+
assert result["matrix_columns"] == ["stateId", "condition", "rating"]
|
|
306
|
+
assert result["dropped_writes"] == [{"variable": "rating", "reason": "mismatch"}]
|
|
307
|
+
assert result["mode"] == "replay"
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
@responses.activate
|
|
311
|
+
def test_get_variables_dict(api_key):
|
|
312
|
+
"""get_variables with output='dict' returns raw write and timeline lists."""
|
|
313
|
+
responses.get(
|
|
314
|
+
f"{BASE_URL}/data/variables/room/room_xyz",
|
|
315
|
+
json={
|
|
316
|
+
"status": "success",
|
|
317
|
+
"data": [{"variable": "rating", "value": 7}],
|
|
318
|
+
"metadata": {},
|
|
319
|
+
},
|
|
320
|
+
status=200,
|
|
321
|
+
)
|
|
322
|
+
client = HyperStudy(api_key=api_key, base_url=BASE_URL)
|
|
323
|
+
result = client.get_variables("room_xyz", output="dict")
|
|
324
|
+
assert result["writes"] == [{"variable": "rating", "value": 7}]
|
|
325
|
+
assert result["timeline"] == []
|
|
326
|
+
assert result["variable_names"] == []
|
|
327
|
+
assert result["dropped_writes"] == []
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
@responses.activate
|
|
331
|
+
def test_get_counts(api_key):
|
|
332
|
+
"""get_counts passes roomId and unwraps the counts dict."""
|
|
333
|
+
responses.get(
|
|
334
|
+
f"{BASE_URL}/data/counts/participant/user_1",
|
|
335
|
+
json={
|
|
336
|
+
"status": "success",
|
|
337
|
+
"data": [
|
|
338
|
+
{
|
|
339
|
+
"participantId": "user_1",
|
|
340
|
+
"roomId": "room_xyz",
|
|
341
|
+
"counts": {"textchat": 4, "sync": 0},
|
|
342
|
+
"hasData": {"textchat": True, "sync": False},
|
|
343
|
+
}
|
|
344
|
+
],
|
|
345
|
+
"metadata": {},
|
|
346
|
+
},
|
|
347
|
+
status=200,
|
|
348
|
+
)
|
|
349
|
+
client = HyperStudy(api_key=api_key, base_url=BASE_URL)
|
|
350
|
+
result = client.get_counts("user_1", room_id="room_xyz")
|
|
351
|
+
|
|
352
|
+
assert isinstance(result, dict)
|
|
353
|
+
assert result["counts"]["textchat"] == 4
|
|
354
|
+
assert result["hasData"]["sync"] is False
|
|
355
|
+
assert "roomId=room_xyz" in responses.calls[0].request.url
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def test_get_counts_requires_room_id(api_key):
|
|
359
|
+
"""get_counts requires room_id (the server 400s without it)."""
|
|
360
|
+
client = HyperStudy(api_key=api_key, base_url=BASE_URL)
|
|
361
|
+
with pytest.raises(TypeError):
|
|
362
|
+
client.get_counts("user_1")
|
|
363
|
+
|
|
364
|
+
|
|
245
365
|
@responses.activate
|
|
246
366
|
def test_get_sync_with_aggregation(api_key, events_response):
|
|
247
367
|
"""get_sync passes aggregationWindow param."""
|
|
@@ -519,7 +639,8 @@ def test_get_all_data(api_key, events_response, pre_experiment_response):
|
|
|
519
639
|
"""get_all_data returns a dict of DataFrames."""
|
|
520
640
|
# Mock all data type endpoints for participant scope
|
|
521
641
|
for dtype in ("events", "recordings", "chat", "videochat", "sync",
|
|
522
|
-
"ratings/continuous", "ratings/sparse", "components"
|
|
642
|
+
"ratings/continuous", "ratings/sparse", "components",
|
|
643
|
+
"eyetracking"):
|
|
523
644
|
responses.get(
|
|
524
645
|
f"{BASE_URL}/data/{dtype}/participant/user_1",
|
|
525
646
|
json=events_response,
|
|
@@ -541,7 +662,7 @@ def test_get_all_data(api_key, events_response, pre_experiment_response):
|
|
|
541
662
|
assert isinstance(result, dict)
|
|
542
663
|
assert set(result.keys()) == {
|
|
543
664
|
"events", "recordings", "chat", "videochat", "sync",
|
|
544
|
-
"ratings_continuous", "ratings_sparse", "components",
|
|
665
|
+
"ratings_continuous", "ratings_sparse", "components", "eyetracking",
|
|
545
666
|
"questionnaire", "instructions", "consent", "agent_decisions",
|
|
546
667
|
}
|
|
547
668
|
for v in result.values():
|
|
@@ -99,6 +99,38 @@ def test_get_experiment_config(experiment_single_response):
|
|
|
99
99
|
assert config["id"] == "exp_abc123"
|
|
100
100
|
|
|
101
101
|
|
|
102
|
+
@responses.activate
|
|
103
|
+
def test_export_experiment():
|
|
104
|
+
"""export_experiment returns the portable experiment JSON."""
|
|
105
|
+
export_response = {
|
|
106
|
+
"status": "success",
|
|
107
|
+
"metadata": {
|
|
108
|
+
"dataType": "experimentExport",
|
|
109
|
+
"scope": "experiment",
|
|
110
|
+
"scopeId": "exp_abc123",
|
|
111
|
+
"filename": "emotion-study-export.json",
|
|
112
|
+
"mediaAssetsCount": 2,
|
|
113
|
+
},
|
|
114
|
+
"data": [
|
|
115
|
+
{
|
|
116
|
+
"experiment": {"name": "Emotion Study", "states": []},
|
|
117
|
+
"mediaAssets": [{"url": "https://cdn.example.com/clip.mp4"}],
|
|
118
|
+
}
|
|
119
|
+
],
|
|
120
|
+
}
|
|
121
|
+
responses.get(
|
|
122
|
+
f"{BASE_URL}/experiments/exp_abc123/export",
|
|
123
|
+
json=export_response,
|
|
124
|
+
status=200,
|
|
125
|
+
)
|
|
126
|
+
client = HyperStudy(api_key="hst_test_key", base_url=BASE_URL)
|
|
127
|
+
export = client.export_experiment("exp_abc123")
|
|
128
|
+
|
|
129
|
+
assert isinstance(export, dict)
|
|
130
|
+
assert export["experiment"]["name"] == "Emotion Study"
|
|
131
|
+
assert len(export["mediaAssets"]) == 1
|
|
132
|
+
|
|
133
|
+
|
|
102
134
|
@responses.activate
|
|
103
135
|
def test_create_experiment():
|
|
104
136
|
"""create_experiment POSTs to the experiments endpoint."""
|
|
@@ -112,6 +112,76 @@ def test_update_persona_requires_fields(api_key):
|
|
|
112
112
|
client.update_persona("persona-1")
|
|
113
113
|
|
|
114
114
|
|
|
115
|
+
@responses.activate
|
|
116
|
+
def test_create_persona_strips_offline_cognition(api_key):
|
|
117
|
+
"""offlineCognition is never sent — the server hard-discards it.
|
|
118
|
+
|
|
119
|
+
Covers both a Persona rebuilt from an older API response (extra field)
|
|
120
|
+
and the snake_case kwargs path. Offline loops now live inside
|
|
121
|
+
cognition.config.contexts.<name>.offline."""
|
|
122
|
+
responses.post(f"{BASE_URL}/personas", json=_envelope(PERSONA_DOC), status=201)
|
|
123
|
+
responses.post(f"{BASE_URL}/personas", json=_envelope(PERSONA_DOC), status=201)
|
|
124
|
+
|
|
125
|
+
client = HyperStudy(api_key=api_key)
|
|
126
|
+
|
|
127
|
+
# Persona rebuilt from an older response doc carrying offlineCognition.
|
|
128
|
+
persona = Persona(**{
|
|
129
|
+
"name": "Rebuilt",
|
|
130
|
+
"cognition": {"config": {"contexts": {"social": {"offline": {"cadence": "endOfState"}}}}},
|
|
131
|
+
"offlineCognition": {"loops": []},
|
|
132
|
+
})
|
|
133
|
+
client.create_persona(persona=persona)
|
|
134
|
+
sent = json.loads(responses.calls[0].request.body)
|
|
135
|
+
assert "offlineCognition" not in sent
|
|
136
|
+
assert "offline_cognition" not in sent
|
|
137
|
+
assert sent["cognition"]["config"]["contexts"]["social"]["offline"] == {
|
|
138
|
+
"cadence": "endOfState"
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
# Snake_case kwargs path.
|
|
142
|
+
client.create_persona(name="Kwargs", offline_cognition={"loops": []})
|
|
143
|
+
sent = json.loads(responses.calls[1].request.body)
|
|
144
|
+
assert "offlineCognition" not in sent
|
|
145
|
+
assert "offline_cognition" not in sent
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@responses.activate
|
|
149
|
+
def test_update_persona_strips_offline_cognition(api_key):
|
|
150
|
+
"""update_persona also drops offlineCognition from the payload."""
|
|
151
|
+
responses.put(f"{BASE_URL}/personas/persona-1", json=_envelope(PERSONA_DOC), status=200)
|
|
152
|
+
|
|
153
|
+
client = HyperStudy(api_key=api_key)
|
|
154
|
+
client.update_persona("persona-1", description="Updated", offline_cognition={"loops": []})
|
|
155
|
+
|
|
156
|
+
sent = json.loads(responses.calls[0].request.body)
|
|
157
|
+
assert sent == {"description": "Updated"}
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def test_persona_model_has_no_offline_cognition_field():
|
|
161
|
+
"""The typed Persona model no longer declares offline_cognition."""
|
|
162
|
+
assert "offline_cognition" not in Persona.model_fields
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@responses.activate
|
|
166
|
+
def test_get_cognition_catalog(api_key):
|
|
167
|
+
"""get_cognition_catalog returns the abilities/recipes/offlineRecipes dict."""
|
|
168
|
+
catalog = {
|
|
169
|
+
"abilities": {"perceive": {"description": "..."}},
|
|
170
|
+
"recipes": {"default": ["perceive"]},
|
|
171
|
+
"offlineRecipes": {"consolidate": ["reflect"]},
|
|
172
|
+
}
|
|
173
|
+
# The v3 envelope array-wraps non-array data; the client unwraps it.
|
|
174
|
+
responses.get(
|
|
175
|
+
f"{BASE_URL}/agent-cognition/catalog", json=_envelope([catalog]), status=200
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
client = HyperStudy(api_key=api_key)
|
|
179
|
+
result = client.get_cognition_catalog()
|
|
180
|
+
|
|
181
|
+
assert result == catalog
|
|
182
|
+
assert set(result.keys()) == {"abilities", "recipes", "offlineRecipes"}
|
|
183
|
+
|
|
184
|
+
|
|
115
185
|
@responses.activate
|
|
116
186
|
def test_delete_and_duplicate_persona(api_key):
|
|
117
187
|
responses.delete(f"{BASE_URL}/personas/persona-1", json=_envelope({"deleted": True}), status=200)
|
|
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
|
{hyperstudy-0.4.0 → hyperstudy-0.5.0}/tests/fixtures/agent_decisions_truncated_response.json
RENAMED
|
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
|
|
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
|