hyperstudy 0.3.1__tar.gz → 0.4.0__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.3.1 → hyperstudy-0.4.0}/CHANGELOG.md +15 -0
- {hyperstudy-0.3.1 → hyperstudy-0.4.0}/PKG-INFO +46 -2
- {hyperstudy-0.3.1 → hyperstudy-0.4.0}/README.md +45 -1
- {hyperstudy-0.3.1 → hyperstudy-0.4.0}/pyproject.toml +1 -1
- {hyperstudy-0.3.1 → hyperstudy-0.4.0}/src/hyperstudy/__init__.py +9 -1
- {hyperstudy-0.3.1 → hyperstudy-0.4.0}/src/hyperstudy/_types.py +2 -0
- {hyperstudy-0.3.1 → hyperstudy-0.4.0}/src/hyperstudy/client.py +176 -1
- {hyperstudy-0.3.1 → hyperstudy-0.4.0}/src/hyperstudy/models.py +78 -1
- hyperstudy-0.4.0/src/hyperstudy/personas.py +103 -0
- hyperstudy-0.4.0/tests/fixtures/agent_decision_detail_response.json +30 -0
- hyperstudy-0.4.0/tests/fixtures/agent_decisions_response.json +63 -0
- hyperstudy-0.4.0/tests/fixtures/agent_decisions_truncated_response.json +26 -0
- hyperstudy-0.4.0/tests/fixtures/agent_runs_response.json +46 -0
- hyperstudy-0.4.0/tests/test_agent_data.py +184 -0
- {hyperstudy-0.3.1 → hyperstudy-0.4.0}/tests/test_client.py +6 -1
- hyperstudy-0.4.0/tests/test_deployments_write.py +101 -0
- {hyperstudy-0.3.1 → hyperstudy-0.4.0}/tests/test_models.py +55 -0
- hyperstudy-0.4.0/tests/test_personas.py +127 -0
- {hyperstudy-0.3.1 → hyperstudy-0.4.0}/.github/workflows/publish.yml +0 -0
- {hyperstudy-0.3.1 → hyperstudy-0.4.0}/.github/workflows/sync-release-notes.yml +0 -0
- {hyperstudy-0.3.1 → hyperstudy-0.4.0}/.github/workflows/test.yml +0 -0
- {hyperstudy-0.3.1 → hyperstudy-0.4.0}/.gitignore +0 -0
- {hyperstudy-0.3.1 → hyperstudy-0.4.0}/LICENSE +0 -0
- {hyperstudy-0.3.1 → hyperstudy-0.4.0}/docs/superpowers/specs/2026-04-10-recording-downloads-design.md +0 -0
- {hyperstudy-0.3.1 → hyperstudy-0.4.0}/src/hyperstudy/_dataframe.py +0 -0
- {hyperstudy-0.3.1 → hyperstudy-0.4.0}/src/hyperstudy/_display.py +0 -0
- {hyperstudy-0.3.1 → hyperstudy-0.4.0}/src/hyperstudy/_downloads.py +0 -0
- {hyperstudy-0.3.1 → hyperstudy-0.4.0}/src/hyperstudy/_http.py +0 -0
- {hyperstudy-0.3.1 → hyperstudy-0.4.0}/src/hyperstudy/_pagination.py +0 -0
- {hyperstudy-0.3.1 → hyperstudy-0.4.0}/src/hyperstudy/exceptions.py +0 -0
- {hyperstudy-0.3.1 → hyperstudy-0.4.0}/src/hyperstudy/experiments.py +0 -0
- {hyperstudy-0.3.1 → hyperstudy-0.4.0}/tests/__init__.py +0 -0
- {hyperstudy-0.3.1 → hyperstudy-0.4.0}/tests/conftest.py +0 -0
- {hyperstudy-0.3.1 → hyperstudy-0.4.0}/tests/fixtures/deployment_sessions_response.json +0 -0
- {hyperstudy-0.3.1 → hyperstudy-0.4.0}/tests/fixtures/deployment_single_response.json +0 -0
- {hyperstudy-0.3.1 → hyperstudy-0.4.0}/tests/fixtures/deployments_list_response.json +0 -0
- {hyperstudy-0.3.1 → hyperstudy-0.4.0}/tests/fixtures/error_401.json +0 -0
- {hyperstudy-0.3.1 → hyperstudy-0.4.0}/tests/fixtures/error_403.json +0 -0
- {hyperstudy-0.3.1 → hyperstudy-0.4.0}/tests/fixtures/events_response.json +0 -0
- {hyperstudy-0.3.1 → hyperstudy-0.4.0}/tests/fixtures/experiment.schema.json +0 -0
- {hyperstudy-0.3.1 → hyperstudy-0.4.0}/tests/fixtures/experiment_single_response.json +0 -0
- {hyperstudy-0.3.1 → hyperstudy-0.4.0}/tests/fixtures/experiments_list_response.json +0 -0
- {hyperstudy-0.3.1 → hyperstudy-0.4.0}/tests/fixtures/paginated_page1.json +0 -0
- {hyperstudy-0.3.1 → hyperstudy-0.4.0}/tests/fixtures/paginated_page2.json +0 -0
- {hyperstudy-0.3.1 → hyperstudy-0.4.0}/tests/fixtures/pre_experiment_response.json +0 -0
- {hyperstudy-0.3.1 → hyperstudy-0.4.0}/tests/fixtures/recordings_response.json +0 -0
- {hyperstudy-0.3.1 → hyperstudy-0.4.0}/tests/fixtures/sparse_ratings_response.json +0 -0
- {hyperstudy-0.3.1 → hyperstudy-0.4.0}/tests/fixtures/warnings_response.json +0 -0
- {hyperstudy-0.3.1 → hyperstudy-0.4.0}/tests/test_dataframe.py +0 -0
- {hyperstudy-0.3.1 → hyperstudy-0.4.0}/tests/test_downloads.py +0 -0
- {hyperstudy-0.3.1 → hyperstudy-0.4.0}/tests/test_experiments.py +0 -0
- {hyperstudy-0.3.1 → hyperstudy-0.4.0}/tests/test_http.py +0 -0
- {hyperstudy-0.3.1 → hyperstudy-0.4.0}/tests/test_pagination.py +0 -0
- {hyperstudy-0.3.1 → hyperstudy-0.4.0}/tests/test_recordings_download.py +0 -0
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v0.4.0
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- **Agent data access**: new methods for AI-agent experiment data —
|
|
8
|
+
`get_agent_decisions(scope_id, scope="experiment"|"room", detail=, limit=, participant_id=)` (per-turn decision logs plus run-manifest rows), `get_agent_decision(room_id, decision_id)` (single decision with full detail blobs: prompt, reasoning chain, peer-model snapshot, prediction update), and `get_agent_runs(experiment_id)` (run manifests with orphaned-run flags). Emits a warning when the server truncates decisions at the per-room limit.
|
|
9
|
+
- `DataType.AGENT_DECISIONS` and `DataType.AGENT_RUNS` enum entries; `get_all_data` now includes an `agent_decisions` key (room-scope fetch filtered to the participant).
|
|
10
|
+
- **Agent-role experiment authoring**: `Role` accepts `mode="agent"` and `persona_id`; new `AgentConfig` (per-role `role_overrides`, `pacing`, `seed`) and `PromptLayer` models; `Experiment` gains `runtime` and `agent_config` fields. Role-name keys in `role_overrides` are preserved (not camelized) on the wire.
|
|
11
|
+
- **Persona management**: `list_personas`, `get_persona`, `create_persona`, `update_persona` (merge-patch), `delete_persona`, `duplicate_persona`, with typed `Persona` / `Guardrails` builders. Requires API-key scopes `read:personas` / `write:personas`.
|
|
12
|
+
- **Agent deployment launch & control**: `create_deployment` (including agent-only deployments — rooms launch server-side immediately), `get_agent_spend`, `run_more`, `stop_room`, `retry_room`. Requires the `write:deployments` scope.
|
|
13
|
+
|
|
14
|
+
### Fixes
|
|
15
|
+
|
|
16
|
+
- `__version__` now matches the packaged version (was stuck at 0.3.0 while pyproject said 0.3.1).
|
|
17
|
+
|
|
3
18
|
## v0.3.1
|
|
4
19
|
|
|
5
20
|
### Features
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hyperstudy
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
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
|
|
@@ -175,13 +175,57 @@ dep = hs.get_deployment("deployment_id")
|
|
|
175
175
|
sessions = hs.get_deployment_sessions("deployment_id")
|
|
176
176
|
```
|
|
177
177
|
|
|
178
|
+
## AI Agents
|
|
179
|
+
|
|
180
|
+
Full agent workflow support: personas (the agent library), agent-role experiment authoring, agent-only deployment launch/control, and agent data.
|
|
181
|
+
|
|
182
|
+
```python
|
|
183
|
+
from hyperstudy import Persona, PromptLayer, Guardrails, Role, AgentConfig
|
|
184
|
+
|
|
185
|
+
# --- Personas (agent library) ---
|
|
186
|
+
persona = hs.create_persona(persona=Persona(
|
|
187
|
+
name="Curious Undergrad",
|
|
188
|
+
provider="anthropic",
|
|
189
|
+
model="claude-opus-4-8",
|
|
190
|
+
prompt=PromptLayer(persona="You are a curious undergraduate...",
|
|
191
|
+
objective="Converse naturally with your partner."),
|
|
192
|
+
guardrails=Guardrails(max_turns=50, budget_usd=2.0),
|
|
193
|
+
))
|
|
194
|
+
personas = hs.list_personas()
|
|
195
|
+
hs.update_persona(persona["id"], description="Updated") # merge-patch
|
|
196
|
+
|
|
197
|
+
# --- Agent roles in experiments ---
|
|
198
|
+
exp = hs.create_experiment(experiment=Experiment(
|
|
199
|
+
name="Agent study",
|
|
200
|
+
runtime="v2", # required for agent-only deployments
|
|
201
|
+
roles={"partner": Role(mode="agent", persona_id=persona["id"])},
|
|
202
|
+
agent_config=AgentConfig(seed=42),
|
|
203
|
+
states=[...],
|
|
204
|
+
))
|
|
205
|
+
|
|
206
|
+
# --- Agent-only deployment ---
|
|
207
|
+
dep = hs.create_deployment(exp["id"], config={
|
|
208
|
+
"type": "agent-only",
|
|
209
|
+
"agentDeployment": {"rooms": 10, "budgetUsd": 5.0},
|
|
210
|
+
})
|
|
211
|
+
spend = hs.get_agent_spend(dep["id"]) # live spend vs budget
|
|
212
|
+
hs.run_more(dep["id"], rooms=5, budget_usd=2.5) # additive batch
|
|
213
|
+
|
|
214
|
+
# --- Agent data ---
|
|
215
|
+
decisions = hs.get_agent_decisions(exp["id"]) # per-turn logs
|
|
216
|
+
runs = hs.get_agent_runs(exp["id"]) # run manifests
|
|
217
|
+
detail = hs.get_agent_decision("room_id", "participantId_3") # full blobs
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Persona methods need the `read:personas` / `write:personas` API-key scopes; deployment launch/control needs `write:deployments`; agent data reads need `read:events`.
|
|
221
|
+
|
|
178
222
|
## All Data for a Participant
|
|
179
223
|
|
|
180
224
|
```python
|
|
181
225
|
data = hs.get_all_data("participant_id", room_id="room_id")
|
|
182
226
|
# Returns dict with keys: events, recordings, chat, videochat, sync,
|
|
183
227
|
# ratings_continuous, ratings_sparse, components, questionnaire,
|
|
184
|
-
# instructions, consent
|
|
228
|
+
# instructions, consent, agent_decisions
|
|
185
229
|
```
|
|
186
230
|
|
|
187
231
|
## API Key
|
|
@@ -140,13 +140,57 @@ dep = hs.get_deployment("deployment_id")
|
|
|
140
140
|
sessions = hs.get_deployment_sessions("deployment_id")
|
|
141
141
|
```
|
|
142
142
|
|
|
143
|
+
## AI Agents
|
|
144
|
+
|
|
145
|
+
Full agent workflow support: personas (the agent library), agent-role experiment authoring, agent-only deployment launch/control, and agent data.
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
from hyperstudy import Persona, PromptLayer, Guardrails, Role, AgentConfig
|
|
149
|
+
|
|
150
|
+
# --- Personas (agent library) ---
|
|
151
|
+
persona = hs.create_persona(persona=Persona(
|
|
152
|
+
name="Curious Undergrad",
|
|
153
|
+
provider="anthropic",
|
|
154
|
+
model="claude-opus-4-8",
|
|
155
|
+
prompt=PromptLayer(persona="You are a curious undergraduate...",
|
|
156
|
+
objective="Converse naturally with your partner."),
|
|
157
|
+
guardrails=Guardrails(max_turns=50, budget_usd=2.0),
|
|
158
|
+
))
|
|
159
|
+
personas = hs.list_personas()
|
|
160
|
+
hs.update_persona(persona["id"], description="Updated") # merge-patch
|
|
161
|
+
|
|
162
|
+
# --- Agent roles in experiments ---
|
|
163
|
+
exp = hs.create_experiment(experiment=Experiment(
|
|
164
|
+
name="Agent study",
|
|
165
|
+
runtime="v2", # required for agent-only deployments
|
|
166
|
+
roles={"partner": Role(mode="agent", persona_id=persona["id"])},
|
|
167
|
+
agent_config=AgentConfig(seed=42),
|
|
168
|
+
states=[...],
|
|
169
|
+
))
|
|
170
|
+
|
|
171
|
+
# --- Agent-only deployment ---
|
|
172
|
+
dep = hs.create_deployment(exp["id"], config={
|
|
173
|
+
"type": "agent-only",
|
|
174
|
+
"agentDeployment": {"rooms": 10, "budgetUsd": 5.0},
|
|
175
|
+
})
|
|
176
|
+
spend = hs.get_agent_spend(dep["id"]) # live spend vs budget
|
|
177
|
+
hs.run_more(dep["id"], rooms=5, budget_usd=2.5) # additive batch
|
|
178
|
+
|
|
179
|
+
# --- Agent data ---
|
|
180
|
+
decisions = hs.get_agent_decisions(exp["id"]) # per-turn logs
|
|
181
|
+
runs = hs.get_agent_runs(exp["id"]) # run manifests
|
|
182
|
+
detail = hs.get_agent_decision("room_id", "participantId_3") # full blobs
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Persona methods need the `read:personas` / `write:personas` API-key scopes; deployment launch/control needs `write:deployments`; agent data reads need `read:events`.
|
|
186
|
+
|
|
143
187
|
## All Data for a Participant
|
|
144
188
|
|
|
145
189
|
```python
|
|
146
190
|
data = hs.get_all_data("participant_id", room_id="room_id")
|
|
147
191
|
# Returns dict with keys: events, recordings, chat, videochat, sync,
|
|
148
192
|
# ratings_continuous, ratings_sparse, components, questionnaire,
|
|
149
|
-
# instructions, consent
|
|
193
|
+
# instructions, consent, agent_decisions
|
|
150
194
|
```
|
|
151
195
|
|
|
152
196
|
## API Key
|
|
@@ -30,13 +30,17 @@ from .exceptions import (
|
|
|
30
30
|
ValidationError,
|
|
31
31
|
)
|
|
32
32
|
from .models import (
|
|
33
|
+
AgentConfig,
|
|
33
34
|
ComponentType,
|
|
34
35
|
DisconnectTimeout,
|
|
35
36
|
Experiment,
|
|
36
37
|
FocusComponent,
|
|
37
38
|
GlobalComponentType,
|
|
39
|
+
Guardrails,
|
|
38
40
|
InstructionsPage,
|
|
41
|
+
Persona,
|
|
39
42
|
PostExperimentQuestionnaire,
|
|
43
|
+
PromptLayer,
|
|
40
44
|
Role,
|
|
41
45
|
State,
|
|
42
46
|
TransitionRules,
|
|
@@ -52,7 +56,7 @@ from .models import (
|
|
|
52
56
|
waiting,
|
|
53
57
|
)
|
|
54
58
|
|
|
55
|
-
__version__ = "0.
|
|
59
|
+
__version__ = "0.4.0"
|
|
56
60
|
|
|
57
61
|
__all__ = [
|
|
58
62
|
"HyperStudy",
|
|
@@ -75,6 +79,10 @@ __all__ = [
|
|
|
75
79
|
"State",
|
|
76
80
|
"FocusComponent",
|
|
77
81
|
"Role",
|
|
82
|
+
"AgentConfig",
|
|
83
|
+
"PromptLayer",
|
|
84
|
+
"Persona",
|
|
85
|
+
"Guardrails",
|
|
78
86
|
"TransitionRules",
|
|
79
87
|
"WaitingRoomConfig",
|
|
80
88
|
"DisconnectTimeout",
|
|
@@ -15,9 +15,10 @@ from ._pagination import fetch_all_pages
|
|
|
15
15
|
from ._types import Scope
|
|
16
16
|
from .exceptions import HyperStudyError
|
|
17
17
|
from .experiments import ExperimentMixin
|
|
18
|
+
from .personas import PersonaMixin
|
|
18
19
|
|
|
19
20
|
|
|
20
|
-
class HyperStudy(ExperimentMixin):
|
|
21
|
+
class HyperStudy(ExperimentMixin, PersonaMixin):
|
|
21
22
|
"""Client for the HyperStudy API v3.
|
|
22
23
|
|
|
23
24
|
Args:
|
|
@@ -441,6 +442,177 @@ class HyperStudy(ExperimentMixin):
|
|
|
441
442
|
data = body.get("data", [])
|
|
442
443
|
return self._convert_output(data, output)
|
|
443
444
|
|
|
445
|
+
# ------------------------------------------------------------------
|
|
446
|
+
# Deployments — agent-deployment write surface
|
|
447
|
+
# ------------------------------------------------------------------
|
|
448
|
+
|
|
449
|
+
def create_deployment(
|
|
450
|
+
self,
|
|
451
|
+
experiment_id: str,
|
|
452
|
+
*,
|
|
453
|
+
config: dict[str, Any] | None = None,
|
|
454
|
+
**kwargs: Any,
|
|
455
|
+
) -> dict[str, Any]:
|
|
456
|
+
"""Create a deployment (requires the ``write:deployments`` scope).
|
|
457
|
+
|
|
458
|
+
For an agent-only deployment, pass::
|
|
459
|
+
|
|
460
|
+
hs.create_deployment(
|
|
461
|
+
"exp_123",
|
|
462
|
+
config={
|
|
463
|
+
"name": "Pilot batch",
|
|
464
|
+
"type": "agent-only",
|
|
465
|
+
"agentDeployment": {"rooms": 10, "budgetUsd": 5.0},
|
|
466
|
+
},
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
Agent rooms launch server-side immediately after creation. Preflight
|
|
470
|
+
failures (missing persona binding, missing provider key, unreachable
|
|
471
|
+
custom endpoint) raise :class:`ValidationError` with per-role reasons.
|
|
472
|
+
|
|
473
|
+
Args:
|
|
474
|
+
experiment_id: The experiment to deploy (agent-only requires
|
|
475
|
+
``runtime="v2"`` and at least one agent-mode role).
|
|
476
|
+
config: Deployment config dict (camelCase keys, as above).
|
|
477
|
+
**kwargs: Extra config fields, merged into ``config``.
|
|
478
|
+
|
|
479
|
+
Returns:
|
|
480
|
+
The created deployment dict.
|
|
481
|
+
"""
|
|
482
|
+
merged = {**(config or {}), **kwargs}
|
|
483
|
+
body = self._transport.post(
|
|
484
|
+
"deployments", json={"experimentId": experiment_id, "config": merged}
|
|
485
|
+
)
|
|
486
|
+
return body.get("data", {})
|
|
487
|
+
|
|
488
|
+
def get_agent_spend(self, deployment_id: str) -> dict[str, Any]:
|
|
489
|
+
"""Total + per-room agent LLM spend for an agent-only deployment."""
|
|
490
|
+
body = self._transport.get(f"deployments/{deployment_id}/agent-spend")
|
|
491
|
+
return body.get("data", {})
|
|
492
|
+
|
|
493
|
+
def run_more(
|
|
494
|
+
self, deployment_id: str, *, rooms: int, budget_usd: float
|
|
495
|
+
) -> dict[str, Any]:
|
|
496
|
+
"""Launch additional agent rooms on an existing agent-only deployment.
|
|
497
|
+
|
|
498
|
+
Args:
|
|
499
|
+
deployment_id: The deployment to extend.
|
|
500
|
+
rooms: Number of additional rooms.
|
|
501
|
+
budget_usd: Additional budget for this batch (added to the
|
|
502
|
+
deployment's cumulative budget cap).
|
|
503
|
+
|
|
504
|
+
Returns:
|
|
505
|
+
Dict with the launched ``batchId`` and ``requestedRooms``.
|
|
506
|
+
"""
|
|
507
|
+
body = self._transport.post(
|
|
508
|
+
f"deployments/{deployment_id}/run-more",
|
|
509
|
+
json={"rooms": rooms, "budgetUsd": budget_usd},
|
|
510
|
+
)
|
|
511
|
+
return body.get("data", {})
|
|
512
|
+
|
|
513
|
+
def stop_room(self, deployment_id: str, room_id: str) -> None:
|
|
514
|
+
"""Force-end a running agent room."""
|
|
515
|
+
self._transport.post(f"deployments/{deployment_id}/rooms/{room_id}/stop")
|
|
516
|
+
|
|
517
|
+
def retry_room(self, deployment_id: str, room_id: str) -> dict[str, Any]:
|
|
518
|
+
"""Re-spawn a fresh room for a spawn-failed one (reuses the budget pool)."""
|
|
519
|
+
body = self._transport.post(
|
|
520
|
+
f"deployments/{deployment_id}/rooms/{room_id}/retry"
|
|
521
|
+
)
|
|
522
|
+
return body.get("data", {})
|
|
523
|
+
|
|
524
|
+
# ------------------------------------------------------------------
|
|
525
|
+
# Agent data — decisions and run manifests
|
|
526
|
+
# ------------------------------------------------------------------
|
|
527
|
+
|
|
528
|
+
def get_agent_decisions(
|
|
529
|
+
self,
|
|
530
|
+
scope_id: str,
|
|
531
|
+
*,
|
|
532
|
+
scope: str = "experiment",
|
|
533
|
+
detail: bool = False,
|
|
534
|
+
limit: int | None = None,
|
|
535
|
+
participant_id: str | None = None,
|
|
536
|
+
output: str = "pandas",
|
|
537
|
+
):
|
|
538
|
+
"""Fetch AI-agent decision logs (and run manifests).
|
|
539
|
+
|
|
540
|
+
Every agent turn is logged as a decision record; each agent's run
|
|
541
|
+
also produces one manifest row. Room-scope responses tag rows with
|
|
542
|
+
``_type`` (``"decision"`` or ``"run"``).
|
|
543
|
+
|
|
544
|
+
Args:
|
|
545
|
+
scope_id: Experiment ID or room ID, depending on ``scope``.
|
|
546
|
+
scope: ``"experiment"`` (default, all rooms) or ``"room"``.
|
|
547
|
+
detail: Include full detail blobs (prompt, reasoning chain,
|
|
548
|
+
peer-model snapshot, prediction update) on each decision row.
|
|
549
|
+
limit: Max decisions per room (server default 5000). When the
|
|
550
|
+
cap is hit a truncation warning is emitted.
|
|
551
|
+
participant_id: Optionally filter rows to one agent participant
|
|
552
|
+
(applied client-side).
|
|
553
|
+
output: ``"pandas"`` (default), ``"polars"``, or ``"dict"``.
|
|
554
|
+
"""
|
|
555
|
+
if scope not in ("experiment", "room"):
|
|
556
|
+
raise ValueError(f"scope must be 'experiment' or 'room', got {scope!r}")
|
|
557
|
+
|
|
558
|
+
params: dict[str, Any] = {}
|
|
559
|
+
if detail:
|
|
560
|
+
params["detail"] = "true"
|
|
561
|
+
if limit is not None:
|
|
562
|
+
params["limit"] = limit
|
|
563
|
+
|
|
564
|
+
body = self._transport.get(
|
|
565
|
+
f"data/agent-decisions/{scope}/{scope_id}", params=params or None
|
|
566
|
+
)
|
|
567
|
+
data = body.get("data", [])
|
|
568
|
+
if (body.get("metadata") or {}).get("truncated"):
|
|
569
|
+
warnings.warn(
|
|
570
|
+
"Agent decisions were truncated by the per-room limit; "
|
|
571
|
+
"pass a higher limit= to fetch more.",
|
|
572
|
+
stacklevel=2,
|
|
573
|
+
)
|
|
574
|
+
if participant_id is not None:
|
|
575
|
+
data = [row for row in data if row.get("participantId") == participant_id]
|
|
576
|
+
return self._convert_output(data, output)
|
|
577
|
+
|
|
578
|
+
def get_agent_decision(
|
|
579
|
+
self, room_id: str, decision_id: str
|
|
580
|
+
) -> dict[str, Any]:
|
|
581
|
+
"""Fetch one agent decision with full detail blobs.
|
|
582
|
+
|
|
583
|
+
Args:
|
|
584
|
+
room_id: Room the decision belongs to.
|
|
585
|
+
decision_id: Decision document ID (``{participantId}_{seq}``).
|
|
586
|
+
|
|
587
|
+
Returns:
|
|
588
|
+
Decision dict including prompt, reasoning chain, peer-model
|
|
589
|
+
snapshot, and prediction update.
|
|
590
|
+
"""
|
|
591
|
+
body = self._transport.get(
|
|
592
|
+
f"data/agent-decisions/room/{room_id}/decision/{decision_id}"
|
|
593
|
+
)
|
|
594
|
+
return body.get("data", {})
|
|
595
|
+
|
|
596
|
+
def get_agent_runs(
|
|
597
|
+
self,
|
|
598
|
+
experiment_id: str,
|
|
599
|
+
*,
|
|
600
|
+
output: str = "pandas",
|
|
601
|
+
):
|
|
602
|
+
"""Fetch agent run manifests for an experiment.
|
|
603
|
+
|
|
604
|
+
One row per agent run: model, provider, token/cost totals, seed,
|
|
605
|
+
persona ID, and code version. Runs whose room ended abnormally are
|
|
606
|
+
flagged as orphaned.
|
|
607
|
+
|
|
608
|
+
Args:
|
|
609
|
+
experiment_id: Experiment ID.
|
|
610
|
+
output: ``"pandas"`` (default), ``"polars"``, or ``"dict"``.
|
|
611
|
+
"""
|
|
612
|
+
body = self._transport.get(f"data/agent-runs/experiment/{experiment_id}")
|
|
613
|
+
data = body.get("data", [])
|
|
614
|
+
return self._convert_output(data, output)
|
|
615
|
+
|
|
444
616
|
# ------------------------------------------------------------------
|
|
445
617
|
# Convenience: all data for a participant
|
|
446
618
|
# ------------------------------------------------------------------
|
|
@@ -470,6 +642,9 @@ class HyperStudy(ExperimentMixin):
|
|
|
470
642
|
"questionnaire": self.get_questionnaire(participant_id, **common),
|
|
471
643
|
"instructions": self.get_instructions(participant_id, **common),
|
|
472
644
|
"consent": self.get_consent(participant_id, **common),
|
|
645
|
+
"agent_decisions": self.get_agent_decisions(
|
|
646
|
+
room_id, scope="room", participant_id=participant_id, output=output
|
|
647
|
+
),
|
|
473
648
|
}
|
|
474
649
|
|
|
475
650
|
# ------------------------------------------------------------------
|
|
@@ -26,6 +26,7 @@ def _new_id() -> str:
|
|
|
26
26
|
# Their values are still recursed into.
|
|
27
27
|
_FREEFORM_MAP_FIELDS = frozenset({
|
|
28
28
|
"variables", "roles", "globalComponents", "globalComponentsVisibility",
|
|
29
|
+
"roleOverrides",
|
|
29
30
|
})
|
|
30
31
|
|
|
31
32
|
|
|
@@ -132,10 +133,84 @@ class State(_Model):
|
|
|
132
133
|
|
|
133
134
|
|
|
134
135
|
class Role(_Model):
|
|
135
|
-
"""Participant role definition for multi-participant experiments.
|
|
136
|
+
"""Participant role definition for multi-participant experiments.
|
|
137
|
+
|
|
138
|
+
Set ``mode="agent"`` and ``persona_id`` to have an AI agent (persona
|
|
139
|
+
from your library) fill this role instead of a human participant.
|
|
140
|
+
"""
|
|
136
141
|
|
|
137
142
|
name: Optional[str] = None
|
|
138
143
|
participant_count: Optional[int] = Field(default=None, ge=0)
|
|
144
|
+
mode: Optional[str] = None # "human" (default) or "agent"
|
|
145
|
+
persona_id: Optional[str] = None
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class PromptLayer(_Model):
|
|
149
|
+
"""Structured prompt fields, used for per-role agent overrides.
|
|
150
|
+
|
|
151
|
+
Overrides are APPENDED to the bound persona's own prompt fields —
|
|
152
|
+
they never replace the persona's identity.
|
|
153
|
+
"""
|
|
154
|
+
|
|
155
|
+
persona: Optional[str] = None
|
|
156
|
+
objective: Optional[str] = None
|
|
157
|
+
guidance: Optional[str] = None
|
|
158
|
+
examples: Optional[list[str]] = None
|
|
159
|
+
additional_instructions: Optional[str] = None
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class AgentConfig(_Model):
|
|
163
|
+
"""Experiment-level agent inputs.
|
|
164
|
+
|
|
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.
|
|
168
|
+
"""
|
|
169
|
+
|
|
170
|
+
role_overrides: Optional[dict[str, PromptLayer]] = None
|
|
171
|
+
pacing: Optional[dict[str, Any]] = None
|
|
172
|
+
seed: Optional[int] = None
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class Guardrails(_Model):
|
|
176
|
+
"""Per-agent safety limits. Unset fields use platform defaults
|
|
177
|
+
(100 turns / 500k tokens); budgets fail closed when exceeded."""
|
|
178
|
+
|
|
179
|
+
max_turns: Optional[int] = Field(default=None, ge=1)
|
|
180
|
+
budget_tokens: Optional[int] = Field(default=None, ge=1000)
|
|
181
|
+
budget_usd: Optional[float] = Field(default=None, gt=0)
|
|
182
|
+
max_consecutive_decision_errors: Optional[int] = Field(default=None, ge=1)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class Persona(_Model):
|
|
186
|
+
"""A reusable AI-agent definition (agent library entry).
|
|
187
|
+
|
|
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.
|
|
191
|
+
"""
|
|
192
|
+
|
|
193
|
+
name: str = Field(min_length=1)
|
|
194
|
+
description: Optional[str] = None
|
|
195
|
+
prompt: Optional[PromptLayer] = None
|
|
196
|
+
provider: Optional[str] = None # anthropic (default) | openai | gemini | custom
|
|
197
|
+
model: Optional[str] = None # required when provider == "custom"
|
|
198
|
+
temperature: Optional[float] = None
|
|
199
|
+
top_p: Optional[float] = None
|
|
200
|
+
max_tokens: Optional[int] = None
|
|
201
|
+
thinking: Optional[bool] = None
|
|
202
|
+
reasoning_effort: Optional[str] = None
|
|
203
|
+
extra_params: Optional[dict[str, Any]] = None
|
|
204
|
+
disable_thinking: Optional[bool] = None
|
|
205
|
+
seed: Optional[int] = None
|
|
206
|
+
cognition: Optional[dict[str, Any]] = None
|
|
207
|
+
offline_cognition: Optional[dict[str, Any]] = None
|
|
208
|
+
memory_persistence: Optional[str] = None # "none" (default) | "cross-experiment"
|
|
209
|
+
language: Optional[str] = None
|
|
210
|
+
seed_memories: Optional[list[dict[str, Any]]] = None
|
|
211
|
+
guardrails: Optional[Guardrails] = None
|
|
212
|
+
pacing: Optional[dict[str, Any]] = None
|
|
213
|
+
visibility: Optional[str] = None # create-time only: private | organization | public
|
|
139
214
|
|
|
140
215
|
|
|
141
216
|
class WaitingRoomConfig(_Model):
|
|
@@ -170,6 +245,8 @@ class Experiment(_Model):
|
|
|
170
245
|
name: str = Field(min_length=1)
|
|
171
246
|
description: Optional[str] = None
|
|
172
247
|
required_participants: Optional[int] = Field(default=None, ge=1)
|
|
248
|
+
runtime: Optional[str] = None # "v2" required for agent-only deployments
|
|
249
|
+
agent_config: Optional[AgentConfig] = None
|
|
173
250
|
randomize_states: Optional[bool] = None
|
|
174
251
|
states: Optional[list[State]] = None
|
|
175
252
|
roles: Optional[dict[str, Role]] = None
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Persona (agent library) methods, mixed into the HyperStudy client.
|
|
2
|
+
|
|
3
|
+
Personas are reusable AI-agent definitions. These methods target the
|
|
4
|
+
/api/v3/personas routes and require API-key scopes ``read:personas`` /
|
|
5
|
+
``write:personas``.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import TYPE_CHECKING, Any
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from .models import Persona
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _persona_payload(persona: "Persona | None", kwargs: dict[str, Any]) -> dict[str, Any]:
|
|
17
|
+
"""Build a wire-ready persona payload from a typed Persona and/or kwargs."""
|
|
18
|
+
from .models import Persona as PersonaModel
|
|
19
|
+
from .models import camelize_wire
|
|
20
|
+
|
|
21
|
+
payload: dict[str, Any] = {}
|
|
22
|
+
if persona is not None:
|
|
23
|
+
if not isinstance(persona, PersonaModel):
|
|
24
|
+
raise TypeError("persona must be a hyperstudy.Persona instance")
|
|
25
|
+
payload.update(persona.model_dump(by_alias=True, exclude_none=True))
|
|
26
|
+
if kwargs:
|
|
27
|
+
payload.update(camelize_wire(kwargs))
|
|
28
|
+
return payload
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class PersonaMixin:
|
|
32
|
+
"""Persona CRUD methods for the HyperStudy client."""
|
|
33
|
+
|
|
34
|
+
def list_personas(self, *, output: str = "pandas"):
|
|
35
|
+
"""List personas you own or can view (organization-scoped).
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
output: ``"pandas"`` (default), ``"polars"``, or ``"dict"``.
|
|
39
|
+
"""
|
|
40
|
+
body = self._transport.get("personas")
|
|
41
|
+
return self._convert_output(body.get("data", []), output)
|
|
42
|
+
|
|
43
|
+
def get_persona(self, persona_id: str) -> dict[str, Any]:
|
|
44
|
+
"""Get one persona by ID."""
|
|
45
|
+
body = self._transport.get(f"personas/{persona_id}")
|
|
46
|
+
return body.get("data", {})
|
|
47
|
+
|
|
48
|
+
def create_persona(
|
|
49
|
+
self,
|
|
50
|
+
*,
|
|
51
|
+
persona: "Persona | None" = None,
|
|
52
|
+
name: str | None = None,
|
|
53
|
+
**kwargs: Any,
|
|
54
|
+
) -> dict[str, Any]:
|
|
55
|
+
"""Create a persona.
|
|
56
|
+
|
|
57
|
+
Two equivalent ways to call this:
|
|
58
|
+
|
|
59
|
+
- **Typed builder** (recommended): pass a :class:`Persona` instance.
|
|
60
|
+
- **Kwargs**: pass snake_case fields directly
|
|
61
|
+
(``create_persona(name="Confederate", provider="anthropic", ...)``).
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
The created persona (including its ``id``).
|
|
65
|
+
"""
|
|
66
|
+
if name is not None:
|
|
67
|
+
kwargs["name"] = name
|
|
68
|
+
payload = _persona_payload(persona, kwargs)
|
|
69
|
+
if not payload.get("name"):
|
|
70
|
+
raise ValueError("Persona name is required")
|
|
71
|
+
body = self._transport.post("personas", json=payload)
|
|
72
|
+
return body.get("data", {})
|
|
73
|
+
|
|
74
|
+
def update_persona(
|
|
75
|
+
self,
|
|
76
|
+
persona_id: str,
|
|
77
|
+
*,
|
|
78
|
+
persona: "Persona | None" = None,
|
|
79
|
+
**kwargs: Any,
|
|
80
|
+
) -> dict[str, Any]:
|
|
81
|
+
"""Update a persona (merge-patch).
|
|
82
|
+
|
|
83
|
+
Only the fields you pass are changed; everything else on the stored
|
|
84
|
+
persona is preserved.
|
|
85
|
+
"""
|
|
86
|
+
payload = _persona_payload(persona, kwargs)
|
|
87
|
+
if not payload:
|
|
88
|
+
raise ValueError("No fields to update")
|
|
89
|
+
body = self._transport.put(f"personas/{persona_id}", json=payload)
|
|
90
|
+
return body.get("data", {})
|
|
91
|
+
|
|
92
|
+
def delete_persona(self, persona_id: str) -> None:
|
|
93
|
+
"""Delete a persona."""
|
|
94
|
+
self._transport.delete(f"personas/{persona_id}")
|
|
95
|
+
|
|
96
|
+
def duplicate_persona(self, persona_id: str) -> dict[str, Any]:
|
|
97
|
+
"""Copy a viewable persona into a new private persona you own.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
The new persona (named ``"<original name> (copy)"``).
|
|
101
|
+
"""
|
|
102
|
+
body = self._transport.post(f"personas/{persona_id}/duplicate")
|
|
103
|
+
return body.get("data", {})
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"status": "success",
|
|
3
|
+
"data": {
|
|
4
|
+
"id": "agent_r1_p1_2",
|
|
5
|
+
"participantId": "agent_r1_p1",
|
|
6
|
+
"roomId": "room_abc123",
|
|
7
|
+
"seq": 2,
|
|
8
|
+
"stateId": "state_1",
|
|
9
|
+
"stateName": "Rating",
|
|
10
|
+
"turnKind": "component",
|
|
11
|
+
"action": "respond",
|
|
12
|
+
"value": 72,
|
|
13
|
+
"model": "claude-opus-4-8",
|
|
14
|
+
"provider": "anthropic",
|
|
15
|
+
"prompt": "You are a curious undergraduate...\n\nCurrent situation: ...",
|
|
16
|
+
"reasoning": "The video felt moderately positive, so I will rate 72.",
|
|
17
|
+
"chain": [
|
|
18
|
+
{"stage": "goal_infer", "output": "Partner seems to want agreement."},
|
|
19
|
+
{"stage": "decide", "output": "respond(72)"}
|
|
20
|
+
],
|
|
21
|
+
"peerModelSnapshot": {"peer_1": {"inferredGoal": "agreement"}},
|
|
22
|
+
"predictionUpdate": {"predictionError": 0.18},
|
|
23
|
+
"timestamp": "2026-07-15T18:23:04.000Z"
|
|
24
|
+
},
|
|
25
|
+
"metadata": {
|
|
26
|
+
"dataType": "agentDecisions",
|
|
27
|
+
"scope": "room",
|
|
28
|
+
"scopeId": "room_abc123"
|
|
29
|
+
}
|
|
30
|
+
}
|