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.
- {hyperstudy-0.4.1 → hyperstudy-0.4.3}/.github/workflows/publish.yml +2 -1
- {hyperstudy-0.4.1 → hyperstudy-0.4.3}/.github/workflows/test.yml +2 -1
- {hyperstudy-0.4.1 → hyperstudy-0.4.3}/CHANGELOG.md +41 -0
- {hyperstudy-0.4.1 → hyperstudy-0.4.3}/PKG-INFO +85 -3
- {hyperstudy-0.4.1 → hyperstudy-0.4.3}/README.md +83 -1
- {hyperstudy-0.4.1 → hyperstudy-0.4.3}/pyproject.toml +1 -1
- {hyperstudy-0.4.1 → hyperstudy-0.4.3}/src/hyperstudy/__init__.py +25 -1
- {hyperstudy-0.4.1 → hyperstudy-0.4.3}/src/hyperstudy/_http.py +17 -0
- {hyperstudy-0.4.1 → hyperstudy-0.4.3}/src/hyperstudy/_types.py +1 -0
- {hyperstudy-0.4.1 → hyperstudy-0.4.3}/src/hyperstudy/client.py +134 -14
- {hyperstudy-0.4.1 → hyperstudy-0.4.3}/src/hyperstudy/experiments.py +18 -17
- hyperstudy-0.4.3/src/hyperstudy/models.py +716 -0
- {hyperstudy-0.4.1 → hyperstudy-0.4.3}/src/hyperstudy/personas.py +29 -8
- {hyperstudy-0.4.1 → hyperstudy-0.4.3}/tests/conftest.py +14 -0
- hyperstudy-0.4.3/tests/fixtures/agent_decision_detail_response.json +44 -0
- hyperstudy-0.4.3/tests/fixtures/deployment_single_response.json +22 -0
- hyperstudy-0.4.3/tests/test_agent_authoring.py +190 -0
- {hyperstudy-0.4.1 → hyperstudy-0.4.3}/tests/test_client.py +2 -2
- {hyperstudy-0.4.1 → hyperstudy-0.4.3}/tests/test_deployments_write.py +6 -10
- {hyperstudy-0.4.1 → hyperstudy-0.4.3}/tests/test_http.py +20 -0
- {hyperstudy-0.4.1 → hyperstudy-0.4.3}/tests/test_personas.py +12 -16
- hyperstudy-0.4.3/tests/test_randomization_variables.py +291 -0
- {hyperstudy-0.4.1 → hyperstudy-0.4.3}/tests/test_recordings_download.py +4 -2
- hyperstudy-0.4.3/tests/test_voice_and_filters.py +139 -0
- hyperstudy-0.4.3/tests/test_wire_keys.py +195 -0
- hyperstudy-0.4.1/src/hyperstudy/models.py +0 -438
- hyperstudy-0.4.1/tests/fixtures/agent_decision_detail_response.json +0 -30
- hyperstudy-0.4.1/tests/fixtures/deployment_single_response.json +0 -20
- {hyperstudy-0.4.1 → hyperstudy-0.4.3}/.github/workflows/sync-release-notes.yml +0 -0
- {hyperstudy-0.4.1 → hyperstudy-0.4.3}/.gitignore +0 -0
- {hyperstudy-0.4.1 → hyperstudy-0.4.3}/LICENSE +0 -0
- {hyperstudy-0.4.1 → hyperstudy-0.4.3}/docs/superpowers/specs/2026-04-10-recording-downloads-design.md +0 -0
- {hyperstudy-0.4.1 → hyperstudy-0.4.3}/src/hyperstudy/_dataframe.py +0 -0
- {hyperstudy-0.4.1 → hyperstudy-0.4.3}/src/hyperstudy/_display.py +0 -0
- {hyperstudy-0.4.1 → hyperstudy-0.4.3}/src/hyperstudy/_downloads.py +0 -0
- {hyperstudy-0.4.1 → hyperstudy-0.4.3}/src/hyperstudy/_pagination.py +0 -0
- {hyperstudy-0.4.1 → hyperstudy-0.4.3}/src/hyperstudy/exceptions.py +0 -0
- {hyperstudy-0.4.1 → hyperstudy-0.4.3}/tests/__init__.py +0 -0
- {hyperstudy-0.4.1 → hyperstudy-0.4.3}/tests/fixtures/agent_decisions_response.json +0 -0
- {hyperstudy-0.4.1 → hyperstudy-0.4.3}/tests/fixtures/agent_decisions_truncated_response.json +0 -0
- {hyperstudy-0.4.1 → hyperstudy-0.4.3}/tests/fixtures/agent_runs_response.json +0 -0
- {hyperstudy-0.4.1 → hyperstudy-0.4.3}/tests/fixtures/deployment_sessions_response.json +0 -0
- {hyperstudy-0.4.1 → hyperstudy-0.4.3}/tests/fixtures/deployments_list_response.json +0 -0
- {hyperstudy-0.4.1 → hyperstudy-0.4.3}/tests/fixtures/error_401.json +0 -0
- {hyperstudy-0.4.1 → hyperstudy-0.4.3}/tests/fixtures/error_403.json +0 -0
- {hyperstudy-0.4.1 → hyperstudy-0.4.3}/tests/fixtures/events_response.json +0 -0
- {hyperstudy-0.4.1 → hyperstudy-0.4.3}/tests/fixtures/experiment.schema.json +0 -0
- {hyperstudy-0.4.1 → hyperstudy-0.4.3}/tests/fixtures/experiment_single_response.json +0 -0
- {hyperstudy-0.4.1 → hyperstudy-0.4.3}/tests/fixtures/experiments_list_response.json +0 -0
- {hyperstudy-0.4.1 → hyperstudy-0.4.3}/tests/fixtures/paginated_page1.json +0 -0
- {hyperstudy-0.4.1 → hyperstudy-0.4.3}/tests/fixtures/paginated_page2.json +0 -0
- {hyperstudy-0.4.1 → hyperstudy-0.4.3}/tests/fixtures/pre_experiment_response.json +0 -0
- {hyperstudy-0.4.1 → hyperstudy-0.4.3}/tests/fixtures/recordings_response.json +0 -0
- {hyperstudy-0.4.1 → hyperstudy-0.4.3}/tests/fixtures/sparse_ratings_response.json +0 -0
- {hyperstudy-0.4.1 → hyperstudy-0.4.3}/tests/fixtures/warnings_response.json +0 -0
- {hyperstudy-0.4.1 → hyperstudy-0.4.3}/tests/test_agent_data.py +0 -0
- {hyperstudy-0.4.1 → hyperstudy-0.4.3}/tests/test_dataframe.py +0 -0
- {hyperstudy-0.4.1 → hyperstudy-0.4.3}/tests/test_downloads.py +0 -0
- {hyperstudy-0.4.1 → hyperstudy-0.4.3}/tests/test_experiments.py +0 -0
- {hyperstudy-0.4.1 → hyperstudy-0.4.3}/tests/test_models.py +0 -0
- {hyperstudy-0.4.1 → hyperstudy-0.4.3}/tests/test_pagination.py +0 -0
|
@@ -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.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: hyperstudy
|
|
3
|
-
Version: 0.4.
|
|
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
|
|
|
@@ -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.
|
|
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
|
|