multi-agent-platform 0.1.0__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.
- cli/__init__.py +0 -0
- cli/action_item_escalation.py +177 -0
- cli/agent_client.py +554 -0
- cli/bridge_state.py +43 -0
- cli/commands/__init__.py +13 -0
- cli/commands/action.py +142 -0
- cli/commands/agent.py +117 -0
- cli/commands/audit.py +68 -0
- cli/commands/docs.py +179 -0
- cli/commands/experiment.py +755 -0
- cli/commands/feedback.py +106 -0
- cli/commands/notification.py +213 -0
- cli/commands/persona.py +63 -0
- cli/commands/project.py +87 -0
- cli/commands/runtime.py +105 -0
- cli/commands/topic.py +361 -0
- cli/e2e_collab.py +602 -0
- cli/git_checkpoint.py +68 -0
- cli/host_worker_types.py +151 -0
- cli/main.py +1553 -0
- cli/map_command_client.py +497 -0
- cli/participant_worker.py +255 -0
- cli/reviewer_worker.py +263 -0
- cli/runtime/__init__.py +5 -0
- cli/runtime/run_lock.py +497 -0
- cli/runtime_chat.py +317 -0
- cli/session_wake_log.py +235 -0
- cli/simple_waker.py +950 -0
- cli/table_render.py +113 -0
- cli/wake_backend.py +236 -0
- cli/worker_cycle_log.py +36 -0
- map_client/__init__.py +37 -0
- map_client/bootstrap.py +193 -0
- map_client/client.py +1045 -0
- map_client/config.py +21 -0
- map_client/errors.py +283 -0
- map_client/exceptions.py +130 -0
- map_client/plan_evidence.py +159 -0
- map_client/project_config.py +153 -0
- map_client/result_template.py +167 -0
- map_client/testing.py +27 -0
- map_mcp/__init__.py +4 -0
- map_mcp/_utils.py +28 -0
- map_mcp/auth.py +34 -0
- map_mcp/config.py +50 -0
- map_mcp/context.py +39 -0
- map_mcp/main.py +75 -0
- map_mcp/server.py +573 -0
- map_mcp/session.py +79 -0
- map_sdk/__init__.py +29 -0
- map_sdk/evidence.py +68 -0
- map_types/__init__.py +203 -0
- map_types/enums.py +199 -0
- map_types/schemas.py +1351 -0
- multi_agent_platform-0.1.0.dist-info/METADATA +298 -0
- multi_agent_platform-0.1.0.dist-info/RECORD +144 -0
- multi_agent_platform-0.1.0.dist-info/WHEEL +5 -0
- multi_agent_platform-0.1.0.dist-info/entry_points.txt +6 -0
- multi_agent_platform-0.1.0.dist-info/licenses/LICENSE +21 -0
- multi_agent_platform-0.1.0.dist-info/top_level.txt +6 -0
- server/__init__.py +0 -0
- server/__version__.py +14 -0
- server/api/__init__.py +0 -0
- server/api/action_items.py +138 -0
- server/api/agents.py +412 -0
- server/api/audit.py +54 -0
- server/api/background_tasks.py +18 -0
- server/api/common.py +117 -0
- server/api/deps.py +30 -0
- server/api/experiments.py +858 -0
- server/api/feedback.py +75 -0
- server/api/notifications.py +22 -0
- server/api/projects.py +209 -0
- server/api/router.py +25 -0
- server/api/status.py +33 -0
- server/api/topics.py +302 -0
- server/api/webhooks.py +74 -0
- server/auth/__init__.py +8 -0
- server/auth/experiment_access.py +66 -0
- server/config.py +38 -0
- server/db/__init__.py +3 -0
- server/db/base.py +5 -0
- server/db/deadlock_retry.py +146 -0
- server/db/session.py +41 -0
- server/domain/__init__.py +3 -0
- server/domain/encrypted_types.py +63 -0
- server/domain/models.py +713 -0
- server/domain/schemas.py +3 -0
- server/domain/state_machine.py +79 -0
- server/domain/topic_ack_constants.py +9 -0
- server/main.py +148 -0
- server/scripts/__init__.py +0 -0
- server/scripts/migrate_notification_unique.py +231 -0
- server/scripts/purge_audit_pollution.py +116 -0
- server/services/__init__.py +0 -0
- server/services/_lookups.py +26 -0
- server/services/acceptance_service.py +90 -0
- server/services/action_item_migration_service.py +190 -0
- server/services/action_item_service.py +200 -0
- server/services/agent_work_service.py +405 -0
- server/services/archive_lint_service.py +156 -0
- server/services/audit_service.py +457 -0
- server/services/auth.py +66 -0
- server/services/comment_service.py +173 -0
- server/services/errors.py +65 -0
- server/services/escalation_resolver.py +248 -0
- server/services/evidence_service.py +88 -0
- server/services/experiment_capabilities_service.py +277 -0
- server/services/inbound_event_service.py +111 -0
- server/services/lock_service.py +273 -0
- server/services/log_service.py +202 -0
- server/services/mention_service.py +730 -0
- server/services/notification_service.py +939 -0
- server/services/notification_stream.py +138 -0
- server/services/permissions.py +147 -0
- server/services/persona_activity_service.py +108 -0
- server/services/phase_owner_resolver.py +95 -0
- server/services/phase_service.py +381 -0
- server/services/plan_marker_service.py +235 -0
- server/services/plan_service.py +186 -0
- server/services/platform_feedback_service.py +114 -0
- server/services/project_service.py +534 -0
- server/services/project_status_service.py +132 -0
- server/services/review_service.py +707 -0
- server/services/secret_encryption.py +97 -0
- server/services/similarity_service.py +119 -0
- server/services/sse_event_schemas.py +17 -0
- server/services/status_service.py +68 -0
- server/services/template_service.py +134 -0
- server/services/text_utils.py +19 -0
- server/services/thread_activity.py +180 -0
- server/services/todo_persona_filter.py +73 -0
- server/services/todo_service.py +604 -0
- server/services/topic_ack_service.py +312 -0
- server/services/topic_action_item_ops.py +538 -0
- server/services/topic_comment_kind.py +14 -0
- server/services/topic_comment_service.py +237 -0
- server/services/topic_helpers.py +32 -0
- server/services/topic_lifecycle_service.py +478 -0
- server/services/topic_progress_service.py +40 -0
- server/services/topic_resolve_service.py +234 -0
- server/services/topic_service.py +102 -0
- server/services/topic_work_item_service.py +570 -0
- server/services/webhook_service.py +273 -0
map_client/client.py
ADDED
|
@@ -0,0 +1,1045 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
from urllib.parse import urlparse
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
from map_types import (
|
|
10
|
+
ActionItemCancel,
|
|
11
|
+
AgentCreateResponse,
|
|
12
|
+
AgentRead,
|
|
13
|
+
AgentRole,
|
|
14
|
+
AgentWorkRead,
|
|
15
|
+
AgentWorkSummaryRead,
|
|
16
|
+
AuditLogRead,
|
|
17
|
+
CommentAnchorType,
|
|
18
|
+
CommentCreate,
|
|
19
|
+
CommentRead,
|
|
20
|
+
CommentTreeNode,
|
|
21
|
+
CrossPersonaCallRecord,
|
|
22
|
+
DismissAllMentionsResultRead,
|
|
23
|
+
DismissMentionResultRead,
|
|
24
|
+
EscalationTargetRead,
|
|
25
|
+
ExperimentBundleRead,
|
|
26
|
+
ExperimentComplete,
|
|
27
|
+
ExperimentCreate,
|
|
28
|
+
ExperimentDetailRead,
|
|
29
|
+
ExperimentLockRead,
|
|
30
|
+
ExperimentLockStalledScanRead,
|
|
31
|
+
ExperimentLogCreate,
|
|
32
|
+
ExperimentLogRead,
|
|
33
|
+
ExperimentPhase,
|
|
34
|
+
ExperimentResultDecision,
|
|
35
|
+
ExperimentSummaryRead,
|
|
36
|
+
ExperimentUpdate,
|
|
37
|
+
FeedbackCategory,
|
|
38
|
+
FeedbackStatus,
|
|
39
|
+
GlobalStatusRead,
|
|
40
|
+
InboundEventCreate,
|
|
41
|
+
InboundEventRecordResult,
|
|
42
|
+
LogCreateResponse,
|
|
43
|
+
NotificationCategory,
|
|
44
|
+
NotificationListRead,
|
|
45
|
+
NotificationRead,
|
|
46
|
+
PlanRevise,
|
|
47
|
+
PlanVersionRead,
|
|
48
|
+
PlatformFeedbackCreate,
|
|
49
|
+
PlatformFeedbackRead,
|
|
50
|
+
PlatformFeedbackUpdate,
|
|
51
|
+
ProjectCreate,
|
|
52
|
+
ProjectRead,
|
|
53
|
+
ProjectStatusRead,
|
|
54
|
+
ProjectStatusRevise,
|
|
55
|
+
ProjectStatusVersionRead,
|
|
56
|
+
ProjectUpdate,
|
|
57
|
+
ReviewCreate,
|
|
58
|
+
ReviewItemRead,
|
|
59
|
+
ReviewItemStatus,
|
|
60
|
+
ReviewItemUpdate,
|
|
61
|
+
ReviewRead,
|
|
62
|
+
TodoRead,
|
|
63
|
+
TopicActionItemRead,
|
|
64
|
+
TopicActionItemStatus,
|
|
65
|
+
TopicAdvanceRound,
|
|
66
|
+
TopicCommentCreate,
|
|
67
|
+
TopicCommentRead,
|
|
68
|
+
TopicCommentTreeNode,
|
|
69
|
+
TopicCreate,
|
|
70
|
+
TopicDecisionRead,
|
|
71
|
+
TopicProgressListRead,
|
|
72
|
+
TopicRead,
|
|
73
|
+
TopicReadCursorRead,
|
|
74
|
+
TopicResolve,
|
|
75
|
+
TopicStatus,
|
|
76
|
+
TopicSummaryRead,
|
|
77
|
+
TopicUpdate,
|
|
78
|
+
WebhookCreate,
|
|
79
|
+
WebhookCreateResponse,
|
|
80
|
+
WebhookDeliveryRead,
|
|
81
|
+
WebhookRead,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
from map_client.config import load_config
|
|
85
|
+
from map_client.exceptions import raise_for_status
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
_LOCAL_HOSTS = {"localhost", "127.0.0.1", "::1", ""}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _is_local_url(url: str) -> bool:
|
|
92
|
+
"""判断 URL 的 host 是否指向本机。
|
|
93
|
+
|
|
94
|
+
localhost / 127.0.0.1 / ::1 / 未指定 host 时返回 True。此类场景应
|
|
95
|
+
忽略环境变量代理,避免因 SOCKS 代理初始化失败等问题影响对本地 MAP
|
|
96
|
+
服务的访问。
|
|
97
|
+
"""
|
|
98
|
+
try:
|
|
99
|
+
host = (urlparse(url).hostname or "").lower()
|
|
100
|
+
except Exception:
|
|
101
|
+
return False
|
|
102
|
+
return host in _LOCAL_HOSTS
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class MAPClient:
|
|
106
|
+
"""HTTP client for the Multi-Agent Platform REST API."""
|
|
107
|
+
|
|
108
|
+
def __init__(
|
|
109
|
+
self,
|
|
110
|
+
base_url: str,
|
|
111
|
+
token: str,
|
|
112
|
+
*,
|
|
113
|
+
transport: httpx.BaseTransport | None = None,
|
|
114
|
+
timeout: float = 30.0,
|
|
115
|
+
) -> None:
|
|
116
|
+
if not token:
|
|
117
|
+
raise ValueError("API token is required")
|
|
118
|
+
self.base_url = base_url.rstrip("/")
|
|
119
|
+
self.token = token
|
|
120
|
+
self._transport = transport
|
|
121
|
+
# 本地地址(localhost / 127.0.0.1 / ::1)忽略环境代理,避免因
|
|
122
|
+
# SOCKS 代理初始化失败等影响本地 MAP 服务访问;其他地址沿用
|
|
123
|
+
# 环境代理配置。transport 已显式指定时 trust_env 不生效。
|
|
124
|
+
trust_env = not _is_local_url(self.base_url) if transport is None else True
|
|
125
|
+
self._http = httpx.Client(
|
|
126
|
+
base_url=f"{self.base_url}/api/v1",
|
|
127
|
+
headers={"Authorization": f"Bearer {token}"},
|
|
128
|
+
timeout=timeout,
|
|
129
|
+
transport=transport,
|
|
130
|
+
trust_env=trust_env,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
@classmethod
|
|
134
|
+
def from_env(cls, *, transport: httpx.BaseTransport | None = None) -> MAPClient:
|
|
135
|
+
cfg = load_config()
|
|
136
|
+
if not cfg.get("token"):
|
|
137
|
+
raise ValueError("MAP_TOKEN not set and no token in ~/.map/config.yaml")
|
|
138
|
+
return cls(cfg["api_url"], cfg["token"], transport=transport)
|
|
139
|
+
|
|
140
|
+
@classmethod
|
|
141
|
+
def from_config(
|
|
142
|
+
cls,
|
|
143
|
+
config_path: str | None = None,
|
|
144
|
+
*,
|
|
145
|
+
transport: httpx.BaseTransport | None = None,
|
|
146
|
+
) -> MAPClient:
|
|
147
|
+
cfg = load_config(Path(config_path) if config_path else None)
|
|
148
|
+
if not cfg.get("token"):
|
|
149
|
+
raise ValueError("MAP_TOKEN not set and no token in config")
|
|
150
|
+
return cls(cfg["api_url"], cfg["token"], transport=transport)
|
|
151
|
+
|
|
152
|
+
def close(self) -> None:
|
|
153
|
+
if self._transport is None:
|
|
154
|
+
self._http.close()
|
|
155
|
+
|
|
156
|
+
def __enter__(self) -> MAPClient:
|
|
157
|
+
return self
|
|
158
|
+
|
|
159
|
+
def __exit__(self, *args: object) -> None:
|
|
160
|
+
self.close()
|
|
161
|
+
|
|
162
|
+
def _request(self, method: str, path: str, **kwargs: Any) -> httpx.Response:
|
|
163
|
+
response = self._http.request(method, path, **kwargs)
|
|
164
|
+
if response.status_code >= 400:
|
|
165
|
+
detail = response.text
|
|
166
|
+
error_code: str | None = None
|
|
167
|
+
hint: str | None = None
|
|
168
|
+
retryable: bool | None = None
|
|
169
|
+
if response.content:
|
|
170
|
+
try:
|
|
171
|
+
payload = response.json()
|
|
172
|
+
if isinstance(payload, dict):
|
|
173
|
+
if "detail" in payload:
|
|
174
|
+
detail = str(payload["detail"])
|
|
175
|
+
# I1(d): surface server-side subcodes so callers can
|
|
176
|
+
# branch on structured failure modes instead of
|
|
177
|
+
# pattern-matching the human-readable ``detail``.
|
|
178
|
+
if isinstance(payload.get("error_code"), str):
|
|
179
|
+
error_code = payload["error_code"]
|
|
180
|
+
if isinstance(payload.get("hint"), str):
|
|
181
|
+
hint = payload["hint"]
|
|
182
|
+
if isinstance(payload.get("retryable"), bool):
|
|
183
|
+
retryable = payload["retryable"]
|
|
184
|
+
except Exception:
|
|
185
|
+
detail = response.text
|
|
186
|
+
# P2 #2: 根据 status_code raise 具体子类(NotFound / Conflict 等),
|
|
187
|
+
# 调用方可 catch 子类写语义化处理,不再依赖 if status_code == 404。
|
|
188
|
+
raise_for_status(
|
|
189
|
+
response.status_code,
|
|
190
|
+
detail,
|
|
191
|
+
error_code=error_code,
|
|
192
|
+
hint=hint,
|
|
193
|
+
retryable=retryable,
|
|
194
|
+
)
|
|
195
|
+
return response
|
|
196
|
+
|
|
197
|
+
def _json(self, method: str, path: str, **kwargs: Any) -> Any:
|
|
198
|
+
response = self._request(method, path, **kwargs)
|
|
199
|
+
if response.status_code == 204:
|
|
200
|
+
return None
|
|
201
|
+
return response.json()
|
|
202
|
+
|
|
203
|
+
@staticmethod
|
|
204
|
+
def _total_count(response: httpx.Response) -> int:
|
|
205
|
+
raw = response.headers.get("X-Total-Count")
|
|
206
|
+
if raw is None:
|
|
207
|
+
return 0
|
|
208
|
+
try:
|
|
209
|
+
return int(raw)
|
|
210
|
+
except ValueError:
|
|
211
|
+
return 0
|
|
212
|
+
|
|
213
|
+
# --- agents ---
|
|
214
|
+
|
|
215
|
+
def list_agents(
|
|
216
|
+
self,
|
|
217
|
+
*,
|
|
218
|
+
project_id: uuid.UUID | None = None,
|
|
219
|
+
role: AgentRole | None = None,
|
|
220
|
+
) -> list[AgentRead]:
|
|
221
|
+
params: dict[str, str] = {}
|
|
222
|
+
if project_id is not None:
|
|
223
|
+
params["project_id"] = str(project_id)
|
|
224
|
+
if role is not None:
|
|
225
|
+
params["role"] = role.value
|
|
226
|
+
data = self._json("GET", "/agents", params=params)
|
|
227
|
+
return [AgentRead.model_validate(item) for item in data]
|
|
228
|
+
|
|
229
|
+
def register_agent(
|
|
230
|
+
self,
|
|
231
|
+
name: str,
|
|
232
|
+
role: str = "agent",
|
|
233
|
+
*,
|
|
234
|
+
project_key: str | None = None,
|
|
235
|
+
project_id: uuid.UUID | None = None,
|
|
236
|
+
) -> AgentCreateResponse:
|
|
237
|
+
# cleanup PR5: server expects a JSON body (AgentCreate), not query
|
|
238
|
+
# params. ``json=`` carries the same fields and matches the new
|
|
239
|
+
# contract.
|
|
240
|
+
body: dict[str, object] = {"name": name, "role": role}
|
|
241
|
+
if project_key is not None:
|
|
242
|
+
body["project_key"] = project_key
|
|
243
|
+
if project_id is not None:
|
|
244
|
+
body["project_id"] = str(project_id)
|
|
245
|
+
data = self._json("POST", "/agents", json=body)
|
|
246
|
+
return AgentCreateResponse.model_validate(data)
|
|
247
|
+
|
|
248
|
+
def get_me(self) -> AgentRead:
|
|
249
|
+
return AgentRead.model_validate(self._json("GET", "/agents/me"))
|
|
250
|
+
|
|
251
|
+
def get_escalation_target(
|
|
252
|
+
self, experiment_id: uuid.UUID | None = None
|
|
253
|
+
) -> EscalationTargetRead:
|
|
254
|
+
"""Resolve the escalation contact for a STATE_MACHINE.* error.
|
|
255
|
+
|
|
256
|
+
Calls ``GET /agents/me/escalation-target?experiment_id=...`` and
|
|
257
|
+
returns the chosen agent + the tier that picked it. Used by the
|
|
258
|
+
CLI on ``MAPHTTPError`` to append ``Escalation: @<name>`` to the
|
|
259
|
+
error output.
|
|
260
|
+
|
|
261
|
+
Args:
|
|
262
|
+
experiment_id: When provided, the experiment's
|
|
263
|
+
``escalation_target_agent_id`` override takes precedence.
|
|
264
|
+
When None, the role-based fallback chain runs without it.
|
|
265
|
+
"""
|
|
266
|
+
params: dict[str, Any] = {}
|
|
267
|
+
if experiment_id is not None:
|
|
268
|
+
params["experiment_id"] = str(experiment_id)
|
|
269
|
+
return EscalationTargetRead.model_validate(
|
|
270
|
+
self._json("GET", "/agents/me/escalation-target", params=params or None)
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
def resolve_project_id(
|
|
274
|
+
self,
|
|
275
|
+
project_id: uuid.UUID | None = None,
|
|
276
|
+
*,
|
|
277
|
+
project_key: str | None = None,
|
|
278
|
+
) -> uuid.UUID:
|
|
279
|
+
if project_id is not None:
|
|
280
|
+
return project_id
|
|
281
|
+
if project_key is not None:
|
|
282
|
+
return self.get_project_by_key(project_key).id
|
|
283
|
+
me = self.get_me()
|
|
284
|
+
if me.project_id is not None:
|
|
285
|
+
return me.project_id
|
|
286
|
+
raise ValueError("project_id or project_key is required")
|
|
287
|
+
|
|
288
|
+
# --- projects ---
|
|
289
|
+
|
|
290
|
+
def create_project(
|
|
291
|
+
self,
|
|
292
|
+
project_key: str,
|
|
293
|
+
name: str,
|
|
294
|
+
workspace_path: str,
|
|
295
|
+
description: str | None = None,
|
|
296
|
+
) -> ProjectRead:
|
|
297
|
+
payload = ProjectCreate(
|
|
298
|
+
project_key=project_key,
|
|
299
|
+
name=name,
|
|
300
|
+
workspace_path=workspace_path,
|
|
301
|
+
description=description,
|
|
302
|
+
)
|
|
303
|
+
data = self._json("POST", "/projects", json=payload.model_dump())
|
|
304
|
+
return ProjectRead.model_validate(data)
|
|
305
|
+
|
|
306
|
+
def list_projects(self, *, include_archived: bool = False) -> list[ProjectRead]:
|
|
307
|
+
data = self._json("GET", "/projects", params={"include_archived": include_archived})
|
|
308
|
+
return [ProjectRead.model_validate(item) for item in data]
|
|
309
|
+
|
|
310
|
+
def get_project(self, project_id: uuid.UUID) -> ProjectRead:
|
|
311
|
+
return ProjectRead.model_validate(self._json("GET", f"/projects/{project_id}"))
|
|
312
|
+
|
|
313
|
+
def get_project_by_key(self, project_key: str) -> ProjectRead:
|
|
314
|
+
return ProjectRead.model_validate(self._json("GET", f"/projects/by-key/{project_key}"))
|
|
315
|
+
|
|
316
|
+
def update_project(self, project_id: uuid.UUID, payload: ProjectUpdate) -> ProjectRead:
|
|
317
|
+
data = self._json("PATCH", f"/projects/{project_id}", json=payload.model_dump(exclude_unset=True))
|
|
318
|
+
return ProjectRead.model_validate(data)
|
|
319
|
+
|
|
320
|
+
def get_project_status(self, project_id: uuid.UUID) -> ProjectStatusRead:
|
|
321
|
+
return ProjectStatusRead.model_validate(self._json("GET", f"/projects/{project_id}/status"))
|
|
322
|
+
|
|
323
|
+
def list_project_decisions(self, project_id: uuid.UUID, *, limit: int = 20) -> list[TopicDecisionRead]:
|
|
324
|
+
data = self._json("GET", f"/projects/{project_id}/decisions", params={"limit": limit})
|
|
325
|
+
return [TopicDecisionRead.model_validate(item) for item in data]
|
|
326
|
+
|
|
327
|
+
def list_project_action_items(
|
|
328
|
+
self,
|
|
329
|
+
project_id: uuid.UUID,
|
|
330
|
+
*,
|
|
331
|
+
owner_agent_id: uuid.UUID | None = None,
|
|
332
|
+
status: TopicActionItemStatus | None = None,
|
|
333
|
+
limit: int = 100,
|
|
334
|
+
) -> list[TopicActionItemRead]:
|
|
335
|
+
params: dict[str, Any] = {"limit": limit}
|
|
336
|
+
if owner_agent_id is not None:
|
|
337
|
+
params["owner_agent_id"] = str(owner_agent_id)
|
|
338
|
+
if status is not None:
|
|
339
|
+
params["status"] = status.value
|
|
340
|
+
data = self._json("GET", f"/projects/{project_id}/action-items", params=params)
|
|
341
|
+
return [TopicActionItemRead.model_validate(item) for item in data]
|
|
342
|
+
|
|
343
|
+
def complete_action_item(self, action_item_id: uuid.UUID) -> TopicActionItemRead:
|
|
344
|
+
data = self._json("POST", f"/action-items/{action_item_id}/complete")
|
|
345
|
+
return TopicActionItemRead.model_validate(data)
|
|
346
|
+
|
|
347
|
+
def deliver_action_item(self, action_item_id: uuid.UUID) -> TopicActionItemRead:
|
|
348
|
+
data = self._json("POST", f"/action-items/{action_item_id}/deliver")
|
|
349
|
+
return TopicActionItemRead.model_validate(data)
|
|
350
|
+
|
|
351
|
+
def cancel_action_item(
|
|
352
|
+
self,
|
|
353
|
+
action_item_id: uuid.UUID,
|
|
354
|
+
payload: ActionItemCancel,
|
|
355
|
+
) -> TopicActionItemRead:
|
|
356
|
+
data = self._json(
|
|
357
|
+
"POST",
|
|
358
|
+
f"/action-items/{action_item_id}/cancel",
|
|
359
|
+
json=payload.model_dump(exclude_none=True),
|
|
360
|
+
)
|
|
361
|
+
return TopicActionItemRead.model_validate(data)
|
|
362
|
+
|
|
363
|
+
def link_action_item(
|
|
364
|
+
self,
|
|
365
|
+
action_item_id: uuid.UUID,
|
|
366
|
+
experiment_id: uuid.UUID,
|
|
367
|
+
) -> TopicActionItemRead:
|
|
368
|
+
data = self._json(
|
|
369
|
+
"POST",
|
|
370
|
+
f"/action-items/{action_item_id}/link",
|
|
371
|
+
params={"experiment_id": str(experiment_id)},
|
|
372
|
+
)
|
|
373
|
+
return TopicActionItemRead.model_validate(data)
|
|
374
|
+
|
|
375
|
+
def mark_wake_sent(self, action_item_id: uuid.UUID) -> TopicActionItemRead:
|
|
376
|
+
"""Bump ``wake_count`` + stamp ``last_woken_at`` + audit row.
|
|
377
|
+
|
|
378
|
+
Experiment B / I4 — called by the runtime-waker CLI process when
|
|
379
|
+
``should_wake_action_item`` returns ``'wake'``. Owner or admin only.
|
|
380
|
+
"""
|
|
381
|
+
data = self._json("POST", f"/action-items/{action_item_id}/mark-wake-sent")
|
|
382
|
+
return TopicActionItemRead.model_validate(data)
|
|
383
|
+
|
|
384
|
+
def mark_stale(self, action_item_id: uuid.UUID) -> TopicActionItemRead:
|
|
385
|
+
"""Stamp ``stale_at`` + write the ``action_item.stale`` audit row.
|
|
386
|
+
|
|
387
|
+
Experiment B / I4 — called by the runtime-waker CLI process when
|
|
388
|
+
``should_wake_action_item`` returns ``'stale'`` (4th unanswered wake).
|
|
389
|
+
Admin only.
|
|
390
|
+
"""
|
|
391
|
+
data = self._json("POST", f"/action-items/{action_item_id}/mark-stale")
|
|
392
|
+
return TopicActionItemRead.model_validate(data)
|
|
393
|
+
|
|
394
|
+
def revise_project_status(
|
|
395
|
+
self,
|
|
396
|
+
project_id: uuid.UUID,
|
|
397
|
+
payload: ProjectStatusRevise,
|
|
398
|
+
) -> ProjectStatusVersionRead:
|
|
399
|
+
data = self._json("POST", f"/projects/{project_id}/status/revisions", json=payload.model_dump())
|
|
400
|
+
return ProjectStatusVersionRead.model_validate(data)
|
|
401
|
+
|
|
402
|
+
def list_project_status_versions(self, project_id: uuid.UUID) -> list[ProjectStatusVersionRead]:
|
|
403
|
+
data = self._json("GET", f"/projects/{project_id}/status/versions")
|
|
404
|
+
return [ProjectStatusVersionRead.model_validate(item) for item in data]
|
|
405
|
+
|
|
406
|
+
def get_project_status_version(
|
|
407
|
+
self,
|
|
408
|
+
project_id: uuid.UUID,
|
|
409
|
+
version: int,
|
|
410
|
+
) -> ProjectStatusVersionRead:
|
|
411
|
+
data = self._json("GET", f"/projects/{project_id}/status/versions/{version}")
|
|
412
|
+
return ProjectStatusVersionRead.model_validate(data)
|
|
413
|
+
|
|
414
|
+
# --- experiments ---
|
|
415
|
+
|
|
416
|
+
def create_experiment(self, project_id: uuid.UUID, payload: ExperimentCreate) -> ExperimentSummaryRead:
|
|
417
|
+
data = self._json("POST", f"/projects/{project_id}/experiments", json=payload.model_dump(mode="json"))
|
|
418
|
+
return ExperimentSummaryRead.model_validate(data)
|
|
419
|
+
|
|
420
|
+
def list_experiments(
|
|
421
|
+
self,
|
|
422
|
+
project_id: uuid.UUID,
|
|
423
|
+
*,
|
|
424
|
+
phase: ExperimentPhase | None = None,
|
|
425
|
+
creator_agent_id: uuid.UUID | None = None,
|
|
426
|
+
q: str | None = None,
|
|
427
|
+
page: int = 1,
|
|
428
|
+
page_size: int = 100,
|
|
429
|
+
include_archived: bool = False,
|
|
430
|
+
) -> list[ExperimentSummaryRead]:
|
|
431
|
+
data, _total = self.list_experiments_page(
|
|
432
|
+
project_id,
|
|
433
|
+
phase=phase,
|
|
434
|
+
creator_agent_id=creator_agent_id,
|
|
435
|
+
q=q,
|
|
436
|
+
page=page,
|
|
437
|
+
page_size=page_size,
|
|
438
|
+
include_archived=include_archived,
|
|
439
|
+
)
|
|
440
|
+
return data
|
|
441
|
+
|
|
442
|
+
def list_experiments_page(
|
|
443
|
+
self,
|
|
444
|
+
project_id: uuid.UUID,
|
|
445
|
+
*,
|
|
446
|
+
phase: ExperimentPhase | None = None,
|
|
447
|
+
creator_agent_id: uuid.UUID | None = None,
|
|
448
|
+
q: str | None = None,
|
|
449
|
+
page: int = 1,
|
|
450
|
+
page_size: int = 100,
|
|
451
|
+
include_archived: bool = False,
|
|
452
|
+
) -> tuple[list[ExperimentSummaryRead], int]:
|
|
453
|
+
params: dict[str, Any] = {
|
|
454
|
+
"page": page,
|
|
455
|
+
"page_size": page_size,
|
|
456
|
+
"include_archived": include_archived,
|
|
457
|
+
}
|
|
458
|
+
if phase is not None:
|
|
459
|
+
params["phase"] = phase.value
|
|
460
|
+
if creator_agent_id is not None:
|
|
461
|
+
params["creator_agent_id"] = str(creator_agent_id)
|
|
462
|
+
if q:
|
|
463
|
+
params["q"] = q
|
|
464
|
+
response = self._request("GET", f"/projects/{project_id}/experiments", params=params)
|
|
465
|
+
data = response.json()
|
|
466
|
+
return [ExperimentSummaryRead.model_validate(item) for item in data], self._total_count(response)
|
|
467
|
+
|
|
468
|
+
def get_experiment(self, experiment_id: uuid.UUID) -> ExperimentDetailRead:
|
|
469
|
+
return ExperimentDetailRead.model_validate(self._json("GET", f"/experiments/{experiment_id}"))
|
|
470
|
+
|
|
471
|
+
def get_experiment_bundle(self, experiment_id: uuid.UUID) -> ExperimentBundleRead:
|
|
472
|
+
return ExperimentBundleRead.model_validate(self._json("GET", f"/experiments/{experiment_id}/bundle"))
|
|
473
|
+
|
|
474
|
+
def delete_experiment(self, experiment_id: uuid.UUID) -> None:
|
|
475
|
+
self._request("DELETE", f"/experiments/{experiment_id}")
|
|
476
|
+
|
|
477
|
+
# --- phase transitions ---
|
|
478
|
+
|
|
479
|
+
def submit_for_review(self, experiment_id: uuid.UUID) -> ExperimentSummaryRead:
|
|
480
|
+
data = self._json("POST", f"/experiments/{experiment_id}/submit-review")
|
|
481
|
+
return ExperimentSummaryRead.model_validate(data)
|
|
482
|
+
|
|
483
|
+
def approve_experiment(self, experiment_id: uuid.UUID) -> ExperimentSummaryRead:
|
|
484
|
+
data = self._json("POST", f"/experiments/{experiment_id}/approve")
|
|
485
|
+
return ExperimentSummaryRead.model_validate(data)
|
|
486
|
+
|
|
487
|
+
def withdraw_from_review(self, experiment_id: uuid.UUID) -> ExperimentSummaryRead:
|
|
488
|
+
data = self._json("POST", f"/experiments/{experiment_id}/withdraw")
|
|
489
|
+
return ExperimentSummaryRead.model_validate(data)
|
|
490
|
+
|
|
491
|
+
def cancel_experiment(self, experiment_id: uuid.UUID) -> ExperimentSummaryRead:
|
|
492
|
+
data = self._json("POST", f"/experiments/{experiment_id}/cancel")
|
|
493
|
+
return ExperimentSummaryRead.model_validate(data)
|
|
494
|
+
|
|
495
|
+
def start_experiment(self, experiment_id: uuid.UUID) -> ExperimentSummaryRead:
|
|
496
|
+
data = self._json("POST", f"/experiments/{experiment_id}/start")
|
|
497
|
+
return ExperimentSummaryRead.model_validate(data)
|
|
498
|
+
|
|
499
|
+
def complete_experiment(self, experiment_id: uuid.UUID, payload: ExperimentComplete) -> ExperimentSummaryRead:
|
|
500
|
+
data = self._json(
|
|
501
|
+
"POST",
|
|
502
|
+
f"/experiments/{experiment_id}/complete",
|
|
503
|
+
json=payload.model_dump(mode="json"),
|
|
504
|
+
)
|
|
505
|
+
return ExperimentSummaryRead.model_validate(data)
|
|
506
|
+
|
|
507
|
+
def accept_experiment_result(
|
|
508
|
+
self,
|
|
509
|
+
experiment_id: uuid.UUID,
|
|
510
|
+
payload: ExperimentResultDecision,
|
|
511
|
+
) -> ExperimentSummaryRead:
|
|
512
|
+
data = self._json(
|
|
513
|
+
"POST",
|
|
514
|
+
f"/experiments/{experiment_id}/accept-result",
|
|
515
|
+
json=payload.model_dump(mode="json"),
|
|
516
|
+
)
|
|
517
|
+
return ExperimentSummaryRead.model_validate(data)
|
|
518
|
+
|
|
519
|
+
def reject_experiment_result(
|
|
520
|
+
self,
|
|
521
|
+
experiment_id: uuid.UUID,
|
|
522
|
+
payload: ExperimentResultDecision,
|
|
523
|
+
) -> ExperimentSummaryRead:
|
|
524
|
+
data = self._json(
|
|
525
|
+
"POST",
|
|
526
|
+
f"/experiments/{experiment_id}/reject-result",
|
|
527
|
+
json=payload.model_dump(mode="json"),
|
|
528
|
+
)
|
|
529
|
+
return ExperimentSummaryRead.model_validate(data)
|
|
530
|
+
|
|
531
|
+
# --- execution lock (CP-3) ---
|
|
532
|
+
|
|
533
|
+
def acquire_experiment_lock(
|
|
534
|
+
self, experiment_id: uuid.UUID, *, ttl_seconds: int
|
|
535
|
+
) -> ExperimentLockRead:
|
|
536
|
+
data = self._json(
|
|
537
|
+
"POST",
|
|
538
|
+
f"/experiments/{experiment_id}/lock/acquire",
|
|
539
|
+
json={"ttl_seconds": ttl_seconds},
|
|
540
|
+
)
|
|
541
|
+
return ExperimentLockRead.model_validate(data)
|
|
542
|
+
|
|
543
|
+
def release_experiment_lock(self, experiment_id: uuid.UUID) -> ExperimentLockRead:
|
|
544
|
+
data = self._json("POST", f"/experiments/{experiment_id}/lock/release")
|
|
545
|
+
return ExperimentLockRead.model_validate(data)
|
|
546
|
+
|
|
547
|
+
def force_release_experiment_lock(
|
|
548
|
+
self,
|
|
549
|
+
experiment_id: uuid.UUID,
|
|
550
|
+
*,
|
|
551
|
+
reason: str,
|
|
552
|
+
actor: str | None = None,
|
|
553
|
+
) -> ExperimentLockRead:
|
|
554
|
+
payload: dict[str, Any] = {"reason": reason}
|
|
555
|
+
if actor:
|
|
556
|
+
payload["actor"] = actor
|
|
557
|
+
data = self._json("POST", f"/experiments/{experiment_id}/lock/force-release", json=payload)
|
|
558
|
+
return ExperimentLockRead.model_validate(data)
|
|
559
|
+
|
|
560
|
+
def record_experiment_lock_skip(
|
|
561
|
+
self,
|
|
562
|
+
experiment_id: uuid.UUID,
|
|
563
|
+
*,
|
|
564
|
+
next_attempt_at: str,
|
|
565
|
+
) -> ExperimentLockRead:
|
|
566
|
+
data = self._json(
|
|
567
|
+
"POST",
|
|
568
|
+
f"/experiments/{experiment_id}/lock/skip",
|
|
569
|
+
json={"next_attempt_at": next_attempt_at},
|
|
570
|
+
)
|
|
571
|
+
return ExperimentLockRead.model_validate(data)
|
|
572
|
+
|
|
573
|
+
def scan_stalled_experiment_locks(self) -> ExperimentLockStalledScanRead:
|
|
574
|
+
data = self._json("POST", "/experiments/lock/scan-stalled")
|
|
575
|
+
return ExperimentLockStalledScanRead.model_validate(data)
|
|
576
|
+
|
|
577
|
+
# --- plans ---
|
|
578
|
+
|
|
579
|
+
def list_plans(self, experiment_id: uuid.UUID) -> list[PlanVersionRead]:
|
|
580
|
+
data = self._json("GET", f"/experiments/{experiment_id}/plans")
|
|
581
|
+
return [PlanVersionRead.model_validate(item) for item in data]
|
|
582
|
+
|
|
583
|
+
def get_plan(self, experiment_id: uuid.UUID, version: int) -> PlanVersionRead:
|
|
584
|
+
return PlanVersionRead.model_validate(
|
|
585
|
+
self._json("GET", f"/experiments/{experiment_id}/plans/{version}")
|
|
586
|
+
)
|
|
587
|
+
|
|
588
|
+
def revise_plan(self, experiment_id: uuid.UUID, payload: PlanRevise) -> PlanVersionRead:
|
|
589
|
+
data = self._json("POST", f"/experiments/{experiment_id}/plans", json=payload.model_dump(mode="json"))
|
|
590
|
+
return PlanVersionRead.model_validate(data)
|
|
591
|
+
|
|
592
|
+
# --- reviews ---
|
|
593
|
+
|
|
594
|
+
def create_review(self, experiment_id: uuid.UUID, payload: ReviewCreate) -> ReviewRead:
|
|
595
|
+
data = self._json("POST", f"/experiments/{experiment_id}/reviews", json=payload.model_dump())
|
|
596
|
+
review = ReviewRead.model_validate(data)
|
|
597
|
+
return review
|
|
598
|
+
|
|
599
|
+
def list_reviews(
|
|
600
|
+
self,
|
|
601
|
+
experiment_id: uuid.UUID,
|
|
602
|
+
*,
|
|
603
|
+
include_archived: bool = False,
|
|
604
|
+
plan_version: int | None = None,
|
|
605
|
+
) -> list[ReviewRead]:
|
|
606
|
+
params: dict[str, Any] = {}
|
|
607
|
+
if include_archived:
|
|
608
|
+
params["include_archived"] = "true"
|
|
609
|
+
if plan_version is not None:
|
|
610
|
+
params["plan_version"] = str(plan_version)
|
|
611
|
+
data = self._json(
|
|
612
|
+
"GET",
|
|
613
|
+
f"/experiments/{experiment_id}/reviews",
|
|
614
|
+
params=params or None,
|
|
615
|
+
)
|
|
616
|
+
return [ReviewRead.model_validate(item) for item in data]
|
|
617
|
+
|
|
618
|
+
def withdraw_review(self, experiment_id: uuid.UUID, review_id: uuid.UUID) -> None:
|
|
619
|
+
self._json("POST", f"/experiments/{experiment_id}/reviews/{review_id}/withdraw")
|
|
620
|
+
|
|
621
|
+
def update_review_item(self, item_id: uuid.UUID, status: ReviewItemStatus) -> ReviewItemRead:
|
|
622
|
+
payload = ReviewItemUpdate(status=status)
|
|
623
|
+
data = self._json(
|
|
624
|
+
"PATCH",
|
|
625
|
+
f"/review-items/{item_id}",
|
|
626
|
+
json=payload.model_dump(mode="json"),
|
|
627
|
+
)
|
|
628
|
+
return ReviewItemRead.model_validate(data)
|
|
629
|
+
|
|
630
|
+
# --- comments ---
|
|
631
|
+
|
|
632
|
+
def create_comment(self, experiment_id: uuid.UUID, payload: CommentCreate) -> CommentRead:
|
|
633
|
+
data = self._json(
|
|
634
|
+
"POST",
|
|
635
|
+
f"/experiments/{experiment_id}/comments",
|
|
636
|
+
json=payload.model_dump(mode="json"),
|
|
637
|
+
)
|
|
638
|
+
return CommentRead.model_validate(data)
|
|
639
|
+
|
|
640
|
+
def list_comments(
|
|
641
|
+
self,
|
|
642
|
+
experiment_id: uuid.UUID,
|
|
643
|
+
*,
|
|
644
|
+
tree: bool = False,
|
|
645
|
+
) -> list[CommentRead] | list[CommentTreeNode]:
|
|
646
|
+
data = self._json("GET", f"/experiments/{experiment_id}/comments", params={"tree": tree})
|
|
647
|
+
if tree:
|
|
648
|
+
return [CommentTreeNode.model_validate(item) for item in data]
|
|
649
|
+
return [CommentRead.model_validate(item) for item in data]
|
|
650
|
+
|
|
651
|
+
# --- logs ---
|
|
652
|
+
|
|
653
|
+
def create_log(
|
|
654
|
+
self, experiment_id: uuid.UUID, payload: ExperimentLogCreate
|
|
655
|
+
) -> LogCreateResponse:
|
|
656
|
+
data = self._json("POST", f"/experiments/{experiment_id}/logs", json=payload.model_dump())
|
|
657
|
+
return LogCreateResponse.model_validate(data)
|
|
658
|
+
|
|
659
|
+
def list_logs(self, experiment_id: uuid.UUID) -> list[ExperimentLogRead]:
|
|
660
|
+
data = self._json("GET", f"/experiments/{experiment_id}/logs")
|
|
661
|
+
return [ExperimentLogRead.model_validate(item) for item in data]
|
|
662
|
+
|
|
663
|
+
# 0db51e10 I2(5e): record a cross-persona acceptance_status compare call.
|
|
664
|
+
# Writes an audit_logs row with action="cross_persona_call" + the
|
|
665
|
+
# per-persona visibility diff so R6 metrics / admin audit list can
|
|
666
|
+
# aggregate later. Host persona only (the endpoint enforces
|
|
667
|
+
# ensure_experiment_access).
|
|
668
|
+
def record_cross_persona_call(
|
|
669
|
+
self,
|
|
670
|
+
experiment_id: uuid.UUID,
|
|
671
|
+
*,
|
|
672
|
+
visibility_diff: dict[str, Any] | None = None,
|
|
673
|
+
result_partition_count: int = 0,
|
|
674
|
+
diff_size: int = 0,
|
|
675
|
+
) -> AuditLogRead:
|
|
676
|
+
payload = CrossPersonaCallRecord(
|
|
677
|
+
visibility_diff=visibility_diff or {},
|
|
678
|
+
result_partition_count=result_partition_count,
|
|
679
|
+
diff_size=diff_size,
|
|
680
|
+
)
|
|
681
|
+
data = self._json(
|
|
682
|
+
"POST",
|
|
683
|
+
f"/experiments/{experiment_id}/cross-persona-call",
|
|
684
|
+
json=payload.model_dump(mode="json"),
|
|
685
|
+
)
|
|
686
|
+
return AuditLogRead.model_validate(data)
|
|
687
|
+
|
|
688
|
+
# --- topics ---
|
|
689
|
+
|
|
690
|
+
def create_topic(self, project_id: uuid.UUID, payload: TopicCreate) -> TopicSummaryRead:
|
|
691
|
+
data = self._json("POST", f"/projects/{project_id}/topics", json=payload.model_dump())
|
|
692
|
+
return TopicSummaryRead.model_validate(data)
|
|
693
|
+
|
|
694
|
+
def list_topics(
|
|
695
|
+
self,
|
|
696
|
+
project_id: uuid.UUID,
|
|
697
|
+
*,
|
|
698
|
+
status: TopicStatus | None = None,
|
|
699
|
+
creator_agent_id: uuid.UUID | None = None,
|
|
700
|
+
q: str | None = None,
|
|
701
|
+
page: int = 1,
|
|
702
|
+
page_size: int = 100,
|
|
703
|
+
include_archived: bool = False,
|
|
704
|
+
) -> list[TopicSummaryRead]:
|
|
705
|
+
data, _total = self.list_topics_page(
|
|
706
|
+
project_id,
|
|
707
|
+
status=status,
|
|
708
|
+
creator_agent_id=creator_agent_id,
|
|
709
|
+
q=q,
|
|
710
|
+
page=page,
|
|
711
|
+
page_size=page_size,
|
|
712
|
+
include_archived=include_archived,
|
|
713
|
+
)
|
|
714
|
+
return data
|
|
715
|
+
|
|
716
|
+
def list_topics_page(
|
|
717
|
+
self,
|
|
718
|
+
project_id: uuid.UUID,
|
|
719
|
+
*,
|
|
720
|
+
status: TopicStatus | None = None,
|
|
721
|
+
creator_agent_id: uuid.UUID | None = None,
|
|
722
|
+
q: str | None = None,
|
|
723
|
+
page: int = 1,
|
|
724
|
+
page_size: int = 100,
|
|
725
|
+
include_archived: bool = False,
|
|
726
|
+
) -> tuple[list[TopicSummaryRead], int]:
|
|
727
|
+
params: dict[str, Any] = {
|
|
728
|
+
"page": page,
|
|
729
|
+
"page_size": page_size,
|
|
730
|
+
"include_archived": include_archived,
|
|
731
|
+
}
|
|
732
|
+
if status is not None:
|
|
733
|
+
params["status"] = status.value
|
|
734
|
+
if creator_agent_id is not None:
|
|
735
|
+
params["creator_agent_id"] = str(creator_agent_id)
|
|
736
|
+
if q:
|
|
737
|
+
params["q"] = q
|
|
738
|
+
response = self._request("GET", f"/projects/{project_id}/topics", params=params)
|
|
739
|
+
data = response.json()
|
|
740
|
+
return [TopicSummaryRead.model_validate(item) for item in data], self._total_count(response)
|
|
741
|
+
|
|
742
|
+
def get_topic(self, topic_id: uuid.UUID) -> TopicRead:
|
|
743
|
+
return TopicRead.model_validate(self._json("GET", f"/topics/{topic_id}"))
|
|
744
|
+
|
|
745
|
+
def resolve_topic(self, topic_id: uuid.UUID, payload: TopicResolve) -> TopicDecisionRead:
|
|
746
|
+
data = self._json("POST", f"/topics/{topic_id}/resolve", json=payload.model_dump(mode="json"))
|
|
747
|
+
return TopicDecisionRead.model_validate(data)
|
|
748
|
+
|
|
749
|
+
def advance_topic_round(self, topic_id: uuid.UUID, payload: TopicAdvanceRound | None = None) -> TopicSummaryRead:
|
|
750
|
+
body = (payload or TopicAdvanceRound()).model_dump(mode="json")
|
|
751
|
+
data = self._json("POST", f"/topics/{topic_id}/advance-round", json=body)
|
|
752
|
+
return TopicSummaryRead.model_validate(data)
|
|
753
|
+
|
|
754
|
+
def update_topic(self, topic_id: uuid.UUID, payload: TopicUpdate) -> TopicSummaryRead:
|
|
755
|
+
data = self._json("PATCH", f"/topics/{topic_id}", json=payload.model_dump(exclude_unset=True))
|
|
756
|
+
return TopicSummaryRead.model_validate(data)
|
|
757
|
+
|
|
758
|
+
def update_experiment(
|
|
759
|
+
self, experiment_id: uuid.UUID, payload: ExperimentUpdate
|
|
760
|
+
) -> ExperimentSummaryRead:
|
|
761
|
+
data = self._json(
|
|
762
|
+
"PATCH",
|
|
763
|
+
f"/experiments/{experiment_id}",
|
|
764
|
+
json=payload.model_dump(exclude_unset=True),
|
|
765
|
+
)
|
|
766
|
+
return ExperimentSummaryRead.model_validate(data)
|
|
767
|
+
|
|
768
|
+
def delete_topic(self, topic_id: uuid.UUID) -> None:
|
|
769
|
+
self._request("DELETE", f"/topics/{topic_id}")
|
|
770
|
+
|
|
771
|
+
def close_topic(self, topic_id: uuid.UUID) -> TopicSummaryRead:
|
|
772
|
+
return TopicSummaryRead.model_validate(self._json("POST", f"/topics/{topic_id}/close"))
|
|
773
|
+
|
|
774
|
+
def reopen_topic(self, topic_id: uuid.UUID) -> TopicSummaryRead:
|
|
775
|
+
return TopicSummaryRead.model_validate(self._json("POST", f"/topics/{topic_id}/reopen"))
|
|
776
|
+
|
|
777
|
+
def dismiss_topic(self, topic_id: uuid.UUID) -> TopicSummaryRead:
|
|
778
|
+
return TopicSummaryRead.model_validate(self._json("POST", f"/topics/{topic_id}/dismiss"))
|
|
779
|
+
|
|
780
|
+
def mark_topic_read(self, topic_id: uuid.UUID) -> TopicReadCursorRead:
|
|
781
|
+
data = self._json("POST", f"/agents/me/topics/{topic_id}/read")
|
|
782
|
+
return TopicReadCursorRead.model_validate(data)
|
|
783
|
+
|
|
784
|
+
def create_topic_comment(
|
|
785
|
+
self,
|
|
786
|
+
topic_id: uuid.UUID,
|
|
787
|
+
payload: TopicCommentCreate,
|
|
788
|
+
) -> TopicCommentRead:
|
|
789
|
+
data = self._json("POST", f"/topics/{topic_id}/comments", json=payload.model_dump(mode="json"))
|
|
790
|
+
return TopicCommentRead.model_validate(data)
|
|
791
|
+
|
|
792
|
+
def list_topic_comments(
|
|
793
|
+
self,
|
|
794
|
+
topic_id: uuid.UUID,
|
|
795
|
+
*,
|
|
796
|
+
tree: bool = False,
|
|
797
|
+
) -> list[TopicCommentRead] | list[TopicCommentTreeNode]:
|
|
798
|
+
data = self._json("GET", f"/topics/{topic_id}/comments", params={"tree": tree})
|
|
799
|
+
if tree:
|
|
800
|
+
return [TopicCommentTreeNode.model_validate(item) for item in data]
|
|
801
|
+
return [TopicCommentRead.model_validate(item) for item in data]
|
|
802
|
+
|
|
803
|
+
# --- todos ---
|
|
804
|
+
|
|
805
|
+
def get_todos(self) -> TodoRead:
|
|
806
|
+
return TodoRead.model_validate(self._json("GET", "/agents/me/todos"))
|
|
807
|
+
|
|
808
|
+
def get_agent_work(
|
|
809
|
+
self,
|
|
810
|
+
*,
|
|
811
|
+
notification_limit: int = 50,
|
|
812
|
+
notification_category: NotificationCategory | str | None = "wakeable",
|
|
813
|
+
) -> AgentWorkRead:
|
|
814
|
+
params: dict[str, Any] = {"notification_limit": notification_limit}
|
|
815
|
+
if notification_category is None or notification_category == "all":
|
|
816
|
+
params["notification_category"] = "all"
|
|
817
|
+
elif isinstance(notification_category, NotificationCategory):
|
|
818
|
+
params["notification_category"] = notification_category.value
|
|
819
|
+
else:
|
|
820
|
+
params["notification_category"] = notification_category
|
|
821
|
+
return AgentWorkRead.model_validate(self._json("GET", "/agents/me/work", params=params))
|
|
822
|
+
|
|
823
|
+
def get_agent_work_summary(
|
|
824
|
+
self,
|
|
825
|
+
*,
|
|
826
|
+
include_all_personas: bool = False,
|
|
827
|
+
topics_limit: int = 10,
|
|
828
|
+
experiments_limit: int = 5,
|
|
829
|
+
) -> AgentWorkSummaryRead:
|
|
830
|
+
params: dict[str, Any] = {
|
|
831
|
+
"include_all_personas": str(include_all_personas).lower(),
|
|
832
|
+
"topics_limit": topics_limit,
|
|
833
|
+
"experiments_limit": experiments_limit,
|
|
834
|
+
}
|
|
835
|
+
return AgentWorkSummaryRead.model_validate(
|
|
836
|
+
self._json("GET", "/agents/me/work/summary", params=params)
|
|
837
|
+
)
|
|
838
|
+
|
|
839
|
+
def get_topic_progress(self) -> TopicProgressListRead:
|
|
840
|
+
return TopicProgressListRead.model_validate(self._json("GET", "/agents/me/topic-progress"))
|
|
841
|
+
|
|
842
|
+
def dismiss_mention(self, mention_id: uuid.UUID) -> DismissMentionResultRead:
|
|
843
|
+
data = self._json("POST", f"/agents/me/mentions/{mention_id}/dismiss")
|
|
844
|
+
return DismissMentionResultRead.model_validate(data)
|
|
845
|
+
|
|
846
|
+
def dismiss_all_mentions(self) -> DismissAllMentionsResultRead:
|
|
847
|
+
data = self._json("POST", "/agents/me/mentions/dismiss-all")
|
|
848
|
+
return DismissAllMentionsResultRead.model_validate(data)
|
|
849
|
+
|
|
850
|
+
# --- notifications ---
|
|
851
|
+
|
|
852
|
+
def list_notifications(
|
|
853
|
+
self,
|
|
854
|
+
*,
|
|
855
|
+
unread_only: bool = False,
|
|
856
|
+
category: NotificationCategory | str | None = None,
|
|
857
|
+
target_type: str | None = None,
|
|
858
|
+
limit: int = 50,
|
|
859
|
+
offset: int = 0,
|
|
860
|
+
) -> NotificationListRead:
|
|
861
|
+
params: dict[str, Any] = {"unread_only": unread_only, "limit": limit, "offset": offset}
|
|
862
|
+
if category is not None:
|
|
863
|
+
params["category"] = category.value if isinstance(category, NotificationCategory) else category
|
|
864
|
+
if target_type is not None:
|
|
865
|
+
params["target_type"] = target_type
|
|
866
|
+
data = self._json(
|
|
867
|
+
"GET",
|
|
868
|
+
"/agents/me/notifications",
|
|
869
|
+
params=params,
|
|
870
|
+
)
|
|
871
|
+
return NotificationListRead.model_validate(data)
|
|
872
|
+
|
|
873
|
+
def mark_notification_read(self, notification_id: uuid.UUID) -> NotificationRead:
|
|
874
|
+
data = self._json("POST", f"/notifications/{notification_id}/read")
|
|
875
|
+
return NotificationRead.model_validate(data)
|
|
876
|
+
|
|
877
|
+
def mark_all_notifications_read(self) -> dict[str, int]:
|
|
878
|
+
return self._json("POST", "/agents/me/notifications/read-all")
|
|
879
|
+
|
|
880
|
+
# --- inbound events (runtime-waker dedup gate; D6) ---
|
|
881
|
+
|
|
882
|
+
def record_inbound_event(
|
|
883
|
+
self,
|
|
884
|
+
payload: InboundEventCreate,
|
|
885
|
+
) -> InboundEventRecordResult:
|
|
886
|
+
"""Record that the caller is about to act on ``event_id``.
|
|
887
|
+
|
|
888
|
+
Raises :class:`MAPHTTPError` with ``status_code == 409`` when the
|
|
889
|
+
fingerprint already exists (server gate). Callers should treat 409 as
|
|
890
|
+
"already woken" and skip the resume step (see plan D6 / A1 / A2).
|
|
891
|
+
"""
|
|
892
|
+
data = self._json(
|
|
893
|
+
"POST",
|
|
894
|
+
"/agents/me/inbound-events",
|
|
895
|
+
json=payload.model_dump(mode="json"),
|
|
896
|
+
)
|
|
897
|
+
return InboundEventRecordResult.model_validate(data)
|
|
898
|
+
|
|
899
|
+
# --- webhooks (admin) ---
|
|
900
|
+
|
|
901
|
+
def create_webhook(self, payload: WebhookCreate) -> WebhookCreateResponse:
|
|
902
|
+
data = self._json("POST", "/webhooks", json=payload.model_dump(mode="json"))
|
|
903
|
+
return WebhookCreateResponse.model_validate(data)
|
|
904
|
+
|
|
905
|
+
def list_webhooks(self, project_id: uuid.UUID | None = None) -> list[WebhookRead]:
|
|
906
|
+
params = {"project_id": str(project_id)} if project_id else None
|
|
907
|
+
data = self._json("GET", "/webhooks", params=params)
|
|
908
|
+
return [WebhookRead.model_validate(w) for w in data]
|
|
909
|
+
|
|
910
|
+
def delete_webhook(self, webhook_id: uuid.UUID) -> None:
|
|
911
|
+
self._request("DELETE", f"/webhooks/{webhook_id}")
|
|
912
|
+
|
|
913
|
+
def list_webhook_deliveries(self, webhook_id: uuid.UUID) -> list[WebhookDeliveryRead]:
|
|
914
|
+
data = self._json("GET", f"/webhooks/{webhook_id}/deliveries")
|
|
915
|
+
return [WebhookDeliveryRead.model_validate(d) for d in data]
|
|
916
|
+
|
|
917
|
+
# --- audit ---
|
|
918
|
+
|
|
919
|
+
def list_audit_for_target(self, target_type: str, target_id: uuid.UUID) -> list[AuditLogRead]:
|
|
920
|
+
data = self._json(
|
|
921
|
+
"GET",
|
|
922
|
+
"/audit",
|
|
923
|
+
params={"target_type": target_type, "target_id": str(target_id)},
|
|
924
|
+
)
|
|
925
|
+
return [AuditLogRead.model_validate(a) for a in data]
|
|
926
|
+
|
|
927
|
+
def list_audit_global(
|
|
928
|
+
self,
|
|
929
|
+
*,
|
|
930
|
+
page: int = 1,
|
|
931
|
+
page_size: int = 50,
|
|
932
|
+
kind: str | None = None,
|
|
933
|
+
experiment_id: uuid.UUID | None = None,
|
|
934
|
+
) -> tuple[list[AuditLogRead], int]:
|
|
935
|
+
"""List global audit entries with optional admin filters.
|
|
936
|
+
|
|
937
|
+
``kind`` matches ``AuditLog.action`` (e.g. ``"review_item.mutation"``).
|
|
938
|
+
``experiment_id`` filters to events whose ``payload_json`` carries
|
|
939
|
+
the matching experiment id (used by ``map audit list --experiment <id>``).
|
|
940
|
+
Both filters compose so the CLI can scope down to a single
|
|
941
|
+
experiment × audit kind slice.
|
|
942
|
+
"""
|
|
943
|
+
params: dict[str, str | int] = {"page": page, "page_size": page_size}
|
|
944
|
+
if kind is not None:
|
|
945
|
+
params["kind"] = kind
|
|
946
|
+
if experiment_id is not None:
|
|
947
|
+
params["experiment_id"] = str(experiment_id)
|
|
948
|
+
resp = self._request("GET", "/admin/audit", params=params)
|
|
949
|
+
items = [AuditLogRead.model_validate(a) for a in resp.json()]
|
|
950
|
+
return items, int(resp.headers.get("X-Total-Count", len(items)))
|
|
951
|
+
|
|
952
|
+
# --- status ---
|
|
953
|
+
|
|
954
|
+
def get_global_status(self, project_id: uuid.UUID | None = None) -> GlobalStatusRead:
|
|
955
|
+
params = {"project_id": str(project_id)} if project_id else None
|
|
956
|
+
return GlobalStatusRead.model_validate(self._json("GET", "/status", params=params))
|
|
957
|
+
|
|
958
|
+
# --- feedback ---
|
|
959
|
+
|
|
960
|
+
def submit_feedback(self, payload: PlatformFeedbackCreate) -> PlatformFeedbackRead:
|
|
961
|
+
data = self._json("POST", "/feedback", json=payload.model_dump(mode="json"))
|
|
962
|
+
return PlatformFeedbackRead.model_validate(data)
|
|
963
|
+
|
|
964
|
+
def list_feedback(
|
|
965
|
+
self,
|
|
966
|
+
*,
|
|
967
|
+
status: FeedbackStatus | None = None,
|
|
968
|
+
category: FeedbackCategory | None = None,
|
|
969
|
+
project_id: uuid.UUID | None = None,
|
|
970
|
+
page: int = 1,
|
|
971
|
+
page_size: int = 50,
|
|
972
|
+
include_archived: bool = False,
|
|
973
|
+
) -> list[PlatformFeedbackRead]:
|
|
974
|
+
data, _total = self.list_feedback_page(
|
|
975
|
+
status=status,
|
|
976
|
+
category=category,
|
|
977
|
+
project_id=project_id,
|
|
978
|
+
page=page,
|
|
979
|
+
page_size=page_size,
|
|
980
|
+
include_archived=include_archived,
|
|
981
|
+
)
|
|
982
|
+
return data
|
|
983
|
+
|
|
984
|
+
def list_feedback_page(
|
|
985
|
+
self,
|
|
986
|
+
*,
|
|
987
|
+
status: FeedbackStatus | None = None,
|
|
988
|
+
category: FeedbackCategory | None = None,
|
|
989
|
+
project_id: uuid.UUID | None = None,
|
|
990
|
+
page: int = 1,
|
|
991
|
+
page_size: int = 50,
|
|
992
|
+
include_archived: bool = False,
|
|
993
|
+
) -> tuple[list[PlatformFeedbackRead], int]:
|
|
994
|
+
params: dict[str, Any] = {
|
|
995
|
+
"page": page,
|
|
996
|
+
"page_size": page_size,
|
|
997
|
+
"include_archived": include_archived,
|
|
998
|
+
}
|
|
999
|
+
if status is not None:
|
|
1000
|
+
params["status"] = status.value
|
|
1001
|
+
if category is not None:
|
|
1002
|
+
params["category"] = category.value
|
|
1003
|
+
if project_id is not None:
|
|
1004
|
+
params["project_id"] = str(project_id)
|
|
1005
|
+
response = self._request("GET", "/feedback", params=params)
|
|
1006
|
+
data = response.json()
|
|
1007
|
+
return (
|
|
1008
|
+
[PlatformFeedbackRead.model_validate(item) for item in data],
|
|
1009
|
+
self._total_count(response),
|
|
1010
|
+
)
|
|
1011
|
+
|
|
1012
|
+
def get_feedback(self, feedback_id: uuid.UUID) -> PlatformFeedbackRead:
|
|
1013
|
+
data = self._json("GET", f"/feedback/{feedback_id}")
|
|
1014
|
+
return PlatformFeedbackRead.model_validate(data)
|
|
1015
|
+
|
|
1016
|
+
def update_feedback(
|
|
1017
|
+
self, feedback_id: uuid.UUID, payload: PlatformFeedbackUpdate
|
|
1018
|
+
) -> PlatformFeedbackRead:
|
|
1019
|
+
data = self._json(
|
|
1020
|
+
"PATCH",
|
|
1021
|
+
f"/feedback/{feedback_id}",
|
|
1022
|
+
json=payload.model_dump(exclude_unset=True, mode="json"),
|
|
1023
|
+
)
|
|
1024
|
+
return PlatformFeedbackRead.model_validate(data)
|
|
1025
|
+
|
|
1026
|
+
|
|
1027
|
+
# Re-export types useful for SDK consumers
|
|
1028
|
+
__all__ = [
|
|
1029
|
+
"MAPClient",
|
|
1030
|
+
"CommentAnchorType",
|
|
1031
|
+
"ExperimentPhase",
|
|
1032
|
+
"ReviewItemStatus",
|
|
1033
|
+
"ExperimentCreate",
|
|
1034
|
+
"ExperimentComplete",
|
|
1035
|
+
"ExperimentResultDecision",
|
|
1036
|
+
"ExperimentLogCreate",
|
|
1037
|
+
"PlanRevise",
|
|
1038
|
+
"ReviewCreate",
|
|
1039
|
+
"CommentCreate",
|
|
1040
|
+
"ProjectUpdate",
|
|
1041
|
+
"TopicCreate",
|
|
1042
|
+
"TopicUpdate",
|
|
1043
|
+
"TopicCommentCreate",
|
|
1044
|
+
"TopicStatus",
|
|
1045
|
+
]
|