hyperstudy 0.4.0__py3-none-any.whl → 0.4.2__py3-none-any.whl

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/__init__.py CHANGED
@@ -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",
hyperstudy/_http.py CHANGED
@@ -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
 
hyperstudy/_types.py CHANGED
@@ -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"
hyperstudy/client.py CHANGED
@@ -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,
@@ -235,6 +298,35 @@ class HyperStudy(ExperimentMixin, PersonaMixin):
235
298
  output=output, progress=progress,
236
299
  )
237
300
 
301
+ def get_eyetracking(
302
+ self,
303
+ scope_id: str,
304
+ *,
305
+ scope: str = "experiment",
306
+ room_id: str | None = None,
307
+ limit: int | None = None,
308
+ offset: int = 0,
309
+ output: str = "pandas",
310
+ progress: bool = True,
311
+ ):
312
+ """Fetch eye-tracking gaze data.
313
+
314
+ Args:
315
+ scope_id: ID of the experiment, room, or participant.
316
+ scope: ``"experiment"``, ``"room"``, or ``"participant"``.
317
+ room_id: Required when ``scope="participant"``.
318
+ limit: Max records. ``None`` fetches all pages.
319
+ offset: Starting offset.
320
+ output: ``"pandas"`` (default), ``"polars"``, or ``"dict"``.
321
+ progress: Show progress bar when paginating.
322
+ """
323
+ return self._fetch_data(
324
+ "eyetracking", scope_id,
325
+ scope=scope, room_id=room_id,
326
+ limit=limit, offset=offset,
327
+ output=output, progress=progress,
328
+ )
329
+
238
330
  def get_components(
239
331
  self,
240
332
  scope_id: str,
@@ -242,17 +334,41 @@ class HyperStudy(ExperimentMixin, PersonaMixin):
242
334
  scope: str = "experiment",
243
335
  deployment_id: str | None = None,
244
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,
245
344
  limit: int | None = None,
246
345
  offset: int = 0,
247
346
  output: str = "pandas",
248
347
  progress: bool = True,
249
348
  ):
250
- """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"
251
364
  return self._fetch_data(
252
365
  "components", scope_id,
253
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,
254
369
  limit=limit, offset=offset,
255
370
  output=output, progress=progress,
371
+ **extra,
256
372
  )
257
373
 
258
374
  def get_participants(
@@ -262,17 +378,32 @@ class HyperStudy(ExperimentMixin, PersonaMixin):
262
378
  scope: str = "experiment",
263
379
  deployment_id: str | None = None,
264
380
  room_id: str | None = None,
381
+ completion_code: str | None = None,
382
+ sort: str | None = None,
383
+ order: str | None = None,
265
384
  limit: int | None = None,
266
385
  offset: int = 0,
267
386
  output: str = "pandas",
268
387
  progress: bool = True,
269
388
  ):
270
- """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
271
400
  return self._fetch_data(
272
401
  "participants", scope_id,
273
402
  scope=scope, deployment_id=deployment_id, room_id=room_id,
403
+ sort=sort, order=order,
274
404
  limit=limit, offset=offset,
275
405
  output=output, progress=progress,
406
+ **extra,
276
407
  )
277
408
 
278
409
  def get_rooms(
@@ -281,19 +412,98 @@ class HyperStudy(ExperimentMixin, PersonaMixin):
281
412
  *,
282
413
  scope: str = "experiment",
283
414
  deployment_id: str | None = None,
415
+ sort: str | None = None,
416
+ order: str | None = None,
284
417
  limit: int | None = None,
285
418
  offset: int = 0,
286
419
  output: str = "pandas",
287
420
  progress: bool = True,
288
421
  ):
289
- """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
+ """
290
428
  return self._fetch_data(
291
429
  "rooms", scope_id,
292
430
  scope=scope, deployment_id=deployment_id,
431
+ sort=sort, order=order,
293
432
  limit=limit, offset=offset,
294
433
  output=output, progress=progress,
295
434
  )
296
435
 
436
+ def get_variables(
437
+ self,
438
+ room_id: str,
439
+ *,
440
+ output: str = "pandas",
441
+ ):
442
+ """Fetch the reconstructed shared-variable timeline for a room.
443
+
444
+ Returns a dict with four keys:
445
+
446
+ - ``writes`` — the time-ordered write log (one row per variable
447
+ write: seeded constant, human component response, or agent
448
+ submit-response, tagged with source and persisted flag), converted
449
+ per ``output``
450
+ - ``timeline`` — the derived per-state forward-filled variable
451
+ snapshot matrix, converted per ``output``
452
+ - ``variable_names`` — ordered list of every variable seen
453
+ - ``matrix_columns`` — ordered column set of the timeline matrix
454
+ - ``dropped_writes`` — writes that failed the server's ground-truth
455
+ cross-checks (a bug signal; empty is the healthy state)
456
+ - ``mode`` — the reconstruction mode the server processor ran in
457
+
458
+ Args:
459
+ room_id: Room ID.
460
+ output: ``"pandas"`` (default), ``"polars"``, or ``"dict"``
461
+ for the tabular values.
462
+ """
463
+ body = self._transport.get(f"data/variables/room/{room_id}")
464
+ metadata = body.get("metadata") or {}
465
+ return {
466
+ "writes": self._convert_output(body.get("data", []), output),
467
+ "timeline": self._convert_output(metadata.get("timeline") or [], output),
468
+ "variable_names": metadata.get("variableNames") or [],
469
+ "matrix_columns": metadata.get("matrixColumns") or [],
470
+ "dropped_writes": metadata.get("droppedWrites") or [],
471
+ "mode": metadata.get("mode"),
472
+ }
473
+
474
+ def get_counts(
475
+ self,
476
+ participant_id: str,
477
+ room_id: str,
478
+ ) -> dict[str, Any]:
479
+ """Fetch per-data-type document counts for a participant in a room.
480
+
481
+ Cheap count queries grouped by data type — useful for checking
482
+ which data types exist before fetching them.
483
+
484
+ Args:
485
+ participant_id: Participant ID.
486
+ room_id: Room ID.
487
+
488
+ Returns:
489
+ Dict with ``counts`` and ``hasData`` keyed by data type.
490
+ """
491
+ body = self._transport.get(
492
+ f"data/counts/participant/{participant_id}", params={"roomId": room_id}
493
+ )
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"]}
506
+
297
507
  # ------------------------------------------------------------------
298
508
  # Convenience: category-filtered events
299
509
  # ------------------------------------------------------------------
@@ -423,8 +633,7 @@ class HyperStudy(ExperimentMixin, PersonaMixin):
423
633
  Deployment dict.
424
634
  """
425
635
  body = self._transport.get(f"deployments/{deployment_id}")
426
- data = body.get("data", [])
427
- return data[0] if isinstance(data, list) and data else data
636
+ return single_record(body)
428
637
 
429
638
  def get_deployment_sessions(
430
639
  self,
@@ -483,12 +692,12 @@ class HyperStudy(ExperimentMixin, PersonaMixin):
483
692
  body = self._transport.post(
484
693
  "deployments", json={"experimentId": experiment_id, "config": merged}
485
694
  )
486
- return body.get("data", {})
695
+ return single_record(body)
487
696
 
488
697
  def get_agent_spend(self, deployment_id: str) -> dict[str, Any]:
489
698
  """Total + per-room agent LLM spend for an agent-only deployment."""
490
699
  body = self._transport.get(f"deployments/{deployment_id}/agent-spend")
491
- return body.get("data", {})
700
+ return single_record(body)
492
701
 
493
702
  def run_more(
494
703
  self, deployment_id: str, *, rooms: int, budget_usd: float
@@ -508,7 +717,7 @@ class HyperStudy(ExperimentMixin, PersonaMixin):
508
717
  f"deployments/{deployment_id}/run-more",
509
718
  json={"rooms": rooms, "budgetUsd": budget_usd},
510
719
  )
511
- return body.get("data", {})
720
+ return single_record(body)
512
721
 
513
722
  def stop_room(self, deployment_id: str, room_id: str) -> None:
514
723
  """Force-end a running agent room."""
@@ -519,7 +728,7 @@ class HyperStudy(ExperimentMixin, PersonaMixin):
519
728
  body = self._transport.post(
520
729
  f"deployments/{deployment_id}/rooms/{room_id}/retry"
521
730
  )
522
- return body.get("data", {})
731
+ return single_record(body)
523
732
 
524
733
  # ------------------------------------------------------------------
525
734
  # Agent data — decisions and run manifests
@@ -591,7 +800,7 @@ class HyperStudy(ExperimentMixin, PersonaMixin):
591
800
  body = self._transport.get(
592
801
  f"data/agent-decisions/room/{room_id}/decision/{decision_id}"
593
802
  )
594
- return body.get("data", {})
803
+ return single_record(body)
595
804
 
596
805
  def get_agent_runs(
597
806
  self,
@@ -639,6 +848,8 @@ class HyperStudy(ExperimentMixin, PersonaMixin):
639
848
  "ratings_continuous": self.get_ratings(participant_id, kind="continuous", **common),
640
849
  "ratings_sparse": self.get_ratings(participant_id, kind="sparse", **common),
641
850
  "components": self.get_components(participant_id, **common),
851
+ "eyetracking": self.get_eyetracking(participant_id, **common),
852
+ "voice": self.get_voice(participant_id, **common),
642
853
  "questionnaire": self.get_questionnaire(participant_id, **common),
643
854
  "instructions": self.get_instructions(participant_id, **common),
644
855
  "consent": self.get_consent(participant_id, **common),
@@ -661,7 +872,7 @@ class HyperStudy(ExperimentMixin, PersonaMixin):
661
872
  download_path = recording.get("downloadPath")
662
873
  if download_path:
663
874
  body = self._transport.get(download_path)
664
- return (body.get("data") or {}).get("url")
875
+ return single_record(body).get("url")
665
876
  return get_download_url(recording)
666
877
 
667
878
  def download_recording(
hyperstudy/experiments.py CHANGED
@@ -5,10 +5,9 @@ from __future__ import annotations
5
5
  from typing import TYPE_CHECKING, Any
6
6
 
7
7
  from pydantic import BaseModel
8
- from pydantic.alias_generators import to_camel
9
8
 
10
9
  from ._display import ExperimentInfo
11
- from ._http import HttpTransport
10
+ from ._http import HttpTransport, single_record
12
11
  from ._pagination import fetch_all_pages
13
12
  from .models import camelize_wire
14
13
 
@@ -43,9 +42,7 @@ def _build_experiment_payload(
43
42
  raise TypeError(
44
43
  f"experiment must be an Experiment or dict, got {type(experiment).__name__}"
45
44
  )
46
- for key, value in overrides.items():
47
- ck = to_camel(key)
48
- payload[ck] = camelize_wire(value, _parent_key=ck)
45
+ payload.update(camelize_wire(overrides))
49
46
  return payload
50
47
 
51
48
 
@@ -105,15 +102,23 @@ class ExperimentMixin:
105
102
  An :class:`ExperimentInfo` object (dict-like, with ``_repr_html_``).
106
103
  """
107
104
  body = self._transport.get(f"experiments/{experiment_id}")
108
- data = body.get("data", [])
109
- record = data[0] if isinstance(data, list) and data else data
105
+ record = single_record(body)
110
106
  return ExperimentInfo(record)
111
107
 
112
108
  def get_experiment_config(self, experiment_id: str) -> dict[str, Any]:
113
109
  """Get raw experiment config (lightweight, no enrichment)."""
114
110
  body = self._transport.get(f"experiments/{experiment_id}/config")
115
- data = body.get("data", [])
116
- return data[0] if isinstance(data, list) and data else data
111
+ return single_record(body)
112
+
113
+ def export_experiment(self, experiment_id: str) -> dict[str, Any]:
114
+ """Export an experiment as portable JSON.
115
+
116
+ Returns:
117
+ The export payload (experiment definition plus media-asset
118
+ manifest), suitable for re-import.
119
+ """
120
+ body = self._transport.get(f"experiments/{experiment_id}/export")
121
+ return single_record(body)
117
122
 
118
123
  # ------------------------------------------------------------------
119
124
  # Write
@@ -160,8 +165,7 @@ class ExperimentMixin:
160
165
  "(pass name=... or experiment=Experiment(name=...))"
161
166
  )
162
167
  body = self._transport.post("experiments", json=payload)
163
- data = body.get("data", [])
164
- record = data[0] if isinstance(data, list) and data else data
168
+ record = single_record(body)
165
169
  return ExperimentInfo(record)
166
170
 
167
171
  def update_experiment(
@@ -173,6 +177,11 @@ class ExperimentMixin:
173
177
  ) -> dict[str, Any]:
174
178
  """Update an experiment.
175
179
 
180
+ Each top-level field you pass replaces the stored one whole: passing
181
+ ``agent_config=AgentConfig(voice_turn_taking=...)`` also clears any
182
+ stored ``role_overrides``, so send the complete ``agent_config``.
183
+ Only the owner or an organization admin may change ``visibility``.
184
+
176
185
  Args:
177
186
  experiment_id: ID of the experiment to update.
178
187
  experiment: Optional typed :class:`Experiment` carrying the new
@@ -190,8 +199,7 @@ class ExperimentMixin:
190
199
  "(pass experiment=... or keyword fields)."
191
200
  )
192
201
  body = self._transport.put(f"experiments/{experiment_id}", json=payload)
193
- data = body.get("data", [])
194
- return data[0] if isinstance(data, list) and data else data
202
+ return single_record(body)
195
203
 
196
204
  def validate_experiment(
197
205
  self,
@@ -210,8 +218,7 @@ class ExperimentMixin:
210
218
  raise TypeError("validate_experiment requires an Experiment or dict")
211
219
  payload = _build_experiment_payload(experiment)
212
220
  body = self._transport.post("experiments/validate", json=payload)
213
- data = body.get("data", [])
214
- return data[0] if isinstance(data, list) and data else data
221
+ return single_record(body)
215
222
 
216
223
  def delete_experiment(
217
224
  self, experiment_id: str, *, skip_data_check: bool = False
hyperstudy/models.py CHANGED
@@ -29,20 +29,56 @@ _FREEFORM_MAP_FIELDS = frozenset({
29
29
  "roleOverrides",
30
30
  })
31
31
 
32
+ # Fields whose whole value goes on the wire exactly as written, as
33
+ # (parent field, field) pairs; a parent of None is the top level of an
34
+ # experiment or persona payload. Their keys belong to someone else: a
35
+ # provider's raw request params (extraParams), the voice engine's
36
+ # turn-detection settings, which use OpenAI's snake_case names
37
+ # (engineTurnDetection), and names the researcher chose (cognition contexts,
38
+ # stimulus mappings, free-form metadata). Matching on position, not the bare
39
+ # key, keeps a variable or role that happens to be named "metadata" a variable.
40
+ _VERBATIM_FIELDS = frozenset({
41
+ (None, "extraParams"), (None, "cognition"), (None, "stimulusMappings"),
42
+ (None, "metadata"), (None, "prolific_config"),
43
+ ("voiceTurnTaking", "engineTurnDetection"),
44
+ })
45
+
46
+ # Experiment fields the backend stores under snake_case names.
47
+ _SNAKE_CASE_FIELDS = frozenset({"prolific_enabled", "prolific_config"})
48
+
49
+
50
+ def _wire_key(key: str) -> str:
51
+ return key if key in _SNAKE_CASE_FIELDS else to_camel(key)
52
+
53
+
54
+ def _dump(model: BaseModel) -> dict[str, Any]:
55
+ return model.model_dump(by_alias=True, exclude_none=True)
56
+
32
57
 
33
58
  def camelize_wire(value: Any, _parent_key: "str | None" = None) -> Any:
34
59
  """Recursively convert snake_case dict keys to camelCase for the wire format.
35
60
 
36
61
  Skips converting the immediate keys of free-form map fields (variables, roles,
37
62
  globalComponents, globalComponentsVisibility) — those are user-defined names —
38
- but still recurses into their values.
63
+ but still recurses into their values. Values of ``_VERBATIM_FIELDS`` are
64
+ passed through untouched; their positions are relative to the payload root,
65
+ so call this on a whole experiment or persona payload, not a sub-dict.
66
+
67
+ A typed model is dumped in its own wire format, the same one the typed
68
+ builders send: its declared fields are aliased and its dict-valued fields
69
+ are sent as written.
39
70
  """
71
+ if isinstance(value, BaseModel):
72
+ return _dump(value)
40
73
  if isinstance(value, dict):
41
74
  skip_keys = _parent_key in _FREEFORM_MAP_FIELDS
42
75
  out: dict[str, Any] = {}
43
76
  for k, v in value.items():
44
- new_key = k if skip_keys else (to_camel(k) if isinstance(k, str) else k)
45
- out[new_key] = camelize_wire(v, _parent_key=new_key)
77
+ new_key = k if skip_keys else (_wire_key(k) if isinstance(k, str) else k)
78
+ if (_parent_key, new_key) in _VERBATIM_FIELDS:
79
+ out[new_key] = _dump(v) if isinstance(v, BaseModel) else v
80
+ else:
81
+ out[new_key] = camelize_wire(v, _parent_key=new_key)
46
82
  return out
47
83
  if isinstance(value, list):
48
84
  return [camelize_wire(v, _parent_key=_parent_key) for v in value]
@@ -159,17 +195,62 @@ class PromptLayer(_Model):
159
195
  additional_instructions: Optional[str] = None
160
196
 
161
197
 
198
+ class EngineTurnDetection(BaseModel):
199
+ """Turn detection handed to the voice engine.
200
+
201
+ The keys are the engine's own (OpenAI realtime) names, so unlike every
202
+ other model here this one is sent in snake_case, exactly as written.
203
+ ``server_vad`` takes ``threshold``, ``prefix_padding_ms`` and
204
+ ``silence_duration_ms``; ``semantic_vad`` takes ``eagerness``.
205
+ """
206
+
207
+ model_config = ConfigDict(extra="allow")
208
+
209
+ type: str # "server_vad" | "semantic_vad"
210
+ threshold: Optional[float] = Field(default=None, ge=0, le=1)
211
+ prefix_padding_ms: Optional[int] = Field(default=None, ge=0, le=2000)
212
+ silence_duration_ms: Optional[int] = Field(default=None, ge=100, le=5000)
213
+ eagerness: Optional[str] = None # semantic_vad: low | medium | high | auto
214
+
215
+
216
+ class VoiceTurnTaking(_Model):
217
+ """How an experiment's voice agents take turns.
218
+
219
+ Unset, the engine detects turns with ``server_vad`` and 500 ms of
220
+ silence, and a spoken reply is cut off after 60 s.
221
+ """
222
+
223
+ mode: Optional[str] = None # "engine" (the only mode so far)
224
+ engine_turn_detection: Optional[EngineTurnDetection] = None
225
+ min_response_delay_ms: Optional[int] = Field(default=None, ge=0, le=10_000)
226
+ max_response_ms: Optional[int] = Field(default=None, ge=1000, le=300_000)
227
+
228
+
229
+ class VoiceTranscription(_Model):
230
+ """Transcription of participants' speech by the voice engine (off unless enabled).
231
+
232
+ ``language`` is a two-letter ISO-639-1 hint for a study run in one
233
+ language; unset, the model detects it.
234
+ """
235
+
236
+ enabled: Optional[bool] = None
237
+ model: Optional[str] = None # gpt-4o-transcribe (default) | gpt-4o-mini-transcribe | whisper-1
238
+ language: Optional[str] = Field(default=None, pattern=r"^[a-z]{2}$")
239
+
240
+
162
241
  class AgentConfig(_Model):
163
242
  """Experiment-level agent inputs.
164
243
 
165
- The bound persona is the source of truth for provider/model/prompt;
166
- the experiment contributes only per-role prompt overrides, pacing,
167
- and the random seed.
244
+ The bound persona is the source of truth for provider/model/prompt and
245
+ voice; the experiment contributes per-role prompt overrides, pacing, the
246
+ random seed, and how its voice agents take turns and transcribe speech.
168
247
  """
169
248
 
170
249
  role_overrides: Optional[dict[str, PromptLayer]] = None
171
- pacing: Optional[dict[str, Any]] = None
250
+ pacing: Optional[dict[str, Any]] = None # sent as written: {"readingOffsetMs": 300, ...}
172
251
  seed: Optional[int] = None
252
+ voice_turn_taking: Optional[VoiceTurnTaking] = None
253
+ voice_transcription: Optional[VoiceTranscription] = None
173
254
 
174
255
 
175
256
  class Guardrails(_Model):
@@ -182,15 +263,50 @@ class Guardrails(_Model):
182
263
  max_consecutive_decision_errors: Optional[int] = Field(default=None, ge=1)
183
264
 
184
265
 
266
+ class RealtimeVoice(_Model):
267
+ """The realtime speech engine behind a voice persona."""
268
+
269
+ provider: str # "openai" (the only provider so far)
270
+ model: Optional[str] = None # e.g. "gpt-realtime-2.1"; the provider's default when unset
271
+
272
+
273
+ class PersonaVoice(_Model):
274
+ """Gives a persona a voice in the experiment's audio.
275
+
276
+ Only ``engine="realtime"`` with ``realtime=RealtimeVoice(provider="openai")``
277
+ is supported so far. ``preset`` is the provider's voice (OpenAI: alloy,
278
+ ash, ballad, coral, echo, sage, shimmer, verse, marin, cedar); ``style``
279
+ is natural-language delivery direction, e.g. ``"warm, a little hesitant"``.
280
+ """
281
+
282
+ engine: str # "realtime"
283
+ preset: Optional[str] = None
284
+ speed: Optional[float] = Field(default=None, ge=0.25, le=1.5)
285
+ style: Optional[str] = Field(default=None, max_length=500)
286
+ realtime: Optional[RealtimeVoice] = None
287
+
288
+
289
+ class PersonaChat(_Model):
290
+ """Whether the agent opens a text chat, and how long it waits first."""
291
+
292
+ initiate: Optional[bool] = None
293
+ initiate_delay_ms: Optional[int] = Field(default=None, gt=0)
294
+
295
+
185
296
  class Persona(_Model):
186
297
  """A reusable AI-agent definition (agent library entry).
187
298
 
188
- The persona owns the agent's identity, model settings, guardrails and
189
- (optionally) cognition. ``cognition`` / ``offline_cognition`` are accepted
190
- as plain dicts — their schema is experimental and still evolving.
299
+ The persona owns the agent's identity, model settings, guardrails,
300
+ voice and (optionally) cognition. ``cognition`` is accepted as a plain
301
+ dict — its schema is experimental and still evolving. Offline loops live
302
+ inside ``cognition["config"]["contexts"][<name>]["offline"]``.
303
+
304
+ ``display_name`` is the name other participants see in chat and video;
305
+ empty, they see ``name``.
191
306
  """
192
307
 
193
308
  name: str = Field(min_length=1)
309
+ display_name: Optional[str] = None
194
310
  description: Optional[str] = None
195
311
  prompt: Optional[PromptLayer] = None
196
312
  provider: Optional[str] = None # anthropic (default) | openai | gemini | custom
@@ -204,13 +320,13 @@ class Persona(_Model):
204
320
  disable_thinking: Optional[bool] = None
205
321
  seed: Optional[int] = None
206
322
  cognition: Optional[dict[str, Any]] = None
207
- offline_cognition: Optional[dict[str, Any]] = None
208
323
  memory_persistence: Optional[str] = None # "none" (default) | "cross-experiment"
209
- language: Optional[str] = None
210
324
  seed_memories: Optional[list[dict[str, Any]]] = None
211
325
  guardrails: Optional[Guardrails] = None
212
- pacing: Optional[dict[str, Any]] = None
213
- visibility: Optional[str] = None # create-time only: private | organization | public
326
+ pacing: Optional[dict[str, Any]] = None # sent as written: {"readingOffsetMs": 300, ...}
327
+ chat: Optional[PersonaChat] = None
328
+ voice: Optional[PersonaVoice] = None
329
+ visibility: Optional[str] = None # private (default) | organization | public
214
330
 
215
331
 
216
332
  class WaitingRoomConfig(_Model):
@@ -224,6 +340,16 @@ class DisconnectTimeout(_Model):
224
340
  auto_reconnect_delay: Optional[int] = Field(default=None, ge=0)
225
341
 
226
342
 
343
+ class ExperimenterMessageStyle(_Model):
344
+ """Text style of messages the experimenter sends from deployment monitoring.
345
+
346
+ Unset, messages are 20px white.
347
+ """
348
+
349
+ font_size: Optional[float] = Field(default=None, ge=10, le=72)
350
+ color: Optional[str] = Field(default=None, pattern=r"^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$")
351
+
352
+
227
353
  class InstructionsPage(_Model):
228
354
  title: Optional[str] = None
229
355
  content: Optional[str] = None
@@ -263,6 +389,16 @@ class Experiment(_Model):
263
389
  instructions_content: Optional[str] = None
264
390
  instructions_pages: Optional[list[InstructionsPage]] = None
265
391
  post_experiment_questionnaire: Optional[PostExperimentQuestionnaire] = None
392
+ # Only the owner or an organization admin may change visibility.
393
+ visibility: Optional[str] = None # private (default) | organization | public
394
+ tags: Optional[list[str]] = None
395
+ enable_participant_messaging: Optional[bool] = None
396
+ experimenter_message_style: Optional[ExperimenterMessageStyle] = None
397
+ stimulus_mappings: Optional[dict[str, Any]] = None
398
+ device_integration: Optional[dict[str, Any]] = None
399
+ # Stored under snake_case names by the backend.
400
+ prolific_enabled: Optional[bool] = Field(default=None, alias="prolific_enabled")
401
+ prolific_config: Optional[dict[str, Any]] = Field(default=None, alias="prolific_config")
266
402
 
267
403
 
268
404
  # ---------------------------------------------------------------------------
@@ -280,11 +416,16 @@ class Experiment(_Model):
280
416
  # of ``output_variable=``) gets the factory's value, not their typo.
281
417
 
282
418
 
419
+ def _config_extra(extra: dict[str, Any]) -> dict[str, Any]:
420
+ """A factory's extra kwargs, converted as fields of a component ``config``."""
421
+ return camelize_wire(extra, _parent_key="config")
422
+
423
+
283
424
  def show_text(text: str, *, id: Optional[str] = None, **extra: Any) -> FocusComponent:
284
425
  """Build a ``showtext`` focus component."""
285
426
  return FocusComponent(
286
427
  type=ComponentType.SHOW_TEXT,
287
- config={**camelize_wire(extra), "text": text},
428
+ config={**_config_extra(extra), "text": text},
288
429
  id=id,
289
430
  )
290
431
 
@@ -293,7 +434,7 @@ def show_image(url: str, *, id: Optional[str] = None, **extra: Any) -> FocusComp
293
434
  """Build a ``showimage`` focus component."""
294
435
  return FocusComponent(
295
436
  type=ComponentType.SHOW_IMAGE,
296
- config={**camelize_wire(extra), "url": url},
437
+ config={**_config_extra(extra), "url": url},
297
438
  id=id,
298
439
  )
299
440
 
@@ -302,7 +443,7 @@ def show_video(url: str, *, id: Optional[str] = None, **extra: Any) -> FocusComp
302
443
  """Build a ``showvideo`` focus component."""
303
444
  return FocusComponent(
304
445
  type=ComponentType.SHOW_VIDEO,
305
- config={**camelize_wire(extra), "url": url},
446
+ config={**_config_extra(extra), "url": url},
306
447
  id=id,
307
448
  )
308
449
 
@@ -317,7 +458,7 @@ def vas_rating(
317
458
  """Build a ``vasrating`` (visual analog scale) focus component."""
318
459
  return FocusComponent(
319
460
  type=ComponentType.VAS_RATING,
320
- config={**camelize_wire(extra), "prompt": prompt, "outputVariable": output_variable},
461
+ config={**_config_extra(extra), "prompt": prompt, "outputVariable": output_variable},
321
462
  id=id,
322
463
  )
323
464
 
@@ -332,7 +473,7 @@ def text_input(
332
473
  """Build a ``textinput`` focus component."""
333
474
  return FocusComponent(
334
475
  type=ComponentType.TEXT_INPUT,
335
- config={**camelize_wire(extra), "prompt": prompt, "outputVariable": output_variable},
476
+ config={**_config_extra(extra), "prompt": prompt, "outputVariable": output_variable},
336
477
  id=id,
337
478
  )
338
479
 
@@ -349,7 +490,7 @@ def multiple_choice(
349
490
  return FocusComponent(
350
491
  type=ComponentType.MULTIPLE_CHOICE,
351
492
  config={
352
- **camelize_wire(extra),
493
+ **_config_extra(extra),
353
494
  "prompt": prompt,
354
495
  "options": list(options),
355
496
  "outputVariable": output_variable,
@@ -362,7 +503,7 @@ def waiting(duration_ms: int, *, id: Optional[str] = None, **extra: Any) -> Focu
362
503
  """Build a ``waiting`` focus component."""
363
504
  return FocusComponent(
364
505
  type=ComponentType.WAITING,
365
- config={**camelize_wire(extra), "durationMs": int(duration_ms)},
506
+ config={**_config_extra(extra), "durationMs": int(duration_ms)},
366
507
  id=id,
367
508
  )
368
509
 
@@ -379,7 +520,7 @@ def likert_scale(
379
520
  return FocusComponent(
380
521
  type=ComponentType.LIKERT_SCALE,
381
522
  config={
382
- **camelize_wire(extra),
523
+ **_config_extra(extra),
383
524
  "prompt": prompt,
384
525
  "outputVariable": output_variable,
385
526
  "scalePoints": int(scale_points),
@@ -400,7 +541,7 @@ def ranking(
400
541
  return FocusComponent(
401
542
  type=ComponentType.RANKING,
402
543
  config={
403
- **camelize_wire(extra),
544
+ **_config_extra(extra),
404
545
  "prompt": prompt,
405
546
  "options": list(options),
406
547
  "outputVariable": output_variable,
@@ -425,6 +566,18 @@ __all__ = [
425
566
  "DisconnectTimeout",
426
567
  "InstructionsPage",
427
568
  "PostExperimentQuestionnaire",
569
+ "ExperimenterMessageStyle",
570
+ # Agent models
571
+ "AgentConfig",
572
+ "PromptLayer",
573
+ "VoiceTurnTaking",
574
+ "EngineTurnDetection",
575
+ "VoiceTranscription",
576
+ "Persona",
577
+ "PersonaVoice",
578
+ "RealtimeVoice",
579
+ "PersonaChat",
580
+ "Guardrails",
428
581
  # Factories
429
582
  "show_text",
430
583
  "show_image",
hyperstudy/personas.py CHANGED
@@ -7,8 +7,11 @@ Personas are reusable AI-agent definitions. These methods target the
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
+ import warnings
10
11
  from typing import TYPE_CHECKING, Any
11
12
 
13
+ from ._http import single_record
14
+
12
15
  if TYPE_CHECKING:
13
16
  from .models import Persona
14
17
 
@@ -25,6 +28,22 @@ def _persona_payload(persona: "Persona | None", kwargs: dict[str, Any]) -> dict[
25
28
  payload.update(persona.model_dump(by_alias=True, exclude_none=True))
26
29
  if kwargs:
27
30
  payload.update(camelize_wire(kwargs))
31
+ # The server hard-discards offlineCognition (offline loops now live inside
32
+ # cognition.config.contexts.<name>.offline), so never send it — including
33
+ # when a Persona was rebuilt from an older API response via extra fields.
34
+ payload.pop("offlineCognition", None)
35
+ payload.pop("offline_cognition", None)
36
+ # Personas no longer have a language: nothing read it, and the server
37
+ # discards it. Older stored personas still carry the key, so strip it. It
38
+ # was stored as null unless someone set it, so a value means someone
39
+ # believed it did something: tell them, wherever it came from.
40
+ if payload.pop("language", None):
41
+ warnings.warn(
42
+ "Persona 'language' was removed and is ignored by the server. To set "
43
+ "the language participants speak, use the experiment's "
44
+ "agent_config.voice_transcription.language.",
45
+ stacklevel=3,
46
+ )
28
47
  return payload
29
48
 
30
49
 
@@ -43,7 +62,7 @@ class PersonaMixin:
43
62
  def get_persona(self, persona_id: str) -> dict[str, Any]:
44
63
  """Get one persona by ID."""
45
64
  body = self._transport.get(f"personas/{persona_id}")
46
- return body.get("data", {})
65
+ return single_record(body)
47
66
 
48
67
  def create_persona(
49
68
  self,
@@ -69,7 +88,7 @@ class PersonaMixin:
69
88
  if not payload.get("name"):
70
89
  raise ValueError("Persona name is required")
71
90
  body = self._transport.post("personas", json=payload)
72
- return body.get("data", {})
91
+ return single_record(body)
73
92
 
74
93
  def update_persona(
75
94
  self,
@@ -80,14 +99,22 @@ class PersonaMixin:
80
99
  ) -> dict[str, Any]:
81
100
  """Update a persona (merge-patch).
82
101
 
83
- Only the fields you pass are changed; everything else on the stored
84
- persona is preserved.
102
+ Only the top-level fields you pass are changed; everything else on
103
+ the stored persona is preserved. A nested block you pass (``voice``,
104
+ ``chat``, ``prompt``, ``guardrails``, ``cognition``) replaces the
105
+ stored block whole, so send all of it. For a persona that has a
106
+ voice (``voice`` is ``None`` on a text-only persona)::
107
+
108
+ voice = hs.get_persona(pid)["voice"]
109
+ hs.update_persona(pid, voice={**voice, "speed": 1.1})
110
+
111
+ Only the owner or an organization admin may change ``visibility``.
85
112
  """
86
113
  payload = _persona_payload(persona, kwargs)
87
114
  if not payload:
88
115
  raise ValueError("No fields to update")
89
116
  body = self._transport.put(f"personas/{persona_id}", json=payload)
90
- return body.get("data", {})
117
+ return single_record(body)
91
118
 
92
119
  def delete_persona(self, persona_id: str) -> None:
93
120
  """Delete a persona."""
@@ -100,4 +127,16 @@ class PersonaMixin:
100
127
  The new persona (named ``"<original name> (copy)"``).
101
128
  """
102
129
  body = self._transport.post(f"personas/{persona_id}/duplicate")
103
- return body.get("data", {})
130
+ return single_record(body)
131
+
132
+ def get_cognition_catalog(self) -> dict[str, Any]:
133
+ """Fetch the cognition authoring catalog.
134
+
135
+ Lists the valid building blocks for persona ``cognition`` configs.
136
+ Requires the ``read:personas`` scope.
137
+
138
+ Returns:
139
+ Dict with ``abilities``, ``recipes``, and ``offlineRecipes``.
140
+ """
141
+ body = self._transport.get("agent-cognition/catalog")
142
+ return single_record(body)
@@ -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
@@ -0,0 +1,16 @@
1
+ hyperstudy/__init__.py,sha256=8yoNbRsLUNtzRWR98EHvrb78hcr4YeN7DoTy7rnnsA0,2369
2
+ hyperstudy/_dataframe.py,sha256=8pjuMG8phMv8vwdY7sgZ-aF_6_o0UMgP2XXNp5muZ0s,3620
3
+ hyperstudy/_display.py,sha256=5svr4NFOUJkVif57-xJzrXNOSdA5O9tKesu8UhE_jYo,1896
4
+ hyperstudy/_downloads.py,sha256=8s-Ml0oK_3f5CNn9hSp0z5d1oyDBR7ldPYVPbDo0zss,2616
5
+ hyperstudy/_http.py,sha256=QG9JA_2Pug620PKlQRqo_PZ9ycMWjG4FD_EOGclLUJ8,5196
6
+ hyperstudy/_pagination.py,sha256=2NbvNGW_jFQkYquSTi3SNkAmGgsbKop7J9LkGaZWwWA,1812
7
+ hyperstudy/_types.py,sha256=nREuLxEe4ylnyxRPyR1kgBh0V4fDKf5OmfiLEmTkg-o,734
8
+ hyperstudy/client.py,sha256=DRKDmLND65bxXAIBO9EDK2IikX6RJ_6O0GgmyI6ISTI,40319
9
+ hyperstudy/exceptions.py,sha256=z_1Ngiq-RU-1yyDI_JqvQ5CvYm2jQ_lyJ9TUbqUKKv8,1529
10
+ hyperstudy/experiments.py,sha256=cQN0O6sPeSAJ9qqIGdIIR5Mvu1Mf7HJ9BKUc01_-cn0,8695
11
+ hyperstudy/models.py,sha256=Tdi5ygqDwkA-p9ibqdT-70zCaMi9BBIZSVPL4Y6e6Yg,20288
12
+ hyperstudy/personas.py,sha256=T4cbI0wWfokAthxKzgM73NA_we3mBtR-j_lcn3iYl4Q,5328
13
+ hyperstudy-0.4.2.dist-info/METADATA,sha256=aYnlSHF7l0VJ7ruRo-P0mafpdEHVtmyZOlXwJO_6dQc,10499
14
+ hyperstudy-0.4.2.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
15
+ hyperstudy-0.4.2.dist-info/licenses/LICENSE,sha256=WJdwRhBIOiiQ6lyK4l95u5IJYuuT-XxzwYRraS31NME,1067
16
+ hyperstudy-0.4.2.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.31.0
2
+ Generator: hatchling 1.33.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,16 +0,0 @@
1
- hyperstudy/__init__.py,sha256=ZbHxlP2hnepEMK29ebttkRY_uJC1YuMTPcbMt_ov7cs,2047
2
- hyperstudy/_dataframe.py,sha256=8pjuMG8phMv8vwdY7sgZ-aF_6_o0UMgP2XXNp5muZ0s,3620
3
- hyperstudy/_display.py,sha256=5svr4NFOUJkVif57-xJzrXNOSdA5O9tKesu8UhE_jYo,1896
4
- hyperstudy/_downloads.py,sha256=8s-Ml0oK_3f5CNn9hSp0z5d1oyDBR7ldPYVPbDo0zss,2616
5
- hyperstudy/_http.py,sha256=_RS0ZaRAcKvFQ2NF9II7PpcojE-lTYT6CslaJNlgfPQ,4569
6
- hyperstudy/_pagination.py,sha256=2NbvNGW_jFQkYquSTi3SNkAmGgsbKop7J9LkGaZWwWA,1812
7
- hyperstudy/_types.py,sha256=Mw5KJFMXgLi17U6F08UR8BRfhAYrsj95Y1byhHCJTDU,682
8
- hyperstudy/client.py,sha256=5WJGP3ggWIHq-NII_ikKydpj2RVVf56KRc2MEklb3I4,31897
9
- hyperstudy/exceptions.py,sha256=z_1Ngiq-RU-1yyDI_JqvQ5CvYm2jQ_lyJ9TUbqUKKv8,1529
10
- hyperstudy/experiments.py,sha256=ZGCbOEwmzXq-HVJ-f7iPsinYZDxq1RXUZ7mBqo2T3hU,8472
11
- hyperstudy/models.py,sha256=1wA_USRzqKSY5pEQkK89wj0dDhkMeBFanzyRsIL_pvU,13820
12
- hyperstudy/personas.py,sha256=LukXLMiz7N3t06m1t7RZWQLrQRMMU1KsfGkwE7BhOBM,3485
13
- hyperstudy-0.4.0.dist-info/METADATA,sha256=FOwcQWXh2iwAnvcIjohWyZ6EiitqqNucEvcS8O-nd40,7900
14
- hyperstudy-0.4.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
15
- hyperstudy-0.4.0.dist-info/licenses/LICENSE,sha256=WJdwRhBIOiiQ6lyK4l95u5IJYuuT-XxzwYRraS31NME,1067
16
- hyperstudy-0.4.0.dist-info/RECORD,,