diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index 0000fb6..03857ef 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -35,7 +35,12 @@ jobs: registry-url: "https://registry.npmjs.org" cache: pnpm - run: pnpm install --frozen-lockfile + # Same gate ci.yml runs on a PR. The release commit reaches main through + # release-please rather than through CI, so without these the publish is + # the one step that ships unverified. - run: pnpm build + - run: pnpm typecheck + - run: pnpm test - run: pnpm publish --no-git-checks --provenance --access public env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.gitignore b/.gitignore index af3f68a..ac664cb 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,7 @@ dist .env *.log .DS_Store + +# Local codebase-analysis output. Not part of the package; keep it out of the +# public repo so a stray `git add -A` can't publish it. +.understand-anything/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f7f24b5..2fa3336 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,8 +5,28 @@ ```bash pnpm install pnpm test # vitest (mocked fetch — no live API calls) +pnpm test:contract # just tests/contract — the API contract suite pnpm typecheck # tsc over src + tests + examples pnpm build # tsup → dist/ +pnpm spec:refresh # re-vendor tests/contract/openapi.yaml from docs.tako.com +``` + +`pnpm test` includes `tests/contract/types.conformance.test.ts`, which shells out +to a cold `tsc` run, so expect it to take a second or two — much longer than the +rest of the suite. It is the slowest test and the one that fails if `src/types.ts` +drifts from the API. + +## Keeping the API contract honest + +`tests/contract/` checks this SDK's types against two pinned references: the +vendored `openapi.yaml` and the `tako-sdk` version in the lockfile. Both are +snapshots, so **the suite catches a regression in this repo, not a change Tako +ships.** To check for upstream drift, refresh them and re-run: + +```bash +pnpm spec:refresh +pnpm update tako-sdk --latest +pnpm test ``` Examples make live calls; run them manually with keys set in `.env` (see `.env.example`): diff --git a/MIGRATING.md b/MIGRATING.md new file mode 100644 index 0000000..d3fc30b --- /dev/null +++ b/MIGRATING.md @@ -0,0 +1,103 @@ +# Migrating + +## 2.x → 3.0 + +3.0 realigns this SDK's types with the current Tako API. Every change below is a case where 2.x described something the API no longer does — so if code depended on it, it was already broken at runtime, whatever TypeScript said. + +`tests/contract/` validates these types against Tako's published OpenAPI document and against [`tako-sdk`](https://www.npmjs.com/package/tako-sdk), Tako's official generated client, so a type that stops matching either one fails CI. Both are pinned snapshots, refreshed deliberately rather than continuously. + +### Config + +| 2.x | 3.0 | +| --- | --- | +| `sources.data.deferDataRetrieval` | **Removed.** No replacement. | + +The API removed `defer_data_retrieval` from its data-source settings, and those settings forbid unknown properties — so any request that set this option was **rejected outright**, not silently ignored. Delete the option; the request starts working. + +### Responses + +| 2.x | 3.0 | +| --- | --- | +| `result.contents_total_cost: number` | **No replacement.** Read per-item `content.cost` / `content.export_pricing` (see below) | +| `content.format` | `content.content_format` | +| `TakoContentFormat = 'csv' \| 'text'` | `'csv' \| 'json_records' \| 'json_compact'` | + +**Cost.** `contents_total_cost` no longer exists — confirmed absent from every live response. The spec defines `usage` as its successor, but **the API does not currently populate `usage` either**, so there is no drop-in replacement for an aggregate request cost: + +```ts +// 2.x +const cost = result.contents_total_cost; // undefined at runtime + +// 3.0 — typed, but currently always undefined in practice +const cost = result.usage?.total_cost_usd; + +// 3.0 — where pricing actually lives today: per item +for (const card of result.cards) { + card.content?.cost; // USD, e.g. 0.001 + card.content?.export_pricing; // rate card for a full /contents export +} +``` + +Verified 2026-08 across plain, `deep` and `includeContents` search, answer, and both contents modes: `usage` was absent from every response. It is typed `usage?: TakoUsage | null` so it will light up if Tako starts emitting it, but do not build cost tracking on it yet. Sum the per-item `cost` fields instead. + +**Content format.** The field was renamed *and* its values changed. `'text'` is gone: web page text is signalled by the absence of a format. The field is optional as well as nullable, so it may arrive as `null` **or** be missing entirely — test it loosely with `== null`, never `=== null`. + +```ts +// 2.x +if (item.format === 'csv') parseCsv(item.data); +else if (item.format === 'text') readProse(item.data); // both branches dead + +// 3.0 +if (item.content_format == null) readProse(item.data); // web page text +else parseCsv(item.data); // card data +``` + +Three payload fields were also missing and are now typed: `records` (for `json_records`), `dataset` (for `json_compact`), plus `export_pricing` and `manifest`. + +### Source taxonomy + +| 2.x | 3.0 | +| --- | --- | +| `TakoCardSourceIndex = 'tako' \| 'web' \| 'connected_data' \| 'tako_deep_v2'` | `TakoSourceIndex = 'data' \| 'web'` | +| `TakoCardSourceIndexSegment` | **Removed** — never existed in the API | +| `TakoCardSourcePrivateIndex` | **Removed** — never existed in the API | +| `TakoKnowledgeCardSource` | `TakoCardSource` (old name kept as a deprecated alias) | + +The curated Tako source is `'data'`, not `'tako'`. This one fails silently, so it's worth grepping for: + +```ts +if (src.source_index === 'tako') // never matches — compiles fine, never runs +if (src.source_index === 'data') // correct +``` + +`source_index` is now the required two-member union `'data' | 'web'`. 2.x modelled it as a union that could also be an object (`{ index_type, segment_id }`), which the API has never sent on this surface — any code narrowing on that shape can be deleted. + +### Guaranteed collections + +The API guarantees only `request_id` (plus `answer` on the answer surface); the collections are not in its `required` list, so a valid response may omit them. In practice the API currently does send `cards: []` on a web-only search, so 2.x's always-present typing was a latent hazard rather than an active crash — but the contract permits omission, and `tako-sdk` decodes an absent collection to `undefined`. + +3.0 keeps them non-optional **and makes it true**: the tools normalize absent collections to `[]` before returning. No caller changes needed, and no `?.` required. + +If you want the unnormalized wire shape, import `TakoSearchResponse`, `TakoAnswerResponse` or `TakoContentsResponse`. + +### New card fields + +`TakoCard` gained five fields the API was already sending: + +- **`exportable`** — whether `takoContents` can download this card's data. `false` means the call returns 403, so skip it. `true` is eligibility, not a guarantee. +- **`data_freshness`** — `{ data_as_of, last_updated }`. +- **`relevance_score`** — 1.0–5.0, populated for entitled accounts. +- **`nodes`** — the graph entities and metrics behind the card. +- **`metric_definitions`** — definitions of the metrics displayed. + +`exportable` is the practical one; filtering on it avoids calls that cannot succeed: + +```ts +const downloadable = result.cards.filter((c) => c.exportable); +``` + +### Also corrected + +`TakoKnowledgeCardMethodology.methodology_name` and `.methodology_description` are required keys with nullable values (`string | null`), not optional — matching the spec. + +Documentation fixes: 2.x claimed inline contents were "capped at 1000 rows". The real behaviour is a 20-row default against a 2,000-row ceiling. Raising it needs `max_rows`, which this SDK does not expose yet. diff --git a/README.md b/README.md index 78f81bb..108b507 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ npm install @takoviz/ai-sdk ai ## Setup -Get an API key from the [Tako developer console](https://developer.tako.com/console/api-keys) and set it as an environment variable: +Get an API key from the [Tako developer console](https://tako.com/console/api-keys) and set it as an environment variable: ```bash export TAKO_API_KEY=your_api_key_here @@ -63,7 +63,7 @@ takoSearch({ baseUrl: 'https://tako.com', // optional; override for staging effort: 'fast', // 'fast' (default) | 'instant' | 'deep' sources: { // a source is searched iff its key is present; omit to search both - data: { count: 5, includeContents: false, deferDataRetrieval: false }, // legacy alias: tako + data: { count: 5, includeContents: false }, // legacy alias: tako web: { count: 5, includeContents: false }, }, countryCode: 'US', // default 'US' @@ -96,12 +96,40 @@ The LLM supplies only the dynamic input: `{ query }` for `takoSearch`/`takoAnswe { cards: TakoCard[]; // Tako knowledge cards (title, description, image_url, webpage_url, sources, ...) web_results: TakoWebResult[]; - contents_total_cost: number; request_id: string; + usage?: TakoUsage | null; // { total_cost_usd, compute?, data? } — see note } ``` -`takoAnswer` additionally includes `answer: string` (with `cards[0]` as the lead card). `takoContents` resolves to `{ contents: TakoContentItem[]; request_id: string }`, where each item has a `format` (`'csv'` | `'text'`), a `cost`, and either a presigned `url`/`expires_at` (url mode) or inline `data`/`total_rows`/`truncated` (inline mode). +> **Cost reporting.** `usage` is what the API spec defines for per-request cost, but as of 2026-08 it is not populated on any endpoint. For pricing today, read the per-item `content.cost` and `content.export_pricing` on each card, which are populated. + +`takoAnswer` additionally includes `answer: string` (with `cards[0]` as the lead card). `takoContents` resolves to `{ contents: TakoContentItem[]; request_id: string; usage? }`. + +The API guarantees only `request_id` — the contract permits omitting the collections — so the tools normalize: `cards`, `web_results` and `contents` are **always arrays**. No `?.` needed. + +### Reading a card + +Two fields are worth knowing about: + +- **`exportable`** — whether `takoContents` can download that card's data. `false` means don't bother; the call returns 403. `true` is eligibility, not a guarantee, so still handle errors. +- **`data_freshness`** — `{ data_as_of, last_updated }`, so you can tell how current a number is. + +### Reading a contents item + +Each item carries a `cost` (USD) and either a presigned `url` + `expires_at` (url mode) or an inline payload (inline mode). `content_format` tells you what you got: + +| `content_format` | Payload field | Meaning | +| --- | --- | --- | +| `null` *or absent* | `data` | A web page's extracted text | +| `'csv'` | `data` | Card data as CSV | +| `'json_records'` | `records` | Card data as row objects | +| `'json_compact'` | `dataset` | Card data as typed columns + positional rows | + +`total_rows` and `truncated` tell you whether the card held more rows than were returned. + +Which format you get depends on the surface: `takoContents` returns `'csv'` for cards and no format for web pages, while a card inlined by `sources.data.includeContents` arrives as `'json_compact'` (a `dataset`). Requesting a specific format is not configurable yet. + +`content_format` is optional as well as nullable, so branch on it loosely — `content_format == null` means web text; `=== null` misses the absent case. Full type definitions ship with the package. @@ -115,17 +143,25 @@ import type { TakoAnswerResult, TakoContentsResult, TakoCard, + TakoCardSource, TakoWebResult, TakoContentItem, + TakoDataset, + TakoUsage, } from '@takoviz/ai-sdk'; ``` +The types mirror Tako's published OpenAPI document. `tests/contract/` validates them against a vendored copy of that spec and against [`tako-sdk`](https://www.npmjs.com/package/tako-sdk), Tako's official generated client, so a type that stops matching the API fails CI. Both references are pinned snapshots, refreshed by `pnpm spec:refresh` and a `tako-sdk` bump. + +If you need the raw wire shapes (where collections are optional, before the tools normalize them), import `TakoSearchResponse`, `TakoAnswerResponse` or `TakoContentsResponse`. + ## License MIT ## Links +- [Migrating from 2.x](./MIGRATING.md) - [Tako documentation](https://docs.tako.com) - [Vercel AI SDK](https://sdk.vercel.ai/docs) - [GitHub repository](https://github.com/TakoData/ai-sdk) diff --git a/examples/contents.ts b/examples/contents.ts index b583511..a252b99 100644 --- a/examples/contents.ts +++ b/examples/contents.ts @@ -1,4 +1,5 @@ import { takoSearch, takoContents } from "../src/index"; +import type { TakoContentsResult, TakoSearchResult } from "../src/index"; // Run: pnpm exec tsx --env-file=.env examples/contents.ts // Cast: calling a tool's execute() directly (outside generateText) needs a @@ -10,26 +11,50 @@ async function main() { const result = (await search.execute!( { query: "Nvidia full-time employees since 2013" }, opts, - )) as any; + )) as TakoSearchResult; - // Candidate URLs to drill into: card data first, then web results. Some Tako - // cards come from protected sources whose data can't be exported (the API - // returns 403) — so try each until one resolves, the way an agent would. + // `cards` and `web_results` are always arrays — the tools normalize the + // API's optional collections, so no guard is needed here. + console.log(`${result.cards.length} cards, ${result.web_results.length} web results`); + + // Only exportable cards can be downloaded; a non-exportable card returns 403, + // so filtering on the flag avoids a call that cannot succeed. Web results are + // always downloadable as text. const candidates: string[] = [ - ...(result.cards ?? []).map((c: any) => c.webpage_url), - ...(result.web_results ?? []).map((w: any) => w.url), - ].filter(Boolean); + ...result.cards.filter((c) => c.exportable).map((c) => c.webpage_url), + ...result.web_results.map((w) => w.url), + ].filter((url): url is string => Boolean(url)); + + const skipped = result.cards.filter((c) => !c.exportable).length; + if (skipped) console.log(`Skipped ${skipped} card(s) whose data is not exportable.`); const contents = takoContents({ mode: "inline" }); for (const url of candidates) { try { - const downloaded = (await contents.execute!({ url }, opts)) as any; - const item = downloaded.contents?.[0]; + const downloaded = (await contents.execute!({ url }, opts)) as TakoContentsResult; + const item = downloaded.contents[0]; + if (!item) continue; + console.log("Source:", url); - console.log("Format:", item?.format, "| cost:", item?.cost); - console.log("Data (first 500 chars):\n", item?.data?.slice(0, 500)); + // content_format names the serialization for a card's tabular data. For web + // page text it is null — or absent entirely, since the field is optional — + // so compare loosely rather than with `=== null`. + console.log( + item.content_format == null + ? "Web page text" + : `Card data (${item.content_format})`, + "| cost:", + item.cost, + ); + if (item.total_rows != null) { + console.log(`Rows: ${item.total_rows}${item.truncated ? " (truncated)" : ""}`); + } + console.log("Data (first 500 chars):\n", item.data?.slice(0, 500)); + // Per-item cost is what the API populates; the aggregate `usage` is not. + console.log("Item cost (USD):", item.cost ?? 0); return; } catch (err) { + // `exportable: true` is eligibility, not a guarantee — still fall back. console.log(`Skip ${url}: ${(err as Error).message}`); } } diff --git a/package.json b/package.json index 3dd6415..0d82519 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,8 @@ "typecheck": "tsc --noEmit -p tsconfig.check.json", "test": "vitest run", "test:watch": "vitest", + "test:contract": "vitest run tests/contract", + "spec:refresh": "curl -fsSL https://docs.tako.com/api-reference/openapi.yaml -o tests/contract/openapi.yaml", "prepublishOnly": "pnpm build" }, "keywords": [ @@ -50,9 +52,13 @@ "@ai-sdk/openai": "^4.0.0", "@types/node": "^24.10.1", "ai": "^7.0.0", + "ajv": "^8.20.0", + "ajv-formats": "^3.0.1", + "tako-sdk": "^1.1.10", "tsup": "^8.5.0", "tsx": "^4.20.6", "typescript": "^5.9.3", - "vitest": "^3.0.0" + "vitest": "^3.0.0", + "yaml": "^2.9.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 671039b..e916ccb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -21,9 +21,18 @@ importers: ai: specifier: ^7.0.0 version: 7.0.0(zod@4.4.3) + ajv: + specifier: ^8.20.0 + version: 8.20.0 + ajv-formats: + specifier: ^3.0.1 + version: 3.0.1(ajv@8.20.0) + tako-sdk: + specifier: ^1.1.10 + version: 1.1.10 tsup: specifier: ^8.5.0 - version: 8.5.1(postcss@8.5.15)(tsx@4.22.4)(typescript@5.9.3) + version: 8.5.1(postcss@8.5.15)(tsx@4.22.4)(typescript@5.9.3)(yaml@2.9.0) tsx: specifier: ^4.20.6 version: 4.22.4 @@ -32,7 +41,10 @@ importers: version: 5.9.3 vitest: specifier: ^3.0.0 - version: 3.2.6(@types/node@24.13.2)(tsx@4.22.4) + version: 3.2.6(@types/node@24.13.2)(tsx@4.22.4)(yaml@2.9.0) + yaml: + specifier: ^2.9.0 + version: 2.9.0 packages: @@ -583,6 +595,17 @@ packages: peerDependencies: zod: ^3.25.76 || ^4.1.8 + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + any-promise@1.3.0: resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} @@ -660,6 +683,12 @@ packages: resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-uri@3.1.5: + resolution: {integrity: sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==} + fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -684,6 +713,9 @@ packages: js-tokens@9.0.1: resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + json-schema@0.4.0: resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} @@ -769,6 +801,10 @@ packages: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + resolve-from@5.0.0: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} @@ -803,6 +839,10 @@ packages: engines: {node: '>=16 || 14 >=14.17'} hasBin: true + tako-sdk@1.1.10: + resolution: {integrity: sha512-g7W/rSEIeorCqOMOqeGgbAnEbgs8iGbnqk5Fs2ttvjUsov+uc/N7zHzgOYhRcxZxLiDJitG9ktbNNBLlKBvOhg==} + engines: {node: '>=18'} + thenify-all@1.6.0: resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} engines: {node: '>=0.8'} @@ -952,6 +992,11 @@ packages: engines: {node: '>=8'} hasBin: true + yaml@2.9.0: + resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} + engines: {node: '>= 14.6'} + hasBin: true + zod@4.4.3: resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} @@ -1252,13 +1297,13 @@ snapshots: chai: 5.3.3 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.6(vite@7.3.5(@types/node@24.13.2)(tsx@4.22.4))': + '@vitest/mocker@3.2.6(vite@7.3.5(@types/node@24.13.2)(tsx@4.22.4)(yaml@2.9.0))': dependencies: '@vitest/spy': 3.2.6 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.5(@types/node@24.13.2)(tsx@4.22.4) + vite: 7.3.5(@types/node@24.13.2)(tsx@4.22.4)(yaml@2.9.0) '@vitest/pretty-format@3.2.6': dependencies: @@ -1297,6 +1342,17 @@ snapshots: '@ai-sdk/provider-utils': 5.0.0(zod@4.4.3) zod: 4.4.3 + ajv-formats@3.0.1(ajv@8.20.0): + optionalDependencies: + ajv: 8.20.0 + + ajv@8.20.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.5 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + any-promise@1.3.0: {} assertion-error@2.0.1: {} @@ -1402,6 +1458,10 @@ snapshots: expect-type@1.3.0: {} + fast-deep-equal@3.1.3: {} + + fast-uri@3.1.5: {} + fdir@6.5.0(picomatch@4.0.4): optionalDependencies: picomatch: 4.0.4 @@ -1419,6 +1479,8 @@ snapshots: js-tokens@9.0.1: {} + json-schema-traverse@1.0.0: {} + json-schema@0.4.0: {} lilconfig@3.1.3: {} @@ -1468,12 +1530,13 @@ snapshots: mlly: 1.8.2 pathe: 2.0.3 - postcss-load-config@6.0.1(postcss@8.5.15)(tsx@4.22.4): + postcss-load-config@6.0.1(postcss@8.5.15)(tsx@4.22.4)(yaml@2.9.0): dependencies: lilconfig: 3.1.3 optionalDependencies: postcss: 8.5.15 tsx: 4.22.4 + yaml: 2.9.0 postcss@8.5.15: dependencies: @@ -1483,6 +1546,8 @@ snapshots: readdirp@4.1.2: {} + require-from-string@2.0.2: {} + resolve-from@5.0.0: {} rollup@4.62.2: @@ -1540,6 +1605,8 @@ snapshots: tinyglobby: 0.2.17 ts-interface-checker: 0.1.13 + tako-sdk@1.1.10: {} + thenify-all@1.6.0: dependencies: thenify: 3.3.1 @@ -1567,7 +1634,7 @@ snapshots: ts-interface-checker@0.1.13: {} - tsup@8.5.1(postcss@8.5.15)(tsx@4.22.4)(typescript@5.9.3): + tsup@8.5.1(postcss@8.5.15)(tsx@4.22.4)(typescript@5.9.3)(yaml@2.9.0): dependencies: bundle-require: 5.1.0(esbuild@0.27.7) cac: 6.7.14 @@ -1578,7 +1645,7 @@ snapshots: fix-dts-default-cjs-exports: 1.0.1 joycon: 3.1.1 picocolors: 1.1.1 - postcss-load-config: 6.0.1(postcss@8.5.15)(tsx@4.22.4) + postcss-load-config: 6.0.1(postcss@8.5.15)(tsx@4.22.4)(yaml@2.9.0) resolve-from: 5.0.0 rollup: 4.62.2 source-map: 0.7.6 @@ -1607,13 +1674,13 @@ snapshots: undici-types@7.18.2: {} - vite-node@3.2.4(@types/node@24.13.2)(tsx@4.22.4): + vite-node@3.2.4(@types/node@24.13.2)(tsx@4.22.4)(yaml@2.9.0): dependencies: cac: 6.7.14 debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.3.5(@types/node@24.13.2)(tsx@4.22.4) + vite: 7.3.5(@types/node@24.13.2)(tsx@4.22.4)(yaml@2.9.0) transitivePeerDependencies: - '@types/node' - jiti @@ -1628,7 +1695,7 @@ snapshots: - tsx - yaml - vite@7.3.5(@types/node@24.13.2)(tsx@4.22.4): + vite@7.3.5(@types/node@24.13.2)(tsx@4.22.4)(yaml@2.9.0): dependencies: esbuild: 0.27.7 fdir: 6.5.0(picomatch@4.0.4) @@ -1640,12 +1707,13 @@ snapshots: '@types/node': 24.13.2 fsevents: 2.3.3 tsx: 4.22.4 + yaml: 2.9.0 - vitest@3.2.6(@types/node@24.13.2)(tsx@4.22.4): + vitest@3.2.6(@types/node@24.13.2)(tsx@4.22.4)(yaml@2.9.0): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.6 - '@vitest/mocker': 3.2.6(vite@7.3.5(@types/node@24.13.2)(tsx@4.22.4)) + '@vitest/mocker': 3.2.6(vite@7.3.5(@types/node@24.13.2)(tsx@4.22.4)(yaml@2.9.0)) '@vitest/pretty-format': 3.2.6 '@vitest/runner': 3.2.6 '@vitest/snapshot': 3.2.6 @@ -1663,8 +1731,8 @@ snapshots: tinyglobby: 0.2.17 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.3.5(@types/node@24.13.2)(tsx@4.22.4) - vite-node: 3.2.4(@types/node@24.13.2)(tsx@4.22.4) + vite: 7.3.5(@types/node@24.13.2)(tsx@4.22.4)(yaml@2.9.0) + vite-node: 3.2.4(@types/node@24.13.2)(tsx@4.22.4)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 24.13.2 @@ -1687,4 +1755,6 @@ snapshots: siginfo: 2.0.0 stackback: 0.0.2 + yaml@2.9.0: {} + zod@4.4.3: {} diff --git a/src/index.ts b/src/index.ts index becf3d2..22f7670 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,24 +3,48 @@ export { takoAnswer } from "./tools/answer"; export { takoContents } from "./tools/contents"; export type { + // Config TakoBaseConfig, TakoRetrievalConfig, TakoContentsConfig, TakoSourceOptions, TakoCardSourceOptions, + // Enums / unions TakoSearchEffort, TakoContentsMode, TakoContentFormat, + TakoSourceIndex, TakoCardSourceIndex, TakoKnowledgeCardRelevance, + TakoGraphNodeType, + TakoDatasetColumnType, + // Usage + TakoUsage, + TakoUsageCompute, + TakoUsageData, + // Content payloads TakoResultContent, - TakoCardSourceIndexSegment, - TakoCardSourcePrivateIndex, + TakoContentItem, + TakoDataset, + TakoDatasetCell, + TakoDatasetColumn, + TakoDatasetSource, + TakoExportPricing, + TakoColumnDescriptor, + // Cards and web results + TakoCard, + TakoCardSource, TakoKnowledgeCardSource, TakoKnowledgeCardMethodology, - TakoCard, + TakoCardNode, + TakoMetricDefinition, + TakoDataFreshness, TakoWebResult, - TakoContentItem, + // Wire responses (what the API sends; only request_id is guaranteed) + TakoSearchResponse, + TakoAnswerResponse, + TakoContentsResponse, + // Tool results (normalized: collections always present) TakoSearchResult, TakoAnswerResult, TakoContentsResult, diff --git a/src/request.ts b/src/request.ts index c454a74..8dd1d12 100644 --- a/src/request.ts +++ b/src/request.ts @@ -1,4 +1,13 @@ -import type { TakoRetrievalConfig } from "./types"; +import type { + TakoAnswerResponse, + TakoAnswerResult, + TakoContentsMode, + TakoContentsResponse, + TakoContentsResult, + TakoRetrievalConfig, + TakoSearchResponse, + TakoSearchResult, +} from "./types"; const DEFAULT_BASE_URL = "https://tako.com"; @@ -16,7 +25,7 @@ export interface SearchRequestBody { country_code: string; locale: string; sources?: { - data?: { count?: number; include_contents?: boolean; defer_data_retrieval?: boolean }; + data?: { count?: number; include_contents?: boolean }; web?: { count?: number; include_contents?: boolean }; }; timezone?: string; @@ -40,7 +49,6 @@ export function buildSearchRequestBody(config: TakoRetrievalConfig, query: strin const data: NonNullable["data"]> = {}; if (dataSource.count !== undefined) data.count = dataSource.count; if (dataSource.includeContents !== undefined) data.include_contents = dataSource.includeContents; - if (dataSource.deferDataRetrieval !== undefined) data.defer_data_retrieval = dataSource.deferDataRetrieval; sources.data = data; } if (config.sources.web) { @@ -63,3 +71,40 @@ export function buildSearchRequestBody(config: TakoRetrievalConfig, query: strin return body; } + +export interface ContentsRequestBody { + url: string; + mode: TakoContentsMode; +} + +/** Map a url + delivery mode to the POST body the contents endpoint expects. */ +export function buildContentsRequestBody(url: string, mode: TakoContentsMode): ContentsRequestBody { + return { url, mode }; +} + +// ----- Response normalizers ----- +// +// The API guarantees only `request_id` (plus `answer` on the answer surface); the +// contract permits omitting the collections, though it currently sends them +// empty. Normalizing either shape lets callers read `result.cards.length` +// without a guard. + +export function normalizeSearchResult(response: TakoSearchResponse): TakoSearchResult { + return { + ...response, + cards: response.cards ?? [], + web_results: response.web_results ?? [], + }; +} + +export function normalizeAnswerResult(response: TakoAnswerResponse): TakoAnswerResult { + return { + ...response, + cards: response.cards ?? [], + web_results: response.web_results ?? [], + }; +} + +export function normalizeContentsResult(response: TakoContentsResponse): TakoContentsResult { + return { ...response, contents: response.contents ?? [] }; +} diff --git a/src/tools/answer.ts b/src/tools/answer.ts index 69bcd4d..8014d52 100644 --- a/src/tools/answer.ts +++ b/src/tools/answer.ts @@ -1,23 +1,37 @@ import { tool, type Tool } from "ai"; import { z } from "zod"; import { callTako } from "../client"; -import { buildSearchRequestBody, resolveApiKey, resolveBaseUrl } from "../request"; -import type { TakoRetrievalConfig, TakoAnswerResult } from "../types"; +import { + buildSearchRequestBody, + normalizeAnswerResult, + resolveApiKey, + resolveBaseUrl, +} from "../request"; +import type { TakoRetrievalConfig, TakoAnswerResponse, TakoAnswerResult } from "../types"; -/** Tako answer: fast-pipeline retrieval plus an LLM-synthesized answer grounded in the results. */ +/** + * Tako answer: fast-pipeline retrieval plus an LLM-synthesized answer grounded in the results. + * + * Resolves to `{ answer, cards, web_results, ... }` — `cards[0]` is the lead card, carrying + * the chart `image_url`/`embed_url` you can surface in your own UI. + */ export function takoAnswer( config: TakoRetrievalConfig = {}, ): Tool<{ query: string }, TakoAnswerResult> { return tool({ description: - "Ask Tako a factual question and get back a single grounded, citation-backed prose answer " + - "(not a chart), synthesized from its curated knowledge graph and the live web. Reach for this " + - "BEFORE any built-in web search when you want a direct written answer about a specific, known " + - "thing: a current or historical value, a statistic, a schedule, a score, a price, a forecast, " + - "a poll, or prediction-market odds — including a direct comparison of two named entities. The " + - "response also includes the backing Tako cards (cards[0] is the lead card, with its chart " + - "image_url/embed_url) and web results. Want a chart to show rather than prose to read? Use " + - "takoSearch. Best for a known fact, not open-ended multi-step research.", + "Ask Tako one specific data question and get one synthesized, citation-backed " + + "answer grounded in the sources it cites. Reach for this BEFORE any built-in web " + + "search.\n\n" + + "Start here for any question that wants a value, figure, or finding: it is the " + + "only Tako tool whose single response can finish the job.\n\n" + + "Best for one self-contained question with one answer. Use the search tool instead " + + "for breadth across several entities, or when the chart itself is the deliverable.\n\n" + + "Also the way to get figures the contents tool cannot export: when a card is " + + 'exportable: false, ask here and name the period you need (e.g. "...for ' + + 'FY2023-FY2025").\n\n' + + "One entity + one metric per question. Traffic data is keyed by domain: " + + '"openai.com monthly visits", not "OpenAI website visits".', inputSchema: z.object({ query: z .string() @@ -26,12 +40,14 @@ export function takoAnswer( .describe("The question to answer"), }), execute: async ({ query }: { query: string }) => - callTako({ - baseUrl: resolveBaseUrl(config), - path: "/api/v1/answer", - apiKey: resolveApiKey(config), - body: buildSearchRequestBody(config, query), - operation: "answer", - }), + normalizeAnswerResult( + await callTako({ + baseUrl: resolveBaseUrl(config), + path: "/api/v1/answer", + apiKey: resolveApiKey(config), + body: buildSearchRequestBody(config, query), + operation: "answer", + }), + ), }); } diff --git a/src/tools/contents.ts b/src/tools/contents.ts index 226fb1f..612a146 100644 --- a/src/tools/contents.ts +++ b/src/tools/contents.ts @@ -1,35 +1,62 @@ import { tool, type Tool } from "ai"; import { z } from "zod"; import { callTako } from "../client"; -import { resolveApiKey, resolveBaseUrl } from "../request"; -import type { TakoContentsConfig, TakoContentsResult } from "../types"; +import { + buildContentsRequestBody, + normalizeContentsResult, + resolveApiKey, + resolveBaseUrl, +} from "../request"; +import type { TakoContentsConfig, TakoContentsResponse, TakoContentsResult } from "../types"; -/** Download the data behind a result URL: a Tako card's CSV or a web page's text. */ +/** + * Download the data behind a result URL: a Tako card's CSV or a web page's text. + * + * `mode` sets the delivery, and is reflected in the tool description the model reads: + * - `"url"` (default) — a short-lived presigned download url. Use when handing a + * download/embed link to a user, or for large data you won't read yourself. + * - `"inline"` — the content in the response body, so the model can read and reason + * over the numbers directly. + */ export function takoContents( config: TakoContentsConfig = {}, ): Tool<{ url: string }, TakoContentsResult> { + const mode = config.mode ?? "url"; return tool({ description: - "Fetch the underlying data behind a result URL — a Tako card's webpage_url yields a CSV " + - "of the card's data; any other URL (a web result's url) yields the page's extracted full " + - "text. Pass a single url taken from a prior takoSearch/takoAnswer result. Delivery is set " + - 'at construction via `mode`: the default "url" returns a short-lived presigned download_url ' + - '(no row cap) for handing over a download/embed link or for large data you won\'t read ' + - 'yourself; "inline" instead returns the content in the response (CSV capped at 1000 rows, ' + - "with total_rows/truncated, or web text) so you can read and reason over the numbers directly.", + "Fetch the real data behind a result url — a Tako card's webpage_url yields its " + + "rows; any other url (a web result's) yields the page's full extracted text. Only " + + "call this on a url returned by a prior search or answer call, which gives you a " + + "caption and a chart but not the rows.\n\n" + + (mode === "inline" + ? "Returns the content in the response body — read and compute over the numbers " + + "directly.\n\n" + : "Returns a short-lived presigned download url, NOT the data itself: surface the " + + "link, do not parse it or call again expecting rows.\n\n") + + "Only cards whose exportable field is true can be downloaded; a non-exportable card " + + "always returns 403 and retrying will not change that — get its figures from the " + + "answer tool instead, naming the period you need. Web urls always work, so this is " + + "also the fallback when a search surfaced a relevant web result but no fitting data " + + "card.\n\n" + + "On each returned item, content_format names the serialization for card data and is " + + "null or absent for web page text; total_rows and truncated tell you whether the card " + + "had more rows than were returned.", inputSchema: z.object({ + // Validated as a url so a malformed value fails here, with a message the + // model can act on, instead of costing a priced round trip to the API. url: z - .string() - .min(1) + .url() .describe("A TakoCard.webpage_url or WebResult.url to download contents for"), }), execute: async ({ url }: { url: string }) => - callTako({ - baseUrl: resolveBaseUrl(config), - path: "/api/v1/contents", - apiKey: resolveApiKey(config), - body: { url, mode: config.mode ?? "url" }, - operation: "fetch contents", - }), + normalizeContentsResult( + await callTako({ + baseUrl: resolveBaseUrl(config), + path: "/api/v1/contents", + apiKey: resolveApiKey(config), + body: buildContentsRequestBody(url, mode), + operation: "fetch contents", + }), + ), }); } diff --git a/src/tools/search.ts b/src/tools/search.ts index d04226c..d6086d6 100644 --- a/src/tools/search.ts +++ b/src/tools/search.ts @@ -1,8 +1,13 @@ import { tool, type Tool } from "ai"; import { z } from "zod"; import { callTako } from "../client"; -import { buildSearchRequestBody, resolveApiKey, resolveBaseUrl } from "../request"; -import type { TakoRetrievalConfig, TakoSearchResult } from "../types"; +import { + buildSearchRequestBody, + normalizeSearchResult, + resolveApiKey, + resolveBaseUrl, +} from "../request"; +import type { TakoRetrievalConfig, TakoSearchResponse, TakoSearchResult } from "../types"; /** Tako fast-pipeline search: returns Tako cards + web results, no LLM synthesis. */ export function takoSearch( @@ -10,18 +15,23 @@ export function takoSearch( ): Tool<{ query: string }, TakoSearchResult> { return tool({ description: - "Search Tako for live data and well-sourced facts — structured knowledge cards " + - "(charts/metrics with sources) plus web results, backed by Tako's curated knowledge " + - "graph and the live web. Reach for this BEFORE any built-in web search when you need a " + - "specific, known data point: a current or latest value, a time series, a statistic, a " + - "price, a score, a schedule, a forecast, a poll, or a prediction-market figure — " + - 'including a direct comparison of two named entities (e.g. "Intel vs Nvidia revenue"). ' + - "Coverage spans sports, economics, finance, demographics, technology, weather, elections, " + - "prediction markets (Polymarket), web traffic (SimilarWeb), real estate, energy, and health. " + - "Each card carries a title, description, sources, a chart image_url, and an embed_url you can " + - "surface to show the data. Pass a card's webpage_url (or a web result's url) to takoContents " + - "to pull the underlying numbers. Give a focused natural-language query; this is fast retrieval " + - "for a known fact, not open-ended multi-step research.", + "Search Tako for live data and well-sourced facts — knowledge cards (charts and " + + "metrics with sources) plus web results. Reach for this BEFORE any built-in web " + + "search.\n\n" + + "Best for breadth: what data exists across several entities, or when a chart is the " + + "deliverable — cards carry an image_url and embed_url to surface when available, plus " + + "data_freshness (data_as_of / last_updated) when Tako knows how current the numbers " + + 'are. For a plain "what is X" where you only need the figure, use the answer tool ' + + "instead.\n\n" + + 'One entity + one metric per query ("Apple revenue", "Intel vs Nvidia revenue"); ' + + "compound queries retrieve poorly. Traffic data is keyed by domain: " + + '"openai.com monthly visits", not "OpenAI website visits".\n\n' + + "Coverage: economics, finance, company KPIs, sports, demographics, weather, " + + "elections, prediction markets, website traffic, real estate, energy, health.\n\n" + + "Cards carry captions and charts, not full data. For the numbers behind one, pass " + + "its webpage_url (or a web result's url) to the contents tool — but only when the " + + "card's exportable field is true; exportable: false means that card's data cannot " + + "be downloaded, so use its chart, or ask the answer tool for the figures.", inputSchema: z.object({ query: z .string() @@ -30,12 +40,14 @@ export function takoSearch( .describe("Natural-language description of what you're looking for"), }), execute: async ({ query }: { query: string }) => - callTako({ - baseUrl: resolveBaseUrl(config), - path: "/api/v3/search", - apiKey: resolveApiKey(config), - body: buildSearchRequestBody(config, query), - operation: "search", - }), + normalizeSearchResult( + await callTako({ + baseUrl: resolveBaseUrl(config), + path: "/api/v3/search", + apiKey: resolveApiKey(config), + body: buildSearchRequestBody(config, query), + operation: "search", + }), + ), }); } diff --git a/src/types.ts b/src/types.ts index 2f54f81..aad7f0b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,11 +1,28 @@ -// ----- Enums / unions (mirror the backend StrEnums) ----- +// Types mirror Tako's published OpenAPI document. `tests/contract/` validates +// them against a vendored copy of that spec and against `tako-sdk`, Tako's +// official generated client, so drift fails CI. Refresh with `pnpm spec:refresh`. + +// ----- Enums / unions ----- export type TakoSearchEffort = "fast" | "instant" | "deep"; export type TakoContentsMode = "url" | "inline"; -export type TakoContentFormat = "csv" | "text"; -/** CardSourceIndex on the response surface. */ -export type TakoCardSourceIndex = "tako" | "web" | "connected_data" | "tako_deep_v2"; + +/** Serialization of tabular (Tako card) data. Web text carries no format. */ +export type TakoContentFormat = "csv" | "json_records" | "json_compact"; + +/** Public source taxonomy for the card surfaces. */ +export type TakoSourceIndex = "data" | "web"; + +/** + * @deprecated Renamed to {@link TakoSourceIndex}, and the value set collapsed: + * 2.x had `"tako" | "web" | "connected_data" | "tako_deep_v2"`, this resolves to + * `"data" | "web"`. Comparisons against the removed values no longer compile. + */ +export type TakoCardSourceIndex = TakoSourceIndex; + export type TakoKnowledgeCardRelevance = "High" | "Medium" | "Low"; +export type TakoGraphNodeType = "metric" | "entity"; +export type TakoDatasetColumnType = "string" | "number" | "boolean" | "date" | "datetime"; // ----- Config (developer-facing, camelCase) ----- @@ -23,10 +40,13 @@ export interface TakoSourceOptions { includeContents?: boolean; } -export interface TakoCardSourceOptions extends TakoSourceOptions { - /** Defer data retrieval (faster, less detail). Mutually exclusive with includeContents. */ - deferDataRetrieval?: boolean; -} +/** + * Options for the curated Tako data source. + * + * The API's `DataSourceSettings` also carries `mode`, `content_format`, + * `node_ids` and `strict`; those are not surfaced yet. + */ +export interface TakoCardSourceOptions extends TakoSourceOptions {} export interface TakoRetrievalConfig extends TakoBaseConfig { /** "fast" (default) | "instant" | "deep". */ @@ -57,41 +77,168 @@ export interface TakoContentsConfig extends TakoBaseConfig { mode?: TakoContentsMode; } -// ----- Response types (mirror the API wire shape, snake_case) ----- +// ----- Usage / billing ----- + +export interface TakoUsageCompute { + /** USD cost of running the operation. */ + cost_usd: number; +} + +export interface TakoUsageData { + /** USD cost of the inline data delivered in the response. */ + cost_usd: number; + /** Number of billed data units (datasets) in the response. */ + datasets: number; +} +/** + * Usage for one metered request. `total_cost_usd` always equals the sum of + * whichever breakdown components are present. + * + * The spec defines this as the successor to the removed `contents_total_cost`, + * but as of 2026-08 the API does not populate it on search, answer or contents + * (verified live across plain, deep and include_contents calls). Treat it as + * genuinely optional. For per-item pricing today, read `TakoResultContent.cost` + * and `TakoResultContent.export_pricing`, which are populated. + */ +export interface TakoUsage { + /** Total quoted USD cost of this request. */ + total_cost_usd: number; + /** Compute breakdown. Absent on surfaces with no compute step (contents). */ + compute?: TakoUsageCompute | null; + /** Inline-data breakdown. Present only when billable inline data was emitted. */ + data?: TakoUsageData | null; +} + +// ----- Content payloads ----- + +export interface TakoDatasetColumn { + name: string; + type: TakoDatasetColumnType; + /** Structured unit, e.g. "USD billions", "%". Null when unitless. */ + unit?: string | null; +} + +export interface TakoDatasetSource { + /** Human-readable source name, e.g. "FRED". */ + name: string; + index?: TakoSourceIndex; +} + +export type TakoDatasetCell = string | number | boolean | null; + +/** Exact retrieved rows as positional arrays in `columns` order. */ +export interface TakoDataset { + columns: TakoDatasetColumn[]; + rows: TakoDatasetCell[][]; + total_rows: number; + truncated: boolean; + /** Source URL the dataset was derived from. */ + ref: string; + sources: TakoDatasetSource[]; + provenance?: "query" | "web_extraction"; +} + +/** + * Rate card for a card export, so cost can be computed before fetching: + * `baseline_usd + row_cpm_usd * max(0, rows - free_rows) / 1000`. + */ +export interface TakoExportPricing { + baseline_usd: number; + row_cpm_usd: number; + free_rows: number; + max_rows_ceiling: number; +} + +/** Per-column metadata; entry i describes column i. */ +export interface TakoColumnDescriptor { + name?: string | null; + metric?: string | null; + entity?: string | null; + unit?: string | null; + dtype?: TakoDatasetColumnType | null; +} + +/** + * Describes the downloadable content behind a result. + * + * Exactly one payload group is populated once contents are delivered: `data` + * (CSV or web text), `records` (verbose JSON), `dataset` (compact), or + * `url` + `expires_at` (presigned download). When every payload field is unset + * this is just a price quote. + * + * `content_format` distinguishes a web page's extracted text from a card's + * tabular data, but it is optional as well as nullable — web text may arrive as + * either `null` or an absent key. Test it loosely (`content_format == null`), + * never with `=== null`. + */ export interface TakoResultContent { - format: TakoContentFormat; - cost: number; + content_format?: TakoContentFormat | null; + /** USD price of this item. On search/answer cards this is a prospective /contents quote. */ + cost?: number; + /** Inline payload as text: CSV card data, or a web page's extracted text. */ data?: string | null; + /** Inline card data as row objects keyed by column name ("json_records"). */ + records?: Record[] | null; + /** Inline card data as a compact dataset ("json_compact"). */ + dataset?: TakoDataset | null; + /** Presigned download URL ("url" delivery mode). */ + url?: string | null; + expires_at?: string | null; + /** True total rows in the card's data, independent of how many were returned. */ total_rows?: number | null; truncated?: boolean; + export_pricing?: TakoExportPricing | null; + manifest?: TakoColumnDescriptor[] | null; } -export interface TakoCardSourceIndexSegment { - index_type: TakoCardSourceIndex; - segment_id: string; +export interface TakoContentItem extends TakoResultContent { + /** The originating result URL from the request. */ + source_url: string; } -export interface TakoCardSourcePrivateIndex { - index_type: TakoCardSourceIndex; - private_index_id: string; - /** Optional for private indexes. */ - segment_id?: string | null; -} +// ----- Cards and web results ----- -export interface TakoKnowledgeCardSource { - source_name: string | null; - source_description: string | null; - source_index: TakoCardSourceIndex | TakoCardSourceIndexSegment | TakoCardSourcePrivateIndex; - url: string | null; +export interface TakoCardSource { + source_name?: string | null; + source_description?: string | null; + source_index: TakoSourceIndex; + url?: string | null; + /** Raw excerpts from the source page. Present for web sources; null for data. */ source_text?: string | null; } +/** @deprecated Renamed to {@link TakoCardSource}. */ +export type TakoKnowledgeCardSource = TakoCardSource; + +/** Both keys are always present on the wire, though either value may be null. */ export interface TakoKnowledgeCardMethodology { methodology_name: string | null; methodology_description: string | null; } +/** Graph node (entity or metric) behind a card. */ +export interface TakoCardNode { + /** Opaque public id (`ent::…` / `mt::…`). Not durable across graph rebuilds. */ + id: string; + type: TakoGraphNodeType; + name: string; + description?: string | null; +} + +export interface TakoMetricDefinition { + name: string; + definition: string; +} + +/** Freshness dates for a card's data. */ +export interface TakoDataFreshness { + /** Coverage date of the data. */ + data_as_of?: string | null; + /** Date the data was last refreshed. */ + last_updated?: string | null; +} + export interface TakoCard { card_id?: string | null; title?: string | null; @@ -100,35 +247,75 @@ export interface TakoCard { webpage_url?: string | null; image_url?: string | null; embed_url?: string | null; - sources?: TakoKnowledgeCardSource[] | null; + sources?: TakoCardSource[] | null; methodologies?: TakoKnowledgeCardMethodology[] | null; - source_indexes?: (TakoCardSourceIndex | TakoCardSourceIndexSegment)[] | null; + source_indexes?: TakoSourceIndex[] | null; card_type?: string | null; relevance?: TakoKnowledgeCardRelevance | null; content?: TakoResultContent | null; + /** + * Whether /contents can download this card's data. `false` means the export is + * unavailable — don't call takoContents on it. `true` is eligible but not + * guaranteed (a 403 is still possible), so fall back to the inline preview. + */ + exportable?: boolean; + /** Relevance on a 1.0–5.0 scale. Only populated for entitled accounts. */ + relevance_score?: number | null; + /** Graph nodes behind this card. Absent for web-only cards. */ + nodes?: TakoCardNode[] | null; + metric_definitions?: TakoMetricDefinition[] | null; + data_freshness?: TakoDataFreshness | null; } export interface TakoWebResult { title: string; url: string; + /** Excerpt(s) from the page that matched the query. */ snippet?: string | null; source_name?: string | null; publish_date?: string | null; content?: TakoResultContent | null; + /** 1-based citation number for inline [N] markers. Null on raw retrieval. */ citation_number?: number | null; } -export interface TakoContentItem extends TakoResultContent { - source_url: string; - url?: string | null; - expires_at?: string | null; +// ----- Wire responses (exactly what the API sends) ----- + +/** + * The raw `POST /api/v3/search` body. Only `request_id` is guaranteed — the + * contract permits omitting the collections, though the API currently sends them + * empty. Tools normalize either shape and return {@link TakoSearchResult}. + */ +export interface TakoSearchResponse { + cards?: TakoCard[]; + web_results?: TakoWebResult[]; + request_id: string; + usage?: TakoUsage | null; } +/** The raw `POST /api/v1/answer` body. */ +export interface TakoAnswerResponse { + answer: string; + cards?: TakoCard[]; + web_results?: TakoWebResult[]; + request_id: string; + usage?: TakoUsage | null; +} + +/** The raw `POST /api/v1/contents` body. */ +export interface TakoContentsResponse { + contents?: TakoContentItem[]; + request_id: string; + usage?: TakoUsage | null; +} + +// ----- Tool results (normalized: collections always present) ----- + export interface TakoSearchResult { cards: TakoCard[]; web_results: TakoWebResult[]; - contents_total_cost: number; request_id: string; + usage?: TakoUsage | null; } export interface TakoAnswerResult { @@ -137,11 +324,12 @@ export interface TakoAnswerResult { /** Backing cards; cards[0] is the lead card. */ cards: TakoCard[]; web_results: TakoWebResult[]; - contents_total_cost: number; request_id: string; + usage?: TakoUsage | null; } export interface TakoContentsResult { contents: TakoContentItem[]; request_id: string; + usage?: TakoUsage | null; } diff --git a/tests/answer.test.ts b/tests/answer.test.ts index aa015ad..345d4ec 100644 --- a/tests/answer.test.ts +++ b/tests/answer.test.ts @@ -2,7 +2,7 @@ import { describe, it, expect, afterEach, vi } from "vitest"; import { takoAnswer } from "../src/tools/answer"; import { stubFetch, runTool } from "./_helpers"; -const OK = JSON.stringify({ answer: "AMD grew faster.", cards: [], web_results: [], contents_total_cost: 0, request_id: "r" }); +const OK = JSON.stringify({ answer: "AMD grew faster.", cards: [], web_results: [], request_id: "r" }); afterEach(() => vi.unstubAllGlobals()); @@ -21,4 +21,14 @@ describe("takoAnswer", () => { }); expect((res as any).answer).toBe("AMD grew faster."); }); + + it("normalizes absent collections to empty arrays", async () => { + // The contract guarantees only `answer` and `request_id` here, so a bare + // response is valid. Callers still get arrays they can read without a guard. + stubFetch(200, JSON.stringify({ answer: "x", request_id: "r" })); + const res = (await runTool(takoAnswer({ apiKey: "key" }), { query: "q" })) as any; + expect(res.cards).toEqual([]); + expect(res.web_results).toEqual([]); + expect(res.answer).toBe("x"); + }); }); diff --git a/tests/client.test.ts b/tests/client.test.ts index f044d6c..f8d679a 100644 --- a/tests/client.test.ts +++ b/tests/client.test.ts @@ -8,7 +8,7 @@ describe("callTako", () => { it("posts JSON with X-API-Key and returns the parsed body", async () => { const fetchMock = stubFetch(200, JSON.stringify({ ok: true })); const res = await callTako<{ ok: boolean }>({ - baseUrl: "https://trytako.com", + baseUrl: "https://e.com", path: "/api/v3/search", apiKey: "key", body: { query: "x" }, @@ -16,7 +16,7 @@ describe("callTako", () => { }); expect(res).toEqual({ ok: true }); const [url, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]; - expect(url).toBe("https://trytako.com/api/v3/search"); + expect(url).toBe("https://e.com/api/v3/search"); expect(init.method).toBe("POST"); const headers = init.headers as Record; expect(headers["X-API-Key"]).toBe("key"); @@ -27,7 +27,7 @@ describe("callTako", () => { it("throws a clear error when apiKey is missing (before fetching)", async () => { const fetchMock = stubFetch(200, "{}"); await expect( - callTako({ baseUrl: "https://trytako.com", path: "/x", apiKey: undefined, body: {}, operation: "search" }), + callTako({ baseUrl: "https://e.com", path: "/x", apiKey: undefined, body: {}, operation: "search" }), ).rejects.toThrow(/TAKO_API_KEY is required/); expect(fetchMock).not.toHaveBeenCalled(); }); @@ -35,7 +35,7 @@ describe("callTako", () => { it("wraps a non-2xx response with status and body text", async () => { stubFetch(401, "unauthorized", "text/plain"); await expect( - callTako({ baseUrl: "https://trytako.com", path: "/x", apiKey: "k", body: {}, operation: "search" }), + callTako({ baseUrl: "https://e.com", path: "/x", apiKey: "k", body: {}, operation: "search" }), ).rejects.toThrow(/Failed to search with Tako: Tako API error: 401 - unauthorized/); }); }); diff --git a/tests/contents.test.ts b/tests/contents.test.ts index d7b2251..03eda45 100644 --- a/tests/contents.test.ts +++ b/tests/contents.test.ts @@ -3,7 +3,16 @@ import { takoContents } from "../src/tools/contents"; import { stubFetch, runTool } from "./_helpers"; const OK = JSON.stringify({ - contents: [{ source_url: "https://tako.com/card/x", url: "https://signed", expires_at: "2026-01-01T00:00:00Z", format: "csv", cost: 0, truncated: false }], + contents: [ + { + source_url: "https://tako.com/card/x", + url: "https://signed", + expires_at: "2026-01-01T00:00:00Z", + content_format: "csv", + cost: 0, + truncated: false, + }, + ], request_id: "r", }); @@ -17,7 +26,61 @@ describe("takoContents", () => { const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]; expect(url).toBe("https://tako.com/api/v1/contents"); expect(JSON.parse(init.body as string)).toEqual({ url: "https://tako.com/card/x", mode: "url" }); - expect((res as any).contents[0].format).toBe("csv"); + expect((res as any).contents[0].content_format).toBe("csv"); + }); + + it("rejects a non-url input before any request is made", () => { + // The AI SDK validates inputSchema before calling execute, so a malformed + // url costs nothing. Asserted on the schema directly, since runTool() + // bypasses that layer. + const schema = takoContents({ apiKey: "key" }).inputSchema as { + safeParse: (v: unknown) => { success: boolean }; + }; + for (const url of ["https://tako.com/card/x", "https://e.com/a?b=1#c"]) { + expect(schema.safeParse({ url }).success).toBe(true); + } + for (const url of ["not a url", "", "card/x"]) { + expect(schema.safeParse({ url }).success).toBe(false); + } + }); + + it("normalizes an absent contents collection to an empty array", async () => { + stubFetch(200, JSON.stringify({ request_id: "r" })); + const res = (await runTool(takoContents({ apiKey: "key" }), { url: "https://tako.com/card/x" })) as any; + expect(res.contents).toEqual([]); + }); + + it("surfaces web text when content_format is absent entirely", async () => { + // content_format is optional as well as nullable, so a web-text item may + // omit the key rather than send null. Consumers must branch loosely. + stubFetch( + 200, + JSON.stringify({ + contents: [{ source_url: "https://e.com/a", data: "prose" }], + request_id: "r", + }), + ); + const res = (await runTool(takoContents({ apiKey: "key", mode: "inline" }), { + url: "https://e.com/a", + })) as any; + expect(res.contents[0].content_format).toBeUndefined(); + expect(res.contents[0].content_format == null).toBe(true); + expect(res.contents[0].data).toBe("prose"); + }); + + it("surfaces web text, which carries a null content_format", async () => { + stubFetch( + 200, + JSON.stringify({ + contents: [{ source_url: "https://e.com/a", content_format: null, data: "prose" }], + request_id: "r", + }), + ); + const res = (await runTool(takoContents({ apiKey: "key", mode: "inline" }), { + url: "https://e.com/a", + })) as any; + expect(res.contents[0].content_format).toBeNull(); + expect(res.contents[0].data).toBe("prose"); }); it("uses inline mode when configured", async () => { diff --git a/tests/contract/openapi.yaml b/tests/contract/openapi.yaml new file mode 100644 index 0000000..badddc6 --- /dev/null +++ b/tests/contract/openapi.yaml @@ -0,0 +1,4005 @@ +# GENERATED - do not edit; synced from the Tako monorepo. +# Produced by the Tako monorepo OpenAPI sync workflows. +openapi: 3.1.0 +info: + title: Knowledge Search API + version: 1.0.0 +servers: +- url: https://tako.com/api/ + description: Tako Production API Server +paths: + /v3/search: + post: + tags: + - tako + summary: Search + description: Fast-pipeline knowledge search. Returns Tako cards (and web results + when requested) with no LLM synthesis. + operationId: search + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/SearchRequest' + responses: + '200': + description: Fast-pipeline search results (Tako cards + web results) + content: + application/json: + schema: + $ref: '#/components/schemas/SearchResponse' + '400': + description: Invalid request data (validation or malformed body). + content: + application/json: + schema: + $ref: '#/components/schemas/BaseAPIError' + '401': + description: Missing or invalid API key. + content: + application/json: + schema: + $ref: '#/components/schemas/BaseAPIError' + '408': + description: The request exceeded the processing time limit. + content: + application/json: + schema: + $ref: '#/components/schemas/BaseAPIError' + '529': + description: 'Tako is overloaded and did not process this request. Do not + retry automatically. The response carries the header x-should-retry: false. + Send the request again later.' + content: + application/json: + schema: + $ref: '#/components/schemas/BaseAPIError' + security: + - apiKey: [] + /v1/answer: + post: + tags: + - tako + summary: Answer + description: Fast-pipeline retrieval plus an LLM-synthesized answer. Tako may + omit sources that cannot provide usable text. This endpoint returns 3 web + results when you omit sources.web.count. The shared request schema shows a + default of 5, which applies to POST /v3/search. + operationId: answer + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/SearchRequest' + responses: + '200': + description: Synthesized answer grounded in Tako results, web results, or + both + content: + application/json: + schema: + $ref: '#/components/schemas/AnswerResponse' + '400': + description: Invalid request data (validation or malformed body). + content: + application/json: + schema: + $ref: '#/components/schemas/BaseAPIError' + '401': + description: Missing or invalid API key. + content: + application/json: + schema: + $ref: '#/components/schemas/BaseAPIError' + '408': + description: The request exceeded the processing time limit. + content: + application/json: + schema: + $ref: '#/components/schemas/BaseAPIError' + '529': + description: 'Tako is overloaded and did not process this request. Do not + retry automatically. The response carries the header x-should-retry: false. + Send the request again later.' + content: + application/json: + schema: + $ref: '#/components/schemas/BaseAPIError' + security: + - apiKey: [] + /v1/graph/search: + get: + tags: + - tako + summary: Search the data graph + description: 'Resolve a metric or entity in Tako''s data graph by name. Use + this to discover and confirm what data exists before you query. For example, + resolve the metric and the entity here, then ask /v3/search or /v1/answer + for that entity + metric combination. Requires an API key. Note: Tako does + not yet filter results to the production-ready inventory. A returned metric + or entity without a production fact table can 404 when you pass it to /v1/graph/related. + Pass label to prefer a NER label (a ranking boost, not a filter); by default + Tako infers the label from q. This endpoint was previously at /beta/graph/search. + That path still works until 1 February 2027. Move to /v1/graph/search.' + operationId: graphSearch + parameters: + - description: Search text (min 2 chars). + required: true + schema: + type: string + name: q + in: query + - description: 'Comma-separated facets: metric,entity.' + required: false + schema: + type: string + name: types + in: query + - description: Max results (default 20, max 50). + required: false + schema: + type: integer + name: limit + in: query + - description: Prefer results with this NER label (boost, not a filter — matching + nodes rank higher; others still return). Supplying label disables inference. + required: false + schema: + type: string + enum: + - PERSON + - ORG + - GPE + - LOC + - PRODUCT + - EVENT + - LANGUAGE + - MONEY + - METRIC + - STOCK_TICKER + - WEBSITE + name: label + in: query + - description: When true, Tako NER infers the label and grounded-node boosts + from q. Set false to disable. Tako ignores this parameter when you supply + label. Default true. + required: false + schema: + type: boolean + name: infer_label + in: query + responses: + '200': + description: Matching metrics and entities + content: + application/json: + schema: + $ref: '#/components/schemas/GraphSearchResponse' + '400': + description: Invalid request data (validation or malformed body). + content: + application/json: + schema: + $ref: '#/components/schemas/BaseAPIError' + '401': + description: Missing or invalid API key. + content: + application/json: + schema: + $ref: '#/components/schemas/BaseAPIError' + '503': + description: A backing data store is temporarily unavailable. + content: + application/json: + schema: + $ref: '#/components/schemas/BaseAPIError' + security: + - apiKey: [] + /v1/graph/related: + get: + tags: + - tako + summary: Get a graph node's related nodes + description: 'Explore what a node connects to. The overview returns an ordered + `relations` list: named relationships (for example `rel:competes_with`, `rel:in_industry`) + first, then a node''s metrics and entities, then similar nodes and membership + (part_of, members). The metrics and entities are the entity + metric combinations + Tako covers — query /v3/search or /v1/answer for one. Each group carries a + stable `key`. Pass `relation`= plus cursor to paginate that one group. + Pass the optional q to narrow by a case-insensitive substring on names and + aliases (for example, the metrics of an entity that match ''gdp''). Requires + an API key. This endpoint was previously at /beta/graph/related. That path + still works until 1 February 2027. Move to /v1/graph/related.' + operationId: graphRelated + parameters: + - description: Opaque public id of the node. + required: true + schema: + type: string + name: node_id + in: query + - description: Relation key to paginate, for example `rel:competes_with`, `metrics`, + `entities`, `siblings`, or `members`. + required: false + schema: + type: string + name: relation + in: query + - description: 'Deprecated: use `relation`. A legacy facet name (metric, entity, + sibling, or member) that maps to a key.' + required: false + deprecated: true + schema: + type: string + name: relation_type + in: query + - description: Optional case-insensitive substring filter on the related nodes' + names and aliases. It filters every group of the overview, or the single + paginated group when you set `relation`. + required: false + schema: + type: string + name: q + in: query + - description: Opaque pagination cursor. + required: false + schema: + type: string + name: cursor + in: query + - description: Page size (default 50, max 100). + required: false + schema: + type: integer + name: limit + in: query + - description: 'Prefer related nodes with this NER label. This is a boost, not + a filter: matching nodes rank higher within each relation, and totals do + not change. Supplying label disables inference.' + required: false + schema: + type: string + enum: + - PERSON + - ORG + - GPE + - LOC + - PRODUCT + - EVENT + - LANGUAGE + - MONEY + - METRIC + - STOCK_TICKER + - WEBSITE + name: label + in: query + - description: When true, Tako NER infers the label and grounded-node boosts + from q. Set false to disable. Tako ignores this parameter when you supply + label. It applies only when q is present. Default true. + required: false + schema: + type: boolean + name: infer_label + in: query + responses: + '200': + description: A node's details plus related nodes by facet + content: + application/json: + schema: + $ref: '#/components/schemas/GraphRelatedResponse' + '400': + description: Invalid request data (validation or malformed body). + content: + application/json: + schema: + $ref: '#/components/schemas/BaseAPIError' + '401': + description: Missing or invalid API key. + content: + application/json: + schema: + $ref: '#/components/schemas/BaseAPIError' + '404': + description: The requested resource does not exist or has no exportable + data. + content: + application/json: + schema: + $ref: '#/components/schemas/BaseAPIError' + '503': + description: A backing data store is temporarily unavailable. + content: + application/json: + schema: + $ref: '#/components/schemas/BaseAPIError' + security: + - apiKey: [] + /v1/graph/node/{id}: + get: + tags: + - tako + summary: Get a graph node by id + description: Resolve a single node by its opaque public id. Search cards, /v1/graph/search, + and /v1/graph/related return these ids. Returns the node's name, type, aliases, + and description. Requires an API key. This endpoint was previously at /beta/graph/node/{id}. + That path still works until 1 February 2027. Move to /v1/graph/node/{id}. + operationId: graphNode + parameters: + - description: Opaque public id of the node. + required: true + schema: + type: string + name: id + in: path + responses: + '200': + description: A single graph node + content: + application/json: + schema: + $ref: '#/components/schemas/GraphNode' + '401': + description: Missing or invalid API key. + content: + application/json: + schema: + $ref: '#/components/schemas/BaseAPIError' + '404': + description: The requested resource does not exist or has no exportable + data. + content: + application/json: + schema: + $ref: '#/components/schemas/BaseAPIError' + '503': + description: A backing data store is temporarily unavailable. + content: + application/json: + schema: + $ref: '#/components/schemas/BaseAPIError' + security: + - apiKey: [] + /v1/contents: + post: + tags: + - tako + summary: Download content + description: 'Download the content behind a search result: a CSV of a Tako card''s + underlying data, or the full text of a web page. Returns a short-lived presigned + download URL. Protected-source cards (data export not available) return 403. + Send `quote_only: true` to get only the export''s price (`cost` + `export_pricing`) + with an empty payload. A quote is free: Tako fetches nothing and charges nothing.' + operationId: contents + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ContentsRequest' + responses: + '200': + description: 'Downloadable content for a result: a presigned URL plus format + and cost metadata. For a `quote_only` request the item instead carries + only the price (`cost` + `export_pricing`) with all payload and url fields + null.' + content: + application/json: + schema: + $ref: '#/components/schemas/ContentsResponse' + '400': + description: Invalid request data (validation or malformed body). + content: + application/json: + schema: + $ref: '#/components/schemas/BaseAPIError' + '401': + description: Missing or invalid API key. + content: + application/json: + schema: + $ref: '#/components/schemas/BaseAPIError' + '403': + description: Action not permitted for this resource (for example, a protected-source + export). + content: + application/json: + schema: + $ref: '#/components/schemas/BaseAPIError' + '404': + description: The requested resource does not exist or has no exportable + data. + content: + application/json: + schema: + $ref: '#/components/schemas/BaseAPIError' + security: + - apiKey: [] + /v1/thin_viz/create/: + post: + tags: + - tako + description: 'Create a visualization card directly from component configurations. + Supported component types: header, generic_timeseries, categorical_bar, stock_boxes, + financial_boxes, table.' + operationId: createCard + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/CreateCardRequest' + responses: + '200': + description: Card created successfully from schema + content: + application/json: + schema: + $ref: '#/components/schemas/ThinVizCard' + '400': + description: Bad request - validation error or component mismatch + content: + application/json: + schema: + properties: + error: + type: string + type: object + required: + - error + '404': + description: Schema not found + content: + application/json: + schema: + properties: + error: + type: string + type: object + required: + - error + '500': + description: Internal server error + content: + application/json: + schema: + properties: + error: + type: string + type: object + required: + - error + security: + - apiKey: [] + /v1/agent/answer/runs: + get: + tags: + - agent + summary: List answer agent runs + description: List the authenticated caller's answer agent runs, newest first. + Returns trimmed run summaries. Fetch full detail via GET /v1/agent/answer/runs/{run_id}. + operationId: listAnswerAgentRuns + parameters: + - description: Opaque pagination cursor from a previous response's next_cursor. + required: false + schema: + type: string + name: cursor + in: query + - description: Max runs to return (default 20, max 100). + required: false + schema: + type: integer + maximum: 100.0 + minimum: 1.0 + name: limit + in: query + responses: + '200': + description: A page of the caller's answer agent runs, newest first. + content: + application/json: + schema: + $ref: '#/components/schemas/AnswerAgentRunList' + '400': + description: Invalid pagination parameter (limit or cursor). + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorObject' + '401': + description: Missing or invalid API key. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorObject' + security: + - apiKey: [] + post: + tags: + - agent + summary: Dispatch an answer agent run + description: Dispatch an answer agent run. Returns 202 with an AnswerAgentRun + object. Poll GET /v1/agent/answer/runs/{run_id} until status is 'completed' + or 'failed'. + operationId: createAnswerAgentRun + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/AnswerAgentRunRequest' + responses: + '202': + description: 'Run dispatched. With Accept: application/json, poll GET /v1/agent/answer/runs/{run_id} + for the run status. With Accept: text/event-stream, the response is an + SSE stream of AnswerAgentStreamEnvelope events. The stream ends at stream_done. + If the stream ends without an agent_result event, poll GET /v1/agent/answer/runs/{run_id} + for the terminal status.' + content: + application/json: + schema: + $ref: '#/components/schemas/AnswerAgentRun' + text/event-stream: + schema: + $ref: '#/components/schemas/AnswerAgentStreamEnvelope' + '400': + description: Invalid request (for example, a blank query or a malformed + body). + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorObject' + '401': + description: Missing or invalid API key. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorObject' + '402': + description: Insufficient API credit balance (PAYG pre-dispatch gate). + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorObject' + '404': + description: thread_id does not exist or is not owned by the caller. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorObject' + '409': + description: 'Conflict. code is one of: ''conflict'' (the thread already + has a run in flight); ''thread_product_mismatch'' (the thread belongs + to a different agent product); ''source_indexes_mismatch'' (a follow-up + changed the thread''s pinned source_indexes).' + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorObject' + '500': + description: Failed to dispatch the agent run. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorObject' + security: + - apiKey: [] + /v1/agent/answer/runs/{run_id}: + get: + tags: + - agent + summary: Poll an answer agent run + description: Retrieve the current state of an answer agent run. Poll until status + is 'completed' or 'failed'. The response includes result when status is 'completed'. + operationId: getAnswerAgentRun + parameters: + - description: The run ID from the POST /v1/agent/answer/runs response. + required: true + schema: + type: string + name: run_id + in: path + - description: 'SSE resume cursor (Accept: text/event-stream only). The stream + replays events with seq greater than this value. It is equivalent to the + Last-Event-ID header.' + required: false + schema: + type: integer + minimum: 0.0 + name: starting_after + in: query + responses: + '200': + description: 'Current state of the agent run. With Accept: text/event-stream, + the response replays and then follows the run as an SSE stream of AnswerAgentStreamEnvelope + events. Use starting_after or Last-Event-ID to resume. If the stream ends + without an agent_result event, poll this endpoint with Accept: application/json + for the terminal status.' + content: + application/json: + schema: + $ref: '#/components/schemas/AnswerAgentRun' + text/event-stream: + schema: + $ref: '#/components/schemas/AnswerAgentStreamEnvelope' + '401': + description: Missing or invalid API key. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorObject' + '403': + description: The run is not owned by the caller. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorObject' + '404': + description: Run not found. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorObject' + security: + - apiKey: [] + /v1/agent/retrieval/runs: + get: + tags: + - agent + summary: List retrieval agent runs + description: List the authenticated caller's retrieval agent runs, newest first. + Returns trimmed run summaries. Fetch full detail via GET /v1/agent/retrieval/runs/{run_id}. + operationId: listRetrievalAgentRuns + parameters: + - description: Opaque pagination cursor from a previous response's next_cursor. + required: false + schema: + type: string + name: cursor + in: query + - description: Max runs to return (default 20, max 100). + required: false + schema: + type: integer + maximum: 100.0 + minimum: 1.0 + name: limit + in: query + responses: + '200': + description: A page of the caller's retrieval agent runs, newest first. + content: + application/json: + schema: + $ref: '#/components/schemas/RetrievalAgentRunList' + '400': + description: Invalid pagination parameter (limit or cursor). + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorObject' + '401': + description: Missing or invalid API key. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorObject' + security: + - apiKey: [] + post: + tags: + - agent + summary: Dispatch a retrieval agent run + description: Dispatch a retrieval agent run. Returns 202 with a RetrievalAgentRun + object. Poll GET /v1/agent/retrieval/runs/{run_id} until status is 'completed' + or 'failed'. + operationId: createRetrievalAgentRun + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/RetrievalAgentRunRequest' + responses: + '202': + description: 'Run dispatched. With Accept: application/json, poll GET /v1/agent/retrieval/runs/{run_id} + for the run status. With Accept: text/event-stream, the response is an + SSE stream of RetrievalAgentStreamEnvelope events. The stream ends at + stream_done. If the stream ends without an agent_result event, poll GET + /v1/agent/retrieval/runs/{run_id} for the terminal status.' + content: + application/json: + schema: + $ref: '#/components/schemas/RetrievalAgentRun' + text/event-stream: + schema: + $ref: '#/components/schemas/RetrievalAgentStreamEnvelope' + '400': + description: Invalid request (for example, a blank query or a malformed + body). + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorObject' + '401': + description: Missing or invalid API key. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorObject' + '402': + description: Insufficient API credit balance (PAYG pre-dispatch gate). + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorObject' + '404': + description: thread_id does not exist or is not owned by the caller. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorObject' + '409': + description: 'Conflict. code is one of: ''conflict'' (the thread already + has a run in flight); ''thread_product_mismatch'' (the thread belongs + to a different agent product); ''source_indexes_mismatch'' (a follow-up + changed the thread''s pinned source_indexes).' + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorObject' + '500': + description: Failed to dispatch the agent run. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorObject' + security: + - apiKey: [] + /v1/agent/retrieval/runs/{run_id}: + get: + tags: + - agent + summary: Poll a retrieval agent run + description: Retrieve the current state of a retrieval agent run. Poll until + status is 'completed' or 'failed'. The response includes result when status + is 'completed'. + operationId: getRetrievalAgentRun + parameters: + - description: The run ID from the POST /v1/agent/retrieval/runs response. + required: true + schema: + type: string + name: run_id + in: path + - description: 'SSE resume cursor (Accept: text/event-stream only). The stream + replays events with seq greater than this value. It is equivalent to the + Last-Event-ID header.' + required: false + schema: + type: integer + minimum: 0.0 + name: starting_after + in: query + responses: + '200': + description: 'Current state of the agent run. With Accept: text/event-stream, + the response replays and then follows the run as an SSE stream of RetrievalAgentStreamEnvelope + events. Use starting_after or Last-Event-ID to resume. If the stream ends + without an agent_result event, poll this endpoint with Accept: application/json + for the terminal status.' + content: + application/json: + schema: + $ref: '#/components/schemas/RetrievalAgentRun' + text/event-stream: + schema: + $ref: '#/components/schemas/RetrievalAgentStreamEnvelope' + '401': + description: Missing or invalid API key. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorObject' + '403': + description: The run is not owned by the caller. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorObject' + '404': + description: Run not found. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorObject' + security: + - apiKey: [] +components: + schemas: + TakoDataset: + properties: + columns: + items: + $ref: '#/components/schemas/TakoDatasetColumn' + type: array + title: Columns + description: Ordered column headers (name + type), one per position in every + row. + rows: + items: + items: + anyOf: + - type: string + - type: number + - type: integer + - type: boolean + - type: 'null' + type: array + type: array + title: Rows + description: Row data as positional cell arrays aligned to `columns` order. + Cells are string/number/boolean/null; nulls are preserved, never coerced. + total_rows: + type: integer + title: Total Rows + description: True total number of rows in the underlying data, before any + truncation. + truncated: + type: boolean + title: Truncated + description: True when `rows` was capped and total_rows exceeds the number + returned. + ref: + type: string + title: Ref + description: Source URL the dataset was derived from (e.g. the Tako card + URL). + sources: + items: + $ref: '#/components/schemas/TakoDatasetSource' + type: array + title: Sources + description: 'Provenance for the dataset: the sources the rows were drawn + from.' + provenance: + type: string + enum: + - query + - web_extraction + title: Provenance + description: 'How the rows were produced: ''query'' (Tako data) or ''web_extraction''.' + default: query + type: object + required: + - columns + - rows + - total_rows + - truncated + - ref + - sources + title: TakoDataset + description: 'The dataset-slot envelope: exact retrieved rows as positional + arrays + + in `columns` order. The rows come directly from the data source; the LLM + + never transcribes them.' + APIErrorType: + type: string + enum: + - BAD_REQUEST + - AUTHENTICATION_ERROR + - INTERNAL_SERVER_ERROR + - RELEVANT_RESULTS_NOT_FOUND + - RATE_LIMIT_EXCEEDED + - PAYMENT_REQUIRED + - REQUEST_TIMEOUT + - FORBIDDEN + - NOT_FOUND + - SERVICE_UNAVAILABLE + - SERVICE_OVERLOADED + title: APIErrorType + AgentAnswerAssumption: + properties: + title: + type: string + title: Title + description: + type: string + title: Description + category: + anyOf: + - type: string + - type: 'null' + title: Category + source_ref: + anyOf: + - type: integer + - type: 'null' + title: Source Ref + type: object + required: + - title + - description + title: AgentAnswerAssumption + AgentAnswerCitation: + properties: + index: + type: integer + title: Index + title: + type: string + title: Title + url: + anyOf: + - type: string + - type: 'null' + title: Url + source_name: + anyOf: + - type: string + - type: 'null' + title: Source Name + source_index: + anyOf: + - $ref: '#/components/schemas/TakoSourceIndex' + - type: 'null' + excerpt: + anyOf: + - type: string + - type: 'null' + title: Excerpt + publish_date: + anyOf: + - type: string + - type: 'null' + title: Publish Date + content: + anyOf: + - $ref: '#/components/schemas/ResultContent' + - type: 'null' + type: object + required: + - index + - title + title: AgentAnswerCitation + description: 'One indexed source behind the answer. Every inline [n] marker + in the + + answer joins to a citation''s index. A citation can also back the answer + + without a surviving inline marker — markers map to citations, not 1:1. + + This covers web and Tako sources alike, and it is the single shared + + citation object across both agent products. The extended fields + + (source_index, excerpt, publish_date, content) are additive and nullable. + + The Answer Agent populates source_index and leaves the rest null. The + + Retrieval Agent also populates excerpt and publish_date for web + + sources.' + AgentAnswerDefinition: + properties: + term: + type: string + title: Term + definition: + type: string + title: Definition + source_ref: + anyOf: + - type: integer + - type: 'null' + title: Source Ref + type: object + required: + - term + - definition + title: AgentAnswerDefinition + AgentAnswerMetadata: + properties: + citations: + anyOf: + - items: + $ref: '#/components/schemas/AgentAnswerCitation' + type: array + - type: 'null' + title: Citations + definitions: + anyOf: + - items: + $ref: '#/components/schemas/AgentAnswerDefinition' + type: array + - type: 'null' + title: Definitions + assumptions: + anyOf: + - items: + $ref: '#/components/schemas/AgentAnswerAssumption' + type: array + - type: 'null' + title: Assumptions + methodology: + anyOf: + - items: + $ref: '#/components/schemas/AgentAnswerMethodologyNote' + type: array + - type: 'null' + title: Methodology + type: object + title: AgentAnswerMetadata + description: 'Supplementary answer metadata. All fields are optional — population + + varies by effort and engine (low fills citations only today). Tako adds + + new components here additively.' + AgentAnswerMethodologyNote: + properties: + title: + type: string + title: Title + description: + type: string + title: Description + type: object + required: + - title + - description + title: AgentAnswerMethodologyNote + AgentOutputSettings: + properties: + image_dark_mode: + anyOf: + - type: boolean + - type: 'null' + title: Image Dark Mode + description: Render card preview images in dark mode. Omit to use the default + (dark). + type: object + title: AgentOutputSettings + AgentRunStatus: + type: string + enum: + - queued + - running + - completed + - failed + title: AgentRunStatus + AnswerAgentEffort: + type: string + enum: + - medium + title: AnswerAgentEffort + description: 'Effort taxonomy for the Answer Agent (POST /v1/agent/answer/runs). + + + The Answer Agent shares the effort vocabulary with the Retrieval Agent, + + but only ''medium'' is available today. Unsupported values return 400. + + Effort does not select an engine; it is a forward-compatible quality and + + pricing control.' + AnswerAgentMetadata: + properties: + definitions: + anyOf: + - items: + $ref: '#/components/schemas/AgentAnswerDefinition' + type: array + - type: 'null' + title: Definitions + assumptions: + anyOf: + - items: + $ref: '#/components/schemas/AgentAnswerAssumption' + type: array + - type: 'null' + title: Assumptions + methodology: + anyOf: + - items: + $ref: '#/components/schemas/AgentAnswerMethodologyNote' + type: array + - type: 'null' + title: Methodology + type: object + title: AnswerAgentMetadata + description: 'Answer-agent metadata. Citations live in the top-level `citations` + + registry, so this carries only definitions, assumptions, and methodology. + + Distinct from the shared AgentAnswerMetadata, which retains citations for + + the legacy AgentResult.' + AnswerAgentResult: + properties: + answer: + anyOf: + - type: string + - type: 'null' + title: Answer + cards: + items: + $ref: '#/components/schemas/TakoCard' + type: array + title: Cards + citations: + items: + $ref: '#/components/schemas/AgentAnswerCitation' + type: array + title: Citations + metadata: + anyOf: + - $ref: '#/components/schemas/AnswerAgentMetadata' + - type: 'null' + request_id: + anyOf: + - type: string + - type: 'null' + title: Request Id + type: object + title: AnswerAgentResult + description: 'Final answer-agent output. answer is markdown prose with [n] citation + + markers. citations is the unified top-level registry that the [n] markers + + join. cards reuse the sibling TakoCard. metadata carries definitions, + + assumptions, and methodology. There is no inline data, no structured + + output, and no web_results — ever. A prose-only result (empty cards) is + + legitimate.' + AnswerAgentResultEvent: + properties: + kind: + type: string + const: agent_result + title: Kind + default: agent_result + id: + type: string + title: Id + data: + $ref: '#/components/schemas/AnswerAgentResult' + type: object + required: + - id + - data + title: AnswerAgentResultEvent + AnswerAgentRun: + properties: + run_id: + type: string + title: Run Id + object: + type: string + const: agent.run + title: Object + default: agent.run + thread_id: + anyOf: + - type: string + - type: 'null' + title: Thread Id + status: + $ref: '#/components/schemas/AgentRunStatus' + created_at: + type: string + title: Created At + completed_at: + anyOf: + - type: string + - type: 'null' + title: Completed At + result: + anyOf: + - $ref: '#/components/schemas/AnswerAgentResult' + - type: 'null' + error: + anyOf: + - $ref: '#/components/schemas/ErrorObject' + - type: 'null' + usage: + anyOf: + - $ref: '#/components/schemas/Usage' + - type: 'null' + request: + anyOf: + - $ref: '#/components/schemas/AnswerAgentRunRequest' + - type: 'null' + type: object + required: + - run_id + - status + - created_at + title: AnswerAgentRun + description: The answer-agent run resource returned by dispatch (202) and poll + (GET). + AnswerAgentRunList: + properties: + object: + type: string + const: list + title: Object + default: list + data: + items: + $ref: '#/components/schemas/AnswerAgentRunSummary' + type: array + title: Data + has_more: + type: boolean + title: Has More + default: false + next_cursor: + anyOf: + - type: string + - type: 'null' + title: Next Cursor + type: object + required: + - data + title: AnswerAgentRunList + description: 'List envelope for GET /v1/agent/answer/runs. The shape follows + the + + OpenAI and Exa list convention.' + AnswerAgentRunRequest: + properties: + query: + type: string + title: Query + description: Natural-language request for the answer agent. + examples: + - How have American Airlines' margins held up against fuel shocks? + thread_id: + anyOf: + - type: string + format: uuid + - type: 'null' + title: Thread Id + description: Existing thread to continue (follow-up). Omit to start a new + thread. + effort: + $ref: '#/components/schemas/AnswerAgentEffort' + description: Answer-agent effort. Only 'medium' is currently supported. + default: medium + source_indexes: + items: + type: string + enum: + - data + - web + type: array + title: Source Indexes + description: 'Which sources the agent may use: ''data'' (curated knowledge), + ''web'' (open-web search), or both. Defaults to [''data'', ''web'']. Tako + accepts the legacy value ''tako'' as a synonym for ''data''.' + examples: + - - data + - - web + - - data + - web + locale: + type: string + title: Locale + description: BCP-47 locale. Drives the language of the agent's answer and + the locale used when rendering card preview images. Defaults to en-US. + default: en-US + timezone: + anyOf: + - type: string + - type: 'null' + title: Timezone + description: IANA timezone (for example, 'America/New_York'). The agent + uses it to render dates and times in card preview images. It does not + affect the returned data. + output_settings: + anyOf: + - $ref: '#/components/schemas/AgentOutputSettings' + - type: 'null' + description: Settings that control the response and rendering. + type: object + required: + - query + title: AnswerAgentRunRequest + description: 'Request body for POST /v1/agent/answer/runs. + + + Frozen contract: no output_schema, no structured outputs, and no inline + + data — ever. Cards are the only data-export path (via /v1/contents).' + AnswerAgentRunSummary: + properties: + run_id: + type: string + title: Run Id + status: + $ref: '#/components/schemas/AgentRunStatus' + created_at: + type: string + title: Created At + completed_at: + anyOf: + - type: string + - type: 'null' + title: Completed At + thread_id: + anyOf: + - type: string + - type: 'null' + title: Thread Id + usage: + anyOf: + - $ref: '#/components/schemas/Usage' + - type: 'null' + object: + type: string + const: agent.run + title: Object + default: agent.run + type: object + required: + - run_id + - status + - created_at + title: AnswerAgentRunSummary + AnswerAgentStreamEnvelope: + properties: + seq: + type: integer + minimum: 0.0 + title: Seq + run_id: + type: string + title: Run Id + thread_id: + anyOf: + - type: string + - type: 'null' + title: Thread Id + category: + $ref: '#/components/schemas/StreamCategory' + block: + oneOf: + - $ref: '#/components/schemas/ToolCallEvent' + - $ref: '#/components/schemas/ToolResultEvent' + - $ref: '#/components/schemas/ToolErrorEvent' + - $ref: '#/components/schemas/ToolRetryEvent' + - $ref: '#/components/schemas/StatusEvent' + - $ref: '#/components/schemas/SubagentEvent' + - $ref: '#/components/schemas/ReasoningEvent' + - $ref: '#/components/schemas/TextEvent' + - $ref: '#/components/schemas/DataPipelineAnswerEvent' + - $ref: '#/components/schemas/AnswerAgentResultEvent' + - $ref: '#/components/schemas/RunSummaryEvent' + - $ref: '#/components/schemas/HeartbeatEvent' + - $ref: '#/components/schemas/StreamResetEvent' + - $ref: '#/components/schemas/StreamDoneEvent' + title: Block + discriminator: + propertyName: kind + mapping: + agent_result: '#/components/schemas/AnswerAgentResultEvent' + data_pipeline_answer: '#/components/schemas/DataPipelineAnswerEvent' + heartbeat: '#/components/schemas/HeartbeatEvent' + reasoning: '#/components/schemas/ReasoningEvent' + run_summary: '#/components/schemas/RunSummaryEvent' + status: '#/components/schemas/StatusEvent' + stream_done: '#/components/schemas/StreamDoneEvent' + stream_reset: '#/components/schemas/StreamResetEvent' + subagent: '#/components/schemas/SubagentEvent' + text: '#/components/schemas/TextEvent' + tool_call: '#/components/schemas/ToolCallEvent' + tool_error: '#/components/schemas/ToolErrorEvent' + tool_result: '#/components/schemas/ToolResultEvent' + tool_retry: '#/components/schemas/ToolRetryEvent' + type: object + required: + - seq + - run_id + - category + - block + title: AnswerAgentStreamEnvelope + description: 'Public SSE envelope for the Answer Agent run stream. Identical + wire shape + + to AgentStreamEnvelope; the only difference is the terminal agent_result + + block carries AnswerAgentResult (top-level citations, no web_results).' + AnswerResponse: + properties: + answer: + type: string + title: Answer + description: Synthesized text answer. + cards: + items: + $ref: '#/components/schemas/TakoCard' + type: array + title: Cards + description: Tako cards backing the answer; cards[0] is the lead card — + the best one to show alongside the answer. + web_results: + items: + $ref: '#/components/schemas/WebResult' + type: array + title: Web Results + request_id: + type: string + title: Request Id + usage: + anyOf: + - $ref: '#/components/schemas/Usage' + - type: 'null' + type: object + required: + - answer + - request_id + title: AnswerResponse + description: 'Response for POST /api/v1/answer: the synthesized answer plus + the + + retrieval behind it.' + BaseAPIError: + properties: + error_message: + type: string + title: Error Message + error_type: + $ref: '#/components/schemas/APIErrorType' + type: object + required: + - error_message + - error_type + title: BaseAPIError + ColumnDescriptor: + properties: + name: + anyOf: + - type: string + - type: 'null' + title: Name + metric: + anyOf: + - type: string + - type: 'null' + title: Metric + entity: + anyOf: + - type: string + - type: 'null' + title: Entity + unit: + anyOf: + - type: string + - type: 'null' + title: Unit + dtype: + anyOf: + - $ref: '#/components/schemas/TakoDatasetColumnType' + - type: 'null' + type: object + title: ColumnDescriptor + description: 'Structured description of one exported value column. + + + Producers set the semantic parts they know (``metric``, ``entity``, + + ``unit``). ``name`` and ``dtype`` are filled centrally: ``name`` is the + + rendered canonical header (set by ``apply_descriptor_names``), and ``dtype`` + + is inferred from the real column data in ``finalize_export_frame``. A + + producer-set ``dtype`` would drift from the cells, and a producer cannot know + + the disambiguated ``name`` before the whole frame is rendered. + + + In the exported manifest ``name`` equals the column''s actual header, and + the + + manifest is positionally aligned with the exported columns (manifest entry + + ``i`` describes column ``i``).' + ComponentConfig: + properties: + component_type: + $ref: '#/components/schemas/ComponentTypeEnum' + description: 'Component type: + + - bubble: Bubble chart (scatter plot with size dimension for 3-variable + data) + + - categorical_bar: Bar chart with categorical x-axis (for example, regions + or products) + + - choropleth: Choropleth map showing geographic data with color intensity + by region (US states or world) + + - data_table_chart: Bar chart with auto-generated data table below showing + values + + - financial_boxes: Financial metric boxes with values and growth indicators + (for example, Revenue or EPS) + + - generic_timeseries: timeseries chart + + - header: Card header with title and description, automatically styled + with theme + + - heatmap: 2D heatmap with color intensity representing values (for example, + a correlation matrix) + + - histogram: Histogram chart showing frequency distribution of values + + - person_card: Person profile card from an Exa person search result (includes + career, education, and about tabs) + + - pie: Pie chart showing proportional data as slices of a circle + + - scatter: Scatter plot showing relationships between two continuous variables + + - table: Data table with configurable columns and rows + + - boxplot: Box plot showing statistical distributions (min, Q1, median, + Q3, max) + + - treemap: Treemap chart showing hierarchical data as proportional rectangles + + - waterfall: Waterfall chart showing incremental positive and negative + changes (for example, an income statement breakdown)' + component_variant: + anyOf: + - type: string + - type: 'null' + title: Component Variant + description: Component variant (for example, 'simple' or 'financial'). + config: + additionalProperties: true + type: object + title: Config + description: Component configuration data + type: object + required: + - component_type + - config + title: ComponentConfig + description: Configuration for a single component in a card. + ComponentTypeEnum: + type: string + enum: + - categorical_bar + - choropleth + - data_table_chart + - financial_boxes + - generic_timeseries + - header + - heatmap + - histogram + - marimekko + - pie + - scatter + - table + - boxplot + - treemap + - waterfall + - sankey + - bubble + - person_card + - timeline + - top_level_metric + title: ComponentTypeEnum + description: 'Component types supported for thin viz schemas with builder support. + + + These component types have dedicated builders that process configurations, + + add defaults, and apply theme styling automatically.' + ContentItem: + properties: + content_format: + anyOf: + - $ref: '#/components/schemas/ContentsFormat' + - type: 'null' + description: 'Serialization of the returned card data: ''csv'', ''json_records'', + or ''json_compact''. Null for web text (always returned as raw text) and + for a quote-only response (nothing was serialized).' + cost: + type: number + title: Cost + description: Price of this item in USD. On a /contents response this is + the amount actually billed; on a quote_only response or a search/answer + downloadable card it is a prospective /contents export price (nothing + was fetched or billed). Pair with export_pricing to compute a larger export's + cost before fetching. + default: 0.0 + data: + anyOf: + - type: string + - type: 'null' + title: Data + description: 'Inline payload as text: card data serialized to CSV, or a + web page''s extracted text. Set only for the ''csv'' card format and for + web text; null otherwise.' + records: + anyOf: + - items: + additionalProperties: + anyOf: + - type: string + - type: number + - type: integer + - type: boolean + - type: 'null' + type: object + type: array + - type: 'null' + title: Records + description: 'Inline card data as verbose JSON: a list of row objects keyed + by column name. Set only when content_format is ''json_records''.' + dataset: + anyOf: + - $ref: '#/components/schemas/TakoDataset' + - type: 'null' + description: Inline card data as a compact TakoDataset (typed column headers + plus positional row arrays). Set only when content_format is 'json_compact'. + url: + anyOf: + - type: string + - type: 'null' + title: Url + description: Presigned download URL for the content, returned in 'url' delivery + mode. Null for inline delivery and for quotes; pair with expires_at. + expires_at: + anyOf: + - type: string + - type: 'null' + title: Expires At + description: ISO-8601 timestamp after which the presigned url stops working. + Null whenever url is null. + total_rows: + anyOf: + - type: integer + - type: 'null' + title: Total Rows + description: True total number of rows in the card's data, independent of + how many rows were returned. Compare with truncated to tell whether more + rows are available via a larger max_rows. Null for web text. + truncated: + type: boolean + title: Truncated + description: True when the returned rows were capped (by the request's max_rows + or the 2,000-row system ceiling) and total_rows exceeds the number returned. + default: false + export_pricing: + anyOf: + - $ref: '#/components/schemas/ExportPricing' + - type: 'null' + description: Rate card for a downloadable card CSV, so a caller can compute + a full export's cost before fetching. Null for web text and other non-downloadable + content. + manifest: + anyOf: + - items: + $ref: '#/components/schemas/ColumnDescriptor' + type: array + - type: 'null' + title: Manifest + description: 'Per-column metadata, one entry per exported column, in column + order: entry i describes column i (CSV header i, json_records key i, or + dataset.columns[i]). Each entry carries name (the column header), dtype, + unit, metric, and entity. Null for web or quote responses that carry no + tabular columns.' + source_url: + type: string + title: Source Url + description: The originating result URL from the request. + type: object + required: + - source_url + title: ContentItem + description: 'A single downloadable artifact. + + + It inherits `content_format`, `cost`, the payload fields (`data`, + + `records`, `dataset`, `total_rows`, `truncated`), and the presigned `url` + + and `expires_at` from ResultContent. URL mode populates `url` and + + `expires_at`. INLINE mode populates one of the payload fields and leaves + + `url` and `expires_at` null. A `quote_only` request yields a third shape: + + `cost` (plus `export_pricing` for a card) with every payload and url + + field null and `content_format` null — the price of the export, with + + nothing fetched or charged.' + ContentsDeliveryMode: + type: string + enum: + - url + - inline + title: ContentsDeliveryMode + description: 'How contents reach the caller. URL returns a short-lived presigned + file + + download; INLINE returns the content in the response body.' + ContentsFormat: + type: string + enum: + - csv + - json_records + - json_compact + title: ContentsFormat + description: 'Serialization of tabular (Tako card) data. Web content is always + raw text + + and carries no content format (content_format is null).' + ContentsRequest: + properties: + url: + type: string + title: Url + description: The result URL to fetch downloadable content for (a TakoCard.webpage_url + or a WebResult.url). A Tako card URL yields a CSV of the card's data; + any other URL yields the page's extracted text. + examples: + - https://tako.com/card/abc123 + mode: + $ref: '#/components/schemas/ContentsDeliveryMode' + description: 'Delivery mode. ''url'' (the default) returns a presigned download + link. ''inline'' returns the content in the response body: CSV data up + to the 2,000-row system ceiling (with total_rows and truncated reported), + or web text.' + default: url + content_format: + $ref: '#/components/schemas/ContentsFormat' + description: 'Serialization for Tako card data: ''csv'' (default), ''json_records'', + or ''json_compact''. Ignored for web URLs (always text).' + default: csv + max_rows: + anyOf: + - type: integer + minimum: 1.0 + - type: 'null' + title: Max Rows + description: Optional cap on the rows returned for a Tako card CSV export. + When omitted, it defaults to the free-row allowance (20 rows), billed + at the baseline only. Raise it to export more rows, up to the 2,000-row + system ceiling (Tako clamps larger values). Rows beyond the free allowance + bill at the per-1,000-row rate. Billing counts the rows actually returned. + Web URLs ignore this field. + examples: + - 100 + max_chars: + type: integer + maximum: 1000000.0 + minimum: 1.0 + title: Max Chars + description: Character cap on extracted web page text. Ignored for Tako + card URLs (they use max_rows). The default is the full page text, up to + the 1000000-character ceiling. To truncate, pass a smaller value. + default: 1000000 + examples: + - 50000 + quote_only: + type: boolean + title: Quote Only + description: When true, return only the price of the export (cost + export_pricing) + without fetching content or charging. The response item's payload and + url fields are null, and the request is free. `max_rows` shapes the quote + and defaults to the 20-row free allowance, as a real export does. The + request ignores `mode` and `content_format`. The same export-safe gate + applies, so an unexportable card still returns 403. + default: false + type: object + required: + - url + title: ContentsRequest + description: 'Request body for POST /api/v1/contents. + + + The caller passes the result URL it wants downloadable content for, and + + the endpoint detects the right content from the URL itself. A Tako card + + URL resolves to the card''s underlying data. Any other URL resolves to + + the page''s extracted full text. `mode` controls delivery: `url` (the + + default) returns a presigned download link, and `inline` returns the + + content in the response. `content_format` selects the card serialization + + (csv, json_records, or json_compact); web URLs ignore it (always + + text).' + ContentsResponse: + properties: + contents: + items: + $ref: '#/components/schemas/ContentItem' + type: array + title: Contents + description: Downloadable artifacts for the requested URL. + request_id: + type: string + title: Request Id + description: Unique identifier for this request. + usage: + anyOf: + - $ref: '#/components/schemas/Usage' + - type: 'null' + type: object + required: + - request_id + title: ContentsResponse + description: 'Response for POST /api/v1/contents. + + + `contents` is a list so that the contract stays stable if a single result + + ever yields multiple artifacts. Today it always carries exactly one + + item.' + CreateCardRequest: + properties: + components: + items: + $ref: '#/components/schemas/ComponentConfig' + type: array + title: Components + description: Full component configurations + title: + anyOf: + - type: string + - type: 'null' + title: Title + description: Card title (falls back to header component title) + description: + anyOf: + - type: string + - type: 'null' + title: Description + description: Card description + source: + anyOf: + - type: string + - type: 'null' + title: Source + description: Data source attribution (displayed in footer) + height: + anyOf: + - type: integer + maximum: 2000.0 + minimum: 100.0 + - type: 'null' + title: Height + description: Chart height in pixels. When set, overrides the default aspect-ratio-based + height for all chart components in this card. Must be between 100 and + 2000. + postmessage_embed: + type: boolean + title: Postmessage Embed + description: When True, the embed iframe operates in postMessage mode. The + parent page injects visualization_data via window.postMessage after the + iframe loads; the embed URL carries no inline data. The response includes + embed_mode='postmessage' when this is True, and embed_mode='post' otherwise. + default: false + normalize_currencies: + anyOf: + - type: string + - type: 'null' + title: Normalize Currencies + description: Target ISO 4217 currency code (for example, 'USD' or 'EUR'). + When set, Tako converts datasets with recognized currency units to this + currency with historical exchange rates. Tako also adds a methodology + section that explains the conversion. + image_ttl_minutes: + anyOf: + - type: integer + maximum: 1440.0 + minimum: 1.0 + - type: 'null' + title: Image Ttl Minutes + description: Minutes to keep the preview image available for download (zero-data-retention + (ZDR) cards only). Min 1, max 1440 (24 hours). When set on a ZDR card, + Tako generates a temporary preview image and keeps it available for download + until the TTL expires. Non-ZDR cards ignore this field. + type: object + required: + - components + title: CreateCardRequest + description: Request model for creating a card directly with components. + DataFreshness: + properties: + data_as_of: + anyOf: + - type: string + - type: 'null' + title: Data As Of + description: Date of the most recent observation in the card's data, as + an ISO date (YYYY-MM-DD). Null for card types without a coverage date. + examples: + - '2026-06-30' + last_updated: + anyOf: + - type: string + - type: 'null' + title: Last Updated + description: Date Tako last refreshed the underlying data, as an ISO date + (YYYY-MM-DD). + examples: + - '2026-07-14' + type: object + title: DataFreshness + description: 'When the card''s data was last observed and last refreshed. Either + field + + may be null when that date isn''t available for the card type.' + DataPipelineAnswerEvent: + properties: + kind: + type: string + const: data_pipeline_answer + title: Kind + default: data_pipeline_answer + id: + type: string + title: Id + chart_refs: + items: + type: string + type: array + title: Chart Refs + type: object + required: + - id + title: DataPipelineAnswerEvent + description: 'Signals that the data pipeline produced one or more charts for + the + + answer. It carries only chart references. The answer text streams in + + `text` events, and the structured result arrives in the terminal + + `agent_result` event, not here.' + DataSourceSettings: + properties: + count: + type: integer + maximum: 20.0 + minimum: 1.0 + title: Count + description: Maximum number of results to return for this source. 1-20. + default: 5 + include_contents: + type: boolean + title: Include Contents + description: Inline this source's underlying data directly in the response. + For the Tako data source, that is serialized card data (see content_format). + For web results, that is the extracted text. + default: false + mode: + $ref: '#/components/schemas/ContentsDeliveryMode' + description: Delivery for inlined card data when include_contents is true. + For Tako cards, include_contents always returns a small free inline preview + of the most-recent rows in the response body. This field therefore has + no effect on Tako cards; it stays for schema stability. total_rows and + truncated on the returned content indicate when more data is available. + For the full, priced export, call POST /api/v1/contents, which supports + mode='url' for a presigned download link. + default: inline + content_format: + $ref: '#/components/schemas/ContentsFormat' + description: 'Serialization for card data: ''json_compact'' (default), ''json_records'', + or ''csv''.' + default: json_compact + node_ids: + items: + type: string + type: array + maxItems: 20 + title: Node Ids + description: 'Graph node ids to pin into the search; the /v1/graph endpoints + return these ids. Pinned nodes always become retrieval candidates and + get a strong boost; organic results do not change. Ids are not durable + across knowledge-graph rebuilds: the search skips an id that no longer + resolves (in strict mode it simply cannot match). Malformed ids fail the + request with a 400. Max 20.' + strict: + type: boolean + title: Strict + description: When true, return only data cards that match at least one node + in node_ids (which must then be non-empty). When false (the default), + pinned nodes rank first but organic results still return. Web results + do not change either way. + default: false + additionalProperties: false + type: object + title: DataSourceSettings + description: 'Tako data (card) source settings. It adds the two contents-export + + fields on top of the base count and include_contents. Web sources use the + + base SourceSettings: Tako always inlines web content as raw text, so web + + carries neither field.' + EntityClassName: + type: string + enum: + - Companies + - Cryptocurrencies + - Financial Instruments + - Internet Browsers + - Commodities + - People + - Currencies + - Stock Exchanges + - Securities + - IPOs + - Government Debt Instruments + - Treasury Securities + - Airports + - Airlines + - Vehicle Types + - Transportation Modes + - Drugs + - Drug Categories + - Diseases + - Chemical Elements + - Chemical Compounds + - Celestial Bodies + - Occupations + - Social Media Platforms + - Operating Systems + - Search Engines + - Device Types + - LLMs + - LLM Families + - LLM Benchmarks + - Industries + - NAICS Industries + - BLS Industries + - PSC Categories + - Federal Contractors + - Federal Agencies + - Federal Subagencies + - ICE Contracts + - ICE Contractors + - Elections + - Political Offices + - Pollsters + - Agricultural Products + - Tobacco Products + - Prediction Markets + - Prediction Events + - Real Estate Property Types + - Continents + - World Regions + - Countries + - States + - Counties + - Metro Areas + - Cities + - F1 Drivers + - F1 Teams + - F1 Circuits + - F1 Events + - NASCAR Drivers + - NASCAR Teams + - NASCAR Tracks + - NASCAR Events + - NASCAR Races + - NASCAR Owners + - NASCAR Manufacturers + - Soccer Teams + - Soccer Players + - Soccer Competitions + - Soccer Conferences + - Basketball Teams + - Basketball Players + - Basketball Conferences + - Basketball Divisions + - Baseball Teams + - Baseball Players + - Baseball Conferences + - Baseball Divisions + - Baseball Competitions + - Football Teams + - Football Players + - Football Conferences + - Football Divisions + - Sports + - Sports Leagues + title: EntityClassName + ErrorObject: + properties: + code: + type: string + title: Code + message: + type: string + title: Message + type: object + required: + - code + - message + title: ErrorObject + ExportPricing: + properties: + baseline_usd: + type: number + title: Baseline Usd + description: Flat USD charged once per card export, independent of row count. + row_cpm_usd: + type: number + title: Row Cpm Usd + description: USD charged per 1,000 rows on rows beyond the free allowance + (free_rows). Card-level total across the card's priced sources; no per-source + breakdown. + free_rows: + type: integer + title: Free Rows + description: Rows included at the baseline price before the per-1,000-row + rate (row_cpm_usd) begins to apply. + max_rows_ceiling: + type: integer + title: Max Rows Ceiling + description: Hard cap on rows a single export can return and bill; a larger + requested max_rows is clamped to this. + type: object + required: + - baseline_usd + - row_cpm_usd + - free_rows + - max_rows_ceiling + title: ExportPricing + description: 'Card-CSV export pricing RATE, published so a caller can compute + an export''s + + cost before fetching. Full charge = + + baseline_usd + row_cpm_usd * max(0, rows - free_rows) / 1000, rows <= max_rows_ceiling. + + row_cpm_usd is the card-level total (sum of the card''s distinct priced sources'' + + per-1,000-row rate); no per-source breakdown.' + GeoLocation: + properties: + latitude: + type: number + maximum: 90.0 + minimum: -90.0 + title: Latitude + description: Latitude of the end user (degrees, -90 to 90). + longitude: + type: number + maximum: 180.0 + minimum: -180.0 + title: Longitude + description: Longitude of the end user (degrees, -180 to 180). + additionalProperties: false + type: object + required: + - latitude + - longitude + title: GeoLocation + GraphNode: + properties: + id: + type: string + title: Id + description: Opaque, human-friendly public id (::::, + where prefix is ent for an entity or mt for a metric). Ids are not durable + across knowledge-graph rebuilds — resolve them at request time rather + than storing them. + type: + $ref: '#/components/schemas/GraphNodeType' + name: + type: string + title: Name + aliases: + items: + type: string + type: array + title: Aliases + description: + anyOf: + - type: string + - type: 'null' + title: Description + subtype: + anyOf: + - $ref: '#/components/schemas/EntityClassName' + - type: 'null' + label: + anyOf: + - $ref: '#/components/schemas/NerLabel' + - type: 'null' + description: Public NER label of the node (PERSON, ORG, GPE, ...). Derived + from the node's stored annotation; null when the internal annotation has + no public equivalent. + type: object + required: + - id + - type + - name + title: GraphNode + GraphNodeType: + type: string + enum: + - metric + - entity + title: GraphNodeType + GraphRelatedResponse: + properties: + node: + $ref: '#/components/schemas/GraphNode' + relations: + anyOf: + - items: + $ref: '#/components/schemas/GraphRelation' + type: array + - type: 'null' + title: Relations + relation: + anyOf: + - $ref: '#/components/schemas/GraphRelationPage' + - type: 'null' + inferred_labels: + anyOf: + - items: + $ref: '#/components/schemas/NerLabel' + type: array + - type: 'null' + title: Inferred Labels + description: Labels that Tako NER inferred from `q` when infer_label ran + (boost applied). An empty list means inference ran and found nothing. + The field is absent when you supplied an explicit `label`, when infer_label=false, + or when there was no q. + type: object + required: + - node + title: GraphRelatedResponse + GraphRelation: + properties: + key: + type: string + title: Key + description: 'Stable pagination handle. Fixed keys: metrics, entities, siblings, + part_of, members. Named relations: rel:.' + kind: + $ref: '#/components/schemas/RelationKind' + label: + type: string + title: Label + description: Human-readable group label. + items: + items: + $ref: '#/components/schemas/GraphNode' + type: array + title: Items + total: + type: integer + title: Total + total_capped: + type: boolean + title: Total Capped + description: True when `total` hit the server-side fetch cap; the true count + is at least `total`. Render as 'total+'. + default: false + type: object + required: + - key + - kind + - label + - items + - total + title: GraphRelation + GraphRelationPage: + properties: + key: + type: string + title: Key + description: The relation key of this page. + kind: + $ref: '#/components/schemas/RelationKind' + label: + type: string + title: Label + description: Human-readable group label. + items: + items: + $ref: '#/components/schemas/GraphNode' + type: array + title: Items + total: + type: integer + title: Total + total_capped: + type: boolean + title: Total Capped + description: True when `total` hit the server-side fetch cap; the true count + is at least `total`. Pagination ends at the cap — narrow with `q` to reach + the tail. + default: false + next_cursor: + anyOf: + - type: string + - type: 'null' + title: Next Cursor + type: object + required: + - key + - kind + - label + - items + - total + title: GraphRelationPage + GraphSearchResponse: + properties: + results: + items: + $ref: '#/components/schemas/GraphNode' + type: array + title: Results + inferred_labels: + anyOf: + - items: + $ref: '#/components/schemas/NerLabel' + type: array + - type: 'null' + title: Inferred Labels + description: Labels that Tako NER inferred from `q` when infer_label ran + (boost applied). An empty list means inference ran and found nothing. + The field is absent when you supplied an explicit `label`, when infer_label=false, + or when there was no q. + type: object + required: + - results + title: GraphSearchResponse + HeartbeatEvent: + properties: + kind: + type: string + const: heartbeat + title: Kind + default: heartbeat + type: object + title: HeartbeatEvent + KnowledgeCardMethodology: + properties: + methodology_name: + anyOf: + - type: string + - type: 'null' + title: Methodology Name + description: The name of the methodology + examples: + - Where the Data Comes From - S&P Global + methodology_description: + anyOf: + - type: string + - type: 'null' + title: Methodology Description + description: 'A concise, one-sentence summary of the methodology: the source + and what it measures. When no concise summary is available, this carries + the full methodology text.' + examples: + - Financial metrics standardized by S&P Global from company regulatory filings, + press releases, and restatements. + type: object + required: + - methodology_name + - methodology_description + title: KnowledgeCardMethodology + KnowledgeCardRelevance: + type: string + enum: + - High + - Medium + - Low + title: KnowledgeCardRelevance + MetricDefinition: + properties: + name: + type: string + title: Name + description: The metric's display name + examples: + - Gross Domestic Product (current US$) + definition: + type: string + title: Definition + description: Human-readable definition of the metric + type: object + required: + - name + - definition + title: MetricDefinition + description: 'Definition of a metric shown on a card. Sourced from the metric''s + + ValueType definition — the same text as the in-app methodology ''Metrics'' + + tab.' + NerLabel: + type: string + enum: + - PERSON + - ORG + - GPE + - LOC + - PRODUCT + - EVENT + - LANGUAGE + - MONEY + - METRIC + - STOCK_TICKER + - WEBSITE + title: NerLabel + OutputSettings: + properties: + image_dark_mode: + anyOf: + - type: boolean + - type: 'null' + title: Image Dark Mode + description: Whether to render card preview images in dark mode. + force_refresh: + type: boolean + title: Force Refresh + description: 'Instant mode only, and currently informational: on these endpoints, + instant (effort=''instant'') always operates in build-and-refresh mode + regardless of this flag. Instant retrieves data for embeds that are missing + or stale, then creates or refreshes their static embeds. Embeds that are + already fresh (within the refresh cadence) return as-is; Tako does not + re-retrieve them. Identical queries therefore reuse the same content-addressed + embed and return a stable embed URL.' + default: false + additionalProperties: false + type: object + title: OutputSettings + ReasoningEvent: + properties: + kind: + type: string + const: reasoning + title: Kind + default: reasoning + id: + type: string + title: Id + delta: + type: string + title: Delta + done: + type: boolean + title: Done + default: false + type: object + required: + - id + - delta + title: ReasoningEvent + RelationKind: + type: string + enum: + - related + - data + - sibling + - membership + title: RelationKind + description: How Tako derived a relation group. + ResultContent: + properties: + content_format: + anyOf: + - $ref: '#/components/schemas/ContentsFormat' + - type: 'null' + description: 'Serialization of the returned card data: ''csv'', ''json_records'', + or ''json_compact''. Null for web text (always returned as raw text) and + for a quote-only response (nothing was serialized).' + cost: + type: number + title: Cost + description: Price of this item in USD. On a /contents response this is + the amount actually billed; on a quote_only response or a search/answer + downloadable card it is a prospective /contents export price (nothing + was fetched or billed). Pair with export_pricing to compute a larger export's + cost before fetching. + default: 0.0 + data: + anyOf: + - type: string + - type: 'null' + title: Data + description: 'Inline payload as text: card data serialized to CSV, or a + web page''s extracted text. Set only for the ''csv'' card format and for + web text; null otherwise.' + records: + anyOf: + - items: + additionalProperties: + anyOf: + - type: string + - type: number + - type: integer + - type: boolean + - type: 'null' + type: object + type: array + - type: 'null' + title: Records + description: 'Inline card data as verbose JSON: a list of row objects keyed + by column name. Set only when content_format is ''json_records''.' + dataset: + anyOf: + - $ref: '#/components/schemas/TakoDataset' + - type: 'null' + description: Inline card data as a compact TakoDataset (typed column headers + plus positional row arrays). Set only when content_format is 'json_compact'. + url: + anyOf: + - type: string + - type: 'null' + title: Url + description: Presigned download URL for the content, returned in 'url' delivery + mode. Null for inline delivery and for quotes; pair with expires_at. + expires_at: + anyOf: + - type: string + - type: 'null' + title: Expires At + description: ISO-8601 timestamp after which the presigned url stops working. + Null whenever url is null. + total_rows: + anyOf: + - type: integer + - type: 'null' + title: Total Rows + description: True total number of rows in the card's data, independent of + how many rows were returned. Compare with truncated to tell whether more + rows are available via a larger max_rows. Null for web text. + truncated: + type: boolean + title: Truncated + description: True when the returned rows were capped (by the request's max_rows + or the 2,000-row system ceiling) and total_rows exceeds the number returned. + default: false + export_pricing: + anyOf: + - $ref: '#/components/schemas/ExportPricing' + - type: 'null' + description: Rate card for a downloadable card CSV, so a caller can compute + a full export's cost before fetching. Null for web text and other non-downloadable + content. + manifest: + anyOf: + - items: + $ref: '#/components/schemas/ColumnDescriptor' + type: array + - type: 'null' + title: Manifest + description: 'Per-column metadata, one entry per exported column, in column + order: entry i describes column i (CSV header i, json_records key i, or + dataset.columns[i]). Each entry carries name (the column header), dtype, + unit, metric, and entity. Null for web or quote responses that carry no + tabular columns.' + type: object + title: ResultContent + description: 'Describes the downloadable content behind a result. + + + Pricing: for a Tako card CSV export, the charge is a flat per-export + + baseline plus a per-source, per-row CPM on the rows returned beyond the + + free-row allowance. Web text bills at the standard Contents rate. + + + The meaning of `cost` depends on the surface. On `/contents` responses, + + `cost` is the actual charge, and it reconciles with what that response + + billed. The exception is a `quote_only` response, where `cost` is a + + prospective price: Tako neither fetched nor billed the export, and the + + payload and url fields are null. On search and answer downloadable + + cards, `cost` is a prospective `/contents` quote (the per-export + + baseline floor), not what this response billed. On a metered + + `include_contents` request, the per-card `cost` therefore diverges from + + `usage.total_cost_usd` (the inline preview bills about $0), and the sum + + of per-card `cost` will not reconcile with `usage`. Treat `cost` plus + + `export_pricing` as the price of a `/contents` export of this card. + + + `export_pricing` carries the rate so that a caller can compute the full + + charge before fetching: baseline_usd + row_cpm_usd * max(0, rows - + + free_rows) / 1000, with rows <= max_rows_ceiling. `export_pricing` is + + null for web text and non-downloadable content. + + + Tako populates exactly one payload group once it delivers contents: + + `data` (CSV or web text), `records` (verbose JSON), `dataset` (compact + + TakoDataset), or `url` plus `expires_at` (presigned download). + + `content_format` names the serialization; it is null for web text and + + for an undelivered quote. When every payload field is unset, this is + + just the quote (`cost`); fetch the content later via the Contents + + endpoint.' + RetrievalAgentEffort: + type: string + enum: + - medium + title: RetrievalAgentEffort + description: 'Effort taxonomy for the Retrieval Agent (POST /v1/agent/retrieval/runs). + + + Only ''medium'' is available today. `effort` stays a request field for + + forward compatibility and for parity with AnswerAgentEffort. The values + + ''low'' and ''high'' return 400 at validation.' + RetrievalAgentResult: + properties: + answer: + anyOf: + - type: string + - type: 'null' + title: Answer + cards: + items: + $ref: '#/components/schemas/TakoCard' + type: array + title: Cards + citations: + items: + $ref: '#/components/schemas/AgentAnswerCitation' + type: array + title: Citations + metadata: + anyOf: + - $ref: '#/components/schemas/AgentAnswerMetadata' + - type: 'null' + structured_output: + anyOf: + - additionalProperties: true + type: object + - type: 'null' + title: Structured Output + description: Caller-shaped structured output (present only if the request + carried output_schema and status is not 'failed'). Dataset slots contain + a TakoDataset envelope of exact retrieved rows, or null when the agent + left them unfilled. + structured_output_status: + anyOf: + - $ref: '#/components/schemas/StructuredOutputStatus' + - type: 'null' + description: complete | partial | failed. Present when the request supplied + output_schema and the run reached the finalize step. It can be absent + when the whole run failed or timed out before finalize; the run-level + status and error convey that, and the echoed request.output_schema still + identifies the run as structured. + structured_output_citations: + anyOf: + - additionalProperties: + items: + type: integer + type: array + type: object + - type: 'null' + title: Structured Output Citations + description: Best-effort field-path -> citation-index map joining the top-level + citations registry (same [n] index space as the answer). Absent when status + is 'failed'. + unfilled_fields: + anyOf: + - items: + type: string + type: array + - type: 'null' + title: Unfilled Fields + description: Dot-separated paths of unfilled dataset slots; present only + if status is 'partial'. + structured_output_error: + anyOf: + - $ref: '#/components/schemas/ErrorObject' + - type: 'null' + description: Why structured output failed; present only if status is 'failed'. + request_id: + anyOf: + - type: string + - type: 'null' + title: Request Id + type: object + title: RetrievalAgentResult + description: 'Final retrieval-agent output. answer is markdown prose with [n] + + citation markers. cards reuse the sibling TakoCard. citations is the + + unified top-level registry for data and web sources — there is no + + web_results field. metadata carries definitions, assumptions, and + + methodology. The structured_output_* fields carry the caller-shaped + + output_schema result; they are present only if the request supplied + + output_schema. See each field''s description for the exact presence + + rules.' + RetrievalAgentResultEvent: + properties: + kind: + type: string + const: agent_result + title: Kind + default: agent_result + id: + type: string + title: Id + data: + $ref: '#/components/schemas/RetrievalAgentResult' + type: object + required: + - id + - data + title: RetrievalAgentResultEvent + RetrievalAgentRun: + properties: + run_id: + type: string + title: Run Id + object: + type: string + const: agent.retrieval.run + title: Object + default: agent.retrieval.run + thread_id: + anyOf: + - type: string + - type: 'null' + title: Thread Id + status: + $ref: '#/components/schemas/AgentRunStatus' + created_at: + type: string + title: Created At + completed_at: + anyOf: + - type: string + - type: 'null' + title: Completed At + result: + anyOf: + - $ref: '#/components/schemas/RetrievalAgentResult' + - type: 'null' + error: + anyOf: + - $ref: '#/components/schemas/ErrorObject' + - type: 'null' + usage: + anyOf: + - $ref: '#/components/schemas/Usage' + - type: 'null' + request: + anyOf: + - $ref: '#/components/schemas/RetrievalAgentRunRequest' + - type: 'null' + type: object + required: + - run_id + - status + - created_at + title: RetrievalAgentRun + description: The retrieval-agent run resource returned by dispatch (202) and + poll (GET). + RetrievalAgentRunList: + properties: + object: + type: string + const: list + title: Object + default: list + data: + items: + $ref: '#/components/schemas/RetrievalAgentRunSummary' + type: array + title: Data + has_more: + type: boolean + title: Has More + default: false + next_cursor: + anyOf: + - type: string + - type: 'null' + title: Next Cursor + type: object + required: + - data + title: RetrievalAgentRunList + description: 'List envelope for GET /v1/agent/retrieval/runs. The shape follows + the + + OpenAI and Exa list convention.' + RetrievalAgentRunRequest: + properties: + query: + type: string + title: Query + description: Natural-language data-retrieval request for the retrieval agent. + examples: + - S&P 500 semiconductor companies' 2024 revenue and YoY growth + thread_id: + anyOf: + - type: string + format: uuid + - type: 'null' + title: Thread Id + description: Existing thread to continue (follow-up). Omit to start a new + thread. + effort: + $ref: '#/components/schemas/RetrievalAgentEffort' + description: Retrieval-agent effort. Only 'medium' is currently supported. + default: medium + source_indexes: + items: + type: string + enum: + - data + - web + type: array + title: Source Indexes + description: 'Which sources the agent may use: ''data'' (curated knowledge), + ''web'' (open-web search), or both. Defaults to [''data'', ''web'']. Tako + accepts the legacy value ''tako'' as a synonym for ''data''.' + examples: + - - data + - - web + - - data + - web + cards: + type: boolean + title: Cards + description: 'Whether the agent may build visualization cards. The default + is true and permissive: the agent may build cards, but it does not have + to (a completed run with no cards is legitimate). Set false to suppress + card building entirely.' + default: true + output_schema: + anyOf: + - additionalProperties: true + type: object + - type: 'null' + title: Output Schema + description: 'JSON Schema for structured output. The agent writes the synthesized + fields. Dataset slots (marked with x-tako-dataset: true) receive exact + retrieved rows as a TakoDataset. Supported subset: the object, array, + string, number, integer, and boolean types, plus enum, required, and description. + Caps: 16KB, depth 5, 64 properties, 4 dataset slots. Violations return + 400 output_schema_invalid.' + locale: + type: string + title: Locale + description: BCP-47 locale. Drives the language of the agent's answer and + the locale used when rendering card preview images. Defaults to en-US. + default: en-US + timezone: + anyOf: + - type: string + - type: 'null' + title: Timezone + description: IANA timezone (for example, 'America/New_York'). The agent + uses it to render dates and times in card preview images. It does not + affect the returned data. + output_settings: + anyOf: + - $ref: '#/components/schemas/AgentOutputSettings' + - type: 'null' + description: Settings that control the response and rendering. + type: object + required: + - query + title: RetrievalAgentRunRequest + description: Request body for POST /v1/agent/retrieval/runs. + RetrievalAgentRunSummary: + properties: + run_id: + type: string + title: Run Id + status: + $ref: '#/components/schemas/AgentRunStatus' + created_at: + type: string + title: Created At + completed_at: + anyOf: + - type: string + - type: 'null' + title: Completed At + thread_id: + anyOf: + - type: string + - type: 'null' + title: Thread Id + usage: + anyOf: + - $ref: '#/components/schemas/Usage' + - type: 'null' + object: + type: string + const: agent.retrieval.run + title: Object + default: agent.retrieval.run + type: object + required: + - run_id + - status + - created_at + title: RetrievalAgentRunSummary + RetrievalAgentStreamEnvelope: + properties: + seq: + type: integer + minimum: 0.0 + title: Seq + run_id: + type: string + title: Run Id + thread_id: + anyOf: + - type: string + - type: 'null' + title: Thread Id + category: + $ref: '#/components/schemas/StreamCategory' + block: + oneOf: + - $ref: '#/components/schemas/ToolCallEvent' + - $ref: '#/components/schemas/ToolResultEvent' + - $ref: '#/components/schemas/ToolErrorEvent' + - $ref: '#/components/schemas/ToolRetryEvent' + - $ref: '#/components/schemas/StatusEvent' + - $ref: '#/components/schemas/SubagentEvent' + - $ref: '#/components/schemas/ReasoningEvent' + - $ref: '#/components/schemas/TextEvent' + - $ref: '#/components/schemas/DataPipelineAnswerEvent' + - $ref: '#/components/schemas/RetrievalAgentResultEvent' + - $ref: '#/components/schemas/RunSummaryEvent' + - $ref: '#/components/schemas/HeartbeatEvent' + - $ref: '#/components/schemas/StreamResetEvent' + - $ref: '#/components/schemas/StreamDoneEvent' + title: Block + discriminator: + propertyName: kind + mapping: + agent_result: '#/components/schemas/RetrievalAgentResultEvent' + data_pipeline_answer: '#/components/schemas/DataPipelineAnswerEvent' + heartbeat: '#/components/schemas/HeartbeatEvent' + reasoning: '#/components/schemas/ReasoningEvent' + run_summary: '#/components/schemas/RunSummaryEvent' + status: '#/components/schemas/StatusEvent' + stream_done: '#/components/schemas/StreamDoneEvent' + stream_reset: '#/components/schemas/StreamResetEvent' + subagent: '#/components/schemas/SubagentEvent' + text: '#/components/schemas/TextEvent' + tool_call: '#/components/schemas/ToolCallEvent' + tool_error: '#/components/schemas/ToolErrorEvent' + tool_result: '#/components/schemas/ToolResultEvent' + tool_retry: '#/components/schemas/ToolRetryEvent' + type: object + required: + - seq + - run_id + - category + - block + title: RetrievalAgentStreamEnvelope + description: 'Public SSE envelope for the Retrieval Agent run stream. It has + the same + + wire shape as AgentStreamEnvelope. The only difference is that the + + terminal agent_result block carries RetrievalAgentResult (unified + + top-level citations, no web_results), so the SSE terminal matches the + + GET poll result.' + RunSummaryEvent: + properties: + kind: + type: string + const: run_summary + title: Kind + default: run_summary + status: + $ref: '#/components/schemas/AgentRunStatus' + created_at: + type: string + title: Created At + completed_at: + anyOf: + - type: string + - type: 'null' + title: Completed At + error: + anyOf: + - $ref: '#/components/schemas/ErrorObject' + - type: 'null' + usage: + anyOf: + - $ref: '#/components/schemas/Usage' + - type: 'null' + type: object + required: + - status + - created_at + title: RunSummaryEvent + description: 'Terminal run metadata. The stream emits it once, immediately before + + stream_done. It mirrors the GET-poll run fields that the stream otherwise + + lacks; the result stays in the agent_result event. status, created_at, + + and completed_at are always present. error appears only on failure. usage + + appears only on metered (pay-as-you-go) runs and is null otherwise. + + agent_result plus run_summary equals the GET poll object. + + + It carries the same envelope `seq` as the stream_done that follows it (it + + has no seq of its own), and the stream suppresses its SSE `id:`, so it + + advances no resume cursor. Identify it by `kind`; never assume `seq` is + + unique per frame.' + SearchEffortLevel: + type: string + enum: + - fast + - instant + - deep + title: SearchEffortLevel + description: 'Public effort taxonomy for search and answer. FAST is the default. + + INSTANT serves cached embeds without a new data retrieval and works in + + all environments. DEEP widens Tako retrieval and adds an LLM rerank for + + higher-quality results; it is slower and bills at a premium tier.' + SearchRequest: + properties: + query: + type: string + title: Query + description: Natural language search query. + examples: + - Intel vs Nvidia headcount since 2013 + effort: + $ref: '#/components/schemas/SearchEffortLevel' + description: 'Search effort level: ''fast'' (default), ''instant'', or ''deep''.' + default: fast + sources: + $ref: '#/components/schemas/Sources' + description: Per-source settings. The search includes an index only if its + key is present. Defaults to {data:{}, web:{}} (data and web, count 5 each). + Tako accepts the legacy key 'tako' as a synonym for 'data'. + location: + anyOf: + - $ref: '#/components/schemas/GeoLocation' + - type: 'null' + description: Optional coordinates of the end user. Resolves the location + for implicit-location queries (for example, weather). An explicit location + in the query overrides these coordinates. + country_code: + type: string + title: Country Code + description: ISO 3166-1 alpha-2 country code for localization. + default: US + locale: + type: string + title: Locale + description: BCP-47 locale tag for language and formatting. + default: en-US + timezone: + anyOf: + - type: string + - type: 'null' + title: Timezone + description: IANA timezone (for example, 'America/New_York'). + output_settings: + anyOf: + - $ref: '#/components/schemas/OutputSettings' + - type: 'null' + description: Settings that control the response shape. + additionalProperties: false + type: object + required: + - query + title: SearchRequest + SearchResponse: + properties: + cards: + items: + $ref: '#/components/schemas/TakoCard' + type: array + title: Cards + web_results: + items: + $ref: '#/components/schemas/WebResult' + type: array + title: Web Results + request_id: + type: string + title: Request Id + usage: + anyOf: + - $ref: '#/components/schemas/Usage' + - type: 'null' + type: object + required: + - request_id + title: SearchResponse + Sources: + properties: + data: + anyOf: + - $ref: '#/components/schemas/DataSourceSettings' + - type: 'null' + description: Tako data source (curated knowledge). The search includes it + only if present. Tako accepts the legacy key 'tako' as a synonym. + web: + anyOf: + - $ref: '#/components/schemas/WebSourceSettings' + - type: 'null' + description: Web source. The search includes it only if present. + additionalProperties: false + type: object + title: Sources + description: 'Per-source settings. The search includes an index only if its + field is + + present. + + + The Tako data source is named `data`. Tako still accepts the legacy key + + `tako` as a synonym and maps it to `data` before validation.' + StatusEvent: + properties: + kind: + type: string + const: status + title: Kind + default: status + message: + type: string + title: Message + parent_id: + anyOf: + - type: string + - type: 'null' + title: Parent Id + type: object + required: + - message + title: StatusEvent + StreamCategory: + type: string + enum: + - content + - activity + - control + title: StreamCategory + StreamDoneEvent: + properties: + kind: + type: string + const: stream_done + title: Kind + default: stream_done + type: object + title: StreamDoneEvent + StreamResetEvent: + properties: + kind: + type: string + const: stream_reset + title: Kind + default: stream_reset + type: object + title: StreamResetEvent + StructuredOutputStatus: + type: string + enum: + - complete + - partial + - failed + title: StructuredOutputStatus + description: Terminal status of the structured-output channel. + SubagentEvent: + properties: + kind: + type: string + const: subagent + title: Kind + default: subagent + agent_id: + type: string + title: Agent Id + subagent_type: + type: string + title: Subagent Type + parent_id: + anyOf: + - type: string + - type: 'null' + title: Parent Id + event: + type: string + enum: + - dispatch + - complete + title: Event + type: object + required: + - agent_id + - subagent_type + - event + title: SubagentEvent + TakoCard: + properties: + card_id: + anyOf: + - type: string + - type: 'null' + title: Card Id + title: + anyOf: + - type: string + - type: 'null' + title: Title + description: + anyOf: + - type: string + - type: 'null' + title: Description + semantic_description: + anyOf: + - type: string + - type: 'null' + title: Semantic Description + webpage_url: + anyOf: + - type: string + - type: 'null' + title: Webpage Url + image_url: + anyOf: + - type: string + - type: 'null' + title: Image Url + embed_url: + anyOf: + - type: string + - type: 'null' + title: Embed Url + sources: + anyOf: + - items: + $ref: '#/components/schemas/TakoCardSource' + type: array + - type: 'null' + title: Sources + methodologies: + anyOf: + - items: + $ref: '#/components/schemas/KnowledgeCardMethodology' + type: array + - type: 'null' + title: Methodologies + source_indexes: + anyOf: + - items: + $ref: '#/components/schemas/TakoSourceIndex' + type: array + - type: 'null' + title: Source Indexes + card_type: + anyOf: + - type: string + - type: 'null' + title: Card Type + relevance: + anyOf: + - $ref: '#/components/schemas/KnowledgeCardRelevance' + - type: 'null' + content: + anyOf: + - $ref: '#/components/schemas/ResultContent' + - type: 'null' + exportable: + type: boolean + title: Exportable + description: Whether the /contents endpoint can download this card's data. + false means the export is not available; use the card's chart and inline + preview instead. true means the export is eligible but not guaranteed; + the /contents endpoint can still return 403, so fall back to the preview + on error. + default: false + relevance_score: + anyOf: + - type: number + - type: 'null' + title: Relevance Score + description: Numeric relevance of this card to the query on a 1.0-5.0 scale + (5.0 = exact match; higher is more relevant). Only populated for entitled + accounts; null otherwise. + nodes: + anyOf: + - items: + $ref: '#/components/schemas/TakoCardNode' + type: array + - type: 'null' + title: Nodes + description: Graph nodes (entities and metrics) behind this card. The response + includes them by default. Absent for web-only cards or when node resolution + was not available. Use each id with /v1/graph/node/{id} for full detail + (aliases, subtype), or pass ids in sources.data.node_ids to pin these + nodes in future searches. + metric_definitions: + anyOf: + - items: + $ref: '#/components/schemas/MetricDefinition' + type: array + - type: 'null' + title: Metric Definitions + description: Definitions of the metrics this card displays (name + definition). + Null when no displayed metric has a definition available. + data_freshness: + anyOf: + - $ref: '#/components/schemas/DataFreshness' + - type: 'null' + description: 'Freshness dates for the card''s data: the coverage date (data_as_of) + and the last refresh date (last_updated). Null when neither date is available.' + type: object + title: TakoCard + description: 'A Tako knowledge card on the search and answer surfaces. It carries + a + + `content` download descriptor.' + TakoCardNode: + properties: + id: + type: string + title: Id + description: Opaque, human-friendly public id (::::, + where prefix is ent for an entity or mt for a metric). Ids are not durable + across knowledge-graph rebuilds — resolve them at request time rather + than storing them. + type: + $ref: '#/components/schemas/GraphNodeType' + name: + type: string + title: Name + description: + anyOf: + - type: string + - type: 'null' + title: Description + type: object + required: + - id + - type + - name + title: TakoCardNode + description: 'Slim graph node for the search and answer card surface + + (TakoCard.nodes). It is deliberately narrower than GraphNode: it carries + + no `aliases` and no `subtype`. Callers that need the full node resolve + + the id via /v1/graph/node/{id}.' + TakoCardSource: + properties: + source_name: + anyOf: + - type: string + - type: 'null' + title: Source Name + description: The name of the source + examples: + - S&P Global + - The World Bank + source_description: + anyOf: + - type: string + - type: 'null' + title: Source Description + description: The description of the source + source_index: + $ref: '#/components/schemas/TakoSourceIndex' + description: The index of the source + examples: + - data + - web + url: + anyOf: + - type: string + - type: 'null' + title: Url + description: The URL of the source + examples: + - https://xignite.com + source_text: + anyOf: + - type: string + - type: 'null' + title: Source Text + description: Raw excerpts retrieved from the source page — the unmodified + web content that grounded the answer. Present for WEB sources; null for + DATA sources. + type: object + required: + - source_index + title: TakoCardSource + description: 'A source that backs a TakoCard on the SDK surfaces. It uses the + + {data, web} TakoSourceIndex taxonomy.' + TakoDatasetColumn: + properties: + name: + type: string + title: Name + description: Column name. + type: + $ref: '#/components/schemas/TakoDatasetColumnType' + description: 'Logical column type: ''string'', ''number'', ''boolean'', + ''date'', or ''datetime''. Temporal cells are ISO-8601 strings.' + unit: + anyOf: + - type: string + - type: 'null' + title: Unit + description: Structured unit for the column values, e.g. 'USD billions', + '%'. Null when unitless. + type: object + required: + - name + - type + title: TakoDatasetColumn + description: Typed header entry; `type` is the JSON-facing column type. + TakoDatasetColumnType: + type: string + enum: + - string + - number + - boolean + - date + - datetime + title: TakoDatasetColumnType + description: 'Logical column type declared in a TakoDataset header. Temporal + cells + + are ISO-8601 strings. Each column declares ''date'' or ''datetime'' on its + + own: a temporal column whose non-null values are all timezone-naive + + midnights declares ''date''.' + TakoDatasetSource: + properties: + name: + type: string + title: Name + description: Human-readable source name (e.g. 'FRED', 'S&P Global'). + index: + type: string + enum: + - data + - web + title: Index + description: 'Source index the rows came from: ''data'' (Tako) or ''web''.' + default: data + type: object + required: + - name + title: TakoDatasetSource + description: 'Per-dataset provenance entry. `index` names the source index, + not a + + citation display number. It is "data" for every dataset today — web + + content never fills a dataset slot.' + TakoSourceIndex: + type: string + enum: + - data + - web + title: TakoSourceIndex + description: 'Public source taxonomy for the SDK card surfaces (v3 search, v1 + answer, + + agent). It is symmetric with the request taxonomy {data, web}.' + TextEvent: + properties: + kind: + type: string + const: text + title: Kind + default: text + id: + type: string + title: Id + delta: + type: string + title: Delta + done: + type: boolean + title: Done + default: false + type: object + required: + - id + - delta + title: TextEvent + ThinVizCard: + properties: + card_id: + anyOf: + - type: string + - type: 'null' + title: Card Id + description: Public ID of the created card. + title: + anyOf: + - type: string + - type: 'null' + title: Title + description: Card title. + description: + anyOf: + - type: string + - type: 'null' + title: Description + description: Card description. + webpage_url: + anyOf: + - type: string + - type: 'null' + title: Webpage Url + description: Hosted page URL for the card. + image_url: + anyOf: + - type: string + - type: 'null' + title: Image Url + description: Static preview image URL for the card. + embed_url: + anyOf: + - type: string + - type: 'null' + title: Embed Url + description: Embeddable URL for the card. + card_type: + anyOf: + - type: string + - type: 'null' + title: Card Type + description: The card's chart type (for example, 'bar'). + visualization_data: + anyOf: + - additionalProperties: true + type: object + - type: 'null' + title: Visualization Data + description: Inline chart config + data for rendering the card. + examples: + - data: + - x: '2013' + y: 10000 + viz_config: + title: Revenue + embed_mode: + anyOf: + - type: string + enum: + - post + - postmessage + - type: 'null' + title: Embed Mode + description: 'How Tako delivers the embed: ''postmessage'' for postMessage + embeds, and ''post'' otherwise.' + type: object + title: ThinVizCard + description: 'Response for POST /api/v1/thin_viz/create — a created visualization + card. + + + Distinct from KnowledgeCard: thin_viz builds a self-contained chart from + + a caller-supplied component schema, so it never carries retrieval + + provenance (sources, source_indexes) or downloadable raw data. Render the + + card via `embed_url` and preview it via `image_url`. + + `visualization_data` carries the inline chart config and data. + + + This schema documents the populated subset of the response. The wire + + payload also includes the remaining KnowledgeCard fields (sources, + + source_indexes, methodologies, data_url, and so on) as null.' + ToolCallEvent: + properties: + kind: + type: string + const: tool_call + title: Kind + default: tool_call + id: + type: string + title: Id + tool: + type: string + title: Tool + status_message: + anyOf: + - type: string + - type: 'null' + title: Status Message + parent_id: + anyOf: + - type: string + - type: 'null' + title: Parent Id + done: + type: boolean + title: Done + default: false + type: object + required: + - id + - tool + title: ToolCallEvent + ToolErrorEvent: + properties: + kind: + type: string + const: tool_error + title: Kind + default: tool_error + id: + type: string + title: Id + tool: + type: string + title: Tool + error: + type: string + title: Error + parent_id: + anyOf: + - type: string + - type: 'null' + title: Parent Id + type: object + required: + - id + - tool + - error + title: ToolErrorEvent + ToolResultEvent: + properties: + kind: + type: string + const: tool_result + title: Kind + default: tool_result + id: + type: string + title: Id + tool: + type: string + title: Tool + elapsed_ms: + type: integer + title: Elapsed Ms + default: 0 + link: + anyOf: + - type: string + - type: 'null' + title: Link + parent_id: + anyOf: + - type: string + - type: 'null' + title: Parent Id + type: object + required: + - id + - tool + title: ToolResultEvent + ToolRetryEvent: + properties: + kind: + type: string + const: tool_retry + title: Kind + default: tool_retry + id: + type: string + title: Id + tool: + type: string + title: Tool + error: + type: string + title: Error + elapsed_ms: + type: integer + title: Elapsed Ms + default: 0 + parent_id: + anyOf: + - type: string + - type: 'null' + title: Parent Id + type: object + required: + - id + - tool + - error + title: ToolRetryEvent + Usage: + properties: + total_cost_usd: + type: number + title: Total Cost Usd + description: Total quoted USD cost of this request. Sum of compute cost + and data cost. + compute: + anyOf: + - $ref: '#/components/schemas/UsageCompute' + - type: 'null' + description: Compute cost breakdown. Present only on surfaces with a compute + step (absent for contents). + data: + anyOf: + - $ref: '#/components/schemas/UsageData' + - type: 'null' + description: Inline-data cost breakdown. Present only when the surface emitted + billable inline data. + type: object + required: + - total_cost_usd + title: Usage + description: 'Usage for one metered request. `total_cost_usd` is always present + (the + + total quoted charge). `compute` and `data` are the additive breakdown; + + each appears only where it applies. total_cost_usd always equals the sum + + of the components that appear.' + UsageCompute: + properties: + cost_usd: + type: number + title: Cost Usd + description: USD cost of running the operation. + type: object + required: + - cost_usd + title: UsageCompute + description: 'The cost of running the operation. Absent on surfaces with no + compute + + step (contents).' + UsageData: + properties: + cost_usd: + type: number + title: Cost Usd + description: USD cost of the inline data delivered in the response. + datasets: + type: integer + title: Datasets + description: Number of billed data units (datasets) included in the response. + type: object + required: + - cost_usd + - datasets + title: UsageData + description: 'The cost and quantity of inline data delivered in the response: + the + + agent per-dataset surcharge, the search and answer include_contents + + charge, or the contents per-item cost. `datasets` is the count of billed + + data units. Absent when the surface did not or cannot emit inline data + + (for example, the answer agent).' + WebCategory: + type: string + enum: + - news + - sports + - finance + title: WebCategory + description: 'Web search category filter. Only ''news'' maps to a provider category + + today; ''sports'' and ''finance'' are accepted but have no effect yet.' + WebResult: + properties: + title: + type: string + title: Title + description: Title of the web page. + url: + type: string + title: Url + description: URL of the web page. + snippet: + anyOf: + - type: string + - type: 'null' + title: Snippet + description: Excerpt(s) from the page that matched the query. + source_name: + anyOf: + - type: string + - type: 'null' + title: Source Name + description: Publisher or domain name, when Tako can extract it from the + URL. + publish_date: + anyOf: + - type: string + - type: 'null' + title: Publish Date + description: Publication date of the page, when available. + content: + anyOf: + - $ref: '#/components/schemas/ResultContent' + - type: 'null' + description: Downloadable content descriptor for this result, fetched via + the Contents endpoint. Web results are always downloadable as text. None + for callers that do not populate it. + citation_number: + anyOf: + - type: integer + - type: 'null' + title: Citation Number + description: 1-based citation number that the answer's inline [N] markers + reference. Set only when the answer inline-cites this result (the Agent + API); None on raw-retrieval surfaces. + type: object + required: + - title + - url + title: WebResult + description: 'A single raw web search result from the WEB source index. + + + Distinct from `KnowledgeCardSource` (a citation inside a synthesized + + answer) and `KnowledgeCard` (a Tako visualization). Web results are + + raw retrieval output — title, URL, optional snippet. They do not depend + + on any LLM synthesis that may also happen over them.' + WebSourceSettings: + properties: + count: + type: integer + maximum: 20.0 + minimum: 1.0 + title: Count + description: Maximum number of results to return for this source. 1-20. + default: 5 + include_contents: + type: boolean + title: Include Contents + description: Inline this source's underlying data directly in the response. + For the Tako data source, that is serialized card data (see content_format). + For web results, that is the extracted text. + default: false + category: + anyOf: + - $ref: '#/components/schemas/WebCategory' + - type: 'null' + description: Restrict web results to a category. 'news' filters to news + sources. 'sports' and 'finance' are accepted but have no effect yet. Omit + for a general web search. + include_domains: + items: + type: string + type: array + maxItems: 20 + title: Include Domains + description: Return only results from these domains (bare hosts, for example + 'cnn.com'). Max 20. + exclude_domains: + items: + type: string + type: array + maxItems: 20 + title: Exclude Domains + description: Drop results from these domains (bare hosts, for example 'cnn.com'). + Max 20. + snippet_max_chars: + type: integer + maximum: 20000.0 + minimum: 1.0 + title: Snippet Max Chars + description: Character cap on the text excerpt returned per web result. + Default 1000, maximum 20000. + default: 1000 + article_content_max_chars: + type: integer + maximum: 1000000.0 + minimum: 1.0 + title: Article Content Max Chars + description: Character cap on the full article text when include_contents + is true. Default 30000, maximum 1000000. + default: 30000 + published_after: + anyOf: + - type: string + format: date + - type: 'null' + title: Published After + description: Keep only web results published on or after this date (ISO + 'YYYY-MM-DD'). Tako applies the date on the provider where possible, and + always filters the returned results. Results with no known publication + date are kept. Omit for no lower bound. + published_before: + anyOf: + - type: string + format: date + - type: 'null' + title: Published Before + description: Keep only web results published on or before this date (ISO + 'YYYY-MM-DD'). Results with no known publication date are kept. Omit for + no upper bound. + additionalProperties: false + type: object + title: WebSourceSettings + description: 'Web source settings. Adds category, domain filters, and character + caps + + on top of the base count and include_contents.' + securitySchemes: + apiKey: + type: apiKey + name: X-API-Key + in: header diff --git a/tests/contract/request.contract.test.ts b/tests/contract/request.contract.test.ts new file mode 100644 index 0000000..1e1cdc5 --- /dev/null +++ b/tests/contract/request.contract.test.ts @@ -0,0 +1,131 @@ +import { describe, expect, it } from "vitest"; +import { buildContentsRequestBody, buildSearchRequestBody } from "../../src/request"; +import { check, propertiesOf, spec } from "./spec"; + +/** + * Request-side contract: every body this SDK sends must validate against Tako's + * published request schemas. + * + * `SearchRequest` and its nested `Sources` / `DataSourceSettings` / + * `WebSourceSettings` / `OutputSettings` declare `additionalProperties: false`, + * so the API rejects an unknown property with a 400 rather than ignoring it — + * verified live, see the P0-1 case below. + * + * `ContentsRequest` is the one request schema that does NOT declare it, so + * unknown properties pass validation there. The contents cases below therefore + * gate field *shape*, not extras. + */ +describe("POST /v3/search — request body contract", () => { + it("sends a spec-valid body for the default config", () => { + const { valid, errors } = check("SearchRequest", buildSearchRequestBody({}, "q")); + expect(errors).toEqual([]); + expect(valid).toBe(true); + }); + + it("sends a spec-valid body for every documented config field", () => { + const body = buildSearchRequestBody( + { + effort: "deep", + countryCode: "GB", + locale: "en-GB", + timezone: "Europe/London", + sources: { data: { count: 10, includeContents: true }, web: { count: 3 } }, + outputSettings: { imageDarkMode: true, forceRefresh: false }, + }, + "q", + ); + const { valid, errors } = check("SearchRequest", body); + expect(errors).toEqual([]); + expect(valid).toBe(true); + }); + + // ---- P0-1: deferDataRetrieval is a field the API removed ---- + + it("DataSourceSettings does not accept defer_data_retrieval", () => { + // Guards the spec fact the bug depends on, so this test explains itself + // if Tako ever reintroduces the field. + expect(propertiesOf("DataSourceSettings")).toEqual([ + "count", + "include_contents", + "mode", + "content_format", + "node_ids", + "strict", + ]); + }); + + it("never emits defer_data_retrieval, whichever source key is used", () => { + // The config option is gone, so no caller can reach the field. This asserts + // the request builder itself cannot emit it either — including via the + // deprecated `tako` alias, which maps onto the same `data` wire key. + for (const sources of [ + { data: { count: 10, includeContents: true } }, + { tako: { count: 10, includeContents: true } }, + ]) { + const body = buildSearchRequestBody({ sources }, "q"); + expect(JSON.stringify(body)).not.toContain("defer"); + expect(check("SearchRequest", body).errors).toEqual([]); + } + }); + + it("rejects defer_data_retrieval if it is ever reintroduced by hand", () => { + // Pins the reason the option was removed: the API forbids unknown + // properties, so smuggling one back in is a 400, not a no-op. + const body = buildSearchRequestBody({ sources: { data: { count: 1 } } }, "q"); + const smuggled = { + ...body, + sources: { data: { ...body.sources!.data, defer_data_retrieval: true } }, + }; + expect(check("SearchRequest", smuggled).errors).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + instancePath: "/sources/data", + keyword: "additionalProperties", + params: { additionalProperty: "defer_data_retrieval" }, + }), + ]), + ); + }); +}); + +/** + * Request-side contract for POST /v1/contents, built through the same helper the + * tool uses (`buildContentsRequestBody`), so a change to the body shape breaks + * these cases rather than sliding past a hand-written copy. + */ +describe("POST /v1/contents — request body contract", () => { + it("sends a spec-valid body in both delivery modes", () => { + for (const mode of ["url", "inline"] as const) { + const body = buildContentsRequestBody("https://tako.com/card/abc123", mode); + expect(check("ContentsRequest", body).errors).toEqual([]); + expect(body.mode).toBe(mode); + } + }); + + it("emits only properties the schema defines", () => { + // ContentsRequest does not set additionalProperties: false, so ajv cannot + // catch an extra field here. Assert it directly instead, so this surface is + // still gated the way the search surface is by the schema itself. + const allowed = propertiesOf("ContentsRequest"); + const body = buildContentsRequestBody("https://tako.com/card/abc123", "inline"); + expect(Object.keys(body).filter((k) => !allowed.includes(k))).toEqual([]); + }); + + it("pins that ContentsRequest is the lone schema without additionalProperties:false", () => { + // Documents the asymmetry the comment above relies on. If Tako tightens this + // schema, this test fails and the comment (plus spec.ts) should be updated. + const forbidsExtras = (name: string) => + spec.components.schemas[name].additionalProperties === false; + for (const name of [ + "SearchRequest", + "Sources", + "DataSourceSettings", + "WebSourceSettings", + "OutputSettings", + "GeoLocation", + ]) { + expect(forbidsExtras(name), `${name} should forbid extras`).toBe(true); + } + expect(forbidsExtras("ContentsRequest")).toBe(false); + }); +}); diff --git a/tests/contract/response.contract.test.ts b/tests/contract/response.contract.test.ts new file mode 100644 index 0000000..490a06e --- /dev/null +++ b/tests/contract/response.contract.test.ts @@ -0,0 +1,183 @@ +import { describe, expect, it } from "vitest"; +import { + ContentItemFromJSON, + ContentsFormat, + SearchResponseFromJSON, + TakoSourceIndex, +} from "tako-sdk"; +import { enumOf, propertiesOf, requiredOf, spec } from "./spec"; + +const schemas: Record = spec.components.schemas; + +/** + * Response-side contract, checked against two independent oracles: + * + * 1. Tako's vendored OpenAPI document (./openapi.yaml). + * 2. `tako-sdk`, Tako's official TypeScript client, generated from that same + * document. + * + * When both agree that a field is absent, this SDK's type declaring it is drift. + * The `tako-sdk` decoders are the sharper oracle: they materialise exactly the + * fields the model knows, so a field they drop is a field the API stopped + * sending. + */ + +describe("SearchResponse / AnswerResponse — P0-2: usage replaced contents_total_cost", () => { + it("the spec declares usage and not contents_total_cost", () => { + expect(propertiesOf("SearchResponse")).toContain("usage"); + expect(propertiesOf("SearchResponse")).not.toContain("contents_total_cost"); + expect(propertiesOf("AnswerResponse")).toContain("usage"); + expect(propertiesOf("AnswerResponse")).not.toContain("contents_total_cost"); + }); + + it("the official client drops contents_total_cost and keeps usage", () => { + const decoded = SearchResponseFromJSON({ + cards: [], + web_results: [], + contents_total_cost: 0, + request_id: "r", + usage: { total_cost_usd: 0.01, compute: { cost_usd: 0.01 } }, + }); + expect(Object.keys(decoded)).not.toContain("contents_total_cost"); + expect(decoded.usage).toEqual({ + total_cost_usd: 0.01, + compute: { cost_usd: 0.01 }, + data: undefined, + }); + }); + + it("Usage carries the total plus an additive compute/data breakdown", () => { + expect(propertiesOf("Usage")).toEqual(["total_cost_usd", "compute", "data"]); + expect(requiredOf("Usage")).toEqual(["total_cost_usd"]); + }); +}); + +describe("SearchResponse — P0-5: only request_id is guaranteed", () => { + it("cards and web_results are optional in the spec", () => { + expect(requiredOf("SearchResponse")).toEqual(["request_id"]); + }); + + it("the official client leaves cards/web_results undefined when omitted", () => { + const decoded = SearchResponseFromJSON({ request_id: "r" }); + expect(decoded.request_id).toBe("r"); + expect(decoded.cards).toBeUndefined(); + expect(decoded.web_results).toBeUndefined(); + }); + + it("AnswerResponse guarantees answer alongside request_id", () => { + expect(requiredOf("AnswerResponse")).toEqual(["answer", "request_id"]); + }); +}); + +describe("ResultContent / ContentItem — P0-3: format became content_format", () => { + it("the spec names the field content_format, not format", () => { + for (const schema of ["ResultContent", "ContentItem"]) { + expect(propertiesOf(schema)).toContain("content_format"); + expect(propertiesOf(schema)).not.toContain("format"); + } + }); + + it("the serialization enum is csv/json_records/json_compact — 'text' is gone", () => { + expect(enumOf("ContentsFormat")).toEqual(["csv", "json_records", "json_compact"]); + expect(enumOf("ContentsFormat")).not.toContain("text"); + expect(Object.values(ContentsFormat)).toEqual(["csv", "json_records", "json_compact"]); + }); + + it("the official client drops `format` and materialises content_format", () => { + const decoded = ContentItemFromJSON({ + source_url: "https://tako.com/card/x", + format: "csv", // what this SDK's type claims + content_format: "csv", // what the API actually sends + cost: 0, + truncated: false, + }); + expect(Object.keys(decoded)).not.toContain("format"); + expect(decoded.content_format).toBe("csv"); + }); + + it("carries payload and pricing fields this SDK omits entirely", () => { + const props = propertiesOf("ResultContent"); + expect(props).toContain("records"); // json_records payload + expect(props).toContain("dataset"); // json_compact payload (TakoDataset) + expect(props).toContain("export_pricing"); // rate card for a priced export + expect(props).toContain("manifest"); // per-column metadata + }); +}); + +describe("TakoCardSource — P0-4: the source index taxonomy collapsed to {data, web}", () => { + it("the spec enum is exactly data | web", () => { + expect(enumOf("TakoSourceIndex")).toEqual(["data", "web"]); + expect(Object.values(TakoSourceIndex)).toEqual(["data", "web"]); + }); + + it("dropped the legacy tako / connected_data / tako_deep_v2 values", () => { + const values = enumOf("TakoSourceIndex"); + for (const legacy of ["tako", "connected_data", "tako_deep_v2"]) { + expect(values).not.toContain(legacy); + } + }); + + it("source_index is a bare enum — there are no segment/private-index object shapes", () => { + // 2.x exported TakoCardSourceIndexSegment and TakoCardSourcePrivateIndex as + // public types. Neither ever existed in the API; both were removed in 3.0. + const names = Object.keys(schemas); + expect(names).not.toContain("CardSourceIndexSegment"); + expect(names).not.toContain("CardSourcePrivateIndex"); + expect(names).not.toContain("TakoCardSourceIndexSegment"); + expect(names).not.toContain("TakoCardSourcePrivateIndex"); + + const sourceIndex = schemas.TakoCardSource.properties.source_index; + expect(sourceIndex).toEqual({ + $ref: "#/components/schemas/TakoSourceIndex", + description: "The index of the source", + examples: ["data", "web"], + }); + }); +}); + +describe("dataset cells — justifies the one patch in types.conformance.ts", () => { + // tako-sdk's generator renders these unions as an empty interface + // (`RowsInnerInner {}`). The spec is unambiguous, so TakoDatasetCell follows + // the spec and the conformance fixture patches the official type instead. + const CELL_UNION = [ + { type: "string" }, + { type: "number" }, + { type: "integer" }, + { type: "boolean" }, + { type: "null" }, + ]; + + it("dataset rows hold string | number | boolean | null", () => { + expect(schemas.TakoDataset.properties.rows.items.items.anyOf).toEqual(CELL_UNION); + }); + + it("json_records values hold the same union", () => { + expect(schemas.ResultContent.properties.records.anyOf[0].items.additionalProperties.anyOf).toEqual( + CELL_UNION, + ); + }); +}); + +describe("methodologies — required keys with nullable values", () => { + it("both fields are required, so the keys are always present", () => { + expect(requiredOf("KnowledgeCardMethodology")).toEqual([ + "methodology_name", + "methodology_description", + ]); + }); +}); + +describe("TakoCard — P0-6: agent-relevant fields missing from this SDK", () => { + it("the spec carries exportable, relevance_score, nodes, metric_definitions, data_freshness", () => { + const props = propertiesOf("TakoCard"); + expect(props).toContain("exportable"); + expect(props).toContain("relevance_score"); + expect(props).toContain("nodes"); + expect(props).toContain("metric_definitions"); + expect(props).toContain("data_freshness"); + }); + + it("data_freshness reports coverage and refresh dates", () => { + expect(propertiesOf("DataFreshness")).toEqual(["data_as_of", "last_updated"]); + }); +}); diff --git a/tests/contract/spec.ts b/tests/contract/spec.ts new file mode 100644 index 0000000..35e5359 --- /dev/null +++ b/tests/contract/spec.ts @@ -0,0 +1,62 @@ +import { readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import Ajv2020 from "ajv/dist/2020"; +import addFormats from "ajv-formats"; +import YAML from "yaml"; + +/** + * Tako's published OpenAPI document, vendored verbatim from + * https://docs.tako.com/api-reference/openapi.yaml — the authoritative + * description of the live API. + * + * Tests validate against it so a mismatch fails CI instead of shipping. The copy + * is a pinned snapshot; refresh it with `pnpm spec:refresh`. + */ +const SPEC_PATH = fileURLToPath(new URL("./openapi.yaml", import.meta.url)); + +export const spec = YAML.parse(readFileSync(SPEC_PATH, "utf8")); + +const ajv = new Ajv2020({ + strict: false, // the document carries OpenAPI keywords ajv doesn't know + allErrors: true, +}); +addFormats(ajv); +ajv.addSchema(spec, "openapi"); + +/** + * Compile a validator for one `#/components/schemas/` entry. + * + * Where a request schema declares `additionalProperties: false`, the API rejects + * unknown properties with a 400 rather than ignoring them — so a body that fails + * here fails against the live API too. Note that `ContentsRequest` does *not* + * declare it, so unknown properties pass validation on that schema. + */ +export function validator(schemaName: string) { + const validate = ajv.getSchema(`openapi#/components/schemas/${schemaName}`); + if (!validate) throw new Error(`No such schema in the Tako spec: ${schemaName}`); + return validate; +} + +/** Validate `value`, returning the ajv error list (empty when valid). */ +export function check(schemaName: string, value: unknown) { + const validate = validator(schemaName); + const valid = validate(value); + return { valid, errors: validate.errors ?? [] }; +} + +/** The property names the spec allows on a schema. */ +export function propertiesOf(schemaName: string): string[] { + return Object.keys(spec.components.schemas[schemaName].properties ?? {}); +} + +/** The `required` list the spec declares for a schema. */ +export function requiredOf(schemaName: string): string[] { + return spec.components.schemas[schemaName].required ?? []; +} + +/** Resolve a schema's enum values, following a single `$ref` hop if present. */ +export function enumOf(schemaName: string): unknown[] { + const schema = spec.components.schemas[schemaName]; + if (!schema) throw new Error(`No such schema in the Tako spec: ${schemaName}`); + return schema.enum ?? []; +} diff --git a/tests/contract/types.conformance.test.ts b/tests/contract/types.conformance.test.ts new file mode 100644 index 0000000..f4cf6d2 --- /dev/null +++ b/tests/contract/types.conformance.test.ts @@ -0,0 +1,55 @@ +import { execFile } from "node:child_process"; +import { promisify } from "node:util"; +import { fileURLToPath } from "node:url"; +import { describe, expect, it } from "vitest"; + +const execFileAsync = promisify(execFile); +const REPO_ROOT = fileURLToPath(new URL("../..", import.meta.url)); + +/** + * Compiles tests/contract/types.conformance.ts and returns tsc's diagnostics + * (empty string on a clean compile). + * + * That file assigns `tako-sdk`'s official generated types — produced from the + * same OpenAPI document the live API is built from — into this SDK's declared + * public types, and back again. Any diagnostic is real drift: a field this SDK + * promises TypeScript will be present which the API does not send, or an enum + * value one side knows and the other doesn't. + * + * Keeping this green is what stops `src/types.ts` silently rotting against the + * API the way it did between 2.0.1 and 3.0.0. When Tako ships an API change and + * `tako-sdk` is bumped, this test fails until the types catch up. + */ +async function compileConformance(): Promise { + try { + await execFileAsync("pnpm", ["exec", "tsc", "-p", "tsconfig.conformance.json"], { + cwd: REPO_ROOT, + }); + return ""; // exit 0 — tsc ran and found nothing + } catch (err) { + const e = err as { code?: unknown; stdout?: string; stderr?: string }; + + // tsc reports diagnostics on stdout and exits non-zero. Anything else means + // the check never ran — a spawn failure, or pnpm/node refusing to start. + // Returning "" there would make "could not check" indistinguishable from + // "no drift", so this test would pass while proving nothing. + if (typeof e.code === "number" && e.stdout && e.stdout.trim() !== "") { + return e.stdout; + } + throw new Error( + `The conformance compile did not run, so drift is unverified. ` + + `exit=${String(e.code)}\nstdout: ${e.stdout ?? "(empty)"}\nstderr: ${e.stderr ?? "(empty)"}`, + ); + } +} + +describe("type conformance against tako-sdk (the official generated client)", () => { + it( + "this SDK's types match the API", + async () => { + expect(await compileConformance()).toBe(""); + }, + // A cold tsc run over src + the conformance fixture. + 60_000, + ); +}); diff --git a/tests/contract/types.conformance.ts b/tests/contract/types.conformance.ts new file mode 100644 index 0000000..f723912 --- /dev/null +++ b/tests/contract/types.conformance.ts @@ -0,0 +1,196 @@ +/** + * Type-level conformance between this SDK's public types and `tako-sdk`, Tako's + * official client generated from the published OpenAPI document. + * + * This file is deliberately NOT part of `pnpm typecheck`. It is compiled by + * `types.conformance.test.ts`, which asserts it compiles clean. Every + * assignment below is a question: + * + * "Can this SDK's declared type actually hold what the API sends?" + * + * A clean compile means yes. Any error is a real runtime hazard: a field the + * SDK promises TypeScript will be there, which the API does not send. + * + * The `Tako*Response` types are the wire shapes and must match the official + * client field for field. The `Tako*Result` types are what the tools return + * after normalization, so they are checked in the other direction: whatever the + * SDK guarantees must be a shape the API could actually have produced. + */ +import type { + ContentItem as OfficialContentItem, + ContentsFormat as OfficialContentsFormat, + ResultContent as OfficialResultContent, + SearchResponse as OfficialSearchResponse, + AnswerResponse as OfficialAnswerResponse, + ContentsResponse as OfficialContentsResponse, + TakoCard as OfficialTakoCard, + TakoDataset as OfficialTakoDataset, + WebResult as OfficialWebResult, + TakoSourceIndex as OfficialTakoSourceIndex, + // Nested mirrored types — gated for key symmetry below. + ColumnDescriptor as OfficialColumnDescriptor, + DataFreshness as OfficialDataFreshness, + ExportPricing as OfficialExportPricing, + KnowledgeCardMethodology as OfficialKnowledgeCardMethodology, + MetricDefinition as OfficialMetricDefinition, + TakoCardNode as OfficialTakoCardNode, + TakoCardSource as OfficialTakoCardSource, + TakoDatasetColumn as OfficialTakoDatasetColumn, + TakoDatasetSource as OfficialTakoDatasetSource, + Usage as OfficialUsage, + UsageCompute as OfficialUsageCompute, + UsageData as OfficialUsageData, +} from "tako-sdk"; +import type { + TakoAnswerResponse, + TakoAnswerResult, + TakoCard, + TakoCardNode, + TakoCardSource, + TakoColumnDescriptor, + TakoContentFormat, + TakoContentItem, + TakoContentsResponse, + TakoContentsResult, + TakoDataFreshness, + TakoDataset, + TakoDatasetColumn, + TakoDatasetSource, + TakoExportPricing, + TakoKnowledgeCardMethodology, + TakoMetricDefinition, + TakoResultContent, + TakoSearchResponse, + TakoSearchResult, + TakoSourceIndex, + TakoUsage, + TakoUsageCompute, + TakoUsageData, + TakoWebResult, +} from "../../src/types"; + +/** + * One documented deviation from the official client. + * + * The spec types dataset cells and `records` values as + * `anyOf [string, number, integer, boolean, null]`, but the OpenAPI generator + * renders that union as an empty interface (`RowsInnerInner {}`), which admits + * objects and rejects null — strictly less precise than the spec. + * `TakoDatasetCell` follows the spec instead, so the official type is patched at + * that one leaf rather than degrading ours. `response.contract.test.ts` pins the + * spec's actual union so this stays evidence-based. + * + * Everything outside these two fields is compared strictly. + */ +type Cell = string | number | boolean | null; +type FixedDataset = Omit & { rows: Cell[][] }; +type FixedContent = Omit & { + records?: Record[] | null; + dataset?: FixedDataset | null; +}; +type WithFixedContent = Omit & { content?: FixedContent | null }; +type FixedCard = WithFixedContent; +type FixedWebResult = WithFixedContent; +type WithFixedCollections = Omit & { + cards?: FixedCard[]; + web_results?: FixedWebResult[]; +}; + +declare const officialSearch: WithFixedCollections; +declare const officialAnswer: WithFixedCollections; +declare const officialContents: Omit & { + contents?: (Omit & FixedContent)[]; +}; +declare const officialContentItem: Omit & FixedContent; +declare const officialCard: FixedCard; +declare const officialSourceIndex: OfficialTakoSourceIndex; +declare const officialFormat: OfficialContentsFormat; + +// --- Wire types: must accept exactly what the API sends --- + +export const search: TakoSearchResponse = officialSearch; +export const answer: TakoAnswerResponse = officialAnswer; +export const contents: TakoContentsResponse = officialContents; +export const contentItem: TakoContentItem = officialContentItem; +export const card: TakoCard = officialCard; + +// --- Enums: must agree in both directions --- + +export const format: TakoContentFormat = officialFormat; +export const formatBack: OfficialContentsFormat = null as unknown as TakoContentFormat; +export const sourceIndex: TakoSourceIndex = officialSourceIndex; +export const sourceIndexBack: OfficialTakoSourceIndex = null as unknown as TakoSourceIndex; + +// --- Key symmetry --- +// +// The assignments above prove our types are not over-strict, but they cannot +// catch drift by a whole key in either direction: an optional key we declare and +// the API does not send is still assignable, and a key the API sends that we +// omit is simply ignored (no excess-property check applies to a `declare const`). +// Deleting `data_freshness` from TakoCard passed every assignment above. +// +// This compares key sets directly, so both classes fail the compile and the +// error names the offending key. Compared against the unpatched official types: +// only value types were ever patched, never key sets. +type KeyDiff = + | Exclude + | Exclude; +type SameKeys = [KeyDiff] extends [never] + ? true + : { KEY_DRIFT: KeyDiff }; + +export const cardKeys: true = true as SameKeys; +export const webResultKeys: true = true as SameKeys; +export const resultContentKeys: true = true as SameKeys; +export const contentItemKeys: true = true as SameKeys; +export const datasetKeys: true = true as SameKeys; +export const searchKeys: true = true as SameKeys; +export const answerKeys: true = true as SameKeys; +export const contentsKeys: true = true as SameKeys; + +// Nested types need the same gate. Checking only the eight above leaves both +// drift classes live one level down: deleting `last_updated` from +// TakoDataFreshness compiled clean, because `keyof TakoCard` never changes and +// the narrowed nested type stays assignable. That is the P0-6 regression sitting +// directly under the field the check was written to protect. +export const dataFreshnessKeys: true = true as SameKeys; +export const cardSourceKeys: true = true as SameKeys; +export const cardNodeKeys: true = true as SameKeys; +export const metricDefinitionKeys: true = true as SameKeys< + TakoMetricDefinition, + OfficialMetricDefinition +>; +export const methodologyKeys: true = true as SameKeys< + TakoKnowledgeCardMethodology, + OfficialKnowledgeCardMethodology +>; +export const usageKeys: true = true as SameKeys; +export const usageComputeKeys: true = true as SameKeys; +export const usageDataKeys: true = true as SameKeys; +export const exportPricingKeys: true = true as SameKeys; +export const columnDescriptorKeys: true = true as SameKeys< + TakoColumnDescriptor, + OfficialColumnDescriptor +>; +export const datasetColumnKeys: true = true as SameKeys< + TakoDatasetColumn, + OfficialTakoDatasetColumn +>; +export const datasetSourceKeys: true = true as SameKeys< + TakoDatasetSource, + OfficialTakoDatasetSource +>; + +// --- Result types: the normalized shapes the tools return --- +// +// Checked the other way round. Normalization only fills in absent collections, +// so every result must still be a valid wire response — that proves the +// normalizer adds guarantees without inventing fields. + +declare const searchResult: TakoSearchResult; +declare const answerResult: TakoAnswerResult; +declare const contentsResult: TakoContentsResult; + +export const searchResultIsWireValid: WithFixedCollections = searchResult; +export const answerResultIsWireValid: WithFixedCollections = answerResult; +export const contentsResultIsWireValid: typeof officialContents = contentsResult; diff --git a/tests/index.test.ts b/tests/index.test.ts index cdf4063..fc27dc3 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -12,7 +12,7 @@ describe("index barrel", () => { }); it("the exported takoSearch produces a working tool", async () => { - stubFetch(200, JSON.stringify({ cards: [], web_results: [], contents_total_cost: 0, request_id: "r" })); + stubFetch(200, JSON.stringify({ cards: [], web_results: [], request_id: "r" })); const res = await runTool(takoSearch({ apiKey: "key" }), { query: "x" }); expect((res as any).request_id).toBe("r"); }); diff --git a/tests/search.test.ts b/tests/search.test.ts index 7fc3329..fc20480 100644 --- a/tests/search.test.ts +++ b/tests/search.test.ts @@ -2,7 +2,7 @@ import { describe, it, expect, afterEach, vi } from "vitest"; import { takoSearch } from "../src/tools/search"; import { stubFetch, runTool } from "./_helpers"; -const OK = JSON.stringify({ cards: [], web_results: [], contents_total_cost: 0, request_id: "r" }); +const OK = JSON.stringify({ cards: [], web_results: [], request_id: "r" }); afterEach(() => vi.unstubAllGlobals()); @@ -28,7 +28,7 @@ describe("takoSearch", () => { const t = takoSearch({ apiKey: "key", effort: "deep", - sources: { data: { count: 10, deferDataRetrieval: true } }, + sources: { data: { count: 10, includeContents: true } }, timezone: "America/New_York", outputSettings: { imageDarkMode: true }, }); @@ -39,7 +39,7 @@ describe("takoSearch", () => { effort: "deep", country_code: "US", locale: "en-US", - sources: { data: { count: 10, defer_data_retrieval: true } }, + sources: { data: { count: 10, include_contents: true } }, timezone: "America/New_York", output_settings: { image_dark_mode: true }, }); @@ -49,19 +49,45 @@ describe("takoSearch", () => { const fetchMock = stubFetch(200, OK); const t = takoSearch({ apiKey: "key", - sources: { tako: { count: 10, deferDataRetrieval: true } }, + sources: { tako: { count: 10, includeContents: true } }, }); await runTool(t, { query: "x" }); const init = fetchMock.mock.calls[0][1] as RequestInit; const body = JSON.parse(init.body as string); - expect(body.sources).toEqual({ data: { count: 10, defer_data_retrieval: true } }); + expect(body.sources).toEqual({ data: { count: 10, include_contents: true } }); + }); + + it("normalizes absent collections to empty arrays", async () => { + // The API guarantees only request_id, so a valid response can omit both + // collections. Callers still get arrays they can read without a guard. + stubFetch(200, JSON.stringify({ request_id: "r" })); + const res = (await runTool(takoSearch({ apiKey: "key" }), { query: "x" })) as any; + expect(res.cards).toEqual([]); + expect(res.web_results).toEqual([]); + expect(res.request_id).toBe("r"); + }); + + it("passes usage through untouched", async () => { + stubFetch( + 200, + JSON.stringify({ + request_id: "r", + usage: { total_cost_usd: 0.03, compute: { cost_usd: 0.01 }, data: { cost_usd: 0.02, datasets: 2 } }, + }), + ); + const res = (await runTool(takoSearch({ apiKey: "key" }), { query: "x" })) as any; + expect(res.usage).toEqual({ + total_cost_usd: 0.03, + compute: { cost_usd: 0.01 }, + data: { cost_usd: 0.02, datasets: 2 }, + }); }); it("honors baseUrl override (trailing slash stripped)", async () => { const fetchMock = stubFetch(200, OK); - const t = takoSearch({ apiKey: "key", baseUrl: "https://staging.trytako.com/" }); + const t = takoSearch({ apiKey: "key", baseUrl: "https://e.com/" }); await runTool(t, { query: "x" }); - expect(fetchMock.mock.calls[0][0]).toBe("https://staging.trytako.com/api/v3/search"); + expect(fetchMock.mock.calls[0][0]).toBe("https://e.com/api/v3/search"); }); it("falls back to TAKO_API_KEY env and throws clearly when unset", async () => { diff --git a/tests/types.test.ts b/tests/types.test.ts index eeb0f80..eeffc3e 100644 --- a/tests/types.test.ts +++ b/tests/types.test.ts @@ -4,11 +4,12 @@ import type { TakoAnswerResult, TakoContentsResult, TakoCard, + TakoCardSource, TakoWebResult, TakoContentItem, TakoRetrievalConfig, TakoContentsConfig, - TakoKnowledgeCardSource, + TakoUsage, } from "../src/types"; describe("types", () => { @@ -18,60 +19,101 @@ describe("types", () => { title: "Nvidia vs AMD", semantic_description: "headcount", relevance: "High", - source_indexes: ["tako", { index_type: "connected_data", segment_id: "123" }], - sources: [{ source_name: "S&P", source_description: null, source_index: "tako", url: null }], + relevance_score: 4.5, + exportable: true, + source_indexes: ["data", "web"], + sources: [{ source_name: "S&P", source_description: null, source_index: "data", url: null }], methodologies: [{ methodology_name: "m", methodology_description: null }], - content: { format: "csv", cost: 0 }, + nodes: [{ id: "ent::nvidia::ab12", type: "entity", name: "Nvidia" }], + metric_definitions: [{ name: "Full Time Employees", definition: "Headcount at fiscal year end." }], + data_freshness: { data_as_of: "2026-01-31", last_updated: "2026-02-14" }, + content: { content_format: "csv", cost: 0 }, }; const web: TakoWebResult = { title: "W", url: "https://e.com", citation_number: 1 }; - const res: TakoSearchResult = { cards: [card], web_results: [web], contents_total_cost: 0, request_id: "r" }; + const usage: TakoUsage = { total_cost_usd: 0.02, compute: { cost_usd: 0.02 } }; + const res: TakoSearchResult = { cards: [card], web_results: [web], request_id: "r", usage }; expect(res.cards[0].card_id).toBe("c1"); + expect(res.usage?.total_cost_usd).toBe(0.02); }); - it("models all three source_index shapes", () => { - const sources: TakoKnowledgeCardSource[] = [ - { source_name: "S&P", source_description: null, source_index: "tako", url: null }, - { - source_name: "Segment", - source_description: null, - source_index: { index_type: "connected_data", segment_id: "123" }, - url: null, - }, - { - source_name: "Private", - source_description: null, - source_index: { index_type: "connected_data", private_index_id: "pi_1" }, - url: null, - }, + it("models a card source over the {data, web} taxonomy", () => { + const sources: TakoCardSource[] = [ + { source_name: "S&P", source_index: "data" }, + { source_name: "Reuters", source_index: "web", source_text: "raw excerpt" }, ]; - expect(sources).toHaveLength(3); + expect(sources.map((s) => s.source_index)).toEqual(["data", "web"]); }); it("models an answer result", () => { - const res: TakoAnswerResult = { answer: "A", cards: [], web_results: [], contents_total_cost: 0, request_id: "r" }; + const res: TakoAnswerResult = { answer: "A", cards: [], web_results: [], request_id: "r" }; expect(res.answer).toBe("A"); }); - it("models a contents result", () => { + it("models a contents result in url mode", () => { const item: TakoContentItem = { source_url: "https://tako.com/card/x", url: "https://signed", expires_at: "2026-01-01T00:00:00Z", - format: "csv", + content_format: "csv", cost: 0, total_rows: 5, truncated: false, + export_pricing: { baseline_usd: 0.01, row_cpm_usd: 0.5, free_rows: 20, max_rows_ceiling: 2000 }, }; const res: TakoContentsResult = { contents: [item], request_id: "r" }; - expect(res.contents[0].format).toBe("csv"); + expect(res.contents[0].content_format).toBe("csv"); + }); + + it("models the json_compact dataset payload", () => { + const item: TakoContentItem = { + source_url: "https://tako.com/card/x", + content_format: "json_compact", + dataset: { + columns: [ + { name: "year", type: "date" }, + { name: "employees", type: "number", unit: "count" }, + ], + rows: [ + ["2024-01-31", 29600], + ["2025-01-31", 36000], + ], + total_rows: 2, + truncated: false, + ref: "https://tako.com/card/x", + sources: [{ name: "S&P Global", index: "data" }], + provenance: "query", + }, + }; + expect(item.dataset?.rows[1][1]).toBe(36000); + }); + + it("models web page text, which carries no content_format", () => { + // The field is optional as well as nullable: web text arrives as either an + // explicit null or an absent key, so both must typecheck and both must be + // caught by a loose `== null` test. + const explicitNull: TakoContentItem = { + source_url: "https://e.com/article", + content_format: null, + data: "extracted prose", + }; + const absent: TakoContentItem = { + source_url: "https://e.com/article", + data: "extracted prose", + }; + expect(explicitNull.content_format).toBeNull(); + expect(absent.content_format).toBeUndefined(); + for (const item of [explicitNull, absent]) { + expect(item.content_format == null).toBe(true); + expect(item.data).toBe("extracted prose"); + } }); it("accepts retrieval + contents config", () => { const r: TakoRetrievalConfig = { apiKey: "k", - baseUrl: "https://staging.trytako.com", + baseUrl: "https://e.com", effort: "deep", - sources: { data: { count: 10, deferDataRetrieval: true }, web: { count: 3, includeContents: true } }, + sources: { data: { count: 10, includeContents: true }, web: { count: 3, includeContents: true } }, countryCode: "US", locale: "en-US", timezone: "America/New_York", diff --git a/tsconfig.check.json b/tsconfig.check.json index 7905020..e6b2696 100644 --- a/tsconfig.check.json +++ b/tsconfig.check.json @@ -1,5 +1,9 @@ { "extends": "./tsconfig.json", "compilerOptions": { "noEmit": true, "rootDir": "." }, - "include": ["src", "tests", "examples"] + "include": ["src", "tests", "examples"], + // Compiled on purpose by tests/contract/types.conformance.test.ts, which + // asserts it compiles clean against tako-sdk. Excluded here so drift surfaces + // as that one named test failing, not as noise across `pnpm typecheck`. + "exclude": ["node_modules", "dist", "tests/contract/types.conformance.ts"] } diff --git a/tsconfig.conformance.json b/tsconfig.conformance.json new file mode 100644 index 0000000..29f3e32 --- /dev/null +++ b/tsconfig.conformance.json @@ -0,0 +1,5 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { "noEmit": true, "rootDir": "." }, + "include": ["tests/contract/types.conformance.ts", "src"] +}