@alvera-ai/platform-sdk 0.17.0 → 0.18.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 (83) hide show
  1. package/.agent/AGENTS.md +82 -144
  2. package/.agent/account_management.md +2 -2
  3. package/.agent/action_logs.md +4 -4
  4. package/.agent/ai_agents.md +28 -21
  5. package/.agent/ai_sandbox.md +49 -39
  6. package/.agent/connected_apps.md +3 -3
  7. package/.agent/cookbook/_fixtures/README.md +1 -1
  8. package/.agent/cookbook/_fixtures/{foundation → organic-marketing}/_lead_submissions_foundation_generic_table.liquid +1 -1
  9. package/.agent/cookbook/_fixtures/organic-marketing/_lead_submissions_foundation_legal_entity.liquid +80 -0
  10. package/.agent/cookbook/_fixtures/{foundation → organic-marketing}/_lead_submissions_foundation_mdm.liquid +2 -1
  11. package/.agent/cookbook/_fixtures/payments-compliance/_compliance_screenings_generic_table.liquid +57 -0
  12. package/.agent/cookbook/_fixtures/payments-compliance/_compliance_screenings_legal_entity.liquid +30 -0
  13. package/.agent/cookbook/_fixtures/payments-compliance/_compliance_screenings_mdm.liquid +44 -0
  14. package/.agent/cookbook/_fixtures/payments-compliance/_payment_accounts_generic_table.liquid +57 -0
  15. package/.agent/cookbook/_fixtures/payments-compliance/_payment_accounts_legal_entity.liquid +36 -0
  16. package/.agent/cookbook/_fixtures/payments-compliance/_payment_accounts_mdm.liquid +41 -0
  17. package/.agent/cookbook/_fixtures/primary-care-feedback/_cahps_appointments_generic_table.liquid +70 -0
  18. package/.agent/cookbook/_fixtures/primary-care-feedback/_cahps_appointments_legal_entity.liquid +52 -0
  19. package/.agent/cookbook/_fixtures/primary-care-feedback/_cahps_appointments_mdm.liquid +42 -0
  20. package/.agent/cookbook/_fixtures/subscription-saas/_customers_subscription_generic_table.liquid +38 -0
  21. package/.agent/cookbook/_fixtures/subscription-saas/_customers_subscription_legal_entity.liquid +48 -0
  22. package/.agent/cookbook/_fixtures/subscription-saas/_customers_subscription_mdm.liquid +49 -0
  23. package/.agent/cookbook/organic-marketing.md +2801 -0
  24. package/.agent/cookbook/payments-compliance.md +2180 -0
  25. package/.agent/cookbook/primary-care.md +2175 -0
  26. package/.agent/cookbook/subscription-saas.md +2403 -0
  27. package/.agent/data_activation_clients.md +65 -52
  28. package/.agent/datalakes.md +338 -171
  29. package/.agent/errors.md +3 -3
  30. package/.agent/generic_tables.md +151 -62
  31. package/.agent/interoperability_contracts.md +57 -22
  32. package/.agent/mdm.md +136 -153
  33. package/.agent/messages.md +36 -34
  34. package/.agent/mock-services.md +1 -1
  35. package/.agent/mutations.md +2 -2
  36. package/.agent/templates.md +14 -13
  37. package/.agent/tools.md +63 -21
  38. package/.agent/type_naming.md +13 -13
  39. package/.agent/workflows.md +99 -53
  40. package/README.md +2 -2
  41. package/dist/bin/platform-sdk.mjs +33 -47
  42. package/dist/bin/platform-sdk.mjs.map +1 -1
  43. package/dist/index.d.mts +565 -379
  44. package/dist/index.d.mts.map +1 -1
  45. package/dist/index.mjs +494 -59
  46. package/dist/index.mjs.map +1 -1
  47. package/package.json +4 -3
  48. package/.agent/cookbook/_fixtures/foundation/_lead_submissions_foundation_legal_entity.liquid +0 -88
  49. package/.agent/cookbook/_fixtures/healthcare/_cahps_appointments_healthcare_appointment.liquid +0 -47
  50. package/.agent/cookbook/_fixtures/healthcare/_cahps_appointments_healthcare_mdm.liquid +0 -24
  51. package/.agent/cookbook/_fixtures/healthcare/_cahps_appointments_healthcare_patient.liquid +0 -38
  52. package/.agent/cookbook/_fixtures/payments/_compliance_screenings_payments_compliance_screening.liquid +0 -59
  53. package/.agent/cookbook/_fixtures/payments/_compliance_screenings_payments_mdm.liquid +0 -36
  54. package/.agent/cookbook/_fixtures/payments/_payment_accounts_payments_mdm.liquid +0 -30
  55. package/.agent/cookbook/_fixtures/payments/_payment_accounts_payments_payment_account.liquid +0 -55
  56. package/.agent/cookbook/_fixtures/subscription/_customers_subscription_mdm.liquid +0 -20
  57. package/.agent/cookbook/_setup/foundation.md +0 -359
  58. package/.agent/cookbook/_setup/healthcare.md +0 -361
  59. package/.agent/cookbook/_setup/payments.md +0 -365
  60. package/.agent/cookbook/_setup/subscription.md +0 -364
  61. package/.agent/cookbook/action-status-updaters.md +0 -278
  62. package/.agent/cookbook/ai-agent-invoke.md +0 -279
  63. package/.agent/cookbook/appointment-review-sms-workflow.md +0 -801
  64. package/.agent/cookbook/birthday-greeting-sms-trigger.md +0 -696
  65. package/.agent/cookbook/bulk-ingest.md +0 -302
  66. package/.agent/cookbook/contact-us-triage-with-llm.md +0 -663
  67. package/.agent/cookbook/dunning-sms-for-delinquent.md +0 -659
  68. package/.agent/cookbook/generic-tables.md +0 -244
  69. package/.agent/cookbook/invite-team.md +0 -200
  70. package/.agent/cookbook/kyc-notification-on-account-activation.md +0 -661
  71. package/.agent/cookbook/marketing-campaign-send.md +0 -1044
  72. package/.agent/cookbook/paginated-restapi-poller.md +0 -383
  73. package/.agent/cookbook/rest-fetch.md +0 -273
  74. package/.agent/cookbook/sanctions-screening-with-agent-review.md +0 -773
  75. package/.agent/cookbook/score-leads-with-llm-categorization.md +0 -665
  76. package/.agent/cookbook/system-templates.md +0 -165
  77. package/.agent/cookbook/talk-to-data.md +0 -178
  78. package/.agent/cookbook/triage-prospects-by-priority.md +0 -571
  79. package/.agent/cookbook/welcome-sms-for-customers.md +0 -647
  80. /package/.agent/cookbook/_fixtures/{healthcare → primary-care-feedback}/memorandum-of-association-01.png +0 -0
  81. /package/.agent/cookbook/_fixtures/{healthcare → primary-care-feedback}/sample_two_page.pdf +0 -0
  82. /package/.agent/cookbook/_fixtures/{subscription → subscription-saas}/_customers_subscription_customer.liquid +0 -0
  83. /package/.agent/cookbook/_fixtures/{subscription → subscription-saas}/stripe_customers_batch1.csv +0 -0
@@ -1,359 +0,0 @@
1
- ---
2
- title: Foundation industry bootstrap setup
3
- summary: Auth as root, sign up + confirm a fresh industry-admin user, create a foundation tenant + datalake. Shared by every foundation scenario cookbook.
4
- industry: foundation
5
- slug: foundation
6
- vitest_source:
7
- - integration-tests/tests/foundation/bootstrap.test.ts
8
- status: green
9
- ---
10
-
11
- # Problem
12
-
13
- Every foundation scenario cookbook needs the same starting point:
14
- a fresh tenant on the platform with a foundation-domain datalake
15
- attached, accessed by an industry-admin user. Authoring this
16
- prelude inline inside every scenario cookbook would duplicate the
17
- auth + tenant + datalake chain across two cookbooks (and four more
18
- across the other industries). The validator auto-discovers this
19
- file from each cookbook's `industry: foundation` front-matter and
20
- inlines its numbered steps ahead of the scenario's own, so the
21
- generated bun:test spec is hermetic without the cookbook author
22
- having to copy-paste this prelude.
23
-
24
- # Composition
25
-
26
- | Resource provisioned | Owner |
27
- |-------------------------------|-------------|
28
- | Root session | setup |
29
- | Industry-admin user (`sarah`) | setup |
30
- | Foundation tenant | setup |
31
- | Foundation datalake | setup |
32
-
33
- # Walkthrough
34
-
35
- ## 001 — root admin signs in and provisions a fresh industry-admin user
36
-
37
- Authenticate as the platform's root admin (`admin@dev.local` /
38
- `devpassword` in local dev) via the tenantless bootstrap login —
39
- keyless by structural necessity (no tenant exists yet to scope a
40
- key to), and a dev/test-only surface. Then sign up a per-run industry-admin
41
- user (`sarah`) under a unique email derived from `runSuffix`, and
42
- confirm them so they can sign in. Both the signup and the confirm
43
- endpoints require root scope.
44
-
45
- ```typescript
46
- ctx.rootSession = await createBootstrapSession({
47
- baseUrl: process.env.ALVERA_BASE_URL!,
48
- email: process.env.ALVERA_ROOT_EMAIL!,
49
- password: process.env.ALVERA_ROOT_PASSWORD!,
50
- })
51
- ctx.rootApi = createIsolatedPlatformApi({
52
- baseUrl: process.env.ALVERA_BASE_URL!,
53
- sessionToken: ctx.rootSession.sessionToken,
54
- apiKey: '',
55
- })
56
-
57
- ctx.sarahEmail = `cookbook-sarah-${runSuffix}@dev.local`
58
- ctx.sarahPassword = 'CookbookPass1!'
59
-
60
- const signUpResp = await ctx.rootApi.admin.signUp({
61
- email: ctx.sarahEmail,
62
- password: ctx.sarahPassword,
63
- first_name: 'Cookbook',
64
- last_name: 'Sarah',
65
- })
66
- ctx.sarahUserId = signUpResp.data.id
67
- await ctx.rootApi.admin.confirmUser(ctx.sarahUserId)
68
- ```
69
-
70
- ## 002 — sarah signs in (tenantless) and creates the foundation tenant
71
-
72
- Sarah signs in for the first time without a tenant scope (no
73
- tenant exists yet for her), then immediately creates a fresh
74
- foundation tenant. The tenant's server-derived `slug` is captured
75
- into the closure-scoped `tenantSlug` slot for downstream steps.
76
-
77
- ```typescript
78
- ctx.sarahTenantlessSession = await createBootstrapSession({
79
- baseUrl: process.env.ALVERA_BASE_URL!,
80
- email: ctx.sarahEmail,
81
- password: ctx.sarahPassword,
82
- })
83
- ctx.sarahTenantlessApi = createIsolatedPlatformApi({
84
- baseUrl: process.env.ALVERA_BASE_URL!,
85
- sessionToken: ctx.sarahTenantlessSession.sessionToken,
86
- apiKey: '',
87
- })
88
-
89
- const tenantResp = await ctx.sarahTenantlessApi.tenants.create({
90
- name: `Cookbook Foundation ${runSuffix}`,
91
- })
92
- tenantSlug = tenantResp.data.slug!
93
- ```
94
-
95
- ## 003 — sarah signs in tenant-scoped — the canonical client
96
-
97
- Re-mint sarah's session with the new tenant slug. This bearer is
98
- the canonical tenant-scoped client every subsequent step uses; it
99
- is assigned to the closure-scoped `api` slot so the scenario
100
- cookbook's steps inherit it via inlining.
101
-
102
- ```typescript
103
- // A tenant-scoped login requires X-API-Key — mint a public_api key for
104
- // the fresh tenant via the platform-admin side door (root Bearer). In the
105
- // web console this is Settings -> API Keys; any of the tenant's keys
106
- // satisfies the login gate, and the minted Bearer inherits this key's
107
- // origin policy.
108
- const { data: mintedKey } = await ctx.rootApi.admin.createTenantApiKey(tenantSlug, {
109
- name: `Cookbook Bootstrap Key ${runSuffix}`,
110
- data_access_mode: 'unregulated',
111
- })
112
- ctx.tenantApiKey = mintedKey.api_key
113
-
114
- const sarahTenantSession = await createSession({
115
- baseUrl: process.env.ALVERA_BASE_URL!,
116
- email: ctx.sarahEmail,
117
- password: ctx.sarahPassword,
118
- tenantSlug,
119
- apiKey: ctx.tenantApiKey,
120
- })
121
- api = createIsolatedPlatformApi({
122
- baseUrl: process.env.ALVERA_BASE_URL!,
123
- sessionToken: sarahTenantSession.sessionToken,
124
- apiKey: ctx.tenantApiKey,
125
- })
126
- ```
127
-
128
- ## 004 — sarah creates the foundation datalake
129
-
130
- Provision a foundation-domain datalake on the new tenant. The DB
131
- schemas are scoped to the cookbook run via `runSuffix` so parallel
132
- cookbook runs do not collide on schema names. Local-dev defaults
133
- (`postgres` on `localhost:5432`, `alvera_dev_foundation`) match
134
- the seeded `dev.exs` setup; LocalStack S3 (`localhost:4566`)
135
- serves the unregulated + regulated buckets. The datalake's
136
- server-derived `slug` is captured into `datalakeSlug`.
137
-
138
- ```typescript
139
- const DB_HOST = 'localhost'
140
- const DB_PORT = 5432
141
- const DB_USER = 'postgres'
142
- const DB_PASS = 'postgres'
143
- const DB_NAME = 'alvera_dev_foundation'
144
- const UNREG_SCHEMA = `cookbook_${runSuffix}_unreg`
145
- const REG_SCHEMA = `cookbook_${runSuffix}_reg`
146
-
147
- const S3 = {
148
- cloud_storage_type: 'aws' as const,
149
- region: 'us-east-1',
150
- access_key_id: 'test',
151
- secret_access_key: 'test',
152
- endpoint: 'http://localhost:4566',
153
- }
154
-
155
- const datalakeResp = await api.datalakes.create(tenantSlug, {
156
- name: `Cookbook Foundation Datalake ${runSuffix}`,
157
- description: 'Foundation datalake provisioned by cookbook doctest.',
158
- data_domain: 'foundation',
159
- timezone: 'America/New_York',
160
- pool_size: 5,
161
-
162
- unregulated_db_writer_host: DB_HOST,
163
- unregulated_db_writer_port: DB_PORT,
164
- unregulated_db_writer_name: DB_NAME,
165
- unregulated_db_writer_schema: UNREG_SCHEMA,
166
- unregulated_db_writer_auth_method: 'password',
167
- unregulated_db_writer_user: DB_USER,
168
- unregulated_db_writer_pass: DB_PASS,
169
- unregulated_db_writer_enable_ssl: false,
170
- unregulated_db_reader_host: DB_HOST,
171
- unregulated_db_reader_port: DB_PORT,
172
- unregulated_db_reader_name: DB_NAME,
173
- unregulated_db_reader_schema: UNREG_SCHEMA,
174
- unregulated_db_reader_auth_method: 'password',
175
- unregulated_db_reader_user: DB_USER,
176
- unregulated_db_reader_pass: DB_PASS,
177
- unregulated_db_reader_enable_ssl: false,
178
-
179
- regulated_data_db_writer_host: DB_HOST,
180
- regulated_data_db_writer_port: DB_PORT,
181
- regulated_data_db_writer_name: DB_NAME,
182
- regulated_data_db_writer_schema: REG_SCHEMA,
183
- regulated_data_db_writer_auth_method: 'password',
184
- regulated_data_db_writer_user: DB_USER,
185
- regulated_data_db_writer_pass: DB_PASS,
186
- regulated_data_db_writer_enable_ssl: false,
187
- regulated_data_db_reader_host: DB_HOST,
188
- regulated_data_db_reader_port: DB_PORT,
189
- regulated_data_db_reader_name: DB_NAME,
190
- regulated_data_db_reader_schema: REG_SCHEMA,
191
- regulated_data_db_reader_auth_method: 'password',
192
- regulated_data_db_reader_user: DB_USER,
193
- regulated_data_db_reader_pass: DB_PASS,
194
- regulated_data_db_reader_enable_ssl: false,
195
-
196
- unregulated_cloud_storage: { ...S3, bucket: 'foundation-lake-unregulated' },
197
- regulated_cloud_storage: { ...S3, bucket: 'foundation-lake-regulated' },
198
- })
199
- datalakeSlug = datalakeResp.data.slug!
200
- // `tools.create` and a few other endpoints accept the datalake by
201
- // UUID in the request body rather than only the slug in the URL,
202
- // so keep it on `ctx` for downstream steps that need it.
203
- ctx.datalakeId = datalakeResp.data.id!
204
- ```
205
-
206
- ## 005 — enqueue the datalake migrations
207
-
208
- `datalakes.create` persists the datalake row in `:new` status but
209
- does not itself run the schema migrations. Migration is a
210
- separately-triggered async job so the operator controls when the
211
- (potentially slow) per-industry DDL runs. `datalakes.migrate`
212
- enqueues the `DatalakeMigrationWorker` Oban job and returns
213
- immediately with `status: 'enqueued'` plus the Oban `job_id`. The
214
- poll in §006 then waits for that worker to finish — without this
215
- call the datalake would sit at `:new` forever.
216
-
217
- ```typescript
218
- const migrateResp = await api.datalakes.migrate(tenantSlug, datalakeSlug)
219
- if (migrateResp.data.status !== 'enqueued') {
220
- throw new Error(
221
- `datalake migration not enqueued (status: ${migrateResp.data.status})`,
222
- )
223
- }
224
- ```
225
-
226
- ## 006 — poll the datalake until status is ready
227
-
228
- Datalake migrations are async — the §005 migrate call enqueued a
229
- `DatalakeMigrationWorker` Oban job that runs the per-industry
230
- schema migrations, deploys PostgREST roles, and provisions the
231
- service-account bot user. Downstream resource creates (tools,
232
- workflows) only need `datalakeId` to be persisted (which it is
233
- the moment §004 returns), but any scenario step that touches
234
- the regulated/unregulated DBs or the dataset infrastructure
235
- requires `status: 'ready'`. Polling here makes every foundation
236
- scenario cookbook deterministic regardless of how long the cold
237
- migration takes on a given host; the loop exits the moment the
238
- datalake reaches `:ready`, so the 5-minute cap is paid only in
239
- failure mode.
240
-
241
- ```typescript
242
- const READY_TIMEOUT_MS = 5 * 60_000
243
- const READY_POLL_MS = 5_000
244
- const deadline = Date.now() + READY_TIMEOUT_MS
245
- let datalakeStatus: string | undefined
246
- while (Date.now() < deadline) {
247
- const { data } = await api.datalakes.get(tenantSlug, ctx.datalakeId)
248
- datalakeStatus = data.status
249
- if (datalakeStatus === 'ready') break
250
- await new Promise((r) => setTimeout(r, READY_POLL_MS))
251
- }
252
- if (datalakeStatus !== 'ready') {
253
- throw new Error(
254
- `datalake did not reach :ready within ${READY_TIMEOUT_MS}ms (last status: ${datalakeStatus})`,
255
- )
256
- }
257
- ```
258
-
259
- ## 007 — a reusable "wait until the run has fired" helper
260
-
261
- `workflows.run` only **schedules** a run. It records it and returns
262
- immediately with `workflow_run_id` / `status` / `scheduled_at`; the
263
- `workflow_run_log_id` and `batch_id` a scenario needs are written
264
- later, when the run actually fires, and are read back from
265
- `workflowRuns.get`.
266
-
267
- Two traps live in that gap, so every foundation scenario shares one
268
- helper rather than re-deriving it:
269
-
270
- - **Poll until `workflow_run_log_id` is a string — NOT until `status`
271
- leaves `'scheduled'`.** Those are different moments: the run reaches
272
- `processing` first and writes the log id a beat later. A predicate on
273
- status alone releases you to read a `null`, and because
274
- `typeof null === 'object'` the symptom is a baffling *"expected
275
- string, got object"* rather than an obvious nil.
276
- - **Raise on `failed` carrying `failure_reason` rather than polling to
277
- the deadline.** A scenario blocked on a run that will never fire
278
- should say why on the first read, not thirty seconds later behind a
279
- generic timeout.
280
-
281
- The helper is stored on the shared `ctx` bag rather than declared as a
282
- plain function because each numbered step compiles into its own `it()`
283
- block — a bare `function` here would not be in scope for the steps that
284
- call it.
285
-
286
- ```typescript
287
- const FIRED_TIMEOUT_MS = 60_000
288
- const FIRED_POLL_MS = 1_000
289
-
290
- ctx.waitForFiredRun = async (
291
- runDatalakeSlug: string,
292
- runId: string,
293
- timeoutMs: number = FIRED_TIMEOUT_MS,
294
- ): Promise<{ workflowRunLogId: string; batchId: string | null }> => {
295
- const deadline = Date.now() + timeoutMs
296
- let lastStatus: string | undefined
297
- while (Date.now() < deadline) {
298
- const { data } = await api.workflowRuns.get(tenantSlug, runDatalakeSlug, runId)
299
- lastStatus = data.status
300
- if (data.status === 'failed') {
301
- throw new Error(
302
- `workflow run ${runId} failed: ${data.failure_reason ?? 'no failure_reason given'}`,
303
- )
304
- }
305
- if (typeof data.workflow_run_log_id === 'string') {
306
- return { workflowRunLogId: data.workflow_run_log_id, batchId: data.batch_id ?? null }
307
- }
308
- await new Promise((r) => setTimeout(r, FIRED_POLL_MS))
309
- }
310
- throw new Error(
311
- `workflow run ${runId} never fired within ${timeoutMs}ms (last status: ${lastStatus})`,
312
- )
313
- }
314
- ```
315
-
316
- # Rollback
317
-
318
- The cookbook doctest harness does not currently tear down the
319
- created tenant / datalake / user — each run mints fresh names via
320
- `runSuffix` so reruns do not collide, and the seeded local DB is
321
- cheap to reset (`mix ecto.reset` on the platform). When the
322
- validator graduates to continuous integration at Milestone 18
323
- (running against the mockoon-backed mock env at `ALVERA_ENV=mock`),
324
- no real state is created at all.
325
-
326
- # Outcome
327
-
328
- After this setup runs, the closure-scoped slots are populated as:
329
-
330
- - `api` — a tenant-scoped `PlatformApi` client authenticated as
331
- the industry-admin user
332
- - `tenantSlug` — server-derived slug of the fresh foundation
333
- tenant
334
- - `datalakeSlug` — server-derived slug of the fresh foundation
335
- datalake
336
- - `ctx.sarahEmail` / `ctx.sarahPassword` — the per-run admin
337
- credentials (kept on `ctx` because no scenario cookbook needs
338
- to re-authenticate by default)
339
- - `ctx.tenantApiKey` — the tenant's publishable API key (minted in
340
- setup via `admin.createTenantApiKey`); thread it into any
341
- additional tenant-scoped `createSession` a scenario performs
342
- - `ctx.waitForFiredRun(datalakeSlug, runId)` — polls `workflowRuns.get`
343
- until a scheduled run has actually fired, then returns its
344
- `{ workflowRunLogId, batchId }`. Use it after every `workflows.run`;
345
- reading `workflow_run_log_id` off the run response returns `null`
346
- because run-workflow only schedules (see §007)
347
-
348
- Scenario cookbooks under `industry: foundation` start at their own
349
- `§001` with these slots already populated.
350
-
351
- # See also
352
-
353
- - `.agent/datalakes.md` — datalake create body shape (regulated +
354
- unregulated tier configuration)
355
- - `.agent/AGENTS.md` § SDK auth + client construction — root vs
356
- tenantless vs tenant-scoped session scopes
357
- - `integration-tests/tests/foundation/bootstrap.test.ts` — the
358
- green test these snippets are lifted from (the §7b sibling
359
- datalake creation is out of scope for this lean cookbook setup)