hyperstudy 0.4.1__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.
- {hyperstudy-0.4.1 → hyperstudy-0.4.2}/.github/workflows/publish.yml +2 -1
- {hyperstudy-0.4.1 → hyperstudy-0.4.2}/.github/workflows/test.yml +2 -1
- {hyperstudy-0.4.1 → hyperstudy-0.4.2}/CHANGELOG.md +27 -0
- {hyperstudy-0.4.1 → hyperstudy-0.4.2}/PKG-INFO +53 -3
- {hyperstudy-0.4.1 → hyperstudy-0.4.2}/README.md +51 -1
- {hyperstudy-0.4.1 → hyperstudy-0.4.2}/pyproject.toml +1 -1
- {hyperstudy-0.4.1 → hyperstudy-0.4.2}/src/hyperstudy/__init__.py +15 -1
- {hyperstudy-0.4.1 → hyperstudy-0.4.2}/src/hyperstudy/_http.py +17 -0
- {hyperstudy-0.4.1 → hyperstudy-0.4.2}/src/hyperstudy/_types.py +1 -0
- {hyperstudy-0.4.1 → hyperstudy-0.4.2}/src/hyperstudy/client.py +134 -14
- {hyperstudy-0.4.1 → hyperstudy-0.4.2}/src/hyperstudy/experiments.py +13 -17
- {hyperstudy-0.4.1 → hyperstudy-0.4.2}/src/hyperstudy/models.py +176 -23
- {hyperstudy-0.4.1 → hyperstudy-0.4.2}/src/hyperstudy/personas.py +29 -8
- {hyperstudy-0.4.1 → hyperstudy-0.4.2}/tests/conftest.py +14 -0
- hyperstudy-0.4.2/tests/fixtures/agent_decision_detail_response.json +44 -0
- hyperstudy-0.4.2/tests/fixtures/deployment_single_response.json +22 -0
- hyperstudy-0.4.2/tests/test_agent_authoring.py +190 -0
- {hyperstudy-0.4.1 → hyperstudy-0.4.2}/tests/test_client.py +2 -2
- {hyperstudy-0.4.1 → hyperstudy-0.4.2}/tests/test_deployments_write.py +6 -10
- {hyperstudy-0.4.1 → hyperstudy-0.4.2}/tests/test_http.py +20 -0
- {hyperstudy-0.4.1 → hyperstudy-0.4.2}/tests/test_personas.py +12 -16
- {hyperstudy-0.4.1 → hyperstudy-0.4.2}/tests/test_recordings_download.py +4 -2
- hyperstudy-0.4.2/tests/test_voice_and_filters.py +139 -0
- hyperstudy-0.4.2/tests/test_wire_keys.py +195 -0
- 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.2}/.github/workflows/sync-release-notes.yml +0 -0
- {hyperstudy-0.4.1 → hyperstudy-0.4.2}/.gitignore +0 -0
- {hyperstudy-0.4.1 → hyperstudy-0.4.2}/LICENSE +0 -0
- {hyperstudy-0.4.1 → hyperstudy-0.4.2}/docs/superpowers/specs/2026-04-10-recording-downloads-design.md +0 -0
- {hyperstudy-0.4.1 → hyperstudy-0.4.2}/src/hyperstudy/_dataframe.py +0 -0
- {hyperstudy-0.4.1 → hyperstudy-0.4.2}/src/hyperstudy/_display.py +0 -0
- {hyperstudy-0.4.1 → hyperstudy-0.4.2}/src/hyperstudy/_downloads.py +0 -0
- {hyperstudy-0.4.1 → hyperstudy-0.4.2}/src/hyperstudy/_pagination.py +0 -0
- {hyperstudy-0.4.1 → hyperstudy-0.4.2}/src/hyperstudy/exceptions.py +0 -0
- {hyperstudy-0.4.1 → hyperstudy-0.4.2}/tests/__init__.py +0 -0
- {hyperstudy-0.4.1 → hyperstudy-0.4.2}/tests/fixtures/agent_decisions_response.json +0 -0
- {hyperstudy-0.4.1 → hyperstudy-0.4.2}/tests/fixtures/agent_decisions_truncated_response.json +0 -0
- {hyperstudy-0.4.1 → hyperstudy-0.4.2}/tests/fixtures/agent_runs_response.json +0 -0
- {hyperstudy-0.4.1 → hyperstudy-0.4.2}/tests/fixtures/deployment_sessions_response.json +0 -0
- {hyperstudy-0.4.1 → hyperstudy-0.4.2}/tests/fixtures/deployments_list_response.json +0 -0
- {hyperstudy-0.4.1 → hyperstudy-0.4.2}/tests/fixtures/error_401.json +0 -0
- {hyperstudy-0.4.1 → hyperstudy-0.4.2}/tests/fixtures/error_403.json +0 -0
- {hyperstudy-0.4.1 → hyperstudy-0.4.2}/tests/fixtures/events_response.json +0 -0
- {hyperstudy-0.4.1 → hyperstudy-0.4.2}/tests/fixtures/experiment.schema.json +0 -0
- {hyperstudy-0.4.1 → hyperstudy-0.4.2}/tests/fixtures/experiment_single_response.json +0 -0
- {hyperstudy-0.4.1 → hyperstudy-0.4.2}/tests/fixtures/experiments_list_response.json +0 -0
- {hyperstudy-0.4.1 → hyperstudy-0.4.2}/tests/fixtures/paginated_page1.json +0 -0
- {hyperstudy-0.4.1 → hyperstudy-0.4.2}/tests/fixtures/paginated_page2.json +0 -0
- {hyperstudy-0.4.1 → hyperstudy-0.4.2}/tests/fixtures/pre_experiment_response.json +0 -0
- {hyperstudy-0.4.1 → hyperstudy-0.4.2}/tests/fixtures/recordings_response.json +0 -0
- {hyperstudy-0.4.1 → hyperstudy-0.4.2}/tests/fixtures/sparse_ratings_response.json +0 -0
- {hyperstudy-0.4.1 → hyperstudy-0.4.2}/tests/fixtures/warnings_response.json +0 -0
- {hyperstudy-0.4.1 → hyperstudy-0.4.2}/tests/test_agent_data.py +0 -0
- {hyperstudy-0.4.1 → hyperstudy-0.4.2}/tests/test_dataframe.py +0 -0
- {hyperstudy-0.4.1 → hyperstudy-0.4.2}/tests/test_downloads.py +0 -0
- {hyperstudy-0.4.1 → hyperstudy-0.4.2}/tests/test_experiments.py +0 -0
- {hyperstudy-0.4.1 → hyperstudy-0.4.2}/tests/test_models.py +0 -0
- {hyperstudy-0.4.1 → hyperstudy-0.4.2}/tests/test_pagination.py +0 -0
|
@@ -1,5 +1,32 @@
|
|
|
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
|
+
|
|
3
30
|
## v0.4.1
|
|
4
31
|
|
|
5
32
|
### 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.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,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
|
|
@@ -231,6 +238,49 @@ detail = hs.get_agent_decision("room_id", "participantId_3") # full blobs
|
|
|
231
238
|
catalog = hs.get_cognition_catalog() # {"abilities": ..., "recipes": ..., "offlineRecipes": ...}
|
|
232
239
|
```
|
|
233
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
|
+
|
|
234
284
|
Persona methods need the `read:personas` / `write:personas` API-key scopes; deployment launch/control needs `write:deployments`; agent data reads need `read:events`.
|
|
235
285
|
|
|
236
286
|
## All Data for a Participant
|
|
@@ -238,7 +288,7 @@ Persona methods need the `read:personas` / `write:personas` API-key scopes; depl
|
|
|
238
288
|
```python
|
|
239
289
|
data = hs.get_all_data("participant_id", room_id="room_id")
|
|
240
290
|
# Returns dict with keys: events, recordings, chat, videochat, sync,
|
|
241
|
-
# ratings_continuous, ratings_sparse, components, eyetracking,
|
|
291
|
+
# ratings_continuous, ratings_sparse, components, eyetracking, voice,
|
|
242
292
|
# questionnaire, instructions, consent, agent_decisions
|
|
243
293
|
```
|
|
244
294
|
|
|
@@ -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
|
|
@@ -196,6 +203,49 @@ detail = hs.get_agent_decision("room_id", "participantId_3") # full blobs
|
|
|
196
203
|
catalog = hs.get_cognition_catalog() # {"abilities": ..., "recipes": ..., "offlineRecipes": ...}
|
|
197
204
|
```
|
|
198
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
|
+
|
|
199
249
|
Persona methods need the `read:personas` / `write:personas` API-key scopes; deployment launch/control needs `write:deployments`; agent data reads need `read:events`.
|
|
200
250
|
|
|
201
251
|
## All Data for a Participant
|
|
@@ -203,7 +253,7 @@ Persona methods need the `read:personas` / `write:personas` API-key scopes; depl
|
|
|
203
253
|
```python
|
|
204
254
|
data = hs.get_all_data("participant_id", room_id="room_id")
|
|
205
255
|
# Returns dict with keys: events, recordings, chat, videochat, sync,
|
|
206
|
-
# ratings_continuous, ratings_sparse, components, eyetracking,
|
|
256
|
+
# ratings_continuous, ratings_sparse, components, eyetracking, voice,
|
|
207
257
|
# questionnaire, instructions, consent, agent_decisions
|
|
208
258
|
```
|
|
209
259
|
|
|
@@ -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.
|
|
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
|
|
|
@@ -10,7 +10,7 @@ from tqdm.auto import tqdm
|
|
|
10
10
|
|
|
11
11
|
from ._dataframe import to_pandas, to_polars
|
|
12
12
|
from ._downloads import build_filename, download_file, get_download_url
|
|
13
|
-
from ._http import HttpTransport
|
|
13
|
+
from ._http import HttpTransport, single_record
|
|
14
14
|
from ._pagination import fetch_all_pages
|
|
15
15
|
from ._types import Scope
|
|
16
16
|
from .exceptions import HyperStudyError
|
|
@@ -146,6 +146,69 @@ class HyperStudy(ExperimentMixin, PersonaMixin):
|
|
|
146
146
|
output=output, progress=progress,
|
|
147
147
|
)
|
|
148
148
|
|
|
149
|
+
def get_voice(
|
|
150
|
+
self,
|
|
151
|
+
scope_id: str,
|
|
152
|
+
*,
|
|
153
|
+
scope: str = "experiment",
|
|
154
|
+
deployment_id: str | None = None,
|
|
155
|
+
room_id: str | None = None,
|
|
156
|
+
include_context: bool = True,
|
|
157
|
+
order: str | None = None,
|
|
158
|
+
limit: int | None = None,
|
|
159
|
+
offset: int = 0,
|
|
160
|
+
output: str = "pandas",
|
|
161
|
+
progress: bool = True,
|
|
162
|
+
):
|
|
163
|
+
"""Fetch voice agent conversations, one flat row per voice event.
|
|
164
|
+
|
|
165
|
+
``kind`` says what a row is:
|
|
166
|
+
|
|
167
|
+
- ``utterance``: a speech turn that reached the room. ``speaker`` is
|
|
168
|
+
``participant`` or ``agent``; ``transcript`` is what was said (for an
|
|
169
|
+
agent, the generated text).
|
|
170
|
+
- ``discarded``: a false start, or an agent reply dropped before any
|
|
171
|
+
of it was heard.
|
|
172
|
+
- ``response``: an agent reply that acted without speaking, such as
|
|
173
|
+
answering an on-screen question (see ``toolCalls``).
|
|
174
|
+
- ``context``: what the voice engine was told (``contextKind``,
|
|
175
|
+
``contextText``).
|
|
176
|
+
- ``connection``: the engine connection was lost, restored or failed
|
|
177
|
+
(``connectionEvent``); the study was paused meanwhile.
|
|
178
|
+
|
|
179
|
+
Agent rows also report how much of the generated speech the room
|
|
180
|
+
heard (``heardMs``, ``generatedMs``, ``truncated``) and
|
|
181
|
+
``responseLatencyMs``, from the participant's end of speech to the
|
|
182
|
+
agent's first audio.
|
|
183
|
+
|
|
184
|
+
Participant scope returns only that participant's own events. For a
|
|
185
|
+
whole conversation, every speaker plus what the engine was told, use
|
|
186
|
+
``scope="room"``. Requires the ``read:events`` scope.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
scope_id: ID of the experiment, room, or participant.
|
|
190
|
+
scope: ``"experiment"``, ``"room"``, or ``"participant"``.
|
|
191
|
+
deployment_id: Filter by deployment (experiment scope only).
|
|
192
|
+
room_id: Narrow a participant's events to one room.
|
|
193
|
+
include_context: Include ``context`` rows (default True).
|
|
194
|
+
order: ``"asc"`` (default) or ``"desc"`` by time.
|
|
195
|
+
limit: Max records. ``None`` fetches all pages.
|
|
196
|
+
offset: Starting offset.
|
|
197
|
+
output: ``"pandas"`` (default), ``"polars"``, or ``"dict"``.
|
|
198
|
+
progress: Show progress bar when paginating.
|
|
199
|
+
"""
|
|
200
|
+
extra: dict[str, Any] = {}
|
|
201
|
+
if not include_context:
|
|
202
|
+
extra["includeContext"] = "false"
|
|
203
|
+
return self._fetch_data(
|
|
204
|
+
"voice", scope_id,
|
|
205
|
+
scope=scope, deployment_id=deployment_id, room_id=room_id,
|
|
206
|
+
order=order,
|
|
207
|
+
limit=limit, offset=offset,
|
|
208
|
+
output=output, progress=progress,
|
|
209
|
+
**extra,
|
|
210
|
+
)
|
|
211
|
+
|
|
149
212
|
def get_videochat(
|
|
150
213
|
self,
|
|
151
214
|
scope_id: str,
|
|
@@ -271,17 +334,41 @@ class HyperStudy(ExperimentMixin, PersonaMixin):
|
|
|
271
334
|
scope: str = "experiment",
|
|
272
335
|
deployment_id: str | None = None,
|
|
273
336
|
room_id: str | None = None,
|
|
337
|
+
component_type: str | None = None,
|
|
338
|
+
state_id: str | None = None,
|
|
339
|
+
completed: bool | None = None,
|
|
340
|
+
start_time: str | None = None,
|
|
341
|
+
end_time: str | None = None,
|
|
342
|
+
sort: str | None = None,
|
|
343
|
+
order: str | None = None,
|
|
274
344
|
limit: int | None = None,
|
|
275
345
|
offset: int = 0,
|
|
276
346
|
output: str = "pandas",
|
|
277
347
|
progress: bool = True,
|
|
278
348
|
):
|
|
279
|
-
"""Fetch component response data.
|
|
349
|
+
"""Fetch component response data.
|
|
350
|
+
|
|
351
|
+
Args:
|
|
352
|
+
component_type: Only this component type (e.g. ``"vasrating"``).
|
|
353
|
+
state_id: Only components shown in this state.
|
|
354
|
+
completed: Only completed (True) or uncompleted (False) components.
|
|
355
|
+
*: All other args are the same as :meth:`get_events`.
|
|
356
|
+
"""
|
|
357
|
+
extra: dict[str, Any] = {}
|
|
358
|
+
if component_type:
|
|
359
|
+
extra["componentType"] = component_type
|
|
360
|
+
if state_id:
|
|
361
|
+
extra["stateId"] = state_id
|
|
362
|
+
if completed is not None:
|
|
363
|
+
extra["completed"] = "true" if completed else "false"
|
|
280
364
|
return self._fetch_data(
|
|
281
365
|
"components", scope_id,
|
|
282
366
|
scope=scope, deployment_id=deployment_id, room_id=room_id,
|
|
367
|
+
start_time=start_time, end_time=end_time,
|
|
368
|
+
sort=sort, order=order,
|
|
283
369
|
limit=limit, offset=offset,
|
|
284
370
|
output=output, progress=progress,
|
|
371
|
+
**extra,
|
|
285
372
|
)
|
|
286
373
|
|
|
287
374
|
def get_participants(
|
|
@@ -291,17 +378,32 @@ class HyperStudy(ExperimentMixin, PersonaMixin):
|
|
|
291
378
|
scope: str = "experiment",
|
|
292
379
|
deployment_id: str | None = None,
|
|
293
380
|
room_id: str | None = None,
|
|
381
|
+
completion_code: str | None = None,
|
|
382
|
+
sort: str | None = None,
|
|
383
|
+
order: str | None = None,
|
|
294
384
|
limit: int | None = None,
|
|
295
385
|
offset: int = 0,
|
|
296
386
|
output: str = "pandas",
|
|
297
387
|
progress: bool = True,
|
|
298
388
|
):
|
|
299
|
-
"""Fetch participant data.
|
|
389
|
+
"""Fetch participant data.
|
|
390
|
+
|
|
391
|
+
Args:
|
|
392
|
+
completion_code: Only participants with this outcome: ``SUCCESS``,
|
|
393
|
+
``TIMEOUT``, ``TECHNICAL``, ``NOCONSENT``, ``ABANDONED``, or the
|
|
394
|
+
derived ``IN_PROGRESS`` / ``COMPLETED_NO_CODE``.
|
|
395
|
+
*: All other args are the same as :meth:`get_events`.
|
|
396
|
+
"""
|
|
397
|
+
extra: dict[str, Any] = {}
|
|
398
|
+
if completion_code:
|
|
399
|
+
extra["completionCode"] = completion_code
|
|
300
400
|
return self._fetch_data(
|
|
301
401
|
"participants", scope_id,
|
|
302
402
|
scope=scope, deployment_id=deployment_id, room_id=room_id,
|
|
403
|
+
sort=sort, order=order,
|
|
303
404
|
limit=limit, offset=offset,
|
|
304
405
|
output=output, progress=progress,
|
|
406
|
+
**extra,
|
|
305
407
|
)
|
|
306
408
|
|
|
307
409
|
def get_rooms(
|
|
@@ -310,15 +412,23 @@ class HyperStudy(ExperimentMixin, PersonaMixin):
|
|
|
310
412
|
*,
|
|
311
413
|
scope: str = "experiment",
|
|
312
414
|
deployment_id: str | None = None,
|
|
415
|
+
sort: str | None = None,
|
|
416
|
+
order: str | None = None,
|
|
313
417
|
limit: int | None = None,
|
|
314
418
|
offset: int = 0,
|
|
315
419
|
output: str = "pandas",
|
|
316
420
|
progress: bool = True,
|
|
317
421
|
):
|
|
318
|
-
"""Fetch room/session data.
|
|
422
|
+
"""Fetch room/session data.
|
|
423
|
+
|
|
424
|
+
Each room reports ``completionReason`` (how it ended: ``completed``,
|
|
425
|
+
or an end reason such as ``ABANDONED`` or ``disconnect_timeout``) and
|
|
426
|
+
``agentStartFailures`` (why its agents could not start, by role).
|
|
427
|
+
"""
|
|
319
428
|
return self._fetch_data(
|
|
320
429
|
"rooms", scope_id,
|
|
321
430
|
scope=scope, deployment_id=deployment_id,
|
|
431
|
+
sort=sort, order=order,
|
|
322
432
|
limit=limit, offset=offset,
|
|
323
433
|
output=output, progress=progress,
|
|
324
434
|
)
|
|
@@ -381,8 +491,18 @@ class HyperStudy(ExperimentMixin, PersonaMixin):
|
|
|
381
491
|
body = self._transport.get(
|
|
382
492
|
f"data/counts/participant/{participant_id}", params={"roomId": room_id}
|
|
383
493
|
)
|
|
384
|
-
|
|
385
|
-
|
|
494
|
+
return single_record(body)
|
|
495
|
+
|
|
496
|
+
def check_data_access(self, experiment_id: str) -> dict[str, bool]:
|
|
497
|
+
"""Check whether you may view and export an experiment's data.
|
|
498
|
+
|
|
499
|
+
Returns no data, so it is a cheap check before a large fetch.
|
|
500
|
+
|
|
501
|
+
Returns:
|
|
502
|
+
``{"canView": bool, "canExport": bool}``
|
|
503
|
+
"""
|
|
504
|
+
body = self._transport.get(f"data/check-access/experiment/{experiment_id}")
|
|
505
|
+
return {"canView": body["canView"], "canExport": body["canExport"]}
|
|
386
506
|
|
|
387
507
|
# ------------------------------------------------------------------
|
|
388
508
|
# Convenience: category-filtered events
|
|
@@ -513,8 +633,7 @@ class HyperStudy(ExperimentMixin, PersonaMixin):
|
|
|
513
633
|
Deployment dict.
|
|
514
634
|
"""
|
|
515
635
|
body = self._transport.get(f"deployments/{deployment_id}")
|
|
516
|
-
|
|
517
|
-
return data[0] if isinstance(data, list) and data else data
|
|
636
|
+
return single_record(body)
|
|
518
637
|
|
|
519
638
|
def get_deployment_sessions(
|
|
520
639
|
self,
|
|
@@ -573,12 +692,12 @@ class HyperStudy(ExperimentMixin, PersonaMixin):
|
|
|
573
692
|
body = self._transport.post(
|
|
574
693
|
"deployments", json={"experimentId": experiment_id, "config": merged}
|
|
575
694
|
)
|
|
576
|
-
return body
|
|
695
|
+
return single_record(body)
|
|
577
696
|
|
|
578
697
|
def get_agent_spend(self, deployment_id: str) -> dict[str, Any]:
|
|
579
698
|
"""Total + per-room agent LLM spend for an agent-only deployment."""
|
|
580
699
|
body = self._transport.get(f"deployments/{deployment_id}/agent-spend")
|
|
581
|
-
return body
|
|
700
|
+
return single_record(body)
|
|
582
701
|
|
|
583
702
|
def run_more(
|
|
584
703
|
self, deployment_id: str, *, rooms: int, budget_usd: float
|
|
@@ -598,7 +717,7 @@ class HyperStudy(ExperimentMixin, PersonaMixin):
|
|
|
598
717
|
f"deployments/{deployment_id}/run-more",
|
|
599
718
|
json={"rooms": rooms, "budgetUsd": budget_usd},
|
|
600
719
|
)
|
|
601
|
-
return body
|
|
720
|
+
return single_record(body)
|
|
602
721
|
|
|
603
722
|
def stop_room(self, deployment_id: str, room_id: str) -> None:
|
|
604
723
|
"""Force-end a running agent room."""
|
|
@@ -609,7 +728,7 @@ class HyperStudy(ExperimentMixin, PersonaMixin):
|
|
|
609
728
|
body = self._transport.post(
|
|
610
729
|
f"deployments/{deployment_id}/rooms/{room_id}/retry"
|
|
611
730
|
)
|
|
612
|
-
return body
|
|
731
|
+
return single_record(body)
|
|
613
732
|
|
|
614
733
|
# ------------------------------------------------------------------
|
|
615
734
|
# Agent data — decisions and run manifests
|
|
@@ -681,7 +800,7 @@ class HyperStudy(ExperimentMixin, PersonaMixin):
|
|
|
681
800
|
body = self._transport.get(
|
|
682
801
|
f"data/agent-decisions/room/{room_id}/decision/{decision_id}"
|
|
683
802
|
)
|
|
684
|
-
return body
|
|
803
|
+
return single_record(body)
|
|
685
804
|
|
|
686
805
|
def get_agent_runs(
|
|
687
806
|
self,
|
|
@@ -730,6 +849,7 @@ class HyperStudy(ExperimentMixin, PersonaMixin):
|
|
|
730
849
|
"ratings_sparse": self.get_ratings(participant_id, kind="sparse", **common),
|
|
731
850
|
"components": self.get_components(participant_id, **common),
|
|
732
851
|
"eyetracking": self.get_eyetracking(participant_id, **common),
|
|
852
|
+
"voice": self.get_voice(participant_id, **common),
|
|
733
853
|
"questionnaire": self.get_questionnaire(participant_id, **common),
|
|
734
854
|
"instructions": self.get_instructions(participant_id, **common),
|
|
735
855
|
"consent": self.get_consent(participant_id, **common),
|
|
@@ -752,7 +872,7 @@ class HyperStudy(ExperimentMixin, PersonaMixin):
|
|
|
752
872
|
download_path = recording.get("downloadPath")
|
|
753
873
|
if download_path:
|
|
754
874
|
body = self._transport.get(download_path)
|
|
755
|
-
return (body
|
|
875
|
+
return single_record(body).get("url")
|
|
756
876
|
return get_download_url(recording)
|
|
757
877
|
|
|
758
878
|
def download_recording(
|