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.
- zizkadb_sdk-0.2.1/PKG-INFO +117 -0
- zizkadb_sdk-0.2.1/README.md +97 -0
- zizkadb_sdk-0.2.1/pyproject.toml +31 -0
- zizkadb_sdk-0.2.1/setup.cfg +4 -0
- zizkadb_sdk-0.2.1/zizkadb/__init__.py +5 -0
- zizkadb_sdk-0.2.1/zizkadb/client.py +484 -0
- zizkadb_sdk-0.2.1/zizkadb/exceptions.py +18 -0
- zizkadb_sdk-0.2.1/zizkadb/models.py +118 -0
- zizkadb_sdk-0.2.1/zizkadb/telemetry.py +90 -0
- zizkadb_sdk-0.2.1/zizkadb_sdk.egg-info/PKG-INFO +117 -0
- zizkadb_sdk-0.2.1/zizkadb_sdk.egg-info/SOURCES.txt +12 -0
- zizkadb_sdk-0.2.1/zizkadb_sdk.egg-info/dependency_links.txt +1 -0
- zizkadb_sdk-0.2.1/zizkadb_sdk.egg-info/requires.txt +1 -0
- zizkadb_sdk-0.2.1/zizkadb_sdk.egg-info/top_level.txt +1 -0
|
@@ -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,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
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
httpx>=0.27.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
zizkadb
|