Skip to content

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.