Skip to content
Open
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
19 changes: 13 additions & 6 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -97,22 +97,29 @@ EVA_MODEL__TTS=cartesia
#x pipeline_mode=LLM,AudioLLM
EVA_MODEL__TTS_PARAMS='{"api_key": "your_cartesia_api_key", "model": "sonic"}'

#i TTS gender for the voice pipeline. For use in gendered languages to ensure the LLM generates speech
#i which matches the gender of the TTS voice.
#d enum
#e M,F,none
#x pipeline_mode=LLM,AudioLLM
#v EVA_MODEL__ASSISTANT_GENDER=none

# --- LLM mode: cascade latency optimizations ---
#i Prompt a model-generated lead-in before a tool call: 'off' or 'auto'.
#d enum
#e off,auto
#x pipeline_mode=LLM
# EVA_MODEL__PRE_TOOL_SPEECH=off
#x pipeline_mode=LLM,AudioLLM
#v EVA_MODEL__PRE_TOOL_SPEECH=off

#i Stream Chat Completions output to TTS sentence-by-sentence; Responses API falls back with a warning.
#d bool
#x pipeline_mode=LLM
# EVA_MODEL__LLM_STREAMING=false
#x pipeline_mode=LLM,AudioLLM
#v EVA_MODEL__LLM_STREAMING=false

#i Forward provider parallel_tool_calls when tools are present; leave unset for defaults.
#d bool
#x pipeline_mode=LLM
# EVA_MODEL__PARALLEL_TOOL_CALLS=false
#x pipeline_mode=LLM,AudioLLM
#v EVA_MODEL__PARALLEL_TOOL_CALLS=false

# --- S2S mode ---
#i Speech-to-speech model name.
Expand Down
6 changes: 5 additions & 1 deletion configs/prompts/simulation.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,13 @@ agent:

pre_tool_speech: |
## Responsiveness
Before calling a tool, say one brief sentence first. For lookups, use a short lead-in (e.g. "Let me pull that up."). Before any action that is irreversible or has real-world consequences, state what you'll do, get confirmation, then call the tool.
Before calling a tool, say one brief sentence first. For lookups, use a short lead-in (e.g. "Let me pull that up."). Before any action that is irreversible or has real-world consequences, state what you'll do, get confirmation, then call the tool.
If calling multiple tools at once, only one lead-in is needed.

gender_instruction: |
## Speaking Gender
You are speaking as a {gender_word} voice. In languages with grammatical gender, consistently use {gender_word} forms (verb conjugations, adjectives, self-references) for yourself throughout the conversation.

audio_llm_agent:
system_prompt: |
You are an AI voice assistant on a live phone call.
Expand Down
8 changes: 8 additions & 0 deletions src/eva/assistant/agentic/audio_llm_system.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ def __init__(
pre_tool_speech: str = "off",
llm_streaming: bool = False,
full_audio_context: bool = False,
assistant_gender: str | None = None,
):
super().__init__(
current_date_time=current_date_time,
Expand All @@ -52,6 +53,7 @@ def __init__(
output_dir=output_dir,
pre_tool_speech=pre_tool_speech,
llm_streaming=llm_streaming,
assistant_gender=assistant_gender,
)
self.alm_client: BaseALMClient = alm_client
# When True, every user turn is sent as audio (full audio context). When False
Expand All @@ -69,6 +71,12 @@ def __init__(
# Reuse the shared pre-tool lead-in prompt appended to the system prompt.
if self.pre_tool_speech == "auto":
self.system_prompt += "\n\n" + self.prompt_manager.get_prompt("agent.pre_tool_speech")
# Reuse the shared gender instruction appended to the system prompt.
if self.assistant_gender is not None:
gender_word = "male" if self.assistant_gender == "M" else "female"
self.system_prompt += "\n\n" + self.prompt_manager.get_prompt(
"agent.gender_instruction", gender_word=gender_word
)

# Per-turn audio history: list of (audio_bytes, sample_rate)
# One entry per user turn, aligned 1:1 with the user messages in the conversation history
Expand Down
9 changes: 9 additions & 0 deletions src/eva/assistant/agentic/system.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ def __init__(
output_dir: Path | None = None, # Output directory for performance stats
pre_tool_speech: str = "off",
llm_streaming: bool = False,
assistant_gender: str | None = None,
):
"""Initialize the agentic system.

Expand All @@ -118,6 +119,8 @@ def __init__(
output_dir: Optional output directory for saving performance stats
pre_tool_speech: Lead-in mode ('off'|'auto')
llm_streaming: Stream LLM output sentence-by-sentence
assistant_gender: Speaking gender for the assistant ('M'|'F'); None adds no
gender instruction (for gender-neutral languages like English)
"""
self.agent = agent
self.tool_handler = tool_handler
Expand All @@ -127,6 +130,7 @@ def __init__(
self.current_date_time = current_date_time
self.pre_tool_speech = pre_tool_speech
self.llm_streaming = llm_streaming
self.assistant_gender = assistant_gender
self._warned_responses_streaming_fallback = False

self.prompt_manager = PromptManager()
Expand All @@ -143,6 +147,11 @@ def __init__(
)
if self.pre_tool_speech == "auto":
self.system_prompt += "\n\n" + self.prompt_manager.get_prompt("agent.pre_tool_speech")
if self.assistant_gender is not None:
gender_word = "male" if self.assistant_gender == "M" else "female"
self.system_prompt += "\n\n" + self.prompt_manager.get_prompt(
"agent.gender_instruction", gender_word=gender_word
)
# Build tools for the LLM
self.tools = agent.build_tools_for_agent()

Expand Down
2 changes: 2 additions & 0 deletions src/eva/assistant/pipecat_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,7 @@ async def _realtime_tool_handler(params) -> None:
pre_tool_speech=self.pipeline_config.pre_tool_speech,
llm_streaming=self.pipeline_config.llm_streaming,
full_audio_context=self.pipeline_config.audio_llm_params.get("full_audio_context", False),
assistant_gender=self.pipeline_config.assistant_gender,
)
audio_llm_processor.on_assistant_response = lambda msg: self._save_transcript_message_from_turn(
role="assistant", content=msg, timestamp=self._current_iso_timestamp()
Expand Down Expand Up @@ -444,6 +445,7 @@ async def on_user_transcription(text: str, timestamp: str, turn_id: int | None)
output_dir=self.output_dir,
pre_tool_speech=self.pipeline_config.pre_tool_speech,
llm_streaming=self.pipeline_config.llm_streaming,
assistant_gender=self.pipeline_config.assistant_gender,
)

async def on_assistant_response(msg: str) -> None:
Expand Down
4 changes: 4 additions & 0 deletions src/eva/assistant/pipeline/agent_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ def __init__(
output_dir=None,
pre_tool_speech: str = "off",
llm_streaming: bool = False,
assistant_gender: str | None = None,
**kwargs,
) -> None:
"""Initialize the agent processor.
Expand All @@ -78,6 +79,8 @@ def __init__(
output_dir: Optional output directory for saving performance stats
pre_tool_speech: Lead-in mode ('off'|'auto')
llm_streaming: Stream LLM output sentence-by-sentence
assistant_gender: Speaking gender for the assistant ('M'|'F'); None adds no
gender instruction
**kwargs: Additional keyword arguments passed to FrameProcessor

The turn-end fallback timer is hosted by ``UserObserver`` (on the pipeline spine), which
Expand All @@ -101,6 +104,7 @@ def __init__(
output_dir=output_dir,
pre_tool_speech=pre_tool_speech,
llm_streaming=llm_streaming,
assistant_gender=assistant_gender,
)

# State tracking
Expand Down
2 changes: 2 additions & 0 deletions src/eva/assistant/pipeline/audio_llm_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ def __init__(
pre_tool_speech: str = "off",
llm_streaming: bool = False,
full_audio_context: bool = False,
assistant_gender: str | None = None,
**kwargs,
) -> None:
super().__init__(**kwargs)
Expand All @@ -216,6 +217,7 @@ def __init__(
pre_tool_speech=pre_tool_speech,
llm_streaming=llm_streaming,
full_audio_context=full_audio_context,
assistant_gender=assistant_gender,
)

# State tracking (mirrors BenchmarkAgentProcessor)
Expand Down
19 changes: 19 additions & 0 deletions src/eva/models/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,20 @@ def _normalize_pre_tool_speech(cls, value: str) -> str:
None,
description="Forward parallel_tool_calls when tools are present; None leaves provider defaults.",
)
assistant_gender: Literal["M", "F"] | None = Field(
None,
description=(
"Assistant speaking gender ('M' or 'F'), appended as a small system prompt "
"instruction so gendered languages (e.g. Hindi) produce grammatically consistent "
"speech. CASCADE/AUDIO_LLM only — S2S providers manage their own voice/gender. "
"None (default) adds no instruction, for gender-neutral languages like English."
),
)

@field_validator("assistant_gender", mode="before")
@classmethod
def _normalize_assistant_gender(cls, value: Any) -> Any:
return value.upper() if isinstance(value, str) else value

@property
def pipeline_type(self) -> "PipelineType":
Expand Down Expand Up @@ -308,6 +322,11 @@ def _validate_latency_optimizations(self) -> "ModelConfig":
"parallel_tool_calls applies only to the CASCADE pipeline; it will be ignored "
f"for pipeline_type={self.pipeline_type}."
)
if self.assistant_gender is not None and self.pipeline_type == PipelineType.S2S:
logger.warning(
"assistant_gender applies only to CASCADE/AUDIO_LLM pipelines; it will be "
"ignored for pipeline_type=s2s (S2S providers manage their own voice/gender)."
)
return self


Expand Down
Loading