# Search

# Search

Consumer-facing guide to `POST /search` — the **fast lane**: one blocking call that returns data now, no agent run. It is the low-latency counterpart to [Chat](chat.md). Where Chat opens a conversation and runs a multi-step Master agent turn server-side (always async, handles only), Search blocks for a single answer: it turns your ask into SQL over your `interactions` warehouse and runs it, and in `blended` mode adds a quick web + news citation pass.

## Search or Chat?

| Reach for **Search** (`POST /search`) when…                          | Reach for **Chat** (`POST /chat`) when…                              |
| --------------------------------------------------------------------- | -------------------------------------------------------------------- |
| You want rows / a count back in one call, right now.                  | The question needs several steps, judgement, or a written narrative. |
| The ask is a single question over your interaction data.              | The ask spans surfaces, needs delegation, or long-form synthesis.    |
| A few web + news citations alongside the data is enough.              | You want the full market fan-out + the internal-vs-market divergence map. |
| You are wiring a dashboard cell, a slash command, an autocomplete.    | You are running an investigation Amdahl should carry out end to end. |

Search settles in seconds (a ~15s hard ceiling). Chat can take minutes. If an ask is too open-ended for the fast lane, Search says so — the response carries `escalate_to_chat: true` and a plain-language `message`.

## Call it

```bash
curl -X POST https://api.amdahl.com/api/platform/v1/search \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "objection-tagged calls in the last 30 days",
    "mode": "internal"
  }'
```

### Parameters

| Field            | Default    | What it does                                                                                     |
| ---------------- | ---------- | ------------------------------------------------------------------------------------------------ |
| `query`          | (required) | The ask, in plain language.                                                                       |
| `mode`           | `internal` | `internal` = warehouse only. `blended` = also fan out web + news for citations.                  |
| `limit`          | `50`       | Internal row cap. Max `1000`.                                                                     |
| `external_limit` | `10`       | Blended citation cap. Max `24` — the fast lane returns a handful of high-signal citations, not a research corpus. |
| `synthesize`     | `false`    | Opt into a one-paragraph headline over the merged evidence. Off by default: the fast lane returns data, not prose, unless asked. |

Unknown fields are stripped; a bad `mode` / `limit` surfaces as one `invalid_argument` error rather than a silent clamp gone wrong.

### internal vs blended

- **`internal`** (default) writes SQL over your `interactions` warehouse and runs it through the same query gate the rest of the platform uses — your tenant scope, your data-access policy, the truncation guard, and the 1-hour query cache all apply, and one self-repair round re-writes the SQL if the first attempt fails.
- **`blended`** does everything `internal` does **and** fans out a quick `web` + `news` citation pass under a tight deadline. There is deliberately **no rerank and no fusion** here — that heavier market synthesis (and the divergence map) is [Chat](chat.md) / the `external_search` tool. Blended needs the `external_search:execute` scope; without it the call quietly degrades to internal-only and sets `external_omitted: "missing_scope"` — it never fails for the missing scope.

## The result shape

Search never hands back a raw error or stack. Past parameter validation it always returns `success: true`; every failure mode is a typed field on the result, and a **partial answer always beats a 500**.

```json
{
  "success": true,
  "query": "objection-tagged calls in the last 30 days",
  "mode": "blended",
  "internal": {
    "status": "ok",
    "sql": "SELECT objection, COUNT(*) AS n FROM interactions WHERE …",
    "explanation": "Counts objection-tagged interactions in the last 30 days.",
    "rows": [{ "objection": "pricing", "n": 42 }],
    "row_count": 1,
    "truncated": false,
    "cached": false,
    "repaired": false,
    "note": null
  },
  "external": {
    "citations": [
      { "title": "…", "url": "https://…", "snippet": "…", "source": "web" }
    ],
    "sources_timed_out": []
  },
  "external_omitted": null,
  "synthesis": null,
  "message": "Found 1 result over your interactions.",
  "escalate_to_chat": false
}
```

### Reading it

- **`internal.status`** is the warehouse leg's outcome:
  - `ok` — SQL ran and returned rows (in `internal.rows`).
  - `empty` — SQL ran, zero rows matched. The ask is answerable; nothing fit. `internal.note` explains, and `internal.sql` shows what ran.
  - `unsupported` — the ask does not fit a single SELECT over the interactions surface, so no SQL was written. `escalate_to_chat` is `true`.
  - `failed` — SQL was written but execution failed after one self-repair round (or the phase ran out of time). `internal.note` is a plain-language explanation — never the raw database error.
- **`internal.sql`** is the SQL that ran (or the last attempt). Re-run it yourself via `data.query` for live numbers, or show `internal.rows` as the snapshot.
- **`internal.truncated`** is `true` when the row set was capped at `limit`; **`cached`** is `true` when the 1-hour cache served it; **`repaired`** is `true` when the self-repair round rewrote the SQL.
- **`external`** is present only on the `blended` path when the external leg ran (`null` otherwise). Any source that missed its deadline is listed by id in `external.sources_timed_out` — a slow source loses a citation, never the whole call.
- **`external_omitted`** is `"missing_scope"` when you asked for `blended` but lack `external_search:execute`; `null` otherwise.
- **`synthesis`** is the opt-in one-paragraph headline (only when you passed `synthesize: true` and it succeeded; `null` otherwise).
- **`escalate_to_chat`** is `true` when the ask is out of the fast lane's reach and [Chat](chat.md) is the better door.

The only `success: false` shape is a parameter-validation failure:

```json
{ "success": false, "error": { "code": "invalid_argument", "message": "Invalid search request: …" } }
```

## Over MCP

The same fast lane is the `search` tool on the Amdahl MCP server — one action, `run`, with the same parameters:

```json
{ "action": "run", "query": "top objections this quarter", "mode": "blended" }
```

Use `search` when you want data back in one call; use the [`agents` tool's Chat actions](chat.md#over-mcp) (`start_chat` / `chat_status` / `respond` / `cancel_chat`) when you want Amdahl to run a multi-step investigation server-side. The result is data only — no directive text.

## Scopes

`data:read` is all `internal` needs, so a read-only key can call it. The `blended` leg additionally checks `external_search:execute` and degrades to internal-only when it is absent. Both scopes are on the customer-agent key bundle.
