> This page is for version Nightly (default).
> For other versions, use one of these documentation indexes:
> - Nightly (default): https://docs.nvidia.com/nemo/labs-voice-agent/nightly/llms.txt

> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.nvidia.com/nemo/labs-voice-agent/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.nvidia.com/nemo/labs-voice-agent/_mcp/server.

# nemo_voice_agent.evaluation.scenarios.data.tau2_telecom.base

Tau2-telecom scenario base.

`Tau2TelecomBaseScenario` extends `Tau2BaseScenario` with three
telecom-specific concerns the airline/retail bases don't share:

1. **Dual DB.** A separate user-side `TelecomUserDB` (mock phone state +
   user surroundings) lives alongside the agent-side `TelecomDB`.
   `has_user_state = True` triggers user-side seeding in
   `_gold_replay` and tells the bridge to dual-pull.
2. **Multi-file policy.** The agent system prompt is two files concatenated:
   `main_policy.md` + `tech_support_&#123;manual,workflow&#125;.md`. We
   override `policy` to do the concat using a markdown horizontal-rule
   separator (no XML tags — matches airline/retail's single-markdown
   convention; both upstream files are well-formed standalone markdown).
3. **`db_state_assertions` + `initialization_actions` translation.**
   Upstream uses `env_type ∈ &#123;"user", "assistant"&#125;` on both fields;
   the registry/framework uses `side ∈ &#123;"user", "agent"&#125;`. We translate
   at scenario load time so the runner / bridge / registries all see a
   uniform shape. The helper lives in `tau2_common` for reuse if
   another tau2 domain adds these fields.

Subclasses only need to set `name` and `tau2_id`; everything else
derives from the upstream task JSON.

## Module Contents

### Classes

| Name                                                                                                                               | Description                                                                    |
| ---------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
| [`Tau2TelecomBaseScenario`](#nemo_voice_agent-evaluation-scenarios-data-tau2_telecom-base-Tau2TelecomBaseScenario)                 | Base class for scenarios ported from tau2-bench/telecom (voice-user-sim-v1.0). |
| [`Tau2TelecomWorkflowBaseScenario`](#nemo_voice_agent-evaluation-scenarios-data-tau2_telecom-base-Tau2TelecomWorkflowBaseScenario) | Workflow policy variant of `Tau2TelecomBaseScenario`.                          |

### Data

[`TELECOM_AGENT_HOME_NETWORK_NOTE`](#nemo_voice_agent-evaluation-scenarios-data-tau2_telecom-base-TELECOM_AGENT_HOME_NETWORK_NOTE)

[`TELECOM_AGENT_STAY_ON_TASK_GUIDELINE`](#nemo_voice_agent-evaluation-scenarios-data-tau2_telecom-base-TELECOM_AGENT_STAY_ON_TASK_GUIDELINE)

[`TELECOM_AGENT_TOOL_AVAILABILITY_NOTE`](#nemo_voice_agent-evaluation-scenarios-data-tau2_telecom-base-TELECOM_AGENT_TOOL_AVAILABILITY_NOTE)

[`TELECOM_PASSIVE_TOOL_USE_GUIDELINE`](#nemo_voice_agent-evaluation-scenarios-data-tau2_telecom-base-TELECOM_PASSIVE_TOOL_USE_GUIDELINE)

[`TELECOM_USER_INSTRUCTIONS`](#nemo_voice_agent-evaluation-scenarios-data-tau2_telecom-base-TELECOM_USER_INSTRUCTIONS)

### API

```python
class nemo_voice_agent.evaluation.scenarios.data.tau2_telecom.base.Tau2TelecomBaseScenario()
```

**Bases:** [Tau2BaseScenario](/nemo/labs-voice-agent/nemo-voice-agent/nemo_voice_agent/evaluation/scenarios/data/tau2_common#nemo_voice_agent-evaluation-scenarios-data-tau2_common-Tau2BaseScenario)

Base class for scenarios ported from tau2-bench/telecom (voice-user-sim-v1.0).

Subclasses only declare `name` and `tau2_id` (the upstream task id
string). For example::

@register_eval_scenario
class Tau2TelecomMobileDataIssueAirplaneOnDataOff(Tau2TelecomBaseScenario):
name = "tau2_telecom\_\_mobile_data_issue\_\_airplane_mode_on\_\_data_mode_off"
tau2_id = "\[mobile_data_issue]airplane_mode_on|data_mode_off\[PERSONA:None]"

**`agent_resources`**

Agent-side resources — full tau2_telecom tool surface + `EndConversationTool`.

OVERRIDES `Tau2BaseScenario.agent_resources`. Tool keys are
snake_case `cls.name` strings matching upstream method names —
the same identifiers the policy.md text references (e.g.
`get_customer_by_phone`, `send_payment_request`). Single
source of truth: the same key drives the bot's
`tool_factory` registry lookup, the LLM-visible function
name, and gold-replay dispatch in `_build_tool_map`.

`EndConversationTool` is the voice-harness termination signal
(registered under `"default"` in the global registry; the
`get_schema_tool_for_eval` lookup falls back from
`"tau2_telecom"` to `"default"` for shared harness tools).
It uses its PascalCase registry key here intentionally — that's
how it's registered.

---

**`db_state_assertions`**

Per-task `db_state_assertions` records, translated from
upstream's `env_assertions`.

Reads `evaluation_criteria.env_assertions` and runs each record
through `_normalize_env_record` to rename `env_type → side` +
`"assistant" → "agent"`. Returns `None` (not `[]`) when the
task has no assertions so the runner's `if scenario.db_state_assertions:`
guard correctly skips verdict aggregation.

---

**`domain`**

---

**`has_user_state`**

---

**`initialization_actions`**

Per-task `initialization_actions` records, translated from upstream.

Source: `initial_state.initialization_actions`. Same translation
as `db_state_assertions` — `env_type → side`, ``"assistant"
→ "agent"`. Returns `None`` when the task has none.

---

**`nl_assertions`**

Per-task `nl_assertions` for the LLM judge. `None` when absent.

---

**`policy`**

Concatenate `main_policy.md` + `tech_support_&#123;variant&#125;.md`.

Both files are well-formed standalone markdown (their own `#` /
`##` / `###` header hierarchies). We join with a markdown
horizontal-rule separator so the LLM has a clean section
boundary without XML tags. The upstream telecom wraps each file
in `&lt;main_policy&gt;`/`&lt;tech_support_policy&gt;` tags; we drop
those because (a) airline / retail's single-markdown-string
convention doesn't use XML, (b) markdown structure already
provides hierarchy, (c) it saves \~30 tokens per request.

---

**`policy_variant`**

---

**`success_signals`**

Predicate-only gating; whole-DB hash and action match are informational.

Telecom has an open solution space — different valid action
sequences land in different whole-DB states but satisfy the
same outcome predicates. `DB_STATE_ASSERTION` (per-predicate,
intent-focused) is the authoritative outcome signal. `NL_ASSERTION`
is added when a task opts in. `DB_STATE_MATCH` and `ACTION_MATCH`
are computed and saved for diagnostics but DO NOT gate the verdict
(they appear in `success_breakdown.excluded`). `JUDGE_PASSED`
(overall) is also non-gating — when the judge is enabled it
contributes per-NL-assertion verdicts via `NL_ASSERTION`.

---

**`user_actions`**

User-side actions — telecom-specific procedural script + passive-tool-use constraint.

OVERRIDES `Tau2BaseScenario.user_actions` (which returns just the
`VOICE_ALPHANUMERIC_RULE` guideline). Telecom is the first dual-side
domain whose user-sim has LLM-callable tools, and the default
tau2 `task_instructions` (preserved in `user_persona.personality`)
was written for text-mode where the user-sim chains tool calls
inside one turn. In voice mode the user-sim has to talk to the
agent first — hence the explicit script + constraint here.

* `instructions` carry the procedural sequence (describe →
  follow → report → close). The model uses these to anchor
  itself in the conversational rhythm.
* `guidelines` carry behavioral constraints (passive tool
  use, voice-readable identifiers). Stay constraint-shaped so
  the model can check any single turn against them.

Inherits the parent's guidelines (`VOICE_ALPHANUMERIC_RULE`)
and appends the telecom-specific passive-tool-use rule.

---

**`user_db`**

Initial user-side DB state — loaded once per process from `user_db.json`.

The Pydantic round-trip in `scripts/prepare_tau2_data/prepare_telecom.py`
materializes default fields (`surroundings` block,
`signal_strength` per-network table, `app_statuses` defaults)
that the raw TOML doesn't carry, so downstream init / predicate
code can read fields via dict access without defensive
`.get(default)` calls.

`setup_shared_state` deepcopies this when seeding user-side
state so per-scenario mutations don't leak.

---

**`user_resources`**

User-side resources — phone-control LLM tools + `known_info` /
`unknown_info` from the parent.

OVERRIDES the parent's user_resources (which returns the same
info-sections but with empty `tools=&#123;&#125;`). Telecom is the
first domain that populates `user_resources.tools` — the
user-sim's LLM needs phone-control tools (toggle_data,
toggle_airplane_mode, etc.) to actually act on the device the
agent is troubleshooting.

Tool kwargs are empty because tau2 tools take only
`shared_state` which the bot server injects automatically.

---

```python
nemo_voice_agent.evaluation.scenarios.data.tau2_telecom.base.Tau2TelecomBaseScenario._build_tool_map(
    state: dict
) -> typing.Dict[str, typing.Any]
```

Tool name → instance map for BOTH user-side and agent-side tools.

Consumed by `Tau2BaseScenario._gold_replay` to dispatch each
reference action by name. Telecom is the first dual-side
domain, so the map must cover both surfaces:

* **User-side tools** operate on the user_db. In gold-replay
  the same `state` carries both `state["db"]` (agent's
  TelecomDB) and `state["user_db"]` (user's TelecomUserDB).
  We bridge by wrapping `state` in a user-view dict where
  `state["db"]` aliases `state["user_db"]` (gold replay)
  or falls back to `state["db"]` (live runtime, where the
  user bot's state\["db"] IS the user_db).
* **Agent-side tools** operate on the agent TelecomDB
  directly via `state["db"]` — no view wrapping needed.

The shared `state["actions"]` list collects both sides' write
records; the gold-replay caller (`Tau2BaseScenario._gold_replay`)
side-tags them after replay so the runner's comparator sees
per-side action streams that align with the per-action `side`
field in `reference_answer`.

Tool-name collisions between sides are impossible — upstream's
user-side and agent-side surfaces have disjoint method names
(user: `toggle_*` / `check_*` / `run_speed_test`; agent:
`get_*` / `send_payment_request` / `refuel_data` etc.).

```python
nemo_voice_agent.evaluation.scenarios.data.tau2_telecom.base.Tau2TelecomBaseScenario.get_agent_prompt() -> str
```

Parent's policy + voice notes + telecom-specific addenda.

Appends three telecom-only blocks to the parent's
`policy + Additional Notes` output:

* `TELECOM_AGENT_TOOL_AVAILABILITY_NOTE`: explicit enumeration
  of which tools the agent CAN call vs which live on the user's
  phone. Substitutes the actual snake_case tool names so the
  model can map policy.md references unambiguously.
* `TELECOM_AGENT_STAY_ON_TASK_GUIDELINE`: prevents drift to
  unrelated discovered issues (e.g. overdue bills).
* `TELECOM_AGENT_HOME_NETWORK_NOTE`: states the home country
  (US) and frontloads the "check roaming first when abroad"
  diagnostic rule — disambiguates user-reported locations
  ("France", "overseas") that policy.md mentions only
  generically as "outside the home network".

See module-level docstrings on the three constants for the
upstream-vs-voice-mode structural reasoning.

```python
nemo_voice_agent.evaluation.scenarios.data.tau2_telecom.base.Tau2TelecomBaseScenario.setup_shared_state(
    state: dict,
    side: str
) -> None
```

Seed `state["db_path"]` per side (live-runtime path).

* `side="agent"` → `tau2_telecom/db.json` (TelecomDB)
* `side="user"`  → `tau2_telecom/user_db.json` (TelecomUserDB)

Path-based seeding (not inline DB) — the bot-server's
`rtvi_actions.create_update_system_prompt_action` handler
resolves the path against `EVAL_DATA_ROOT` and loads the file
into `shared_state["db"]` on the bot side. Each bot ends up
with its own DB at `state["db"]` (per-bot single-DB
convention).

Gold replay runs in-process and bypasses this method's
user-side branch — the parent class's `_gold_replay`
inline-loads `self.user_db` directly into `gold_state["user_db"]`.

```python
nemo_voice_agent.evaluation.scenarios.data.tau2_telecom.base.Tau2TelecomBaseScenario.sync_state(
    agent_db: dict,
    user_db: dict
) -> typing.Dict[str, typing.Dict[str, typing.Any]]
```

Delegate to the pure `sync_telecom_state` function.

Called by the bridge after each write action on either bot. The
actual reconciliation logic — propagating `line.status`,
`line.roaming_enabled`, data-usage, and payment-request state
between the two DBs — lives in `sync_telecom_state` so it can
be unit-tested without bridge or scenario plumbing.

See `Scenario.sync_state` for the framework contract and
`sync_telecom_state`'s docstring for the propagation paths.

```python
class nemo_voice_agent.evaluation.scenarios.data.tau2_telecom.base.Tau2TelecomWorkflowBaseScenario()
```

**Bases:** [Tau2TelecomBaseScenario](#nemo_voice_agent-evaluation-scenarios-data-tau2_telecom-base-Tau2TelecomBaseScenario)

Workflow policy variant of `Tau2TelecomBaseScenario`.

Mirrors upstream tau2's `--domain telecom-workflow` registration:
same task set, same reference actions, same predicates, same
initialization actions, same agent / user tools, same data files.
The ONLY difference is which tech_support policy file gets
concatenated into the agent prompt — `tech_support_workflow.md`
(procedural step-by-step) instead of `tech_support_manual.md`
(long-form documentation).

`domain` stays `"tau2_telecom"` (inherited) so tool registry
lookup, data-file paths (`nemo_voice_agent/evaluation/data/tau2_telecom/`),
sync-applier dispatch, and predicate / init-function registries
are all unchanged. Only `scenario.name` (the
organizational-bucket key derived from
`name.split("__")[0]`) and `policy_variant` (the policy file
suffix) differ.

Subclasses set `name` and `tau2_id` exactly as for the manual
variant — the scaffold generator emits both classes per upstream
task:

@register_eval_scenario
class Tau2TelecomMobileDataIssueDataModeOff(Tau2TelecomBaseScenario):
name = "tau2_telecom\_\_mobile_data_issue\_\_data_mode_off"
tau2_id = "\[mobile_data_issue]data_mode_off\[PERSONA:None]"

@register_eval_scenario
class Tau2TelecomWorkflowMobileDataIssueDataModeOff(Tau2TelecomWorkflowBaseScenario):
name = "tau2_telecom_workflow\_\_mobile_data_issue\_\_data_mode_off"
tau2_id = "\[mobile_data_issue]data_mode_off\[PERSONA:None]"

Both variants resolve identical `expected_scenario_db` /
`reference_answer` / `db_state_assertions` /
`initialization_actions` from the upstream task — same scoring
contract — so a side-by-side run yields a clean A/B comparison of
which policy-prose variant produces better agent behavior.

**`policy_variant`**

---

```python
nemo_voice_agent.evaluation.scenarios.data.tau2_telecom.base.TELECOM_AGENT_HOME_NETWORK_NOTE = 'Home network context + location-probe rule. This telecom company operates in th...
```

```python
nemo_voice_agent.evaluation.scenarios.data.tau2_telecom.base.TELECOM_AGENT_STAY_ON_TASK_GUIDELINE = "Stay on the user's stated problem. Address the PRIMARY issue the user reported ...
```

```python
nemo_voice_agent.evaluation.scenarios.data.tau2_telecom.base.TELECOM_AGENT_TOOL_AVAILABILITY_NOTE = 'Tool availability for THIS voice scenario. The policy text above references man...
```

```python
nemo_voice_agent.evaluation.scenarios.data.tau2_telecom.base.TELECOM_PASSIVE_TOOL_USE_GUIDELINE: str = 'Passive tool use only: never call a tool unless the agent has just instructed y...
```

```python
nemo_voice_agent.evaluation.scenarios.data.tau2_telecom.base.TELECOM_USER_INSTRUCTIONS: List[str] = ["Greet the agent and explain your issue in natural language. Describe the sympt...
```