# Analytics Chat API — Integration Guide

A natural-language question-answering API for TMS device and transaction data.
Ask a plain-English question; the system writes and runs real SQL against a
curated schema and returns a human-readable answer, grounded only in real
query results — never a guess.

Same engine that powers the `/agents/analytics-chat` UI in the TMS admin
console (`AgentCore.Analytics.Loop`), exposed here so other systems (e.g.
MMS) can ask the same questions programmatically.

## 1. Endpoint

```
POST /api/v1/analytics/ask
Content-Type: application/json
Authorization: Bearer <your-api-key>
```

### Request body

| Field             | Type   | Required | Description |
|--------------------|--------|----------|-------------|
| `question`         | string | yes      | Your question, in plain English. |
| `conversation_id`  | string | no       | Pass the `conversation_id` from a previous response to ask a follow-up **in the same conversation**, with real context from earlier turns. Omit it to start a new conversation. |

### Response body

| Field             | Type          | Description |
|--------------------|---------------|-------------|
| `question`         | string        | Echoes the question you asked. |
| `answer`           | string        | The human-readable answer. |
| `status`           | string        | `"complete"`, `"budget_truncated"`, or `"failed"` — see [Status values](#4-status-values). |
| `conversation_id`  | string        | The id for this exchange. Save it and pass it back on your next call to continue the conversation. |
| `sql_used`         | array[string] | The actual SQL statement(s) run to produce this answer — for audit/debugging, not required for normal use. |

HTTP status is always `200` for a completed request (including
`budget_truncated`/`failed` outcomes — those are valid, expected answers, not
transport errors). See [Errors](#5-errors) for the cases that aren't `200`.

## 2. Authentication

Each consumer (e.g. "MMS") gets its own API key. Keys are shown exactly once
at creation time and only their hash is stored — if you lose it, a new one
must be generated (the old one can be individually revoked without affecting
other consumers).

**Requesting a key**: ask a TMS admin to run:

```bash
mix analytics_chat.create_api_consumer "Your Consumer Name"
```

This prints the raw key once. Send it as a bearer token on every request:

```
Authorization: Bearer <key>
```

## 3. Examples

### New conversation

```bash
curl -s -X POST "https://<host>/api/v1/analytics/ask" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"question": "How many devices are online right now?"}'
```

```json
{
  "status": "complete",
  "question": "How many devices are online right now?",
  "answer": "There are currently 3 devices online.",
  "conversation_id": "vaNOCtOj-DjhJv6S_Jaf-g",
  "sql_used": [
    "SELECT COUNT(*) AS online_devices FROM tms_terminals WHERE status IN ('online','Online','connected')"
  ]
}
```

### Follow-up in the same conversation

Reuse `conversation_id` from the previous response — the model sees the
prior question and answer as real context, not just the new question in
isolation.

```bash
curl -s -X POST "https://<host>/api/v1/analytics/ask" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"question": "And how many are offline?", "conversation_id": "vaNOCtOj-DjhJv6S_Jaf-g"}'
```

```json
{
  "status": "complete",
  "question": "And how many are offline?",
  "answer": "There are currently 41 devices that are offline.",
  "conversation_id": "vaNOCtOj-DjhJv6S_Jaf-g",
  "sql_used": [
    "SELECT COUNT(*) AS offline_devices FROM tms_terminals WHERE status NOT IN ('online','Online','connected')"
  ]
}
```

Omit `conversation_id` (or send it as an empty string / `null`) any time you
want to start a fresh, unrelated conversation instead.

### A multi-table question (device → merchant/store)

```bash
curl -s -X POST "https://<host>/api/v1/analytics/ask" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"question": "What merchant and store was the last POS transaction for terminal 98250623730002, and how much was it?"}'
```

```json
{
  "status": "complete",
  "question": "What merchant and store was the last POS transaction for terminal 98250623730002, and how much was it?",
  "answer": "The last POS transaction for terminal 98250623730002 was for merchant \"Kasi's Cafe\" at the store named \"test,\" and the transaction amount was 3.00.",
  "conversation_id": "8xQ2mP...",
  "sql_used": [
    "SELECT pt.terminal_id, pt.serial_number, pm.merchant_name, s.name AS store_name, ptm.total_amount, ptm.created_dateTime\nFROM pos_terminals pt\nJOIN pos_transaction ptm ON pt.terminal_id = ptm.s_tid\nJOIN pos_merchant pm ON pt.pos_merchant_id = pm.id\nJOIN stores s ON pt.store_id = s.id\nWHERE pt.serial_number = '98250623730002'\nORDER BY ptm.created_dateTime DESC\nLIMIT 1;"
  ]
}
```

## 4. Status values

| `status`            | Meaning |
|----------------------|---------|
| `complete`           | A real, grounded answer was produced. |
| `budget_truncated`   | The question needed more query rounds than the system allows (currently 4 rounds / 25 queries) and no answer was reached. `answer` explains this — treat it like "try rephrasing, or ask something narrower." |
| `failed`             | Something went wrong before an answer could be produced (e.g. the LLM call itself errored). `answer` contains the failure reason. |

## 5. Errors

| HTTP status | Cause | Body |
|---|---|---|
| `400` | Missing or empty `question` field | `{"error": "missing required field: question"}` |
| `401` | Missing, malformed, unknown, or disabled API key | `{"error": "missing or invalid API key"}` |
| `500` | Unexpected internal failure | `{"error": "failed to answer", "reason": "..."}` |

## 6. What can be asked

Answers are grounded in a **curated, admin-maintained schema** — only
specific tables/columns/relations are visible to the system, not the whole
database. Today that covers:

- **Devices**: status (online/offline), vendor, model, area, heartbeat history (`tms_terminals`, `tms_terminal_status_logs`)
- **POS-channel transactions**: amount, time, approval/response codes, and — via a real, verified join — the merchant name, store name, and city for that terminal (`pos_transaction` → `pos_terminals` → `pos_merchant`/`stores`/`addresses`)
- **QR-channel transactions**: amount, status, time, linked to a device by serial number (`transactions`)
- **Unified/reconciled transactions**: aggregate counts and sums by date, currency, type, or settlement status (`core_transactions`) — **date-range and aggregate questions only**; there is currently no verified link from this table to a specific device, so per-device questions against it will get an honest "I can't answer that" rather than a guess

Card/key material (PAN, CVV, track data, encryption keys, etc.) is never
exposed to the model, enforced independently of the schema config — even a
misconfiguration can't leak it.

If a question needs data outside this schema, the answer will say so
explicitly rather than fabricating a plausible-looking wrong answer. That's
expected, correct behavior, not a bug.

## 7. Practical notes

- **Timeout**: set your HTTP client timeout to at least 60 seconds. Most
  questions answer in 2–5 seconds, but a question needing several query
  rounds can take longer; the request blocks until a `complete` /
  `budget_truncated` / `failed` outcome is reached — there's no
  webhook/polling flow to build against.
- **Every question is logged** (question, the SQL actually run, the answer,
  your consumer id) for audit — nothing about this API is silent or
  unrecoverable if something looks wrong.
- **Rotate your key** by generating a new consumer if it's ever exposed —
  the old one can be disabled independently.

## 8. Troubleshooting

**"Something went wrong answering this: :missing_api_key"** (as the
`answer` on a `complete`-looking response, or a `failed` status) — this
means the *server* isn't configured with an OpenAI credential
(`OPENAI_KEY`), not a problem with your request or API key. Ask whoever
operates that environment to set `OPENAI_KEY` (and `AI_CHAT_DB_USERNAME` /
`AI_CHAT_DB_PASSWORD` for the underlying read-only DB connection) and
restart the server process.

**401 on every request** — double check the header is exactly
`Authorization: Bearer <key>` (note the literal word `Bearer` and single
space), and that the key hasn't been disabled.

**Answer says the schema can't cover this** — this is intentional (see
§6). If you have a real, verified relation that should be added, ask a TMS
admin to update the schema config
(`apps/agent_core/priv/analytics_chat_schema.exs`) — it's admin-editable and
takes effect on the very next question, no deploy needed.
