Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
79f63e1
remove unused Backend/Role scaffolding
tara-bogavelli Aug 6, 2026
edcbda8
add CascadeSimulatorConfig
tara-bogavelli Aug 6, 2026
aadcc13
fix stale accent-perturbation guard, add cascade param docs and tests
tara-bogavelli Aug 6, 2026
91dd94b
add cascade package with tick timing constants
tara-bogavelli Aug 6, 2026
2f6f983
clarify tick encoding docstrings and strengthen cascade constants tests
tara-bogavelli Aug 6, 2026
d9b199b
add TickResult and tick-boundary audio splitting
tara-bogavelli Aug 6, 2026
0be9943
polish TickResult: default bytes_per_tick, NamedTuple return, docstrings
tara-bogavelli Aug 6, 2026
1c2859f
add Adapter ABC
tara-bogavelli Aug 6, 2026
6c545af
add TickScheduler turn-state machine and playout queue
tara-bogavelli Aug 7, 2026
8da8927
harden TickScheduler: peek-then-commit playout, reuse split_tick_audi…
tara-bogavelli Aug 7, 2026
903bb61
add RealtimeWSAdapter
tara-bogavelli Aug 7, 2026
814b1a9
Carry resampler state across ratecv calls in RealtimeWSAdapter
tara-bogavelli Aug 7, 2026
d70d93c
Fix critical drain bug and other issues in RealtimeWSAdapter
tara-bogavelli Aug 7, 2026
b18c630
add streaming STT client with transcript buffering
tara-bogavelli Aug 7, 2026
c141dd2
Add Cartesia TTS client for caller speech synthesis
tara-bogavelli Aug 7, 2026
c7970ce
add cascade caller turn JSON contract prompt
tara-bogavelli Aug 7, 2026
ff45644
add CascadeUserSimulator
tara-bogavelli Aug 7, 2026
086fee5
warn when cascade ignores unsupported audio perturbation
tara-bogavelli Aug 7, 2026
1436fc0
wire cascade simulator into the factory
tara-bogavelli Aug 7, 2026
3ee5f94
Enforce minimum tick duration in RealtimeWSAdapter.run_tick
tara-bogavelli Aug 7, 2026
8ef6187
Always send a full tick of audio in RealtimeWSAdapter, silence or real
tara-bogavelli Aug 7, 2026
c7755d8
fix cascade caller LLM role inversion and Scribe idle-close
tara-bogavelli Aug 7, 2026
281dace
add awaiting-reply gate to TickScheduler.may_take_turn
tara-bogavelli Aug 7, 2026
5cb14a9
Reconnect ScribeStreamingSTT on Scribe's max-session-duration close
tara-bogavelli Aug 7, 2026
39e9e4b
point cascade caller at a dedicated low-reasoning-effort deployment
tara-bogavelli Aug 7, 2026
713ee14
Replace the raw Scribe socket with a LiveKit STT plugin and stop stal…
tara-bogavelli Aug 7, 2026
9a25164
Emit audio_start/audio_end from the cascade simulator
tara-bogavelli Aug 7, 2026
80f5e80
End a stalled cascade conversation with inactivity_timeout
tara-bogavelli Aug 7, 2026
ea6a7fd
Fix uninitialised tick counters in the cascade simulator
tara-bogavelli Aug 7, 2026
ecae187
add out-of-turn behavior constants
tara-bogavelli Aug 7, 2026
89bbb64
add cascade behavior flags
tara-bogavelli Aug 7, 2026
2fefd2f
add check-tick predicate to the scheduler
tara-bogavelli Aug 7, 2026
8e2fc95
port tau interrupt and backchannel decision prompts
tara-bogavelli Aug 7, 2026
89d355e
add listener-reaction decision checks
tara-bogavelli Aug 7, 2026
fddb3dc
add pre-rendered phrase cache
tara-bogavelli Aug 7, 2026
0f664b8
add backchannel behavior
tara-bogavelli Aug 7, 2026
6d7bb39
add streaming synthesis to the TTS client
tara-bogavelli Aug 7, 2026
8d3680b
add reactive interruption with slip tracking
tara-bogavelli Aug 7, 2026
1f72553
add self-correction behavior
tara-bogavelli Aug 7, 2026
a2acc3f
add per-tick ambient noise mixing
tara-bogavelli Aug 7, 2026
90e29f2
add speculative interruption generation with relevance gate
tara-bogavelli Aug 7, 2026
cb834b7
stop a backchannel from consuming the caller's turn
tara-bogavelli Aug 8, 2026
7dba2ba
fix swallowed hang-up and unreachable self-correction in the cascade …
tara-bogavelli Aug 19, 2026
6d466ae
record ablation defect root causes and the corrected diagnosis for de…
tara-bogavelli Aug 19, 2026
9744888
gate barge-ins to a fraction of assistant turns, at most one each
tara-bogavelli Aug 19, 2026
1d139c0
give the interrupt decision the caller's goal
tara-bogavelli Aug 19, 2026
abd1a42
fix cumulative-silence timeout, turn boundaries, and orphaned openers
tara-bogavelli Aug 19, 2026
3cb17e3
remove the interrupt rate gate and rely on the goal-aware decision
tara-bogavelli Aug 19, 2026
4d08eba
add paced_output flag to the assistant server base
tara-bogavelli Aug 19, 2026
fcd7b68
honor paced_output and count user activity in audio deltas
tara-bogavelli Aug 19, 2026
a0eedc3
add playback-position truncation fields to TickResult
tara-bogavelli Aug 19, 2026
af64efa
add TickDrivenAdapter
tara-bogavelli Aug 19, 2026
4f5f16e
select the cascade adapter by assistant framework and plumb the pacin…
tara-bogavelli Aug 19, 2026
c1cf2e8
truncate provider audio at the played position on barge-in
tara-bogavelli Aug 19, 2026
526788b
record the tick-driven adapter working log
tara-bogavelli Aug 19, 2026
c0ffdba
make the tick-driven path actually run against a live provider
tara-bogavelli Aug 19, 2026
b3525ac
record ablation findings: 89% barge-in drop rate on the real-time path
tara-bogavelli Aug 20, 2026
74c4463
Merge branch 'worktree-user-sim-phase-3' into feat/cascade-user-simul…
tara-bogavelli Aug 20, 2026
07df88f
Merge remote-tracking branch 'origin/main' into feat/cascade-user-sim…
tara-bogavelli Aug 20, 2026
a099942
Remove temp files
tara-bogavelli Aug 21, 2026
dc5bf03
Accept paced_output on every assistant server
tara-bogavelli Aug 21, 2026
238748b
Add TranscriptBuffer.heard_text for transcript use
tara-bogavelli Aug 21, 2026
1f761dc
Report what each listener check did, not just its answer
tara-bogavelli Aug 21, 2026
d80618f
Add a decision trace for the cascade caller
tara-bogavelli Aug 21, 2026
1a6a2a6
Remove self-correction, retime barge-in staleness, trace caller decis…
tara-bogavelli Aug 21, 2026
f5dea15
Update README.md
gabegma Aug 21, 2026
8467d0a
add multilingual barge in, interruption
raghavm243512 Aug 25, 2026
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
9 changes: 9 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,15 @@ EVA_MODEL_LIST='[
"aws_secret_access_key": "os.environ/AWS_SECRET_ACCESS_KEY",
"max_parallel_requests": 5
}
},
{
"model_name": "user-llm",
"litellm_params": {
"model": "openai/gpt-5.5",
"api_key": "os.environ/OPENAI_API_KEY",
"max_parallel_requests": 5,
"reasoning_effort": "low"
}
}
]'

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,7 @@ output/<run_id>/
| **🎯 EVA-A · Accuracy** | **✨ EVA-X · Experience** |
|---|---|
| *Did the agent complete the task correctly?* | *Was the conversational experience high quality?* |
| **Task Completion** · Deterministic | **Turn Taking** · LLM Judge `BETA` |
| **Task Completion** · Deterministic | **Turn Taking** · Deterministic |
| **Agent Speech Fidelity** · Audio LLM Judge `BETA` | **Conciseness** · LLM Judge |
| **Faithfulness** · LLM Judge | **Conversation Progression** · LLM Judge |

Expand Down
69 changes: 69 additions & 0 deletions configs/caller_phrases.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
de:
backchannels:
- mhm
- aha
- ja
barge_in_openers:
- Moment—
- Entschuldigung—
- Warte—
- Also—
en:
backchannels:
- uh-huh
- mm-hmm
barge_in_openers:
- Wait—
- Sorry—
- Hold on—
- Actually—
es:
backchannels:
- ajá
- mmm
- ya
barge_in_openers:
- Espera
- Perdona
- Un momento
- Bueno
fr:
backchannels:
- hum hum
- oui
- d’accord
barge_in_openers:
- Attendez
- Pardon
- Juste
- En fait
fr-CA:
backchannels:
- hum hum
- ouais
- OK
barge_in_openers:
- Attends
- Excuse
- Juste
- En fait
hi:
backchannels:
- हूँ
- अच्छा
- हाँ
barge_in_openers:
- रुकिए
- सुनिए
- माफ़ कीजिए
- असल में
ko:
backchannels:
- 네
- 음
- 아
barge_in_openers:
- 잠깐만요
- 죄송한데
- 아니요
- 근데
143 changes: 143 additions & 0 deletions configs/prompts/simulation.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -507,3 +507,146 @@ user_simulator:
For languages that use non-Latin scripts, spell out characters using their standard phonetic names in your language.

IMPORTANT: Before ending the conversation, confirm with the agent that there are no outstanding actions. The end_call tool should only be called in a turn that is a brief goodbye — never in the same turn where you are providing the agent with data, an identifier, a request to transfer to a live agent, an approval to proceed, or any kind of additional information.

interruption_decision: |
You are analyzing a conversation to decide if the user should interrupt the agent.

This is what the user called to accomplish, so you can judge what they still have left
to do. The fields mean:

- GOAL: what the user is trying to achieve overall.
- MUST HAVE: non-negotiable requirements. The user will never accept an outcome that
fails any of these, and will not hang up until all of them are met.
- NICE TO HAVE: things the user wants but will give up if necessary.
- HOW THEY EVALUATE OPTIONS: the steps the user follows when the agent presents choices.
- RESOLVED WHEN: the success condition. Once this is met the user says a brief goodbye
and ends the call, so there is nothing left worth interrupting for.
- FAILED WHEN: the failure condition. This also ends the call.
- ESCALATION: how the user handles being transferred to a live agent.

<user_goal>
{user_goal}
</user_goal>

Conversation history (most recent at bottom):

<conversation_history>
{conversation_history}
</conversation_history>

The agent is CURRENTLY speaking (you can see their ongoing speech in the conversation above).

Based on the conversation so far, should the user interrupt the agent NOW?

Consider:
- Has the user heard enough to understand what the agent is asking or saying?
- Has the user heard enough to have a response, question, or correction ready?
- Did the agent just complete the sentence which has all the pertinent information the user was looking for?
- Do NOT repeatedly interrupt the agent if it has spoken only a few words (roughly fewer than 5 words in English, or the equivalent short fragment in whatever language the conversation is in — word counts are not comparable across languages).
- Are the user's request(s) basically accomplished and the user is likely to hang up on the next turn (if so it should not interrupt)?
- Is this a logical point in the conversation to interrupt?

Respond with ONLY "YES" if the user should interrupt now, or "NO" if they should keep listening.

backchannel_decision: |
You simulate a natural listener who occasionally makes a brief continuer sound to show they're following along.

The conversation may be in any language. Judge the FUNCTION of what is said, not the
presence of any particular word: every language has these sounds, and the caller draws
its own from a vocabulary configured for the conversation's language. The English
examples below illustrate the judgement, not the words to look for.

<conversation_history>
{conversation_history}
</conversation_history>

The agent is still speaking [CURRENTLY SPEAKING, INCOMPLETE]. Ignore the trailing incomplete word/phrase — focus only on the COMPLETE sentences delivered so far in the agent's current turn.

Continuers (English: "uh-huh", "mm-hmm", "yeah"; every language has equivalents) are brief
sounds that mean "I'm listening, keep going." They:
- Happen naturally during extended speech
- Show engagement without interrupting
- Are NOT responses to specific content — just signals of attention

Say YES if:
- The agent has completed at least 2 full, substantive sentences in their current turn
(Short phrases like "Thanks for your patience" or "Let me check on that" don't count as substantive)
- The user hasn't spoken or backchanneled recently (check the last 3 exchanges for ANY brief
acknowledgement sound from the user, in whatever language the conversation is in)
- It would feel natural to briefly signal "I'm still here"

Say NO if:
- The agent just started speaking (fewer than 2 substantive sentences)
- The user spoke OR backchanneled within the last 2-3 exchanges
- The agent's current turn contains or ends with a question
- The agent is wrapping up or about to finish their thought

Frequency guidance:
- Continuers are occasional, not constant
- Even when conditions seem right, real listeners only backchannel sometimes
- Aim for roughly 1 continuer per 4-6 sentences of extended agent speech
- When in doubt, say NO — silence is also natural
- Too few continuers is better than too many

Examples (English, for illustration — apply the same judgement in any language):

AGENT: "Hi there! How can I hel [CURRENTLY SPEAKING, INCOMPLETE]"
→ NO (just started)

AGENT: "Thanks for your patience. [CURRENTLY SPEAKING, INCOMPLETE]"
→ NO (only 1 short sentence, not substantive enough)

AGENT: "Sure, I can help with that. First I'll need to verify your account. Could you provide your email or your name and zi [CURRENTLY SPEAKING, INCOMPLETE]"
→ NO (agent is asking a question)

AGENT: "No problem. We can use your name and zip code instead. Let me look that up for you. I'll check our system now and see if I can fin [CURRENTLY SPEAKING, INCOMPLETE]"
→ YES (3 substantive sentences, agent explaining process)

AGENT: "I found your order. It includes a keyboard, thermostat, and headphones. The order was delivered last Tuesday. Now for the exchange, we have a few opti [CURRENTLY SPEAKING, INCOMPLETE]"
→ YES (extended explanation with specific details)

[If the user made a brief acknowledgement sound 2 exchanges ago]
AGENT: "...and those are the available options. Now I'll need your input on which [CURRENTLY SPEAKING, INCOMPLETE]"
→ NO (user backchanneled recently, don't do it again so soon)

Respond with ONLY "YES" or "NO".

cascade_next_interruption: |
You just said this line to the agent:

<what_you_just_said>
{utterance}
</what_you_just_said>

Write the single line you would say to cut the agent off mid-reply, if its
answer turns out to need correcting or clarifying.

Rules:
- Write it as a natural interruption, not a full turn. Short.
- It must follow from your own goal, not from any specific thing the agent
might say — you have not heard the reply yet.
- Write it in the same language as the line above; the caller does not switch
languages mid-call.
- Reply with ONLY the spoken line, nothing else. No quotes, no labels.
- If you would have no reason to interrupt whatever the agent says next,
reply with exactly NONE.

cascade_relevance_gate: |
The caller prepared this line a moment ago, before hearing the rest of what
the agent is currently saying:

<prepared_line>
{candidate}
</prepared_line>

Here is what the agent has said so far in its current turn:

<heard_so_far>
{heard}
</heard_so_far>

Would saying the prepared line right now still make sense as a natural
interruption? Answer NO if it has become irrelevant, already been addressed,
or would read as a non-sequitur.

Respond with ONLY "YES" or "NO".
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ dependencies = [
"azure-cognitiveservices-speech>=1.31.0",
"cartesia>=1.0.0",
"assemblyai>=0.17.0",
"livekit-agents>=1.6.8",
"livekit-plugins-elevenlabs>=1.6.8",
"setuptools>=65.0.0",
"fastapi>=0.100.0",
"uvicorn>=0.23.0",
Expand Down
68 changes: 68 additions & 0 deletions scripts/add_culture_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@
REPO_ROOT = Path(__file__).resolve().parent.parent
DATA_DIR = REPO_ROOT / "data"
INITIAL_MESSAGES_PATH = REPO_ROOT / "configs" / "agents" / "initial_messages.yaml"
CALLER_PHRASES_PATH = REPO_ROOT / "configs" / "caller_phrases.yaml"
WER_CONFIGS_DIR = REPO_ROOT / "src" / "eva" / "utils" / "wer_normalization" / "configs"

DEFAULT_MODEL = "gpt-5.5-2026-04-23"
Expand Down Expand Up @@ -353,6 +354,63 @@ def _update_initial_messages(language: str, message: str) -> None:
INITIAL_MESSAGES_PATH.write_text(yaml.safe_dump(existing, allow_unicode=True, sort_keys=True), encoding="utf-8")


async def _translate_caller_phrases(
language: str, language_name: str, llm: LLMClient, overwrite: bool = False
) -> dict[str, list[str]] | None:
"""Generate the simulated caller's out-of-turn vocabulary for ``language``.

These are the continuers and barge-in openers the caller speaks *as audio*, so
they must be things a native speaker actually says, not translations of the
English ones — "uh-huh" has no word-for-word equivalent in most languages.

Returns None when the language already has phrases and ``overwrite`` is unset.
"""
existing: dict[str, Any] = {}
if CALLER_PHRASES_PATH.exists():
existing = yaml.safe_load(CALLER_PHRASES_PATH.read_text(encoding="utf-8")) or {}
if language in existing and not overwrite:
return None

english = existing.get("en", {})
prompt = (
f"You are localising a simulated phone caller for {language_name}.\n\n"
"Produce two short lists of things the caller says out loud:\n\n"
"1. backchannels — brief continuer sounds meaning 'I am listening, keep going', "
"said while the other person is still talking. Give the sounds a native speaker "
"actually makes, not translations of English ones.\n"
"2. barge_in_openers — the very first word or two of an interruption, said just "
"before the interrupting sentence. Keep them to one or two words so they sound "
"like a real cut-in.\n\n"
"These are spoken aloud by a text-to-speech voice, so use ordinary spelling with "
"no stage directions, no parentheses and no transliteration hints. Give 2-3 "
"backchannels and 3-4 openers.\n\n"
f"For reference, the English set is: {json.dumps(english, ensure_ascii=False)}\n\n"
'Return JSON: {"backchannels": ["..."], "barge_in_openers": ["..."]}'
)
text, _ = await llm.generate_text(
[{"role": "user", "content": prompt}],
response_format={"type": "json_object"},
)
data = extract_and_load_json(text)
backchannels = [str(p).strip() for p in (data.get("backchannels") or []) if str(p).strip()]
openers = [str(p).strip() for p in (data.get("barge_in_openers") or []) if str(p).strip()]
if not backchannels or not openers:
raise ValueError(f"Caller phrase generation returned an incomplete result: {data!r}")
return {"backchannels": backchannels, "barge_in_openers": openers}


def _update_caller_phrases(language: str, phrases: dict[str, list[str]]) -> None:
"""Merge one language's phrase set into configs/caller_phrases.yaml."""
existing: dict[str, Any] = {}
if CALLER_PHRASES_PATH.exists():
existing = yaml.safe_load(CALLER_PHRASES_PATH.read_text(encoding="utf-8")) or {}
if existing.get(language) == phrases:
return
existing[language] = phrases
CALLER_PHRASES_PATH.parent.mkdir(parents=True, exist_ok=True)
CALLER_PHRASES_PATH.write_text(yaml.safe_dump(existing, allow_unicode=True, sort_keys=True), encoding="utf-8")


async def _translate_aliases(
name_to_base: dict[str, list[str]],
language_name: str,
Expand Down Expand Up @@ -680,6 +738,16 @@ async def amain(args: argparse.Namespace) -> int:
_update_initial_messages(args.language, initial_message)
logger.info(f"Updated {INITIAL_MESSAGES_PATH}")

logger.info(f"Generating caller out-of-turn phrases for {args.language_name}")
caller_phrases = await _translate_caller_phrases(args.language, args.language_name, llm, args.overwrite_all)
if caller_phrases is None:
logger.info(f"Caller phrases for {args.language} already present — skipping")
elif args.dry_run:
logger.info(f"[dry-run] would write caller phrases for {args.language}: {caller_phrases}")
else:
_update_caller_phrases(args.language, caller_phrases)
logger.info(f"Updated {CALLER_PHRASES_PATH}: {caller_phrases}")

for domain in domains:
logger.info(f"=== Domain: {domain} ===")
if domain == "airline":
Expand Down
2 changes: 1 addition & 1 deletion src/eva/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

# Bump simulation_version when changes affect benchmark outputs (agent code,
# user simulator, orchestrator, simulation prompts, agent configs, tool mocks).
simulation_version = "2.0.2"
simulation_version = "2.0.34"

# Bump metrics_version when changes affect metric computation (metrics code,
# judge prompts, pricing tables, postprocessor).
Expand Down
20 changes: 20 additions & 0 deletions src/eva/assistant/base_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,15 @@ class AbstractAssistantServer(ABC):
5. Populate the AuditLog with conversation events
"""

supports_unpaced_output: bool = False
"""Whether this server can honor ``paced_output=False``.

Only a server whose outbound relay throttle we own can drop it. A server whose
pacing lives inside a third-party runtime cannot, and must reject the request
rather than accept it and keep pacing, which would leave a tick-driven caller
believing the assistant was unpaced.
"""

def __init__(
self,
current_date_time: str,
Expand All @@ -52,6 +61,7 @@ def __init__(
port: int,
conversation_id: str,
language: str = "en",
paced_output: bool = True,
):
"""Initialize the assistant server.

Expand All @@ -65,10 +75,20 @@ def __init__(
port: Port to listen on
conversation_id: Unique ID for this conversation
language: BCP 47 language tag for STT/TTS/S2S services (e.g. 'en', 'fr', 'es-MX')
paced_output: Whether to emit audio at real-time cadence. True for callers
that infer turn boundaries from silence timing (the ElevenLabs simulator).
False for a tick-driven caller, which buffers whatever arrives and
releases it one tick at a time, so pacing here would only add latency.
"""
self.current_date_time = current_date_time
self.pipeline_config = pipeline_config
self.language = language
if not paced_output and not self.supports_unpaced_output:
raise ValueError(
f"{type(self).__name__} cannot honor paced_output=False: its output pacing is not ours to remove. "
"Tick-driving this framework requires leaving pacing on."
)
self.paced_output = paced_output
self.initial_message = get_initial_message(language)
self.agent: AgentConfig = agent
self.agent_config_path = agent_config_path
Expand Down
Loading