zizkadb-livekit 0.1.0__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_livekit-0.1.0/PKG-INFO +94 -0
- zizkadb_livekit-0.1.0/README.md +79 -0
- zizkadb_livekit-0.1.0/pyproject.toml +26 -0
- zizkadb_livekit-0.1.0/setup.cfg +4 -0
- zizkadb_livekit-0.1.0/zizkadb_livekit/__init__.py +7 -0
- zizkadb_livekit-0.1.0/zizkadb_livekit/_report.py +103 -0
- zizkadb_livekit-0.1.0/zizkadb_livekit/observer.py +315 -0
- zizkadb_livekit-0.1.0/zizkadb_livekit.egg-info/PKG-INFO +94 -0
- zizkadb_livekit-0.1.0/zizkadb_livekit.egg-info/SOURCES.txt +10 -0
- zizkadb_livekit-0.1.0/zizkadb_livekit.egg-info/dependency_links.txt +1 -0
- zizkadb_livekit-0.1.0/zizkadb_livekit.egg-info/requires.txt +6 -0
- zizkadb_livekit-0.1.0/zizkadb_livekit.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: zizkadb-livekit
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: LiveKit Agents observer for ZizkaDB — one session per call, transcript from LiveKit (no audio stored)
|
|
5
|
+
License-Expression: AGPL-3.0-or-later
|
|
6
|
+
Project-URL: Repository, https://github.com/Zizka-ai/ZizkaDB
|
|
7
|
+
Project-URL: Documentation, https://db.zizka.ai/docs
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
Requires-Dist: zizkadb-sdk>=0.2.7
|
|
11
|
+
Requires-Dist: livekit-agents>=1.3.0
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
14
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
15
|
+
|
|
16
|
+
# ZizkaDB + LiveKit Agents
|
|
17
|
+
|
|
18
|
+
**PyPI:** [zizkadb-livekit 0.1.0](https://pypi.org/project/zizkadb-livekit/) · depends on `zizkadb-sdk>=0.2.7` + `livekit-agents>=1.3.0`
|
|
19
|
+
|
|
20
|
+
Log voice calls to ZizkaDB with the same **Activity → Sessions / Events** model as text agents.
|
|
21
|
+
|
|
22
|
+
- **One `session_id` per call** (use LiveKit room name or job id)
|
|
23
|
+
- **Transcript only** — text copied from LiveKit `make_session_report()` (no audio in ZizkaDB)
|
|
24
|
+
- **Backend events** — `tool_call`, `tool_result`, `error`, pipeline events from the report
|
|
25
|
+
- **Causal lineage** — `parent_id` chains for Why? in the dashboard
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install "zizkadb-livekit>=0.1.0"
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Start ZizkaDB (Docker quickstart):
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
curl -fsSL https://raw.githubusercontent.com/Zizka-ai/ZizkaDB/main/scripts/quickstart-remote.sh | bash
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Usage
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
import os
|
|
43
|
+
from livekit.agents import AgentServer, JobContext, AgentSession, Agent
|
|
44
|
+
from zizkadb import ZizkaDB
|
|
45
|
+
from zizkadb_livekit import ZizkaDBLiveKitObserver
|
|
46
|
+
|
|
47
|
+
server = AgentServer()
|
|
48
|
+
|
|
49
|
+
async def on_session_end(ctx: JobContext) -> None:
|
|
50
|
+
await observer.ingest_session_report(ctx)
|
|
51
|
+
|
|
52
|
+
@server.rtc_session(agent_name="support-voice", on_session_end=on_session_end)
|
|
53
|
+
async def entrypoint(ctx: JobContext):
|
|
54
|
+
await ctx.connect()
|
|
55
|
+
|
|
56
|
+
db = ZizkaDB(host=os.getenv("ZIZKADB_HOST", "http://localhost:8000"))
|
|
57
|
+
global observer
|
|
58
|
+
observer = ZizkaDBLiveKitObserver(
|
|
59
|
+
db,
|
|
60
|
+
agent=os.getenv("ZIZKADB_AGENT", "support-voice"),
|
|
61
|
+
session_id=f"call_{ctx.room.name}",
|
|
62
|
+
)
|
|
63
|
+
await observer.log_session_started(room=ctx.room.name, job_id=ctx.job.id)
|
|
64
|
+
|
|
65
|
+
session = AgentSession(...)
|
|
66
|
+
observer.attach(session, job_ctx=ctx) # optional realtime turns
|
|
67
|
+
|
|
68
|
+
await session.start(agent=Agent(instructions="..."), room=ctx.room)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Open **http://localhost:3001** → Activity → your agent → Sessions.
|
|
72
|
+
|
|
73
|
+
## Event types
|
|
74
|
+
|
|
75
|
+
| Event | Source |
|
|
76
|
+
|-------|--------|
|
|
77
|
+
| `session_started` | Call connect |
|
|
78
|
+
| `user_message` | LiveKit user transcript |
|
|
79
|
+
| `assistant_response` | LiveKit agent transcript |
|
|
80
|
+
| `tool_call` / `tool_result` | Tools + report pipeline events |
|
|
81
|
+
| `error` | Failures |
|
|
82
|
+
| `session_ended` | Post-call ingest |
|
|
83
|
+
|
|
84
|
+
Manual helpers: `log_tool_call`, `log_tool_result`, `log_error`.
|
|
85
|
+
|
|
86
|
+
## Environment
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
export ZIZKADB_HOST=http://localhost:8000 # self-host
|
|
90
|
+
export ZIZKADB_AGENT=support-voice
|
|
91
|
+
# Cloud: export ZIZKADB_API_KEY=zizkadb_live_...
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
See [examples/livekit-agent](../../examples/livekit-agent/) for a full sample.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# ZizkaDB + LiveKit Agents
|
|
2
|
+
|
|
3
|
+
**PyPI:** [zizkadb-livekit 0.1.0](https://pypi.org/project/zizkadb-livekit/) · depends on `zizkadb-sdk>=0.2.7` + `livekit-agents>=1.3.0`
|
|
4
|
+
|
|
5
|
+
Log voice calls to ZizkaDB with the same **Activity → Sessions / Events** model as text agents.
|
|
6
|
+
|
|
7
|
+
- **One `session_id` per call** (use LiveKit room name or job id)
|
|
8
|
+
- **Transcript only** — text copied from LiveKit `make_session_report()` (no audio in ZizkaDB)
|
|
9
|
+
- **Backend events** — `tool_call`, `tool_result`, `error`, pipeline events from the report
|
|
10
|
+
- **Causal lineage** — `parent_id` chains for Why? in the dashboard
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pip install "zizkadb-livekit>=0.1.0"
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Start ZizkaDB (Docker quickstart):
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
curl -fsSL https://raw.githubusercontent.com/Zizka-ai/ZizkaDB/main/scripts/quickstart-remote.sh | bash
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
import os
|
|
28
|
+
from livekit.agents import AgentServer, JobContext, AgentSession, Agent
|
|
29
|
+
from zizkadb import ZizkaDB
|
|
30
|
+
from zizkadb_livekit import ZizkaDBLiveKitObserver
|
|
31
|
+
|
|
32
|
+
server = AgentServer()
|
|
33
|
+
|
|
34
|
+
async def on_session_end(ctx: JobContext) -> None:
|
|
35
|
+
await observer.ingest_session_report(ctx)
|
|
36
|
+
|
|
37
|
+
@server.rtc_session(agent_name="support-voice", on_session_end=on_session_end)
|
|
38
|
+
async def entrypoint(ctx: JobContext):
|
|
39
|
+
await ctx.connect()
|
|
40
|
+
|
|
41
|
+
db = ZizkaDB(host=os.getenv("ZIZKADB_HOST", "http://localhost:8000"))
|
|
42
|
+
global observer
|
|
43
|
+
observer = ZizkaDBLiveKitObserver(
|
|
44
|
+
db,
|
|
45
|
+
agent=os.getenv("ZIZKADB_AGENT", "support-voice"),
|
|
46
|
+
session_id=f"call_{ctx.room.name}",
|
|
47
|
+
)
|
|
48
|
+
await observer.log_session_started(room=ctx.room.name, job_id=ctx.job.id)
|
|
49
|
+
|
|
50
|
+
session = AgentSession(...)
|
|
51
|
+
observer.attach(session, job_ctx=ctx) # optional realtime turns
|
|
52
|
+
|
|
53
|
+
await session.start(agent=Agent(instructions="..."), room=ctx.room)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Open **http://localhost:3001** → Activity → your agent → Sessions.
|
|
57
|
+
|
|
58
|
+
## Event types
|
|
59
|
+
|
|
60
|
+
| Event | Source |
|
|
61
|
+
|-------|--------|
|
|
62
|
+
| `session_started` | Call connect |
|
|
63
|
+
| `user_message` | LiveKit user transcript |
|
|
64
|
+
| `assistant_response` | LiveKit agent transcript |
|
|
65
|
+
| `tool_call` / `tool_result` | Tools + report pipeline events |
|
|
66
|
+
| `error` | Failures |
|
|
67
|
+
| `session_ended` | Post-call ingest |
|
|
68
|
+
|
|
69
|
+
Manual helpers: `log_tool_call`, `log_tool_result`, `log_error`.
|
|
70
|
+
|
|
71
|
+
## Environment
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
export ZIZKADB_HOST=http://localhost:8000 # self-host
|
|
75
|
+
export ZIZKADB_AGENT=support-voice
|
|
76
|
+
# Cloud: export ZIZKADB_API_KEY=zizkadb_live_...
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
See [examples/livekit-agent](../../examples/livekit-agent/) for a full sample.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "zizkadb-livekit"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "LiveKit Agents observer for ZizkaDB — one session per call, transcript from LiveKit (no audio stored)"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "AGPL-3.0-or-later"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
dependencies = [
|
|
13
|
+
"zizkadb-sdk>=0.2.7",
|
|
14
|
+
"livekit-agents>=1.3.0",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[project.optional-dependencies]
|
|
18
|
+
dev = ["pytest>=8.0", "pytest-asyncio>=0.23"]
|
|
19
|
+
|
|
20
|
+
[project.urls]
|
|
21
|
+
Repository = "https://github.com/Zizka-ai/ZizkaDB"
|
|
22
|
+
Documentation = "https://db.zizka.ai/docs"
|
|
23
|
+
|
|
24
|
+
[tool.setuptools.packages.find]
|
|
25
|
+
where = ["."]
|
|
26
|
+
include = ["zizkadb_livekit*"]
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Parse LiveKit SessionReport dicts for ZizkaDB event ingest."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def normalize_report(report: dict[str, Any]) -> dict[str, Any]:
|
|
9
|
+
"""Accept make_session_report().to_dict() or an equivalent structure."""
|
|
10
|
+
if not isinstance(report, dict):
|
|
11
|
+
raise TypeError("report must be a dict from SessionReport.to_dict()")
|
|
12
|
+
return report
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def chat_items(report: dict[str, Any]) -> list[dict[str, Any]]:
|
|
16
|
+
history = report.get("chat_history") or {}
|
|
17
|
+
if isinstance(history, list):
|
|
18
|
+
items = history
|
|
19
|
+
elif isinstance(history, dict):
|
|
20
|
+
items = history.get("items") or []
|
|
21
|
+
else:
|
|
22
|
+
items = []
|
|
23
|
+
out: list[dict[str, Any]] = []
|
|
24
|
+
for raw in items:
|
|
25
|
+
if not isinstance(raw, dict):
|
|
26
|
+
continue
|
|
27
|
+
item = dict(raw)
|
|
28
|
+
item_id = item.get("id") or item.get("item_id")
|
|
29
|
+
if item_id is not None:
|
|
30
|
+
item["id"] = str(item_id)
|
|
31
|
+
out.append(item)
|
|
32
|
+
out.sort(key=_sort_key)
|
|
33
|
+
return out
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def backend_events(report: dict[str, Any]) -> list[dict[str, Any]]:
|
|
37
|
+
"""Non-transcript pipeline events from the session report."""
|
|
38
|
+
rows: list[dict[str, Any]] = []
|
|
39
|
+
for raw in report.get("events") or []:
|
|
40
|
+
if not isinstance(raw, dict):
|
|
41
|
+
continue
|
|
42
|
+
if raw.get("type") == "metrics_collected":
|
|
43
|
+
continue
|
|
44
|
+
rows.append(dict(raw))
|
|
45
|
+
rows.sort(key=_sort_key)
|
|
46
|
+
return rows
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def item_text(item: dict[str, Any]) -> str:
|
|
50
|
+
content = item.get("content")
|
|
51
|
+
if isinstance(content, str):
|
|
52
|
+
return content
|
|
53
|
+
if isinstance(content, list):
|
|
54
|
+
parts: list[str] = []
|
|
55
|
+
for part in content:
|
|
56
|
+
if isinstance(part, str):
|
|
57
|
+
parts.append(part)
|
|
58
|
+
elif isinstance(part, dict):
|
|
59
|
+
text = part.get("text") or part.get("content")
|
|
60
|
+
if text:
|
|
61
|
+
parts.append(str(text))
|
|
62
|
+
return "\n".join(parts)
|
|
63
|
+
if content is not None:
|
|
64
|
+
return str(content)
|
|
65
|
+
return ""
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def item_role(item: dict[str, Any]) -> str:
|
|
69
|
+
role = (item.get("role") or item.get("speaker") or "unknown").lower()
|
|
70
|
+
if role in ("assistant", "agent"):
|
|
71
|
+
return "assistant"
|
|
72
|
+
if role in ("user", "human"):
|
|
73
|
+
return "user"
|
|
74
|
+
return role
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def zizka_event_for_role(role: str) -> str | None:
|
|
78
|
+
if role == "user":
|
|
79
|
+
return "user_message"
|
|
80
|
+
if role == "assistant":
|
|
81
|
+
return "assistant_response"
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def zizka_event_for_report_event(ev: dict[str, Any]) -> str:
|
|
86
|
+
et = (ev.get("type") or ev.get("event") or "livekit_event").lower()
|
|
87
|
+
if "tool" in et and "result" in et:
|
|
88
|
+
return "tool_result"
|
|
89
|
+
if "tool" in et or "function" in et:
|
|
90
|
+
return "tool_call"
|
|
91
|
+
if "error" in et or "fail" in et:
|
|
92
|
+
return "error"
|
|
93
|
+
if "api" in et or "http" in et:
|
|
94
|
+
return "api_call"
|
|
95
|
+
return "livekit_event"
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _sort_key(row: dict[str, Any]) -> float:
|
|
99
|
+
for key in ("created_at", "timestamp", "started_at"):
|
|
100
|
+
val = row.get(key)
|
|
101
|
+
if isinstance(val, (int, float)):
|
|
102
|
+
return float(val)
|
|
103
|
+
return 0.0
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
"""
|
|
2
|
+
LiveKit Agents → ZizkaDB observer.
|
|
3
|
+
|
|
4
|
+
Maps each voice call to one ZizkaDB session (session_id) and logs transcript
|
|
5
|
+
turns plus backend events. Transcript text is copied from LiveKit's session
|
|
6
|
+
report — ZizkaDB does not record or store audio.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
from typing import Any, Callable
|
|
13
|
+
|
|
14
|
+
from zizkadb import ZizkaDB
|
|
15
|
+
from zizkadb.models import LogResult
|
|
16
|
+
|
|
17
|
+
from ._report import (
|
|
18
|
+
backend_events,
|
|
19
|
+
chat_items,
|
|
20
|
+
item_role,
|
|
21
|
+
item_text,
|
|
22
|
+
normalize_report,
|
|
23
|
+
zizka_event_for_report_event,
|
|
24
|
+
zizka_event_for_role,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
log = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
SessionIdFactory = Callable[[Any], str]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ZizkaDBLiveKitObserver:
|
|
33
|
+
"""
|
|
34
|
+
Log LiveKit voice calls to ZizkaDB using the same Activity model as text agents:
|
|
35
|
+
one session_id per call, transcript as user_message / assistant_response events.
|
|
36
|
+
|
|
37
|
+
Usage:
|
|
38
|
+
db = ZizkaDB(host="http://localhost:8000")
|
|
39
|
+
observer = ZizkaDBLiveKitObserver(db, agent="voice-support", session_id=ctx.room.name)
|
|
40
|
+
await observer.log_session_started(room=ctx.room.name, job_id=ctx.job.id)
|
|
41
|
+
observer.attach(session) # optional realtime turns
|
|
42
|
+
# in on_session_end:
|
|
43
|
+
await observer.ingest_session_report(ctx)
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
db: ZizkaDB,
|
|
49
|
+
agent: str,
|
|
50
|
+
session_id: str,
|
|
51
|
+
*,
|
|
52
|
+
session_id_from: SessionIdFactory | None = None,
|
|
53
|
+
) -> None:
|
|
54
|
+
self.db = db
|
|
55
|
+
self.agent = agent
|
|
56
|
+
self.session_id = session_id
|
|
57
|
+
self.session_id_from = session_id_from
|
|
58
|
+
self.last_event_id: str | None = None
|
|
59
|
+
self._seen_item_ids: set[str] = set()
|
|
60
|
+
self._session_started = False
|
|
61
|
+
self._session_ended = False
|
|
62
|
+
self._realtime_attached = False
|
|
63
|
+
self._last_result: LogResult | None = None
|
|
64
|
+
self._report_ingested = False
|
|
65
|
+
|
|
66
|
+
async def log_session_started(
|
|
67
|
+
self,
|
|
68
|
+
*,
|
|
69
|
+
room: str | None = None,
|
|
70
|
+
job_id: str | None = None,
|
|
71
|
+
**extra: Any,
|
|
72
|
+
) -> LogResult:
|
|
73
|
+
if self._session_started:
|
|
74
|
+
assert self._last_result is not None
|
|
75
|
+
return self._last_result
|
|
76
|
+
data: dict[str, Any] = {"source": "livekit", **extra}
|
|
77
|
+
if room is not None:
|
|
78
|
+
data["room"] = room
|
|
79
|
+
if job_id is not None:
|
|
80
|
+
data["job_id"] = job_id
|
|
81
|
+
result = await self._log(
|
|
82
|
+
event="session_started",
|
|
83
|
+
data=data,
|
|
84
|
+
parent_id=None,
|
|
85
|
+
)
|
|
86
|
+
self._session_started = True
|
|
87
|
+
return result
|
|
88
|
+
|
|
89
|
+
async def log_session_ended(
|
|
90
|
+
self,
|
|
91
|
+
*,
|
|
92
|
+
turn_count: int | None = None,
|
|
93
|
+
duration_s: float | None = None,
|
|
94
|
+
livekit_job_id: str | None = None,
|
|
95
|
+
**extra: Any,
|
|
96
|
+
) -> LogResult:
|
|
97
|
+
data: dict[str, Any] = {"source": "livekit", **extra}
|
|
98
|
+
if turn_count is not None:
|
|
99
|
+
data["turn_count"] = turn_count
|
|
100
|
+
if duration_s is not None:
|
|
101
|
+
data["duration_s"] = duration_s
|
|
102
|
+
if livekit_job_id is not None:
|
|
103
|
+
data["livekit_job_id"] = livekit_job_id
|
|
104
|
+
result = await self._log(
|
|
105
|
+
event="session_ended",
|
|
106
|
+
data=data,
|
|
107
|
+
parent_id=self.last_event_id,
|
|
108
|
+
)
|
|
109
|
+
self._session_ended = True
|
|
110
|
+
return result
|
|
111
|
+
|
|
112
|
+
def attach(self, session: Any, job_ctx: Any | None = None) -> None:
|
|
113
|
+
if self._realtime_attached:
|
|
114
|
+
return
|
|
115
|
+
|
|
116
|
+
if job_ctx is not None and self.session_id_from is not None:
|
|
117
|
+
self.session_id = self.session_id_from(job_ctx)
|
|
118
|
+
|
|
119
|
+
def on_conversation_item_added(ev: Any) -> None:
|
|
120
|
+
try:
|
|
121
|
+
import asyncio
|
|
122
|
+
|
|
123
|
+
asyncio.get_running_loop().create_task(self._on_conversation_item(ev))
|
|
124
|
+
except RuntimeError:
|
|
125
|
+
log.debug("conversation_item_added outside running loop; skipped")
|
|
126
|
+
|
|
127
|
+
if hasattr(session, "on"):
|
|
128
|
+
session.on("conversation_item_added", on_conversation_item_added)
|
|
129
|
+
self._realtime_attached = True
|
|
130
|
+
|
|
131
|
+
async def ingest_session_report(self, ctx: Any) -> list[LogResult]:
|
|
132
|
+
make_report = getattr(ctx, "make_session_report", None)
|
|
133
|
+
if make_report is None:
|
|
134
|
+
raise TypeError("ctx must provide make_session_report() (LiveKit JobContext)")
|
|
135
|
+
report = make_report()
|
|
136
|
+
to_dict = getattr(report, "to_dict", None)
|
|
137
|
+
if to_dict is None:
|
|
138
|
+
raise TypeError("SessionReport must provide to_dict()")
|
|
139
|
+
return await self.ingest_report(to_dict())
|
|
140
|
+
|
|
141
|
+
async def ingest_report(self, report: dict[str, Any]) -> list[LogResult]:
|
|
142
|
+
report = normalize_report(report)
|
|
143
|
+
results: list[LogResult] = []
|
|
144
|
+
backfill_only = self._report_ingested
|
|
145
|
+
|
|
146
|
+
if not self._session_started:
|
|
147
|
+
results.append(
|
|
148
|
+
await self.log_session_started(
|
|
149
|
+
room=report.get("room"),
|
|
150
|
+
job_id=report.get("job_id"),
|
|
151
|
+
room_id=report.get("room_id"),
|
|
152
|
+
)
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
for item in chat_items(report):
|
|
156
|
+
item_id = item.get("id")
|
|
157
|
+
if item_id and item_id in self._seen_item_ids:
|
|
158
|
+
continue
|
|
159
|
+
role = item_role(item)
|
|
160
|
+
event_type = zizka_event_for_role(role)
|
|
161
|
+
if event_type is None:
|
|
162
|
+
continue
|
|
163
|
+
text = item_text(item)
|
|
164
|
+
if not text.strip():
|
|
165
|
+
continue
|
|
166
|
+
data: dict[str, Any] = {"content": text}
|
|
167
|
+
if item.get("interrupted") is not None:
|
|
168
|
+
data["interrupted"] = item["interrupted"]
|
|
169
|
+
if item.get("created_at") is not None:
|
|
170
|
+
data["created_at"] = item["created_at"]
|
|
171
|
+
metadata = {"source": "livekit"}
|
|
172
|
+
if item_id:
|
|
173
|
+
metadata["livekit_item_id"] = item_id
|
|
174
|
+
results.append(
|
|
175
|
+
await self._log(
|
|
176
|
+
event=event_type,
|
|
177
|
+
data=data,
|
|
178
|
+
parent_id=self.last_event_id,
|
|
179
|
+
metadata=metadata,
|
|
180
|
+
)
|
|
181
|
+
)
|
|
182
|
+
if item_id:
|
|
183
|
+
self._seen_item_ids.add(item_id)
|
|
184
|
+
|
|
185
|
+
if not backfill_only:
|
|
186
|
+
for ev in backend_events(report):
|
|
187
|
+
event_type = zizka_event_for_report_event(ev)
|
|
188
|
+
ev_type = ev.get("type") or ev.get("event")
|
|
189
|
+
data = {k: v for k, v in ev.items() if k not in ("type", "event")}
|
|
190
|
+
if ev_type:
|
|
191
|
+
data["livekit_type"] = ev_type
|
|
192
|
+
results.append(
|
|
193
|
+
await self._log(
|
|
194
|
+
event=event_type,
|
|
195
|
+
data=data,
|
|
196
|
+
parent_id=self.last_event_id,
|
|
197
|
+
metadata={"source": "livekit", "livekit_event": True},
|
|
198
|
+
)
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
if not self._session_ended:
|
|
202
|
+
results.append(
|
|
203
|
+
await self.log_session_ended(
|
|
204
|
+
turn_count=len(chat_items(report)),
|
|
205
|
+
livekit_job_id=report.get("job_id"),
|
|
206
|
+
)
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
self._report_ingested = True
|
|
210
|
+
|
|
211
|
+
return results
|
|
212
|
+
|
|
213
|
+
async def log_tool_call(
|
|
214
|
+
self,
|
|
215
|
+
tool: str,
|
|
216
|
+
*,
|
|
217
|
+
args: dict[str, Any] | None = None,
|
|
218
|
+
parent_id: str | None = None,
|
|
219
|
+
**extra: Any,
|
|
220
|
+
) -> LogResult:
|
|
221
|
+
data: dict[str, Any] = {"tool": tool, **extra}
|
|
222
|
+
if args is not None:
|
|
223
|
+
data["args"] = args
|
|
224
|
+
return await self._log(
|
|
225
|
+
event="tool_call",
|
|
226
|
+
data=data,
|
|
227
|
+
parent_id=parent_id or self.last_event_id,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
async def log_tool_result(
|
|
231
|
+
self,
|
|
232
|
+
tool: str,
|
|
233
|
+
*,
|
|
234
|
+
output: Any = None,
|
|
235
|
+
parent_id: str | None = None,
|
|
236
|
+
**extra: Any,
|
|
237
|
+
) -> LogResult:
|
|
238
|
+
data: dict[str, Any] = {"tool": tool, **extra}
|
|
239
|
+
if output is not None:
|
|
240
|
+
data["output"] = output
|
|
241
|
+
return await self._log(
|
|
242
|
+
event="tool_result",
|
|
243
|
+
data=data,
|
|
244
|
+
parent_id=parent_id or self.last_event_id,
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
async def log_error(
|
|
248
|
+
self,
|
|
249
|
+
message: str,
|
|
250
|
+
*,
|
|
251
|
+
stage: str | None = None,
|
|
252
|
+
parent_id: str | None = None,
|
|
253
|
+
**extra: Any,
|
|
254
|
+
) -> LogResult:
|
|
255
|
+
data: dict[str, Any] = {"error": message, **extra}
|
|
256
|
+
if stage is not None:
|
|
257
|
+
data["stage"] = stage
|
|
258
|
+
return await self._log(
|
|
259
|
+
event="error",
|
|
260
|
+
data=data,
|
|
261
|
+
parent_id=parent_id or self.last_event_id,
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
async def _on_conversation_item(self, ev: Any) -> None:
|
|
265
|
+
item = getattr(ev, "item", None)
|
|
266
|
+
if item is None:
|
|
267
|
+
return
|
|
268
|
+
item_id = getattr(item, "id", None) or getattr(item, "item_id", None)
|
|
269
|
+
if item_id is not None:
|
|
270
|
+
item_id = str(item_id)
|
|
271
|
+
if item_id in self._seen_item_ids:
|
|
272
|
+
return
|
|
273
|
+
role = item_role(
|
|
274
|
+
{
|
|
275
|
+
"role": getattr(item, "role", None),
|
|
276
|
+
"content": getattr(item, "content", None),
|
|
277
|
+
}
|
|
278
|
+
)
|
|
279
|
+
event_type = zizka_event_for_role(role)
|
|
280
|
+
if event_type is None:
|
|
281
|
+
return
|
|
282
|
+
text = item_text({"content": getattr(item, "content", "")})
|
|
283
|
+
if not text.strip():
|
|
284
|
+
return
|
|
285
|
+
metadata = {"source": "livekit", "realtime": True}
|
|
286
|
+
if item_id:
|
|
287
|
+
metadata["livekit_item_id"] = item_id
|
|
288
|
+
await self._log(
|
|
289
|
+
event=event_type,
|
|
290
|
+
data={"content": text},
|
|
291
|
+
parent_id=self.last_event_id,
|
|
292
|
+
metadata=metadata,
|
|
293
|
+
)
|
|
294
|
+
if item_id:
|
|
295
|
+
self._seen_item_ids.add(item_id)
|
|
296
|
+
|
|
297
|
+
async def _log(
|
|
298
|
+
self,
|
|
299
|
+
*,
|
|
300
|
+
event: str,
|
|
301
|
+
data: dict[str, Any],
|
|
302
|
+
parent_id: str | None,
|
|
303
|
+
metadata: dict[str, Any] | None = None,
|
|
304
|
+
) -> LogResult:
|
|
305
|
+
result = await self.db.log(
|
|
306
|
+
agent=self.agent,
|
|
307
|
+
event=event,
|
|
308
|
+
data=data,
|
|
309
|
+
parent_id=parent_id,
|
|
310
|
+
session_id=self.session_id,
|
|
311
|
+
metadata=metadata,
|
|
312
|
+
)
|
|
313
|
+
self.last_event_id = result.event_id
|
|
314
|
+
self._last_result = result
|
|
315
|
+
return result
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: zizkadb-livekit
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: LiveKit Agents observer for ZizkaDB — one session per call, transcript from LiveKit (no audio stored)
|
|
5
|
+
License-Expression: AGPL-3.0-or-later
|
|
6
|
+
Project-URL: Repository, https://github.com/Zizka-ai/ZizkaDB
|
|
7
|
+
Project-URL: Documentation, https://db.zizka.ai/docs
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
Requires-Dist: zizkadb-sdk>=0.2.7
|
|
11
|
+
Requires-Dist: livekit-agents>=1.3.0
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
14
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
15
|
+
|
|
16
|
+
# ZizkaDB + LiveKit Agents
|
|
17
|
+
|
|
18
|
+
**PyPI:** [zizkadb-livekit 0.1.0](https://pypi.org/project/zizkadb-livekit/) · depends on `zizkadb-sdk>=0.2.7` + `livekit-agents>=1.3.0`
|
|
19
|
+
|
|
20
|
+
Log voice calls to ZizkaDB with the same **Activity → Sessions / Events** model as text agents.
|
|
21
|
+
|
|
22
|
+
- **One `session_id` per call** (use LiveKit room name or job id)
|
|
23
|
+
- **Transcript only** — text copied from LiveKit `make_session_report()` (no audio in ZizkaDB)
|
|
24
|
+
- **Backend events** — `tool_call`, `tool_result`, `error`, pipeline events from the report
|
|
25
|
+
- **Causal lineage** — `parent_id` chains for Why? in the dashboard
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install "zizkadb-livekit>=0.1.0"
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Start ZizkaDB (Docker quickstart):
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
curl -fsSL https://raw.githubusercontent.com/Zizka-ai/ZizkaDB/main/scripts/quickstart-remote.sh | bash
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Usage
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
import os
|
|
43
|
+
from livekit.agents import AgentServer, JobContext, AgentSession, Agent
|
|
44
|
+
from zizkadb import ZizkaDB
|
|
45
|
+
from zizkadb_livekit import ZizkaDBLiveKitObserver
|
|
46
|
+
|
|
47
|
+
server = AgentServer()
|
|
48
|
+
|
|
49
|
+
async def on_session_end(ctx: JobContext) -> None:
|
|
50
|
+
await observer.ingest_session_report(ctx)
|
|
51
|
+
|
|
52
|
+
@server.rtc_session(agent_name="support-voice", on_session_end=on_session_end)
|
|
53
|
+
async def entrypoint(ctx: JobContext):
|
|
54
|
+
await ctx.connect()
|
|
55
|
+
|
|
56
|
+
db = ZizkaDB(host=os.getenv("ZIZKADB_HOST", "http://localhost:8000"))
|
|
57
|
+
global observer
|
|
58
|
+
observer = ZizkaDBLiveKitObserver(
|
|
59
|
+
db,
|
|
60
|
+
agent=os.getenv("ZIZKADB_AGENT", "support-voice"),
|
|
61
|
+
session_id=f"call_{ctx.room.name}",
|
|
62
|
+
)
|
|
63
|
+
await observer.log_session_started(room=ctx.room.name, job_id=ctx.job.id)
|
|
64
|
+
|
|
65
|
+
session = AgentSession(...)
|
|
66
|
+
observer.attach(session, job_ctx=ctx) # optional realtime turns
|
|
67
|
+
|
|
68
|
+
await session.start(agent=Agent(instructions="..."), room=ctx.room)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Open **http://localhost:3001** → Activity → your agent → Sessions.
|
|
72
|
+
|
|
73
|
+
## Event types
|
|
74
|
+
|
|
75
|
+
| Event | Source |
|
|
76
|
+
|-------|--------|
|
|
77
|
+
| `session_started` | Call connect |
|
|
78
|
+
| `user_message` | LiveKit user transcript |
|
|
79
|
+
| `assistant_response` | LiveKit agent transcript |
|
|
80
|
+
| `tool_call` / `tool_result` | Tools + report pipeline events |
|
|
81
|
+
| `error` | Failures |
|
|
82
|
+
| `session_ended` | Post-call ingest |
|
|
83
|
+
|
|
84
|
+
Manual helpers: `log_tool_call`, `log_tool_result`, `log_error`.
|
|
85
|
+
|
|
86
|
+
## Environment
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
export ZIZKADB_HOST=http://localhost:8000 # self-host
|
|
90
|
+
export ZIZKADB_AGENT=support-voice
|
|
91
|
+
# Cloud: export ZIZKADB_API_KEY=zizkadb_live_...
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
See [examples/livekit-agent](../../examples/livekit-agent/) for a full sample.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
zizkadb_livekit/__init__.py
|
|
4
|
+
zizkadb_livekit/_report.py
|
|
5
|
+
zizkadb_livekit/observer.py
|
|
6
|
+
zizkadb_livekit.egg-info/PKG-INFO
|
|
7
|
+
zizkadb_livekit.egg-info/SOURCES.txt
|
|
8
|
+
zizkadb_livekit.egg-info/dependency_links.txt
|
|
9
|
+
zizkadb_livekit.egg-info/requires.txt
|
|
10
|
+
zizkadb_livekit.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
zizkadb_livekit
|