Prompts, caller data, and tools¶
VAANI runs your business behavior; it does not invent it.
Greeting and prompt¶
The greeting is the first spoken message. The prompt is the model's continuing instruction. Use strings when every call is the same:
from vaani import Assistant
assistant = Assistant(
name="Reception",
preset="basic-monolingual@1",
greeting="Hello. How can I help?",
prompt="Help briefly. Ask one question at a time. Do not use markdown.",
)
Use functions when wording depends on the call:
from vaani import Call
def greeting_for(call: Call) -> str:
name = call.state.get("name", "there")
return f"Hello {name}. How can I help?"
def prompt_for(call: Call) -> str:
return f"""Help customer {call.state["customer_id"]}.
Use check_points before stating a balance.
Never expose customer IDs or secret values."""
Pass these functions as greeting=greeting_for and prompt=prompt_for.
They may be synchronous or asynchronous and must return non-blank text.
Prepare each call¶
@assistant.prepare runs once before the greeting. Use normalized incoming call
details to fetch business context:
@assistant.prepare
async def load_customer(call: Call) -> object:
customer = await crm.find_by_phone(call.caller)
return {
"customer_id": customer.id,
"name": customer.first_name,
"language": customer.language,
}
Useful input is on call.call_id, call.direction, call.caller,
call.called_party, and preset-allowlisted call.metadata. Return only
JSON-compatible values. VAANI freezes the result as call.state; keep API
clients, credentials, and raw provider responses outside it.
If preparation determines that conversation must not begin, return a
TerminalOutcome instead. The outcome selects an EndRequest or named
TransferRequest, Assistant-approved prerecorded media, required fallback
text, and optional bounded consumer work. This path never resolves dynamic
greeting or prompt functions and never admits conversational tools or turns.
See Assistant basics
for an example.
Handle unknown callers and API failure deliberately. Preparation failure occurs
before VAANI owns the call and therefore produces no CallResult.
Add a read-only business tool¶
@assistant.tool
async def check_points(call: Call) -> str:
"""Return the verified caller's current loyalty points."""
try:
points = await loyalty.points(call.state["customer_id"])
except LoyaltyUnavailable:
return "Points are temporarily unavailable. Do not guess a balance."
return f"The verified balance is {points} points."
Use complete type annotations and a short docstring. VAANI injects the optional
first Call argument and hides it from the model. Return a small, safe result
that tells the model what happened.
Protect state-changing tools¶
The model's request is not authorization. A tool that sends, creates, transfers, redeems, or deletes must validate the caller and inputs server-side:
@assistant.tool
async def redeem_points(call: Call, points: int) -> str:
"""Redeem points after server-side eligibility checks."""
if not call.state["identity_verified"]:
return "Redemption not attempted: identity is not verified."
if points <= 0:
return "Redemption not attempted: points must be positive."
result = await loyalty.redeem_once(
customer_id=call.state["customer_id"],
points=points,
idempotency_key=f"{call.call_id}:{points}",
)
return result.safe_message
Use an idempotency key for external side effects. If a timeout happens after submission and completion cannot be proved, return “completion unknown”; do not retry merely because the model asks again. Never expose secrets or raw provider errors in a result.
Use the standard call tools¶
Register only safe names:
assistant.add_prompt("complaint", complaint_prompt)
assistant.add_prompt("rewards", rewards_prompt)
assistant.add_transfer_route("human-support", support_destination)
When the selected preset and Assistant definition support them, VAANI exposes:
| Tool | Effect |
|---|---|
end_call |
requests one graceful ending |
switch_prompt |
selects a registered business prompt |
switch_language |
selects a complete preset language binding |
transfer_call |
selects a registered deployment-owned route |
The model sees complaint or human-support, not raw prompt text, phone
numbers, SIP addresses, or credentials. A prompt tool appears only with a named
prompt; language switching requires more than one complete binding; transfer
requires a route and adapter. end_call remains available.
Combine a business transition with its prompt update¶
If one business tool already owns intent selection, keep that as the model's only intent-transition operation:
async def load_instructions_for_intent(call: Call, intent: str) -> str:
# Preview only: do not mutate the live business session here.
return await loyalty.preview_intent(call.call_id, intent)
def project_committed_intent(call: Call, intent: str) -> None:
# Optional analytics projection after VAANI commits the prompt.
loyalty.project_committed_intent(call.call_id, intent)
assistant.tool(
load_instructions_for_intent,
prompt_selector=lambda _call, intent: intent.lower(),
on_prompt_update_committed=project_committed_intent,
)
assistant.add_prompt("kyc", kyc_prompt)
assistant.disable_standard_tool("switch_prompt")
VAANI runs the business tool once, resolves the selector's registered prompt,
and requests one atomic CallUpdate before returning the tool result. Only the
prompt update is atomic: VAANI cannot roll back arbitrary work performed by a
business function. Keep the tool itself read-only, or put external effects in a
separate idempotent tool.
The optional on_prompt_update_committed observer runs once only after the
prompt update commits. Use it for non-authoritative analytics projection, never
for an API call or business effect. Observer failure is logged without exposing
its exception text and does not undo the already-committed prompt. Startup fails
if a prompt-owning business tool also exposes switch_prompt; this keeps one
model-facing operation for one transition. A blank or unknown selection is a
typed tool failure and runs no observer.
Supply silence wording¶
The conversation profile owns timing; the bot owns what to say:
@assistant.prepare
async def load_customer(call: Call) -> object:
return {
"participant_silence_nudges": (
"Are you still there?",
"I still cannot hear you.",
),
"participant_inactivity_ending": "I will end this call for now.",
}
For the vertex-v42@1 conversation profile, VAANI waits for a qualifying
participant response, speaks the configured nudges in order, then speaks the
ending line and requests a graceful end. Qualifying speech resets the sequence.
VAANI records missing copy rather than inventing business wording.
Supply response-recovery wording¶
Preparation state stays immutable and JSON-safe. Provide localized recovery
copy plus a terminal descriptor; Assistant validates and binds that descriptor
to a typed EndRequest after preparation freezes:
@assistant.prepare
async def load_customer(call: Call) -> object:
return {
"empty_response_instructions": (
"Please repeat your request.",
),
"empty_response_terminal_action": {
"action": "END",
"source": "CONVERSATION_POLICY",
"reason": "response recovery exhausted",
},
}
For a transfer descriptor, set action to TRANSFER and route_id to a name
already approved with assistant.add_transfer_route(...). Raw destinations and
unregistered route names are rejected. Recovery TTS is attempted once after the
original response timeout and uses one absolute provider-operation budget for
both synthesis and confirmed playout. Missing, failed, timed-out, or cancelled
playout stays terminal and visible in CallResult; synthesized audio is never
reported as heard without transport evidence.
Know what can change mid-call¶
An ordinary Assistant can switch only to registered prompts and complete
language bindings through its standard tools. The initially selected
SmallestAI voice stays fixed for the call.
An Assistant cannot change provider, model, voice, temperature, speed, VAD, endpointing, noise cancellation, deadlines, fallback routes, Judge, metrics, or its preset/profile during a call. Do not accept those values from a model or browser.
Advanced VoiceRuntime consumers may submit one atomic CallUpdate that
replaces complete prompt, language, tools, STT, reasoning, or TTS profiles at a
declared boundary. That is a low-level runtime API, not an Assistant settings
tool or provider-client mutation. See the
configuration reference.
After changing prompts, tools, routes, languages, Judge configuration, or tool
disables, regenerate the Assistant definition lock and rerun ./scripts/check.