maximem-synap-pipecat 0.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.
- maximem_synap_pipecat-0.1.1/PKG-INFO +77 -0
- maximem_synap_pipecat-0.1.1/README.md +61 -0
- maximem_synap_pipecat-0.1.1/maximem_synap_pipecat.egg-info/PKG-INFO +77 -0
- maximem_synap_pipecat-0.1.1/maximem_synap_pipecat.egg-info/SOURCES.txt +9 -0
- maximem_synap_pipecat-0.1.1/maximem_synap_pipecat.egg-info/dependency_links.txt +1 -0
- maximem_synap_pipecat-0.1.1/maximem_synap_pipecat.egg-info/requires.txt +7 -0
- maximem_synap_pipecat-0.1.1/maximem_synap_pipecat.egg-info/top_level.txt +1 -0
- maximem_synap_pipecat-0.1.1/pyproject.toml +35 -0
- maximem_synap_pipecat-0.1.1/setup.cfg +4 -0
- maximem_synap_pipecat-0.1.1/synap_pipecat/__init__.py +41 -0
- maximem_synap_pipecat-0.1.1/synap_pipecat/processors.py +251 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: maximem-synap-pipecat
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Synap memory integration for Pipecat — voice-pipeline context injection + conversation recording
|
|
5
|
+
Author: Synap Team
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Keywords: synap,memory,pipecat,voice,ai,agents
|
|
8
|
+
Requires-Python: >=3.11
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
Requires-Dist: maximem-synap>=0.2.0
|
|
11
|
+
Requires-Dist: synap-integrations-common>=0.1.0
|
|
12
|
+
Requires-Dist: pipecat-ai>=0.0.80
|
|
13
|
+
Provides-Extra: dev
|
|
14
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
15
|
+
Requires-Dist: pytest-asyncio>=0.21; extra == "dev"
|
|
16
|
+
|
|
17
|
+
# synap-pipecat
|
|
18
|
+
|
|
19
|
+
Synap integration for [Pipecat](https://docs.pipecat.ai) — preload long-term memory into voice pipelines and record every turn back to Synap.
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install synap-pipecat
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Requires `pipecat-ai>=0.0.80`, `maximem-synap>=0.2.0`.
|
|
28
|
+
|
|
29
|
+
## Quickstart
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from pipecat.pipeline.pipeline import Pipeline
|
|
33
|
+
from pipecat.processors.aggregators.llm_context import LLMContext
|
|
34
|
+
from pipecat.processors.aggregators.llm_response_universal import (
|
|
35
|
+
LLMContextAggregatorPair,
|
|
36
|
+
LLMUserAggregator,
|
|
37
|
+
LLMAssistantAggregator,
|
|
38
|
+
)
|
|
39
|
+
from maximem_synap import MaximemSynapSDK
|
|
40
|
+
from synap_pipecat import SynapMemoryProcessor, SynapRecorder
|
|
41
|
+
|
|
42
|
+
sdk = MaximemSynapSDK(api_key="sk-...")
|
|
43
|
+
context = LLMContext(messages=[{"role": "system", "content": "You are a helpful assistant."}])
|
|
44
|
+
|
|
45
|
+
user_agg = LLMUserAggregator(context=context)
|
|
46
|
+
asst_agg = LLMAssistantAggregator(context=context)
|
|
47
|
+
|
|
48
|
+
pipeline = Pipeline([
|
|
49
|
+
transport.input(),
|
|
50
|
+
stt,
|
|
51
|
+
SynapMemoryProcessor(sdk, user_id="alice", customer_id="acme"),
|
|
52
|
+
user_agg,
|
|
53
|
+
llm,
|
|
54
|
+
tts,
|
|
55
|
+
transport.output(),
|
|
56
|
+
asst_agg,
|
|
57
|
+
SynapRecorder(sdk, user_id="alice", customer_id="acme"),
|
|
58
|
+
])
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Scope
|
|
62
|
+
|
|
63
|
+
- **`SynapMemoryProcessor`** — sits BEFORE the user context aggregator. On each `TranscriptionFrame`, fetches relevant context from Synap via `sdk.fetch()` and appends it as a system message to the shared `LLMContext`. The transcription frame itself flows through unchanged so the user aggregator can record it as the user turn.
|
|
64
|
+
- **`SynapRecorder`** — sits AFTER the assistant context aggregator. Buffers the latest user transcription and the streamed assistant response; on `LLMFullResponseEndFrame`, calls `sdk.conversation.record_message()` for both turns.
|
|
65
|
+
|
|
66
|
+
## Error policy
|
|
67
|
+
|
|
68
|
+
- **Reads** (`SynapMemoryProcessor`) degrade gracefully — SDK failures log at `ERROR` and the frame continues downstream with no Synap context injected. A Synap blip must never break a live voice call.
|
|
69
|
+
- **Writes** (`SynapRecorder`) surface `SynapIntegrationError` via `wrap_sdk_errors_async`. The processor logs the failure and pushes an `ErrorFrame` upstream; the pipeline itself stays alive (errors propagate as frames, not exceptions).
|
|
70
|
+
- Non-matching frames pass through untouched in both processors — standard Pipecat `push_frame` contract.
|
|
71
|
+
|
|
72
|
+
## Constructor args (both processors)
|
|
73
|
+
|
|
74
|
+
- `sdk`: a configured `MaximemSynapSDK`
|
|
75
|
+
- `user_id`: required — Synap memory is user-scoped
|
|
76
|
+
- `customer_id`: optional customer/org scope; empty string means customer-less
|
|
77
|
+
- `conversation_id`: optional explicit conversation id (auto-generated per process lifetime when absent, shared across both processors if you pass the same instance)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# synap-pipecat
|
|
2
|
+
|
|
3
|
+
Synap integration for [Pipecat](https://docs.pipecat.ai) — preload long-term memory into voice pipelines and record every turn back to Synap.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install synap-pipecat
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Requires `pipecat-ai>=0.0.80`, `maximem-synap>=0.2.0`.
|
|
12
|
+
|
|
13
|
+
## Quickstart
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from pipecat.pipeline.pipeline import Pipeline
|
|
17
|
+
from pipecat.processors.aggregators.llm_context import LLMContext
|
|
18
|
+
from pipecat.processors.aggregators.llm_response_universal import (
|
|
19
|
+
LLMContextAggregatorPair,
|
|
20
|
+
LLMUserAggregator,
|
|
21
|
+
LLMAssistantAggregator,
|
|
22
|
+
)
|
|
23
|
+
from maximem_synap import MaximemSynapSDK
|
|
24
|
+
from synap_pipecat import SynapMemoryProcessor, SynapRecorder
|
|
25
|
+
|
|
26
|
+
sdk = MaximemSynapSDK(api_key="sk-...")
|
|
27
|
+
context = LLMContext(messages=[{"role": "system", "content": "You are a helpful assistant."}])
|
|
28
|
+
|
|
29
|
+
user_agg = LLMUserAggregator(context=context)
|
|
30
|
+
asst_agg = LLMAssistantAggregator(context=context)
|
|
31
|
+
|
|
32
|
+
pipeline = Pipeline([
|
|
33
|
+
transport.input(),
|
|
34
|
+
stt,
|
|
35
|
+
SynapMemoryProcessor(sdk, user_id="alice", customer_id="acme"),
|
|
36
|
+
user_agg,
|
|
37
|
+
llm,
|
|
38
|
+
tts,
|
|
39
|
+
transport.output(),
|
|
40
|
+
asst_agg,
|
|
41
|
+
SynapRecorder(sdk, user_id="alice", customer_id="acme"),
|
|
42
|
+
])
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Scope
|
|
46
|
+
|
|
47
|
+
- **`SynapMemoryProcessor`** — sits BEFORE the user context aggregator. On each `TranscriptionFrame`, fetches relevant context from Synap via `sdk.fetch()` and appends it as a system message to the shared `LLMContext`. The transcription frame itself flows through unchanged so the user aggregator can record it as the user turn.
|
|
48
|
+
- **`SynapRecorder`** — sits AFTER the assistant context aggregator. Buffers the latest user transcription and the streamed assistant response; on `LLMFullResponseEndFrame`, calls `sdk.conversation.record_message()` for both turns.
|
|
49
|
+
|
|
50
|
+
## Error policy
|
|
51
|
+
|
|
52
|
+
- **Reads** (`SynapMemoryProcessor`) degrade gracefully — SDK failures log at `ERROR` and the frame continues downstream with no Synap context injected. A Synap blip must never break a live voice call.
|
|
53
|
+
- **Writes** (`SynapRecorder`) surface `SynapIntegrationError` via `wrap_sdk_errors_async`. The processor logs the failure and pushes an `ErrorFrame` upstream; the pipeline itself stays alive (errors propagate as frames, not exceptions).
|
|
54
|
+
- Non-matching frames pass through untouched in both processors — standard Pipecat `push_frame` contract.
|
|
55
|
+
|
|
56
|
+
## Constructor args (both processors)
|
|
57
|
+
|
|
58
|
+
- `sdk`: a configured `MaximemSynapSDK`
|
|
59
|
+
- `user_id`: required — Synap memory is user-scoped
|
|
60
|
+
- `customer_id`: optional customer/org scope; empty string means customer-less
|
|
61
|
+
- `conversation_id`: optional explicit conversation id (auto-generated per process lifetime when absent, shared across both processors if you pass the same instance)
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: maximem-synap-pipecat
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Synap memory integration for Pipecat — voice-pipeline context injection + conversation recording
|
|
5
|
+
Author: Synap Team
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Keywords: synap,memory,pipecat,voice,ai,agents
|
|
8
|
+
Requires-Python: >=3.11
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
Requires-Dist: maximem-synap>=0.2.0
|
|
11
|
+
Requires-Dist: synap-integrations-common>=0.1.0
|
|
12
|
+
Requires-Dist: pipecat-ai>=0.0.80
|
|
13
|
+
Provides-Extra: dev
|
|
14
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
15
|
+
Requires-Dist: pytest-asyncio>=0.21; extra == "dev"
|
|
16
|
+
|
|
17
|
+
# synap-pipecat
|
|
18
|
+
|
|
19
|
+
Synap integration for [Pipecat](https://docs.pipecat.ai) — preload long-term memory into voice pipelines and record every turn back to Synap.
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install synap-pipecat
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Requires `pipecat-ai>=0.0.80`, `maximem-synap>=0.2.0`.
|
|
28
|
+
|
|
29
|
+
## Quickstart
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from pipecat.pipeline.pipeline import Pipeline
|
|
33
|
+
from pipecat.processors.aggregators.llm_context import LLMContext
|
|
34
|
+
from pipecat.processors.aggregators.llm_response_universal import (
|
|
35
|
+
LLMContextAggregatorPair,
|
|
36
|
+
LLMUserAggregator,
|
|
37
|
+
LLMAssistantAggregator,
|
|
38
|
+
)
|
|
39
|
+
from maximem_synap import MaximemSynapSDK
|
|
40
|
+
from synap_pipecat import SynapMemoryProcessor, SynapRecorder
|
|
41
|
+
|
|
42
|
+
sdk = MaximemSynapSDK(api_key="sk-...")
|
|
43
|
+
context = LLMContext(messages=[{"role": "system", "content": "You are a helpful assistant."}])
|
|
44
|
+
|
|
45
|
+
user_agg = LLMUserAggregator(context=context)
|
|
46
|
+
asst_agg = LLMAssistantAggregator(context=context)
|
|
47
|
+
|
|
48
|
+
pipeline = Pipeline([
|
|
49
|
+
transport.input(),
|
|
50
|
+
stt,
|
|
51
|
+
SynapMemoryProcessor(sdk, user_id="alice", customer_id="acme"),
|
|
52
|
+
user_agg,
|
|
53
|
+
llm,
|
|
54
|
+
tts,
|
|
55
|
+
transport.output(),
|
|
56
|
+
asst_agg,
|
|
57
|
+
SynapRecorder(sdk, user_id="alice", customer_id="acme"),
|
|
58
|
+
])
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Scope
|
|
62
|
+
|
|
63
|
+
- **`SynapMemoryProcessor`** — sits BEFORE the user context aggregator. On each `TranscriptionFrame`, fetches relevant context from Synap via `sdk.fetch()` and appends it as a system message to the shared `LLMContext`. The transcription frame itself flows through unchanged so the user aggregator can record it as the user turn.
|
|
64
|
+
- **`SynapRecorder`** — sits AFTER the assistant context aggregator. Buffers the latest user transcription and the streamed assistant response; on `LLMFullResponseEndFrame`, calls `sdk.conversation.record_message()` for both turns.
|
|
65
|
+
|
|
66
|
+
## Error policy
|
|
67
|
+
|
|
68
|
+
- **Reads** (`SynapMemoryProcessor`) degrade gracefully — SDK failures log at `ERROR` and the frame continues downstream with no Synap context injected. A Synap blip must never break a live voice call.
|
|
69
|
+
- **Writes** (`SynapRecorder`) surface `SynapIntegrationError` via `wrap_sdk_errors_async`. The processor logs the failure and pushes an `ErrorFrame` upstream; the pipeline itself stays alive (errors propagate as frames, not exceptions).
|
|
70
|
+
- Non-matching frames pass through untouched in both processors — standard Pipecat `push_frame` contract.
|
|
71
|
+
|
|
72
|
+
## Constructor args (both processors)
|
|
73
|
+
|
|
74
|
+
- `sdk`: a configured `MaximemSynapSDK`
|
|
75
|
+
- `user_id`: required — Synap memory is user-scoped
|
|
76
|
+
- `customer_id`: optional customer/org scope; empty string means customer-less
|
|
77
|
+
- `conversation_id`: optional explicit conversation id (auto-generated per process lifetime when absent, shared across both processors if you pass the same instance)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
maximem_synap_pipecat.egg-info/PKG-INFO
|
|
4
|
+
maximem_synap_pipecat.egg-info/SOURCES.txt
|
|
5
|
+
maximem_synap_pipecat.egg-info/dependency_links.txt
|
|
6
|
+
maximem_synap_pipecat.egg-info/requires.txt
|
|
7
|
+
maximem_synap_pipecat.egg-info/top_level.txt
|
|
8
|
+
synap_pipecat/__init__.py
|
|
9
|
+
synap_pipecat/processors.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
synap_pipecat
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "maximem-synap-pipecat"
|
|
7
|
+
version = "0.1.1"
|
|
8
|
+
description = "Synap memory integration for Pipecat — voice-pipeline context injection + conversation recording"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = "Apache-2.0"
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "Synap Team"}
|
|
14
|
+
]
|
|
15
|
+
keywords = ["synap", "memory", "pipecat", "voice", "ai", "agents"]
|
|
16
|
+
|
|
17
|
+
dependencies = [
|
|
18
|
+
"maximem-synap>=0.2.0",
|
|
19
|
+
"synap-integrations-common>=0.1.0",
|
|
20
|
+
"pipecat-ai>=0.0.80",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.optional-dependencies]
|
|
24
|
+
dev = [
|
|
25
|
+
"pytest>=7.0",
|
|
26
|
+
"pytest-asyncio>=0.21",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[tool.setuptools.packages.find]
|
|
30
|
+
where = ["."]
|
|
31
|
+
include = ["synap_pipecat*"]
|
|
32
|
+
|
|
33
|
+
[tool.pytest.ini_options]
|
|
34
|
+
testpaths = ["tests"]
|
|
35
|
+
asyncio_mode = "auto"
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Synap integration for Pipecat voice pipelines.
|
|
2
|
+
|
|
3
|
+
Pipecat's pipeline is a chain of :class:`FrameProcessor` nodes that exchange
|
|
4
|
+
``Frame`` objects. Long-term memory integrations plug in at two points:
|
|
5
|
+
|
|
6
|
+
1. **Before the user context aggregator** — inject relevant memory as an
|
|
7
|
+
extra system message so the LLM sees it before generating its reply.
|
|
8
|
+
Provided here as :class:`SynapMemoryProcessor`.
|
|
9
|
+
2. **After the assistant context aggregator** — capture the user's final
|
|
10
|
+
transcription and the assistant's streamed response so they can be
|
|
11
|
+
persisted to a long-term store. Provided here as :class:`SynapRecorder`.
|
|
12
|
+
|
|
13
|
+
Typical wiring::
|
|
14
|
+
|
|
15
|
+
from pipecat.pipeline.pipeline import Pipeline
|
|
16
|
+
from pipecat.processors.aggregators.llm_context import LLMContext
|
|
17
|
+
from pipecat.processors.aggregators.llm_response_universal import (
|
|
18
|
+
LLMUserAggregator, LLMAssistantAggregator,
|
|
19
|
+
)
|
|
20
|
+
from synap_pipecat import SynapMemoryProcessor, SynapRecorder
|
|
21
|
+
|
|
22
|
+
context = LLMContext(messages=[...])
|
|
23
|
+
user_agg = LLMUserAggregator(context=context)
|
|
24
|
+
asst_agg = LLMAssistantAggregator(context=context)
|
|
25
|
+
|
|
26
|
+
pipeline = Pipeline([
|
|
27
|
+
transport.input(), stt,
|
|
28
|
+
SynapMemoryProcessor(sdk, user_id="alice", customer_id="acme"),
|
|
29
|
+
user_agg, llm, tts, transport.output(), asst_agg,
|
|
30
|
+
SynapRecorder(sdk, user_id="alice", customer_id="acme"),
|
|
31
|
+
])
|
|
32
|
+
|
|
33
|
+
See :class:`SynapMemoryProcessor` and :class:`SynapRecorder` for the full
|
|
34
|
+
frame-by-frame contract and error policy (reads degrade silently — a
|
|
35
|
+
Synap blip must never break a live call; writes surface as
|
|
36
|
+
``SynapIntegrationError`` via ``ErrorFrame``).
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
from synap_pipecat.processors import SynapMemoryProcessor, SynapRecorder
|
|
40
|
+
|
|
41
|
+
__all__ = ["SynapMemoryProcessor", "SynapRecorder"]
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
"""Synap memory processor + recorder for Pipecat pipelines.
|
|
2
|
+
|
|
3
|
+
Two :class:`FrameProcessor` subclasses:
|
|
4
|
+
|
|
5
|
+
- :class:`SynapMemoryProcessor` — READ path. Inserted BEFORE the user
|
|
6
|
+
context aggregator. On each :class:`TranscriptionFrame`, fetches Synap
|
|
7
|
+
context via ``sdk.fetch`` and inlines it as a system message into the
|
|
8
|
+
shared ``LLMContext`` so the LLM sees it before replying. The frame
|
|
9
|
+
itself is passed through unchanged (the user aggregator still needs to
|
|
10
|
+
see it to record the user turn).
|
|
11
|
+
|
|
12
|
+
- :class:`SynapRecorder` — WRITE path. Inserted AFTER the assistant
|
|
13
|
+
context aggregator. Buffers the most recent user transcription and the
|
|
14
|
+
streamed assistant text; on :class:`LLMFullResponseEndFrame` it records
|
|
15
|
+
both turns via ``sdk.conversation.record_message``.
|
|
16
|
+
|
|
17
|
+
Error policy (see README):
|
|
18
|
+
- Read failures degrade gracefully — log at ERROR and skip context
|
|
19
|
+
injection. A Synap blip must NEVER break a live voice call.
|
|
20
|
+
- Write failures surface as :class:`SynapIntegrationError` via
|
|
21
|
+
``wrap_sdk_errors_async``. The processor pushes an ``ErrorFrame``
|
|
22
|
+
upstream so observability hooks fire, but swallows the exception
|
|
23
|
+
locally — Pipecat's own error contract is frames-not-raises.
|
|
24
|
+
- Non-matching frames pass through untouched.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import logging
|
|
30
|
+
import uuid
|
|
31
|
+
from typing import List, Optional
|
|
32
|
+
|
|
33
|
+
from maximem_synap import MaximemSynapSDK
|
|
34
|
+
|
|
35
|
+
from pipecat.frames.frames import (
|
|
36
|
+
ErrorFrame,
|
|
37
|
+
Frame,
|
|
38
|
+
LLMFullResponseEndFrame,
|
|
39
|
+
LLMTextFrame,
|
|
40
|
+
TranscriptionFrame,
|
|
41
|
+
)
|
|
42
|
+
from pipecat.processors.aggregators.llm_context import LLMContext
|
|
43
|
+
from pipecat.processors.frame_processor import FrameDirection, FrameProcessor
|
|
44
|
+
|
|
45
|
+
from synap_integrations_common import (
|
|
46
|
+
SynapIntegrationError,
|
|
47
|
+
wrap_sdk_errors_async,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
logger = logging.getLogger(__name__)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
_SYSTEM_MEMORY_PREAMBLE = (
|
|
54
|
+
"Relevant long-term memory about this user (from Synap):\n{body}\n"
|
|
55
|
+
"Use this context only when it helps answer the user's current turn."
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class SynapMemoryProcessor(FrameProcessor):
|
|
60
|
+
"""Injects Synap context into an ``LLMContext`` on each transcription.
|
|
61
|
+
|
|
62
|
+
Placement: BEFORE the user context aggregator in the pipeline. On each
|
|
63
|
+
:class:`TranscriptionFrame`, fetches user-scoped context from Synap
|
|
64
|
+
and appends it as a system message to the shared ``LLMContext``
|
|
65
|
+
before forwarding the frame downstream. If no ``LLMContext`` is
|
|
66
|
+
supplied at construction, the processor becomes a read-only tap —
|
|
67
|
+
frames still pass through, but no context is injected (useful if you
|
|
68
|
+
want to plug in a recorder without changing the prompt).
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
sdk: Configured :class:`MaximemSynapSDK`.
|
|
72
|
+
user_id: Required — Synap memory is user-scoped.
|
|
73
|
+
customer_id: Optional customer/org scope. Empty string means
|
|
74
|
+
customer-less.
|
|
75
|
+
context: Shared :class:`LLMContext` used by the user/assistant
|
|
76
|
+
aggregators. When ``None``, the processor is inert (no
|
|
77
|
+
injection) but still passes frames through.
|
|
78
|
+
mode: Synap fetch mode (``"accurate"`` or ``"fast"``).
|
|
79
|
+
max_results: Cap on ``sdk.fetch`` per turn.
|
|
80
|
+
include_conversation_context: Whether to request Synap's recent
|
|
81
|
+
conversation block alongside the semantic memory.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
def __init__(
|
|
85
|
+
self,
|
|
86
|
+
sdk: MaximemSynapSDK,
|
|
87
|
+
*,
|
|
88
|
+
user_id: str,
|
|
89
|
+
customer_id: str = "",
|
|
90
|
+
context: Optional[LLMContext] = None,
|
|
91
|
+
mode: str = "accurate",
|
|
92
|
+
max_results: int = 10,
|
|
93
|
+
include_conversation_context: bool = False,
|
|
94
|
+
name: Optional[str] = None,
|
|
95
|
+
**kwargs,
|
|
96
|
+
) -> None:
|
|
97
|
+
if sdk is None:
|
|
98
|
+
raise ValueError("SynapMemoryProcessor requires a non-None sdk")
|
|
99
|
+
if not user_id:
|
|
100
|
+
raise ValueError("SynapMemoryProcessor requires a non-empty user_id")
|
|
101
|
+
super().__init__(name=name, **kwargs)
|
|
102
|
+
self.sdk = sdk
|
|
103
|
+
self.user_id = user_id
|
|
104
|
+
self.customer_id = customer_id
|
|
105
|
+
self._context = context
|
|
106
|
+
self.mode = mode
|
|
107
|
+
self.max_results = max_results
|
|
108
|
+
self.include_conversation_context = include_conversation_context
|
|
109
|
+
|
|
110
|
+
def set_context(self, context: LLMContext) -> None:
|
|
111
|
+
"""Attach or swap the shared :class:`LLMContext` at runtime."""
|
|
112
|
+
self._context = context
|
|
113
|
+
|
|
114
|
+
async def process_frame(self, frame: Frame, direction: FrameDirection) -> None:
|
|
115
|
+
await super().process_frame(frame, direction)
|
|
116
|
+
|
|
117
|
+
if isinstance(frame, TranscriptionFrame) and frame.text:
|
|
118
|
+
await self._inject_context(frame.text)
|
|
119
|
+
|
|
120
|
+
await self.push_frame(frame, direction)
|
|
121
|
+
|
|
122
|
+
async def _inject_context(self, query: str) -> None:
|
|
123
|
+
if self._context is None:
|
|
124
|
+
return
|
|
125
|
+
|
|
126
|
+
try:
|
|
127
|
+
response = await self.sdk.fetch(
|
|
128
|
+
user_id=self.user_id,
|
|
129
|
+
customer_id=self.customer_id or None,
|
|
130
|
+
search_query=[query],
|
|
131
|
+
max_results=self.max_results,
|
|
132
|
+
mode=self.mode,
|
|
133
|
+
include_conversation_context=self.include_conversation_context,
|
|
134
|
+
)
|
|
135
|
+
except Exception as exc: # noqa: BLE001 — read-side graceful degrade
|
|
136
|
+
logger.error(
|
|
137
|
+
"SynapMemoryProcessor: sdk.fetch failed user_id=%s error=%s",
|
|
138
|
+
self.user_id, exc, exc_info=True,
|
|
139
|
+
)
|
|
140
|
+
return
|
|
141
|
+
|
|
142
|
+
formatted = getattr(response, "formatted_context", None) or ""
|
|
143
|
+
if not formatted.strip():
|
|
144
|
+
return # nothing to inject — pass through silently
|
|
145
|
+
|
|
146
|
+
self._context.add_message({
|
|
147
|
+
"role": "system",
|
|
148
|
+
"content": _SYSTEM_MEMORY_PREAMBLE.format(body=formatted.strip()),
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class SynapRecorder(FrameProcessor):
|
|
153
|
+
"""Records user + assistant turns to Synap at end-of-response.
|
|
154
|
+
|
|
155
|
+
Placement: AFTER the assistant context aggregator in the pipeline.
|
|
156
|
+
Buffers the most recent user transcription (from
|
|
157
|
+
:class:`TranscriptionFrame`) and the streamed assistant tokens (from
|
|
158
|
+
:class:`LLMTextFrame`); on :class:`LLMFullResponseEndFrame`, flushes
|
|
159
|
+
both turns via ``sdk.conversation.record_message`` and resets the
|
|
160
|
+
buffer.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
sdk: Configured :class:`MaximemSynapSDK`.
|
|
164
|
+
user_id: Required — Synap conversations are user-scoped.
|
|
165
|
+
customer_id: Optional customer/org scope. Empty string means
|
|
166
|
+
customer-less.
|
|
167
|
+
conversation_id: Stable id for this call. Auto-generated per
|
|
168
|
+
processor lifetime when absent.
|
|
169
|
+
"""
|
|
170
|
+
|
|
171
|
+
def __init__(
|
|
172
|
+
self,
|
|
173
|
+
sdk: MaximemSynapSDK,
|
|
174
|
+
*,
|
|
175
|
+
user_id: str,
|
|
176
|
+
customer_id: str = "",
|
|
177
|
+
conversation_id: Optional[str] = None,
|
|
178
|
+
name: Optional[str] = None,
|
|
179
|
+
**kwargs,
|
|
180
|
+
) -> None:
|
|
181
|
+
if sdk is None:
|
|
182
|
+
raise ValueError("SynapRecorder requires a non-None sdk")
|
|
183
|
+
if not user_id:
|
|
184
|
+
raise ValueError("SynapRecorder requires a non-empty user_id")
|
|
185
|
+
super().__init__(name=name, **kwargs)
|
|
186
|
+
self.sdk = sdk
|
|
187
|
+
self.user_id = user_id
|
|
188
|
+
self.customer_id = customer_id
|
|
189
|
+
self.conversation_id = conversation_id or f"pipecat-{uuid.uuid4().hex[:12]}"
|
|
190
|
+
|
|
191
|
+
self._user_buffer: Optional[str] = None
|
|
192
|
+
self._assistant_parts: List[str] = []
|
|
193
|
+
|
|
194
|
+
async def process_frame(self, frame: Frame, direction: FrameDirection) -> None:
|
|
195
|
+
await super().process_frame(frame, direction)
|
|
196
|
+
|
|
197
|
+
if isinstance(frame, TranscriptionFrame) and frame.text:
|
|
198
|
+
# Latest transcription wins — voice pipelines can emit several
|
|
199
|
+
# refinements before the LLM responds; only the last is the
|
|
200
|
+
# committed user turn.
|
|
201
|
+
self._user_buffer = frame.text
|
|
202
|
+
elif isinstance(frame, LLMTextFrame) and frame.text:
|
|
203
|
+
self._assistant_parts.append(frame.text)
|
|
204
|
+
elif isinstance(frame, LLMFullResponseEndFrame):
|
|
205
|
+
await self._flush()
|
|
206
|
+
|
|
207
|
+
await self.push_frame(frame, direction)
|
|
208
|
+
|
|
209
|
+
async def _flush(self) -> None:
|
|
210
|
+
user_text = self._user_buffer
|
|
211
|
+
assistant_text = "".join(self._assistant_parts).strip()
|
|
212
|
+
# Reset buffers before we await — the next turn may start flowing
|
|
213
|
+
# while we're still writing to Synap.
|
|
214
|
+
self._user_buffer = None
|
|
215
|
+
self._assistant_parts = []
|
|
216
|
+
|
|
217
|
+
if not user_text and not assistant_text:
|
|
218
|
+
return
|
|
219
|
+
|
|
220
|
+
try:
|
|
221
|
+
async with wrap_sdk_errors_async(
|
|
222
|
+
"pipecat.record_turn",
|
|
223
|
+
logger,
|
|
224
|
+
conversation_id=self.conversation_id,
|
|
225
|
+
user_id=self.user_id,
|
|
226
|
+
):
|
|
227
|
+
if user_text:
|
|
228
|
+
await self.sdk.conversation.record_message(
|
|
229
|
+
conversation_id=self.conversation_id,
|
|
230
|
+
role="user",
|
|
231
|
+
content=user_text,
|
|
232
|
+
user_id=self.user_id,
|
|
233
|
+
customer_id=self.customer_id,
|
|
234
|
+
)
|
|
235
|
+
if assistant_text:
|
|
236
|
+
await self.sdk.conversation.record_message(
|
|
237
|
+
conversation_id=self.conversation_id,
|
|
238
|
+
role="assistant",
|
|
239
|
+
content=assistant_text,
|
|
240
|
+
user_id=self.user_id,
|
|
241
|
+
customer_id=self.customer_id,
|
|
242
|
+
)
|
|
243
|
+
except SynapIntegrationError as exc:
|
|
244
|
+
# Write failures surface as ErrorFrame (Pipecat's contract —
|
|
245
|
+
# exceptions inside process_frame would tear down the whole
|
|
246
|
+
# pipeline). The wrap_sdk_errors_async call has already
|
|
247
|
+
# logged the underlying cause.
|
|
248
|
+
await self.push_frame(
|
|
249
|
+
ErrorFrame(error=str(exc), exception=exc),
|
|
250
|
+
FrameDirection.UPSTREAM,
|
|
251
|
+
)
|