Skip to content

Commit fbe475f

Browse files
committed
feat(pipecat): add voice agent integration with persistent memory
Adds a first-class Pipecat integration so developers can drop Vektori long-term memory into any real-time voice pipeline in ~10 lines. vektori/integrations/pipecat/processor.py - VektoriMemoryProcessor: intercepts OpenAILLMContextFrame before the LLM, searches Vektori (facts + episodes), injects a [Memory context] block into the system prompt each turn - VektoriStorageProcessor: buffers user transcription + streamed LLM reply, stores the completed turn in Vektori on LLMFullResponseEndFrame vektori/integrations/pipecat/__init__.py - clean public API with helpful ImportError if pipecat-ai not installed vektori/integrations/__init__.py - namespace package for future integrations examples/pipecat_voice_agent.py - full FastAPI WebSocket server using Deepgram STT, GPT-4o-mini, ElevenLabs TTS, Silero VAD, and both Vektori processors - configurable via env vars; documented alternative STT/TTS providers pyproject.toml - new [pipecat] optional dependency group: pipecat-ai, fastapi, uvicorn
1 parent 8a2974b commit fbe475f

5 files changed

Lines changed: 632 additions & 0 deletions

File tree

examples/pipecat_voice_agent.py

Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
1+
"""
2+
Vektori + Pipecat — Voice Agent with Persistent Memory
3+
=======================================================
4+
5+
A production-ready FastAPI WebSocket server that wires a real-time voice
6+
pipeline with Vektori long-term memory.
7+
8+
Each spoken turn:
9+
1. Deepgram transcribes the audio stream to text.
10+
2. VektoriMemoryProcessor searches the user's memory and injects relevant
11+
facts / episodes into the LLM system prompt (before inference).
12+
3. GPT-4o-mini replies.
13+
4. VektoriStorageProcessor stores the exchange for future recall.
14+
5. ElevenLabs synthesises the reply to speech.
15+
16+
Quick-start
17+
-----------
18+
pip install "vektori[pipecat]"
19+
pip install "pipecat-ai[deepgram,elevenlabs,openai,silero]"
20+
21+
export OPENAI_API_KEY=...
22+
export DEEPGRAM_API_KEY=...
23+
export ELEVENLABS_API_KEY=...
24+
export ELEVENLABS_VOICE_ID=... # e.g. 21m00Tcm4TlvDq8ikWAM (Rachel)
25+
26+
uvicorn examples.pipecat_voice_agent:app --host 0.0.0.0 --port 8765
27+
28+
Connect from any Pipecat-compatible WebSocket client (browser, iOS, Android)
29+
pointing at ws://localhost:8765/ws/{user_id}.
30+
31+
Alternative STT / TTS
32+
----------------------
33+
Swap ``DeepgramSTTService`` for ``WhisperSTTService`` (local) or
34+
``AssemblyAISTTService``.
35+
36+
Swap ``ElevenLabsTTSService`` for ``CartesiaTTSService``,
37+
``PlayHTTTSService``, or ``OpenAITTSService``.
38+
39+
Everything else — Vektori wiring, pipeline shape, session management —
40+
stays the same.
41+
42+
Architecture notes
43+
------------------
44+
* ``VektoriMemoryProcessor`` sits between the user context aggregator and the
45+
LLM. It intercepts ``OpenAILLMContextFrame``, calls ``vektori.search()``,
46+
and rewrites the system message in-place before the LLM sees it.
47+
48+
* ``VektoriStorageProcessor`` sits after the LLM. It buffers the user
49+
transcription and the streamed LLM reply, then calls ``vektori.add()``
50+
once per completed turn (non-blocking via ``asyncio.ensure_future``).
51+
52+
* One ``Vektori`` instance per WebSocket connection — each connection gets its
53+
own async resources and is closed when the client disconnects.
54+
55+
* ``session_id`` encodes the connection timestamp so consecutive sessions for
56+
the same user are kept distinct in Vektori's L2 sentence graph.
57+
"""
58+
59+
import asyncio
60+
import logging
61+
import os
62+
import time
63+
from contextlib import asynccontextmanager
64+
65+
from fastapi import FastAPI, WebSocket
66+
from fastapi.middleware.cors import CORSMiddleware
67+
from pipecat.audio.vad.silero import SileroVADAnalyzer
68+
from pipecat.frames.frames import EndFrame
69+
from pipecat.pipeline.pipeline import Pipeline
70+
from pipecat.pipeline.runner import PipelineRunner
71+
from pipecat.pipeline.task import PipelineParams, PipelineTask
72+
from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext
73+
from pipecat.services.deepgram.stt import DeepgramSTTService
74+
from pipecat.services.elevenlabs.tts import ElevenLabsTTSService
75+
from pipecat.services.openai.llm import OpenAILLMService
76+
from pipecat.transports.network.fastapi_websocket import (
77+
FastAPIWebsocketParams,
78+
FastAPIWebsocketTransport,
79+
)
80+
81+
from vektori import Vektori
82+
from vektori.integrations.pipecat import VektoriMemoryProcessor, VektoriStorageProcessor
83+
84+
# ---------------------------------------------------------------------------
85+
# Logging
86+
# ---------------------------------------------------------------------------
87+
88+
logging.basicConfig(level=logging.INFO)
89+
logger = logging.getLogger(__name__)
90+
91+
# ---------------------------------------------------------------------------
92+
# Configuration — read from environment
93+
# ---------------------------------------------------------------------------
94+
95+
OPENAI_API_KEY = os.environ["OPENAI_API_KEY"]
96+
OPENAI_MODEL = os.getenv("OPENAI_MODEL", "gpt-4o-mini")
97+
98+
DEEPGRAM_API_KEY = os.environ["DEEPGRAM_API_KEY"]
99+
100+
ELEVENLABS_API_KEY = os.environ["ELEVENLABS_API_KEY"]
101+
ELEVENLABS_VOICE_ID = os.getenv("ELEVENLABS_VOICE_ID", "21m00Tcm4TlvDq8ikWAM")
102+
103+
# Vektori — default uses local SQLite + OpenAI embeddings
104+
VEKTORI_EMBEDDING_MODEL = os.getenv("VEKTORI_EMBEDDING_MODEL", "openai:text-embedding-3-small")
105+
VEKTORI_EXTRACTION_MODEL = os.getenv("VEKTORI_EXTRACTION_MODEL", f"openai:{OPENAI_MODEL}")
106+
107+
# Memory retrieval tuning for voice
108+
# l1 = facts + episodes + source sentences — the right balance for voice
109+
# top_k = 5 keeps the injected context short so TTS stays snappy
110+
VEKTORI_DEPTH = os.getenv("VEKTORI_DEPTH", "l1")
111+
VEKTORI_TOP_K = int(os.getenv("VEKTORI_TOP_K", "5"))
112+
113+
SYSTEM_PROMPT = """\
114+
You are a helpful, friendly voice assistant. Keep your answers concise and
115+
conversational — you are speaking out loud, not writing. Avoid bullet lists,
116+
code blocks, or markdown; plain prose only.
117+
118+
If the memory context below contains relevant facts about the user, use them
119+
naturally in your reply without explicitly quoting them.\
120+
"""
121+
122+
# ---------------------------------------------------------------------------
123+
# App lifecycle
124+
# ---------------------------------------------------------------------------
125+
126+
127+
@asynccontextmanager
128+
async def lifespan(app: FastAPI):
129+
logger.info("Voice agent server starting up")
130+
yield
131+
logger.info("Voice agent server shut down")
132+
133+
134+
app = FastAPI(title="Vektori Voice Agent", lifespan=lifespan)
135+
136+
app.add_middleware(
137+
CORSMiddleware,
138+
allow_origins=["*"],
139+
allow_methods=["*"],
140+
allow_headers=["*"],
141+
)
142+
143+
# ---------------------------------------------------------------------------
144+
# WebSocket endpoint — one Pipecat pipeline per connection
145+
# ---------------------------------------------------------------------------
146+
147+
148+
@app.websocket("/ws/{user_id}")
149+
async def voice_endpoint(websocket: WebSocket, user_id: str):
150+
"""
151+
Connect a Pipecat-compatible WebSocket client.
152+
153+
URL: ws://host:port/ws/{user_id}
154+
155+
Each connection spawns an independent pipeline. The ``user_id`` path
156+
parameter maps directly to Vektori's user namespace — all memories stored
157+
in this session are searchable in future sessions for the same user.
158+
"""
159+
session_id = f"pipecat-{user_id}-{int(time.time())}"
160+
logger.info("New voice session: user=%s session=%s", user_id, session_id)
161+
162+
# ------------------------------------------------------------------
163+
# Transport (WebSocket + VAD)
164+
# ------------------------------------------------------------------
165+
transport = FastAPIWebsocketTransport(
166+
websocket=websocket,
167+
params=FastAPIWebsocketParams(
168+
audio_in_enabled=True,
169+
audio_out_enabled=True,
170+
vad_enabled=True,
171+
vad_analyzer=SileroVADAnalyzer(),
172+
vad_audio_passthrough=True,
173+
),
174+
)
175+
176+
# ------------------------------------------------------------------
177+
# AI services
178+
# ------------------------------------------------------------------
179+
stt = DeepgramSTTService(api_key=DEEPGRAM_API_KEY)
180+
llm = OpenAILLMService(api_key=OPENAI_API_KEY, model=OPENAI_MODEL)
181+
tts = ElevenLabsTTSService(
182+
api_key=ELEVENLABS_API_KEY,
183+
voice_id=ELEVENLABS_VOICE_ID,
184+
)
185+
186+
# ------------------------------------------------------------------
187+
# LLM context (shared object — both processors hold a reference)
188+
# ------------------------------------------------------------------
189+
context = OpenAILLMContext(
190+
messages=[{"role": "system", "content": SYSTEM_PROMPT}]
191+
)
192+
ctx_agg = llm.create_context_aggregator(context)
193+
194+
# ------------------------------------------------------------------
195+
# Vektori memory
196+
# ------------------------------------------------------------------
197+
vektori = Vektori(
198+
embedding_model=VEKTORI_EMBEDDING_MODEL,
199+
extraction_model=VEKTORI_EXTRACTION_MODEL,
200+
)
201+
202+
vektori_memory = VektoriMemoryProcessor(
203+
vektori=vektori,
204+
user_id=user_id,
205+
base_system_prompt=SYSTEM_PROMPT,
206+
depth=VEKTORI_DEPTH,
207+
top_k=VEKTORI_TOP_K,
208+
session_id=session_id,
209+
)
210+
211+
vektori_storage = VektoriStorageProcessor(
212+
vektori=vektori,
213+
user_id=user_id,
214+
session_id=session_id,
215+
context=context, # shared reference for fallback reads
216+
)
217+
218+
# ------------------------------------------------------------------
219+
# Pipeline
220+
# ------------------------------------------------------------------
221+
pipeline = Pipeline(
222+
[
223+
transport.input(), # audio in (WebSocket frames → AudioRawFrame)
224+
stt, # Deepgram → TranscriptionFrame
225+
ctx_agg.user(), # TranscriptionFrame → OpenAILLMContextFrame
226+
vektori_memory, # inject memory into system prompt
227+
llm, # OpenAILLMContextFrame → TextFrame stream
228+
vektori_storage, # buffer user + assistant text, store on end
229+
tts, # TextFrame → AudioRawFrame
230+
transport.output(), # audio out (AudioRawFrame → WebSocket frames)
231+
ctx_agg.assistant(), # accumulate assistant reply into context
232+
]
233+
)
234+
235+
task = PipelineTask(
236+
pipeline,
237+
params=PipelineParams(allow_interruptions=True),
238+
)
239+
240+
# ------------------------------------------------------------------
241+
# Lifecycle handlers
242+
# ------------------------------------------------------------------
243+
244+
@transport.event_handler("on_client_connected")
245+
async def on_connected(t, client):
246+
logger.info("Client connected: user=%s", user_id)
247+
248+
@transport.event_handler("on_client_disconnected")
249+
async def on_disconnected(t, client):
250+
logger.info("Client disconnected: user=%s — closing session", user_id)
251+
await task.queue_frames([EndFrame()])
252+
await vektori.close()
253+
254+
# ------------------------------------------------------------------
255+
# Run
256+
# ------------------------------------------------------------------
257+
runner = PipelineRunner()
258+
await runner.run(task)
259+
260+
261+
# ---------------------------------------------------------------------------
262+
# Healthcheck
263+
# ---------------------------------------------------------------------------
264+
265+
266+
@app.get("/health")
267+
async def health():
268+
return {"status": "ok"}
269+
270+
271+
# ---------------------------------------------------------------------------
272+
# Entry-point for direct execution
273+
# ---------------------------------------------------------------------------
274+
275+
if __name__ == "__main__":
276+
import uvicorn
277+
278+
uvicorn.run(app, host="0.0.0.0", port=8765)

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ anthropic = ["anthropic>=0.20", "voyageai>=0.2"]
4141
sentence-transformers = ["sentence-transformers>=2.6"]
4242
bge = ["FlagEmbedding>=1.2"]
4343
litellm = ["litellm>=1.30"]
44+
pipecat = ["pipecat-ai>=0.0.50", "fastapi>=0.110", "uvicorn[standard]>=0.29"]
4445
dev = [
4546
"pytest>=8.0",
4647
"pytest-asyncio>=0.23",

vektori/integrations/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# vektori/integrations — optional third-party framework integrations
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
"""
2+
Vektori × Pipecat integration.
3+
4+
Install the optional dependency first:
5+
pip install "vektori[pipecat]"
6+
7+
Usage
8+
-----
9+
from vektori.integrations.pipecat import VektoriMemoryProcessor, VektoriStorageProcessor
10+
"""
11+
12+
try:
13+
from .processor import VektoriMemoryProcessor, VektoriStorageProcessor
14+
15+
__all__ = ["VektoriMemoryProcessor", "VektoriStorageProcessor"]
16+
except ImportError as exc: # pipecat not installed
17+
raise ImportError(
18+
"Pipecat is not installed. Run: pip install 'vektori[pipecat]'"
19+
) from exc

0 commit comments

Comments
 (0)