Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions ai_agents/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ QWEN_API_KEY=
# Extension: deepseek
DEEPSEEK_API_KEY=

# Optional voice_assistant_jev_router graph
TYPESAFE_API_KEY=
DEEPSEEK_PROXY_URL=

# Stepfun API Key
STEPFUN_API_KEY=
GLADIA_API_KEY=
Expand Down
43 changes: 41 additions & 2 deletions ai_agents/agents/examples/voice-assistant/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,20 @@ A configurable voice assistant with real-time conversation capabilities using Ag
- `DEEPGRAM_API_KEY` for the default `voice_assistant` graph
- `XAI_API_KEY` for `voice_assistant_xai_asr` or `voice_assistant_xai_full`

3. **OpenAI Account**: Get credentials from [OpenAI Platform](https://platform.openai.com/)
- `OPENAI_API_KEY` - Your OpenAI API key (required)
3. **LLM Provider**: Most graphs use an OpenAI-compatible model with
`OPENAI_API_KEY`. The optional Jev graph instead uses `DEEPSEEK_API_KEY`.

4. **TTS Provider**: choose the graph you want to run
- `ELEVENLABS_TTS_KEY` for the default `voice_assistant` graph or `voice_assistant_xai_asr`
- `GRADIUM_API_KEY` for `voice_assistant_gradium`
- `XAI_API_KEY` for `voice_assistant_xai_tts` or `voice_assistant_xai_full`

5. **Optional Jev routing graph**: `voice_assistant_jev_router` needs
`TYPESAFE_API_KEY` and `DEEPSEEK_API_KEY`. Both routes use `deepseek-flash`:
simple conversation runs without thinking, while complex requests enable
thinking. It also needs the same Deepgram and ElevenLabs keys
as the default graph. This optional graph is a chatbot and has no tools.

### Provider-specific keys

- **Deepgram Account**: Get credentials from [Deepgram Console](https://console.deepgram.com/)
Expand All @@ -41,6 +47,7 @@ A configurable voice assistant with real-time conversation capabilities using Ag
- `AGORA_APP_CERTIFICATE` - Agora App Certificate (optional)
- `OPENAI_MODEL` - OpenAI model name (optional, defaults to configured model)
- `OPENAI_PROXY_URL` - Proxy URL for OpenAI API (optional)
- `DEEPSEEK_PROXY_URL` - Proxy URL for the optional Jev graph (optional)
- `WEATHERAPI_API_KEY` - Weather API key for weather tool (optional)

## Setup
Expand All @@ -62,6 +69,9 @@ OPENAI_API_KEY=your_openai_api_key_here
OPENAI_MODEL=gpt-4
OPENAI_PROXY_URL=your_proxy_url_here

# For voice_assistant_jev_router, use these instead of the OpenAI variables
TYPESAFE_API_KEY=your_typesafe_api_key_here
DEEPSEEK_API_KEY=your_deepseek_api_key_here
# ElevenLabs (required for text-to-speech)
ELEVENLABS_TTS_KEY=your_elevenlabs_api_key_here

Expand Down Expand Up @@ -111,14 +121,43 @@ Available graph names:
- `voice_assistant_xai_asr` - xAI STT + OpenAI-compatible LLM + ElevenLabs TTS
- `voice_assistant_xai_tts` - Deepgram STT + OpenAI-compatible LLM + xAI TTS
- `voice_assistant_xai_full` - xAI STT + OpenAI-compatible LLM + xAI TTS
- `voice_assistant_jev_router` - Deepgram STT + Jev thinking-mode routing +
DeepSeek Flash chatbot + ElevenLabs TTS

Examples:

```text
http://localhost:3000/?graph=voice_assistant_xai_full
https://ten-demo.agora.io/?graph=voice_assistant_xai_full
http://localhost:3000/?graph=voice_assistant_jev_router
```

The Jev graph is opt-in; the default `voice_assistant` graph is unchanged.
Its Deepgram Nova-3 ASR uses a 500 ms endpointing window. Stable `is_final`
segments are accumulated. `speech_final` submits the complete utterance to Jev
and DeepSeek immediately; if it does not arrive, a 1000 ms deadline after the
last nonempty ASR result submits the buffered stable text once. Empty ASR
results do not extend the deadline, and late results from an already submitted
segment are ignored. The default graph retains segment-final turn handling.
Barge-in behavior is unchanged.
Before each routed reply, the chat shows a separate, non-spoken Jev route
record with the choice, confidence (when available), selected LLM, latency,
and whether an error or timeout caused a fallback.
Jev evaluates the final English ASR text with a `fast`/`deep` choice. A `fast`
choice with confidence at least 0.8 uses Flash without thinking; `deep` or
an uncertain `fast` choice uses Flash with thinking. Jev errors,
invalid responses, and timeouts fall back to Flash without thinking to keep
the voice response responsive. The Jev request has a 2000 ms deadline and
the controller waits at most 2300 ms. These thresholds are demo starting
points and should be calibrated with real requests before production use.
When thinking is selected, the complete DeepSeek reasoning is shown in a
separate chat message before the final answer; it is never sent to TTS. The
route appears as a normal assistant message, so no frontend changes are
needed. This graph registers no tools.
Both LLM nodes are pinned to `deepseek-flash`; the old
`DEEPSEEK_FAST_MODEL` and `DEEPSEEK_DEEP_MODEL` values do not override this
graph.

## Configuration

The voice assistant is configured in `tenapp/property.json`:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,36 @@
],
"path": "../../../ten_packages/extension/openai_llm2_python"
},
{
"type": "extension",
"name": "deepseek_llm2_python",
"version": "0.1.0",
"hash": "0f5e93ed9706f92ee48c5f9b26ba07941e5bd837c6ada0338f8eafa14fad9a1f",
"dependencies": [
{
"type": "system",
"name": "ten_runtime_python"
},
{
"type": "system",
"name": "ten_ai_base"
}
],
"path": "../../../ten_packages/extension/deepseek_llm2_python"
},
{
"type": "extension",
"name": "typesafe_jev_python",
"version": "0.1.0",
"hash": "4533399d37b1691ec3600dc37cd9d50dcb249ff559099d0a53b298691fd8998d",
"dependencies": [
{
"type": "system",
"name": "ten_runtime_python"
}
],
"path": "../../../ten_packages/extension/typesafe_jev_python"
},
{
"type": "extension",
"name": "anthropic_llm2_python",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,12 @@
{
"path": "../../../ten_packages/extension/openai_llm2_python"
},
{
"path": "../../../ten_packages/extension/deepseek_llm2_python"
},
{
"path": "../../../ten_packages/extension/typesafe_jev_python"
},
{
"path": "../../../ten_packages/extension/anthropic_llm2_python"
},
Expand Down Expand Up @@ -181,4 +187,4 @@
"scripts": {
"start": "scripts/start.sh"
}
}
}
165 changes: 165 additions & 0 deletions ai_agents/agents/examples/voice-assistant/tenapp/property.json
Original file line number Diff line number Diff line change
Expand Up @@ -1779,6 +1779,171 @@
}
]
}
},
{
"name": "voice_assistant_jev_router",
"auto_start": false,
"graph": {
"nodes": [
{
"type": "extension",
"name": "agora_rtc",
"addon": "agora_rtc",
"extension_group": "default",
"property": {
"app_id": "${env:AGORA_APP_ID}",
"app_certificate": "${env:AGORA_APP_CERTIFICATE|}",
"channel": "ten_agent_test",
"stream_id": 1234,
"remote_stream_id": 123,
"subscribe_audio": true,
"publish_audio": true,
"publish_data": true,
"enable_agora_asr": false
}
},
{
"type": "extension",
"name": "stt",
"addon": "deepgram_asr_python",
"extension_group": "stt",
"property": {
"params": {
"api_key": "${env:DEEPGRAM_API_KEY}",
"language": "en-US",
"model": "nova-3",
"endpointing": 500
}
}
},
{
"type": "extension",
"name": "llm_fast",
"addon": "deepseek_llm2_python",
"extension_group": "chatgpt_fast",
"property": {
"base_url": "https://api.deepseek.com",
"api_key": "${env:DEEPSEEK_API_KEY}",
"model": "deepseek-flash",
"max_tokens": 512,
"prompt": "You are a voice assistant. Give concise spoken answers unless the user asks for detail. Never reveal internal reasoning.",
"proxy_url": "${env:DEEPSEEK_PROXY_URL|}"
}
},
{
"type": "extension",
"name": "llm_deep",
"addon": "deepseek_llm2_python",
"extension_group": "chatgpt_deep",
"property": {
"base_url": "https://api.deepseek.com",
"api_key": "${env:DEEPSEEK_API_KEY}",
"model": "deepseek-flash",
"max_tokens": 8192,
"prompt": "You are a voice assistant. Think carefully, then give a concise final answer unless the user asks for detail. Keep reasoning out of the spoken answer; it is delivered separately.",
"proxy_url": "${env:DEEPSEEK_PROXY_URL|}"
}
},
{
"type": "extension",
"name": "jev",
"addon": "typesafe_jev_python",
"extension_group": "decision",
"property": {
"params": {
"api_key": "${env:TYPESAFE_API_KEY}",
"model": "jev-1.13.0",
"timeout_ms": 2000
}
}
},
{
"type": "extension",
"name": "tts",
"addon": "elevenlabs_tts2_python",
"extension_group": "tts",
"property": {
"dump": false,
"dump_path": "./",
"params": {
"key": "${env:ELEVENLABS_TTS_KEY}",
"model_id": "eleven_multilingual_v2",
"voice_id": "pNInz6obpgDQGcFmaJgB",
"output_format": "pcm_16000"
}
}
},
{
"type": "extension",
"name": "main_control",
"addon": "main_jev_python",
"extension_group": "control",
"property": {
"greeting": "TEN Agent connected. How can I help you today?",
"turn_detection_mode": "speech_final",
"asr_final_fallback_ms": 1000,
"model_routing": {
"enabled": true,
"decision_dest": "jev",
"fast_dest": "llm_fast",
"deep_dest": "llm_deep",
"min_confidence": 0.8,
"timeout_ms": 2300,
"question": {
"type": "choice",
"instructions": "Does the latest user request need deliberate multi-step reasoning before a concise spoken answer? Do not judge by length alone.",
"criteria": {
"fast": "Greetings, small talk, and direct questions that can be answered without extended reasoning",
"deep": "Multi-step reasoning, planning with tradeoffs, or nuanced analysis that benefits from deliberate thought"
}
}
}
}
},
{
"type": "extension",
"name": "message_collector",
"addon": "message_collector2",
"extension_group": "transcriber",
"property": {}
},
{
"type": "extension",
"name": "streamid_adapter",
"addon": "streamid_adapter",
"property": {}
}
],
"connections": [
{
"extension": "main_control",
"cmd": [
{"names": ["on_user_joined", "on_user_left"], "source": [{"extension": "agora_rtc"}]}
],
"data": [
{"name": "asr_result", "source": [{"extension": "stt"}]},
{"name": "tts_audio_start", "source": [{"extension": "tts"}]},
{"name": "tts_audio_end", "source": [{"extension": "tts"}]}
]
},
{
"extension": "agora_rtc",
"audio_frame": [
{"name": "pcm_frame", "dest": [{"extension": "streamid_adapter"}]},
{"name": "pcm_frame", "source": [{"extension": "tts"}]}
],
"data": [
{"name": "data", "source": [{"extension": "message_collector"}]}
]
},
{
"extension": "streamid_adapter",
"audio_frame": [
{"name": "pcm_frame", "dest": [{"extension": "stt"}]}
]
}
]
}
}
],
"log": {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Jev voice-assistant controller

`main_jev_python` is used only by the optional
`voice_assistant_jev_router` graph. It combines Deepgram's final English
utterance, asks the `typesafe_jev_python` extension to choose `fast` or
`deep`, and sends the chat request to `llm_fast` or `llm_deep`.

The two LLM nodes both use DeepSeek Flash. `fast` disables thinking; `deep`
enables it. A `fast` choice needs confidence of at least 0.80. A low-confidence
choice uses thinking, while a Jev error or timeout falls back to no thinking.

The route is sent to the message collector as a normal assistant transcript
before the answer. Reasoning is sent as the existing `reasoning` message type.
Neither the route nor the reasoning is sent to TTS. Route, reasoning, and
answer use separate stream IDs so the unchanged playground does not merge
their chat bubbles.

The shared `main_python` controller is not imported or modified by this
extension. Other graphs continue to use it unchanged.
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#
# This file is part of TEN Framework, an open source project.
# Licensed under the Apache License, Version 2.0.
# See the LICENSE file for more information.
#
from . import addon
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#
# This file is part of TEN Framework, an open source project.
# Licensed under the Apache License, Version 2.0.
# See the LICENSE file for more information.
#
from ten_runtime import (
Addon,
register_addon_as_extension,
TenEnv,
)


@register_addon_as_extension("main_jev_python")
class MainJevExtensionAddon(Addon):
def on_create_instance(self, ten_env: TenEnv, name: str, context) -> None:
from .extension import MainControlExtension

ten_env.log_info("on_create_instance")
ten_env.on_create_instance_done(MainControlExtension(name), context)
Loading
Loading