hyperstudy 0.4.0__tar.gz → 0.4.2__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.
Files changed (60) hide show
  1. {hyperstudy-0.4.0 → hyperstudy-0.4.2}/.github/workflows/publish.yml +2 -1
  2. {hyperstudy-0.4.0 → hyperstudy-0.4.2}/.github/workflows/test.yml +2 -1
  3. {hyperstudy-0.4.0 → hyperstudy-0.4.2}/CHANGELOG.md +41 -0
  4. {hyperstudy-0.4.0 → hyperstudy-0.4.2}/PKG-INFO +68 -4
  5. {hyperstudy-0.4.0 → hyperstudy-0.4.2}/README.md +66 -2
  6. {hyperstudy-0.4.0 → hyperstudy-0.4.2}/pyproject.toml +1 -1
  7. {hyperstudy-0.4.0 → hyperstudy-0.4.2}/src/hyperstudy/__init__.py +15 -1
  8. {hyperstudy-0.4.0 → hyperstudy-0.4.2}/src/hyperstudy/_http.py +17 -0
  9. {hyperstudy-0.4.0 → hyperstudy-0.4.2}/src/hyperstudy/_types.py +2 -0
  10. {hyperstudy-0.4.0 → hyperstudy-0.4.2}/src/hyperstudy/client.py +223 -12
  11. {hyperstudy-0.4.0 → hyperstudy-0.4.2}/src/hyperstudy/experiments.py +22 -15
  12. {hyperstudy-0.4.0 → hyperstudy-0.4.2}/src/hyperstudy/models.py +176 -23
  13. {hyperstudy-0.4.0 → hyperstudy-0.4.2}/src/hyperstudy/personas.py +45 -6
  14. {hyperstudy-0.4.0 → hyperstudy-0.4.2}/tests/conftest.py +14 -0
  15. hyperstudy-0.4.2/tests/fixtures/agent_decision_detail_response.json +44 -0
  16. hyperstudy-0.4.2/tests/fixtures/deployment_single_response.json +22 -0
  17. hyperstudy-0.4.2/tests/test_agent_authoring.py +190 -0
  18. {hyperstudy-0.4.0 → hyperstudy-0.4.2}/tests/test_client.py +124 -3
  19. {hyperstudy-0.4.0 → hyperstudy-0.4.2}/tests/test_deployments_write.py +6 -10
  20. {hyperstudy-0.4.0 → hyperstudy-0.4.2}/tests/test_experiments.py +32 -0
  21. {hyperstudy-0.4.0 → hyperstudy-0.4.2}/tests/test_http.py +20 -0
  22. hyperstudy-0.4.2/tests/test_personas.py +193 -0
  23. {hyperstudy-0.4.0 → hyperstudy-0.4.2}/tests/test_recordings_download.py +4 -2
  24. hyperstudy-0.4.2/tests/test_voice_and_filters.py +139 -0
  25. hyperstudy-0.4.2/tests/test_wire_keys.py +195 -0
  26. hyperstudy-0.4.0/tests/fixtures/agent_decision_detail_response.json +0 -30
  27. hyperstudy-0.4.0/tests/fixtures/deployment_single_response.json +0 -20
  28. hyperstudy-0.4.0/tests/test_personas.py +0 -127
  29. {hyperstudy-0.4.0 → hyperstudy-0.4.2}/.github/workflows/sync-release-notes.yml +0 -0
  30. {hyperstudy-0.4.0 → hyperstudy-0.4.2}/.gitignore +0 -0
  31. {hyperstudy-0.4.0 → hyperstudy-0.4.2}/LICENSE +0 -0
  32. {hyperstudy-0.4.0 → hyperstudy-0.4.2}/docs/superpowers/specs/2026-04-10-recording-downloads-design.md +0 -0
  33. {hyperstudy-0.4.0 → hyperstudy-0.4.2}/src/hyperstudy/_dataframe.py +0 -0
  34. {hyperstudy-0.4.0 → hyperstudy-0.4.2}/src/hyperstudy/_display.py +0 -0
  35. {hyperstudy-0.4.0 → hyperstudy-0.4.2}/src/hyperstudy/_downloads.py +0 -0
  36. {hyperstudy-0.4.0 → hyperstudy-0.4.2}/src/hyperstudy/_pagination.py +0 -0
  37. {hyperstudy-0.4.0 → hyperstudy-0.4.2}/src/hyperstudy/exceptions.py +0 -0
  38. {hyperstudy-0.4.0 → hyperstudy-0.4.2}/tests/__init__.py +0 -0
  39. {hyperstudy-0.4.0 → hyperstudy-0.4.2}/tests/fixtures/agent_decisions_response.json +0 -0
  40. {hyperstudy-0.4.0 → hyperstudy-0.4.2}/tests/fixtures/agent_decisions_truncated_response.json +0 -0
  41. {hyperstudy-0.4.0 → hyperstudy-0.4.2}/tests/fixtures/agent_runs_response.json +0 -0
  42. {hyperstudy-0.4.0 → hyperstudy-0.4.2}/tests/fixtures/deployment_sessions_response.json +0 -0
  43. {hyperstudy-0.4.0 → hyperstudy-0.4.2}/tests/fixtures/deployments_list_response.json +0 -0
  44. {hyperstudy-0.4.0 → hyperstudy-0.4.2}/tests/fixtures/error_401.json +0 -0
  45. {hyperstudy-0.4.0 → hyperstudy-0.4.2}/tests/fixtures/error_403.json +0 -0
  46. {hyperstudy-0.4.0 → hyperstudy-0.4.2}/tests/fixtures/events_response.json +0 -0
  47. {hyperstudy-0.4.0 → hyperstudy-0.4.2}/tests/fixtures/experiment.schema.json +0 -0
  48. {hyperstudy-0.4.0 → hyperstudy-0.4.2}/tests/fixtures/experiment_single_response.json +0 -0
  49. {hyperstudy-0.4.0 → hyperstudy-0.4.2}/tests/fixtures/experiments_list_response.json +0 -0
  50. {hyperstudy-0.4.0 → hyperstudy-0.4.2}/tests/fixtures/paginated_page1.json +0 -0
  51. {hyperstudy-0.4.0 → hyperstudy-0.4.2}/tests/fixtures/paginated_page2.json +0 -0
  52. {hyperstudy-0.4.0 → hyperstudy-0.4.2}/tests/fixtures/pre_experiment_response.json +0 -0
  53. {hyperstudy-0.4.0 → hyperstudy-0.4.2}/tests/fixtures/recordings_response.json +0 -0
  54. {hyperstudy-0.4.0 → hyperstudy-0.4.2}/tests/fixtures/sparse_ratings_response.json +0 -0
  55. {hyperstudy-0.4.0 → hyperstudy-0.4.2}/tests/fixtures/warnings_response.json +0 -0
  56. {hyperstudy-0.4.0 → hyperstudy-0.4.2}/tests/test_agent_data.py +0 -0
  57. {hyperstudy-0.4.0 → hyperstudy-0.4.2}/tests/test_dataframe.py +0 -0
  58. {hyperstudy-0.4.0 → hyperstudy-0.4.2}/tests/test_downloads.py +0 -0
  59. {hyperstudy-0.4.0 → hyperstudy-0.4.2}/tests/test_models.py +0 -0
  60. {hyperstudy-0.4.0 → hyperstudy-0.4.2}/tests/test_pagination.py +0 -0
@@ -6,7 +6,8 @@ on:
6
6
 
7
7
  jobs:
8
8
  test:
9
- runs-on: ubuntu-latest
9
+ # Python 3.9 has no build for ubuntu-latest (26.04); pinned until 3.9 support is dropped.
10
+ runs-on: ubuntu-24.04
10
11
  strategy:
11
12
  matrix:
12
13
  python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
@@ -7,7 +7,8 @@ on:
7
7
 
8
8
  jobs:
9
9
  test:
10
- runs-on: ubuntu-latest
10
+ # Python 3.9 has no build for ubuntu-latest (26.04); pinned until 3.9 support is dropped.
11
+ runs-on: ubuntu-24.04
11
12
  strategy:
12
13
  matrix:
13
14
  python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
@@ -1,5 +1,46 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.4.2
4
+
5
+ ### Breaking Changes
6
+
7
+ - **Removed `Persona.language`.** The server discarded it and nothing read it. The SDK strips it from persona payloads, so personas rebuilt from older stored documents still save, and warns when it drops a value someone set. To set the language participants speak, use the experiment's `agent_config.voice_transcription.language`.
8
+
9
+ ### Fixes
10
+
11
+ - **Single-object responses are now returned as objects.** The v3 API always sends `data` as an array, so `get_persona`, `create_persona`, `update_persona`, `duplicate_persona`, `create_deployment`, `get_agent_spend`, `run_more`, `retry_room` and `get_agent_decision` returned a one-element list instead of a dict, and `download_recording` / `download_recordings` raised `AttributeError` for recordings with a `downloadPath`. A single-object response with no record now raises `HyperStudyError` instead of returning `{}`. Test mocks now build the envelope exactly as the server does.
12
+ - **Keys that are not the SDK's own are sent as written** on the kwargs and raw-dict paths. Previously `extra_params={"top_k": 20}` reached a custom provider as `topK`, a cognition context named `post_trial` was stored as `postTrial`, voice turn-detection settings (`silence_duration_ms`, ...) were camelCased and rejected by the server, `prolific_enabled` / `prolific_config` were renamed and dropped, and stimulus mapping names were renamed. Schema fields such as `waiting_room_config` are still converted.
13
+ - **Typed models can be passed as keyword arguments**: `update_experiment(agent_config=AgentConfig(...))`, `update_experiment(waiting_room_config=WaitingRoomConfig(...))` and `update_persona(voice=PersonaVoice(...))` raised `TypeError: Object of type ... is not JSON serializable`.
14
+ - `Persona(display_name=...)` and other undeclared persona fields were sent in snake_case and silently ignored by the server; `display_name` is now a declared field.
15
+
16
+ ### Features
17
+
18
+ - **Voice agent conversations**: `get_voice(scope_id, scope=, deployment_id=, room_id=, include_context=, order=)` returns one row per voice event: every speaker's utterances with transcripts, discarded and silent agent replies, what the voice engine was told, and engine connection drops. `DataType.VOICE`; `get_all_data` includes `voice`. Requires the `read:events` scope.
19
+ - **Voice personas**: `Persona.voice` (`PersonaVoice`, `RealtimeVoice`: engine, preset, speed, style, realtime provider and model), `Persona.display_name`, and `Persona.chat` (`PersonaChat`: `initiate`, `initiate_delay_ms`).
20
+ - **Voice settings per experiment**: `AgentConfig.voice_turn_taking` (`VoiceTurnTaking` with `EngineTurnDetection`, sent in the engine's own snake_case) and `AgentConfig.voice_transcription` (`VoiceTranscription`). Ranges mirror the server's validators, so mistakes fail when the model is built.
21
+ - **Experiment fields**: `visibility`, `tags`, `enable_participant_messaging`, `experimenter_message_style` (`ExperimenterMessageStyle`), `stimulus_mappings`, `device_integration`, `prolific_enabled`, `prolific_config`.
22
+ - **Data filters**: `get_components(component_type=, state_id=, completed=, start_time=, end_time=, sort=, order=)`, `get_participants(completion_code=, sort=, order=)`, `get_rooms(sort=, order=)`.
23
+ - **`check_data_access(experiment_id)`**: `{canView, canExport}` without fetching data.
24
+
25
+ ### Notes
26
+
27
+ - `update_persona` and `update_experiment` replace a nested block (`voice`, `chat`, `prompt`, `guardrails`, `agent_config`, ...) whole; pass the complete block to change one setting in it. Only an experiment's or persona's owner, or an organization admin, may change its `visibility`.
28
+ - New server-side columns arrive in the existing DataFrames with no SDK change: event clock fields (`clientTime`, `timestampSource`, `clockOffsetMs`, `rawOnset`, `clockOffsetApplied`), `decisionError` on agent decisions, and `completionReason` / `agentStartFailures` on rooms.
29
+
30
+ ## v0.4.1
31
+
32
+ ### Breaking Changes
33
+
34
+ - **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.
35
+
36
+ ### Features
37
+
38
+ - **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.
39
+ - **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).
40
+ - **Participant counts**: `get_counts(participant_id, room_id=)` — cheap per-data-type document counts (`counts` / `hasData`) for a participant in a room.
41
+ - **Experiment export**: `export_experiment(experiment_id)` — portable experiment JSON (definition plus media-asset manifest), suitable for re-import.
42
+ - **Cognition catalog**: `get_cognition_catalog()` — the valid building blocks for persona `cognition` configs (`abilities`, `recipes`, `offlineRecipes`). Requires the `read:personas` scope.
43
+
3
44
  ## v0.4.0
4
45
 
5
46
  ### Features
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: hyperstudy
3
- Version: 0.4.0
3
+ Version: 0.4.2
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
@@ -75,9 +75,11 @@ All data retrieval methods follow the same pattern:
75
75
  events = hs.get_events("exp_id")
76
76
  recordings = hs.get_recordings("exp_id")
77
77
  chat = hs.get_chat("exp_id")
78
+ voice = hs.get_voice("exp_id") # voice agent conversations
78
79
  videochat = hs.get_videochat("exp_id")
79
80
  sync = hs.get_sync("exp_id")
80
81
  ratings = hs.get_ratings("exp_id", kind="continuous")
82
+ eyetracking = hs.get_eyetracking("exp_id")
81
83
  components = hs.get_components("exp_id")
82
84
  participants = hs.get_participants("exp_id")
83
85
  rooms = hs.get_rooms("exp_id")
@@ -86,6 +88,16 @@ rooms = hs.get_rooms("exp_id")
86
88
  questionnaire = hs.get_questionnaire("exp_id")
87
89
  instructions = hs.get_instructions("exp_id")
88
90
  consent = hs.get_consent("exp_id")
91
+
92
+ # Room-scoped: reconstructed shared-variable timeline
93
+ # -> {"writes", "timeline", "variable_names", "dropped_writes"}
94
+ variables = hs.get_variables("room_id")
95
+
96
+ # Per-data-type document counts for one participant in a room
97
+ counts = hs.get_counts("participant_id", room_id="room_id")
98
+
99
+ # Whether you may view / export an experiment's data (no data fetched)
100
+ access = hs.check_data_access("exp_id") # {"canView": True, "canExport": False}
89
101
  ```
90
102
 
91
103
  ### Output Formats
@@ -107,6 +119,9 @@ events = hs.get_events(
107
119
  sort="onset",
108
120
  limit=100,
109
121
  )
122
+
123
+ ratings = hs.get_components("exp_id", component_type="vasrating", completed=True)
124
+ abandoned = hs.get_participants("exp_id", completion_code="ABANDONED")
110
125
  ```
111
126
 
112
127
  ### Auto-Pagination
@@ -130,6 +145,9 @@ exp = hs.get_experiment("exp_id")
130
145
  new_exp = hs.create_experiment(name="My Study", description="...")
131
146
  hs.update_experiment("exp_id", name="Updated Name")
132
147
  hs.delete_experiment("exp_id")
148
+
149
+ # Portable experiment JSON (definition + media-asset manifest)
150
+ export = hs.export_experiment("exp_id")
133
151
  ```
134
152
 
135
153
  ### Building experiments programmatically (0.3.0+)
@@ -215,8 +233,54 @@ hs.run_more(dep["id"], rooms=5, budget_usd=2.5) # additive batch
215
233
  decisions = hs.get_agent_decisions(exp["id"]) # per-turn logs
216
234
  runs = hs.get_agent_runs(exp["id"]) # run manifests
217
235
  detail = hs.get_agent_decision("room_id", "participantId_3") # full blobs
236
+
237
+ # --- Cognition authoring catalog ---
238
+ catalog = hs.get_cognition_catalog() # {"abilities": ..., "recipes": ..., "offlineRecipes": ...}
218
239
  ```
219
240
 
241
+ ### Voice agents
242
+
243
+ A persona with a `voice` speaks in the experiment's audio. The experiment sets how its voice agents take turns and whether participants' speech is transcribed.
244
+
245
+ ```python
246
+ from hyperstudy import (
247
+ PersonaVoice, RealtimeVoice, VoiceTurnTaking, EngineTurnDetection, VoiceTranscription,
248
+ )
249
+
250
+ voice_persona = hs.create_persona(persona=Persona(
251
+ name="Interviewer",
252
+ display_name="Sam", # what other participants see
253
+ provider="openai",
254
+ voice=PersonaVoice(
255
+ engine="realtime",
256
+ preset="marin", # the provider's voice
257
+ speed=1.0,
258
+ style="warm, a little hesitant",
259
+ realtime=RealtimeVoice(provider="openai", model="gpt-realtime-2.1"),
260
+ ),
261
+ ))
262
+
263
+ exp = hs.create_experiment(experiment=Experiment(
264
+ name="Voice interview",
265
+ runtime="v2",
266
+ roles={"interviewer": Role(mode="agent", persona_id=voice_persona["id"])},
267
+ agent_config=AgentConfig(
268
+ voice_turn_taking=VoiceTurnTaking(
269
+ engine_turn_detection=EngineTurnDetection(type="server_vad", silence_duration_ms=800),
270
+ min_response_delay_ms=250,
271
+ ),
272
+ voice_transcription=VoiceTranscription(enabled=True, language="en"),
273
+ ),
274
+ states=[...],
275
+ ))
276
+
277
+ # One room's conversation: every speaker's turns, plus what the engine was told
278
+ conversation = hs.get_voice("room_id", scope="room")
279
+ turns = conversation[conversation["kind"] == "utterance"][["onset", "speaker", "transcript"]]
280
+ ```
281
+
282
+ `update_persona` and `update_experiment` replace a nested block whole, so pass the complete `voice` or `agent_config` when changing one setting in it.
283
+
220
284
  Persona methods need the `read:personas` / `write:personas` API-key scopes; deployment launch/control needs `write:deployments`; agent data reads need `read:events`.
221
285
 
222
286
  ## All Data for a Participant
@@ -224,8 +288,8 @@ Persona methods need the `read:personas` / `write:personas` API-key scopes; depl
224
288
  ```python
225
289
  data = hs.get_all_data("participant_id", room_id="room_id")
226
290
  # Returns dict with keys: events, recordings, chat, videochat, sync,
227
- # ratings_continuous, ratings_sparse, components, questionnaire,
228
- # instructions, consent, agent_decisions
291
+ # ratings_continuous, ratings_sparse, components, eyetracking, voice,
292
+ # questionnaire, instructions, consent, agent_decisions
229
293
  ```
230
294
 
231
295
  ## API Key
@@ -40,9 +40,11 @@ All data retrieval methods follow the same pattern:
40
40
  events = hs.get_events("exp_id")
41
41
  recordings = hs.get_recordings("exp_id")
42
42
  chat = hs.get_chat("exp_id")
43
+ voice = hs.get_voice("exp_id") # voice agent conversations
43
44
  videochat = hs.get_videochat("exp_id")
44
45
  sync = hs.get_sync("exp_id")
45
46
  ratings = hs.get_ratings("exp_id", kind="continuous")
47
+ eyetracking = hs.get_eyetracking("exp_id")
46
48
  components = hs.get_components("exp_id")
47
49
  participants = hs.get_participants("exp_id")
48
50
  rooms = hs.get_rooms("exp_id")
@@ -51,6 +53,16 @@ rooms = hs.get_rooms("exp_id")
51
53
  questionnaire = hs.get_questionnaire("exp_id")
52
54
  instructions = hs.get_instructions("exp_id")
53
55
  consent = hs.get_consent("exp_id")
56
+
57
+ # Room-scoped: reconstructed shared-variable timeline
58
+ # -> {"writes", "timeline", "variable_names", "dropped_writes"}
59
+ variables = hs.get_variables("room_id")
60
+
61
+ # Per-data-type document counts for one participant in a room
62
+ counts = hs.get_counts("participant_id", room_id="room_id")
63
+
64
+ # Whether you may view / export an experiment's data (no data fetched)
65
+ access = hs.check_data_access("exp_id") # {"canView": True, "canExport": False}
54
66
  ```
55
67
 
56
68
  ### Output Formats
@@ -72,6 +84,9 @@ events = hs.get_events(
72
84
  sort="onset",
73
85
  limit=100,
74
86
  )
87
+
88
+ ratings = hs.get_components("exp_id", component_type="vasrating", completed=True)
89
+ abandoned = hs.get_participants("exp_id", completion_code="ABANDONED")
75
90
  ```
76
91
 
77
92
  ### Auto-Pagination
@@ -95,6 +110,9 @@ exp = hs.get_experiment("exp_id")
95
110
  new_exp = hs.create_experiment(name="My Study", description="...")
96
111
  hs.update_experiment("exp_id", name="Updated Name")
97
112
  hs.delete_experiment("exp_id")
113
+
114
+ # Portable experiment JSON (definition + media-asset manifest)
115
+ export = hs.export_experiment("exp_id")
98
116
  ```
99
117
 
100
118
  ### Building experiments programmatically (0.3.0+)
@@ -180,8 +198,54 @@ hs.run_more(dep["id"], rooms=5, budget_usd=2.5) # additive batch
180
198
  decisions = hs.get_agent_decisions(exp["id"]) # per-turn logs
181
199
  runs = hs.get_agent_runs(exp["id"]) # run manifests
182
200
  detail = hs.get_agent_decision("room_id", "participantId_3") # full blobs
201
+
202
+ # --- Cognition authoring catalog ---
203
+ catalog = hs.get_cognition_catalog() # {"abilities": ..., "recipes": ..., "offlineRecipes": ...}
183
204
  ```
184
205
 
206
+ ### Voice agents
207
+
208
+ A persona with a `voice` speaks in the experiment's audio. The experiment sets how its voice agents take turns and whether participants' speech is transcribed.
209
+
210
+ ```python
211
+ from hyperstudy import (
212
+ PersonaVoice, RealtimeVoice, VoiceTurnTaking, EngineTurnDetection, VoiceTranscription,
213
+ )
214
+
215
+ voice_persona = hs.create_persona(persona=Persona(
216
+ name="Interviewer",
217
+ display_name="Sam", # what other participants see
218
+ provider="openai",
219
+ voice=PersonaVoice(
220
+ engine="realtime",
221
+ preset="marin", # the provider's voice
222
+ speed=1.0,
223
+ style="warm, a little hesitant",
224
+ realtime=RealtimeVoice(provider="openai", model="gpt-realtime-2.1"),
225
+ ),
226
+ ))
227
+
228
+ exp = hs.create_experiment(experiment=Experiment(
229
+ name="Voice interview",
230
+ runtime="v2",
231
+ roles={"interviewer": Role(mode="agent", persona_id=voice_persona["id"])},
232
+ agent_config=AgentConfig(
233
+ voice_turn_taking=VoiceTurnTaking(
234
+ engine_turn_detection=EngineTurnDetection(type="server_vad", silence_duration_ms=800),
235
+ min_response_delay_ms=250,
236
+ ),
237
+ voice_transcription=VoiceTranscription(enabled=True, language="en"),
238
+ ),
239
+ states=[...],
240
+ ))
241
+
242
+ # One room's conversation: every speaker's turns, plus what the engine was told
243
+ conversation = hs.get_voice("room_id", scope="room")
244
+ turns = conversation[conversation["kind"] == "utterance"][["onset", "speaker", "transcript"]]
245
+ ```
246
+
247
+ `update_persona` and `update_experiment` replace a nested block whole, so pass the complete `voice` or `agent_config` when changing one setting in it.
248
+
185
249
  Persona methods need the `read:personas` / `write:personas` API-key scopes; deployment launch/control needs `write:deployments`; agent data reads need `read:events`.
186
250
 
187
251
  ## All Data for a Participant
@@ -189,8 +253,8 @@ Persona methods need the `read:personas` / `write:personas` API-key scopes; depl
189
253
  ```python
190
254
  data = hs.get_all_data("participant_id", room_id="room_id")
191
255
  # Returns dict with keys: events, recordings, chat, videochat, sync,
192
- # ratings_continuous, ratings_sparse, components, questionnaire,
193
- # instructions, consent, agent_decisions
256
+ # ratings_continuous, ratings_sparse, components, eyetracking, voice,
257
+ # questionnaire, instructions, consent, agent_decisions
194
258
  ```
195
259
 
196
260
  ## API Key
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "hyperstudy"
7
- version = "0.4.0"
7
+ version = "0.4.2"
8
8
  description = "Python SDK for the HyperStudy experiment platform API"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -33,17 +33,24 @@ from .models import (
33
33
  AgentConfig,
34
34
  ComponentType,
35
35
  DisconnectTimeout,
36
+ EngineTurnDetection,
36
37
  Experiment,
38
+ ExperimenterMessageStyle,
37
39
  FocusComponent,
38
40
  GlobalComponentType,
39
41
  Guardrails,
40
42
  InstructionsPage,
41
43
  Persona,
44
+ PersonaChat,
45
+ PersonaVoice,
42
46
  PostExperimentQuestionnaire,
43
47
  PromptLayer,
48
+ RealtimeVoice,
44
49
  Role,
45
50
  State,
46
51
  TransitionRules,
52
+ VoiceTranscription,
53
+ VoiceTurnTaking,
47
54
  WaitingRoomConfig,
48
55
  likert_scale,
49
56
  multiple_choice,
@@ -56,7 +63,7 @@ from .models import (
56
63
  waiting,
57
64
  )
58
65
 
59
- __version__ = "0.4.0"
66
+ __version__ = "0.4.2"
60
67
 
61
68
  __all__ = [
62
69
  "HyperStudy",
@@ -82,7 +89,14 @@ __all__ = [
82
89
  "AgentConfig",
83
90
  "PromptLayer",
84
91
  "Persona",
92
+ "PersonaVoice",
93
+ "RealtimeVoice",
94
+ "PersonaChat",
85
95
  "Guardrails",
96
+ "VoiceTurnTaking",
97
+ "EngineTurnDetection",
98
+ "VoiceTranscription",
99
+ "ExperimenterMessageStyle",
86
100
  "TransitionRules",
87
101
  "WaitingRoomConfig",
88
102
  "DisconnectTimeout",
@@ -25,6 +25,23 @@ _STATUS_MAP = {
25
25
  }
26
26
 
27
27
 
28
+ def single_record(body: dict) -> dict:
29
+ """Return the one record of a single-object response.
30
+
31
+ The v3 envelope always carries ``data`` as an array: a route that responds
32
+ with one object (a persona, a created deployment, a signed download URL)
33
+ arrives as ``[obj]``.
34
+ """
35
+ data = body.get("data")
36
+ if not isinstance(data, list):
37
+ raise HyperStudyError(
38
+ f"Unexpected response: expected 'data' to be an array, got {type(data).__name__}"
39
+ )
40
+ if not data or data[0] is None:
41
+ raise HyperStudyError("Unexpected response: the server returned no record")
42
+ return data[0]
43
+
44
+
28
45
  class HttpTransport:
29
46
  """Thin wrapper around ``requests.Session`` for the HyperStudy API.
30
47
 
@@ -17,9 +17,11 @@ class DataType(str, Enum):
17
17
  EVENTS = "events"
18
18
  RECORDINGS = "recordings"
19
19
  CHAT = "chat"
20
+ VOICE = "voice"
20
21
  VIDEOCHAT = "videochat"
21
22
  SYNC = "sync"
22
23
  RATINGS = "ratings"
24
+ EYETRACKING = "eyetracking"
23
25
  COMPONENTS = "components"
24
26
  PARTICIPANTS = "participants"
25
27
  ROOMS = "rooms"