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.
Files changed (43) hide show
  1. blocklog/__init__.py +78 -0
  2. blocklog/_global.py +20 -0
  3. blocklog/_init_fn.py +95 -0
  4. blocklog/api/approval.py +182 -0
  5. blocklog/api/compliance.py +162 -0
  6. blocklog/api/decisions.py +177 -0
  7. blocklog/api/incidents.py +306 -0
  8. blocklog/api/replay.py +285 -0
  9. blocklog/api/traces.py +137 -0
  10. blocklog/api/verify.py +100 -0
  11. blocklog/approval.py +119 -0
  12. blocklog/async_client.py +28 -0
  13. blocklog/batching/buffer.py +22 -0
  14. blocklog/client.py +194 -0
  15. blocklog/compliance.py +95 -0
  16. blocklog/config.py +17 -0
  17. blocklog/context/managers.py +15 -0
  18. blocklog/context/vars.py +13 -0
  19. blocklog/decorators/__init__.py +4 -0
  20. blocklog/decorators/agent.py +219 -0
  21. blocklog/decorators/tool.py +206 -0
  22. blocklog/incident.py +89 -0
  23. blocklog/integrations/langchain.py +129 -0
  24. blocklog/integrations/langgraph.py +3 -0
  25. blocklog/integrations/openai_agents.py +3 -0
  26. blocklog/managers/__init__.py +3 -0
  27. blocklog/managers/decision.py +336 -0
  28. blocklog/middleware/hooks.py +11 -0
  29. blocklog/models/events.py +33 -0
  30. blocklog/models/responses.py +18 -0
  31. blocklog/replay.py +64 -0
  32. blocklog/signing/canonical.py +5 -0
  33. blocklog/signing/ed25519.py +25 -0
  34. blocklog/transport/auth.py +8 -0
  35. blocklog/transport/httpx_async.py +39 -0
  36. blocklog/transport/httpx_sync.py +36 -0
  37. blocklog/transport/retry.py +26 -0
  38. blocklog/verify.py +72 -0
  39. blocklog-0.2.0.dist-info/METADATA +272 -0
  40. blocklog-0.2.0.dist-info/RECORD +43 -0
  41. blocklog-0.2.0.dist-info/WHEEL +5 -0
  42. blocklog-0.2.0.dist-info/licenses/LICENSE +21 -0
  43. 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
+ )