Skip to content

Commit bc104e5

Browse files
committed
Drop essay comments from the Moss sample files.
Keep only the notes a stranger cannot infer: SSE must end with data: [DONE], Bearer is required off mock, FastAPI mounts skip lifespan, and retrieval notes need their own stream_id.
1 parent 6eabaaf commit bc104e5

5 files changed

Lines changed: 13 additions & 102 deletions

File tree

apps/agora-custom-llm-moss/server/src/llm.py

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,9 @@
1-
"""
2-
Custom LLM endpoint with Moss grounding.
1+
"""OpenAI-compatible /chat/completions with Moss.
32
43
Forked from Agora's custom-llm recipe (MIT):
54
https://github.com/AgoraIO-Conversational-AI/recipe-agent-custom-llm/blob/main/server/src/llm.py
65
7-
Agora cloud POSTs here. Two modes, same index:
8-
9-
ambient last user text -> query_context -> prepend -> upstream LLM
10-
tool advertise search_knowledge_base to the upstream LLM;
11-
run Moss here if the model calls it (cap 2);
12-
stream only the final spoken answer.
13-
14-
SSE contract: each line is `data: {json}`, end with `data: [DONE]`.
15-
Non-mock requests need `Authorization: Bearer`.
6+
SSE must end with `data: [DONE]`. Non-mock requests need Authorization: Bearer.
167
"""
178

189
from __future__ import annotations
@@ -125,7 +116,6 @@ def as_dict(msg: Any) -> dict[str, Any]:
125116

126117

127118
async def query_moss(session, user_text: str) -> str:
128-
"""Fail-open: empty string if Moss is missing or errors."""
129119
if session is None:
130120
return ""
131121
try:
@@ -261,7 +251,7 @@ async def tool_answer(messages: list, session, mock: bool) -> str:
261251

262252

263253
async def call_upstream(messages: list, tools: list | None = None, raw: bool = False):
264-
"""Non-streaming upstream call. We only stream the final spoken answer to Agora."""
254+
# Upstream is non-streaming; only the final answer is sent to Agora as SSE.
265255
import httpx
266256

267257
payload = [as_dict(m) for m in messages]
@@ -295,8 +285,7 @@ def create_app(moss_mode: str = "ambient") -> FastAPI:
295285
state: dict[str, Any] = {"session": None, "ready": False}
296286

297287
async def get_session():
298-
# Open on first use so this works both standalone and mounted
299-
# under server.py (FastAPI does not always run a mount's lifespan).
288+
# FastAPI does not always run a mounted app's lifespan.
300289
if not state["ready"]:
301290
state["session"] = await open_moss()
302291
state["ready"] = True

apps/ten-moss/README.md

Lines changed: 4 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,6 @@ flowchart LR
3737
class ctl,idx moss
3838
```
3939

40-
Everything on the retrieval path runs inside the agent process. There is no network hop between the transcript arriving and the grounded prompt reaching the LLM.
41-
4240
## What's in this directory
4341

4442
This example ships the TEN app plus a small index builder; the run harness (playground, server, Taskfile, Dockerfile) comes from the TEN Framework, so `tenapp/` drops into any TEN checkout.
@@ -97,30 +95,9 @@ The Moss delta lives in `main_python`:
9795
| `extension.py` `on_cmd` | Tool: `query_context(arguments.query)` and return `{type: "llmresult", content: grounding}`. |
9896
| `tenapp/property.json` | `voice_assistant` (ambient, auto-start) and `voice_assistant_tools`. |
9997

100-
Anatomy of a turn:
101-
102-
```mermaid
103-
sequenceDiagram
104-
autonumber
105-
participant User
106-
participant STT as Deepgram STT
107-
participant Ctl as main_control
108-
participant Moss as Moss session (in-process)
109-
participant LLM as OpenAI LLM
110-
participant TTS as ElevenLabs TTS
111-
112-
User->>STT: speech (via agora_rtc + streamid_adapter)
113-
STT->>Ctl: asr_result (final)
114-
Ctl->>Moss: query_context(text)
115-
Moss-->>Ctl: grounding (single-digit ms)
116-
Ctl->>LLM: context + [Current User Question] + text
117-
LLM-->>TTS: streamed response
118-
TTS-->>User: audio (via agora_rtc)
119-
```
120-
12198
## Measure the latency
12299

123-
Every turn, the control extension logs the retrieval cost using the SDK's own `SearchResult.time_taken_ms` (surfaced by `ten-moss` as `last_time_taken_ms`), with the wall clock alongside for reference:
100+
Logs use the SDK `SearchResult.time_taken_ms` (`ten-moss.last_time_taken_ms`), plus wall clock:
124101

125102
```
126103
[retrieval-latency] backend=moss(in-process) time_taken_ms=2 (wall_clock=64ms)
@@ -134,7 +111,7 @@ In the playground transcript you see, per turn, what Moss retrieved plus the SDK
134111
<the assistant's spoken answer>
135112
```
136113

137-
The extension also emits a per-turn latency breakdown, both as a grep-able log line and as a note in the transcript, so you can see where each turn's time goes:
114+
Per-turn breakdown:
138115

139116
```
140117
[latency-breakdown] turn=3 moss_retrieval_ms=2 llm_ttft_ms=480 llm_total_ms=1150 turn_total_ms=1160
@@ -147,11 +124,7 @@ The extension also emits a per-turn latency breakdown, both as a grep-able log l
147124
| `llm_total_ms` | Full LLM generation for the turn. |
148125
| `turn_total_ms` | ASR-final to LLM-final (the whole control-side turn). |
149126

150-
ASR timing appears in the Deepgram STT extension logs and TTS audio-out in the ElevenLabs TTS logs (both per turn in the worker log), so between those and the lines above you get the full component-by-component breakdown.
151-
152-
### Benchmark against TEN's default retrieval
153-
154-
TEN's shipped memory/RAG backends (memU, OceanBase PowerRAG, EverMemOS) are remote services that pay a network round trip every turn, whereas Moss retrieves in-process, so the same grounding is a local call of single-digit milliseconds.
127+
ASR timing is in the Deepgram logs; TTS audio-out is in the ElevenLabs logs.
155128

156129
## Configuration
157130

@@ -178,7 +151,7 @@ No Agora, no Deepgram, no mic. Gold phrases are the 10 FAQs.
178151
python bench/run.py --echo-grounding
179152
```
180153

181-
`--echo-grounding` needs no LLM key. With `MOSS_*` set it reports `moss_retrieval_ms`. Without them it still prints the table from the local FAQ file. The tool arm always searches in that smoke (no LLM to decide). See `bench/README.md`.
154+
See `bench/README.md`.
182155

183156
## Provenance
184157

apps/ten-moss/bench/run.py

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,7 @@
1-
"""Offline gold-phrase bench: ambient vs tool vs no-Moss.
1+
"""Gold-phrase bench. python bench/run.py --echo-grounding
22
3-
python bench/run.py --echo-grounding
4-
5-
No mic, no Agora, no Deepgram, no LLM key.
6-
With MOSS_* set, ambient and tool call MossSessionManager.query_context.
7-
Without them, those arms look up the matching FAQ in data/knowledge.jsonl
8-
so the table still prints (moss_retrieval_ms is then n/a).
9-
10-
--echo-grounding has no chat model, so the tool arm always searches.
3+
No LLM key. Tool arm always searches (no model). Without MOSS_* the
4+
FAQ file is used and moss_retrieval_ms is n/a.
115
"""
126

137
from __future__ import annotations
@@ -44,7 +38,6 @@ def contains_gold(text: str, gold: list[str]) -> bool:
4438

4539

4640
def lookup_faq(query: str, queries: list[dict], docs: list[dict]) -> str:
47-
"""Return the FAQ that belongs to this published query. No Moss needed."""
4841
by_id = {str(doc.get("id")): doc for doc in docs}
4942
for row in queries:
5043
if row.get("query") == query:
@@ -55,7 +48,6 @@ def lookup_faq(query: str, queries: list[dict], docs: list[dict]) -> str:
5548

5649

5750
async def query_moss(session, query: str) -> tuple[str, float | None, float | None]:
58-
"""Fail-open: empty context on error. Returns (text, sdk_ms, wall_ms)."""
5951
t0 = time.perf_counter()
6052
try:
6153
context = await session.query_context(query)

apps/ten-moss/tenapp/ten_packages/extension/main_python/config.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,5 @@
44

55

66
class MainControlConfig(MossSessionConfig):
7-
"""Moss session fields (moss_*) plus greeting and moss_mode."""
8-
97
greeting: str = "Hello, I am your AI assistant."
108
moss_mode: Literal["ambient", "tool"] = "ambient"

apps/ten-moss/tenapp/ten_packages/extension/main_python/extension.py

Lines changed: 2 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,6 @@
3333

3434

3535
class MainControlExtension(AsyncExtension):
36-
"""
37-
The entry point of the agent module.
38-
Consumes semantic AgentEvents from the Agent class and drives the runtime behavior.
39-
"""
4036

4137
def __init__(self, name: str):
4238
super().__init__(name)
@@ -51,9 +47,6 @@ def __init__(self, name: str):
5147
self.turn_id: int = 0
5248
self.session_id: str = "0"
5349

54-
# Per-turn latency breakdown (see _log_latency_breakdown). main_control
55-
# orchestrates retrieval -> LLM -> TTS, so it can time those stages; ASR
56-
# timing lives in the STT extension logs and TTS audio-out in the TTS logs.
5750
self._turn_t0: float | None = None
5851
self._retrieval_ms: float | None = None
5952
self._llm_sent_at: float | None = None
@@ -67,12 +60,9 @@ def _current_metadata(self) -> dict:
6760
async def on_init(self, ten_env: AsyncTenEnv):
6861
self.ten_env = ten_env
6962

70-
# Load config from runtime properties
7163
config_json, _ = await ten_env.get_property_to_json(None)
7264
self.config = MainControlConfig.model_validate_json(config_json)
7365

74-
# Open a Moss session for ambient, session-scoped grounding (best-effort:
75-
# if the session can't open, the agent still runs, just without grounding).
7666
self.moss = None
7767
if self.config.enable_moss and self.config.moss_index_name:
7868
try:
@@ -87,7 +77,6 @@ async def on_init(self, ten_env: AsyncTenEnv):
8777

8878
self.agent = Agent(ten_env)
8979

90-
# Now auto-register decorated methods
9180
for attr_name in dir(self):
9281
fn = getattr(self, attr_name)
9382
event_type = getattr(fn, "_agent_event_type", None)
@@ -97,7 +86,6 @@ async def on_init(self, ten_env: AsyncTenEnv):
9786
if self.config.moss_mode == "tool":
9887
await self._register_search_knowledge_base()
9988

100-
# === Register handlers with decorators ===
10189
@agent_event_handler(UserJoinedEvent)
10290
async def _on_user_joined(self, event: UserJoinedEvent):
10391
self._rtc_user_count += 1
@@ -118,8 +106,6 @@ async def _on_tool_register(self, event: ToolRegisterEvent):
118106
@agent_event_handler(ASRResultEvent)
119107
async def _on_asr_result(self, event: ASRResultEvent):
120108
self.session_id = event.metadata.get("session_id", "100")
121-
# session_id is a string in the event schema; parse defensively so a
122-
# non-numeric value can't crash the ASR handler and break the voice loop.
123109
try:
124110
stream_id = int(self.session_id)
125111
except (TypeError, ValueError):
@@ -130,13 +116,11 @@ async def _on_asr_result(self, event: ASRResultEvent):
130116
await self._interrupt()
131117
if event.final:
132118
self.turn_id += 1
133-
self._turn_t0 = time.perf_counter() # turn clock starts at ASR-final
119+
self._turn_t0 = time.perf_counter()
134120
self._retrieval_ms = None
135121
self._last_grounding = ""
136122
self._last_sdk_ms = None
137123
llm_input = event.text
138-
# Ambient searches here and prepends. Tool mode sends the raw
139-
# transcript; the LLM calls search_knowledge_base if it needs facts.
140124
if self.moss is not None and self.config.moss_mode != "tool":
141125
context = await self._query_moss(event.text)
142126
if context:
@@ -150,7 +134,6 @@ async def _on_asr_result(self, event: ASRResultEvent):
150134

151135
@agent_event_handler(LLMResponseEvent)
152136
async def _on_llm_response(self, event: LLMResponseEvent):
153-
# First streamed token of this turn -> time-to-first-token.
154137
if (
155138
event.type == "message"
156139
and self._llm_first_at is None
@@ -196,7 +179,6 @@ async def on_cmd(self, ten_env: AsyncTenEnv, cmd: Cmd):
196179
async def on_data(self, ten_env: AsyncTenEnv, data: Data):
197180
await self.agent.on_data(data)
198181

199-
# === helpers ===
200182
async def _register_search_knowledge_base(self) -> None:
201183
tool = LLMToolMetadata(
202184
name=SEARCH_KNOWLEDGE_BASE,
@@ -219,7 +201,6 @@ async def _register_search_knowledge_base(self) -> None:
219201
)
220202

221203
async def _on_tool_call(self, cmd: Cmd) -> None:
222-
"""search_knowledge_base: query_context, return empty on error."""
223204
try:
224205
raw, _ = cmd.get_property_to_json(None)
225206
payload = json.loads(raw) if raw else {}
@@ -256,7 +237,6 @@ async def _on_tool_call(self, cmd: Cmd) -> None:
256237
await self.ten_env.return_result(result)
257238

258239
async def _query_moss(self, user_text: str) -> str:
259-
"""query_context + latency log. Never raise into the voice loop."""
260240
if self.moss is None:
261241
return ""
262242
try:
@@ -279,13 +259,6 @@ async def _query_moss(self, user_text: str) -> str:
279259
return ""
280260

281261
async def _log_latency_breakdown(self):
282-
"""Per-turn latency breakdown for onboarding/debugging.
283-
284-
Emits a grep-able log line ('[latency-breakdown]') and a reasoning note
285-
in the transcript so users can see where each turn's time goes:
286-
Moss retrieval, LLM time-to-first-token, and full LLM generation. (ASR
287-
timing is in the STT extension logs; TTS audio-out in the TTS logs.)
288-
"""
289262
now = time.perf_counter()
290263

291264
def _ms(v: float | None) -> str:
@@ -309,23 +282,18 @@ def _ms(v: float | None) -> str:
309282
f"⏱ turn {self.turn_id} · Moss {_ms(retrieval)} ms (time_taken_ms) · "
310283
f"LLM first token {_ms(ttft)} ms · LLM total {_ms(llm_total)} ms"
311284
)
312-
# Own stream id (distinct from the answer's 100 and the retrieval note's).
313285
await self._send_transcript(
314286
"assistant", note, True, 710_000_000 + self.turn_id, data_type="reasoning"
315287
)
316288

317289
async def _send_retrieval_note(self, grounding: str, time_taken_ms):
318-
"""Show what Moss retrieved this turn + the SDK's time_taken_ms, so users
319-
see the retrieval *results* alongside the timing (the LLM answer follows
320-
as its own transcript message)."""
321290
ms_txt = f"{time_taken_ms}" if time_taken_ms is not None else "n/a"
322291
body = (
323292
f"🔎 Moss · retrieved in {ms_txt} ms (SDK time_taken_ms)\n\n{grounding}"
324293
if grounding
325294
else f"🔎 Moss · retrieved in {ms_txt} ms (SDK time_taken_ms) — no match"
326295
)
327-
# Own stream id so this note is a separate transcript item, not merged
328-
# into (and replacing) the assistant answer bubble at stream_id 100.
296+
# Distinct stream_id so this note is not merged into the answer bubble.
329297
await self._send_transcript(
330298
"assistant", body, True, 700_000_000 + self.turn_id, data_type="reasoning"
331299
)
@@ -338,9 +306,6 @@ async def _send_transcript(
338306
stream_id: int,
339307
data_type: Literal["text", "reasoning"] = "text",
340308
):
341-
"""
342-
Sends the transcript (ASR or LLM output) to the message collector.
343-
"""
344309
if data_type == "text":
345310
await _send_data(
346311
self.ten_env,
@@ -381,9 +346,6 @@ async def _send_transcript(
381346
)
382347

383348
async def _send_to_tts(self, text: str, is_final: bool):
384-
"""
385-
Sends a sentence to the TTS system.
386-
"""
387349
request_id = f"tts-request-{self.turn_id}"
388350
await _send_data(
389351
self.ten_env,
@@ -401,9 +363,6 @@ async def _send_to_tts(self, text: str, is_final: bool):
401363
)
402364

403365
async def _interrupt(self):
404-
"""
405-
Interrupts ongoing LLM and TTS generation. Typically called when user speech is detected.
406-
"""
407366
self.sentence_fragment = ""
408367
await self.agent.flush_llm()
409368
await _send_data(

0 commit comments

Comments
 (0)