livekit-memorysync 1.0.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.
@@ -0,0 +1,5 @@
1
+ venv/
2
+ __pycache__/
3
+ .pytest_cache/
4
+ dist/
5
+ *.egg-info/
@@ -0,0 +1,141 @@
1
+ Metadata-Version: 2.5
2
+ Name: livekit-memorysync
3
+ Version: 1.0.0
4
+ Summary: MemorySync for LiveKit Agents: voice agents that remember callers — budgeted recall injection (never a stalled reply), both-role capture with idempotency seeds, and background prefetch.
5
+ Project-URL: Homepage, https://docs.memorysync.io/guides/livekit
6
+ Project-URL: Documentation, https://docs.memorysync.io/guides/livekit
7
+ Project-URL: Repository, https://github.com/Rafay121/memorysync-plugins
8
+ Author-email: MemorySync <support@memorysync.io>
9
+ License-Expression: MIT
10
+ Keywords: agents,livekit,long-term-memory,memory,memorysync,voice
11
+ Classifier: Development Status :: 5 - Production/Stable
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Topic :: Communications :: Conferencing
18
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
19
+ Requires-Python: >=3.10
20
+ Requires-Dist: httpx<1,>=0.25
21
+ Requires-Dist: livekit-agents<2,>=1.0.0
22
+ Description-Content-Type: text/markdown
23
+
24
+ # livekit-memorysync
25
+
26
+ [MemorySync](https://memorysync.io) for [LiveKit Agents](https://docs.livekit.io/agents/) —
27
+ voice agents that remember callers across calls, without ever stalling a reply.
28
+
29
+ ```bash
30
+ pip install livekit-memorysync
31
+ ```
32
+
33
+ ## Why this exists
34
+
35
+ Voice is the one surface where memory latency is *audible*. A text chatbot can
36
+ spend two seconds fetching context; a voice agent that does so sounds broken.
37
+ This package is built around that constraint:
38
+
39
+ - **Budgeted recall.** Memory context is injected in `on_user_turn_completed`
40
+ under a hard timeout (default **1.2 s**). If MemorySync doesn't answer in
41
+ time, the reply proceeds *without* memories — never late.
42
+ - **Background prefetch.** After each turn, the next recall is warmed in the
43
+ background, so the common case is an instant cache hit, not a network call.
44
+ - **Both-role capture.** User *and* assistant turns are persisted (with
45
+ interruption metadata) via `conversation_item_added` — competitors that only
46
+ store user turns lose half the conversation.
47
+ - **Delta-only, idempotent writes.** Every stored turn carries a deterministic
48
+ seed, so retries and reconnects never duplicate memories.
49
+ - **Failure-proof.** Memory outages, quota limits, and dead networks degrade to
50
+ "no memories this turn". The call itself is never affected.
51
+
52
+ ## Quick start (composition — recommended)
53
+
54
+ Keep your own `Agent` subclass; attach memory to it:
55
+
56
+ ```python
57
+ from livekit.agents import Agent, AgentSession
58
+ from livekit_memorysync import MemorySyncMemory
59
+
60
+ memory = MemorySyncMemory(
61
+ api_key="ms_...", # or MEMORYSYNC_API_KEY env var
62
+ user_id="caller-42", # stable end-user id
63
+ thread_id="room-123", # optional: scope to this room/call
64
+ )
65
+
66
+ class Assistant(Agent):
67
+ def __init__(self) -> None:
68
+ super().__init__(instructions="You are a helpful voice assistant.")
69
+
70
+ async def on_user_turn_completed(self, turn_ctx, new_message):
71
+ # Inject memories for THIS turn only (never persisted into the LLM ctx)
72
+ await memory.on_user_turn(self, turn_ctx, new_message)
73
+
74
+ session = AgentSession(...) # your STT/LLM/TTS choices
75
+ memory.attach(session) # capture both roles as they finalize
76
+ await session.start(agent=Assistant(), ...)
77
+ ```
78
+
79
+ ## Quick start (drop-in agent)
80
+
81
+ ```python
82
+ from livekit_memorysync import MemorySyncAgent
83
+
84
+ agent = MemorySyncAgent(
85
+ instructions="You are a helpful voice assistant.",
86
+ api_key="ms_...",
87
+ user_id="caller-42",
88
+ )
89
+ # use like any Agent; recall + capture are wired for you
90
+ ```
91
+
92
+ ## Give the LLM a memory search tool
93
+
94
+ ```python
95
+ from livekit_memorysync import create_memory_search_tool
96
+
97
+ tool = create_memory_search_tool(memory)
98
+ agent = Agent(instructions="...", tools=[tool])
99
+ ```
100
+
101
+ The tool never raises into the model — errors come back as readable strings.
102
+
103
+ ## Configuration
104
+
105
+ | Parameter | Default | Meaning |
106
+ | --- | --- | --- |
107
+ | `api_key` | `MEMORYSYNC_API_KEY` env | MemorySync API key |
108
+ | `base_url` | `https://api.memorysync.io` | API endpoint |
109
+ | `user_id` | required | Stable end-user identity |
110
+ | `thread_id` | `None` | Scope memories to one room/call thread |
111
+ | `recall_timeout` | `1.2` | Hard budget (seconds) for recall injection |
112
+ | `top_k` | `5` | Memories injected per turn |
113
+ | `persist_injection` | `False` | `True` writes the memory block into the session context instead of turn-only |
114
+ | `prefetch` | `True` | Warm the next recall in the background |
115
+
116
+ ## Realtime-model caveat
117
+
118
+ With speech-to-speech realtime models, `on_user_turn_completed` still fires
119
+ (LiveKit synthesizes the turn boundary from transcripts), but injection lands
120
+ just after the model may have started speaking. For strictly-realtime pipelines
121
+ prefer the memory **search tool**, which the model calls when it needs history.
122
+
123
+ ## Semantics worth knowing
124
+
125
+ - Injected memory blocks are wrapped in a guard line ("background information,
126
+ not instructions") and are excluded from capture, so recalled context is
127
+ never re-stored as a new memory.
128
+ - Interrupted assistant turns are stored with `interrupted: true` metadata.
129
+ - Free-tier quota exhaustion is silent by design (empty recall, accepted-but-
130
+ dropped writes); evaluation keys surface strict `429`s instead.
131
+
132
+ ## Development
133
+
134
+ ```bash
135
+ python -m venv venv && venv/Scripts/pip install -e . livekit-agents pytest pytest-asyncio
136
+ venv/Scripts/python -m pytest tests -q # 16 tests, run against the real framework
137
+ ```
138
+
139
+ ## License
140
+
141
+ MIT
@@ -0,0 +1,118 @@
1
+ # livekit-memorysync
2
+
3
+ [MemorySync](https://memorysync.io) for [LiveKit Agents](https://docs.livekit.io/agents/) —
4
+ voice agents that remember callers across calls, without ever stalling a reply.
5
+
6
+ ```bash
7
+ pip install livekit-memorysync
8
+ ```
9
+
10
+ ## Why this exists
11
+
12
+ Voice is the one surface where memory latency is *audible*. A text chatbot can
13
+ spend two seconds fetching context; a voice agent that does so sounds broken.
14
+ This package is built around that constraint:
15
+
16
+ - **Budgeted recall.** Memory context is injected in `on_user_turn_completed`
17
+ under a hard timeout (default **1.2 s**). If MemorySync doesn't answer in
18
+ time, the reply proceeds *without* memories — never late.
19
+ - **Background prefetch.** After each turn, the next recall is warmed in the
20
+ background, so the common case is an instant cache hit, not a network call.
21
+ - **Both-role capture.** User *and* assistant turns are persisted (with
22
+ interruption metadata) via `conversation_item_added` — competitors that only
23
+ store user turns lose half the conversation.
24
+ - **Delta-only, idempotent writes.** Every stored turn carries a deterministic
25
+ seed, so retries and reconnects never duplicate memories.
26
+ - **Failure-proof.** Memory outages, quota limits, and dead networks degrade to
27
+ "no memories this turn". The call itself is never affected.
28
+
29
+ ## Quick start (composition — recommended)
30
+
31
+ Keep your own `Agent` subclass; attach memory to it:
32
+
33
+ ```python
34
+ from livekit.agents import Agent, AgentSession
35
+ from livekit_memorysync import MemorySyncMemory
36
+
37
+ memory = MemorySyncMemory(
38
+ api_key="ms_...", # or MEMORYSYNC_API_KEY env var
39
+ user_id="caller-42", # stable end-user id
40
+ thread_id="room-123", # optional: scope to this room/call
41
+ )
42
+
43
+ class Assistant(Agent):
44
+ def __init__(self) -> None:
45
+ super().__init__(instructions="You are a helpful voice assistant.")
46
+
47
+ async def on_user_turn_completed(self, turn_ctx, new_message):
48
+ # Inject memories for THIS turn only (never persisted into the LLM ctx)
49
+ await memory.on_user_turn(self, turn_ctx, new_message)
50
+
51
+ session = AgentSession(...) # your STT/LLM/TTS choices
52
+ memory.attach(session) # capture both roles as they finalize
53
+ await session.start(agent=Assistant(), ...)
54
+ ```
55
+
56
+ ## Quick start (drop-in agent)
57
+
58
+ ```python
59
+ from livekit_memorysync import MemorySyncAgent
60
+
61
+ agent = MemorySyncAgent(
62
+ instructions="You are a helpful voice assistant.",
63
+ api_key="ms_...",
64
+ user_id="caller-42",
65
+ )
66
+ # use like any Agent; recall + capture are wired for you
67
+ ```
68
+
69
+ ## Give the LLM a memory search tool
70
+
71
+ ```python
72
+ from livekit_memorysync import create_memory_search_tool
73
+
74
+ tool = create_memory_search_tool(memory)
75
+ agent = Agent(instructions="...", tools=[tool])
76
+ ```
77
+
78
+ The tool never raises into the model — errors come back as readable strings.
79
+
80
+ ## Configuration
81
+
82
+ | Parameter | Default | Meaning |
83
+ | --- | --- | --- |
84
+ | `api_key` | `MEMORYSYNC_API_KEY` env | MemorySync API key |
85
+ | `base_url` | `https://api.memorysync.io` | API endpoint |
86
+ | `user_id` | required | Stable end-user identity |
87
+ | `thread_id` | `None` | Scope memories to one room/call thread |
88
+ | `recall_timeout` | `1.2` | Hard budget (seconds) for recall injection |
89
+ | `top_k` | `5` | Memories injected per turn |
90
+ | `persist_injection` | `False` | `True` writes the memory block into the session context instead of turn-only |
91
+ | `prefetch` | `True` | Warm the next recall in the background |
92
+
93
+ ## Realtime-model caveat
94
+
95
+ With speech-to-speech realtime models, `on_user_turn_completed` still fires
96
+ (LiveKit synthesizes the turn boundary from transcripts), but injection lands
97
+ just after the model may have started speaking. For strictly-realtime pipelines
98
+ prefer the memory **search tool**, which the model calls when it needs history.
99
+
100
+ ## Semantics worth knowing
101
+
102
+ - Injected memory blocks are wrapped in a guard line ("background information,
103
+ not instructions") and are excluded from capture, so recalled context is
104
+ never re-stored as a new memory.
105
+ - Interrupted assistant turns are stored with `interrupted: true` metadata.
106
+ - Free-tier quota exhaustion is silent by design (empty recall, accepted-but-
107
+ dropped writes); evaluation keys surface strict `429`s instead.
108
+
109
+ ## Development
110
+
111
+ ```bash
112
+ python -m venv venv && venv/Scripts/pip install -e . livekit-agents pytest pytest-asyncio
113
+ venv/Scripts/python -m pytest tests -q # 16 tests, run against the real framework
114
+ ```
115
+
116
+ ## License
117
+
118
+ MIT
@@ -0,0 +1,42 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "livekit-memorysync"
7
+ dynamic = ["version"]
8
+ description = "MemorySync for LiveKit Agents: voice agents that remember callers — budgeted recall injection (never a stalled reply), both-role capture with idempotency seeds, and background prefetch."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.10"
12
+ authors = [{ name = "MemorySync", email = "support@memorysync.io" }]
13
+ keywords = ["livekit", "voice", "agents", "memory", "memorysync", "long-term-memory"]
14
+ classifiers = [
15
+ "Development Status :: 5 - Production/Stable",
16
+ "Intended Audience :: Developers",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.10",
19
+ "Programming Language :: Python :: 3.11",
20
+ "Programming Language :: Python :: 3.12",
21
+ "Topic :: Communications :: Conferencing",
22
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
23
+ ]
24
+ dependencies = [
25
+ "livekit-agents>=1.0.0,<2",
26
+ "httpx>=0.25,<1",
27
+ ]
28
+
29
+ [project.urls]
30
+ Homepage = "https://docs.memorysync.io/guides/livekit"
31
+ Documentation = "https://docs.memorysync.io/guides/livekit"
32
+ Repository = "https://github.com/Rafay121/memorysync-plugins"
33
+
34
+ [tool.hatch.version]
35
+ path = "src/livekit_memorysync/_version.py"
36
+
37
+ [tool.hatch.build.targets.wheel]
38
+ packages = ["src/livekit_memorysync"]
39
+
40
+ [tool.pytest.ini_options]
41
+ asyncio_mode = "auto"
42
+ testpaths = ["tests"]
@@ -0,0 +1,40 @@
1
+ """MemorySync for LiveKit Agents — voice agents that remember callers.
2
+
3
+ Two ways in, one engine:
4
+
5
+ - **Composition (recommended)** — keep YOUR Agent subclass; wire two lines::
6
+
7
+ memory = MemorySyncMemory(api_key=..., user_id="caller-123", thread_id=room_name)
8
+
9
+ class MyAgent(Agent):
10
+ async def on_user_turn_completed(self, turn_ctx, new_message):
11
+ await memory.on_user_turn(turn_ctx, new_message)
12
+
13
+ memory.attach(session) # captures BOTH sides of the conversation
14
+
15
+ - **Drop-in** — ``MemorySyncAgent`` subclasses ``Agent`` and wires it for you.
16
+
17
+ The voice contract this package is built around: **recall runs under a hard
18
+ time budget** (default 1.2 s) — a slow network yields a memoryless turn,
19
+ never a stalled spoken reply — and **persistence never blocks anything**
20
+ (fire-and-forget with content-hash idempotency seeds, both roles captured,
21
+ interruptions marked). A background prefetch primes the next turn's recall,
22
+ so steady-state injection costs ~0 ms.
23
+ """
24
+
25
+ from ._api import AsyncV1Api, MemorySyncAPIError, fnv1a64
26
+ from ._version import __version__
27
+ from .memory import MemorySyncMemory, MemoryInjection
28
+ from .agent import MemorySyncAgent
29
+ from .tools import create_memory_search_tool
30
+
31
+ __all__ = [
32
+ "MemorySyncMemory",
33
+ "MemorySyncAgent",
34
+ "MemoryInjection",
35
+ "create_memory_search_tool",
36
+ "AsyncV1Api",
37
+ "MemorySyncAPIError",
38
+ "fnv1a64",
39
+ "__version__",
40
+ ]
@@ -0,0 +1,295 @@
1
+ """Async client for the MemorySync v1 data plane used by this integration.
2
+
3
+ Conversation turns persist through the *episodic* ingestion path
4
+ (``POST /v1/memory/add_turn``), which stores text verbatim — no fact
5
+ extraction, no low-value-chatter gate, no rewriting. A voice transcript
6
+ must round-trip byte-for-byte; a plane that second-guessed it would
7
+ corrupt the caller's history.
8
+
9
+ Everything here is async-native because LiveKit's agent runtime drives
10
+ every call from the event loop — a blocking HTTP client inside a turn
11
+ hook would stall the spoken reply.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import os
17
+ from typing import Any, Dict, List, Optional
18
+
19
+ import httpx
20
+
21
+ from ._version import __version__
22
+
23
+ DEFAULT_BASE_URL = "https://api.memorysync.io"
24
+ _USER_AGENT = f"livekit-memorysync/{__version__}"
25
+
26
+ #: Namespace used when the key cannot list projects (see resolve_tenant_id).
27
+ FALLBACK_TENANT = "default"
28
+
29
+
30
+ class MemorySyncAPIError(Exception):
31
+ """A MemorySync call failed. Carries the status code and server detail."""
32
+
33
+ def __init__(self, message: str, *, status_code: Optional[int] = None) -> None:
34
+ super().__init__(message)
35
+ self.status_code = status_code
36
+
37
+
38
+ def resolve_api_key(api_key: Optional[str]) -> str:
39
+ key = api_key or os.environ.get("MEMORYSYNC_API_KEY", "")
40
+ if not key or not key.strip():
41
+ raise ValueError(
42
+ "A MemorySync API key is required. Pass api_key=... or set the "
43
+ "MEMORYSYNC_API_KEY environment variable."
44
+ )
45
+ return key.strip()
46
+
47
+
48
+ def resolve_base_url(base_url: Optional[str]) -> str:
49
+ url = base_url or os.environ.get("MEMORYSYNC_BASE_URL", "") or DEFAULT_BASE_URL
50
+ return url.rstrip("/")
51
+
52
+
53
+ def fnv1a64(value: str) -> str:
54
+ """FNV-1a 64-bit over UTF-16 code units, as a fixed-width hex string.
55
+
56
+ Over UTF-16 code units — not code points, not UTF-8 bytes — so the
57
+ output matches the JavaScript adapters (`fnv1a64` in memorysync-ai-sdk
58
+ and memorysync-mastra hash over ``charCodeAt``) character for
59
+ character. Identical seeds across languages mean a turn persisted by a
60
+ Python surface and again by a JS surface converge on one stored row.
61
+ """
62
+ prime = 0x100000001B3
63
+ mask = 0xFFFFFFFFFFFFFFFF
64
+ h = 0xCBF29CE484222325
65
+ data = value.encode("utf-16-le")
66
+ for i in range(0, len(data), 2):
67
+ unit = data[i] | (data[i + 1] << 8)
68
+ h ^= unit
69
+ h = (h * prime) & mask
70
+ return format(h, "016x")
71
+
72
+
73
+ class AsyncV1Api:
74
+ """Minimal asynchronous v1 client: add_turn, recall, query, list."""
75
+
76
+ def __init__(
77
+ self,
78
+ *,
79
+ api_key: str,
80
+ base_url: str,
81
+ project_id: Optional[str] = None,
82
+ timeout: float = 30.0,
83
+ transport: Optional[httpx.AsyncBaseTransport] = None,
84
+ ) -> None:
85
+ self._api_key = api_key
86
+ self._base_url = base_url.rstrip("/")
87
+ self._project_id = project_id
88
+ self._timeout = timeout
89
+ self._transport = transport
90
+ self._http = httpx.AsyncClient(timeout=timeout, transport=transport)
91
+ self._tenant_id: Optional[str] = None
92
+ self._tenant_is_fallback = False
93
+
94
+ async def aclose(self) -> None:
95
+ await self._http.aclose()
96
+
97
+ @property
98
+ def api_key(self) -> str:
99
+ return self._api_key
100
+
101
+ @property
102
+ def base_url(self) -> str:
103
+ return self._base_url
104
+
105
+ @property
106
+ def project_id(self) -> Optional[str]:
107
+ return self._project_id
108
+
109
+ @property
110
+ def timeout(self) -> float:
111
+ return self._timeout
112
+
113
+ @property
114
+ def transport(self) -> Optional[httpx.AsyncBaseTransport]:
115
+ return self._transport
116
+
117
+ # ── plumbing ─────────────────────────────────────────────────────
118
+
119
+ def _headers(self) -> Dict[str, str]:
120
+ h = {
121
+ "X-API-Key": self._api_key,
122
+ "Accept": "application/json",
123
+ "User-Agent": _USER_AGENT,
124
+ }
125
+ if self._project_id:
126
+ h["X-Project-ID"] = self._project_id
127
+ return h
128
+
129
+ async def _request(
130
+ self,
131
+ method: str,
132
+ path: str,
133
+ *,
134
+ json: Optional[Dict[str, Any]] = None,
135
+ params: Optional[Dict[str, Any]] = None,
136
+ ) -> Any:
137
+ url = f"{self._base_url}{path}"
138
+ try:
139
+ response = await self._http.request(
140
+ method, url, headers=self._headers(), json=json, params=params
141
+ )
142
+ except httpx.TimeoutException as e:
143
+ raise MemorySyncAPIError(f"Request timed out: {e}") from e
144
+ except httpx.HTTPError as e:
145
+ raise MemorySyncAPIError(f"Network error: {e}") from e
146
+
147
+ if response.status_code == 204:
148
+ return None
149
+ try:
150
+ body: Any = response.json()
151
+ except ValueError:
152
+ body = response.text or None
153
+ if response.status_code >= 400:
154
+ detail = body.get("detail") if isinstance(body, dict) else body
155
+ raise MemorySyncAPIError(
156
+ f"{method} {path} failed with HTTP {response.status_code}: {detail}",
157
+ status_code=response.status_code,
158
+ )
159
+ return body
160
+
161
+ # ── calls ────────────────────────────────────────────────────────
162
+
163
+ async def resolve_tenant_id(self) -> str:
164
+ """The tenant id, which the v1 routes need in path or body.
165
+
166
+ Derived from the project listing rather than asked for. Cached for
167
+ the lifetime of this client; one extra GET per process, not per
168
+ turn. Keys without the ``projects:read`` scope (evaluation keys)
169
+ fall back to the fixed namespace ``"default"`` — deterministic, so
170
+ every read and write through this client lands in one namespace.
171
+ Only a definite 401/403 triggers the fallback; a transient server
172
+ error re-raises rather than silently switching namespaces.
173
+ """
174
+ if self._tenant_id:
175
+ return self._tenant_id
176
+ try:
177
+ projects = await self._request("GET", "/org/projects")
178
+ except MemorySyncAPIError as exc:
179
+ if exc.status_code in (401, 403):
180
+ self._tenant_id = FALLBACK_TENANT
181
+ self._tenant_is_fallback = True
182
+ return self._tenant_id
183
+ raise
184
+ first = projects[0] if isinstance(projects, list) and projects else None
185
+ tenant = first.get("tenant_id") if isinstance(first, dict) else None
186
+ if not tenant:
187
+ raise MemorySyncAPIError(
188
+ "Could not determine the tenant for this API key. Pass "
189
+ "tenant_id=... explicitly, or verify the key with "
190
+ "`memorysync doctor`."
191
+ )
192
+ self._tenant_id = str(tenant)
193
+ return self._tenant_id
194
+
195
+ @property
196
+ def tenant_is_fallback(self) -> bool:
197
+ """True when the namespace came from the 401/403 fallback."""
198
+ return self._tenant_is_fallback
199
+
200
+ def set_tenant_id(self, tenant_id: str) -> None:
201
+ self._tenant_id = tenant_id
202
+
203
+ async def add_turn(
204
+ self,
205
+ *,
206
+ tenant_id: str,
207
+ user_id: str,
208
+ text: str,
209
+ speaker: Optional[str] = None,
210
+ occurred_at: Optional[str] = None,
211
+ metadata: Optional[Dict[str, Any]] = None,
212
+ source: str = "livekit",
213
+ sync_embed: bool = False,
214
+ ) -> Dict[str, Any]:
215
+ """Store one item verbatim (episodic ingestion).
216
+
217
+ ``speaker`` + ``occurred_at`` participate in the server's
218
+ idempotency seed, so retrying an identical payload is recognised
219
+ (``already_exists: true``) instead of stored twice.
220
+ """
221
+ body: Dict[str, Any] = {
222
+ "tenant_id": tenant_id,
223
+ "user_id": user_id,
224
+ "source": source,
225
+ "text": text,
226
+ "sync_embed": sync_embed,
227
+ }
228
+ if speaker is not None:
229
+ body["speaker"] = speaker
230
+ if occurred_at is not None:
231
+ body["occurred_at"] = occurred_at
232
+ if metadata is not None:
233
+ body["metadata"] = metadata
234
+ return await self._request("POST", "/v1/memory/add_turn", json=body) or {}
235
+
236
+ async def recall(
237
+ self,
238
+ *,
239
+ tenant_id: str,
240
+ user_id: str,
241
+ prompt: str,
242
+ k: Optional[int] = None,
243
+ types: Optional[List[str]] = None,
244
+ ) -> Dict[str, Any]:
245
+ """Hierarchical recall: grouped, prompt-ready context block."""
246
+ body: Dict[str, Any] = {
247
+ "tenant_id": tenant_id,
248
+ "user_id": user_id,
249
+ "prompt": prompt,
250
+ }
251
+ if k is not None:
252
+ body["k"] = k
253
+ if types is not None:
254
+ body["types"] = types
255
+ return await self._request("POST", "/v1/memory/recall", json=body) or {}
256
+
257
+ async def query(
258
+ self,
259
+ *,
260
+ tenant_id: str,
261
+ user_id: str,
262
+ prompt: str,
263
+ k: Optional[int] = None,
264
+ ) -> Dict[str, Any]:
265
+ """Plain semantic search over the pair's memories (episodic included)."""
266
+ body: Dict[str, Any] = {
267
+ "tenant_id": tenant_id,
268
+ "user_id": user_id,
269
+ "prompt": prompt,
270
+ }
271
+ if k is not None:
272
+ body["k"] = k
273
+ return await self._request("POST", "/v1/memory/query", json=body) or {}
274
+
275
+ async def list_memories(
276
+ self,
277
+ *,
278
+ tenant_id: str,
279
+ user_id: str,
280
+ limit: int = 0,
281
+ ) -> List[Dict[str, Any]]:
282
+ """Every memory for the tenant/user pair, newest first.
283
+
284
+ ``limit=0`` means no limit — a transcript read must never be
285
+ silently truncated, so that is the default here.
286
+ """
287
+ from urllib.parse import quote
288
+
289
+ raw = await self._request(
290
+ "GET",
291
+ f"/v1/memory/{quote(tenant_id, safe='')}/{quote(user_id, safe='')}/list",
292
+ params={"limit": limit},
293
+ )
294
+ memories = raw.get("memories") if isinstance(raw, dict) else None
295
+ return list(memories) if isinstance(memories, list) else []
@@ -0,0 +1,3 @@
1
+ """Version for livekit-memorysync. Single source of truth."""
2
+
3
+ __version__ = "1.0.0"