blocklog 0.2.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.
- blocklog/__init__.py +78 -0
- blocklog/_global.py +20 -0
- blocklog/_init_fn.py +95 -0
- blocklog/api/approval.py +182 -0
- blocklog/api/compliance.py +162 -0
- blocklog/api/decisions.py +177 -0
- blocklog/api/incidents.py +306 -0
- blocklog/api/replay.py +285 -0
- blocklog/api/traces.py +137 -0
- blocklog/api/verify.py +100 -0
- blocklog/approval.py +119 -0
- blocklog/async_client.py +28 -0
- blocklog/batching/buffer.py +22 -0
- blocklog/client.py +194 -0
- blocklog/compliance.py +95 -0
- blocklog/config.py +17 -0
- blocklog/context/managers.py +15 -0
- blocklog/context/vars.py +13 -0
- blocklog/decorators/__init__.py +4 -0
- blocklog/decorators/agent.py +219 -0
- blocklog/decorators/tool.py +206 -0
- blocklog/incident.py +89 -0
- blocklog/integrations/langchain.py +129 -0
- blocklog/integrations/langgraph.py +3 -0
- blocklog/integrations/openai_agents.py +3 -0
- blocklog/managers/__init__.py +3 -0
- blocklog/managers/decision.py +336 -0
- blocklog/middleware/hooks.py +11 -0
- blocklog/models/events.py +33 -0
- blocklog/models/responses.py +18 -0
- blocklog/replay.py +64 -0
- blocklog/signing/canonical.py +5 -0
- blocklog/signing/ed25519.py +25 -0
- blocklog/transport/auth.py +8 -0
- blocklog/transport/httpx_async.py +39 -0
- blocklog/transport/httpx_sync.py +36 -0
- blocklog/transport/retry.py +26 -0
- blocklog/verify.py +72 -0
- blocklog-0.2.0.dist-info/METADATA +272 -0
- blocklog-0.2.0.dist-info/RECORD +43 -0
- blocklog-0.2.0.dist-info/WHEEL +5 -0
- blocklog-0.2.0.dist-info/licenses/LICENSE +21 -0
- blocklog-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""
|
|
2
|
+
blocklog.api.decisions
|
|
3
|
+
~~~~~~~~~~~~~~~~~~~~~~
|
|
4
|
+
Layer 2 client for AI Decisions.
|
|
5
|
+
|
|
6
|
+
Available via ``client.decisions.*`` or via the ``decision()`` context
|
|
7
|
+
manager internally.
|
|
8
|
+
|
|
9
|
+
Backend endpoints
|
|
10
|
+
-----------------
|
|
11
|
+
- POST /api/v1/decisions
|
|
12
|
+
- GET /api/v1/decisions
|
|
13
|
+
- GET /api/v1/decisions/{id}
|
|
14
|
+
- GET /api/v1/decisions/{id}/verify
|
|
15
|
+
- GET /api/v1/decisions/{id}/timeline
|
|
16
|
+
- GET /api/v1/decisions/{id}/evidence
|
|
17
|
+
- GET /api/v1/decisions/{id}/replay
|
|
18
|
+
"""
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from typing import TYPE_CHECKING, Any
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from blocklog.client import BlocklogClient
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class DecisionsClient:
|
|
28
|
+
"""Manage AI Decision records.
|
|
29
|
+
|
|
30
|
+
Accessed as ``client.decisions``.
|
|
31
|
+
|
|
32
|
+
Examples
|
|
33
|
+
--------
|
|
34
|
+
>>> decision = client.decisions.create(
|
|
35
|
+
... decision_type="BUY",
|
|
36
|
+
... asset="TSLA",
|
|
37
|
+
... confidence=0.91,
|
|
38
|
+
... )
|
|
39
|
+
>>> client.decisions.timeline(decision["id"])
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(self, client: "BlocklogClient") -> None:
|
|
43
|
+
self._client = client
|
|
44
|
+
|
|
45
|
+
def create(
|
|
46
|
+
self,
|
|
47
|
+
decision_type: str,
|
|
48
|
+
*,
|
|
49
|
+
asset: str | None = None,
|
|
50
|
+
confidence: float | None = None,
|
|
51
|
+
metadata: dict[str, Any] | None = None,
|
|
52
|
+
trace_id: str | None = None,
|
|
53
|
+
session_id: str | None = None,
|
|
54
|
+
agent_id: str | None = None,
|
|
55
|
+
) -> dict[str, Any]:
|
|
56
|
+
"""Create a new AI Decision record.
|
|
57
|
+
|
|
58
|
+
Parameters
|
|
59
|
+
----------
|
|
60
|
+
decision_type:
|
|
61
|
+
Short identifier for the decision category (``"BUY"``,
|
|
62
|
+
``"SELL"``, ``"APPROVE"``…).
|
|
63
|
+
asset:
|
|
64
|
+
Asset or resource this decision concerns.
|
|
65
|
+
confidence:
|
|
66
|
+
Model confidence score 0–1.
|
|
67
|
+
metadata:
|
|
68
|
+
Arbitrary extra data to store with the decision.
|
|
69
|
+
trace_id:
|
|
70
|
+
Associate with an existing trace.
|
|
71
|
+
session_id:
|
|
72
|
+
Associate with an existing session.
|
|
73
|
+
agent_id:
|
|
74
|
+
Identifier of the agent making the decision.
|
|
75
|
+
|
|
76
|
+
Returns
|
|
77
|
+
-------
|
|
78
|
+
dict
|
|
79
|
+
The created decision record from the backend.
|
|
80
|
+
"""
|
|
81
|
+
payload: dict[str, Any] = {"decision_type": decision_type}
|
|
82
|
+
if asset is not None:
|
|
83
|
+
payload["asset"] = asset
|
|
84
|
+
if confidence is not None:
|
|
85
|
+
payload["confidence"] = confidence
|
|
86
|
+
if metadata is not None:
|
|
87
|
+
payload["metadata"] = metadata
|
|
88
|
+
if trace_id is not None:
|
|
89
|
+
payload["trace_id"] = trace_id
|
|
90
|
+
if session_id is not None:
|
|
91
|
+
payload["session_id"] = session_id
|
|
92
|
+
if agent_id is not None:
|
|
93
|
+
payload["agent_id"] = agent_id
|
|
94
|
+
|
|
95
|
+
return self._client.retry.run(
|
|
96
|
+
lambda: self._client.transport.request("POST", "/decisions", json=payload)
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
def list(self) -> list[dict[str, Any]]:
|
|
100
|
+
"""List all decisions for the authenticated company.
|
|
101
|
+
|
|
102
|
+
Returns
|
|
103
|
+
-------
|
|
104
|
+
list[dict]
|
|
105
|
+
List of decision records.
|
|
106
|
+
"""
|
|
107
|
+
return self._client.retry.run(
|
|
108
|
+
lambda: self._client.transport.request("GET", "/decisions")
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
def get(self, decision_id: str) -> dict[str, Any]:
|
|
112
|
+
"""Fetch a single decision by ID.
|
|
113
|
+
|
|
114
|
+
Parameters
|
|
115
|
+
----------
|
|
116
|
+
decision_id:
|
|
117
|
+
UUID of the decision.
|
|
118
|
+
"""
|
|
119
|
+
return self._client.retry.run(
|
|
120
|
+
lambda: self._client.transport.request("GET", f"/decisions/{decision_id}")
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
def verify(self, decision_id: str) -> dict[str, Any]:
|
|
124
|
+
"""Verify a decision against the blockchain anchor.
|
|
125
|
+
|
|
126
|
+
Parameters
|
|
127
|
+
----------
|
|
128
|
+
decision_id:
|
|
129
|
+
UUID of the decision to verify.
|
|
130
|
+
|
|
131
|
+
Returns
|
|
132
|
+
-------
|
|
133
|
+
dict
|
|
134
|
+
Verification result with Merkle proof and blockchain anchor
|
|
135
|
+
details.
|
|
136
|
+
"""
|
|
137
|
+
return self._client.retry.run(
|
|
138
|
+
lambda: self._client.transport.request("GET", f"/decisions/{decision_id}/verify")
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
def timeline(self, decision_id: str) -> list[dict[str, Any]]:
|
|
142
|
+
"""Return the chronological event timeline for a decision.
|
|
143
|
+
|
|
144
|
+
Parameters
|
|
145
|
+
----------
|
|
146
|
+
decision_id:
|
|
147
|
+
UUID of the decision.
|
|
148
|
+
"""
|
|
149
|
+
return self._client.retry.run(
|
|
150
|
+
lambda: self._client.transport.request("GET", f"/decisions/{decision_id}/timeline")
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
def evidence(self, decision_id: str) -> dict[str, Any]:
|
|
154
|
+
"""Return the evidence bundle for a decision.
|
|
155
|
+
|
|
156
|
+
Parameters
|
|
157
|
+
----------
|
|
158
|
+
decision_id:
|
|
159
|
+
UUID of the decision.
|
|
160
|
+
"""
|
|
161
|
+
return self._client.retry.run(
|
|
162
|
+
lambda: self._client.transport.request("GET", f"/decisions/{decision_id}/evidence")
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
def replay(self, decision_id: str) -> dict[str, Any]:
|
|
166
|
+
"""Return the replay data attached to a specific decision.
|
|
167
|
+
|
|
168
|
+
For a full forensic replay session, use ``client.replay.create()``.
|
|
169
|
+
|
|
170
|
+
Parameters
|
|
171
|
+
----------
|
|
172
|
+
decision_id:
|
|
173
|
+
UUID of the decision.
|
|
174
|
+
"""
|
|
175
|
+
return self._client.retry.run(
|
|
176
|
+
lambda: self._client.transport.request("GET", f"/decisions/{decision_id}/replay")
|
|
177
|
+
)
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
"""
|
|
2
|
+
blocklog.api.incidents
|
|
3
|
+
~~~~~~~~~~~~~~~~~~~~~~
|
|
4
|
+
Layer 2 client for the full Incident lifecycle.
|
|
5
|
+
|
|
6
|
+
Available via ``client.incidents.*``.
|
|
7
|
+
|
|
8
|
+
Backend endpoints
|
|
9
|
+
-----------------
|
|
10
|
+
- GET /api/v1/incidents
|
|
11
|
+
- POST /api/v1/incidents
|
|
12
|
+
- GET /api/v1/incidents/{id}
|
|
13
|
+
- PATCH /api/v1/incidents/{id}
|
|
14
|
+
- POST /api/v1/incidents/{id}/assign
|
|
15
|
+
- POST /api/v1/incidents/{id}/resolve
|
|
16
|
+
- POST /api/v1/incidents/{id}/close
|
|
17
|
+
- POST /api/v1/incidents/{id}/report
|
|
18
|
+
- GET /api/v1/incidents/{id}/report
|
|
19
|
+
- GET /api/v1/incidents/{id}/annotations
|
|
20
|
+
- POST /api/v1/incidents/{id}/annotations
|
|
21
|
+
- GET /api/v1/incidents/{id}/workspace
|
|
22
|
+
- POST /api/v1/incidents/{id}/workspace
|
|
23
|
+
"""
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
from typing import TYPE_CHECKING, Any
|
|
27
|
+
|
|
28
|
+
if TYPE_CHECKING:
|
|
29
|
+
from blocklog.client import BlocklogClient
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class IncidentHandle:
|
|
33
|
+
"""A live handle to a specific incident returned by ``create()`` or ``get()``.
|
|
34
|
+
|
|
35
|
+
Provides a fluent, object-oriented interface to the incident lifecycle.
|
|
36
|
+
|
|
37
|
+
Examples
|
|
38
|
+
--------
|
|
39
|
+
>>> inc = client.incidents.create(title="Anomalous SELL on AAPL", trace_id="...")
|
|
40
|
+
>>> inc.assign("alice@fund.com")
|
|
41
|
+
>>> inc.annotate("Reviewing related decisions from the same session")
|
|
42
|
+
>>> inc.resolve(summary="False positive — model weights corrected")
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(self, data: dict[str, Any], client: "IncidentsClient") -> None:
|
|
46
|
+
self._data = data
|
|
47
|
+
self._client = client
|
|
48
|
+
self.id: str = str(data.get("id", ""))
|
|
49
|
+
|
|
50
|
+
# -- Convenience accessors --
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def title(self) -> str:
|
|
54
|
+
return self._data.get("title", "")
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def status(self) -> str:
|
|
58
|
+
return self._data.get("status", "")
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def severity(self) -> str:
|
|
62
|
+
return self._data.get("severity", "")
|
|
63
|
+
|
|
64
|
+
# -- Lifecycle methods --
|
|
65
|
+
|
|
66
|
+
def assign(self, assignee: str, *, notes: str | None = None) -> "IncidentHandle":
|
|
67
|
+
"""Assign this incident to a team member."""
|
|
68
|
+
self._data = self._client.assign(self.id, assignee=assignee, notes=notes)
|
|
69
|
+
return self
|
|
70
|
+
|
|
71
|
+
def resolve(
|
|
72
|
+
self,
|
|
73
|
+
summary: str,
|
|
74
|
+
*,
|
|
75
|
+
root_cause: Any = None,
|
|
76
|
+
remediation_actions: Any = None,
|
|
77
|
+
) -> "IncidentHandle":
|
|
78
|
+
"""Mark this incident as resolved."""
|
|
79
|
+
self._data = self._client.resolve(
|
|
80
|
+
self.id,
|
|
81
|
+
summary=summary,
|
|
82
|
+
root_cause=root_cause,
|
|
83
|
+
remediation_actions=remediation_actions,
|
|
84
|
+
)
|
|
85
|
+
return self
|
|
86
|
+
|
|
87
|
+
def close(self, *, notes: str = "", approval_status: str = "approved") -> "IncidentHandle":
|
|
88
|
+
"""Close this incident."""
|
|
89
|
+
self._data = self._client.close(self.id, notes=notes, approval_status=approval_status)
|
|
90
|
+
return self
|
|
91
|
+
|
|
92
|
+
def annotate(self, text: str, *, author: str | None = None) -> dict[str, Any]:
|
|
93
|
+
"""Add an annotation (comment/note) to this incident."""
|
|
94
|
+
return self._client.annotate(self.id, text=text, author=author)
|
|
95
|
+
|
|
96
|
+
def add_workspace_item(
|
|
97
|
+
self,
|
|
98
|
+
item_type: str,
|
|
99
|
+
reference_id: str,
|
|
100
|
+
*,
|
|
101
|
+
label: str | None = None,
|
|
102
|
+
) -> dict[str, Any]:
|
|
103
|
+
"""Add a workspace item (trace, decision, log) to this incident."""
|
|
104
|
+
return self._client.add_workspace_item(
|
|
105
|
+
self.id, item_type=item_type, reference_id=reference_id, label=label
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
def report(self) -> dict[str, Any]:
|
|
109
|
+
"""Generate (or retrieve) the investigation report for this incident."""
|
|
110
|
+
return self._client.report(self.id)
|
|
111
|
+
|
|
112
|
+
def annotations(self) -> list[dict[str, Any]]:
|
|
113
|
+
"""Return all annotations on this incident."""
|
|
114
|
+
return self._client.annotations(self.id)
|
|
115
|
+
|
|
116
|
+
def workspace(self) -> list[dict[str, Any]]:
|
|
117
|
+
"""Return workspace items pinned to this incident."""
|
|
118
|
+
return self._client.workspace_items(self.id)
|
|
119
|
+
|
|
120
|
+
def refresh(self) -> "IncidentHandle":
|
|
121
|
+
"""Re-fetch the latest state from the backend."""
|
|
122
|
+
self._data = self._client.get(self.id)._data
|
|
123
|
+
return self
|
|
124
|
+
|
|
125
|
+
def __repr__(self) -> str:
|
|
126
|
+
return f"<IncidentHandle id={self.id!r} status={self.status!r} severity={self.severity!r}>"
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class IncidentsClient:
|
|
130
|
+
"""Manage the full incident lifecycle.
|
|
131
|
+
|
|
132
|
+
Accessed as ``client.incidents``.
|
|
133
|
+
|
|
134
|
+
Examples
|
|
135
|
+
--------
|
|
136
|
+
>>> inc = client.incidents.create(
|
|
137
|
+
... title="Unexpected SELL on AAPL",
|
|
138
|
+
... trace_id="trace-abc",
|
|
139
|
+
... severity="high",
|
|
140
|
+
... )
|
|
141
|
+
>>> inc.assign("alice@fund.com")
|
|
142
|
+
>>> inc.resolve(summary="False positive — corrected")
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
def __init__(self, client: "BlocklogClient") -> None:
|
|
146
|
+
self._client = client
|
|
147
|
+
|
|
148
|
+
def create(
|
|
149
|
+
self,
|
|
150
|
+
title: str,
|
|
151
|
+
*,
|
|
152
|
+
trace_id: str | None = None,
|
|
153
|
+
severity: str = "medium",
|
|
154
|
+
description: str | None = None,
|
|
155
|
+
metadata: dict[str, Any] | None = None,
|
|
156
|
+
) -> IncidentHandle:
|
|
157
|
+
"""Create a new incident.
|
|
158
|
+
|
|
159
|
+
Parameters
|
|
160
|
+
----------
|
|
161
|
+
title:
|
|
162
|
+
Short title describing the incident.
|
|
163
|
+
trace_id:
|
|
164
|
+
UUID of the trace associated with this incident.
|
|
165
|
+
severity:
|
|
166
|
+
``"low"``, ``"medium"``, ``"high"``, or ``"critical"``.
|
|
167
|
+
description:
|
|
168
|
+
Longer free-text description.
|
|
169
|
+
metadata:
|
|
170
|
+
Arbitrary extra fields.
|
|
171
|
+
|
|
172
|
+
Returns
|
|
173
|
+
-------
|
|
174
|
+
IncidentHandle
|
|
175
|
+
A live handle to the created incident.
|
|
176
|
+
"""
|
|
177
|
+
payload: dict[str, Any] = {"title": title, "severity": severity}
|
|
178
|
+
if trace_id is not None:
|
|
179
|
+
payload["trace_id"] = trace_id
|
|
180
|
+
if description is not None:
|
|
181
|
+
payload["description"] = description
|
|
182
|
+
if metadata is not None:
|
|
183
|
+
payload["metadata"] = metadata
|
|
184
|
+
|
|
185
|
+
data = self._client.retry.run(
|
|
186
|
+
lambda: self._client.transport.request("POST", "/incidents", json=payload)
|
|
187
|
+
)
|
|
188
|
+
return IncidentHandle(data, self)
|
|
189
|
+
|
|
190
|
+
def get(self, incident_id: str) -> IncidentHandle:
|
|
191
|
+
"""Fetch a single incident by ID."""
|
|
192
|
+
data = self._client.retry.run(
|
|
193
|
+
lambda: self._client.transport.request("GET", f"/incidents/{incident_id}")
|
|
194
|
+
)
|
|
195
|
+
return IncidentHandle(data, self)
|
|
196
|
+
|
|
197
|
+
def list(self) -> list[IncidentHandle]:
|
|
198
|
+
"""List all incidents for the authenticated company."""
|
|
199
|
+
items = self._client.retry.run(
|
|
200
|
+
lambda: self._client.transport.request("GET", "/incidents")
|
|
201
|
+
)
|
|
202
|
+
return [IncidentHandle(item, self) for item in (items or [])]
|
|
203
|
+
|
|
204
|
+
def update(self, incident_id: str, **fields: Any) -> dict[str, Any]:
|
|
205
|
+
"""Partially update an incident's fields."""
|
|
206
|
+
return self._client.retry.run(
|
|
207
|
+
lambda: self._client.transport.request("PATCH", f"/incidents/{incident_id}", json=fields)
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
def assign(
|
|
211
|
+
self, incident_id: str, *, assignee: str, notes: str | None = None
|
|
212
|
+
) -> dict[str, Any]:
|
|
213
|
+
"""Assign an incident to a reviewer."""
|
|
214
|
+
payload: dict[str, Any] = {"assignee": assignee}
|
|
215
|
+
if notes:
|
|
216
|
+
payload["notes"] = notes
|
|
217
|
+
return self._client.retry.run(
|
|
218
|
+
lambda: self._client.transport.request("POST", f"/incidents/{incident_id}/assign", json=payload)
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
def resolve(
|
|
222
|
+
self,
|
|
223
|
+
incident_id: str,
|
|
224
|
+
*,
|
|
225
|
+
summary: str,
|
|
226
|
+
root_cause: Any = None,
|
|
227
|
+
remediation_actions: Any = None,
|
|
228
|
+
) -> dict[str, Any]:
|
|
229
|
+
"""Mark an incident as resolved."""
|
|
230
|
+
return self._client.retry.run(
|
|
231
|
+
lambda: self._client.transport.request("POST", f"/incidents/{incident_id}/resolve", json={
|
|
232
|
+
"resolution_summary": summary,
|
|
233
|
+
"root_cause": root_cause,
|
|
234
|
+
"remediation_actions": remediation_actions,
|
|
235
|
+
})
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
def close(
|
|
239
|
+
self,
|
|
240
|
+
incident_id: str,
|
|
241
|
+
*,
|
|
242
|
+
notes: str = "",
|
|
243
|
+
approval_status: str = "approved",
|
|
244
|
+
) -> dict[str, Any]:
|
|
245
|
+
"""Close an incident."""
|
|
246
|
+
return self._client.retry.run(
|
|
247
|
+
lambda: self._client.transport.request("POST", f"/incidents/{incident_id}/close", json={
|
|
248
|
+
"closure_notes": notes,
|
|
249
|
+
"approval_status": approval_status,
|
|
250
|
+
})
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
def report(self, incident_id: str) -> dict[str, Any]:
|
|
254
|
+
"""Generate the investigation report for an incident."""
|
|
255
|
+
return self._client.retry.run(
|
|
256
|
+
lambda: self._client.transport.request("POST", f"/incidents/{incident_id}/report", json={})
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
def get_report(self, incident_id: str) -> dict[str, Any]:
|
|
260
|
+
"""Retrieve a previously generated investigation report."""
|
|
261
|
+
return self._client.retry.run(
|
|
262
|
+
lambda: self._client.transport.request("GET", f"/incidents/{incident_id}/report")
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
def annotate(
|
|
266
|
+
self,
|
|
267
|
+
incident_id: str,
|
|
268
|
+
*,
|
|
269
|
+
text: str,
|
|
270
|
+
author: str | None = None,
|
|
271
|
+
) -> dict[str, Any]:
|
|
272
|
+
"""Add a text annotation to an incident."""
|
|
273
|
+
payload: dict[str, Any] = {"text": text}
|
|
274
|
+
if author:
|
|
275
|
+
payload["author"] = author
|
|
276
|
+
return self._client.retry.run(
|
|
277
|
+
lambda: self._client.transport.request("POST", f"/incidents/{incident_id}/annotations", json=payload)
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
def annotations(self, incident_id: str) -> list[dict[str, Any]]:
|
|
281
|
+
"""Return all annotations on an incident."""
|
|
282
|
+
return self._client.retry.run(
|
|
283
|
+
lambda: self._client.transport.request("GET", f"/incidents/{incident_id}/annotations")
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
def add_workspace_item(
|
|
287
|
+
self,
|
|
288
|
+
incident_id: str,
|
|
289
|
+
*,
|
|
290
|
+
item_type: str,
|
|
291
|
+
reference_id: str,
|
|
292
|
+
label: str | None = None,
|
|
293
|
+
) -> dict[str, Any]:
|
|
294
|
+
"""Pin a workspace item (trace, decision, log) to an incident."""
|
|
295
|
+
payload: dict[str, Any] = {"item_type": item_type, "reference_id": reference_id}
|
|
296
|
+
if label:
|
|
297
|
+
payload["label"] = label
|
|
298
|
+
return self._client.retry.run(
|
|
299
|
+
lambda: self._client.transport.request("POST", f"/incidents/{incident_id}/workspace", json=payload)
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
def workspace_items(self, incident_id: str) -> list[dict[str, Any]]:
|
|
303
|
+
"""Return workspace items pinned to an incident."""
|
|
304
|
+
return self._client.retry.run(
|
|
305
|
+
lambda: self._client.transport.request("GET", f"/incidents/{incident_id}/workspace")
|
|
306
|
+
)
|