Skip to content

Commit 3d6afcc

Browse files
feat(ten-moss): Moss ↔ TEN Framework integration — PR 2: voice-assistant app (#399)
## PR 2 of 2 — `apps/ten-moss/`: TEN voice assistant with Moss session grounding The runnable half of the Moss ↔ [TEN Framework](https://github.com/ten-framework/ten-framework) integration. A real-time voice agent that, on each final ASR transcript, asks a **Moss session** for session-scoped grounding (~1–10ms, in-process) and injects it into the LLM prompt before the model responds. > **Depends on #396 — merge that first.** This branch's `main_python` imports > `ten_moss` (`MossSessionManager`), which ships in #396. On `main` (both merged) > the import resolves; in isolation this PR won't import `ten_moss`. ### The Moss delta (the part to review) The `tenapp/` baseline is vendored verbatim from TEN's `voice-assistant` example; the Moss integration is a small, localized delta in the `main_python` control extension: - **`config.py`** — `MainControlConfig(MossSessionConfig)` picks up the `moss_*` properties. - **`extension.py` `on_init`** — opens the session, best-effort: ```python if self.config.enable_moss and self.config.moss_index_name: self.moss = MossSessionManager.from_config(self.config) await self.moss.open() ``` - **`extension.py` `_on_asr_result`** (final turn) — grounds the turn (guarded so a grounding failure can't drop the turn): ```python ctx = await self.moss.query_context(event.text) llm_input = f"{ctx}\n\n[Current User Question]\n{event.text}" if ctx else event.text await self.agent.queue_llm_input(llm_input) ``` - **`manifest.json`** declares the `moss_*` properties; **`requirements.txt`** documents the `ten-moss` install (not a hard PyPI pin, since it isn't published yet). - **`tenapp/property.json`** sets `moss_*` on the `main_control` node (env-substituted). Plus a sample corpus (`data/knowledge.jsonl`) and `create_index.py` to build the demo index. ### Graph `agora_rtc → streamid_adapter → stt(deepgram) → main_control(main_python) → llm(openai) → tts(elevenlabs) → agora_rtc`, with `message_collector2` for transcripts. Identical to TEN's `voice-assistant` graph minus the weather tool. ### Provenance / license `tenapp/` (graph, `main_python`, agent runtime, scripts) is vendored from ten-framework at commit [`c385d27`](https://github.com/ten-framework/ten-framework/tree/c385d2724a1f3e6ac4ee0b81fcc7dada8346c0e0/ai_agents/agents/examples/voice-assistant) under **Apache-2.0** (headers preserved). Only the Moss delta above — plus two small correctness patches (`decorators.py` annotation, defensive `session_id` parse) — is applied on top. The repo-level TEN monorepo harness (Taskfile/Dockerfile/playground/server) is intentionally **not** vendored; this example is designed to run inside a TEN Framework checkout (it references shared `ten_packages` via `../../../`). See the app README. ### Validation Static checks all pass: JSON parse (4 files), JSONL corpus (10 docs), `py_compile` (all 11 modules incl. the vendored runtime), import graph resolves, `create_index.load_documents()` returns 10 docs. **Not** run end-to-end in CI — it needs the TEN toolchain (tman/Docker) plus Agora/Deepgram/OpenAI/ElevenLabs keys. Documented for manual runs in the app README. The `ten-moss` logic itself is covered by the offline unit tests in #396. ### Running (summary) ```bash cp .env.example .env # Moss + provider keys python create_index.py # build the demo index (from this dir; needs only the Moss SDK) ``` Then drop `tenapp/` into a TEN Framework checkout and run it with TEN's own tooling (after `uv pip install -e /path/to/moss/packages/ten-moss`). Full steps in the app README.
1 parent 0f7ad91 commit 3d6afcc

32 files changed

Lines changed: 2182 additions & 0 deletions

AGENTS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ apps/
5050
pipecat-quickstart/ — Cloud-deployable quickstart bot
5151
ollama-local/ — Local LLM + Moss + Pipecat via docker compose
5252
hume-ollama-local/ — Local LLM + Hume AI TTS + Moss + Pipecat
53+
ten-moss/ — TEN Framework voice agent with Moss session-scoped grounding
5354
vapi-moss/ — VAPI Custom Tool webhook server
5455
packages/
5556
agora-moss/ — Agora Conversational AI MCP server package
@@ -115,6 +116,7 @@ asks for an experimental landing spot.
115116
| `pipecat-moss/pipecat-quickstart/` | Pipecat Cloud | Minimal Pipecat bot — local dev → Pipecat Cloud deployment |
116117
| `pipecat-moss/ollama-local/` | Pipecat + Ollama | Full-stack local voice AI: Ollama LLM + Moss RAG + Pipecat audio, one `docker compose up` |
117118
| `pipecat-moss/hume-ollama-local/` | Pipecat + Ollama + Hume | Same as above with Hume AI (Octave) expressive TTS |
119+
| `ten-moss/` | TEN Framework | Voice agent that grounds each turn in a Moss session (`MossSessionManager`); TEN `voice-assistant` example + the Moss delta |
118120
| `vapi-moss/` | VAPI | Webhook server connecting VAPI Custom Tool calls to Moss search; LLM-directed retrieval |
119121

120122
### Other Apps

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,7 @@ Full API reference: [docs.moss.dev](https://docs.moss.dev).
239239
| [Vapi](https://vapi.ai) | Available | [`apps/vapi-moss/`](apps/vapi-moss/) |
240240
| [ElevenLabs](https://elevenlabs.io) | Available | [`apps/elevenlabs-moss/`](apps/elevenlabs-moss/) |
241241
| [Agora](https://www.agora.io/) | Available | [`apps/agora-moss/`](apps/agora-moss/) |
242+
| [TEN Framework](https://github.com/ten-framework/ten-framework) | Available | [`apps/ten-moss/`](apps/ten-moss/) |
242243
| [Strands Agents](https://github.com/strands-agents/sdk-python) | Available | [`packages/strands-agents-moss/`](packages/strands-agents-moss/) |
243244
| [Langflow](https://github.com/langflow-ai/langflow) | Available | [`examples/cookbook/langflow/`](examples/cookbook/langflow/) |
244245
| [Next.js](https://nextjs.org) | Available | [`apps/next-js/`](apps/next-js/) |

apps/ten-moss/.env.example

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# --- Moss (session-scoped grounding) ---
2+
MOSS_PROJECT_ID=your_moss_project_id
3+
MOSS_PROJECT_KEY=your_moss_project_key
4+
MOSS_INDEX_NAME=ten-moss-demo
5+
6+
# --- Agora (real-time audio transport) ---
7+
AGORA_APP_ID=your_agora_app_id
8+
AGORA_APP_CERTIFICATE=
9+
10+
# --- Deepgram (speech-to-text) ---
11+
DEEPGRAM_API_KEY=your_deepgram_api_key
12+
13+
# --- OpenAI (LLM) ---
14+
OPENAI_API_KEY=your_openai_api_key
15+
OPENAI_MODEL=gpt-4o-mini
16+
17+
# --- ElevenLabs (text-to-speech) ---
18+
ELEVENLABS_TTS_KEY=your_elevenlabs_key

apps/ten-moss/.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
__pycache__/
2+
*.py[cod]
3+
.env
4+
.ten/

apps/ten-moss/BENCHMARK.md

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# TEN default retrieval (memU) vs Moss — per-turn latency
2+
3+
Same framework (TEN), same voice pipeline (Agora → Deepgram → LLM → ElevenLabs),
4+
same questions. The only thing that changes is **where retrieval happens**:
5+
6+
| Agent | Retrieval backend | Where it runs |
7+
| --- | --- | --- |
8+
| `voice-assistant-with-memU` (TEN's shipped example) | memU | **remote** — HTTPS to `api.memu.so` every turn |
9+
| `voice-assistant-with-moss` (this repo) | Moss | **in-process** — local session, no network hop |
10+
11+
memU is TEN's flagship memory example; OceanBase PowerRAG and EverMemOS (TEN's other
12+
options) are remote services too. So this is representative of TEN's default: retrieval
13+
is a network round trip. Moss runs where the agent runs.
14+
15+
Both agents log the same line each turn, so you can read the difference directly:
16+
17+
```
18+
[retrieval-latency] backend=memU(cloud) took 380 ms this turn
19+
[retrieval-latency] backend=moss(in-process) took 2 ms this turn
20+
```
21+
22+
## 1. Instrument TEN's memU example (one small edit)
23+
24+
In your TEN checkout, open
25+
`ai_agents/agents/examples/voice-assistant-with-memU/tenapp/ten_packages/extension/main_python/extension.py`
26+
and, inside `_on_asr_result`, wrap the retrieval call:
27+
28+
```python
29+
# before
30+
related_memory = await self._retrieve_related_memory(event.text)
31+
32+
# after
33+
import time
34+
_t0 = time.perf_counter()
35+
related_memory = await self._retrieve_related_memory(event.text)
36+
self.ten_env.log_info(
37+
f"[retrieval-latency] backend=memU(cloud) took "
38+
f"{(time.perf_counter() - _t0) * 1000:.0f} ms this turn"
39+
)
40+
```
41+
42+
The Moss example already logs its line (no edit needed).
43+
44+
## 2. Configure keys (`ai_agents/.env`)
45+
46+
Shared: `AGORA_APP_ID`/`AGORA_APP_CERTIFICATE`, `DEEPGRAM_API_KEY`, `OPENAI_API_KEY`,
47+
`OPENAI_MODEL`, `ELEVENLABS_TTS_KEY`.
48+
memU: `MEMU_API_KEY` (free trial at https://memu.pro).
49+
Moss: `MOSS_PROJECT_ID`, `MOSS_PROJECT_KEY`, `MOSS_INDEX_NAME=ten-moss-demo`.
50+
51+
## 3. Run each agent and ask the same questions
52+
53+
```bash
54+
docker compose up -d
55+
docker exec -it ten_agent_dev bash
56+
57+
# --- Agent A: TEN default (memU) ---
58+
task use AGENT=agents/examples/voice-assistant-with-memU
59+
task run
60+
# open http://localhost:3000, connect, ask: "how long do refunds take?",
61+
# "which payment methods can I use?", "how fast is express shipping?"
62+
# watch the logs for: [retrieval-latency] backend=memU(cloud) took NNN ms
63+
64+
# --- Agent B: Moss (in a second run) ---
65+
uv pip install --system /app/ten_moss-0.0.1-py3-none-any.whl # once
66+
task use AGENT=agents/examples/voice-assistant-with-moss
67+
task run
68+
# ask the same three questions
69+
# watch the logs for: [retrieval-latency] backend=moss(in-process) took N ms
70+
```
71+
72+
Isolate the numbers in the terminal with:
73+
74+
```bash
75+
docker logs -f ten_agent_dev 2>&1 | grep --line-buffered "[retrieval-latency]"
76+
```
77+
78+
## What you'll see
79+
80+
- **memU (TEN default):** hundreds of ms per turn — a network round trip to `api.memu.so`
81+
plus server-side search, right on the hot path before the LLM can start.
82+
- **Moss:** single-digit ms per turn (≈2 ms measured on this demo index) — the retrieval
83+
effectively disappears from the turn budget, so the agent starts replying immediately.
84+
85+
Same agent, same answer quality — Moss removes the retrieval latency that TEN's default,
86+
remote-by-design backends add to every turn.

apps/ten-moss/README.md

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
# Voice Assistant with Moss (TEN Framework)
2+
3+
A real-time voice agent built on the [TEN Framework](https://github.com/ten-framework/ten-framework) that grounds its answers in a [Moss](https://moss.dev) session. On every final ASR transcript the control extension asks Moss for session-scoped context (~1–10ms, in-process) and injects it into the LLM prompt before the model responds — so answers reflect your knowledge base with no perceptible added latency.
4+
5+
The Moss integration lives in the `main_python` control extension and is powered by the [`ten-moss`](../../packages/ten-moss) package (`MossSessionManager`).
6+
7+
## How it works
8+
9+
```
10+
mic ─▶ agora_rtc ─▶ streamid_adapter ─▶ stt (deepgram) ─┐
11+
│ data: asr_result (final)
12+
13+
main_control (main_python)
14+
│ MossSessionManager.query_context(text)
15+
▼ ── Moss session query <10ms ──▶ index
16+
│ ◀── grounding ──
17+
queue_llm_input("{context}\n\n[Current User Question]\n{text}")
18+
19+
llm (openai) ─▶ tts (elevenlabs) ─▶ agora_rtc ─▶ speaker
20+
```
21+
22+
The Moss delta over the stock TEN voice assistant is small and lives in three places in `main_python`:
23+
24+
- `config.py``MainControlConfig` inherits `MossSessionConfig` (the `moss_*` properties).
25+
- `extension.py` `on_init` — opens the Moss session (`MossSessionManager.from_config(...).open()`), best-effort.
26+
- `extension.py` `_on_asr_result``query_context(text)` and prepends the grounding to the user's turn.
27+
28+
## Provenance
29+
30+
The `tenapp/` baseline (graph, `main_python` control extension, agent runtime, scripts) is vendored from the TEN Framework `voice-assistant` example at commit
31+
[`c385d27`](https://github.com/ten-framework/ten-framework/tree/c385d2724a1f3e6ac4ee0b81fcc7dada8346c0e0/ai_agents/agents/examples/voice-assistant),
32+
licensed under **Apache-2.0** (headers preserved). Only the Moss delta described above is Moss-authored.
33+
34+
Two small correctness patches were applied on top of the vendored baseline:
35+
`agent/decorators.py` fixes the `agent_event_handler` annotation to `type[AgentEvent]`,
36+
and `extension.py` parses `session_id` defensively so a non-numeric value can't crash the
37+
ASR handler.
38+
39+
## Prerequisites
40+
41+
- A **TEN Framework checkout**. This example references shared TEN extensions via relative paths (`../../../ten_packages/extension/...`) and runs with TEN's own tooling, so it lives **inside** a TEN Framework repo. It ships the TEN app (`tenapp/`) — not the repo-level run harness (playground / server / Taskfile / Dockerfile), which the TEN Framework provides.
42+
- A **Moss** project (`MOSS_PROJECT_ID` / `MOSS_PROJECT_KEY`) — [moss.dev](https://moss.dev).
43+
- Provider keys: **Agora** (transport), **Deepgram** (STT), **OpenAI** (LLM), **ElevenLabs** (TTS).
44+
45+
## Run
46+
47+
1. **Build the demo knowledge index** (from this directory — needs only the Moss SDK):
48+
```bash
49+
cp .env.example .env # fill in MOSS_PROJECT_ID / MOSS_PROJECT_KEY / MOSS_INDEX_NAME
50+
python create_index.py # reads data/knowledge.jsonl, creates MOSS_INDEX_NAME
51+
```
52+
53+
2. **Drop the app into a TEN checkout.** Copy `tenapp/` to
54+
`ten-framework/ai_agents/agents/examples/voice-assistant-with-moss/tenapp/`, alongside
55+
the sibling `voice-assistant` example whose `Taskfile`/`playground`/`server` harness you
56+
reuse. `main_python` depends on [`ten-moss`](https://pypi.org/project/ten-moss/) (listed
57+
in `main_python/requirements.txt`), so `task install` installs it from PyPI automatically —
58+
no manual step needed.
59+
60+
3. **Run with TEN's tooling** from that example dir (`task install && task run`, per the TEN
61+
docs), providing the same env vars as step 1. Then open the TEN playground
62+
(http://localhost:3000) and ask something covered by `data/knowledge.jsonl` — e.g.
63+
*"how long do refunds take?"* — to hear grounded answers.
64+
65+
## See the difference Moss makes (no voice stack needed)
66+
67+
`compare.py` answers the same questions with the same LLM twice — once **without** Moss and
68+
once **with** the Moss grounding the agent injects — so you can see the improvement without
69+
standing up the full voice pipeline. It needs only your Moss + OpenAI keys (no Agora/STT/TTS):
70+
71+
```bash
72+
cp .env.example .env # MOSS_* + OPENAI_API_KEY (+ optional OPENAI_MODEL)
73+
python create_index.py # build the index once
74+
pip install ten-moss openai python-dotenv
75+
python compare.py # or: python compare.py "your own question?"
76+
```
77+
78+
Sample run (`gpt-4o-mini` over `data/knowledge.jsonl`):
79+
80+
| Question | Without Moss | With Moss |
81+
| --- | --- | --- |
82+
| How long do refunds take? | "5–10 business days" ❌ | "3–5 business days once approved" ✅ |
83+
| Can I cancel my order? | "within a specific timeframe… check our policy" | "within 1 hour of placement" ✅ |
84+
| Which payment methods? | misses American Express | "Visa, Mastercard, Amex, PayPal, Apple Pay" ✅ |
85+
| Do you offer price matching? | "provide competitor details" | "authorized retailers within 14 days" ✅ |
86+
| How fast is express shipping? | "1–3 business days" ❌ | "1–2 business days" ✅ |
87+
88+
Without grounding the model confidently invents plausible-but-wrong specifics; with Moss it
89+
answers from your knowledge base. This is the exact delta the live voice agent applies per
90+
turn — flip `enable_moss` in `property.json` to A/B the same thing in the playground.
91+
92+
## Showcase the speed (live, in the agent)
93+
94+
Every turn, the control extension logs the retrieval cost using the SDK's own
95+
`SearchResult.time_taken_ms` (surfaced by `ten-moss` as `last_time_taken_ms`), with the
96+
wall-clock alongside for reference:
97+
98+
```
99+
[retrieval-latency] backend=moss(in-process) time_taken_ms=2 (wall_clock=64ms)
100+
```
101+
102+
And in the playground transcript you see, per turn, **what Moss retrieved + the SDK
103+
`time_taken_ms`**, followed by the **LLM's answer**:
104+
105+
```
106+
🔎 Moss · retrieved in 2 ms (SDK time_taken_ms)
107+
Relevant knowledge from Moss: [1] Refunds are processed within 3-5 business days…
108+
<the assistant's spoken answer>
109+
```
110+
111+
It also emits a **per-turn latency breakdown** — a grep-able log line *and* a note in the
112+
transcript — so you can see where the turn's time goes across the pipeline:
113+
114+
```
115+
[latency-breakdown] turn=3 moss_retrieval_ms=2 llm_ttft_ms=480 llm_total_ms=1150 turn_total_ms=1160
116+
```
117+
118+
- **moss_retrieval_ms** — the SDK's `SearchResult.time_taken_ms` (in-process retrieval engine time).
119+
- **llm_ttft_ms** — time to the LLM's first token after dispatch.
120+
- **llm_total_ms** — full LLM generation for the turn.
121+
- **turn_total_ms** — ASR-final → LLM-final (the whole control-side turn).
122+
123+
ASR timing appears in the Deepgram STT extension logs and TTS audio-out in the ElevenLabs
124+
TTS logs (both per turn in the worker log) — so between those and the line above you get the
125+
full component-by-component breakdown.
126+
127+
**TEN default retrieval vs Moss (real numbers).** TEN's shipped memory/RAG backends
128+
(memU, OceanBase PowerRAG, EverMemOS) are all remote services — every turn is a network
129+
round trip. To compare TEN's default (memU) against Moss with real logged latency from
130+
both agents, follow **[`BENCHMARK.md`](BENCHMARK.md)**: it adds the same one‑line
131+
latency log to TEN's shipped `voice-assistant-with-memU` example, then you run both agents
132+
and read `[retrieval-latency]` from each (memU: hundreds of ms; Moss: single‑digit ms).
133+
134+
**Quick single‑agent approximation (no memU key).** If you just want to hear the effect in
135+
this one agent, set `moss_simulate_remote_ms` on the `main_control` node in
136+
`tenapp/property.json` to a remote‑like latency and re‑run `task run`:
137+
138+
- `0` → Moss in‑process (~2 ms) — the agent replies immediately.
139+
- `400` → the same agent, same answer, but audibly **pauses ~400 ms before every reply**.
140+
141+
## Configuration
142+
143+
Moss is configured on the `main_control` node in `tenapp/property.json` (env-substituted):
144+
`moss_project_id`, `moss_project_key`, `moss_index_name`, `moss_model_id`,
145+
`moss_top_k`, `moss_alpha`, `moss_context_header`, `enable_moss`,
146+
`moss_simulate_remote_ms`. Set `enable_moss` to `false` to run the plain voice
147+
assistant with no grounding; set `moss_simulate_remote_ms` to imitate a slow remote store.
148+
149+
## Testing status
150+
151+
The `ten-moss` package is covered by offline unit tests (`packages/ten-moss/tests/`).
152+
This end-to-end app is **not** run in CI — it requires the TEN toolchain plus paid
153+
Agora/Deepgram/OpenAI/ElevenLabs credentials, so it is validated manually via the
154+
steps above.

0 commit comments

Comments
 (0)