hyperstudy 0.4.1__tar.gz → 0.4.3__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 (61) hide show
  1. {hyperstudy-0.4.1 → hyperstudy-0.4.3}/.github/workflows/publish.yml +2 -1
  2. {hyperstudy-0.4.1 → hyperstudy-0.4.3}/.github/workflows/test.yml +2 -1
  3. {hyperstudy-0.4.1 → hyperstudy-0.4.3}/CHANGELOG.md +41 -0
  4. {hyperstudy-0.4.1 → hyperstudy-0.4.3}/PKG-INFO +85 -3
  5. {hyperstudy-0.4.1 → hyperstudy-0.4.3}/README.md +83 -1
  6. {hyperstudy-0.4.1 → hyperstudy-0.4.3}/pyproject.toml +1 -1
  7. {hyperstudy-0.4.1 → hyperstudy-0.4.3}/src/hyperstudy/__init__.py +25 -1
  8. {hyperstudy-0.4.1 → hyperstudy-0.4.3}/src/hyperstudy/_http.py +17 -0
  9. {hyperstudy-0.4.1 → hyperstudy-0.4.3}/src/hyperstudy/_types.py +1 -0
  10. {hyperstudy-0.4.1 → hyperstudy-0.4.3}/src/hyperstudy/client.py +134 -14
  11. {hyperstudy-0.4.1 → hyperstudy-0.4.3}/src/hyperstudy/experiments.py +18 -17
  12. hyperstudy-0.4.3/src/hyperstudy/models.py +716 -0
  13. {hyperstudy-0.4.1 → hyperstudy-0.4.3}/src/hyperstudy/personas.py +29 -8
  14. {hyperstudy-0.4.1 → hyperstudy-0.4.3}/tests/conftest.py +14 -0
  15. hyperstudy-0.4.3/tests/fixtures/agent_decision_detail_response.json +44 -0
  16. hyperstudy-0.4.3/tests/fixtures/deployment_single_response.json +22 -0
  17. hyperstudy-0.4.3/tests/test_agent_authoring.py +190 -0
  18. {hyperstudy-0.4.1 → hyperstudy-0.4.3}/tests/test_client.py +2 -2
  19. {hyperstudy-0.4.1 → hyperstudy-0.4.3}/tests/test_deployments_write.py +6 -10
  20. {hyperstudy-0.4.1 → hyperstudy-0.4.3}/tests/test_http.py +20 -0
  21. {hyperstudy-0.4.1 → hyperstudy-0.4.3}/tests/test_personas.py +12 -16
  22. hyperstudy-0.4.3/tests/test_randomization_variables.py +291 -0
  23. {hyperstudy-0.4.1 → hyperstudy-0.4.3}/tests/test_recordings_download.py +4 -2
  24. hyperstudy-0.4.3/tests/test_voice_and_filters.py +139 -0
  25. hyperstudy-0.4.3/tests/test_wire_keys.py +195 -0
  26. hyperstudy-0.4.1/src/hyperstudy/models.py +0 -438
  27. hyperstudy-0.4.1/tests/fixtures/agent_decision_detail_response.json +0 -30
  28. hyperstudy-0.4.1/tests/fixtures/deployment_single_response.json +0 -20
  29. {hyperstudy-0.4.1 → hyperstudy-0.4.3}/.github/workflows/sync-release-notes.yml +0 -0
  30. {hyperstudy-0.4.1 → hyperstudy-0.4.3}/.gitignore +0 -0
  31. {hyperstudy-0.4.1 → hyperstudy-0.4.3}/LICENSE +0 -0
  32. {hyperstudy-0.4.1 → hyperstudy-0.4.3}/docs/superpowers/specs/2026-04-10-recording-downloads-design.md +0 -0
  33. {hyperstudy-0.4.1 → hyperstudy-0.4.3}/src/hyperstudy/_dataframe.py +0 -0
  34. {hyperstudy-0.4.1 → hyperstudy-0.4.3}/src/hyperstudy/_display.py +0 -0
  35. {hyperstudy-0.4.1 → hyperstudy-0.4.3}/src/hyperstudy/_downloads.py +0 -0
  36. {hyperstudy-0.4.1 → hyperstudy-0.4.3}/src/hyperstudy/_pagination.py +0 -0
  37. {hyperstudy-0.4.1 → hyperstudy-0.4.3}/src/hyperstudy/exceptions.py +0 -0
  38. {hyperstudy-0.4.1 → hyperstudy-0.4.3}/tests/__init__.py +0 -0
  39. {hyperstudy-0.4.1 → hyperstudy-0.4.3}/tests/fixtures/agent_decisions_response.json +0 -0
  40. {hyperstudy-0.4.1 → hyperstudy-0.4.3}/tests/fixtures/agent_decisions_truncated_response.json +0 -0
  41. {hyperstudy-0.4.1 → hyperstudy-0.4.3}/tests/fixtures/agent_runs_response.json +0 -0
  42. {hyperstudy-0.4.1 → hyperstudy-0.4.3}/tests/fixtures/deployment_sessions_response.json +0 -0
  43. {hyperstudy-0.4.1 → hyperstudy-0.4.3}/tests/fixtures/deployments_list_response.json +0 -0
  44. {hyperstudy-0.4.1 → hyperstudy-0.4.3}/tests/fixtures/error_401.json +0 -0
  45. {hyperstudy-0.4.1 → hyperstudy-0.4.3}/tests/fixtures/error_403.json +0 -0
  46. {hyperstudy-0.4.1 → hyperstudy-0.4.3}/tests/fixtures/events_response.json +0 -0
  47. {hyperstudy-0.4.1 → hyperstudy-0.4.3}/tests/fixtures/experiment.schema.json +0 -0
  48. {hyperstudy-0.4.1 → hyperstudy-0.4.3}/tests/fixtures/experiment_single_response.json +0 -0
  49. {hyperstudy-0.4.1 → hyperstudy-0.4.3}/tests/fixtures/experiments_list_response.json +0 -0
  50. {hyperstudy-0.4.1 → hyperstudy-0.4.3}/tests/fixtures/paginated_page1.json +0 -0
  51. {hyperstudy-0.4.1 → hyperstudy-0.4.3}/tests/fixtures/paginated_page2.json +0 -0
  52. {hyperstudy-0.4.1 → hyperstudy-0.4.3}/tests/fixtures/pre_experiment_response.json +0 -0
  53. {hyperstudy-0.4.1 → hyperstudy-0.4.3}/tests/fixtures/recordings_response.json +0 -0
  54. {hyperstudy-0.4.1 → hyperstudy-0.4.3}/tests/fixtures/sparse_ratings_response.json +0 -0
  55. {hyperstudy-0.4.1 → hyperstudy-0.4.3}/tests/fixtures/warnings_response.json +0 -0
  56. {hyperstudy-0.4.1 → hyperstudy-0.4.3}/tests/test_agent_data.py +0 -0
  57. {hyperstudy-0.4.1 → hyperstudy-0.4.3}/tests/test_dataframe.py +0 -0
  58. {hyperstudy-0.4.1 → hyperstudy-0.4.3}/tests/test_downloads.py +0 -0
  59. {hyperstudy-0.4.1 → hyperstudy-0.4.3}/tests/test_experiments.py +0 -0
  60. {hyperstudy-0.4.1 → hyperstudy-0.4.3}/tests/test_models.py +0 -0
  61. {hyperstudy-0.4.1 → hyperstudy-0.4.3}/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.3
4
+
5
+ ### Features
6
+
7
+ - **State randomization (V2 runner)**: `Experiment.clusters` (`Cluster`, `ClusterRule`: `type` `shuffle` | `keep`, `across_groups` `independent` | `same`). States and groups join a cluster with `cluster_id`. Each room draws its order once, so everyone in the room runs the same order. Clusters are used only while `randomize_states=True`, the same setting the legacy runner uses to shuffle each participant's order; with it `False` they are kept but not used.
8
+ - **Variable definitions (V2 runner)**: `Experiment.variable_definitions` (`VariableDefinition` with `kind` `constant`, `random` or `computed`; `Distribution` for random variables: `uniform`, `normal`, `exponential` or `list`, drawn `per_use` or `per_room`). A TIMED state draws its duration from a random variable with `TransitionRules(type="TIMED", variable_id=...)`. Ids are generated when omitted.
9
+ - **`Group`** model for `Experiment.groups` (`id`, `name`, `order`, `children`, `cluster_id`).
10
+ - A constant's `value` and a list distribution's `values` are sent exactly as written on the kwargs and raw-dict paths, like other researcher-named data.
11
+
12
+ ### Notes
13
+
14
+ - Both fields need `runtime="v2"`, the server's default for new experiments. The server refuses non-empty `variable_definitions` on any other runtime, and refuses a create or update that would make clusters or variables invalid (`ValidationError`, reasons in `details["errors"]`).
15
+ - Pass `runtime="v2"` to `validate_experiment` when validating variable definitions: servers older than v0.7.32 do not apply create's `"v2"` default in the dry run.
16
+
17
+ ## v0.4.2
18
+
19
+ ### Breaking Changes
20
+
21
+ - **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`.
22
+
23
+ ### Fixes
24
+
25
+ - **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.
26
+ - **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.
27
+ - **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`.
28
+ - `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.
29
+
30
+ ### Features
31
+
32
+ - **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.
33
+ - **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`).
34
+ - **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.
35
+ - **Experiment fields**: `visibility`, `tags`, `enable_participant_messaging`, `experimenter_message_style` (`ExperimenterMessageStyle`), `stimulus_mappings`, `device_integration`, `prolific_enabled`, `prolific_config`.
36
+ - **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=)`.
37
+ - **`check_data_access(experiment_id)`**: `{canView, canExport}` without fetching data.
38
+
39
+ ### Notes
40
+
41
+ - `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`.
42
+ - 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.
43
+
3
44
  ## v0.4.1
4
45
 
5
46
  ### Breaking Changes
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: hyperstudy
3
- Version: 0.4.1
3
+ Version: 0.4.3
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,6 +75,7 @@ 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")
@@ -94,6 +95,9 @@ variables = hs.get_variables("room_id")
94
95
 
95
96
  # Per-data-type document counts for one participant in a room
96
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}
97
101
  ```
98
102
 
99
103
  ### Output Formats
@@ -115,6 +119,9 @@ events = hs.get_events(
115
119
  sort="onset",
116
120
  limit=100,
117
121
  )
122
+
123
+ ratings = hs.get_components("exp_id", component_type="vasrating", completed=True)
124
+ abandoned = hs.get_participants("exp_id", completion_code="ABANDONED")
118
125
  ```
119
126
 
120
127
  ### Auto-Pagination
@@ -172,6 +179,38 @@ info = hs.create_experiment(experiment=exp)
172
179
 
173
180
  Component factories cover `show_text`, `show_image`, `show_video`, `vas_rating`, `text_input`, `multiple_choice`, `waiting`, `likert_scale`, `ranking`. For other component types, construct directly: `FocusComponent(type=ComponentType.X, config={...})`. See the [Experiment Authoring guide](https://docs.hyperstudy.io/experimenters/api-access/experiment-authoring) for the full reference.
174
181
 
182
+ ### Randomization and variables (V2 runner)
183
+
184
+ Clusters randomize the order of states and groups once per room while `randomize_states=True`; with it `False` the clusters are kept but rooms run the listed order. Variable definitions hold constants, random draws and computed values. Both need `runtime="v2"` (the server's default for new experiments). On the legacy runner, `randomize_states=True` shuffles each participant's order instead.
185
+
186
+ ```python
187
+ from hyperstudy import Cluster, ClusterRule, Distribution, TransitionRules, VariableDefinition
188
+
189
+ iti = VariableDefinition(
190
+ name="iti_ms", kind="random",
191
+ distribution=Distribution(type="uniform", min=500, max=1500),
192
+ )
193
+ exp = Experiment(
194
+ name="Randomized study",
195
+ runtime="v2",
196
+ randomize_states=True,
197
+ clusters=[Cluster(id="block", rule=ClusterRule(type="shuffle"))],
198
+ variable_definitions=[
199
+ VariableDefinition(name="endowment", kind="constant", value=10),
200
+ VariableDefinition(name="doubled", kind="computed", expr="endowment * 2"),
201
+ iti,
202
+ ],
203
+ states=[
204
+ State(id="a", cluster_id="block", focus_component=show_text("You have {endowment}")),
205
+ State(id="b", cluster_id="block", focus_component=show_text("Doubled: {doubled}")),
206
+ State(id="pause", focus_component=show_text("+"),
207
+ transition_rules=TransitionRules(type="TIMED", variable_id=iti.id)),
208
+ ],
209
+ )
210
+ ```
211
+
212
+ The server refuses a create or update that would make clusters or variables invalid, raising `ValidationError` with the reasons in `err.details["errors"]`.
213
+
175
214
  ## Deployments
176
215
 
177
216
  ```python
@@ -231,6 +270,49 @@ detail = hs.get_agent_decision("room_id", "participantId_3") # full blobs
231
270
  catalog = hs.get_cognition_catalog() # {"abilities": ..., "recipes": ..., "offlineRecipes": ...}
232
271
  ```
233
272
 
273
+ ### Voice agents
274
+
275
+ 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.
276
+
277
+ ```python
278
+ from hyperstudy import (
279
+ PersonaVoice, RealtimeVoice, VoiceTurnTaking, EngineTurnDetection, VoiceTranscription,
280
+ )
281
+
282
+ voice_persona = hs.create_persona(persona=Persona(
283
+ name="Interviewer",
284
+ display_name="Sam", # what other participants see
285
+ provider="openai",
286
+ voice=PersonaVoice(
287
+ engine="realtime",
288
+ preset="marin", # the provider's voice
289
+ speed=1.0,
290
+ style="warm, a little hesitant",
291
+ realtime=RealtimeVoice(provider="openai", model="gpt-realtime-2.1"),
292
+ ),
293
+ ))
294
+
295
+ exp = hs.create_experiment(experiment=Experiment(
296
+ name="Voice interview",
297
+ runtime="v2",
298
+ roles={"interviewer": Role(mode="agent", persona_id=voice_persona["id"])},
299
+ agent_config=AgentConfig(
300
+ voice_turn_taking=VoiceTurnTaking(
301
+ engine_turn_detection=EngineTurnDetection(type="server_vad", silence_duration_ms=800),
302
+ min_response_delay_ms=250,
303
+ ),
304
+ voice_transcription=VoiceTranscription(enabled=True, language="en"),
305
+ ),
306
+ states=[...],
307
+ ))
308
+
309
+ # One room's conversation: every speaker's turns, plus what the engine was told
310
+ conversation = hs.get_voice("room_id", scope="room")
311
+ turns = conversation[conversation["kind"] == "utterance"][["onset", "speaker", "transcript"]]
312
+ ```
313
+
314
+ `update_persona` and `update_experiment` replace a nested block whole, so pass the complete `voice` or `agent_config` when changing one setting in it.
315
+
234
316
  Persona methods need the `read:personas` / `write:personas` API-key scopes; deployment launch/control needs `write:deployments`; agent data reads need `read:events`.
235
317
 
236
318
  ## All Data for a Participant
@@ -238,7 +320,7 @@ Persona methods need the `read:personas` / `write:personas` API-key scopes; depl
238
320
  ```python
239
321
  data = hs.get_all_data("participant_id", room_id="room_id")
240
322
  # Returns dict with keys: events, recordings, chat, videochat, sync,
241
- # ratings_continuous, ratings_sparse, components, eyetracking,
323
+ # ratings_continuous, ratings_sparse, components, eyetracking, voice,
242
324
  # questionnaire, instructions, consent, agent_decisions
243
325
  ```
244
326
 
@@ -40,6 +40,7 @@ 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")
@@ -59,6 +60,9 @@ variables = hs.get_variables("room_id")
59
60
 
60
61
  # Per-data-type document counts for one participant in a room
61
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}
62
66
  ```
63
67
 
64
68
  ### Output Formats
@@ -80,6 +84,9 @@ events = hs.get_events(
80
84
  sort="onset",
81
85
  limit=100,
82
86
  )
87
+
88
+ ratings = hs.get_components("exp_id", component_type="vasrating", completed=True)
89
+ abandoned = hs.get_participants("exp_id", completion_code="ABANDONED")
83
90
  ```
84
91
 
85
92
  ### Auto-Pagination
@@ -137,6 +144,38 @@ info = hs.create_experiment(experiment=exp)
137
144
 
138
145
  Component factories cover `show_text`, `show_image`, `show_video`, `vas_rating`, `text_input`, `multiple_choice`, `waiting`, `likert_scale`, `ranking`. For other component types, construct directly: `FocusComponent(type=ComponentType.X, config={...})`. See the [Experiment Authoring guide](https://docs.hyperstudy.io/experimenters/api-access/experiment-authoring) for the full reference.
139
146
 
147
+ ### Randomization and variables (V2 runner)
148
+
149
+ Clusters randomize the order of states and groups once per room while `randomize_states=True`; with it `False` the clusters are kept but rooms run the listed order. Variable definitions hold constants, random draws and computed values. Both need `runtime="v2"` (the server's default for new experiments). On the legacy runner, `randomize_states=True` shuffles each participant's order instead.
150
+
151
+ ```python
152
+ from hyperstudy import Cluster, ClusterRule, Distribution, TransitionRules, VariableDefinition
153
+
154
+ iti = VariableDefinition(
155
+ name="iti_ms", kind="random",
156
+ distribution=Distribution(type="uniform", min=500, max=1500),
157
+ )
158
+ exp = Experiment(
159
+ name="Randomized study",
160
+ runtime="v2",
161
+ randomize_states=True,
162
+ clusters=[Cluster(id="block", rule=ClusterRule(type="shuffle"))],
163
+ variable_definitions=[
164
+ VariableDefinition(name="endowment", kind="constant", value=10),
165
+ VariableDefinition(name="doubled", kind="computed", expr="endowment * 2"),
166
+ iti,
167
+ ],
168
+ states=[
169
+ State(id="a", cluster_id="block", focus_component=show_text("You have {endowment}")),
170
+ State(id="b", cluster_id="block", focus_component=show_text("Doubled: {doubled}")),
171
+ State(id="pause", focus_component=show_text("+"),
172
+ transition_rules=TransitionRules(type="TIMED", variable_id=iti.id)),
173
+ ],
174
+ )
175
+ ```
176
+
177
+ The server refuses a create or update that would make clusters or variables invalid, raising `ValidationError` with the reasons in `err.details["errors"]`.
178
+
140
179
  ## Deployments
141
180
 
142
181
  ```python
@@ -196,6 +235,49 @@ detail = hs.get_agent_decision("room_id", "participantId_3") # full blobs
196
235
  catalog = hs.get_cognition_catalog() # {"abilities": ..., "recipes": ..., "offlineRecipes": ...}
197
236
  ```
198
237
 
238
+ ### Voice agents
239
+
240
+ 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.
241
+
242
+ ```python
243
+ from hyperstudy import (
244
+ PersonaVoice, RealtimeVoice, VoiceTurnTaking, EngineTurnDetection, VoiceTranscription,
245
+ )
246
+
247
+ voice_persona = hs.create_persona(persona=Persona(
248
+ name="Interviewer",
249
+ display_name="Sam", # what other participants see
250
+ provider="openai",
251
+ voice=PersonaVoice(
252
+ engine="realtime",
253
+ preset="marin", # the provider's voice
254
+ speed=1.0,
255
+ style="warm, a little hesitant",
256
+ realtime=RealtimeVoice(provider="openai", model="gpt-realtime-2.1"),
257
+ ),
258
+ ))
259
+
260
+ exp = hs.create_experiment(experiment=Experiment(
261
+ name="Voice interview",
262
+ runtime="v2",
263
+ roles={"interviewer": Role(mode="agent", persona_id=voice_persona["id"])},
264
+ agent_config=AgentConfig(
265
+ voice_turn_taking=VoiceTurnTaking(
266
+ engine_turn_detection=EngineTurnDetection(type="server_vad", silence_duration_ms=800),
267
+ min_response_delay_ms=250,
268
+ ),
269
+ voice_transcription=VoiceTranscription(enabled=True, language="en"),
270
+ ),
271
+ states=[...],
272
+ ))
273
+
274
+ # One room's conversation: every speaker's turns, plus what the engine was told
275
+ conversation = hs.get_voice("room_id", scope="room")
276
+ turns = conversation[conversation["kind"] == "utterance"][["onset", "speaker", "transcript"]]
277
+ ```
278
+
279
+ `update_persona` and `update_experiment` replace a nested block whole, so pass the complete `voice` or `agent_config` when changing one setting in it.
280
+
199
281
  Persona methods need the `read:personas` / `write:personas` API-key scopes; deployment launch/control needs `write:deployments`; agent data reads need `read:events`.
200
282
 
201
283
  ## All Data for a Participant
@@ -203,7 +285,7 @@ Persona methods need the `read:personas` / `write:personas` API-key scopes; depl
203
285
  ```python
204
286
  data = hs.get_all_data("participant_id", room_id="room_id")
205
287
  # Returns dict with keys: events, recordings, chat, videochat, sync,
206
- # ratings_continuous, ratings_sparse, components, eyetracking,
288
+ # ratings_continuous, ratings_sparse, components, eyetracking, voice,
207
289
  # questionnaire, instructions, consent, agent_decisions
208
290
  ```
209
291
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "hyperstudy"
7
- version = "0.4.1"
7
+ version = "0.4.3"
8
8
  description = "Python SDK for the HyperStudy experiment platform API"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -31,19 +31,31 @@ from .exceptions import (
31
31
  )
32
32
  from .models import (
33
33
  AgentConfig,
34
+ Cluster,
35
+ ClusterRule,
34
36
  ComponentType,
35
37
  DisconnectTimeout,
38
+ Distribution,
39
+ EngineTurnDetection,
36
40
  Experiment,
41
+ ExperimenterMessageStyle,
37
42
  FocusComponent,
38
43
  GlobalComponentType,
44
+ Group,
39
45
  Guardrails,
40
46
  InstructionsPage,
41
47
  Persona,
48
+ PersonaChat,
49
+ PersonaVoice,
42
50
  PostExperimentQuestionnaire,
43
51
  PromptLayer,
52
+ RealtimeVoice,
44
53
  Role,
45
54
  State,
46
55
  TransitionRules,
56
+ VariableDefinition,
57
+ VoiceTranscription,
58
+ VoiceTurnTaking,
47
59
  WaitingRoomConfig,
48
60
  likert_scale,
49
61
  multiple_choice,
@@ -56,7 +68,7 @@ from .models import (
56
68
  waiting,
57
69
  )
58
70
 
59
- __version__ = "0.4.1"
71
+ __version__ = "0.4.3"
60
72
 
61
73
  __all__ = [
62
74
  "HyperStudy",
@@ -82,8 +94,20 @@ __all__ = [
82
94
  "AgentConfig",
83
95
  "PromptLayer",
84
96
  "Persona",
97
+ "PersonaVoice",
98
+ "RealtimeVoice",
99
+ "PersonaChat",
85
100
  "Guardrails",
101
+ "VoiceTurnTaking",
102
+ "EngineTurnDetection",
103
+ "VoiceTranscription",
104
+ "ExperimenterMessageStyle",
86
105
  "TransitionRules",
106
+ "Group",
107
+ "Cluster",
108
+ "ClusterRule",
109
+ "VariableDefinition",
110
+ "Distribution",
87
111
  "WaitingRoomConfig",
88
112
  "DisconnectTimeout",
89
113
  "InstructionsPage",
@@ -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,6 +17,7 @@ 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"