hindsight-livekit 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,48 @@
1
+ """Hindsight-LiveKit: Persistent memory for LiveKit voice agents.
2
+
3
+ Recalls what Hindsight knows about the user when the agent starts and before
4
+ each LLM reply, and retains the conversation as it happens, one Hindsight
5
+ document per session. Optional tools let the model search and store memories
6
+ itself, which also works with realtime models.
7
+
8
+ Basic usage::
9
+
10
+ from hindsight_livekit import HindsightAgent, HindsightMemory
11
+
12
+ @server.rtc_session()
13
+ async def entrypoint(ctx: JobContext):
14
+ session = AgentSession(stt=..., llm=..., tts=...)
15
+
16
+ memory = HindsightMemory(bank_id="user-123", document_id=ctx.room.name)
17
+ memory.attach(session) # retain the conversation
18
+ ctx.add_shutdown_callback(memory.aclose) # flush on hang-up
19
+
20
+ await session.start(
21
+ agent=HindsightAgent(memory=memory, instructions="You are a helpful voice assistant."),
22
+ room=ctx.room,
23
+ )
24
+ """
25
+
26
+ from .agent import HindsightAgent
27
+ from .config import (
28
+ HindsightLiveKitConfig,
29
+ configure,
30
+ get_config,
31
+ reset_config,
32
+ )
33
+ from .errors import HindsightLiveKitError
34
+ from .memory import HindsightMemory
35
+ from .tools import create_hindsight_tools
36
+
37
+ __version__ = "0.1.0"
38
+
39
+ __all__ = [
40
+ "HindsightAgent",
41
+ "HindsightLiveKitConfig",
42
+ "HindsightLiveKitError",
43
+ "HindsightMemory",
44
+ "configure",
45
+ "create_hindsight_tools",
46
+ "get_config",
47
+ "reset_config",
48
+ ]
@@ -0,0 +1,46 @@
1
+ """Shared Hindsight client resolution logic."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from importlib import metadata
7
+ from typing import Any
8
+
9
+ from hindsight_client import Hindsight
10
+
11
+ from .config import DEFAULT_HINDSIGHT_API_URL, HINDSIGHT_API_KEY_ENV, HINDSIGHT_API_URL_ENV, get_config
12
+
13
+ try:
14
+ _VERSION = metadata.version("hindsight-livekit")
15
+ except metadata.PackageNotFoundError:
16
+ _VERSION = "0.0.0"
17
+ USER_AGENT = f"hindsight-livekit/{_VERSION}"
18
+
19
+
20
+ def resolve_client(
21
+ client: Hindsight | None,
22
+ hindsight_api_url: str | None,
23
+ api_key: str | None,
24
+ ) -> Hindsight:
25
+ """Resolve a Hindsight client from explicit args, global config, or env vars.
26
+
27
+ Order: explicit arguments, then ``configure()``, then the ``HINDSIGHT_API_URL`` /
28
+ ``HINDSIGHT_API_KEY`` env vars, then Hindsight Cloud. A LiveKit worker is usually
29
+ configured through env vars, so they are honoured without a ``configure()`` call.
30
+ """
31
+ if client is not None:
32
+ return client
33
+
34
+ config = get_config()
35
+ url = (
36
+ hindsight_api_url
37
+ or (config.hindsight_api_url if config else None)
38
+ or os.environ.get(HINDSIGHT_API_URL_ENV)
39
+ or DEFAULT_HINDSIGHT_API_URL
40
+ )
41
+ key = api_key or (config.api_key if config else None) or os.environ.get(HINDSIGHT_API_KEY_ENV)
42
+
43
+ kwargs: dict[str, Any] = {"base_url": url, "timeout": 30.0, "user_agent": USER_AGENT}
44
+ if key:
45
+ kwargs["api_key"] = key
46
+ return Hindsight(**kwargs)
@@ -0,0 +1,85 @@
1
+ """LiveKit ``Agent`` with Hindsight memory recall."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import AsyncIterable
6
+ from typing import Any
7
+
8
+ from livekit.agents import Agent, ModelSettings, llm
9
+
10
+ from .memory import HindsightMemory
11
+
12
+
13
+ class HindsightAgent(Agent):
14
+ """A LiveKit ``Agent`` that recalls Hindsight memories.
15
+
16
+ Use it in place of ``Agent`` (or subclass it) and pass the session's
17
+ :class:`HindsightMemory`::
18
+
19
+ class Assistant(HindsightAgent):
20
+ def __init__(self, memory: HindsightMemory) -> None:
21
+ super().__init__(memory=memory, instructions="You are a helpful voice assistant.")
22
+
23
+ Memory is recalled at two points:
24
+
25
+ * **When the agent starts** (``on_enter``): what is known about the user is
26
+ added to the agent's instructions. This works with STT-LLM-TTS pipelines and
27
+ realtime models alike. If you override ``on_enter``, call
28
+ ``await super().on_enter()`` first.
29
+ * **Before each LLM reply** (``llm_node``): memories relevant to the user's
30
+ latest message are added to a copy of the chat context for that call only.
31
+ Realtime models don't use ``llm_node``, so this step only applies to
32
+ STT-LLM-TTS pipelines; give realtime agents the tools from
33
+ :func:`~hindsight_livekit.create_hindsight_tools` to look things up mid-call.
34
+ """
35
+
36
+ def __init__(self, *, memory: HindsightMemory, **kwargs: Any) -> None:
37
+ super().__init__(**kwargs)
38
+ self._memory = memory
39
+ # Instructions without recalled memories, so re-entering the agent (e.g. after
40
+ # a handoff back to it) replaces the memory block instead of stacking another.
41
+ self._base_instructions = self.instructions
42
+
43
+ @property
44
+ def memory(self) -> HindsightMemory:
45
+ return self._memory
46
+
47
+ async def on_enter(self) -> None:
48
+ memories = await self._memory.recall_session_context()
49
+ if memories is not None:
50
+ await self.update_instructions(self._with_memories(memories))
51
+
52
+ async def llm_node(
53
+ self,
54
+ chat_ctx: llm.ChatContext,
55
+ tools: list[llm.Tool],
56
+ model_settings: ModelSettings,
57
+ ) -> AsyncIterable[llm.ChatChunk | str]:
58
+ chat_ctx = await self._ensure_session_memories(chat_ctx)
59
+ chat_ctx = await self._memory.inject_memories(chat_ctx)
60
+ async for chunk in Agent.default.llm_node(self, chat_ctx, tools, model_settings):
61
+ yield chunk
62
+
63
+ def _with_memories(self, memories: str) -> str:
64
+ return f"{self._base_instructions}\n\n{memories}"
65
+
66
+ async def _ensure_session_memories(self, chat_ctx: llm.ChatContext) -> llm.ChatContext:
67
+ """Make sure this LLM call sees the session memories.
68
+
69
+ LiveKit runs ``on_enter`` in the background and doesn't hold the first reply
70
+ for it, so a user who speaks first can get a reply built from the instructions
71
+ as they were before ``on_enter`` added the memories. Wait for the (shared,
72
+ already running) session recall and, if the instructions here are still the
73
+ original ones, use the version with memories for this call.
74
+ """
75
+ memories = await self._memory.recall_session_context()
76
+ if memories is None:
77
+ return chat_ctx
78
+ for i, item in enumerate(chat_ctx.items):
79
+ if item.type == "message" and item.role == "system" and item.text_content == self._base_instructions:
80
+ updated = chat_ctx.copy()
81
+ # copy() shares item objects with the agent's history, so replace the
82
+ # item rather than editing it.
83
+ updated.items[i] = item.model_copy(update={"content": [self._with_memories(memories)]})
84
+ return updated
85
+ return chat_ctx
@@ -0,0 +1,87 @@
1
+ """Global configuration for Hindsight-LiveKit integration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from dataclasses import dataclass
7
+ from typing import Literal
8
+
9
+ DEFAULT_HINDSIGHT_API_URL = "https://api.hindsight.vectorize.io"
10
+ HINDSIGHT_API_URL_ENV = "HINDSIGHT_API_URL"
11
+ HINDSIGHT_API_KEY_ENV = "HINDSIGHT_API_KEY"
12
+
13
+ Budget = Literal["low", "mid", "high"]
14
+ TagsMatch = Literal["any", "all", "any_strict", "all_strict"]
15
+
16
+ # Voice defaults: every recall happens while a caller waits, so they favour speed.
17
+ # Hindsight documents "low" as the budget for fast, simple lookups.
18
+ DEFAULT_RECALL_BUDGET: Budget = "low"
19
+ DEFAULT_RECALL_MAX_TOKENS = 1024
20
+ DEFAULT_RECALL_TIMEOUT = 3.0
21
+ DEFAULT_RECALL_TAGS_MATCH: TagsMatch = "any"
22
+
23
+
24
+ @dataclass
25
+ class HindsightLiveKitConfig:
26
+ """Connection and default settings for the LiveKit integration.
27
+
28
+ Attributes:
29
+ hindsight_api_url: URL of the Hindsight API server.
30
+ api_key: API key for Hindsight authentication.
31
+ recall_budget: Default recall budget level (low/mid/high).
32
+ recall_max_tokens: Default maximum tokens for recall results.
33
+ recall_timeout: Default seconds to wait for recall before answering without memories.
34
+ """
35
+
36
+ hindsight_api_url: str = DEFAULT_HINDSIGHT_API_URL
37
+ api_key: str | None = None
38
+ recall_budget: Budget = DEFAULT_RECALL_BUDGET
39
+ recall_max_tokens: int = DEFAULT_RECALL_MAX_TOKENS
40
+ recall_timeout: float = DEFAULT_RECALL_TIMEOUT
41
+
42
+
43
+ _global_config: HindsightLiveKitConfig | None = None
44
+
45
+
46
+ def configure(
47
+ hindsight_api_url: str | None = None,
48
+ api_key: str | None = None,
49
+ recall_budget: Budget = DEFAULT_RECALL_BUDGET,
50
+ recall_max_tokens: int = DEFAULT_RECALL_MAX_TOKENS,
51
+ recall_timeout: float = DEFAULT_RECALL_TIMEOUT,
52
+ ) -> HindsightLiveKitConfig:
53
+ """Configure Hindsight connection and default settings.
54
+
55
+ Args:
56
+ hindsight_api_url: Hindsight API URL. Falls back to HINDSIGHT_API_URL env var,
57
+ then Hindsight Cloud.
58
+ api_key: API key. Falls back to HINDSIGHT_API_KEY env var.
59
+ recall_budget: Default recall budget (low/mid/high).
60
+ recall_max_tokens: Default max tokens for recall.
61
+ recall_timeout: Default recall timeout in seconds.
62
+
63
+ Returns:
64
+ The configured HindsightLiveKitConfig.
65
+ """
66
+ global _global_config
67
+
68
+ _global_config = HindsightLiveKitConfig(
69
+ hindsight_api_url=hindsight_api_url or os.environ.get(HINDSIGHT_API_URL_ENV) or DEFAULT_HINDSIGHT_API_URL,
70
+ api_key=api_key or os.environ.get(HINDSIGHT_API_KEY_ENV),
71
+ recall_budget=recall_budget,
72
+ recall_max_tokens=recall_max_tokens,
73
+ recall_timeout=recall_timeout,
74
+ )
75
+
76
+ return _global_config
77
+
78
+
79
+ def get_config() -> HindsightLiveKitConfig | None:
80
+ """Get the current global configuration."""
81
+ return _global_config
82
+
83
+
84
+ def reset_config() -> None:
85
+ """Reset global configuration to None."""
86
+ global _global_config
87
+ _global_config = None
@@ -0,0 +1,7 @@
1
+ """Hindsight-LiveKit error types."""
2
+
3
+
4
+ class HindsightLiveKitError(Exception):
5
+ """Exception raised when the Hindsight-LiveKit integration is misconfigured."""
6
+
7
+ pass
@@ -0,0 +1,391 @@
1
+ """Hindsight memory for LiveKit voice agents.
2
+
3
+ ``HindsightMemory`` handles long-term memory for one ``AgentSession``:
4
+
5
+ * **Session recall** — :meth:`HindsightMemory.recall_session_context` looks up what
6
+ is known about the user once, when the agent starts. ``HindsightAgent`` adds it to
7
+ the agent's instructions. Works with STT-LLM-TTS pipelines and realtime models.
8
+ * **Per-turn recall** — :meth:`HindsightMemory.inject_memories` searches Hindsight
9
+ with the user's latest message and returns a copy of the chat context with the
10
+ results inserted just before that message. ``HindsightAgent`` calls it from
11
+ ``llm_node``, which only STT-LLM-TTS pipelines use.
12
+ * **Retain** — :meth:`HindsightMemory.attach` listens to the session's
13
+ ``conversation_item_added`` events and appends the conversation to one Hindsight
14
+ document per session (``update_mode="append"``).
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import asyncio
20
+ import logging
21
+ import uuid
22
+ from dataclasses import dataclass
23
+ from datetime import datetime, timezone
24
+
25
+ from hindsight_client import Hindsight
26
+ from hindsight_client_api.exceptions import NotFoundException
27
+ from livekit.agents import AgentSession, CloseEvent, ConversationItemAddedEvent
28
+ from livekit.agents.llm import ChatContext, ChatMessage
29
+
30
+ from ._client import resolve_client
31
+ from .config import (
32
+ DEFAULT_RECALL_BUDGET,
33
+ DEFAULT_RECALL_MAX_TOKENS,
34
+ DEFAULT_RECALL_TAGS_MATCH,
35
+ DEFAULT_RECALL_TIMEOUT,
36
+ Budget,
37
+ TagsMatch,
38
+ get_config,
39
+ )
40
+ from .errors import HindsightLiveKitError
41
+
42
+ logger = logging.getLogger(__name__)
43
+
44
+ _MEMORY_MARKER = "<hindsight_memories>"
45
+ _DEFAULT_MEMORY_PREFIX = (
46
+ "Relevant memories about the user from past conversations. "
47
+ "Use them only when they help answer, and never read this list out loud."
48
+ )
49
+ _DEFAULT_SESSION_RECALL_QUERY = "Who is this user? Their name, preferences, and important facts about them."
50
+
51
+
52
+ @dataclass
53
+ class _TranscriptLine:
54
+ role: str
55
+ created_at: float
56
+ text: str
57
+
58
+
59
+ def _last_user_message_index(chat_ctx: ChatContext) -> int | None:
60
+ """Return the index of the last user message in ``chat_ctx``, or None."""
61
+ for i in range(len(chat_ctx.items) - 1, -1, -1):
62
+ item = chat_ctx.items[i]
63
+ if item.type == "message" and item.role == "user":
64
+ return i
65
+ return None
66
+
67
+
68
+ def _to_datetime(created_at: float) -> datetime:
69
+ return datetime.fromtimestamp(created_at, tz=timezone.utc)
70
+
71
+
72
+ class HindsightMemory:
73
+ """Persistent Hindsight memory for a single LiveKit ``AgentSession``.
74
+
75
+ Create one per session, attach it to the session so the conversation is
76
+ retained, and give it to a :class:`~hindsight_livekit.HindsightAgent` so
77
+ memories are recalled::
78
+
79
+ memory = HindsightMemory(bank_id="user-123", document_id=ctx.room.name)
80
+ memory.attach(session)
81
+ ctx.add_shutdown_callback(memory.aclose)
82
+ await session.start(agent=HindsightAgent(memory=memory, instructions="..."), room=ctx.room)
83
+
84
+ Args:
85
+ bank_id: Hindsight memory bank to read from and write to (usually one per end user).
86
+ client: Pre-configured Hindsight client. If omitted, one is created from
87
+ ``hindsight_api_url``/``api_key``, then ``configure()``, then the
88
+ ``HINDSIGHT_API_URL``/``HINDSIGHT_API_KEY`` env vars.
89
+ hindsight_api_url: API URL (used if no client provided).
90
+ api_key: API key (used if no client provided).
91
+ document_id: Hindsight document that this session's transcript is appended to.
92
+ Defaults to a random ``livekit-<uuid>`` id; the room name is a good choice.
93
+ recall_budget: Recall budget level — ``"low"``, ``"mid"``, ``"high"``.
94
+ Defaults to ``"low"``, which Hindsight recommends for fast lookups.
95
+ recall_max_tokens: Maximum tokens of memories injected per recall.
96
+ recall_timeout: Seconds to wait for recall. On timeout the agent continues
97
+ without memories instead of leaving the caller in silence.
98
+ recall_types: Memory types to search (``"world"``, ``"experience"``,
99
+ ``"observation"``). ``None`` searches all of them.
100
+ tags: Tags attached to retained content.
101
+ recall_tags: Tags to filter recall by. Defaults to no filter.
102
+ recall_tags_match: How ``recall_tags`` are matched (any/all/any_strict/all_strict).
103
+ retain_context: Short label describing the source, passed to Hindsight's
104
+ fact extraction as ``context``.
105
+ user_name: Speaker label for the user in the retained transcript.
106
+ agent_name: Speaker label for the agent in the retained transcript.
107
+ enable_session_recall: Recall what is known about the user once, when the
108
+ agent starts, and add it to the agent's instructions.
109
+ session_recall_query: Query used for session recall.
110
+ enable_recall: Recall memories relevant to each user message before the
111
+ LLM replies (STT-LLM-TTS pipelines only).
112
+ enable_retain: Store the conversation as it happens.
113
+ memory_prefix: Instruction placed above recalled memories.
114
+ """
115
+
116
+ def __init__(
117
+ self,
118
+ bank_id: str,
119
+ *,
120
+ client: Hindsight | None = None,
121
+ hindsight_api_url: str | None = None,
122
+ api_key: str | None = None,
123
+ document_id: str | None = None,
124
+ recall_budget: Budget | None = None,
125
+ recall_max_tokens: int | None = None,
126
+ recall_timeout: float | None = None,
127
+ recall_types: list[str] | None = None,
128
+ tags: list[str] | None = None,
129
+ recall_tags: list[str] | None = None,
130
+ recall_tags_match: TagsMatch = DEFAULT_RECALL_TAGS_MATCH,
131
+ retain_context: str = "voice conversation",
132
+ user_name: str = "User",
133
+ agent_name: str = "Assistant",
134
+ enable_session_recall: bool = True,
135
+ session_recall_query: str = _DEFAULT_SESSION_RECALL_QUERY,
136
+ enable_recall: bool = True,
137
+ enable_retain: bool = True,
138
+ memory_prefix: str = _DEFAULT_MEMORY_PREFIX,
139
+ ) -> None:
140
+ self._bank_id = bank_id
141
+ self._owns_client = client is None
142
+ self._client = resolve_client(client, hindsight_api_url, api_key)
143
+ config = get_config()
144
+ self._document_id = document_id or f"livekit-{uuid.uuid4().hex}"
145
+ self._recall_budget: Budget = recall_budget or (config.recall_budget if config else DEFAULT_RECALL_BUDGET)
146
+ self._recall_max_tokens = recall_max_tokens or (
147
+ config.recall_max_tokens if config else DEFAULT_RECALL_MAX_TOKENS
148
+ )
149
+ self._recall_timeout = recall_timeout or (config.recall_timeout if config else DEFAULT_RECALL_TIMEOUT)
150
+ self._recall_types = recall_types
151
+ self._tags = tags
152
+ self._recall_tags = recall_tags
153
+ self._recall_tags_match = recall_tags_match
154
+ self._retain_context = retain_context
155
+ self._user_name = user_name
156
+ self._agent_name = agent_name
157
+ self._enable_session_recall = enable_session_recall
158
+ self._session_recall_query = session_recall_query
159
+ self._enable_recall = enable_recall
160
+ self._enable_retain = enable_retain
161
+ self._memory_prefix = memory_prefix
162
+
163
+ self._session: AgentSession | None = None
164
+ # Transcript lines not yet sent to Hindsight. See _on_conversation_item_added
165
+ # for when they are flushed.
166
+ self._pending: list[_TranscriptLine] = []
167
+ # Appends to one document must not overlap (Hindsight rejects concurrent
168
+ # appends), so they run one at a time, in order. An asyncio.Lock is safe here
169
+ # because a HindsightMemory belongs to one AgentSession, which runs on one loop.
170
+ self._retain_lock = asyncio.Lock()
171
+ self._retain_tasks: set[asyncio.Task[None]] = set()
172
+ # Recall result for the current user message, shared by every LLM call that
173
+ # answers it (preemptive generation, tool-call follow-ups). Keyed on message
174
+ # id and text, so a revised transcript gets a fresh recall.
175
+ self._recall_message_id: str | None = None
176
+ self._recall_query: str | None = None
177
+ self._recall_task: asyncio.Task[str | None] | None = None
178
+ self._session_recall_task: asyncio.Task[str | None] | None = None
179
+ self._closed = False
180
+
181
+ @property
182
+ def bank_id(self) -> str:
183
+ return self._bank_id
184
+
185
+ @property
186
+ def document_id(self) -> str:
187
+ return self._document_id
188
+
189
+ @property
190
+ def client(self) -> Hindsight:
191
+ return self._client
192
+
193
+ # ------------------------------------------------------------------
194
+ # Recall
195
+ # ------------------------------------------------------------------
196
+
197
+ async def recall(self, query: str) -> str | None:
198
+ """Search Hindsight and return a formatted memory block, or None.
199
+
200
+ Never raises: errors and timeouts are logged and return None so the
201
+ conversation continues without memories.
202
+ """
203
+ try:
204
+ response = await asyncio.wait_for(
205
+ self._client.arecall(
206
+ bank_id=self._bank_id,
207
+ query=query,
208
+ types=self._recall_types,
209
+ budget=self._recall_budget,
210
+ max_tokens=self._recall_max_tokens,
211
+ tags=self._recall_tags,
212
+ tags_match=self._recall_tags_match,
213
+ ),
214
+ timeout=self._recall_timeout,
215
+ )
216
+ except NotFoundException:
217
+ # A new user's bank doesn't exist until the first retain creates it.
218
+ logger.debug(f"Hindsight bank {self._bank_id!r} not found yet (no memories to recall)")
219
+ return None
220
+ except asyncio.TimeoutError:
221
+ logger.warning(f"Hindsight recall timed out after {self._recall_timeout}s (continuing without memories)")
222
+ return None
223
+ except Exception as e:
224
+ logger.warning(f"Hindsight recall failed (continuing without memories): {e}")
225
+ return None
226
+
227
+ if not response.results:
228
+ return None
229
+ lines = [_MEMORY_MARKER, self._memory_prefix]
230
+ for i, result in enumerate(response.results, 1):
231
+ lines.append(f"{i}. {result.text}")
232
+ lines.append("</hindsight_memories>")
233
+ return "\n".join(lines)
234
+
235
+ async def recall_session_context(self) -> str | None:
236
+ """Recall what is known about the user, for the start of a session.
237
+
238
+ Returns a formatted memory block, or None if session recall is disabled or
239
+ nothing is found. The lookup runs once per session (``attach()`` starts it
240
+ early) and later calls return the same result. Never raises.
241
+ """
242
+ if not self._enable_session_recall:
243
+ return None
244
+ return await asyncio.shield(self._start_session_recall())
245
+
246
+ def _start_session_recall(self) -> asyncio.Task[str | None]:
247
+ if self._session_recall_task is None:
248
+ self._session_recall_task = asyncio.create_task(self.recall(self._session_recall_query))
249
+ return self._session_recall_task
250
+
251
+ async def inject_memories(self, chat_ctx: ChatContext) -> ChatContext:
252
+ """Return ``chat_ctx`` with recalled memories inserted before the last user message.
253
+
254
+ The input context is not modified, so memories never enter the session's
255
+ stored history. Recall runs at most once per user message. If there is no
256
+ user message or nothing relevant is found, ``chat_ctx`` is returned as is.
257
+ """
258
+ if not self._enable_recall:
259
+ return chat_ctx
260
+ index = _last_user_message_index(chat_ctx)
261
+ if index is None:
262
+ return chat_ctx
263
+ user_message = chat_ctx.items[index]
264
+ query = user_message.text_content
265
+ if not query or not query.strip():
266
+ return chat_ctx
267
+
268
+ if self._recall_task is None or self._recall_message_id != user_message.id or self._recall_query != query:
269
+ self._recall_message_id = user_message.id
270
+ self._recall_query = query
271
+ self._recall_task = asyncio.create_task(self.recall(query))
272
+ # Shield so a cancelled generation (e.g. a discarded preemptive reply)
273
+ # doesn't cancel the recall the next generation is waiting on.
274
+ memories = await asyncio.shield(self._recall_task)
275
+ if memories is None:
276
+ return chat_ctx
277
+
278
+ # LiveKit's RAG example adds looked-up context as an assistant message
279
+ # before the user's turn (docs.livekit.io/agents/build/external-data).
280
+ enriched = chat_ctx.copy()
281
+ enriched.items.insert(
282
+ index,
283
+ ChatMessage(
284
+ role="assistant",
285
+ content=[memories],
286
+ created_at=user_message.created_at - 1e-6,
287
+ ),
288
+ )
289
+ return enriched
290
+
291
+ # ------------------------------------------------------------------
292
+ # Retain
293
+ # ------------------------------------------------------------------
294
+
295
+ def attach(self, session: AgentSession) -> None:
296
+ """Retain this session's conversation in Hindsight as it happens.
297
+
298
+ Call once, before ``session.start()``. The transcript is appended to the
299
+ Hindsight document ``document_id`` in the background. Also starts the
300
+ session recall early, so it is usually ready by the time the agent starts.
301
+ """
302
+ if self._session is session:
303
+ return
304
+ if self._session is not None:
305
+ raise HindsightLiveKitError("HindsightMemory is already attached to another AgentSession")
306
+ self._session = session
307
+ session.on("conversation_item_added", self._on_conversation_item_added)
308
+ session.on("close", self._on_session_close)
309
+ # LiveKit runs Agent.on_enter in the background and doesn't hold the first
310
+ # reply for it, so a slow session recall could miss the first turn. Starting
311
+ # it here overlaps it with the room and model connecting in session.start().
312
+ if self._enable_session_recall:
313
+ self._start_session_recall()
314
+
315
+ def _on_conversation_item_added(self, event: ConversationItemAddedEvent) -> None:
316
+ if not self._enable_retain or self._closed:
317
+ return
318
+ item = event.item
319
+ if item.type != "message" or item.role not in ("user", "assistant"):
320
+ return
321
+ text = item.text_content
322
+ if not text or not text.strip():
323
+ return
324
+
325
+ self._pending.append(_TranscriptLine(role=item.role, created_at=item.created_at, text=text.strip()))
326
+ # Flush once the buffer holds both sides of the conversation. With an
327
+ # STT-LLM-TTS pipeline the user's message is always added before the reply.
328
+ # Realtime models may add the user's transcript only after the reply it
329
+ # prompted, so flushing on every reply would store them out of order; waiting
330
+ # for both roles and sorting by created_at keeps the transcript in order.
331
+ roles = {line.role for line in self._pending}
332
+ if "user" in roles and "assistant" in roles:
333
+ self._schedule_retain()
334
+
335
+ def _on_session_close(self, event: CloseEvent) -> None:
336
+ self._schedule_retain()
337
+
338
+ def _schedule_retain(self) -> None:
339
+ if not self._pending:
340
+ return
341
+ lines = sorted(self._pending, key=lambda line: line.created_at)
342
+ self._pending = []
343
+ content = "\n".join(self._format_line(line) for line in lines)
344
+ task = asyncio.create_task(self._retain(content, _to_datetime(lines[-1].created_at)))
345
+ self._retain_tasks.add(task)
346
+ task.add_done_callback(self._retain_tasks.discard)
347
+
348
+ def _format_line(self, line: _TranscriptLine) -> str:
349
+ # "Name (timestamp): text" is the format Hindsight's docs use for conversations,
350
+ # so facts are attributed to the right speaker and time.
351
+ speaker = self._user_name if line.role == "user" else self._agent_name
352
+ timestamp = _to_datetime(line.created_at).isoformat(timespec="seconds")
353
+ return f"{speaker} ({timestamp}): {line.text}"
354
+
355
+ async def _retain(self, content: str, timestamp: datetime) -> None:
356
+ """Append ``content`` to this session's document (errors are logged and swallowed)."""
357
+ async with self._retain_lock:
358
+ try:
359
+ await self._client.aretain(
360
+ bank_id=self._bank_id,
361
+ content=content,
362
+ timestamp=timestamp,
363
+ context=self._retain_context,
364
+ document_id=self._document_id,
365
+ tags=self._tags,
366
+ update_mode="append",
367
+ )
368
+ except Exception as e:
369
+ logger.warning(f"Hindsight retain failed: {e}")
370
+
371
+ async def flush(self) -> None:
372
+ """Send any buffered transcript lines and wait for in-flight retains to finish."""
373
+ self._schedule_retain()
374
+ if self._retain_tasks:
375
+ await asyncio.gather(*self._retain_tasks, return_exceptions=True)
376
+
377
+ async def aclose(self) -> None:
378
+ """Flush pending retains and close the client if this object created it.
379
+
380
+ Register it with ``ctx.add_shutdown_callback(memory.aclose)`` so the end
381
+ of the call is stored before the job exits.
382
+ """
383
+ if self._closed:
384
+ return
385
+ await self.flush()
386
+ self._closed = True
387
+ for task in (self._recall_task, self._session_recall_task):
388
+ if task is not None and not task.done():
389
+ task.cancel()
390
+ if self._owns_client:
391
+ await self._client.aclose()
File without changes
@@ -0,0 +1,168 @@
1
+ """LiveKit function tools for Hindsight memory operations.
2
+
3
+ Provides a factory that creates LiveKit ``FunctionTool`` instances backed by
4
+ Hindsight's retain/recall/reflect APIs. Pass them to ``Agent(tools=[...])``.
5
+ They work with STT-LLM-TTS pipelines and realtime models.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+
12
+ from hindsight_client import Hindsight
13
+ from livekit.agents import RunContext, ToolError, function_tool
14
+ from livekit.agents.llm import FunctionTool
15
+
16
+ from ._client import resolve_client
17
+ from .config import (
18
+ DEFAULT_RECALL_BUDGET,
19
+ DEFAULT_RECALL_MAX_TOKENS,
20
+ DEFAULT_RECALL_TAGS_MATCH,
21
+ Budget,
22
+ TagsMatch,
23
+ get_config,
24
+ )
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+
29
+ def create_hindsight_tools(
30
+ *,
31
+ bank_id: str,
32
+ client: Hindsight | None = None,
33
+ hindsight_api_url: str | None = None,
34
+ api_key: str | None = None,
35
+ budget: Budget | None = None,
36
+ max_tokens: int | None = None,
37
+ tags: list[str] | None = None,
38
+ recall_tags: list[str] | None = None,
39
+ recall_tags_match: TagsMatch = DEFAULT_RECALL_TAGS_MATCH,
40
+ recall_types: list[str] | None = None,
41
+ retain_context: str = "voice conversation",
42
+ include_retain: bool = True,
43
+ include_recall: bool = True,
44
+ include_reflect: bool = True,
45
+ ) -> list[FunctionTool]:
46
+ """Create Hindsight memory tools for a LiveKit agent.
47
+
48
+ Returns ``FunctionTool`` instances for ``Agent(tools=[...])``. With a
49
+ :class:`~hindsight_livekit.HindsightMemory`, share its client and bank::
50
+
51
+ tools = create_hindsight_tools(bank_id=memory.bank_id, client=memory.client)
52
+
53
+ Errors are raised as LiveKit ``ToolError`` so the model hears what went wrong
54
+ and can tell the caller, instead of the call failing.
55
+
56
+ Args:
57
+ bank_id: The Hindsight memory bank to operate on.
58
+ client: Pre-configured Hindsight client (preferred).
59
+ hindsight_api_url: API URL (used if no client provided).
60
+ api_key: API key (used if no client provided).
61
+ budget: Recall/reflect budget level (low/mid/high). Defaults to ``"low"``.
62
+ max_tokens: Maximum tokens for recall and reflect results.
63
+ tags: Tags applied when storing memories via retain.
64
+ recall_tags: Tags to filter by when searching memories.
65
+ recall_tags_match: Tag matching mode (any/all/any_strict/all_strict).
66
+ recall_types: Fact types to search (world, experience, observation).
67
+ retain_context: Source label passed to Hindsight when storing memories.
68
+ include_retain: Include the ``hindsight_retain`` (store) tool.
69
+ include_recall: Include the ``hindsight_recall`` (search) tool.
70
+ include_reflect: Include the ``hindsight_reflect`` (synthesize) tool. Reflect
71
+ runs an LLM on the Hindsight server, so it is the slowest of the three.
72
+
73
+ Returns:
74
+ List of LiveKit FunctionTool instances.
75
+ """
76
+ resolved_client = resolve_client(client, hindsight_api_url, api_key)
77
+ config = get_config()
78
+ effective_budget: Budget = budget or (config.recall_budget if config else DEFAULT_RECALL_BUDGET)
79
+ effective_max_tokens = max_tokens or (config.recall_max_tokens if config else DEFAULT_RECALL_MAX_TOKENS)
80
+
81
+ tools: list[FunctionTool] = []
82
+
83
+ if include_retain:
84
+
85
+ @function_tool
86
+ async def hindsight_retain(context: RunContext, content: str) -> str:
87
+ """Store information in long-term memory so it is remembered in future calls.
88
+
89
+ Use this when the user shares something worth remembering, such as a
90
+ preference, a plan, or a fact about themselves.
91
+
92
+ Args:
93
+ content: The information to store, as a complete sentence.
94
+ """
95
+ try:
96
+ await resolved_client.aretain(
97
+ bank_id=bank_id,
98
+ content=content,
99
+ context=retain_context,
100
+ tags=tags,
101
+ )
102
+ except Exception as e:
103
+ logger.error(f"Hindsight retain failed: {e}")
104
+ raise ToolError("Could not save that to memory right now.") from e
105
+ return "Saved to memory."
106
+
107
+ tools.append(hindsight_retain)
108
+
109
+ if include_recall:
110
+
111
+ @function_tool
112
+ async def hindsight_recall(context: RunContext, query: str) -> str:
113
+ """Search long-term memory for things the user said in earlier conversations.
114
+
115
+ Use this when the user refers to something from the past, or when knowing
116
+ their preferences or history would help you answer.
117
+
118
+ Args:
119
+ query: What to look for, as a short question or phrase.
120
+ """
121
+ try:
122
+ response = await resolved_client.arecall(
123
+ bank_id=bank_id,
124
+ query=query,
125
+ types=recall_types,
126
+ budget=effective_budget,
127
+ max_tokens=effective_max_tokens,
128
+ tags=recall_tags,
129
+ tags_match=recall_tags_match,
130
+ )
131
+ except Exception as e:
132
+ logger.error(f"Hindsight recall failed: {e}")
133
+ raise ToolError("Could not search memory right now.") from e
134
+ if not response.results:
135
+ return "No relevant memories found."
136
+ return "\n".join(f"{i}. {result.text}" for i, result in enumerate(response.results, 1))
137
+
138
+ tools.append(hindsight_recall)
139
+
140
+ if include_reflect:
141
+
142
+ @function_tool
143
+ async def hindsight_reflect(context: RunContext, query: str) -> str:
144
+ """Answer a question by reasoning over everything in long-term memory.
145
+
146
+ Use this for broader questions, like summarising what you know about the
147
+ user, rather than looking up a single fact.
148
+
149
+ Args:
150
+ query: The question to answer from memory.
151
+ """
152
+ try:
153
+ response = await resolved_client.areflect(
154
+ bank_id=bank_id,
155
+ query=query,
156
+ budget=effective_budget,
157
+ max_tokens=effective_max_tokens,
158
+ tags=recall_tags,
159
+ tags_match=recall_tags_match,
160
+ )
161
+ except Exception as e:
162
+ logger.error(f"Hindsight reflect failed: {e}")
163
+ raise ToolError("Could not reflect on memory right now.") from e
164
+ return response.text or "No relevant memories found."
165
+
166
+ tools.append(hindsight_reflect)
167
+
168
+ return tools
@@ -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,12 @@
1
+ hindsight_livekit/__init__.py,sha256=OA-AvFrvZClGocM7msFcnPdKAVAJVmzsj4nQEb1kxLc,1432
2
+ hindsight_livekit/_client.py,sha256=qeFVo0ql5zjOaGWDoNITaI66-7oLSJef6ih1QPpBm5s,1465
3
+ hindsight_livekit/agent.py,sha256=J1Efha5Z2PkGEJiIUsinh4EfsoV7Y_wrAQ4o7Y3m39I,3747
4
+ hindsight_livekit/config.py,sha256=AEEpG_OFXveOW0WubWqPDgmS0LenxY20tbdZ013KPvc,2950
5
+ hindsight_livekit/errors.py,sha256=gPlcCs_KajuRSsLdT2Fsv_msUnrPbe6xE8KZetBrBq8,173
6
+ hindsight_livekit/memory.py,sha256=XaQUuImN_NlCFrrOtb3YvIUB5ggI8-UsTdHazAF-hxM,17585
7
+ hindsight_livekit/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ hindsight_livekit/tools.py,sha256=bg67Qg1GFJnYE-_9i_0DMhFoUEB2m8MU71huLrlwkYk,6543
9
+ hindsight_livekit-0.1.0.dist-info/METADATA,sha256=kfCSkT0wDHqmywDUkiph3Ncd_KcVQB59Jp9BNLRL_8Q,12439
10
+ hindsight_livekit-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
11
+ hindsight_livekit-0.1.0.dist-info/licenses/LICENSE,sha256=juw1vPPlfLMPts6FbNdNfpe_j3FinPgCJpbFppuHpB4,1062
12
+ hindsight_livekit-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -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.