@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.
- package/.agent/AGENTS.md +499 -0
- package/.agent/account_management.md +456 -0
- package/.agent/action_status_updaters.md +264 -0
- package/.agent/ai_agents.md +462 -0
- package/.agent/ai_sandbox.md +265 -0
- package/.agent/async.md +112 -0
- package/.agent/connected_apps.md +408 -0
- package/.agent/cookbook/_fixtures/README.md +106 -0
- package/.agent/cookbook/_fixtures/accounts_receivable/_customers_accounts_receivable_customer.liquid +32 -0
- package/.agent/cookbook/_fixtures/accounts_receivable/_customers_accounts_receivable_mdm.liquid +20 -0
- package/.agent/cookbook/_fixtures/accounts_receivable/stripe_customers_batch1.csv +5 -0
- package/.agent/cookbook/_fixtures/foundation/_lead_submissions_foundation_generic_table.liquid +33 -0
- package/.agent/cookbook/_fixtures/foundation/_lead_submissions_foundation_legal_entity.liquid +88 -0
- package/.agent/cookbook/_fixtures/foundation/_lead_submissions_foundation_mdm.liquid +48 -0
- package/.agent/cookbook/_fixtures/healthcare/_cahps_appointments_healthcare_appointment.liquid +47 -0
- package/.agent/cookbook/_fixtures/healthcare/_cahps_appointments_healthcare_mdm.liquid +24 -0
- package/.agent/cookbook/_fixtures/healthcare/_cahps_appointments_healthcare_patient.liquid +38 -0
- package/.agent/cookbook/_fixtures/healthcare/memorandum-of-association-01.png +0 -0
- package/.agent/cookbook/_fixtures/healthcare/sample_two_page.pdf +43 -0
- package/.agent/cookbook/_fixtures/payment_risk/_compliance_screenings_payment_risk_compliance_screening.liquid +59 -0
- package/.agent/cookbook/_fixtures/payment_risk/_compliance_screenings_payment_risk_mdm.liquid +36 -0
- package/.agent/cookbook/_fixtures/payment_risk/_payment_accounts_payment_risk_mdm.liquid +30 -0
- package/.agent/cookbook/_fixtures/payment_risk/_payment_accounts_payment_risk_payment_account.liquid +55 -0
- package/.agent/cookbook/_setup/accounts_receivable.md +282 -0
- package/.agent/cookbook/_setup/foundation.md +277 -0
- package/.agent/cookbook/_setup/healthcare.md +279 -0
- package/.agent/cookbook/_setup/payment_risk.md +283 -0
- package/.agent/cookbook/action-status-updaters.md +212 -0
- package/.agent/cookbook/ai-agent-invoke.md +243 -0
- package/.agent/cookbook/appointment-review-sms-workflow.md +761 -0
- package/.agent/cookbook/birthday-greeting-sms-trigger.md +656 -0
- package/.agent/cookbook/bulk-ingest.md +254 -0
- package/.agent/cookbook/contact-us-triage-with-llm.md +622 -0
- package/.agent/cookbook/custom-tables.md +201 -0
- package/.agent/cookbook/dunning-sms-for-delinquent.md +619 -0
- package/.agent/cookbook/invite-team.md +194 -0
- package/.agent/cookbook/kyc-notification-on-account-activation.md +619 -0
- package/.agent/cookbook/rest-fetch.md +246 -0
- package/.agent/cookbook/sanctions-screening-with-agent-review.md +733 -0
- package/.agent/cookbook/score-leads-with-llm-categorization.md +624 -0
- package/.agent/cookbook/system-templates.md +129 -0
- package/.agent/cookbook/triage-prospects-by-priority.md +533 -0
- package/.agent/cookbook/welcome-sms-for-customers.md +607 -0
- package/.agent/data_activation_clients.md +559 -0
- package/.agent/data_sources.md +235 -0
- package/.agent/datalakes.md +714 -0
- package/.agent/debugging.md +137 -0
- package/.agent/errors.md +190 -0
- package/.agent/generic_tables.md +351 -0
- package/.agent/interoperability_contracts.md +417 -0
- package/.agent/mdm.md +293 -0
- package/.agent/mutations.md +126 -0
- package/.agent/templates.md +98 -0
- package/.agent/tool-call-configs.md +90 -0
- package/.agent/tools.md +547 -0
- package/.agent/type_naming.md +129 -0
- package/.agent/workflows.md +617 -0
- package/README.md +178 -0
- package/dist/bin/platform-sdk.d.mts +1 -0
- package/dist/bin/platform-sdk.mjs +106 -0
- package/dist/bin/platform-sdk.mjs.map +1 -0
- package/dist/index.d.mts +1310 -44116
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1194 -7344
- package/dist/index.mjs.map +1 -1
- 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
|