zizkadb-sdk 0.2.1__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.
@@ -0,0 +1,117 @@
1
+ Metadata-Version: 2.4
2
+ Name: zizkadb-sdk
3
+ Version: 0.2.1
4
+ Summary: ZizkaDB tells you when your agent stops behaving like itself. Causal lineage, time travel and behavioral baselines for any AI agent.
5
+ Author-email: Zizka AI <hello@zizka.ai>
6
+ License: AGPL-3.0
7
+ Project-URL: Homepage, https://db.zizka.ai
8
+ Project-URL: Documentation, https://db.zizka.ai/docs
9
+ Project-URL: Repository, https://github.com/Zizka-ai/agentdb
10
+ Project-URL: Issues, https://github.com/Zizka-ai/agentdb/issues
11
+ Keywords: ai,agents,database,memory,llm,observability
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Requires-Python: >=3.10
18
+ Description-Content-Type: text/markdown
19
+ Requires-Dist: httpx>=0.27.0
20
+
21
+ # agentdb-sdk
22
+
23
+ Python SDK for [AgentDB](https://agentdb.zizka.ai) — the operational database for AI agents.
24
+
25
+ ## Install
26
+
27
+ ```bash
28
+ pip install agentdb-sdk
29
+ ```
30
+
31
+ > **Note:** There is an unrelated package called `agentdb` on PyPI. Make sure to install `agentdb-sdk` (this package). The import is still `from agentdb import AgentDB`.
32
+
33
+ ## Quickstart
34
+
35
+ ```python
36
+ from agentdb import AgentDB
37
+
38
+ # Cloud (agentdb.zizka.ai)
39
+ db = AgentDB("agdb_live_xxxx")
40
+
41
+ # Self-hosted
42
+ db = AgentDB(host="http://localhost:8000")
43
+ ```
44
+
45
+ ## Log an event
46
+
47
+ ```python
48
+ result = await db.log(
49
+ agent="my-bot",
50
+ event="tool_call",
51
+ data={"tool": "search_web", "query": "competitor pricing"},
52
+ )
53
+ print(result.event_id)
54
+ ```
55
+
56
+ ## Link events causally
57
+
58
+ ```python
59
+ # Log the user message
60
+ msg = await db.log(agent="my-bot", event="user_message",
61
+ data={"text": "why is my bill $200?"})
62
+
63
+ # Log the tool call that happened BECAUSE of the message
64
+ tool = await db.log(agent="my-bot", event="tool_call",
65
+ data={"tool": "get_billing"},
66
+ parent_id=msg.event_id) # <-- causal link
67
+
68
+ # Log the response that happened BECAUSE of the tool call
69
+ await db.log(agent="my-bot", event="agent_response",
70
+ data={"text": "I found an anomaly in your account"},
71
+ parent_id=tool.event_id)
72
+ ```
73
+
74
+ ## Why did something happen?
75
+
76
+ ```python
77
+ chain = await db.why(tool.event_id)
78
+ chain.print()
79
+ # user_message: {'text': 'why is my bill $200?'} [14:32:01]
80
+ # └── tool_call: {'tool': 'get_billing'} [14:32:02]
81
+ # └── agent_response: {'text': '...'} [14:32:03]
82
+ ```
83
+
84
+ ## Semantic search
85
+
86
+ ```python
87
+ results = await db.search("customer angry about billing")
88
+ for event in results:
89
+ print(event.event, event.data)
90
+ ```
91
+
92
+ ## Time travel
93
+
94
+ ```python
95
+ from datetime import datetime
96
+
97
+ state = await db.at("my-bot", datetime(2026, 5, 1, 15, 0))
98
+ print(state.state)
99
+ ```
100
+
101
+ ## Query events
102
+
103
+ ```python
104
+ events = await db.query("my-bot", limit=100, event_type="tool_call")
105
+ ```
106
+
107
+ ## Self-host
108
+
109
+ ```bash
110
+ git clone https://github.com/Zizka-ai/agentdb
111
+ cd agentdb && cp .env.example .env
112
+ docker-compose -f infra/docker-compose.yml up
113
+ ```
114
+
115
+ ## License
116
+
117
+ AGPL-3.0
@@ -0,0 +1,97 @@
1
+ # agentdb-sdk
2
+
3
+ Python SDK for [AgentDB](https://agentdb.zizka.ai) — the operational database for AI agents.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install agentdb-sdk
9
+ ```
10
+
11
+ > **Note:** There is an unrelated package called `agentdb` on PyPI. Make sure to install `agentdb-sdk` (this package). The import is still `from agentdb import AgentDB`.
12
+
13
+ ## Quickstart
14
+
15
+ ```python
16
+ from agentdb import AgentDB
17
+
18
+ # Cloud (agentdb.zizka.ai)
19
+ db = AgentDB("agdb_live_xxxx")
20
+
21
+ # Self-hosted
22
+ db = AgentDB(host="http://localhost:8000")
23
+ ```
24
+
25
+ ## Log an event
26
+
27
+ ```python
28
+ result = await db.log(
29
+ agent="my-bot",
30
+ event="tool_call",
31
+ data={"tool": "search_web", "query": "competitor pricing"},
32
+ )
33
+ print(result.event_id)
34
+ ```
35
+
36
+ ## Link events causally
37
+
38
+ ```python
39
+ # Log the user message
40
+ msg = await db.log(agent="my-bot", event="user_message",
41
+ data={"text": "why is my bill $200?"})
42
+
43
+ # Log the tool call that happened BECAUSE of the message
44
+ tool = await db.log(agent="my-bot", event="tool_call",
45
+ data={"tool": "get_billing"},
46
+ parent_id=msg.event_id) # <-- causal link
47
+
48
+ # Log the response that happened BECAUSE of the tool call
49
+ await db.log(agent="my-bot", event="agent_response",
50
+ data={"text": "I found an anomaly in your account"},
51
+ parent_id=tool.event_id)
52
+ ```
53
+
54
+ ## Why did something happen?
55
+
56
+ ```python
57
+ chain = await db.why(tool.event_id)
58
+ chain.print()
59
+ # user_message: {'text': 'why is my bill $200?'} [14:32:01]
60
+ # └── tool_call: {'tool': 'get_billing'} [14:32:02]
61
+ # └── agent_response: {'text': '...'} [14:32:03]
62
+ ```
63
+
64
+ ## Semantic search
65
+
66
+ ```python
67
+ results = await db.search("customer angry about billing")
68
+ for event in results:
69
+ print(event.event, event.data)
70
+ ```
71
+
72
+ ## Time travel
73
+
74
+ ```python
75
+ from datetime import datetime
76
+
77
+ state = await db.at("my-bot", datetime(2026, 5, 1, 15, 0))
78
+ print(state.state)
79
+ ```
80
+
81
+ ## Query events
82
+
83
+ ```python
84
+ events = await db.query("my-bot", limit=100, event_type="tool_call")
85
+ ```
86
+
87
+ ## Self-host
88
+
89
+ ```bash
90
+ git clone https://github.com/Zizka-ai/agentdb
91
+ cd agentdb && cp .env.example .env
92
+ docker-compose -f infra/docker-compose.yml up
93
+ ```
94
+
95
+ ## License
96
+
97
+ AGPL-3.0
@@ -0,0 +1,31 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "zizkadb-sdk"
7
+ version = "0.2.1"
8
+ description = "ZizkaDB tells you when your agent stops behaving like itself. Causal lineage, time travel and behavioral baselines for any AI agent."
9
+ readme = "README.md"
10
+ license = { text = "AGPL-3.0" }
11
+ requires-python = ">=3.10"
12
+ authors = [{ name = "Zizka AI", email = "hello@zizka.ai" }]
13
+ keywords = ["ai", "agents", "database", "memory", "llm", "observability"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Intended Audience :: Developers",
17
+ "Programming Language :: Python :: 3.10",
18
+ "Programming Language :: Python :: 3.11",
19
+ "Programming Language :: Python :: 3.12",
20
+ ]
21
+ dependencies = ["httpx>=0.27.0"]
22
+
23
+ [project.urls]
24
+ Homepage = "https://db.zizka.ai"
25
+ Documentation = "https://db.zizka.ai/docs"
26
+ Repository = "https://github.com/Zizka-ai/agentdb"
27
+ Issues = "https://github.com/Zizka-ai/agentdb/issues"
28
+
29
+ [tool.setuptools.packages.find]
30
+ where = ["."]
31
+ include = ["zizkadb*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,5 @@
1
+ from .client import ZizkaDB
2
+ from .exceptions import ZizkaDBError, AuthError, NotFoundError
3
+
4
+ __all__ = ["ZizkaDB", "ZizkaDBError", "AuthError", "NotFoundError"]
5
+ __version__ = "0.2.1"
@@ -0,0 +1,484 @@
1
+ """
2
+ ZizkaDB Python SDK
3
+
4
+ Usage:
5
+ from zizkadb import ZizkaDB
6
+
7
+ # Managed (cloud)
8
+ db = ZizkaDB("agdb_live_xxxx")
9
+
10
+ # Self-hosted
11
+ db = ZizkaDB(host="http://localhost:8000")
12
+
13
+ # Log an event
14
+ await db.log(agent="my-bot", event="tool_call", data={"tool": "search"})
15
+
16
+ # Why did something happen?
17
+ chain = await db.why(event_id="...")
18
+ chain.print()
19
+
20
+ # Semantic search
21
+ results = await db.search("what did the agent do about billing")
22
+
23
+ # Time travel
24
+ state = await db.at(agent="my-bot", timestamp=datetime(2026, 5, 1))
25
+ """
26
+
27
+ import httpx
28
+ from datetime import datetime
29
+ from typing import Any
30
+
31
+ from .models import Event, LogResult, CausalChain, AgentState, AgentInfo
32
+ from .exceptions import ZizkaDBError, AuthError, NotFoundError, RateLimitError
33
+ from .telemetry import ping as _telemetry_ping
34
+
35
+ CLOUD_HOST = "https://db.zizka.ai"
36
+
37
+
38
+ class ZizkaDB:
39
+ """
40
+ ZizkaDB client.
41
+
42
+ Args:
43
+ api_key: Your ZizkaDB API key (starts with agdb_live_).
44
+ Get one at db.zizka.ai
45
+ host: URL of your self-hosted instance.
46
+ Defaults to ZizkaDB Cloud if api_key is provided.
47
+ timeout: HTTP timeout in seconds. Default 10.
48
+ """
49
+
50
+ def __init__(
51
+ self,
52
+ api_key: str | None = None,
53
+ host: str | None = None,
54
+ timeout: float = 10.0,
55
+ ):
56
+ if not api_key and not host:
57
+ raise ZizkaDBError(
58
+ "Provide an api_key (cloud) or host (self-hosted).\n"
59
+ " Cloud: ZizkaDB('agdb_live_...')\n"
60
+ " Self-hosted: ZizkaDB(host='http://localhost:8000')"
61
+ )
62
+
63
+ self._api_key = api_key
64
+ self._base_url = host.rstrip("/") if host else CLOUD_HOST
65
+ self._timeout = timeout
66
+ self._client: httpx.AsyncClient | None = None
67
+
68
+ _telemetry_ping(mode="self-hosted" if host else "cloud")
69
+
70
+ # ─────────────────────────────────────────
71
+ # CONTEXT MANAGER
72
+ # ─────────────────────────────────────────
73
+
74
+ async def __aenter__(self):
75
+ self._client = httpx.AsyncClient(
76
+ base_url=self._base_url,
77
+ headers=self._headers(),
78
+ timeout=self._timeout,
79
+ )
80
+ return self
81
+
82
+ async def __aexit__(self, *_):
83
+ if self._client:
84
+ await self._client.aclose()
85
+
86
+ # ─────────────────────────────────────────
87
+ # LOG — log a single event
88
+ # ─────────────────────────────────────────
89
+
90
+ async def log(
91
+ self,
92
+ agent: str,
93
+ event: str,
94
+ data: dict[str, Any],
95
+ parent_id: str | None = None,
96
+ session_id: str | None = None,
97
+ metadata: dict[str, Any] | None = None,
98
+ ) -> LogResult:
99
+ """
100
+ Log an event.
101
+
102
+ Args:
103
+ agent: Your agent's identifier (e.g. "customer-support-bot")
104
+ event: Event type (e.g. "tool_call", "user_message", "error")
105
+ data: Event payload — any dict
106
+ parent_id: ID of the causing event (enables causal lineage)
107
+ session_id: Group related events into a session
108
+ metadata: Optional extra metadata
109
+
110
+ Returns:
111
+ LogResult with event_id, timestamp, sequence_no, checksum
112
+ """
113
+ response = await self._post(
114
+ "/v1/events",
115
+ {
116
+ "agent": agent,
117
+ "event": event,
118
+ "data": data,
119
+ "parent_id": parent_id,
120
+ "session_id": session_id,
121
+ "metadata": metadata,
122
+ },
123
+ )
124
+ return LogResult.from_dict(response)
125
+
126
+ # ─────────────────────────────────────────
127
+ # QUERY — fetch events
128
+ # ─────────────────────────────────────────
129
+
130
+ async def query(
131
+ self,
132
+ agent: str,
133
+ limit: int = 50,
134
+ before: datetime | None = None,
135
+ after: datetime | None = None,
136
+ event_type: str | None = None,
137
+ session_id: str | None = None,
138
+ ) -> list[Event]:
139
+ """
140
+ Query events for an agent.
141
+
142
+ Args:
143
+ agent: Agent identifier
144
+ limit: Max events to return (default 50, max 1000)
145
+ before: Only events before this time
146
+ after: Only events after this time
147
+ event_type: Filter to a specific event type
148
+ session_id: Filter to a specific session
149
+
150
+ Returns:
151
+ List of Event objects, newest first
152
+ """
153
+ params: dict[str, Any] = {"agent": agent, "limit": limit}
154
+ if before:
155
+ params["before"] = before.isoformat()
156
+ if after:
157
+ params["after"] = after.isoformat()
158
+ if event_type:
159
+ params["event_type"] = event_type
160
+ if session_id:
161
+ params["session_id"] = session_id
162
+
163
+ response = await self._get("/v1/events", params)
164
+ return [Event.from_dict(e) for e in response]
165
+
166
+ # ─────────────────────────────────────────
167
+ # WHY — causal chain for an event
168
+ # ─────────────────────────────────────────
169
+
170
+ async def why(self, event_id: str, depth: int = 10) -> CausalChain:
171
+ """
172
+ Get the causal chain for an event.
173
+ Walks parent_id links to show WHY an event happened.
174
+
175
+ Args:
176
+ event_id: The event to explain
177
+ depth: How many levels up to trace (default 10)
178
+
179
+ Returns:
180
+ CausalChain — call .print() to display as a tree
181
+
182
+ Example:
183
+ chain = await db.why("evt_abc123")
184
+ chain.print()
185
+ # user_message: "why is my bill high?"
186
+ # └── tool_call: get_billing(user=123)
187
+ # └── agent_response: "I found an anomaly"
188
+ """
189
+ response = await self._get(
190
+ f"/v1/events/{event_id}/why",
191
+ {"depth": depth},
192
+ )
193
+ return CausalChain(
194
+ event_id=response["event_id"],
195
+ chain_length=response["chain_length"],
196
+ chain=[Event.from_dict(e) for e in response["chain"]],
197
+ )
198
+
199
+ # ─────────────────────────────────────────
200
+ # SEARCH — semantic search
201
+ # ─────────────────────────────────────────
202
+
203
+ async def search(
204
+ self,
205
+ query: str,
206
+ agent: str | None = None,
207
+ limit: int = 10,
208
+ ) -> list[Event]:
209
+ """
210
+ Search agent history using natural language.
211
+
212
+ Args:
213
+ query: Natural language query
214
+ agent: Limit search to a specific agent (optional)
215
+ limit: Number of results (default 10)
216
+
217
+ Returns:
218
+ List of Events sorted by relevance score
219
+
220
+ Example:
221
+ results = await db.search("what happened with billing refunds")
222
+ """
223
+ response = await self._post(
224
+ "/v1/search",
225
+ {"query": query, "agent": agent, "limit": limit},
226
+ )
227
+ return [Event.from_dict(e) for e in response["results"]]
228
+
229
+ # ─────────────────────────────────────────
230
+ # AT — time travel
231
+ # ─────────────────────────────────────────
232
+
233
+ async def at(self, agent: str, timestamp: datetime) -> AgentState:
234
+ """
235
+ Reconstruct agent state at a specific point in time.
236
+
237
+ Args:
238
+ agent: Agent identifier
239
+ timestamp: Point in time to reconstruct state at
240
+
241
+ Returns:
242
+ AgentState with the reconstructed state dict
243
+
244
+ Example:
245
+ from datetime import datetime
246
+ state = await db.at("my-bot", datetime(2026, 5, 1, 15, 0))
247
+ print(state.state)
248
+ """
249
+ response = await self._get(
250
+ "/v1/events/at",
251
+ {"agent": agent, "timestamp": timestamp.isoformat()},
252
+ )
253
+ return AgentState.from_dict(response)
254
+
255
+ # ─────────────────────────────────────────
256
+ # CONTEXT FOR — prompt-ready memory injection
257
+ # ─────────────────────────────────────────
258
+
259
+ async def context_for(
260
+ self,
261
+ agent: str,
262
+ task: str,
263
+ max_tokens: int = 2000,
264
+ session_id: str | None = None,
265
+ ) -> str:
266
+ """
267
+ Get a formatted memory block ready to inject into a system prompt.
268
+ Combines recent events + semantically relevant past events.
269
+
270
+ Args:
271
+ agent: Agent identifier
272
+ task: What the agent is about to do (natural language)
273
+ max_tokens: Token budget for the context block (default 2000)
274
+ session_id: Current session ID to exclude from context
275
+
276
+ Returns:
277
+ A formatted string — paste directly into your system prompt.
278
+
279
+ Example:
280
+ context = await db.context_for(
281
+ agent="support-bot",
282
+ task="user asking about their invoice"
283
+ )
284
+ messages = [
285
+ {"role": "system", "content": f"You are a support agent.\\n\\n{context}"},
286
+ {"role": "user", "content": user_message},
287
+ ]
288
+ """
289
+ response = await self._post(
290
+ "/v1/memory/context",
291
+ {
292
+ "agent": agent,
293
+ "task": task,
294
+ "max_tokens": max_tokens,
295
+ "session_id": session_id,
296
+ },
297
+ )
298
+ return response.get("context", "")
299
+
300
+ async def context_for_full(
301
+ self,
302
+ agent: str,
303
+ task: str,
304
+ max_tokens: int = 2000,
305
+ session_id: str | None = None,
306
+ ) -> dict:
307
+ """
308
+ Same as context_for() but returns the full response including
309
+ source event references and token count.
310
+ """
311
+ return await self._post(
312
+ "/v1/memory/context",
313
+ {
314
+ "agent": agent,
315
+ "task": task,
316
+ "max_tokens": max_tokens,
317
+ "session_id": session_id,
318
+ },
319
+ )
320
+
321
+ # ─────────────────────────────────────────
322
+ # MEMORY DIFF — what changed after a session
323
+ # ─────────────────────────────────────────
324
+
325
+ async def memory_diff(self, session_id: str) -> dict:
326
+ """
327
+ Returns a summary of what happened in a session and what new
328
+ patterns emerged compared to prior sessions.
329
+
330
+ Args:
331
+ session_id: The session to analyse
332
+
333
+ Returns:
334
+ Dict with: summary, event_count, event_types, causal_depth,
335
+ has_errors, new_event_types, top_events
336
+
337
+ Example:
338
+ diff = await db.memory_diff("sess_abc")
339
+ print(diff["summary"])
340
+ # "Session with agent 'support-bot': 12 events over 45s."
341
+ if diff["has_errors"]:
342
+ print("Errors detected:", diff["new_event_types"])
343
+ """
344
+ return await self._get(f"/v1/memory/diff/{session_id}", {})
345
+
346
+ # ─────────────────────────────────────────
347
+ # FORGET — GDPR compliance
348
+ # ─────────────────────────────────────────
349
+
350
+ async def forget(self, filter_key: str, filter_value: str) -> dict:
351
+ """
352
+ Delete all events where data[filter_key] == filter_value.
353
+ Also removes vectors from the search index.
354
+
355
+ Use this for GDPR right-to-erasure requests.
356
+
357
+ Args:
358
+ filter_key: Field in event data to match (e.g. "user_id", "email")
359
+ filter_value: Value to match (e.g. "user_123")
360
+
361
+ Returns:
362
+ Dict with deleted_events count and confirmation message
363
+
364
+ Example:
365
+ result = await db.forget("user_id", "user_123")
366
+ print(result["deleted_events"]) # e.g. 47
367
+ """
368
+ return await self._delete(
369
+ "/v1/memory/forget",
370
+ {"filter_key": filter_key, "filter_value": filter_value},
371
+ )
372
+
373
+ # ─────────────────────────────────────────
374
+ # AGENTS — list agents
375
+ # ─────────────────────────────────────────
376
+
377
+ async def agents(self) -> list[AgentInfo]:
378
+ """List all agents in your project."""
379
+ response = await self._get("/v1/agents", {})
380
+ return [AgentInfo.from_dict(a) for a in response]
381
+
382
+ # ─────────────────────────────────────────
383
+ # BASELINE — behavioral baseline + drift signal
384
+ # ─────────────────────────────────────────
385
+
386
+ async def baseline(self, agent: str, recent_window: int = 50) -> dict:
387
+ """
388
+ Get the behavioral baseline for an agent.
389
+
390
+ Splits this agent's sessions into two windows: the most recent N
391
+ sessions (`recent`) and everything older (`baseline`). Returns
392
+ event-type distribution, parent->child transition distribution,
393
+ average session shape, and error rate for each window, plus a
394
+ drift score (0 = identical, 1 = totally different) and the
395
+ biggest behavioural changes between the two windows.
396
+
397
+ Use this to answer "has this agent started behaving differently
398
+ since I shipped v2?" before users notice.
399
+
400
+ Args:
401
+ agent: Agent identifier
402
+ recent_window: Number of most-recent sessions in the "recent"
403
+ window. Default 50.
404
+
405
+ Returns:
406
+ Dict with: status, sessions, recent_window, baseline, recent,
407
+ drift {score, verdict, biggest_changes, ...}
408
+
409
+ Example:
410
+ b = await db.baseline("support-bot")
411
+ if b["drift"]["score"] > 0.15:
412
+ print("Drift detected:", b["drift"]["biggest_changes"])
413
+ """
414
+ return await self._get(
415
+ f"/v1/agents/{agent}/baseline",
416
+ {"recent_window": recent_window},
417
+ )
418
+
419
+ # ─────────────────────────────────────────
420
+ # HTTP HELPERS
421
+ # ─────────────────────────────────────────
422
+
423
+ def _headers(self) -> dict:
424
+ headers = {"Content-Type": "application/json"}
425
+ if self._api_key:
426
+ headers["Authorization"] = f"Bearer {self._api_key}"
427
+ return headers
428
+
429
+ async def _get_client(self) -> httpx.AsyncClient:
430
+ if self._client:
431
+ return self._client
432
+ return httpx.AsyncClient(
433
+ base_url=self._base_url,
434
+ headers=self._headers(),
435
+ timeout=self._timeout,
436
+ )
437
+
438
+ async def _post(self, path: str, body: dict) -> dict:
439
+ client = await self._get_client()
440
+ try:
441
+ resp = await client.post(path, json=body)
442
+ return self._handle(resp)
443
+ finally:
444
+ if not self._client:
445
+ await client.aclose()
446
+
447
+ async def _get(self, path: str, params: dict) -> Any:
448
+ client = await self._get_client()
449
+ try:
450
+ resp = await client.get(path, params=params)
451
+ return self._handle(resp)
452
+ finally:
453
+ if not self._client:
454
+ await client.aclose()
455
+
456
+ async def _delete(self, path: str, body: dict) -> Any:
457
+ client = await self._get_client()
458
+ try:
459
+ resp = await client.request("DELETE", path, json=body)
460
+ return self._handle(resp)
461
+ finally:
462
+ if not self._client:
463
+ await client.aclose()
464
+
465
+ def _handle(self, resp: httpx.Response) -> Any:
466
+ if resp.status_code == 401:
467
+ raise AuthError(
468
+ "Invalid API key. Check your key at db.zizka.ai/settings/api-keys",
469
+ status_code=401,
470
+ )
471
+ if resp.status_code == 404:
472
+ raise NotFoundError("Resource not found", status_code=404)
473
+ if resp.status_code == 429:
474
+ raise RateLimitError("Rate limit exceeded", status_code=429)
475
+ if resp.status_code >= 400:
476
+ try:
477
+ detail = resp.json().get("detail", resp.text)
478
+ except Exception:
479
+ detail = resp.text
480
+ raise ZizkaDBError(
481
+ f"ZizkaDB error ({resp.status_code}): {detail}",
482
+ status_code=resp.status_code,
483
+ )
484
+ return resp.json()
@@ -0,0 +1,18 @@
1
+ class ZizkaDBError(Exception):
2
+ """Base exception for ZizkaDB SDK."""
3
+
4
+ def __init__(self, message: str, status_code: int | None = None):
5
+ self.status_code = status_code
6
+ super().__init__(message)
7
+
8
+
9
+ class AuthError(ZizkaDBError):
10
+ """Invalid or missing API key."""
11
+
12
+
13
+ class NotFoundError(ZizkaDBError):
14
+ """Event or agent not found."""
15
+
16
+
17
+ class RateLimitError(ZizkaDBError):
18
+ """Too many requests."""
@@ -0,0 +1,118 @@
1
+ from dataclasses import dataclass, field
2
+ from datetime import datetime
3
+ from typing import Any
4
+
5
+
6
+ @dataclass
7
+ class Event:
8
+ event_id: str
9
+ agent: str
10
+ timestamp: datetime
11
+ event: str
12
+ data: dict[str, Any]
13
+ parent_id: str | None = None
14
+ session_id: str | None = None
15
+ sequence_no: int = 0
16
+ score: float | None = None # set on search results
17
+
18
+ @classmethod
19
+ def from_dict(cls, d: dict) -> "Event":
20
+ return cls(
21
+ event_id=d["event_id"],
22
+ agent=d["agent"],
23
+ timestamp=datetime.fromisoformat(d["timestamp"]),
24
+ event=d["event"],
25
+ data=d["data"],
26
+ parent_id=d.get("parent_id"),
27
+ session_id=d.get("session_id"),
28
+ sequence_no=d.get("sequence_no", 0),
29
+ score=d.get("score"),
30
+ )
31
+
32
+ def __repr__(self) -> str:
33
+ return (
34
+ f"Event(id={self.event_id[:8]}... "
35
+ f"agent={self.agent!r} "
36
+ f"event={self.event!r} "
37
+ f"at={self.timestamp.strftime('%H:%M:%S')})"
38
+ )
39
+
40
+
41
+ @dataclass
42
+ class LogResult:
43
+ event_id: str
44
+ timestamp: datetime
45
+ sequence_no: int
46
+ checksum: str
47
+
48
+ @classmethod
49
+ def from_dict(cls, d: dict) -> "LogResult":
50
+ return cls(
51
+ event_id=d["event_id"],
52
+ timestamp=datetime.fromisoformat(d["timestamp"]),
53
+ sequence_no=d["sequence_no"],
54
+ checksum=d["checksum"],
55
+ )
56
+
57
+
58
+ @dataclass
59
+ class CausalChain:
60
+ event_id: str
61
+ chain_length: int
62
+ chain: list[Event]
63
+
64
+ def print(self) -> None:
65
+ """Pretty-print the causal chain as a tree."""
66
+ if not self.chain:
67
+ print("(empty chain)")
68
+ return
69
+
70
+ for i, event in enumerate(self.chain):
71
+ indent = " " * i
72
+ connector = "└── " if i > 0 else ""
73
+ print(
74
+ f"{indent}{connector}"
75
+ f"{event.event}: {_truncate(str(event.data), 60)}"
76
+ f" [{event.timestamp.strftime('%H:%M:%S')}]"
77
+ )
78
+
79
+ def __repr__(self) -> str:
80
+ return f"CausalChain(length={self.chain_length})"
81
+
82
+
83
+ @dataclass
84
+ class AgentState:
85
+ agent: str
86
+ at: datetime
87
+ event_count: int
88
+ state: dict[str, Any]
89
+
90
+ @classmethod
91
+ def from_dict(cls, d: dict) -> "AgentState":
92
+ return cls(
93
+ agent=d["agent"],
94
+ at=datetime.fromisoformat(d["at"]),
95
+ event_count=d["event_count"],
96
+ state=d.get("state", {}),
97
+ )
98
+
99
+
100
+ @dataclass
101
+ class AgentInfo:
102
+ agent: str
103
+ first_seen: datetime
104
+ last_seen: datetime
105
+ event_count: int
106
+
107
+ @classmethod
108
+ def from_dict(cls, d: dict) -> "AgentInfo":
109
+ return cls(
110
+ agent=d["agent"],
111
+ first_seen=datetime.fromisoformat(d["first_seen"]),
112
+ last_seen=datetime.fromisoformat(d["last_seen"]),
113
+ event_count=d["event_count"],
114
+ )
115
+
116
+
117
+ def _truncate(s: str, n: int) -> str:
118
+ return s if len(s) <= n else s[:n] + "..."
@@ -0,0 +1,90 @@
1
+ """
2
+ Anonymous usage telemetry for the ZizkaDB Python SDK.
3
+
4
+ What is sent (once per install, fire-and-forget):
5
+ install_id — random UUID stored in ~/.zizkadb/install_id (never changes)
6
+ sdk_version — e.g. "0.1.0"
7
+ python — e.g. "3.12.3"
8
+ os — e.g. "Linux", "Darwin", "Windows"
9
+ mode — "cloud" or "self-hosted"
10
+
11
+ What is NOT sent: API keys, agent names, event data, IP address, hostname.
12
+
13
+ Opt out at any time:
14
+ export ZIZKADB_TELEMETRY=false # shell
15
+ ZIZKADB_TELEMETRY=false # .env
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import os
21
+ import sys
22
+ import platform
23
+ import threading
24
+ import uuid
25
+ from pathlib import Path
26
+
27
+ _TELEMETRY_URL = "https://db.zizka.ai/v1/telemetry"
28
+ _INSTALL_ID_PATH = Path.home() / ".zizkadb" / "install_id"
29
+ _sent = False # only fire once per process
30
+
31
+
32
+ def _get_or_create_install_id() -> str:
33
+ try:
34
+ _INSTALL_ID_PATH.parent.mkdir(parents=True, exist_ok=True)
35
+ if _INSTALL_ID_PATH.exists():
36
+ iid = _INSTALL_ID_PATH.read_text().strip()
37
+ if iid:
38
+ return iid
39
+ iid = str(uuid.uuid4())
40
+ _INSTALL_ID_PATH.write_text(iid)
41
+ return iid
42
+ except Exception:
43
+ return str(uuid.uuid4()) # ephemeral fallback
44
+
45
+
46
+ def _send(mode: str) -> None:
47
+ try:
48
+ import urllib.request
49
+ import json
50
+
51
+ payload = json.dumps({
52
+ "install_id": _get_or_create_install_id(),
53
+ "sdk": "python",
54
+ "sdk_version": _sdk_version(),
55
+ "python": f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}",
56
+ "os": platform.system(),
57
+ "mode": mode,
58
+ }).encode()
59
+
60
+ req = urllib.request.Request(
61
+ _TELEMETRY_URL,
62
+ data=payload,
63
+ headers={"Content-Type": "application/json"},
64
+ method="POST",
65
+ )
66
+ urllib.request.urlopen(req, timeout=3)
67
+ except Exception:
68
+ pass # never surface telemetry errors
69
+
70
+
71
+ def _sdk_version() -> str:
72
+ try:
73
+ from zizkadb import __version__
74
+ return __version__
75
+ except Exception:
76
+ return "unknown"
77
+
78
+
79
+ def ping(mode: str = "cloud") -> None:
80
+ """Fire a single anonymous telemetry ping in a background thread."""
81
+ global _sent
82
+
83
+ if _sent:
84
+ return
85
+ if os.getenv("ZIZKADB_TELEMETRY", "").lower() in ("false", "0", "no", "off"):
86
+ return
87
+
88
+ _sent = True
89
+ t = threading.Thread(target=_send, args=(mode,), daemon=True)
90
+ t.start()
@@ -0,0 +1,117 @@
1
+ Metadata-Version: 2.4
2
+ Name: zizkadb-sdk
3
+ Version: 0.2.1
4
+ Summary: ZizkaDB tells you when your agent stops behaving like itself. Causal lineage, time travel and behavioral baselines for any AI agent.
5
+ Author-email: Zizka AI <hello@zizka.ai>
6
+ License: AGPL-3.0
7
+ Project-URL: Homepage, https://db.zizka.ai
8
+ Project-URL: Documentation, https://db.zizka.ai/docs
9
+ Project-URL: Repository, https://github.com/Zizka-ai/agentdb
10
+ Project-URL: Issues, https://github.com/Zizka-ai/agentdb/issues
11
+ Keywords: ai,agents,database,memory,llm,observability
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Requires-Python: >=3.10
18
+ Description-Content-Type: text/markdown
19
+ Requires-Dist: httpx>=0.27.0
20
+
21
+ # agentdb-sdk
22
+
23
+ Python SDK for [AgentDB](https://agentdb.zizka.ai) — the operational database for AI agents.
24
+
25
+ ## Install
26
+
27
+ ```bash
28
+ pip install agentdb-sdk
29
+ ```
30
+
31
+ > **Note:** There is an unrelated package called `agentdb` on PyPI. Make sure to install `agentdb-sdk` (this package). The import is still `from agentdb import AgentDB`.
32
+
33
+ ## Quickstart
34
+
35
+ ```python
36
+ from agentdb import AgentDB
37
+
38
+ # Cloud (agentdb.zizka.ai)
39
+ db = AgentDB("agdb_live_xxxx")
40
+
41
+ # Self-hosted
42
+ db = AgentDB(host="http://localhost:8000")
43
+ ```
44
+
45
+ ## Log an event
46
+
47
+ ```python
48
+ result = await db.log(
49
+ agent="my-bot",
50
+ event="tool_call",
51
+ data={"tool": "search_web", "query": "competitor pricing"},
52
+ )
53
+ print(result.event_id)
54
+ ```
55
+
56
+ ## Link events causally
57
+
58
+ ```python
59
+ # Log the user message
60
+ msg = await db.log(agent="my-bot", event="user_message",
61
+ data={"text": "why is my bill $200?"})
62
+
63
+ # Log the tool call that happened BECAUSE of the message
64
+ tool = await db.log(agent="my-bot", event="tool_call",
65
+ data={"tool": "get_billing"},
66
+ parent_id=msg.event_id) # <-- causal link
67
+
68
+ # Log the response that happened BECAUSE of the tool call
69
+ await db.log(agent="my-bot", event="agent_response",
70
+ data={"text": "I found an anomaly in your account"},
71
+ parent_id=tool.event_id)
72
+ ```
73
+
74
+ ## Why did something happen?
75
+
76
+ ```python
77
+ chain = await db.why(tool.event_id)
78
+ chain.print()
79
+ # user_message: {'text': 'why is my bill $200?'} [14:32:01]
80
+ # └── tool_call: {'tool': 'get_billing'} [14:32:02]
81
+ # └── agent_response: {'text': '...'} [14:32:03]
82
+ ```
83
+
84
+ ## Semantic search
85
+
86
+ ```python
87
+ results = await db.search("customer angry about billing")
88
+ for event in results:
89
+ print(event.event, event.data)
90
+ ```
91
+
92
+ ## Time travel
93
+
94
+ ```python
95
+ from datetime import datetime
96
+
97
+ state = await db.at("my-bot", datetime(2026, 5, 1, 15, 0))
98
+ print(state.state)
99
+ ```
100
+
101
+ ## Query events
102
+
103
+ ```python
104
+ events = await db.query("my-bot", limit=100, event_type="tool_call")
105
+ ```
106
+
107
+ ## Self-host
108
+
109
+ ```bash
110
+ git clone https://github.com/Zizka-ai/agentdb
111
+ cd agentdb && cp .env.example .env
112
+ docker-compose -f infra/docker-compose.yml up
113
+ ```
114
+
115
+ ## License
116
+
117
+ AGPL-3.0
@@ -0,0 +1,12 @@
1
+ README.md
2
+ pyproject.toml
3
+ zizkadb/__init__.py
4
+ zizkadb/client.py
5
+ zizkadb/exceptions.py
6
+ zizkadb/models.py
7
+ zizkadb/telemetry.py
8
+ zizkadb_sdk.egg-info/PKG-INFO
9
+ zizkadb_sdk.egg-info/SOURCES.txt
10
+ zizkadb_sdk.egg-info/dependency_links.txt
11
+ zizkadb_sdk.egg-info/requires.txt
12
+ zizkadb_sdk.egg-info/top_level.txt
@@ -0,0 +1 @@
1
+ httpx>=0.27.0
@@ -0,0 +1 @@
1
+ zizkadb