chore: update docs for pre-call data fetch

This commit is contained in:
Abhishek Kumar 2026-06-05 14:16:56 +05:30
parent 79ff04d8a9
commit 418592178c
7 changed files with 142 additions and 45 deletions

View file

@ -15,6 +15,29 @@ from api.utils.credential_auth import build_auth_header
PRE_CALL_FETCH_TIMEOUT_SECONDS = 10 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( async def execute_pre_call_fetch(
*, *,
url: str, url: str,
@ -77,24 +100,16 @@ async def execute_pre_call_fetch(
) )
return {} return {}
# Extract dynamic_variables from Retell-compatible response # Extract the variables to merge into initial_context. Prefers
# Supports: {call_inbound: {dynamic_variables: {...}}} # the canonical `initial_context` key, falling back to the
# or: {dynamic_variables: {...}} # legacy `dynamic_variables` key for backward compatibility.
dynamic_vars = {} initial_context_vars = _extract_initial_context(response_data)
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 = {}
logger.info( logger.info(
f"Pre-call fetch: success ({response.status_code}), " 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: else:
logger.warning( logger.warning(
f"Pre-call fetch: HTTP {response.status_code} - " f"Pre-call fetch: HTTP {response.status_code} - "

View file

@ -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"}) == {}

View file

@ -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: 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` - **[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** — columns beyond `phone_number` automatically become `initial_context` fields for each contact's call - **[Campaign CSV](/core-concepts/campaigns)** — 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 - **[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
```json
{
"phone_number": "+14155550100",
"initial_context": {
"customer_name": "Jane Smith",
"plan": "premium",
"renewal_date": "April 1"
}
}
```
### Template variables ### Template variables
@ -103,7 +93,7 @@ Data the agent collects *during* the call. You configure what to extract in the
<img src="../images/extracted_variables.png" alt="Extracted variables" style={{border: "1px solid #d1d5db", borderRadius: "8px", maxWidth: "100%"}} /> <img src="../images/extracted_variables.png" alt="Extracted variables" style={{border: "1px solid #d1d5db", borderRadius: "8px", maxWidth: "100%"}} />
`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 ## Data flow example

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

View file

@ -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. See [Context & Variables](/core-concepts/context-and-variables) for more on how data flows through a call.

View file

@ -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). 1. A call arrives (inbound) or is initiated (outbound).
2. Dograh sends a **POST** request to your configured endpoint with a standardized payload. 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. 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. 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. 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 ## 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 ```json
{ {
"call_inbound": { "call_inbound": {
"dynamic_variables": { "initial_context": {
"customer_name": "Jane Doe", "customer_name": "Jane Doe",
"account_status": "active", "account_status": "active",
"loyalty_tier": "gold", "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 ```json
{ {
"dynamic_variables": { "initial_context": {
"customer_name": "Jane Doe", "customer_name": "Jane Doe",
"account_status": "active" "account_status": "active"
} }
} }
``` ```
<Note>
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.
</Note>
After the response is received, you can reference these values anywhere template variables are supported: After the response is received, you can reference these values anywhere template variables are supported:
- **Greeting**: `Hello {{customer_name}}, thank you for calling!` - **Greeting**: `Hello {{customer_name}}, thank you for calling!`
- **Prompt**: `The customer is a {{loyalty_tier}} member with {{open_tickets}} open support tickets.` - **Prompt**: `The customer is a {{loyalty_tier}} member with {{open_tickets}} open support tickets.`
<Note> <Note>
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.
</Note> </Note>
## Nested Variables ## 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 ```json
{ {
"call_inbound": { "call_inbound": {
"dynamic_variables": { "initial_context": {
"customer": { "customer": {
"name": "Jane Doe", "name": "Jane Doe",
"address": { "address": {
@ -153,7 +157,7 @@ app.post("/dograh/pre-call", async (req, res) => {
res.json({ res.json({
call_inbound: { call_inbound: {
dynamic_variables: { initial_context: {
customer_name: customer.name, customer_name: customer.name,
account_status: customer.status, account_status: customer.status,
loyalty_tier: customer.tier, loyalty_tier: customer.tier,

View file

@ -4,13 +4,23 @@ description: "You can use Template Variables in your prompts for your Agent node
--- ---
### Template Rendering ### 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": { "initial_context": {
"user": { "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}}.`
<Note>
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).
</Note>
#### 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 ### 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 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.
<img src="../images/template-variables.png" alt="Template Variables panel in workflow Settings, showing a customer_name variable and fields to add new key/value pairs" style={{border: "1px solid #d1d5db", borderRadius: "8px", maxWidth: "100%"}} />
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. 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.
<Note> <Note>