pipecat-memorysync 1.0.1__tar.gz → 1.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pipecat_memorysync-1.1.1/CHANGELOG.md +51 -0
- {pipecat_memorysync-1.0.1 → pipecat_memorysync-1.1.1}/PKG-INFO +7 -1
- {pipecat_memorysync-1.0.1 → pipecat_memorysync-1.1.1}/README.md +6 -0
- {pipecat_memorysync-1.0.1 → pipecat_memorysync-1.1.1}/src/pipecat_memorysync/_version.py +1 -1
- {pipecat_memorysync-1.0.1 → pipecat_memorysync-1.1.1}/src/pipecat_memorysync/service.py +53 -18
- {pipecat_memorysync-1.0.1 → pipecat_memorysync-1.1.1}/tests/test_service.py +58 -0
- pipecat_memorysync-1.0.1/CHANGELOG.md +0 -25
- {pipecat_memorysync-1.0.1 → pipecat_memorysync-1.1.1}/.gitignore +0 -0
- {pipecat_memorysync-1.0.1 → pipecat_memorysync-1.1.1}/examples/foundational.py +0 -0
- {pipecat_memorysync-1.0.1 → pipecat_memorysync-1.1.1}/pyproject.toml +0 -0
- {pipecat_memorysync-1.0.1 → pipecat_memorysync-1.1.1}/src/pipecat_memorysync/__init__.py +0 -0
- {pipecat_memorysync-1.0.1 → pipecat_memorysync-1.1.1}/src/pipecat_memorysync/_api.py +0 -0
- {pipecat_memorysync-1.0.1 → pipecat_memorysync-1.1.1}/tests/conftest.py +0 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `pipecat-memorysync` are documented here.
|
|
4
|
+
|
|
5
|
+
## 1.1.1 — 2026-08-29
|
|
6
|
+
|
|
7
|
+
- **Immediate, loss-proof capture.** 1.1.0's turn-complete merging
|
|
8
|
+
deferred the current utterance until the turn completed — on a browser
|
|
9
|
+
disconnect that deferral raced the brief cancel salvage window and
|
|
10
|
+
could lose the newest (usually most important) turn over slow
|
|
11
|
+
networks. Capture is immediate again: every new user/assistant message
|
|
12
|
+
is in flight the moment its frame passes, BEFORE the LLM replies.
|
|
13
|
+
Junk filtering and fact extraction now happen server-side (the
|
|
14
|
+
platform's low-value gate + conversational ingestion), so client-side
|
|
15
|
+
merging is unnecessary. Suite: 17 checks on Pipecat v1.8.1, including
|
|
16
|
+
the exact disconnect sequence that previously lost data.
|
|
17
|
+
|
|
18
|
+
## 1.1.0 — 2026-08-29
|
|
19
|
+
|
|
20
|
+
- **Turn-complete capture.** Voice aggregators split one utterance across
|
|
21
|
+
several context messages at speech pauses; those fragments previously
|
|
22
|
+
stored as separate rows ("and", "dinner", …). Consecutive new user
|
|
23
|
+
fragments now merge into ONE verbatim turn, stored when the assistant
|
|
24
|
+
reply completes the turn; an in-progress utterance is flushed as one
|
|
25
|
+
merged turn at end of call. Assistant turns still store immediately,
|
|
26
|
+
and a failed store releases its fragments for retry on the next frame.
|
|
27
|
+
- Delta-only and idempotency guarantees unchanged; suite grows to 17
|
|
28
|
+
checks on Pipecat v1.8.1 (fragment merging, end-of-call tail flush,
|
|
29
|
+
failed-store retry).
|
|
30
|
+
|
|
31
|
+
## 1.0.1 — 2026-08-29
|
|
32
|
+
|
|
33
|
+
- Metadata only: the source repository moved to
|
|
34
|
+
`https://github.com/memorysyncio/memorysync-pipecat`; project URLs updated.
|
|
35
|
+
No code changes.
|
|
36
|
+
|
|
37
|
+
## 1.0.0 — 2026-08-29
|
|
38
|
+
|
|
39
|
+
Initial release.
|
|
40
|
+
|
|
41
|
+
- `MemorySyncMemoryService`, a `FrameProcessor` for the position between the
|
|
42
|
+
user context aggregator and the LLM service.
|
|
43
|
+
- Budgeted memory recall (default 1.2 s hard timeout) injected into every
|
|
44
|
+
`LLMContextFrame` as a guarded system message — a slow or unreachable
|
|
45
|
+
backend degrades to an unenriched frame, never a stalled reply.
|
|
46
|
+
- Delta-only capture with deterministic idempotency seeds: only turns not
|
|
47
|
+
seen before are persisted, with role fidelity for user and assistant.
|
|
48
|
+
- Injection exclusion: the injected memory block is never re-captured.
|
|
49
|
+
- Graceful `EndFrame` flush (bounded 3 s window) and `CancelFrame` salvage.
|
|
50
|
+
- Tested with Pipecat v1.8.1 through `pipecat.tests.utils.run_test`
|
|
51
|
+
(14-test suite).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: pipecat-memorysync
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.1.1
|
|
4
4
|
Summary: MemorySync for Pipecat: budgeted memory recall that never stalls a voice reply, delta-only conversation persistence with idempotency seeds, and a pipeline that cannot be broken by a memory outage.
|
|
5
5
|
Project-URL: Homepage, https://docs.memorysync.io/guides/pipecat
|
|
6
6
|
Project-URL: Documentation, https://docs.memorysync.io/guides/pipecat
|
|
@@ -81,6 +81,12 @@ enriched or not, on time.
|
|
|
81
81
|
- **Budgeted recall.** Enrichment runs under a hard timeout (default
|
|
82
82
|
**1.2 s**). A slow or dead memory backend means an unenriched frame, never a
|
|
83
83
|
stalled voice reply.
|
|
84
|
+
- **Immediate, loss-proof capture.** Every new message is sent the moment
|
|
85
|
+
its frame passes — the current utterance is in flight BEFORE the LLM
|
|
86
|
+
replies, so a disconnect can never lose it. Junk filtering and fact
|
|
87
|
+
extraction happen server-side (the platform's low-value gate +
|
|
88
|
+
conversational ingestion): the memory store receives curated facts,
|
|
89
|
+
not transcript clutter.
|
|
84
90
|
- **Delta-only capture.** Only messages *not seen before* are stored, tracked
|
|
85
91
|
by deterministic idempotency seeds. Growing a 50-message context does not
|
|
86
92
|
re-store 50 messages per turn.
|
|
@@ -57,6 +57,12 @@ enriched or not, on time.
|
|
|
57
57
|
- **Budgeted recall.** Enrichment runs under a hard timeout (default
|
|
58
58
|
**1.2 s**). A slow or dead memory backend means an unenriched frame, never a
|
|
59
59
|
stalled voice reply.
|
|
60
|
+
- **Immediate, loss-proof capture.** Every new message is sent the moment
|
|
61
|
+
its frame passes — the current utterance is in flight BEFORE the LLM
|
|
62
|
+
replies, so a disconnect can never lose it. Junk filtering and fact
|
|
63
|
+
extraction happen server-side (the platform's low-value gate +
|
|
64
|
+
conversational ingestion): the memory store receives curated facts,
|
|
65
|
+
not transcript clutter.
|
|
60
66
|
- **Delta-only capture.** Only messages *not seen before* are stored, tracked
|
|
61
67
|
by deterministic idempotency seeds. Growing a 50-message context does not
|
|
62
68
|
re-store 50 messages per turn.
|
|
@@ -7,9 +7,13 @@ predecessors don't:
|
|
|
7
7
|
1. **Recall is budgeted.** Enrichment waits at most ``recall_timeout``
|
|
8
8
|
seconds (default 1.2). On timeout or failure the context frame passes
|
|
9
9
|
through unenriched — a voice reply is never stalled by a slow network.
|
|
10
|
-
2. **Capture is delta-only.**
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
2. **Capture is immediate and delta-only.** Every NEW user/assistant
|
|
11
|
+
message is sent to the platform the moment it appears in a context
|
|
12
|
+
frame — the current utterance is already in flight BEFORE the LLM
|
|
13
|
+
replies, so a disconnect can never lose it. Junk filtering and fact
|
|
14
|
+
extraction are the platform's job (the low-value gate + conversational
|
|
15
|
+
ingestion), not the client's. Only new messages are sent each frame,
|
|
16
|
+
with cross-adapter fnv1a64 idempotency seeds.
|
|
13
17
|
3. **Nothing raises, nothing is dropped.** Every failure path logs and
|
|
14
18
|
pushes the ORIGINAL frame through. The pipeline cannot stall and the
|
|
15
19
|
LLM always gets its context.
|
|
@@ -137,6 +141,9 @@ class MemorySyncMemoryService(FrameProcessor):
|
|
|
137
141
|
self.params = params
|
|
138
142
|
|
|
139
143
|
self._stored_seeds: Set[str] = set()
|
|
144
|
+
# Message-level ledger: which context messages have been sent.
|
|
145
|
+
# A failed store releases its message so the next frame retries.
|
|
146
|
+
self._seen_messages: Set[str] = set()
|
|
140
147
|
self._store_tasks: Set[asyncio.Task] = set()
|
|
141
148
|
self._last_query: Optional[str] = None
|
|
142
149
|
|
|
@@ -237,10 +244,20 @@ class MemorySyncMemoryService(FrameProcessor):
|
|
|
237
244
|
body = "\n".join(lines)
|
|
238
245
|
return f"{self.params.system_prompt}\n{body}\n\n{CONTEXT_GUARD}"
|
|
239
246
|
|
|
240
|
-
# ── capture: delta-only, background
|
|
247
|
+
# ── capture: immediate, delta-only, background ─────────────────────
|
|
241
248
|
|
|
242
249
|
def _capture_delta(self, context: Any) -> None:
|
|
243
|
-
"""Queue storage for messages
|
|
250
|
+
"""Queue storage for NEW messages the moment they appear.
|
|
251
|
+
|
|
252
|
+
The platform owns junk filtering (the low-value gate) and fact
|
|
253
|
+
extraction (conversational ingestion), so the adapter's single
|
|
254
|
+
duty is delivering every turn reliably and EARLY. The current
|
|
255
|
+
user utterance is in the frame BEFORE the LLM replies — sending
|
|
256
|
+
it immediately means a mid-call disconnect can never lose it.
|
|
257
|
+
(1.1.0 deferred the current utterance until the turn completed;
|
|
258
|
+
that deferral raced the disconnect salvage window and could drop
|
|
259
|
+
the newest — usually most important — turn. Never again.)
|
|
260
|
+
"""
|
|
244
261
|
header = self.params.system_prompt
|
|
245
262
|
for message in context.get_messages():
|
|
246
263
|
role = message.get("role")
|
|
@@ -251,19 +268,33 @@ class MemorySyncMemoryService(FrameProcessor):
|
|
|
251
268
|
continue # our own injection (user-role mode) never re-enters
|
|
252
269
|
speaker_role = "human" if role == "user" else "ai"
|
|
253
270
|
trimmed = text if len(text) <= MAX_TURN_CHARS else text[:MAX_TURN_CHARS] + "…"
|
|
254
|
-
|
|
255
|
-
if
|
|
271
|
+
key = f"{speaker_role}:{trimmed}"
|
|
272
|
+
if key in self._seen_messages:
|
|
256
273
|
continue
|
|
257
|
-
self.
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
self.
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
274
|
+
self._seen_messages.add(key)
|
|
275
|
+
self._bound_seen()
|
|
276
|
+
self._queue_store(speaker_role, trimmed, [key])
|
|
277
|
+
|
|
278
|
+
def _bound_seen(self) -> None:
|
|
279
|
+
if len(self._seen_messages) > 4096:
|
|
280
|
+
self._seen_messages.clear()
|
|
281
|
+
|
|
282
|
+
def _queue_store(self, speaker_role: str, text: str, msg_keys: List[str]) -> None:
|
|
283
|
+
seed = f"{speaker_role}:{text}"
|
|
284
|
+
if seed in self._stored_seeds:
|
|
285
|
+
return
|
|
286
|
+
self._stored_seeds.add(seed)
|
|
287
|
+
if len(self._stored_seeds) > 4096:
|
|
288
|
+
self._stored_seeds.clear()
|
|
289
|
+
# Plain asyncio tasks, tracked locally: pipeline teardown must
|
|
290
|
+
# not cancel a persist mid-flight — _flush owns their fate.
|
|
291
|
+
task = asyncio.create_task(self._store_turn(speaker_role, text, seed, msg_keys))
|
|
292
|
+
self._store_tasks.add(task)
|
|
293
|
+
task.add_done_callback(self._store_tasks.discard)
|
|
294
|
+
|
|
295
|
+
async def _store_turn(
|
|
296
|
+
self, speaker_role: str, text: str, seed: str, msg_keys: List[str]
|
|
297
|
+
) -> None:
|
|
267
298
|
try:
|
|
268
299
|
tenant = await self._api.resolve_tenant_id()
|
|
269
300
|
await self._api.add_turn(
|
|
@@ -274,7 +305,11 @@ class MemorySyncMemoryService(FrameProcessor):
|
|
|
274
305
|
metadata={"session_id": self.scope},
|
|
275
306
|
)
|
|
276
307
|
except Exception as exc: # noqa: BLE001
|
|
277
|
-
|
|
308
|
+
# The write never landed: release both ledgers so the next
|
|
309
|
+
# frame re-captures this message and retries the store.
|
|
310
|
+
self._stored_seeds.discard(seed)
|
|
311
|
+
for key in msg_keys:
|
|
312
|
+
self._seen_messages.discard(key)
|
|
278
313
|
logger.debug(f"memorysync: store failed: {exc}")
|
|
279
314
|
|
|
280
315
|
# ── conveniences (outside the pipeline) ───────────────────────────
|
|
@@ -250,3 +250,61 @@ async def test_conveniences_answer(mock, make_service):
|
|
|
250
250
|
async def test_missing_user_id_is_loud_at_construction(mock):
|
|
251
251
|
with pytest.raises(ValueError, match="user_id"):
|
|
252
252
|
MemorySyncMemoryService(api_key="ms_x", user_id="", transport=mock.transport())
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
# ── capture: immediate and loss-proof ─────────────────────────────────
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
async def test_current_utterance_stores_before_any_reply(mock, make_service):
|
|
259
|
+
"""The user's words must be in flight the moment the frame passes —
|
|
260
|
+
BEFORE the LLM replies — so a disconnect can never lose them."""
|
|
261
|
+
service = make_service()
|
|
262
|
+
ctx = context_of([
|
|
263
|
+
{"role": "user", "content": "My age is twenty two and I completed my bachelor's."},
|
|
264
|
+
])
|
|
265
|
+
await drive(service, ctx)
|
|
266
|
+
await drain(mock, 1)
|
|
267
|
+
assert [r["text"] for r in mock.rows] == [
|
|
268
|
+
"human: My age is twenty two and I completed my bachelor's."
|
|
269
|
+
]
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
async def test_demo_disconnect_sequence_loses_nothing(mock, make_service):
|
|
273
|
+
"""The exact sequence that lost data in 1.1.0: greeting frame, then a
|
|
274
|
+
frame carrying the reply + the important utterance, then immediate
|
|
275
|
+
CancelFrame (browser disconnect). Every message must already be
|
|
276
|
+
stored — nothing may depend on a post-cancel flush window."""
|
|
277
|
+
service = make_service()
|
|
278
|
+
f1 = context_of([{"role": "user", "content": "Hello there, anyone home?"}])
|
|
279
|
+
f2 = context_of([
|
|
280
|
+
{"role": "user", "content": "Hello there, anyone home?"},
|
|
281
|
+
{"role": "assistant", "content": "Hi! How can I help you today?"},
|
|
282
|
+
{"role": "user", "content": "My age is twenty two and I completed my bachelor's."},
|
|
283
|
+
])
|
|
284
|
+
await drive(service, f1, f2) # run_test tears the pipeline down right after
|
|
285
|
+
await drain(mock, 3)
|
|
286
|
+
texts = sorted(r["text"] for r in mock.rows)
|
|
287
|
+
assert texts == [
|
|
288
|
+
"ai: Hi! How can I help you today?",
|
|
289
|
+
"human: Hello there, anyone home?",
|
|
290
|
+
"human: My age is twenty two and I completed my bachelor's.",
|
|
291
|
+
]
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
async def test_failed_store_releases_message_for_retry(mock, make_service):
|
|
295
|
+
"""A store that never landed must not consume its message — the next
|
|
296
|
+
frame re-captures and retries, converging on one row."""
|
|
297
|
+
service = make_service()
|
|
298
|
+
mock.fail_next = 503
|
|
299
|
+
turn = [
|
|
300
|
+
{"role": "user", "content": "remember that I fly out of Hyderabad"},
|
|
301
|
+
{"role": "assistant", "content": "Noted!"},
|
|
302
|
+
]
|
|
303
|
+
await drive(service, context_of(turn))
|
|
304
|
+
await asyncio.sleep(0.3) # the 503 lands on exactly one of the two stores
|
|
305
|
+
|
|
306
|
+
await drive(service, context_of(turn)) # same context re-seen → retry
|
|
307
|
+
await drain(mock, 2)
|
|
308
|
+
|
|
309
|
+
texts = sorted(r["text"] for r in mock.rows)
|
|
310
|
+
assert texts == ["ai: Noted!", "human: remember that I fly out of Hyderabad"]
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
All notable changes to `pipecat-memorysync` are documented here.
|
|
4
|
-
|
|
5
|
-
## 1.0.1 — 2026-08-29
|
|
6
|
-
|
|
7
|
-
- Metadata only: the source repository moved to
|
|
8
|
-
`https://github.com/memorysyncio/memorysync-pipecat`; project URLs updated.
|
|
9
|
-
No code changes.
|
|
10
|
-
|
|
11
|
-
## 1.0.0 — 2026-08-29
|
|
12
|
-
|
|
13
|
-
Initial release.
|
|
14
|
-
|
|
15
|
-
- `MemorySyncMemoryService`, a `FrameProcessor` for the position between the
|
|
16
|
-
user context aggregator and the LLM service.
|
|
17
|
-
- Budgeted memory recall (default 1.2 s hard timeout) injected into every
|
|
18
|
-
`LLMContextFrame` as a guarded system message — a slow or unreachable
|
|
19
|
-
backend degrades to an unenriched frame, never a stalled reply.
|
|
20
|
-
- Delta-only capture with deterministic idempotency seeds: only turns not
|
|
21
|
-
seen before are persisted, with role fidelity for user and assistant.
|
|
22
|
-
- Injection exclusion: the injected memory block is never re-captured.
|
|
23
|
-
- Graceful `EndFrame` flush (bounded 3 s window) and `CancelFrame` salvage.
|
|
24
|
-
- Tested with Pipecat v1.8.1 through `pipecat.tests.utils.run_test`
|
|
25
|
-
(14-test suite).
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|