diff --git a/api/services/pipecat/pre_call_fetch.py b/api/services/pipecat/pre_call_fetch.py index 7776111..8a2025b 100644 --- a/api/services/pipecat/pre_call_fetch.py +++ b/api/services/pipecat/pre_call_fetch.py @@ -15,6 +15,29 @@ from api.utils.credential_auth import build_auth_header PRE_CALL_FETCH_TIMEOUT_SECONDS = 10 +def _extract_initial_context(response_data: Dict[str, Any]) -> Dict[str, Any]: + """Pull the context variables out of a pre-call fetch response. + + The canonical key is ``initial_context``. The legacy ``dynamic_variables`` + key is still accepted for backward compatibility, so existing endpoints + keep working; ``initial_context`` takes precedence when both are present. + + Either key may appear at the top level or nested under ``call_inbound``: + {"call_inbound": {"initial_context": {...}}} | {"initial_context": {...}} + {"call_inbound": {"dynamic_variables": {...}}} | {"dynamic_variables": {...}} + """ + container = response_data.get("call_inbound") + if not isinstance(container, dict): + container = response_data + + for key in ("initial_context", "dynamic_variables"): + value = container.get(key) + if isinstance(value, dict): + return value + + return {} + + async def execute_pre_call_fetch( *, url: str, @@ -77,24 +100,16 @@ async def execute_pre_call_fetch( ) return {} - # Extract dynamic_variables from Retell-compatible response - # Supports: {call_inbound: {dynamic_variables: {...}}} - # or: {dynamic_variables: {...}} - dynamic_vars = {} - call_inbound = response_data.get("call_inbound") - if isinstance(call_inbound, dict): - dynamic_vars = call_inbound.get("dynamic_variables", {}) - elif "dynamic_variables" in response_data: - dynamic_vars = response_data["dynamic_variables"] - - if not isinstance(dynamic_vars, dict): - dynamic_vars = {} + # Extract the variables to merge into initial_context. Prefers + # the canonical `initial_context` key, falling back to the + # legacy `dynamic_variables` key for backward compatibility. + initial_context_vars = _extract_initial_context(response_data) logger.info( f"Pre-call fetch: success ({response.status_code}), " - f"dynamic_variables keys: {list(dynamic_vars.keys())}" + f"initial_context keys: {list(initial_context_vars.keys())}" ) - return dynamic_vars + return initial_context_vars else: logger.warning( f"Pre-call fetch: HTTP {response.status_code} - " diff --git a/api/tests/test_pre_call_fetch.py b/api/tests/test_pre_call_fetch.py new file mode 100644 index 0000000..8016da2 --- /dev/null +++ b/api/tests/test_pre_call_fetch.py @@ -0,0 +1,66 @@ +from api.services.pipecat.pre_call_fetch import _extract_initial_context + + +class TestExtractInitialContext: + """Tests for _extract_initial_context, the pre-call fetch response parser.""" + + def test_initial_context_nested_under_call_inbound(self): + """The canonical `initial_context` key nested under `call_inbound`.""" + response = {"call_inbound": {"initial_context": {"customer_name": "Jane"}}} + assert _extract_initial_context(response) == {"customer_name": "Jane"} + + def test_initial_context_at_top_level(self): + """The canonical `initial_context` key at the top level.""" + response = {"initial_context": {"customer_name": "Jane"}} + assert _extract_initial_context(response) == {"customer_name": "Jane"} + + def test_legacy_dynamic_variables_nested(self): + """The legacy `dynamic_variables` key still works nested under `call_inbound`.""" + response = {"call_inbound": {"dynamic_variables": {"customer_name": "Jane"}}} + assert _extract_initial_context(response) == {"customer_name": "Jane"} + + def test_legacy_dynamic_variables_at_top_level(self): + """The legacy `dynamic_variables` key still works at the top level.""" + response = {"dynamic_variables": {"customer_name": "Jane"}} + assert _extract_initial_context(response) == {"customer_name": "Jane"} + + def test_initial_context_takes_precedence_over_legacy(self): + """When both keys are present, `initial_context` wins.""" + response = { + "call_inbound": { + "initial_context": {"source": "new"}, + "dynamic_variables": {"source": "legacy"}, + } + } + assert _extract_initial_context(response) == {"source": "new"} + + def test_falls_back_to_legacy_when_initial_context_not_a_dict(self): + """A non-dict `initial_context` falls back to `dynamic_variables`.""" + response = { + "initial_context": None, + "dynamic_variables": {"customer_name": "Jane"}, + } + assert _extract_initial_context(response) == {"customer_name": "Jane"} + + def test_nested_values_preserved(self): + """Nested objects pass through untouched for dot-notation access.""" + response = { + "call_inbound": { + "initial_context": {"customer": {"address": {"city": "LA"}}} + } + } + assert _extract_initial_context(response) == { + "customer": {"address": {"city": "LA"}} + } + + def test_empty_when_no_known_keys(self): + """A response with neither key yields an empty dict.""" + assert _extract_initial_context({"call_inbound": {"agent_id": 1}}) == {} + + def test_empty_when_call_inbound_missing(self): + """No `call_inbound` and no top-level keys yields an empty dict.""" + assert _extract_initial_context({}) == {} + + def test_non_dict_vars_yield_empty(self): + """A non-dict value under a known key yields an empty dict.""" + assert _extract_initial_context({"initial_context": "nope"}) == {} diff --git a/docs/core-concepts/context-and-variables.mdx b/docs/core-concepts/context-and-variables.mdx index bfd81b0..274689c 100644 --- a/docs/core-concepts/context-and-variables.mdx +++ b/docs/core-concepts/context-and-variables.mdx @@ -18,20 +18,10 @@ initial_context ──► Agent ──► gathered_context Data available to the agent before the call starts — the contact's name, account details, appointment information, anything the agent should know upfront. It can be set from several places: -- **API trigger** — pass it in the request body when calling `POST /public/agent/{uuid}` or `POST /telephony/initiate-call` -- **Campaign CSV** — columns beyond `phone_number` automatically become `initial_context` fields for each contact's call -- **Dashboard** — set default template context variables on the agent, used when no external context is provided - -```json -{ - "phone_number": "+14155550100", - "initial_context": { - "customer_name": "Jane Smith", - "plan": "premium", - "renewal_date": "April 1" - } -} -``` +- **[API trigger](/voice-agent/api-trigger)** — pass it in the request body when calling `POST /public/agent/{uuid}` or `POST /telephony/initiate-call` +- **[Campaign CSV](/core-concepts/campaigns)** — columns beyond `phone_number` automatically become `initial_context` fields for each contact's call +- **[Pre-call data fetch](/voice-agent/pre-call-data-fetch)** — enrich the context with data from your CRM or ERP via an HTTP call as the call starts, before the agent speaks +- **[Agent Settings](/voice-agent/template-variables#using-template-variables-for-testing)** — set template context variables on the agent for testing; they're included in test calls from the workflow editor and ignored on production calls ### Template variables @@ -103,7 +93,7 @@ Data the agent collects *during* the call. You configure what to extract in the Extracted variables -`gathered_context` is returned in the run record after the call completes and is available in [webhook payloads](/developer/webhooks) for downstream processing. +`gathered_context` is returned in the run record after the call completes and is available in [webhook payloads](/developer/webhooks) for downstream processing. It is **not** available as a template variable in Agent prompts — prompts can only reference `initial_context` fields. ## Data flow example diff --git a/docs/images/template-variables.png b/docs/images/template-variables.png new file mode 100644 index 0000000..3594f9d Binary files /dev/null and b/docs/images/template-variables.png differ diff --git a/docs/voice-agent/api-trigger.mdx b/docs/voice-agent/api-trigger.mdx index 1a464b6..e5168c7 100644 --- a/docs/voice-agent/api-trigger.mdx +++ b/docs/voice-agent/api-trigger.mdx @@ -118,7 +118,7 @@ For example, if your request includes: } ``` -You can reference the user's name in your prompt as `{{initial_context.user.name}}`. +You can reference the user's name in your agent prompt as `{{user.name}}` — in Agent prompts, `initial_context` fields are referenced directly by name (not prefixed with `initial_context.`). See [template variables](/voice-agent/template-variables) for the exact syntax in prompts versus webhook payloads. See [Context & Variables](/core-concepts/context-and-variables) for more on how data flows through a call. diff --git a/docs/voice-agent/pre-call-data-fetch.mdx b/docs/voice-agent/pre-call-data-fetch.mdx index 53d1d9a..a793930 100644 --- a/docs/voice-agent/pre-call-data-fetch.mdx +++ b/docs/voice-agent/pre-call-data-fetch.mdx @@ -11,7 +11,7 @@ Pre-Call Data Fetch allows you to enrich the call context with external data bef 1. A call arrives (inbound) or is initiated (outbound). 2. Dograh sends a **POST** request to your configured endpoint with a standardized payload. 3. The caller hears a ring-back tone while waiting for the response. -4. Your API responds with a JSON object containing `dynamic_variables`. +4. Your API responds with a JSON object containing an `initial_context` object. 5. The variables are merged into the call's initial context. 6. The voice agent starts with full access to the fetched data via `{{variable_name}}` syntax. @@ -50,12 +50,12 @@ The `Content-Type` header is set to `application/json`. If you configured a cred ## Expected Response Format -Your API should return a **JSON object** with a `2xx` status code. The variables to inject into the call context should be placed inside the `dynamic_variables` key: +Your API should return a **JSON object** with a `2xx` status code. The variables to inject into the call context should be placed inside the `initial_context` key: ```json { "call_inbound": { - "dynamic_variables": { + "initial_context": { "customer_name": "Jane Doe", "account_status": "active", "loyalty_tier": "gold", @@ -65,34 +65,38 @@ Your API should return a **JSON object** with a `2xx` status code. The variables } ``` -You can also place `dynamic_variables` at the top level: +You can also place `initial_context` at the top level: ```json { - "dynamic_variables": { + "initial_context": { "customer_name": "Jane Doe", "account_status": "active" } } ``` + +The legacy `dynamic_variables` key is still accepted as a drop-in alias for `initial_context`, so existing integrations keep working without any changes. Use `initial_context` for new integrations. If a response contains both keys, `initial_context` takes precedence. + + After the response is received, you can reference these values anywhere template variables are supported: - **Greeting**: `Hello {{customer_name}}, thank you for calling!` - **Prompt**: `The customer is a {{loyalty_tier}} member with {{open_tickets}} open support tickets.` -If the response is not a valid JSON object, does not contain `dynamic_variables`, or the request fails or times out, the call proceeds normally without the additional context. The pre-call fetch never blocks or fails a call. +If the response is not a valid JSON object, does not contain `initial_context` (or the legacy `dynamic_variables`), or the request fails or times out, the call proceeds normally without the additional context. The pre-call fetch never blocks or fails a call. ## Nested Variables -If your `dynamic_variables` contain nested objects, you can access them using dot notation: +If your `initial_context` contains nested objects, you can access them using dot notation: ```json { "call_inbound": { - "dynamic_variables": { + "initial_context": { "customer": { "name": "Jane Doe", "address": { @@ -153,7 +157,7 @@ app.post("/dograh/pre-call", async (req, res) => { res.json({ call_inbound: { - dynamic_variables: { + initial_context: { customer_name: customer.name, account_status: customer.status, loyalty_tier: customer.tier, diff --git a/docs/voice-agent/template-variables.mdx b/docs/voice-agent/template-variables.mdx index 3db4a56..325317f 100644 --- a/docs/voice-agent/template-variables.mdx +++ b/docs/voice-agent/template-variables.mdx @@ -4,13 +4,23 @@ description: "You can use Template Variables in your prompts for your Agent node --- ### Template Rendering -You can reference template variables which is passed as [`initial_context`](/core-concepts/context-and-variables#initial_context) either using the [API Trigger](/voice-agent/api-trigger) or when uploading a Sheet for a [campaign](/core-concepts/campaigns). You can also use any extracted variable as [`gathered_context`](/core-concepts/context-and-variables#gathered_context) -The template rendering can take nested values. +You reference template variables with `{{double_brace}}` syntax. The data comes from [`initial_context`](/core-concepts/context-and-variables#initial_context) — set via the [API Trigger](/voice-agent/api-trigger), a [campaign](/core-concepts/campaigns) sheet, or a [Pre-Call Data Fetch](/voice-agent/pre-call-data-fetch) that enriches the context when the call starts — and, in Webhook payloads only, from [`gathered_context`](/core-concepts/context-and-variables#gathered_context) (variables extracted during the call). -Example: If the initial context is +**The syntax depends on where you use it:** -``` +| Where | `initial_context` | `gathered_context` | +| --- | --- | --- | +| Agent node prompts | `{{field_name}}` (referenced directly) | Not available | +| Webhook Node payloads | `{{initial_context.field_name}}` | `{{gathered_context.field_name}}` | + +#### Agent node prompts + +In an Agent node prompt, reference each `initial_context` field **directly by name**. Nested values are supported with dot notation. + +Example: if the initial context is + +```json { "initial_context": { "user": { @@ -20,14 +30,26 @@ Example: If the initial context is } ``` -You can write your prompt to access the user's name as below +write your prompt to access the user's name as below: -Prompt: `You are Alice, who is talking to {{initial_context.user.name}}.` +Prompt: `You are Alice, who is talking to {{user.name}}.` + + +Variables extracted during the call (`gathered_context`) are **not** available in Agent prompts — a prompt can only reference `initial_context` fields. To act on extracted data, send it to a [Webhook Node](/voice-agent/webhook). + + +#### Webhook Node payloads + +When constructing a [Webhook Node](/voice-agent/webhook) payload, the context objects are nested under their names, so reference them with the `initial_context.` and `gathered_context.` prefixes: + +Payload value: `{{initial_context.user.name}}` or `{{gathered_context.call_disposition}}` ### Using Template Variables for Testing Template variables defined in your workflow **Settings > Context Variables** are included in test calls (both web and phone) made from the workflow editor. This is useful for simulating data that would normally come from telephony or an API trigger. +Template Variables panel in workflow Settings, showing a customer_name variable and fields to add new key/value pairs + For example, you can set `caller_number` and `called_number` as context variables to test [Pre-Call Data Fetch](/voice-agent/pre-call-data-fetch#testing-with-test-calls) without needing a real inbound call.