@alvera-ai/platform-sdk 0.10.0-rc.8 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/.agent/AGENTS.md +499 -0
  2. package/.agent/account_management.md +456 -0
  3. package/.agent/action_status_updaters.md +264 -0
  4. package/.agent/ai_agents.md +462 -0
  5. package/.agent/ai_sandbox.md +265 -0
  6. package/.agent/async.md +112 -0
  7. package/.agent/connected_apps.md +408 -0
  8. package/.agent/cookbook/_fixtures/README.md +106 -0
  9. package/.agent/cookbook/_fixtures/accounts_receivable/_customers_accounts_receivable_customer.liquid +32 -0
  10. package/.agent/cookbook/_fixtures/accounts_receivable/_customers_accounts_receivable_mdm.liquid +20 -0
  11. package/.agent/cookbook/_fixtures/accounts_receivable/stripe_customers_batch1.csv +5 -0
  12. package/.agent/cookbook/_fixtures/foundation/_lead_submissions_foundation_generic_table.liquid +33 -0
  13. package/.agent/cookbook/_fixtures/foundation/_lead_submissions_foundation_legal_entity.liquid +88 -0
  14. package/.agent/cookbook/_fixtures/foundation/_lead_submissions_foundation_mdm.liquid +48 -0
  15. package/.agent/cookbook/_fixtures/healthcare/_cahps_appointments_healthcare_appointment.liquid +47 -0
  16. package/.agent/cookbook/_fixtures/healthcare/_cahps_appointments_healthcare_mdm.liquid +24 -0
  17. package/.agent/cookbook/_fixtures/healthcare/_cahps_appointments_healthcare_patient.liquid +38 -0
  18. package/.agent/cookbook/_fixtures/healthcare/memorandum-of-association-01.png +0 -0
  19. package/.agent/cookbook/_fixtures/healthcare/sample_two_page.pdf +43 -0
  20. package/.agent/cookbook/_fixtures/payment_risk/_compliance_screenings_payment_risk_compliance_screening.liquid +59 -0
  21. package/.agent/cookbook/_fixtures/payment_risk/_compliance_screenings_payment_risk_mdm.liquid +36 -0
  22. package/.agent/cookbook/_fixtures/payment_risk/_payment_accounts_payment_risk_mdm.liquid +30 -0
  23. package/.agent/cookbook/_fixtures/payment_risk/_payment_accounts_payment_risk_payment_account.liquid +55 -0
  24. package/.agent/cookbook/_setup/accounts_receivable.md +282 -0
  25. package/.agent/cookbook/_setup/foundation.md +277 -0
  26. package/.agent/cookbook/_setup/healthcare.md +279 -0
  27. package/.agent/cookbook/_setup/payment_risk.md +283 -0
  28. package/.agent/cookbook/action-status-updaters.md +212 -0
  29. package/.agent/cookbook/ai-agent-invoke.md +243 -0
  30. package/.agent/cookbook/appointment-review-sms-workflow.md +761 -0
  31. package/.agent/cookbook/birthday-greeting-sms-trigger.md +656 -0
  32. package/.agent/cookbook/bulk-ingest.md +254 -0
  33. package/.agent/cookbook/contact-us-triage-with-llm.md +622 -0
  34. package/.agent/cookbook/custom-tables.md +201 -0
  35. package/.agent/cookbook/dunning-sms-for-delinquent.md +619 -0
  36. package/.agent/cookbook/invite-team.md +194 -0
  37. package/.agent/cookbook/kyc-notification-on-account-activation.md +619 -0
  38. package/.agent/cookbook/rest-fetch.md +246 -0
  39. package/.agent/cookbook/sanctions-screening-with-agent-review.md +733 -0
  40. package/.agent/cookbook/score-leads-with-llm-categorization.md +624 -0
  41. package/.agent/cookbook/system-templates.md +129 -0
  42. package/.agent/cookbook/triage-prospects-by-priority.md +533 -0
  43. package/.agent/cookbook/welcome-sms-for-customers.md +607 -0
  44. package/.agent/data_activation_clients.md +559 -0
  45. package/.agent/data_sources.md +235 -0
  46. package/.agent/datalakes.md +714 -0
  47. package/.agent/debugging.md +137 -0
  48. package/.agent/errors.md +190 -0
  49. package/.agent/generic_tables.md +351 -0
  50. package/.agent/interoperability_contracts.md +417 -0
  51. package/.agent/mdm.md +293 -0
  52. package/.agent/mutations.md +126 -0
  53. package/.agent/templates.md +98 -0
  54. package/.agent/tool-call-configs.md +90 -0
  55. package/.agent/tools.md +547 -0
  56. package/.agent/type_naming.md +129 -0
  57. package/.agent/workflows.md +617 -0
  58. package/README.md +178 -0
  59. package/dist/bin/platform-sdk.d.mts +1 -0
  60. package/dist/bin/platform-sdk.mjs +106 -0
  61. package/dist/bin/platform-sdk.mjs.map +1 -0
  62. package/dist/index.d.mts +1310 -44116
  63. package/dist/index.d.mts.map +1 -1
  64. package/dist/index.mjs +1194 -7344
  65. package/dist/index.mjs.map +1 -1
  66. package/package.json +19 -10
@@ -0,0 +1,212 @@
1
+ ---
2
+ title: "Capability: reconcile delivery status of the messages you send"
3
+ summary: A capability walk for action status updaters. Register a poller tool that reads a delivery-log source (here CloudWatch via LocalStack) and an action status updater that runs on a cron, maps log events back to the messages that produced them, and updates their status (`api.actionStatusUpdaters.create`). So a "sent" SMS becomes "delivered" / "failed" without you polling by hand.
4
+ industry: accounts_receivable
5
+ slug: action-status-updaters
6
+ vitest_source:
7
+ - integration-tests/tests/accounts_receivable/action-status-updaters.test.ts
8
+ - integration-tests/tests/accounts_receivable/bootstrap.test.ts
9
+ status: green
10
+ ---
11
+
12
+ # Capability
13
+
14
+ **What you get:** the messages you send (SMS, etc.) get their real delivery
15
+ outcome written back automatically — "sent" becomes "delivered" or "failed" —
16
+ on a schedule, with no manual polling.
17
+
18
+ An **action status updater (ASU)** is a scheduled reconciler:
19
+
20
+ - a **poller tool** reads a delivery-log source (CloudWatch log group here;
21
+ LocalStack stands in locally),
22
+ - the ASU runs on a `cron_expression`,
23
+ - its `message_config` Liquid maps each log event back to the message it came
24
+ from (by `external_id`) and the new `status`,
25
+ - `sender_tool_ids` scopes it to the messages a given sender tool produced.
26
+
27
+ This is **global** — the same shape reconciles any sender's delivery status. See
28
+ `action_status_updaters.md` for the wire reference and `tools.md` for the
29
+ poller-tool body.
30
+
31
+ # Walkthrough
32
+
33
+ The `_setup/accounts_receivable.md` bootstrap left `api`, `tenantSlug`,
34
+ `datalakeSlug`, and `ctx.datalakeId` populated.
35
+
36
+ ## 001 — register a data source for the tools
37
+
38
+ Tools attach to a data source — the origin registration. Create one for the SMS
39
+ gateway / log source.
40
+
41
+ ```typescript
42
+ const { data: ds } = await api.dataSources.create(tenantSlug, datalakeSlug, {
43
+ name: `ASU Source ${runSuffix}`,
44
+ uri: 'sns.local',
45
+ description: 'SNS / CloudWatch origin for the dunning-SMS delivery reconciler.',
46
+ status: 'active',
47
+ is_default: false,
48
+ })
49
+ dataSourceId = ds.id!
50
+ ```
51
+
52
+ ## 002 — create the SMS sender tool
53
+
54
+ The ASU reconciles the delivery status of messages a sender tool produced. Create
55
+ an SNS-backed SMS tool (pointed at LocalStack locally) to be that sender.
56
+
57
+ ```typescript
58
+ const { data: smsTool } = await api.tools.create(tenantSlug, datalakeSlug, {
59
+ name: `ASU SMS Sender ${runSuffix}`,
60
+ description: 'SNS-backed SMS sender whose delivery status the ASU reconciles.',
61
+ intent: 'sms',
62
+ status: 'active',
63
+ datalake_id: ctx.datalakeId,
64
+ data_source_id: dataSourceId,
65
+ body: {
66
+ tool_body_type: 'sns',
67
+ auth_method: 'access_key',
68
+ region: 'us-east-1',
69
+ phone_number: '+15551234567',
70
+ endpoint_url: 'http://localhost:4566',
71
+ access_key_id: 'test',
72
+ secret_access_key: 'test',
73
+ },
74
+ })
75
+ ctx.smsToolId = smsTool.id!
76
+ ```
77
+
78
+ ## 003 — create the CloudWatch poller tool
79
+
80
+ The poller tool supplies the credentials + region for reading the delivery log
81
+ group. `intent: 'status_poller'` tags it as a reconciler source (not a sender),
82
+ and `tool_body_type: 'cloud_watch_log_group'` selects the CloudWatch reader.
83
+
84
+ ```typescript
85
+ const { data: cwTool } = await api.tools.create(tenantSlug, datalakeSlug, {
86
+ name: `ASU CloudWatch Poller ${runSuffix}`,
87
+ description: 'CloudWatch log-group poller — reads SNS delivery events for reconciliation.',
88
+ intent: 'status_poller',
89
+ status: 'active',
90
+ datalake_id: ctx.datalakeId,
91
+ data_source_id: dataSourceId,
92
+ body: {
93
+ tool_body_type: 'cloud_watch_log_group',
94
+ auth_method: 'access_key',
95
+ region: 'us-east-1',
96
+ endpoint_url: 'http://localhost:4566',
97
+ access_key_id: 'test',
98
+ secret_access_key: 'test',
99
+ },
100
+ })
101
+ ctx.cwToolId = cwTool.id!
102
+ ```
103
+
104
+ ## 004 — create the action status updater
105
+
106
+ The ASU ties it together: a `cron_expression` schedule, the `updater_tool_id`
107
+ (the poller), the `sender_tool_ids` it reconciles for, and an `updater_body`
108
+ naming the log group + the time window (Liquid `now` filters). The
109
+ `message_config` Liquid loops the fetched `events` and emits, per event, the
110
+ `external_id` of the message to update and the `set_params` (the new `status`).
111
+
112
+ ```typescript
113
+ const { data: asu } = await api.actionStatusUpdaters.create(tenantSlug, datalakeSlug, {
114
+ name: `Dunning SMS Delivery Updater ${runSuffix}`,
115
+ cron_expression: '*/30 * * * *',
116
+ updater_type: 'cloud_watch',
117
+ updater_tool_id: ctx.cwToolId,
118
+ sender_tool_ids: [ctx.smsToolId],
119
+ datalake_id: ctx.datalakeId,
120
+ updater_body: {
121
+ updater_body_type: 'cloud_watch_request',
122
+ log_group_name: 'sns/us-east-1/000000000000/DirectPublishToPhoneNumber',
123
+ start_time: '{{ now | minutes_ago: 45 }}',
124
+ end_time: '{{ now }}',
125
+ },
126
+ message_config: {
127
+ type: 'custom',
128
+ body: '{% for event in events %}{"external_id": "{{ event.message_id }}", "set_params": {"status": "{{ event.status }}"}}{% endfor %}',
129
+ },
130
+ })
131
+ actionStatusUpdaterId = asu.id!
132
+ ```
133
+
134
+ ## 005 — verify it persisted (read-back + checksum parity)
135
+
136
+ `get` returns the stored row; its fields echo what you sent. `checksum` recomputes
137
+ the fingerprint from the same body without persisting — it must equal the stored
138
+ row's `checksum`, which is how drift is detected.
139
+
140
+ ```typescript
141
+ const { data: row } = await api.actionStatusUpdaters.get(tenantSlug, datalakeSlug, actionStatusUpdaterId)
142
+ if (row.cron_expression !== '*/30 * * * *') {
143
+ throw new Error(`cron mismatch on read-back: ${row.cron_expression}`)
144
+ }
145
+
146
+ const { data: ck } = await api.actionStatusUpdaters.checksum(tenantSlug, datalakeSlug, {
147
+ name: row.name,
148
+ cron_expression: row.cron_expression,
149
+ updater_type: 'cloud_watch',
150
+ updater_tool_id: ctx.cwToolId,
151
+ sender_tool_ids: [ctx.smsToolId],
152
+ datalake_id: ctx.datalakeId,
153
+ updater_body: {
154
+ updater_body_type: 'cloud_watch_request',
155
+ log_group_name: 'sns/us-east-1/000000000000/DirectPublishToPhoneNumber',
156
+ start_time: '{{ now | minutes_ago: 45 }}',
157
+ end_time: '{{ now }}',
158
+ },
159
+ message_config: {
160
+ type: 'custom',
161
+ body: '{% for event in events %}{"external_id": "{{ event.message_id }}", "set_params": {"status": "{{ event.status }}"}}{% endfor %}',
162
+ },
163
+ })
164
+ if (typeof ck.checksum !== 'string' || ck.checksum.length === 0) {
165
+ throw new Error('expected a checksum for the ASU body')
166
+ }
167
+ ```
168
+
169
+ ## 006 — discover it (list + metadata)
170
+
171
+ The ASU shows up in the paginated list, and `metadata` / `metadataDetails`
172
+ return the markdown catalog an agent reads to understand what reconcilers exist.
173
+
174
+ ```typescript
175
+ const { data: list } = await api.actionStatusUpdaters.list(tenantSlug, datalakeSlug)
176
+ if (!(list.data ?? []).some((u) => u.id === actionStatusUpdaterId)) {
177
+ throw new Error('created ASU not found in the list')
178
+ }
179
+
180
+ const { data: catalog } = await api.actionStatusUpdaters.metadata(tenantSlug, datalakeSlug)
181
+ if (typeof catalog !== 'string' || catalog.length === 0) {
182
+ throw new Error('expected a non-empty ASU metadata catalog')
183
+ }
184
+
185
+ const { data: detail } = await api.actionStatusUpdaters.metadataDetails(tenantSlug, datalakeSlug, actionStatusUpdaterId)
186
+ if (typeof detail !== 'string' || detail.length === 0) {
187
+ throw new Error('expected non-empty ASU metadata details')
188
+ }
189
+ ```
190
+
191
+ # Gotchas
192
+
193
+ - **The poller tool's intent is `status_poller`, not the sender's intent.** The
194
+ reconciler reads logs; it doesn't send. `sender_tool_ids` is the separate list
195
+ of sender tools whose messages it updates.
196
+ - **`message_config` maps events back to messages by `external_id`.** The Liquid
197
+ emits, per fetched event, the `external_id` of the message to touch and the
198
+ `set_params` to apply. Get the `external_id` field name wrong and nothing
199
+ updates.
200
+ - **The schedule is a cron, evaluated platform-side.** Creating the ASU registers
201
+ the schedule; you don't trigger it. The `updater_body` time window
202
+ (`{{ now | minutes_ago: 45 }}` … `{{ now }}`) bounds each run's log query.
203
+ - **`checksum` parity is your drift signal.** The fingerprint of the desired body
204
+ must match the stored row's; a mismatch means the resource drifted.
205
+
206
+ # See also
207
+
208
+ - `action_status_updaters.md` — wire shape, `updater_body` variants, `message_config`
209
+ - `tools.md` — `sns` sender body + `cloud_watch_log_group` poller body
210
+ - `_setup/accounts_receivable.md` — the bootstrap this walk starts from
211
+ - `integration-tests/tests/accounts_receivable/action-status-updaters.test.ts` —
212
+ the green test these calls are lifted from
@@ -0,0 +1,243 @@
1
+ ---
2
+ title: "Capability: read an uploaded document and pull structured fields out of it"
3
+ summary: A capability walk for direct AI-agent invocation with file vision. Upload a PDF + image, register an LLM-enrichment tool (the provider adapter) and an AI agent constrained to an output schema, then invoke the agent with the files (`api.aiAgents.invoke`) to get structured JSON back. This is the correct path for reading an uploaded file — the file is uploaded to storage and the agent reads it; the agent does NOT ingest the file as a dataset row.
4
+ industry: healthcare
5
+ slug: ai-agent-invoke
6
+ vitest_source:
7
+ - integration-tests/tests/healthcare/ai-agent-invoke.test.ts
8
+ - integration-tests/tests/healthcare/bootstrap.test.ts
9
+ status: green
10
+ ---
11
+
12
+ # Capability
13
+
14
+ **What you get:** structured JSON pulled out of an uploaded document or image —
15
+ a scanned form, a PDF, an ID — by an AI agent, in one call.
16
+
17
+ The shape:
18
+
19
+ 1. **Upload** the file(s) to storage (`datalakes.createUploadLink` + a raw PUT)
20
+ and keep the returned `key`s.
21
+ 2. Register an **LLM-enrichment tool** — the provider adapter. Its `base_body`
22
+ authors the provider's multimodal request and its `response_extractor` maps
23
+ the provider's envelope back to a canonical `{ output_json, … }`.
24
+ 3. Register an **AI agent** that points at the tool, constrained by an
25
+ `llm_response_schema` (the JSON shape you want back) and an `input_schema`.
26
+ 4. `aiAgents.invoke(..., { input, files: [{ key, content_type }] })` → the
27
+ parsed JSON, an explanation, and token usage.
28
+
29
+ > **File-ingestion note (read this).** This is how you make an agent *read* a
30
+ > file. It is **not** how you turn a file into dataset rows — that is bulk
31
+ > ingestion (`dataActivationClients.ingestFile`; see `bulk-ingest.md`). An AI
32
+ > agent does **enrichment / extraction** on content handed to it; it does not
33
+ > ingest files. When a contract needs per-row vision enrichment during
34
+ > ingestion, nest the agent on the contract (`interoperability_contracts.md`
35
+ > §6) — the row references the uploaded file's key and the agent reads it as
36
+ > enrichment. The file→row step is always the DAC, never the agent.
37
+
38
+ This is **global** to every datalake. See `ai_agents.md` for the agent
39
+ reference. The walk uses a WireMock-stubbed provider so it runs deterministically
40
+ on the mock stack; swap the tool's `base_url` + adapter for a real provider
41
+ unchanged.
42
+
43
+ # Walkthrough
44
+
45
+ The `_setup/healthcare.md` bootstrap left `api`, `tenantSlug`, `datalakeSlug`,
46
+ and `ctx.datalakeId` populated.
47
+
48
+ ## 001 — upload the document and image
49
+
50
+ Mint a presigned URL per file and PUT the bytes with a **raw `fetch`** (the
51
+ upload goes to object storage, not through the SDK). Keep each returned `key` —
52
+ that is how you hand the file to the agent. The fixtures are a 2-page PDF and a
53
+ document image.
54
+
55
+ ```typescript
56
+ const { readFileSync } = await import('node:fs')
57
+ const { join } = await import('node:path')
58
+
59
+ async function uploadFixture(filename: string, contentType: string): Promise<string> {
60
+ const bytes = readFileSync(join(process.env.COOKBOOK_FIXTURES_DIR!, 'healthcare', filename))
61
+ const { data: link } = await api.datalakes.createUploadLink(tenantSlug, datalakeSlug, {
62
+ filename,
63
+ content_type: contentType,
64
+ })
65
+ const put = await fetch(link.url!, { method: 'PUT', headers: { 'Content-Type': contentType }, body: bytes })
66
+ if (put.status !== 200) throw new Error(`upload of ${filename} failed: ${put.status}`)
67
+ return link.key!
68
+ }
69
+
70
+ ctx.pdfKey = await uploadFixture('sample_two_page.pdf', 'application/pdf')
71
+ ctx.imageKey = await uploadFixture('memorandum-of-association-01.png', 'image/png')
72
+ ```
73
+
74
+ ## 002 — register the LLM-enrichment tool (the provider adapter)
75
+
76
+ The tool is the complete provider adapter. `base_body` authors the provider's
77
+ multimodal request (here the OpenAI chat-completions shape, with the agent's
78
+ `llm_response_schema` injected as `{{ schema }}` and uploaded images appended as
79
+ `image_url` parts). `response_extractor` reads the provider's envelope back into
80
+ the canonical `{ output_json, explanation, …_tokens }`; its `output_schema` is
81
+ required for an `llm_enrichment` tool.
82
+
83
+ ```typescript
84
+ const ENRICHMENT_OUTPUT_SCHEMA = {
85
+ type: 'object',
86
+ properties: {
87
+ output_json: {},
88
+ input_tokens: { type: ['integer', 'null'] },
89
+ output_tokens: { type: ['integer', 'null'] },
90
+ total_tokens: { type: ['integer', 'null'] },
91
+ explanation: { type: ['string', 'null'] },
92
+ },
93
+ required: ['output_json'],
94
+ }
95
+
96
+ const OPENAI_BASE_BODY =
97
+ '{"model": "{{ model }}", "messages": [{"role": "user", "content": [{"type": "text", "text": "{{ rendered_prompt | json_escape }}"}{% for img in images %}, {"type": "image_url", "image_url": {"url": "data:{{ img.content_type }};base64,{{ img.data }}"}}{% endfor %}]}], "response_format": {"type": "json_schema", "json_schema": {"name": "output", "strict": true, "schema": {{ schema | to_json }}}}}'
98
+
99
+ const OPENAI_EXTRACTOR =
100
+ '{"output_json": "{{ msg.choices[0].message.content | json_escape }}", "explanation": "{{ msg.choices[0].message.reasoning_content | json_escape }}", "input_tokens": {{ msg.usage.prompt_tokens | default: 0 }}, "output_tokens": {{ msg.usage.completion_tokens | default: 0 }}, "total_tokens": {{ msg.usage.total_tokens | default: 0 }}}'
101
+
102
+ const { data: tool } = await api.tools.create(tenantSlug, datalakeSlug, {
103
+ name: `Legal-Entity Extraction Tool ${runSuffix}`,
104
+ description: 'Multimodal LLM-enrichment adapter for legal_entity extraction.',
105
+ intent: 'llm_enrichment',
106
+ status: 'active',
107
+ datalake_id: ctx.datalakeId,
108
+ response_extractor: { type: 'custom', body: OPENAI_EXTRACTOR, output_schema: ENRICHMENT_OUTPUT_SCHEMA },
109
+ body: {
110
+ tool_body_type: 'rest_api',
111
+ base_url: 'http://localhost:8080/openai',
112
+ base_path: { type: 'custom', body: '/v1/chat/completions' },
113
+ auth_method: 'api_key',
114
+ api_key: 'stub-key',
115
+ api_key_name: 'Authorization',
116
+ api_key_location: 'header',
117
+ request_type: 'json',
118
+ response_type: 'json',
119
+ timeout_ms: 300_000,
120
+ base_body: { type: 'custom', body: OPENAI_BASE_BODY },
121
+ },
122
+ })
123
+ toolId = tool.id!
124
+ ```
125
+
126
+ ## 003 — register the extraction agent
127
+
128
+ The agent points at the tool and is constrained by two schemas: `input_schema`
129
+ (what `invoke` must pass — here `instructions`) and `llm_response_schema` (the
130
+ shape the model must return — load-bearing, the platform constrains the model to
131
+ it). `data_access: 'unregulated'` is the compliance gate for what the agent may
132
+ read.
133
+
134
+ ```typescript
135
+ const LEGAL_ENTITY_RESPONSE_SCHEMA = {
136
+ type: 'object',
137
+ properties: {
138
+ company_name: { type: 'string', nullable: true, maxLength: 200 },
139
+ registered_address: { type: 'string', nullable: true, maxLength: 300 },
140
+ date_of_formation: { type: 'string', nullable: true, maxLength: 40 },
141
+ capital_amount: { type: 'number', nullable: true },
142
+ capital_currency: { type: 'string', nullable: true, maxLength: 20 },
143
+ },
144
+ required: ['company_name'],
145
+ additionalProperties: false,
146
+ }
147
+
148
+ const { data: agent } = await api.aiAgents.create(tenantSlug, datalakeSlug, {
149
+ name: `Legal Entity Extraction Agent ${runSuffix}`,
150
+ description: 'Extracts legal_entity-shaped fields from corporate documents.',
151
+ tool_id: toolId,
152
+ model: 'gpt-4o',
153
+ data_access: 'unregulated',
154
+ temperature: 0.0,
155
+ max_tokens: 8_192,
156
+ enabled: true,
157
+ input_schema: { type: 'object', properties: { instructions: { type: 'string' } }, required: ['instructions'] },
158
+ llm_response_schema: LEGAL_ENTITY_RESPONSE_SCHEMA,
159
+ prompt_config: {
160
+ type: 'custom',
161
+ body: 'You are a legal-entity data extraction specialist.\nExtract structured corporate information from the provided content and return a JSON object aligned to the legal_entity schema.\n\n{{ instructions }}',
162
+ },
163
+ })
164
+ aiAgentId = agent.id!
165
+ ```
166
+
167
+ ## 004 — invoke the agent with the files
168
+
169
+ `invoke` runs the agent against the uploaded files: `input` satisfies the
170
+ `input_schema`, and `files` lists the storage keys + content types to attach. The
171
+ result is the parsed JSON (`output`, matching `llm_response_schema`), an
172
+ `explanation`, and token `usage`.
173
+
174
+ ```typescript
175
+ const { data: result } = await api.aiAgents.invoke(tenantSlug, datalakeSlug, aiAgentId, {
176
+ input: { instructions: 'Extract the company name and any visible registration details from the attached documents.' },
177
+ files: [
178
+ { key: ctx.pdfKey, content_type: 'application/pdf' },
179
+ { key: ctx.imageKey, content_type: 'image/png' },
180
+ ],
181
+ })
182
+
183
+ if (!result.output || typeof result.output !== 'object') {
184
+ throw new Error('invoke did not return a structured output object')
185
+ }
186
+ if (!(result.output as Record<string, unknown>).company_name) {
187
+ throw new Error('expected a company_name in the extracted output')
188
+ }
189
+ if (!result.usage || (result.usage as { input_tokens?: number }).input_tokens === undefined) {
190
+ throw new Error('expected token usage on the invoke result')
191
+ }
192
+ ```
193
+
194
+ ## 005 — a malformed input is rejected
195
+
196
+ `invoke` validates `input` against the agent's `input_schema` before calling the
197
+ model. Omitting the required `instructions` returns a 422 — caught here so the
198
+ walk proves the gate without aborting.
199
+
200
+ ```typescript
201
+ let rejected = false
202
+ try {
203
+ await api.aiAgents.invoke(tenantSlug, datalakeSlug, aiAgentId, {
204
+ input: { wrong_field: 'no instructions here' },
205
+ files: [],
206
+ })
207
+ } catch (err) {
208
+ const status = (err as { _httpStatus?: number })._httpStatus
209
+ if (status !== 422) throw err
210
+ rejected = true
211
+ }
212
+ if (!rejected) {
213
+ throw new Error('expected a 422 for input that violates the agent input_schema')
214
+ }
215
+ ```
216
+
217
+ # Gotchas
218
+
219
+ - **Uploading a file is not ingesting it.** The upload + `invoke` here makes the
220
+ agent *read* the file. To turn a file into dataset rows, use
221
+ `dataActivationClients.ingestFile` (`bulk-ingest.md`). Don't reach for an agent
222
+ to "ingest" a document — agents enrich, the DAC ingests.
223
+ - **`llm_response_schema` is load-bearing.** It is what constrains the model's
224
+ output; the tool's `response_extractor.output_schema` is required too (a
225
+ missing one is a 422 at `/response_extractor/output_schema`).
226
+ - **The model lives on the agent, not the tool.** The tool is the provider
227
+ adapter (URL + request/response shape); the agent sets the `model`. Swapping
228
+ providers means a new tool adapter + the agent's `model`.
229
+ - **The file PUT is raw HTTP.** `createUploadLink` is an SDK call; the upload is a
230
+ plain `fetch` PUT to the presigned URL, exactly like bulk ingest.
231
+ - **`data_access` gates what the agent may read.** `unregulated` here; a stricter
232
+ setting changes what content the agent is allowed to see.
233
+
234
+ # See also
235
+
236
+ - `ai_agents.md` — agent wire shape, `input_schema` / `llm_response_schema`, invoke
237
+ - `interoperability_contracts.md` §6 — nesting an agent on a contract for per-row
238
+ enrichment during ingestion (the file→row path stays the DAC's)
239
+ - `bulk-ingest.md` — the correct path for turning a file into dataset rows
240
+ - `_setup/healthcare.md` — the bootstrap this walk starts from
241
+ - `_fixtures/healthcare/` — the vendored PDF + image
242
+ - `integration-tests/tests/healthcare/ai-agent-invoke.test.ts` — the green test
243
+ these calls are lifted from