hindsight-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.
- hindsight_livekit-0.1.0/.github/workflows/release.yml +21 -0
- hindsight_livekit-0.1.0/.github/workflows/test.yml +42 -0
- hindsight_livekit-0.1.0/.gitignore +9 -0
- hindsight_livekit-0.1.0/LICENSE +21 -0
- hindsight_livekit-0.1.0/PKG-INFO +242 -0
- hindsight_livekit-0.1.0/README.md +216 -0
- hindsight_livekit-0.1.0/examples/realtime_agent.py +74 -0
- hindsight_livekit-0.1.0/examples/voice_agent.py +86 -0
- hindsight_livekit-0.1.0/hindsight_livekit/__init__.py +48 -0
- hindsight_livekit-0.1.0/hindsight_livekit/_client.py +46 -0
- hindsight_livekit-0.1.0/hindsight_livekit/agent.py +85 -0
- hindsight_livekit-0.1.0/hindsight_livekit/config.py +87 -0
- hindsight_livekit-0.1.0/hindsight_livekit/errors.py +7 -0
- hindsight_livekit-0.1.0/hindsight_livekit/memory.py +391 -0
- hindsight_livekit-0.1.0/hindsight_livekit/py.typed +0 -0
- hindsight_livekit-0.1.0/hindsight_livekit/tools.py +168 -0
- hindsight_livekit-0.1.0/pyproject.toml +74 -0
- hindsight_livekit-0.1.0/tests/__init__.py +0 -0
- hindsight_livekit-0.1.0/tests/conftest.py +97 -0
- hindsight_livekit-0.1.0/tests/test_live_integration.py +218 -0
- hindsight_livekit-0.1.0/tests/test_memory.py +547 -0
- hindsight_livekit-0.1.0/tests/test_tools.py +161 -0
- hindsight_livekit-0.1.0/uv.lock +2489 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
# Publishes to PyPI when a version tag (e.g. v0.1.0) is pushed, using PyPI
|
|
4
|
+
# Trusted Publishing: no API token is stored in the repo.
|
|
5
|
+
on:
|
|
6
|
+
push:
|
|
7
|
+
tags: ["v*"]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
publish:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
environment: pypi
|
|
13
|
+
permissions:
|
|
14
|
+
id-token: write
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v6
|
|
17
|
+
- uses: astral-sh/setup-uv@v7
|
|
18
|
+
- name: Check the tag matches the package version
|
|
19
|
+
run: test "v$(uvx --from toml-cli toml get --toml-path pyproject.toml project.version)" = "$GITHUB_REF_NAME"
|
|
20
|
+
- run: uv build
|
|
21
|
+
- run: uv publish --trusted-publishing always
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
name: Test
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
strategy:
|
|
12
|
+
fail-fast: false
|
|
13
|
+
matrix:
|
|
14
|
+
include:
|
|
15
|
+
# Oldest supported versions
|
|
16
|
+
- python: "3.11"
|
|
17
|
+
deps: "livekit-agents==1.2.0 hindsight-client==0.6.0"
|
|
18
|
+
# Latest versions
|
|
19
|
+
- python: "3.12"
|
|
20
|
+
deps: ""
|
|
21
|
+
- python: "3.14"
|
|
22
|
+
deps: ""
|
|
23
|
+
steps:
|
|
24
|
+
- uses: actions/checkout@v6
|
|
25
|
+
- uses: astral-sh/setup-uv@v7
|
|
26
|
+
- name: Lint
|
|
27
|
+
run: |
|
|
28
|
+
uvx ruff check .
|
|
29
|
+
uvx ruff format --check .
|
|
30
|
+
- name: Test
|
|
31
|
+
run: |
|
|
32
|
+
uv venv --python ${{ matrix.python }}
|
|
33
|
+
# LiveKit 1.2-1.5 import httpx without declaring it
|
|
34
|
+
uv pip install -e . httpx pytest pytest-asyncio ${{ matrix.deps }}
|
|
35
|
+
uv run --no-sync pytest tests -v
|
|
36
|
+
|
|
37
|
+
build:
|
|
38
|
+
runs-on: ubuntu-latest
|
|
39
|
+
steps:
|
|
40
|
+
- uses: actions/checkout@v6
|
|
41
|
+
- uses: astral-sh/setup-uv@v7
|
|
42
|
+
- run: uv build
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Naman
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: hindsight-livekit
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: LiveKit Agents integration for Hindsight - persistent memory for voice AI agents
|
|
5
|
+
Project-URL: Homepage, https://github.com/namandidthis/hindsight-livekit
|
|
6
|
+
Project-URL: Repository, https://github.com/namandidthis/hindsight-livekit
|
|
7
|
+
Project-URL: Issues, https://github.com/namandidthis/hindsight-livekit/issues
|
|
8
|
+
Project-URL: Hindsight, https://github.com/vectorize-io/hindsight
|
|
9
|
+
Author-email: Naman <naman6176@gmail.com>
|
|
10
|
+
License: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: agents,ai,hindsight,livekit,memory,realtime,voice
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
22
|
+
Requires-Python: >=3.11
|
|
23
|
+
Requires-Dist: hindsight-client>=0.6.0
|
|
24
|
+
Requires-Dist: livekit-agents<2.0,>=1.2
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# Hindsight LiveKit Integration
|
|
28
|
+
|
|
29
|
+
Persistent long-term memory for [LiveKit Agents](https://github.com/livekit/agents) voice AI via [Hindsight](https://vectorize.io/hindsight). Your agent knows what it learned in earlier calls, and stores each new conversation as it happens. Works with STT-LLM-TTS pipelines and realtime (speech-to-speech) models.
|
|
30
|
+
|
|
31
|
+
## Quick Start
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install hindsight-livekit
|
|
35
|
+
# or the latest from GitHub:
|
|
36
|
+
pip install git+https://github.com/namandidthis/hindsight-livekit
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
> ✨ **Recommended: [Hindsight Cloud](https://ui.hindsight.vectorize.io)** — free tier, no self-hosting required. Sign up and grab an API key in under a minute.
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from livekit.agents import AgentServer, AgentSession, JobContext, inference
|
|
43
|
+
from hindsight_livekit import HindsightAgent, HindsightMemory
|
|
44
|
+
|
|
45
|
+
server = AgentServer()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@server.rtc_session()
|
|
49
|
+
async def entrypoint(ctx: JobContext):
|
|
50
|
+
session = AgentSession(stt=..., tts=...)
|
|
51
|
+
|
|
52
|
+
memory = HindsightMemory(
|
|
53
|
+
bank_id="user-123", # one memory bank per end user
|
|
54
|
+
document_id=ctx.room.name, # one transcript per call
|
|
55
|
+
hindsight_api_url="https://api.hindsight.vectorize.io",
|
|
56
|
+
api_key="hsk_...", # or set HINDSIGHT_API_KEY
|
|
57
|
+
)
|
|
58
|
+
memory.attach(session) # retain the conversation
|
|
59
|
+
ctx.add_shutdown_callback(memory.aclose) # store the end of the call on hang-up
|
|
60
|
+
|
|
61
|
+
await session.start(
|
|
62
|
+
agent=HindsightAgent(
|
|
63
|
+
memory=memory,
|
|
64
|
+
llm=inference.LLM(model="google/gemma-4-31b-it"),
|
|
65
|
+
instructions="You are a friendly voice assistant.",
|
|
66
|
+
),
|
|
67
|
+
room=ctx.room,
|
|
68
|
+
)
|
|
69
|
+
await ctx.connect()
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`HindsightAgent` is a regular LiveKit `Agent`: subclass it, add `@function_tool`s, override `on_enter` (call `await super().on_enter()` first), and so on.
|
|
73
|
+
|
|
74
|
+
Runnable agents built on [agent-starter-python](https://github.com/livekit-examples/agent-starter-python):
|
|
75
|
+
|
|
76
|
+
- [`examples/voice_agent.py`](./examples/voice_agent.py) — STT-LLM-TTS pipeline
|
|
77
|
+
- [`examples/realtime_agent.py`](./examples/realtime_agent.py) — realtime model with memory tools
|
|
78
|
+
|
|
79
|
+
### Self-hosting (local development)
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
docker run --rm -p 8888:8888 -e HINDSIGHT_API_LLM_API_KEY=$OPENAI_API_KEY ghcr.io/vectorize-io/hindsight:latest
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
memory = HindsightMemory(bank_id="user-123", hindsight_api_url="http://localhost:8888")
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
See the [Hindsight installation guide](https://hindsight.vectorize.io/developer/installation) for other options. LiveKit itself can be [self-hosted](https://docs.livekit.io/transport/self-hosting/local/) too; the integration works the same either way.
|
|
90
|
+
|
|
91
|
+
## How It Works
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
memory.attach(session)
|
|
95
|
+
└─ Session recall starts: "who is this user?" (overlaps with session.start)
|
|
96
|
+
|
|
97
|
+
Agent starts (on_enter)
|
|
98
|
+
└─ Known facts about the user are added to the agent's instructions
|
|
99
|
+
|
|
100
|
+
Before each LLM reply (llm_node) — STT-LLM-TTS pipelines only
|
|
101
|
+
└─ Recall memories relevant to the user's latest message
|
|
102
|
+
└─ Insert them before that message, in a copy of the chat context
|
|
103
|
+
|
|
104
|
+
As the conversation happens (conversation_item_added)
|
|
105
|
+
└─ Append "User (time): …" / "Assistant (time): …" lines to the call's Hindsight document
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
| | STT-LLM-TTS pipeline | Realtime model |
|
|
109
|
+
|---|---|---|
|
|
110
|
+
| Recall when the agent starts | ✅ | ✅ |
|
|
111
|
+
| Recall before each reply | ✅ | — (no step before a realtime reply; use the tools) |
|
|
112
|
+
| Memory tools (`create_hindsight_tools`) | ✅ | ✅ |
|
|
113
|
+
| Retain the conversation | ✅ | ✅ (needs the model's input audio transcription for the user's side) |
|
|
114
|
+
|
|
115
|
+
**Session recall** runs once per call. `attach()` starts it, so it overlaps with LiveKit connecting the room and the model, and `HindsightAgent.on_enter` adds the result to the instructions. LiveKit runs `on_enter` in the background, so with a pipeline the first `llm_node` call also waits for the lookup and uses the instructions with memories, even if the user speaks first. A realtime model has no such step: a reply that starts before the lookup finishes (bounded by `recall_timeout`) won't include those memories, which is rare in practice since the lookup starts before the call connects.
|
|
116
|
+
|
|
117
|
+
**Per-turn recall** runs in [`llm_node`](https://docs.livekit.io/agents/build/nodes/), the step every LLM call in a pipeline goes through. That means:
|
|
118
|
+
|
|
119
|
+
- It works for spoken turns, chat text input and `session.generate_reply(user_input=...)`. The `on_user_turn_completed` hook only runs for spoken turns.
|
|
120
|
+
- It works with [preemptive generation](https://docs.livekit.io/agents/build/audio/#preemptive-generation): recall starts while LiveKit is still confirming the end of the user's turn, and the preemptive reply is kept. Editing the context in `on_user_turn_completed` would make LiveKit discard it.
|
|
121
|
+
- Memories are only added to a copy of the context for that call, so they never pile up in the session history. When the LLM calls a tool, the follow-up call reuses the same recall result.
|
|
122
|
+
|
|
123
|
+
As in LiveKit's [RAG example](https://docs.livekit.io/agents/build/external-data/), per-turn memories are added as an `assistant` message just before the user's message, wrapped in `<hindsight_memories>` tags with a note telling the model not to read them out.
|
|
124
|
+
|
|
125
|
+
**Retain** follows Hindsight's [guidance for conversations](https://hindsight.vectorize.io/developer/api/retain#retaining-a-conversation): each call is one document (`document_id`), written as `Name (timestamp): text` lines, grown with [`update_mode="append"`](https://hindsight.vectorize.io/developer/api/retain#update_mode) so Hindsight only extracts facts from the new part. Lines are sent once both the user and the agent have spoken, sorted by time, because realtime models can deliver the user's transcript after the reply it prompted. Appends are sent one at a time, in order, in the background, so they never delay a reply. `aclose()` sends whatever is left when the call ends.
|
|
126
|
+
|
|
127
|
+
**Failures never break the call.** If recall fails or takes longer than `recall_timeout`, the agent carries on without memories. If retain fails, the error is logged.
|
|
128
|
+
|
|
129
|
+
## Memory Tools
|
|
130
|
+
|
|
131
|
+
`create_hindsight_tools()` gives the model `hindsight_retain`, `hindsight_recall` and `hindsight_reflect` tools, like the other Hindsight framework integrations. They are the way for a realtime model to look things up mid-call, and work with pipelines too:
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
from hindsight_livekit import HindsightAgent, create_hindsight_tools
|
|
135
|
+
|
|
136
|
+
agent = HindsightAgent(
|
|
137
|
+
memory=memory,
|
|
138
|
+
llm=inference.RealtimeModel(model="openai/gpt-realtime-mini"),
|
|
139
|
+
tools=create_hindsight_tools(bank_id=memory.bank_id, client=memory.client, include_reflect=False),
|
|
140
|
+
instructions=(
|
|
141
|
+
"You have long-term memory of past calls. When the user asks about themselves, "
|
|
142
|
+
"their past, or their preferences, call hindsight_recall first. When they share "
|
|
143
|
+
"something worth remembering, call hindsight_retain."
|
|
144
|
+
),
|
|
145
|
+
)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Tell the model when to use the tools, as above: models call them far more reliably with explicit guidance. `hindsight_reflect` runs an LLM on the Hindsight server, so it is the slowest; leave it out for snappier calls. Tool errors are raised as LiveKit `ToolError`, so the model hears what went wrong and can tell the caller.
|
|
149
|
+
|
|
150
|
+
## Latency
|
|
151
|
+
|
|
152
|
+
Recall happens while a caller waits, so the defaults are tuned for voice:
|
|
153
|
+
|
|
154
|
+
- `recall_budget="low"`: Hindsight's [fast lookup](https://hindsight.vectorize.io/developer/api/recall#budget) setting.
|
|
155
|
+
- `recall_max_tokens=1024`: keeps the prompt small.
|
|
156
|
+
- `recall_timeout=3.0`: the longest a reply waits on memory before going ahead without it.
|
|
157
|
+
|
|
158
|
+
With preemptive generation turned on, most per-turn recall time overlaps with end-of-turn detection.
|
|
159
|
+
|
|
160
|
+
## Configuration
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
HindsightMemory(
|
|
164
|
+
bank_id="user-123", # Required: memory bank to use
|
|
165
|
+
client=None, # Pre-built hindsight_client.Hindsight (optional)
|
|
166
|
+
hindsight_api_url="...", # Defaults to HINDSIGHT_API_URL, then Hindsight Cloud
|
|
167
|
+
api_key="hsk_...", # Defaults to HINDSIGHT_API_KEY
|
|
168
|
+
document_id=None, # Defaults to a random "livekit-<uuid>"
|
|
169
|
+
recall_budget="low", # "low", "mid", or "high"
|
|
170
|
+
recall_max_tokens=1024, # Max tokens of memories per recall
|
|
171
|
+
recall_timeout=3.0, # Seconds before carrying on without memories
|
|
172
|
+
recall_types=None, # e.g. ["world", "experience"]; None = all
|
|
173
|
+
tags=None, # Tags for retained content
|
|
174
|
+
recall_tags=None, # Tags to filter recall by
|
|
175
|
+
recall_tags_match="any", # "any", "all", "any_strict", "all_strict"
|
|
176
|
+
retain_context="voice conversation", # Source label that guides fact extraction
|
|
177
|
+
user_name="User", # Speaker labels in the stored transcript
|
|
178
|
+
agent_name="Assistant",
|
|
179
|
+
enable_session_recall=True, # Recall known facts when the agent starts
|
|
180
|
+
session_recall_query="Who is this user? ...",
|
|
181
|
+
enable_recall=True, # Recall before each reply (pipelines)
|
|
182
|
+
enable_retain=True, # Store the conversation
|
|
183
|
+
)
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Global configuration
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
from hindsight_livekit import configure
|
|
190
|
+
|
|
191
|
+
configure(
|
|
192
|
+
hindsight_api_url="https://api.hindsight.vectorize.io",
|
|
193
|
+
api_key="hsk_...",
|
|
194
|
+
recall_budget="low",
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
memory = HindsightMemory(bank_id="user-123")
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Using your own `Agent` or `llm_node`
|
|
201
|
+
|
|
202
|
+
`memory.attach(session)` works with any `Agent`. To add recall to an agent that already overrides `llm_node` or `on_enter`, call the memory directly instead of subclassing `HindsightAgent`:
|
|
203
|
+
|
|
204
|
+
```python
|
|
205
|
+
from livekit.agents import Agent
|
|
206
|
+
|
|
207
|
+
class MyAgent(Agent):
|
|
208
|
+
def __init__(self, memory: HindsightMemory) -> None:
|
|
209
|
+
super().__init__(instructions="...")
|
|
210
|
+
self.memory = memory
|
|
211
|
+
|
|
212
|
+
async def on_enter(self):
|
|
213
|
+
if memories := await self.memory.recall_session_context():
|
|
214
|
+
await self.update_instructions(f"{self.instructions}\n\n{memories}")
|
|
215
|
+
|
|
216
|
+
async def llm_node(self, chat_ctx, tools, model_settings):
|
|
217
|
+
chat_ctx = await self.memory.inject_memories(chat_ctx)
|
|
218
|
+
async for chunk in Agent.default.llm_node(self, chat_ctx, tools, model_settings):
|
|
219
|
+
yield chunk
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## Prerequisites
|
|
223
|
+
|
|
224
|
+
- `livekit-agents` 1.2 or later
|
|
225
|
+
- A running Hindsight instance, self-hosted or [Hindsight Cloud](https://ui.hindsight.vectorize.io)
|
|
226
|
+
|
|
227
|
+
## Running Tests
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
uv sync
|
|
231
|
+
uv run pytest tests -v
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
The unit tests run a real LiveKit `AgentSession` with a scripted LLM and a mocked Hindsight client. The live tests in `tests/test_live_integration.py` run against a real Hindsight server when `HINDSIGHT_API_URL` is set, and also against real pipeline and realtime models through LiveKit Inference when LiveKit Cloud credentials are set:
|
|
235
|
+
|
|
236
|
+
```bash
|
|
237
|
+
docker run --rm -p 8888:8888 -e HINDSIGHT_API_LLM_PROVIDER=none ghcr.io/vectorize-io/hindsight:latest
|
|
238
|
+
HINDSIGHT_API_URL=http://localhost:8888 uv run pytest tests -v
|
|
239
|
+
|
|
240
|
+
# optional: real models via LiveKit Inference
|
|
241
|
+
LIVEKIT_URL=wss://... LIVEKIT_API_KEY=... LIVEKIT_API_SECRET=... HINDSIGHT_API_URL=http://localhost:8888 uv run pytest tests -v
|
|
242
|
+
```
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# Hindsight LiveKit Integration
|
|
2
|
+
|
|
3
|
+
Persistent long-term memory for [LiveKit Agents](https://github.com/livekit/agents) voice AI via [Hindsight](https://vectorize.io/hindsight). Your agent knows what it learned in earlier calls, and stores each new conversation as it happens. Works with STT-LLM-TTS pipelines and realtime (speech-to-speech) models.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install hindsight-livekit
|
|
9
|
+
# or the latest from GitHub:
|
|
10
|
+
pip install git+https://github.com/namandidthis/hindsight-livekit
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
> ✨ **Recommended: [Hindsight Cloud](https://ui.hindsight.vectorize.io)** — free tier, no self-hosting required. Sign up and grab an API key in under a minute.
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from livekit.agents import AgentServer, AgentSession, JobContext, inference
|
|
17
|
+
from hindsight_livekit import HindsightAgent, HindsightMemory
|
|
18
|
+
|
|
19
|
+
server = AgentServer()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@server.rtc_session()
|
|
23
|
+
async def entrypoint(ctx: JobContext):
|
|
24
|
+
session = AgentSession(stt=..., tts=...)
|
|
25
|
+
|
|
26
|
+
memory = HindsightMemory(
|
|
27
|
+
bank_id="user-123", # one memory bank per end user
|
|
28
|
+
document_id=ctx.room.name, # one transcript per call
|
|
29
|
+
hindsight_api_url="https://api.hindsight.vectorize.io",
|
|
30
|
+
api_key="hsk_...", # or set HINDSIGHT_API_KEY
|
|
31
|
+
)
|
|
32
|
+
memory.attach(session) # retain the conversation
|
|
33
|
+
ctx.add_shutdown_callback(memory.aclose) # store the end of the call on hang-up
|
|
34
|
+
|
|
35
|
+
await session.start(
|
|
36
|
+
agent=HindsightAgent(
|
|
37
|
+
memory=memory,
|
|
38
|
+
llm=inference.LLM(model="google/gemma-4-31b-it"),
|
|
39
|
+
instructions="You are a friendly voice assistant.",
|
|
40
|
+
),
|
|
41
|
+
room=ctx.room,
|
|
42
|
+
)
|
|
43
|
+
await ctx.connect()
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`HindsightAgent` is a regular LiveKit `Agent`: subclass it, add `@function_tool`s, override `on_enter` (call `await super().on_enter()` first), and so on.
|
|
47
|
+
|
|
48
|
+
Runnable agents built on [agent-starter-python](https://github.com/livekit-examples/agent-starter-python):
|
|
49
|
+
|
|
50
|
+
- [`examples/voice_agent.py`](./examples/voice_agent.py) — STT-LLM-TTS pipeline
|
|
51
|
+
- [`examples/realtime_agent.py`](./examples/realtime_agent.py) — realtime model with memory tools
|
|
52
|
+
|
|
53
|
+
### Self-hosting (local development)
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
docker run --rm -p 8888:8888 -e HINDSIGHT_API_LLM_API_KEY=$OPENAI_API_KEY ghcr.io/vectorize-io/hindsight:latest
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
memory = HindsightMemory(bank_id="user-123", hindsight_api_url="http://localhost:8888")
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
See the [Hindsight installation guide](https://hindsight.vectorize.io/developer/installation) for other options. LiveKit itself can be [self-hosted](https://docs.livekit.io/transport/self-hosting/local/) too; the integration works the same either way.
|
|
64
|
+
|
|
65
|
+
## How It Works
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
memory.attach(session)
|
|
69
|
+
└─ Session recall starts: "who is this user?" (overlaps with session.start)
|
|
70
|
+
|
|
71
|
+
Agent starts (on_enter)
|
|
72
|
+
└─ Known facts about the user are added to the agent's instructions
|
|
73
|
+
|
|
74
|
+
Before each LLM reply (llm_node) — STT-LLM-TTS pipelines only
|
|
75
|
+
└─ Recall memories relevant to the user's latest message
|
|
76
|
+
└─ Insert them before that message, in a copy of the chat context
|
|
77
|
+
|
|
78
|
+
As the conversation happens (conversation_item_added)
|
|
79
|
+
└─ Append "User (time): …" / "Assistant (time): …" lines to the call's Hindsight document
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
| | STT-LLM-TTS pipeline | Realtime model |
|
|
83
|
+
|---|---|---|
|
|
84
|
+
| Recall when the agent starts | ✅ | ✅ |
|
|
85
|
+
| Recall before each reply | ✅ | — (no step before a realtime reply; use the tools) |
|
|
86
|
+
| Memory tools (`create_hindsight_tools`) | ✅ | ✅ |
|
|
87
|
+
| Retain the conversation | ✅ | ✅ (needs the model's input audio transcription for the user's side) |
|
|
88
|
+
|
|
89
|
+
**Session recall** runs once per call. `attach()` starts it, so it overlaps with LiveKit connecting the room and the model, and `HindsightAgent.on_enter` adds the result to the instructions. LiveKit runs `on_enter` in the background, so with a pipeline the first `llm_node` call also waits for the lookup and uses the instructions with memories, even if the user speaks first. A realtime model has no such step: a reply that starts before the lookup finishes (bounded by `recall_timeout`) won't include those memories, which is rare in practice since the lookup starts before the call connects.
|
|
90
|
+
|
|
91
|
+
**Per-turn recall** runs in [`llm_node`](https://docs.livekit.io/agents/build/nodes/), the step every LLM call in a pipeline goes through. That means:
|
|
92
|
+
|
|
93
|
+
- It works for spoken turns, chat text input and `session.generate_reply(user_input=...)`. The `on_user_turn_completed` hook only runs for spoken turns.
|
|
94
|
+
- It works with [preemptive generation](https://docs.livekit.io/agents/build/audio/#preemptive-generation): recall starts while LiveKit is still confirming the end of the user's turn, and the preemptive reply is kept. Editing the context in `on_user_turn_completed` would make LiveKit discard it.
|
|
95
|
+
- Memories are only added to a copy of the context for that call, so they never pile up in the session history. When the LLM calls a tool, the follow-up call reuses the same recall result.
|
|
96
|
+
|
|
97
|
+
As in LiveKit's [RAG example](https://docs.livekit.io/agents/build/external-data/), per-turn memories are added as an `assistant` message just before the user's message, wrapped in `<hindsight_memories>` tags with a note telling the model not to read them out.
|
|
98
|
+
|
|
99
|
+
**Retain** follows Hindsight's [guidance for conversations](https://hindsight.vectorize.io/developer/api/retain#retaining-a-conversation): each call is one document (`document_id`), written as `Name (timestamp): text` lines, grown with [`update_mode="append"`](https://hindsight.vectorize.io/developer/api/retain#update_mode) so Hindsight only extracts facts from the new part. Lines are sent once both the user and the agent have spoken, sorted by time, because realtime models can deliver the user's transcript after the reply it prompted. Appends are sent one at a time, in order, in the background, so they never delay a reply. `aclose()` sends whatever is left when the call ends.
|
|
100
|
+
|
|
101
|
+
**Failures never break the call.** If recall fails or takes longer than `recall_timeout`, the agent carries on without memories. If retain fails, the error is logged.
|
|
102
|
+
|
|
103
|
+
## Memory Tools
|
|
104
|
+
|
|
105
|
+
`create_hindsight_tools()` gives the model `hindsight_retain`, `hindsight_recall` and `hindsight_reflect` tools, like the other Hindsight framework integrations. They are the way for a realtime model to look things up mid-call, and work with pipelines too:
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
from hindsight_livekit import HindsightAgent, create_hindsight_tools
|
|
109
|
+
|
|
110
|
+
agent = HindsightAgent(
|
|
111
|
+
memory=memory,
|
|
112
|
+
llm=inference.RealtimeModel(model="openai/gpt-realtime-mini"),
|
|
113
|
+
tools=create_hindsight_tools(bank_id=memory.bank_id, client=memory.client, include_reflect=False),
|
|
114
|
+
instructions=(
|
|
115
|
+
"You have long-term memory of past calls. When the user asks about themselves, "
|
|
116
|
+
"their past, or their preferences, call hindsight_recall first. When they share "
|
|
117
|
+
"something worth remembering, call hindsight_retain."
|
|
118
|
+
),
|
|
119
|
+
)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Tell the model when to use the tools, as above: models call them far more reliably with explicit guidance. `hindsight_reflect` runs an LLM on the Hindsight server, so it is the slowest; leave it out for snappier calls. Tool errors are raised as LiveKit `ToolError`, so the model hears what went wrong and can tell the caller.
|
|
123
|
+
|
|
124
|
+
## Latency
|
|
125
|
+
|
|
126
|
+
Recall happens while a caller waits, so the defaults are tuned for voice:
|
|
127
|
+
|
|
128
|
+
- `recall_budget="low"`: Hindsight's [fast lookup](https://hindsight.vectorize.io/developer/api/recall#budget) setting.
|
|
129
|
+
- `recall_max_tokens=1024`: keeps the prompt small.
|
|
130
|
+
- `recall_timeout=3.0`: the longest a reply waits on memory before going ahead without it.
|
|
131
|
+
|
|
132
|
+
With preemptive generation turned on, most per-turn recall time overlaps with end-of-turn detection.
|
|
133
|
+
|
|
134
|
+
## Configuration
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
HindsightMemory(
|
|
138
|
+
bank_id="user-123", # Required: memory bank to use
|
|
139
|
+
client=None, # Pre-built hindsight_client.Hindsight (optional)
|
|
140
|
+
hindsight_api_url="...", # Defaults to HINDSIGHT_API_URL, then Hindsight Cloud
|
|
141
|
+
api_key="hsk_...", # Defaults to HINDSIGHT_API_KEY
|
|
142
|
+
document_id=None, # Defaults to a random "livekit-<uuid>"
|
|
143
|
+
recall_budget="low", # "low", "mid", or "high"
|
|
144
|
+
recall_max_tokens=1024, # Max tokens of memories per recall
|
|
145
|
+
recall_timeout=3.0, # Seconds before carrying on without memories
|
|
146
|
+
recall_types=None, # e.g. ["world", "experience"]; None = all
|
|
147
|
+
tags=None, # Tags for retained content
|
|
148
|
+
recall_tags=None, # Tags to filter recall by
|
|
149
|
+
recall_tags_match="any", # "any", "all", "any_strict", "all_strict"
|
|
150
|
+
retain_context="voice conversation", # Source label that guides fact extraction
|
|
151
|
+
user_name="User", # Speaker labels in the stored transcript
|
|
152
|
+
agent_name="Assistant",
|
|
153
|
+
enable_session_recall=True, # Recall known facts when the agent starts
|
|
154
|
+
session_recall_query="Who is this user? ...",
|
|
155
|
+
enable_recall=True, # Recall before each reply (pipelines)
|
|
156
|
+
enable_retain=True, # Store the conversation
|
|
157
|
+
)
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Global configuration
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
from hindsight_livekit import configure
|
|
164
|
+
|
|
165
|
+
configure(
|
|
166
|
+
hindsight_api_url="https://api.hindsight.vectorize.io",
|
|
167
|
+
api_key="hsk_...",
|
|
168
|
+
recall_budget="low",
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
memory = HindsightMemory(bank_id="user-123")
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Using your own `Agent` or `llm_node`
|
|
175
|
+
|
|
176
|
+
`memory.attach(session)` works with any `Agent`. To add recall to an agent that already overrides `llm_node` or `on_enter`, call the memory directly instead of subclassing `HindsightAgent`:
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
from livekit.agents import Agent
|
|
180
|
+
|
|
181
|
+
class MyAgent(Agent):
|
|
182
|
+
def __init__(self, memory: HindsightMemory) -> None:
|
|
183
|
+
super().__init__(instructions="...")
|
|
184
|
+
self.memory = memory
|
|
185
|
+
|
|
186
|
+
async def on_enter(self):
|
|
187
|
+
if memories := await self.memory.recall_session_context():
|
|
188
|
+
await self.update_instructions(f"{self.instructions}\n\n{memories}")
|
|
189
|
+
|
|
190
|
+
async def llm_node(self, chat_ctx, tools, model_settings):
|
|
191
|
+
chat_ctx = await self.memory.inject_memories(chat_ctx)
|
|
192
|
+
async for chunk in Agent.default.llm_node(self, chat_ctx, tools, model_settings):
|
|
193
|
+
yield chunk
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## Prerequisites
|
|
197
|
+
|
|
198
|
+
- `livekit-agents` 1.2 or later
|
|
199
|
+
- A running Hindsight instance, self-hosted or [Hindsight Cloud](https://ui.hindsight.vectorize.io)
|
|
200
|
+
|
|
201
|
+
## Running Tests
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
uv sync
|
|
205
|
+
uv run pytest tests -v
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
The unit tests run a real LiveKit `AgentSession` with a scripted LLM and a mocked Hindsight client. The live tests in `tests/test_live_integration.py` run against a real Hindsight server when `HINDSIGHT_API_URL` is set, and also against real pipeline and realtime models through LiveKit Inference when LiveKit Cloud credentials are set:
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
docker run --rm -p 8888:8888 -e HINDSIGHT_API_LLM_PROVIDER=none ghcr.io/vectorize-io/hindsight:latest
|
|
212
|
+
HINDSIGHT_API_URL=http://localhost:8888 uv run pytest tests -v
|
|
213
|
+
|
|
214
|
+
# optional: real models via LiveKit Inference
|
|
215
|
+
LIVEKIT_URL=wss://... LIVEKIT_API_KEY=... LIVEKIT_API_SECRET=... HINDSIGHT_API_URL=http://localhost:8888 uv run pytest tests -v
|
|
216
|
+
```
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Realtime (speech-to-speech) voice agent with Hindsight memory.
|
|
2
|
+
|
|
3
|
+
Realtime models answer straight from audio, so there is no step before each reply
|
|
4
|
+
where memories can be added. Instead this agent:
|
|
5
|
+
|
|
6
|
+
* recalls what is known about the user when the call starts (HindsightAgent adds it
|
|
7
|
+
to the instructions), and
|
|
8
|
+
* gets ``hindsight_recall`` / ``hindsight_retain`` tools to look things up or save
|
|
9
|
+
them mid-call.
|
|
10
|
+
|
|
11
|
+
The conversation is retained as it happens, as with the STT-LLM-TTS example.
|
|
12
|
+
|
|
13
|
+
Setup is the same as voice_agent.py::
|
|
14
|
+
|
|
15
|
+
lk agent console examples/realtime_agent.py
|
|
16
|
+
lk agent dev examples/realtime_agent.py
|
|
17
|
+
lk token create --identity me --room hindsight-demo --agent hindsight-realtime-agent --join --open meet
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import logging
|
|
21
|
+
import os
|
|
22
|
+
|
|
23
|
+
from dotenv import load_dotenv
|
|
24
|
+
from livekit.agents import AgentServer, AgentSession, JobContext, cli, inference
|
|
25
|
+
|
|
26
|
+
from hindsight_livekit import HindsightAgent, HindsightMemory, create_hindsight_tools
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger("agent")
|
|
29
|
+
|
|
30
|
+
load_dotenv(".env.local")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Assistant(HindsightAgent):
|
|
34
|
+
def __init__(self, memory: HindsightMemory) -> None:
|
|
35
|
+
super().__init__(
|
|
36
|
+
memory=memory,
|
|
37
|
+
# Any realtime model works: e.g. "google/gemini-live" or "xai/grok-voice".
|
|
38
|
+
llm=inference.RealtimeModel(model="openai/gpt-realtime-mini"),
|
|
39
|
+
# The reflect tool runs an LLM on the Hindsight server; leave it out for snappier calls.
|
|
40
|
+
tools=create_hindsight_tools(bank_id=memory.bank_id, client=memory.client, include_reflect=False),
|
|
41
|
+
instructions=(
|
|
42
|
+
"You are a friendly voice assistant with long-term memory of past calls. "
|
|
43
|
+
"When the user asks about themselves, their past, or their preferences, call "
|
|
44
|
+
"hindsight_recall first. When they share something worth remembering, call "
|
|
45
|
+
"hindsight_retain. Keep replies to one to three short sentences."
|
|
46
|
+
),
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
async def on_enter(self) -> None:
|
|
50
|
+
# Recall what's known about the user first, so the greeting can use it.
|
|
51
|
+
await super().on_enter()
|
|
52
|
+
self.session.generate_reply(instructions="Greet the user briefly, by name if you know it.")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
server = AgentServer()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@server.rtc_session(agent_name="hindsight-realtime-agent")
|
|
59
|
+
async def entrypoint(ctx: JobContext) -> None:
|
|
60
|
+
bank_id = os.environ.get("HINDSIGHT_BANK_ID", "livekit-demo-user")
|
|
61
|
+
logger.info(f"using Hindsight bank {bank_id}")
|
|
62
|
+
|
|
63
|
+
session = AgentSession()
|
|
64
|
+
|
|
65
|
+
memory = HindsightMemory(bank_id=bank_id, document_id=ctx.room.name)
|
|
66
|
+
memory.attach(session)
|
|
67
|
+
ctx.add_shutdown_callback(memory.aclose)
|
|
68
|
+
|
|
69
|
+
await session.start(agent=Assistant(memory), room=ctx.room)
|
|
70
|
+
await ctx.connect()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
if __name__ == "__main__":
|
|
74
|
+
cli.run_app(server)
|