> ## Documentation Index
> Fetch the complete documentation index at: https://docs.callkaro.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# The AI Card

> An open-ended conversation inside a flow you control — with your prompt, your code, your data and your files

Most cards are scripted. The **AI assistant** card is the one that is not: it
hands the conversation to a model and lets the visitor talk freely, for as long
as it takes, before returning to the flow.

It is deliberately **not** a second product. It runs on the same models, the
same timeouts and the same function contract as a WhatsApp chat agent — so
anything you already know about writing those applies here unchanged.

<Card title="Open the flow canvas" icon="diagram-project" href="https://callkaro.ai/dashboard/agents">
  Chat agents → your agent → Flow → drag in **AI assistant**
</Card>

***

## The Fields

| Field                          | What it is                                                                                                                  |
| ------------------------------ | --------------------------------------------------------------------------------------------------------------------------- |
| **Opening message**            | What the visitor reads as the card begins. Supports [rich text](/widget/chat/appearance#rich-text-in-messages-and-buttons). |
| **Instructions for the model** | The system prompt for this card only. Templated, so `{{order_id}}` is substituted before the model sees it.                 |
| **Model**                      | A new card is preselected on `callkaro/arjuna-2.5`. Leave it, or pick another.                                              |
| **Save last answer in**        | Optional. The model's final reply is written to this variable for later cards.                                              |
| **Functions**                  | Up to 10. Your Python, or a send-image / send-document.                                                                     |

Everything the flow has collected so far is appended to the prompt
automatically, so the model already knows the visitor's name, their order id and
anything else an earlier card asked for. You do not have to paste variables into
the instructions.

**Blocks publish:** no instructions.

***

## When The Card Ends

There is **no turn cap and no "Done" button**. The card ends when the assistant
calls `finish_conversation`, then the flow takes the card's single exit.

That is deliberate. A counter ends a conversation mid-sentence, and a
*"That's all, thanks"* pill asks the visitor to answer a question only the
assistant is placed to judge.

The assistant is instructed to finish **only** when the person has clearly
signalled they are done — goodbye, thanks that's all, or confirming their
question is fully answered. It is explicitly told **not** to finish because it
could not help, because a function failed, because it did not understand, or
because the request is outside its remit. In those cases it says so and lets the
visitor decide.

<Note>
  The conversation as a whole is still bounded by the session limit of 200 turns,
  and there is a backstop at 30 exchanges on a single AI card. Neither should ever
  be reached in a real conversation.
</Note>

***

## Functions

Two kinds, and they mirror the WhatsApp chat agent's function editor field for
field.

### Custom — your own Python

Write a LangChain tool. Decorate it with `@tool`, and **its signature is the
parameter list** — nothing is declared separately.

```python theme={null}
@tool
async def lookup_order(order_id: str) -> dict:
    """Look up an order and return its status and delivery date.

    Args:
        order_id: the order number the person gave you.
    """
    import httpx

    async with httpx.AsyncClient(timeout=10) as client:
        r = await client.get(
            f"https://api.yourshop.com/orders/{order_id}",
            headers={"Authorization": f"Bearer {x_secrets['SHOP_API_KEY']}"},
        )

    if r.status_code == 404:
        # Returned, not raised — the assistant can tell the person plainly.
        return {"found": "no"}

    order = r.json()
    return {
        "found": "yes",
        "status": order["status"],
        "delivery_date": order["eta"],
    }
```

The **docstring says what it does**; the **Description field says when to use
it**. Both reach the model — the description is appended as *"Usage
Instructions"*.

|                  |                                                                                                                                             |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| **Timeout**      | 5–120 seconds, set per function. A function that overruns returns an error to the model rather than killing the card.                       |
| **Errors**       | An exception is caught and handed to the **model**, not raised. It can tell the person something went wrong.                                |
| **Return value** | Return a `dict` and every scalar in it is **merged into the conversation's variables**, so a later card can use what the assistant fetched. |

### Send image / send document

No code. You give a name and a description; the **assistant** supplies the link.

| Function        | Arguments                                |
| --------------- | ---------------------------------------- |
| `send_image`    | `link` (required), `caption`             |
| `send_document` | `link` (required), `filename`, `caption` |

The **description is where you say where the link comes from** — "the URL
returned by `lookup_invoice`", or a fixed address. Same arrangement as the
WhatsApp send functions.

<Warning>
  `link` must be a public `http(s)` URL. Anything else is refused and the model is
  told so, so it can ask the right lookup function and try again.
</Warning>

***

## What Your Code Can See

Three objects are injected into every custom function. No imports, no setup.

<CardGroup cols={3}>
  <Card title="x_vars" icon="brackets-curly">
    Everything the flow has collected — Ask answers, Set values, API mappings,
    and anything passed in on the URL as `vars`.
  </Card>

  <Card title="x_contact" icon="address-card">
    The CRM record for whoever is in this conversation — name, email, tags,
    custom attributes.
  </Card>

  <Card title="x_secrets" icon="key">
    Your account's secrets by name, so an API key never has to sit in plain
    text inside a flow graph.
  </Card>
</CardGroup>

```python theme={null}
@tool
async def whoami(note: str) -> dict:
    """Report what this function can see.

    Args:
        note: anything to record with the check.
    """
    return {
        "order_no":  x_vars.get("order_no", "(not collected)"),
        "name":      x_contact.get("first_name", "(no contact row)"),
        "has_key":   "yes" if "SHOP_API_KEY" in x_secrets else "no",
    }
```

Also in scope without importing: `asyncio`, `httpx`, `json`, `re`.

<Warning>
  **Never return a secret.** A secret echoed into a chat is a secret in a
  transcript, readable by anyone who can open that contact. Use it, report
  *whether* you had it, and return the result — not the key.
</Warning>

<Tip>
  Secrets and the contact record are only loaded when your code actually mentions
  `x_secrets` or `x_contact`. A card whose functions never touch them pays nothing
  for them.
</Tip>

Manage secrets at
[Settings → Secrets](https://callkaro.ai/dashboard/settings/secrets).

***

## Files, Both Directions

The AI card is one of only two places a visitor can attach something (the other
is a human handoff). The composer shows a paperclip and a microphone.

### In — what the visitor sends

| Kind                    | Limit | What the model gets                                       |
| ----------------------- | ----- | --------------------------------------------------------- |
| Image (JPEG, PNG, WebP) | 5 MB  | The image itself, so it can describe or read it           |
| PDF                     | 20 MB | The extracted text — up to 20 pages and 20,000 characters |
| Word, Excel             | 20 MB | Recorded in the transcript                                |
| Video                   | 16 MB | Recorded in the transcript                                |
| Audio / voice note      | 16 MB | Recorded in the transcript                                |

A PDF is fetched and its text extracted server-side, because no model here takes
a PDF directly. If nothing could be extracted the model is told that explicitly,
rather than the attachment being quietly dropped.

### Out — what the assistant sends

A `send_image` or `send_document` call delivers the file to the visitor **and**
writes it into the dashboard transcript as its own message with a real link — so
a conversation full of images does not read as a conversation of empty replies.

***

## What Gets Recorded

Every AI turn is stored so somebody reviewing the conversation can see what
actually happened:

* **The reply**, as the visitor read it
* **Every function call**, with its arguments and its result, shown in the
  timeline's tool panel
* **Every file sent**, as its own message with the link
* **The cost** of the turn

A reply that quotes an order number is unreviewable if the lookup behind it is
invisible. That is why the calls are stored and not merely logged.

***

## Billing

An AI card is the only card that runs a model, and it is the only one that
costs anything.

* **Accounts on per-message pricing** are charged their per-AI-message rate for
  the turn.
* **Everyone else** is charged the real model cost, which is recorded on the
  message itself.

Every other card records zero, because that is what it is.

<Card title="Billing" icon="credit-card" href="https://callkaro.ai/dashboard/billing">
  Plan, rates and the Widgets subscription
</Card>

***

## Writing A Good AI Card

<AccordionGroup>
  <Accordion title="Give it one job, not the whole product">
    An AI card that "handles anything" is a prompt nobody can test. Route the
    known topics with Buttons cards and let the AI card take what is left —
    then its instructions can be specific enough to be correct.
  </Accordion>

  <Accordion title="Collect before you hand over">
    Ask for the order id on a scripted card first. The model then starts with
    it in `x_vars` instead of spending a turn asking, and your function can look
    it up immediately.
  </Accordion>

  <Accordion title="Say what to do when a function fails">
    Errors reach the model as text. Without an instruction it will improvise.
    *"If a lookup fails, tell the person exactly what the error said and offer
    to pass them to a person"* is usually what you want.
  </Accordion>

  <Accordion title="Make the description say when, not what">
    The docstring already says what the function does. The Description field is
    read as *usage instructions* — "use this whenever the person asks about a
    delivery date" — and that is what makes the model call it at the right
    moment.
  </Accordion>

  <Accordion title="Point the exit somewhere real">
    When the assistant finishes, the flow takes the card's single exit. Wire it
    to an End card with a closing line, or back to your main menu — not to
    nothing.
  </Accordion>
</AccordionGroup>

***

## Limits

|                              |                                           |
| ---------------------------- | ----------------------------------------- |
| Functions per card           | 10                                        |
| Function timeout             | 5–120 seconds (default 30)                |
| Function code                | 20,000 characters                         |
| Function description         | 1,000 characters                          |
| Tool-calling rounds per turn | 6                                         |
| Exchanges on one card        | 30 (a backstop, not a design target)      |
| History the model sees       | The last 20 messages of this card         |
| PDF extraction               | 5 MB fetched, 20 pages, 20,000 characters |

***

## Next Steps

<CardGroup cols={2}>
  <Card title="Variables and identity" icon="brackets-curly" href="/widget/chat/variables">
    What lands in `x_vars`, and how `uid` works
  </Card>

  <Card title="Card types" icon="diagram-project" href="/widget/chat/card-types">
    The other thirteen cards
  </Card>
</CardGroup>
