Skip to content

Voice-agent: live retrieval web UI + region metadata filtering - #348

Open
HarshaNalluru wants to merge 5 commits into
mainfrom
examples/voice-agent-live-retrieval-ui
Open

Voice-agent: live retrieval web UI + region metadata filtering#348
HarshaNalluru wants to merge 5 commits into
mainfrom
examples/voice-agent-live-retrieval-ui

Conversation

@HarshaNalluru

Copy link
Copy Markdown
Contributor

What

Enhances the existing moss-live-labs/examples/voice-agent (customer-service agent) with a web UI and a metadata-filtering feature.

Live retrieval web UI (web/)

A Next.js app that renders the call next to a Moss · Knowledge Base panel, showing the documents retrieved for each turn with scores and per-query latency (result.time_taken_ms). Data is published on a moss.retrieval data channel.

Region metadata filtering

A US / EU picker in the UI publishes the region on a moss.region data channel. The agent scopes each query with a metadata filter:

QueryOptions(filter={"field": "region", "condition": {"$in": [region, "all"]}})

so a US caller and an EU caller get the right policy from the same index (e.g. 30-day vs 14-day returns), while region-agnostic FAQs (region: all) always match. The agent stays customer-facing and never mentions indexes or regions.

Contents added / changed

  • agent.py — per-turn retrieval, region filter, publishes results to the UI
  • seed_index.py + data/faqs.json — 20 FAQs tagged by category/region
  • web/ — Next.js UI (retrieval panel, region picker, transcript)
  • README.md, DEMO_SCRIPT_METADATA.md

Try it

uv sync && cp .env.example .env
python seed_index.py
livekit-server --dev
python agent.py dev
cd web && npm install && cp .env.local.example .env.local && npm run dev

Docs: https://docs.moss.dev/docs/integrate/metadata-filtering

Enhances the customer-service voice-agent example with:
- A Next.js web UI that shows live Moss retrieval side by side with the
  conversation, lighting up per turn with per-query latency.
- Region metadata filtering: a US/EU picker in the UI publishes the
  region on a data channel; the agent scopes queries with a metadata
  filter (region in [selected, all]) so US and EU callers get the right
  policy (e.g. 30-day vs 14-day returns) from one index.
- seed_index.py and data/faqs.json (20 FAQs tagged by category/region).
- A demo script.

The agent stays customer-facing and never mentions indexes or regions.
Copilot AI review requested due to automatic review settings July 7, 2026 03:12
@HarshaNalluru
HarshaNalluru requested a review from r4ghu as a code owner July 7, 2026 03:12
@CLAassistant

CLAassistant commented Jul 7, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR enhances the experimental moss-live-labs/examples/voice-agent demo by adding a Next.js web UI that visualizes per-turn Moss retrieval in real time, and by introducing region-scoped retrieval via metadata filtering (US/EU) communicated over LiveKit data channels.

Changes:

  • Added a Next.js web app (web/) that joins a LiveKit room, renders the transcript, and displays retrieved chunks, scores, and latency from the moss.retrieval data channel.
  • Updated the Python agent to apply a metadata.region filter per query and to publish retrieval payloads to the UI; added live region selection via the moss.region data channel.
  • Added a seeded FAQ dataset (data/faqs.json) and a seed_index.py helper to create the demo index, plus updated docs and a metadata-filtering demo script.

Reviewed changes

Copilot reviewed 19 out of 21 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
moss-live-labs/examples/voice-agent/agent.py Adds region-scoped retrieval filter and publishes retrieval results to the UI; updates voice pipeline settings.
moss-live-labs/examples/voice-agent/seed_index.py Seeds a Moss index with region/category-tagged FAQs.
moss-live-labs/examples/voice-agent/data/faqs.json Adds sample FAQ documents with region and category metadata.
moss-live-labs/examples/voice-agent/README.md Updates setup/run instructions and explains the retrieval panel wiring.
moss-live-labs/examples/voice-agent/DEMO_SCRIPT_METADATA.md Adds a script focused on demonstrating metadata filtering by region.
moss-live-labs/examples/voice-agent/.env.example Adds MOSS_REGION and clarifies provider key usage.
moss-live-labs/examples/voice-agent/web/package.json Introduces the Next.js UI package and its dependencies.
moss-live-labs/examples/voice-agent/web/package-lock.json Locks the UI dependency tree.
moss-live-labs/examples/voice-agent/web/tsconfig.json TypeScript configuration for the Next.js UI.
moss-live-labs/examples/voice-agent/web/next.config.mjs Next.js config for monorepo tracing root behavior.
moss-live-labs/examples/voice-agent/web/.gitignore Ignores build artifacts, deps, and local env.
moss-live-labs/examples/voice-agent/web/.env.local.example LiveKit env template for the UI token endpoint.
moss-live-labs/examples/voice-agent/web/public/moss-wordmark.svg Adds a Moss wordmark asset for the UI header.
moss-live-labs/examples/voice-agent/web/lib/types.ts Defines retrieval payload types consumed by the UI.
moss-live-labs/examples/voice-agent/web/components/Transcript.tsx Renders live STT/TTS transcription turns.
moss-live-labs/examples/voice-agent/web/components/RetrievalPanel.tsx Shows retrieved chunks and provides the US/EU picker publishing moss.region.
moss-live-labs/examples/voice-agent/web/components/AgentSide.tsx Displays voice assistant state, visualizer, and transcript.
moss-live-labs/examples/voice-agent/web/app/page.tsx UI entry: connects to LiveKit, renders agent + retrieval panel, and handles connect flow.
moss-live-labs/examples/voice-agent/web/app/layout.tsx Defines app metadata and global layout/fonts/styles.
moss-live-labs/examples/voice-agent/web/app/globals.css Provides UI styling and LiveKit component overrides.
moss-live-labs/examples/voice-agent/web/app/api/token/route.ts Implements token minting for local demo usage (joining a random room).
Files not reviewed (1)
  • moss-live-labs/examples/voice-agent/web/package-lock.json: Generated file

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread moss-live-labs/examples/voice-agent/web/next.config.mjs
Comment thread moss-live-labs/examples/voice-agent/README.md Outdated
Comment thread moss-live-labs/examples/voice-agent/README.md Outdated
Comment thread moss-live-labs/examples/voice-agent/README.md Outdated
Comment thread moss-live-labs/examples/voice-agent/agent.py
Comment thread moss-live-labs/examples/voice-agent/agent.py
Comment thread moss-live-labs/examples/voice-agent/seed_index.py
- README: list Cartesia as TTS (OpenAI=LLM, Deepgram=STT), add CARTESIA_API_KEY,
  fix DEMO_SCRIPT link to DEMO_SCRIPT_METADATA.md.
- next.config: derive dir from import.meta.url (Node 18 compatible).
- agent.py: validate region from data channel against an allow-list; fix stale
  'upload.py' hint to 'seed_index.py'.
- seed_index.py: fail fast if Moss credentials are missing.
- RetrievalPanel: add type=button + aria-pressed on the region toggle.
Copilot AI review requested due to automatic review settings July 9, 2026 20:14
@HarshaNalluru

Copy link
Copy Markdown
Contributor Author

Addressed the review: README now lists Cartesia as TTS (OpenAI = LLM, Deepgram = STT) and includes CARTESIA_API_KEY; fixed the DEMO_SCRIPT_METADATA.md link; next.config derives its dir from import.meta.url (Node 18 safe); the region from the data channel is validated against an allow-list; seed_index.py fails fast on missing Moss creds; and the region toggle buttons now set type="button" + aria-pressed.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 19 out of 21 changed files in this pull request and generated 2 comments.

Files not reviewed (1)
  • moss-live-labs/examples/voice-agent/web/package-lock.json: Generated file

Comment thread moss-live-labs/examples/voice-agent/web/tsconfig.json
Comment thread moss-live-labs/examples/voice-agent/web/app/api/token/route.ts Outdated
… validates env + uses crypto.randomUUID (no hard-coded cred fallback)
Copilot AI review requested due to automatic review settings July 9, 2026 21:28

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 19 out of 21 changed files in this pull request and generated 3 comments.

Files not reviewed (1)
  • moss-live-labs/examples/voice-agent/web/package-lock.json: Generated file

Comment thread moss-live-labs/examples/voice-agent/agent.py
Comment thread moss-live-labs/examples/voice-agent/agent.py
Comment thread moss-live-labs/examples/voice-agent/web/app/page.tsx Outdated
@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown

Codex review

The PR adds a useful LiveKit/Moss demo, but the new agent has a startup-time runtime error and the demo UI can misrepresent region-scoped retrieval. There is also a dependency declaration that is contradicted by the package's own pnpm override.

Findings not on changed lines:

  • packages/vitepress-plugin-moss/package.json:53 CONSIDER This declares Vite 8, but the same package still has pnpm.overrides.vite pinned to ^6.4.2, and the updated pnpm lock resolves Vite 6. That means pnpm builds do not test the Vite major declared here, while npm installs can.
"vite": "^8.0.6"

Either update the pnpm override and lockfile to Vite 8, or keep this devDependency on the Vite 6 range actually used by the package manager.

- Require the index to load locally (fatal on failure): region metadata filtering
  is ignored on cloud fallback, so a silent warning would break the demo's point.
- Skip empty/interim transcriptions before querying Moss.
- Validate Moss creds in entrypoint; publish_data uses payload= keyword.
- Add roomName to the token response type.
Copilot AI review requested due to automatic review settings July 9, 2026 21:36

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 19 out of 21 changed files in this pull request and generated 5 comments.

Files not reviewed (1)
  • moss-live-labs/examples/voice-agent/web/package-lock.json: Generated file

Comment thread moss-live-labs/examples/voice-agent/agent.py Outdated
Comment thread moss-live-labs/examples/voice-agent/agent.py
Comment thread moss-live-labs/examples/voice-agent/web/components/Transcript.tsx
Comment thread moss-live-labs/examples/voice-agent/web/app/api/token/route.ts
Comment thread moss-live-labs/examples/voice-agent/README.md Outdated
…p transcript history, Node 18.18+, note unauth token route is local-dev only
Copilot AI review requested due to automatic review settings July 9, 2026 21:45
@HarshaNalluru

Copy link
Copy Markdown
Contributor Author

The token endpoint is intentionally unauthenticated for local-dev (added a comment saying to gate it before any non-local deploy) — consistent with the other LiveKit examples in this repo. Resolving.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 19 out of 21 changed files in this pull request and generated 1 comment.

Files not reviewed (1)
  • moss-live-labs/examples/voice-agent/web/package-lock.json: Generated file

Comment on lines +14 to +18
export async function GET() {
try {
if (!LIVEKIT_URL) throw new Error("LIVEKIT_URL is not defined");
if (!API_KEY) throw new Error("LIVEKIT_API_KEY is not defined");
if (!API_SECRET) throw new Error("LIVEKIT_API_SECRET is not defined");
await session.start(agent=agent, room=ctx.room)

# Speak first, instantly — a fixed opener via say() skips the LLM round-trip.
await session.say(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BLOCKING session.say() is not awaitable, so the agent will raise after joining the room and before the greeting is queued.

await session.say(

LiveKit defines say(...) -> SpeechHandle as a regular method, not async def; call it without await:

session.say(
    "Thanks for calling Northwind support. How can I help you today?",
    allow_interruptions=True,
)

(github.com)

key={r}
type="button"
aria-pressed={region === r}
onClick={() => setRegion(r)}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CONSIDER Switching regions keeps the previous retrieval visible, so after asking under US and clicking EU, the panel immediately says region: EU + global while still rendering US chunks until the next question.

onClick={() => setRegion(r)}

Clear or gate the displayed results when the filter changes:

onClick={() => {
  setRegion(r);
  setData(null);
}}
// or render chunks only when data.region === region

@samanyugoyal2010

Copy link
Copy Markdown
Contributor

@cubic-dev-ai review this pull request

@cubic-dev-ai

cubic-dev-ai Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

@cubic-dev-ai review this pull request

@samanyugoyal2010 I have started the AI code review. It will take a few minutes to complete.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

24 issues found across 21 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="moss-live-labs/examples/voice-agent/web/lib/types.ts">

<violation number="1" location="moss-live-labs/examples/voice-agent/web/lib/types.ts:2">
P3: Documents without an ID arrive with `id: null`, while `RetrievalDoc` promises only `string | undefined`. Modeling the emitted nullable field prevents consumers from treating a present `id` as a string.</violation>
</file>

<file name="moss-live-labs/examples/voice-agent/DEMO_SCRIPT_METADATA.md">

<violation number="1" location="moss-live-labs/examples/voice-agent/DEMO_SCRIPT_METADATA.md:4">
P3: The overview incorrectly says metadata is scoped “per line,” which can suggest a phone line or source-text line. “Scoped per query” matches the implemented per-turn `moss.query` behavior.</violation>

<violation number="2" location="moss-live-labs/examples/voice-agent/DEMO_SCRIPT_METADATA.md:16">
P3: The recording cues quote filter labels that the panel never renders, so the presenter cannot point to the stated UI text. Updating both US/EU cues to `region: US + global` and `region: EU + global` would match the demo.</violation>
</file>

<file name="moss-live-labs/examples/voice-agent/web/package.json">

<violation number="1" location="moss-live-labs/examples/voice-agent/web/package.json:9">
P2: `npm run lint` fails instead of checking the app because ESLint, `eslint-config-next`, and an ESLint configuration are absent. Adding the lint dependencies and config would make the newly exposed script usable locally and in CI.</violation>
</file>

<file name="moss-live-labs/examples/voice-agent/web/app/globals.css">

<violation number="1" location="moss-live-labs/examples/voice-agent/web/app/globals.css:7">
P2: Idle states, speaker labels, query labels, and empty-state instructions have only ~2.87:1 contrast against the panel, making this text difficult to read. Raising `--faint` to a shade with at least 4.5:1 contrast preserves the hierarchy while making these messages accessible.</violation>

<violation number="2" location="moss-live-labs/examples/voice-agent/web/app/globals.css:340">
P3: The disabled “Connecting…” button still shifts upward on hover, visually suggesting that it remains interactive. Restricting the hover rule to enabled buttons keeps the disabled state consistent.</violation>
</file>

<file name="moss-live-labs/examples/voice-agent/data/faqs.json">

<violation number="1" location="moss-live-labs/examples/voice-agent/data/faqs.json:5">
P1: EU callers asking about a manufacturing defect can receive a one-year limit even though they retain a minimum two-year legal guarantee. This policy should be split into region-specific entries, or the EU guarantee should otherwise be represented without exposing EU wording to US retrieval.</violation>

<violation number="2" location="moss-live-labs/examples/voice-agent/data/faqs.json:18">
P1: EU callers can be incorrectly told that final-sale status removes their 14-day withdrawal right; “final sale” is not itself a statutory withdrawal exception. Removing this blanket exclusion, or naming only applicable statutory exceptions, avoids presenting an unlawful policy as EU consumer law.</violation>
</file>

<file name="moss-live-labs/examples/voice-agent/web/components/AgentSide.tsx">

<violation number="1" location="moss-live-labs/examples/voice-agent/web/components/AgentSide.tsx:20">
P2: Agent connection failures and pre-connect buffering are shown to users as “idle” because the fallback collapses every unmapped `AgentState`. Preserving the actual state text would keep connection progress and failures visible.</violation>

<violation number="2" location="moss-live-labs/examples/voice-agent/web/components/AgentSide.tsx:21">
P3: A genuinely idle agent is styled as active because the `idle` flag excludes `state === "idle"`. Including that state also removes the unreachable `!state` check, since `useVoiceAssistant` always returns an `AgentState`.</violation>

<violation number="3" location="moss-live-labs/examples/voice-agent/web/components/AgentSide.tsx:26">
P3: This new component uses the deprecated `BarVisualizer.trackRef` API. Using `track` avoids introducing an API already marked for removal by the locked LiveKit version.</violation>
</file>

<file name="moss-live-labs/examples/voice-agent/web/app/api/token/route.ts">

<violation number="1" location="moss-live-labs/examples/voice-agent/web/app/api/token/route.ts:28">
P3: The issued browser token can publish camera and screen-share tracks as well as the intended microphone, so a custom client can consume unnecessary room bandwidth. Restricting `canPublishSources` to the microphone would match this demo's permission needs.</violation>

<violation number="2" location="moss-live-labs/examples/voice-agent/web/app/api/token/route.ts:35">
P3: `roomName` is returned to the browser but never used; `LiveKitRoom` only receives `serverUrl` and `participantToken`. Removing this response field and its `Conn` member would keep the new internal API contract minimal.</violation>

<violation number="3" location="moss-live-labs/examples/voice-agent/web/app/api/token/route.ts:39">
P3: Token-generation failures are returned verbatim to unauthenticated clients and never recorded server-side. Logging the exception and returning a generic response would preserve diagnostics without exposing internals.</violation>
</file>

<file name="moss-live-labs/examples/voice-agent/web/app/page.tsx">

<violation number="1" location="moss-live-labs/examples/voice-agent/web/app/page.tsx:38">
P3: The header reports `live` as soon as token fetching succeeds, before the LiveKit room is connected, so slow or failed attempts display a false status. Track `onConnected`/`onDisconnected` separately, or render status from `useConnectionState` inside the room context.</violation>
</file>

<file name="moss-live-labs/examples/voice-agent/web/components/RetrievalPanel.tsx">

<violation number="1" location="moss-live-labs/examples/voice-agent/web/components/RetrievalPanel.tsx:27">
P3: A syntactically valid but incomplete `moss.retrieval` packet crashes the panel instead of being rejected by the parse error path. Validate `query`, `docs`, each document, and `took_ms` before calling `setData`.</violation>

<violation number="2" location="moss-live-labs/examples/voice-agent/web/components/RetrievalPanel.tsx:37">
P2: A rejected region publication becomes an unhandled promise rejection and bypasses the intended error log. Attach `.catch(...)` to `publishData` (or make and await the callback) so asynchronous failures are handled.</violation>

<violation number="3" location="moss-live-labs/examples/voice-agent/web/components/RetrievalPanel.tsx:66">
P2: After switching regions, the panel can label and display US retrievals as EU (or vice versa) until another query completes, and an in-flight old result can restore the mismatch. Clear results on selection and ignore retrieval payloads whose `region` does not match the current picker value.</violation>
</file>

<file name="moss-live-labs/examples/voice-agent/web/components/Transcript.tsx">

<violation number="1" location="moss-live-labs/examples/voice-agent/web/components/Transcript.tsx:44">
P3: Screen-reader users are not notified when new transcript turns appear because this dynamic conversation has no live-region semantics. Consider keeping a persistent container with `role="log"` and `aria-live="polite"` across both empty and populated states.</violation>

<violation number="2" location="moss-live-labs/examples/voice-agent/web/components/Transcript.tsx:44">
P2: During longer calls, new turns are appended below the transcript viewport while its scroll position stays unchanged, hiding the current conversation until the user scrolls manually. Consider scrolling to the latest turn when entries are added, while preserving position when the user intentionally reviews older turns.</violation>
</file>

<file name="moss-live-labs/examples/voice-agent/agent.py">

<violation number="1" location="moss-live-labs/examples/voice-agent/agent.py:79">
P2: Retrieval payloads can report a different region from the one used for filtering if the picker changes while `moss.query` is in flight. Snapshot the region once per turn and pass that same value to both `QueryOptions` and `_publish_retrieval`.</violation>

<violation number="2" location="moss-live-labs/examples/voice-agent/agent.py:97">
P2: Empty/interim transcripts still proceed to reply generation, so the agent can speak without a user question. LiveKit's empty-turn handling uses `raise StopResponse()` here rather than calling the base hook.</violation>

<violation number="3" location="moss-live-labs/examples/voice-agent/agent.py:113">
P2: When Moss query fails, the panel keeps displaying the previous turn's documents and the LLM receives no explicit failure context. Publishing an empty/error retrieval payload and injecting the same grounded fallback in the exception path would prevent stale UI and an ungrounded reply.</violation>

<violation number="4" location="moss-live-labs/examples/voice-agent/agent.py:183">
P1: The picker can show EU while retrieval remains scoped to US when its initial/change packet arrives before this late listener is registered. A region handshake/ack or resend when the agent becomes available would keep UI and agent state synchronized.</violation>
</file>

Tip: instead of fixing issues one by one fix them all with cubic
Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.

Re-trigger cubic

{
"id": "returns-policy",
"category": "returns",
"region": "all",

@cubic-dev-ai cubic-dev-ai Bot Jul 17, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: EU callers asking about a manufacturing defect can receive a one-year limit even though they retain a minimum two-year legal guarantee. This policy should be split into region-specific entries, or the EU guarantee should otherwise be represented without exposing EU wording to US retrieval.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At moss-live-labs/examples/voice-agent/data/faqs.json, line 5:

<comment>EU callers asking about a manufacturing defect can receive a one-year limit even though they retain a minimum two-year legal guarantee. This policy should be split into region-specific entries, or the EU guarantee should otherwise be represented without exposing EU wording to US retrieval.</comment>

<file context>
@@ -0,0 +1,122 @@
+  {
+    "id": "returns-policy",
+    "category": "returns",
+    "region": "all",
+    "text": "Northwind accepts returns for a refund as long as items are unworn, unwashed, and in their original packaging with tags attached. Start a return from the Orders page in your account and print the prepaid label."
+  },
</file context>
Fix with cubic

"id": "return-window-eu",
"category": "returns",
"region": "EU",
"text": "In the EU, you have a 14-day right of withdrawal starting the day you receive the item, in line with EU consumer law. Return shipping on withdrawals is free, and final-sale items are excluded."

@cubic-dev-ai cubic-dev-ai Bot Jul 17, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: EU callers can be incorrectly told that final-sale status removes their 14-day withdrawal right; “final sale” is not itself a statutory withdrawal exception. Removing this blanket exclusion, or naming only applicable statutory exceptions, avoids presenting an unlawful policy as EU consumer law.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At moss-live-labs/examples/voice-agent/data/faqs.json, line 18:

<comment>EU callers can be incorrectly told that final-sale status removes their 14-day withdrawal right; “final sale” is not itself a statutory withdrawal exception. Removing this blanket exclusion, or naming only applicable statutory exceptions, avoids presenting an unlawful policy as EU consumer law.</comment>

<file context>
@@ -0,0 +1,122 @@
+    "id": "return-window-eu",
+    "category": "returns",
+    "region": "EU",
+    "text": "In the EU, you have a 14-day right of withdrawal starting the day you receive the item, in line with EU consumer law. Return shipping on withdrawals is free, and final-sale items are excluded."
+  },
+  {
</file context>
Fix with cubic

agent = MossSemanticRetrievalAgent(moss_client, ctx.room)

# The UI region picker publishes { "region": "US" | "EU" } on this topic.
@ctx.room.on("data_received")

@cubic-dev-ai cubic-dev-ai Bot Jul 17, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: The picker can show EU while retrieval remains scoped to US when its initial/change packet arrives before this late listener is registered. A region handshake/ack or resend when the agent becomes available would keep UI and agent state synchronized.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At moss-live-labs/examples/voice-agent/agent.py, line 183:

<comment>The picker can show EU while retrieval remains scoped to US when its initial/change packet arrives before this late listener is registered. A region handshake/ack or resend when the agent becomes available would keep UI and agent state synchronized.</comment>

<file context>
@@ -73,32 +138,68 @@ async def on_user_turn_completed(self, turn_ctx: ChatContext, new_message: ChatM
+    agent = MossSemanticRetrievalAgent(moss_client, ctx.room)
+
+    # The UI region picker publishes { "region": "US" | "EU" } on this topic.
+    @ctx.room.on("data_received")
+    def _on_data(pkt: rtc.DataPacket):
+        if pkt.topic == "moss.region":
</file context>
Fix with cubic

"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"

@cubic-dev-ai cubic-dev-ai Bot Jul 17, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: npm run lint fails instead of checking the app because ESLint, eslint-config-next, and an ESLint configuration are absent. Adding the lint dependencies and config would make the newly exposed script usable locally and in CI.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At moss-live-labs/examples/voice-agent/web/package.json, line 9:

<comment>`npm run lint` fails instead of checking the app because ESLint, `eslint-config-next`, and an ESLint configuration are absent. Adding the lint dependencies and config would make the newly exposed script usable locally and in CI.</comment>

<file context>
@@ -0,0 +1,27 @@
+    "dev": "next dev",
+    "build": "next build",
+    "start": "next start",
+    "lint": "next lint"
+  },
+  "dependencies": {
</file context>
Fix with cubic

--panel-soft: #0c110d;
--text: #f1f1f1;
--muted: #8b978c;
--faint: #5a6459;

@cubic-dev-ai cubic-dev-ai Bot Jul 17, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Idle states, speaker labels, query labels, and empty-state instructions have only ~2.87:1 contrast against the panel, making this text difficult to read. Raising --faint to a shade with at least 4.5:1 contrast preserves the hierarchy while making these messages accessible.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At moss-live-labs/examples/voice-agent/web/app/globals.css, line 7:

<comment>Idle states, speaker labels, query labels, and empty-state instructions have only ~2.87:1 contrast against the panel, making this text difficult to read. Raising `--faint` to a shade with at least 4.5:1 contrast preserves the hierarchy while making these messages accessible.</comment>

<file context>
@@ -0,0 +1,376 @@
+  --panel-soft: #0c110d;
+  --text: #f1f1f1;
+  --muted: #8b978c;
+  --faint: #5a6459;
+  --sage: #7ea97e;
+  --mint: #abe6c7;
</file context>
Fix with cubic

{ headers: { "Cache-Control": "no-store" } },
);
} catch (error) {
const msg = error instanceof Error ? error.message : "Unknown error";

@cubic-dev-ai cubic-dev-ai Bot Jul 17, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Token-generation failures are returned verbatim to unauthenticated clients and never recorded server-side. Logging the exception and returning a generic response would preserve diagnostics without exposing internals.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At moss-live-labs/examples/voice-agent/web/app/api/token/route.ts, line 39:

<comment>Token-generation failures are returned verbatim to unauthenticated clients and never recorded server-side. Logging the exception and returning a generic response would preserve diagnostics without exposing internals.</comment>

<file context>
@@ -0,0 +1,42 @@
+      { headers: { "Cache-Control": "no-store" } },
+    );
+  } catch (error) {
+    const msg = error instanceof Error ? error.message : "Unknown error";
+    return new NextResponse(msg, { status: 500 });
+  }
</file context>
Fix with cubic

const grant: VideoGrant = {
room: roomName,
roomJoin: true,
canPublish: true, // publish mic

@cubic-dev-ai cubic-dev-ai Bot Jul 17, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The issued browser token can publish camera and screen-share tracks as well as the intended microphone, so a custom client can consume unnecessary room bandwidth. Restricting canPublishSources to the microphone would match this demo's permission needs.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At moss-live-labs/examples/voice-agent/web/app/api/token/route.ts, line 28:

<comment>The issued browser token can publish camera and screen-share tracks as well as the intended microphone, so a custom client can consume unnecessary room bandwidth. Restricting `canPublishSources` to the microphone would match this demo's permission needs.</comment>

<file context>
@@ -0,0 +1,42 @@
+    const grant: VideoGrant = {
+      room: roomName,
+      roomJoin: true,
+      canPublish: true, // publish mic
+      canPublishData: true,
+      canSubscribe: true,
</file context>
Fix with cubic

<span className="divider" />
<span className="title">Northwind Support · voice agent</span>
</div>
<div className={`status ${conn ? "live" : ""}`}>

@cubic-dev-ai cubic-dev-ai Bot Jul 17, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The header reports live as soon as token fetching succeeds, before the LiveKit room is connected, so slow or failed attempts display a false status. Track onConnected/onDisconnected separately, or render status from useConnectionState inside the room context.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At moss-live-labs/examples/voice-agent/web/app/page.tsx, line 38:

<comment>The header reports `live` as soon as token fetching succeeds, before the LiveKit room is connected, so slow or failed attempts display a false status. Track `onConnected`/`onDisconnected` separately, or render status from `useConnectionState` inside the room context.</comment>

<file context>
@@ -0,0 +1,86 @@
+          <span className="divider" />
+          <span className="title">Northwind Support · voice agent</span>
+        </div>
+        <div className={`status ${conn ? "live" : ""}`}>
+          <span className="dot" />
+          {conn ? "live" : "offline"}
</file context>
Fix with cubic

"moss.retrieval",
useCallback((msg: { payload: Uint8Array }) => {
try {
setData(JSON.parse(new TextDecoder().decode(msg.payload)) as RetrievalPayload);

@cubic-dev-ai cubic-dev-ai Bot Jul 17, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: A syntactically valid but incomplete moss.retrieval packet crashes the panel instead of being rejected by the parse error path. Validate query, docs, each document, and took_ms before calling setData.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At moss-live-labs/examples/voice-agent/web/components/RetrievalPanel.tsx, line 27:

<comment>A syntactically valid but incomplete `moss.retrieval` packet crashes the panel instead of being rejected by the parse error path. Validate `query`, `docs`, each document, and `took_ms` before calling `setData`.</comment>

<file context>
@@ -0,0 +1,126 @@
+    "moss.retrieval",
+    useCallback((msg: { payload: Uint8Array }) => {
+      try {
+        setData(JSON.parse(new TextDecoder().decode(msg.payload)) as RetrievalPayload);
+      } catch (err) {
+        console.error("failed to parse moss.retrieval payload", err);
</file context>
Fix with cubic

}

return (
<div className="transcript">

@cubic-dev-ai cubic-dev-ai Bot Jul 17, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Screen-reader users are not notified when new transcript turns appear because this dynamic conversation has no live-region semantics. Consider keeping a persistent container with role="log" and aria-live="polite" across both empty and populated states.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At moss-live-labs/examples/voice-agent/web/components/Transcript.tsx, line 44:

<comment>Screen-reader users are not notified when new transcript turns appear because this dynamic conversation has no live-region semantics. Consider keeping a persistent container with `role="log"` and `aria-live="polite"` across both empty and populated states.</comment>

<file context>
@@ -0,0 +1,53 @@
+  }
+
+  return (
+    <div className="transcript">
+      {ordered.map((turn) => (
+        <div className={`turn ${turn.isUser ? "user" : "agent"}`} key={turn.id}>
</file context>
Fix with cubic

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants