@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,361 +0,0 @@
1
- ---
2
- title: Healthcare industry bootstrap setup
3
- summary: Auth as root, sign up + confirm a fresh industry-admin user, create a healthcare tenant + datalake. Shared by every healthcare scenario cookbook.
4
- industry: healthcare
5
- slug: healthcare
6
- vitest_source:
7
- - integration-tests/tests/healthcare/bootstrap.test.ts
8
- status: green
9
- ---
10
-
11
- # Problem
12
-
13
- Every healthcare scenario cookbook needs the same starting point:
14
- a fresh tenant on the platform with a healthcare-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 six more
18
- across the other industries). The validator auto-discovers this
19
- file from each cookbook's `industry: healthcare` 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
- | Healthcare tenant | setup |
31
- | Healthcare 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 healthcare 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
- healthcare 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 Healthcare ${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 healthcare datalake
129
-
130
- Provision a healthcare-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_healthcare`) 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_healthcare'
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 Healthcare Datalake ${runSuffix}`,
157
- description: 'Healthcare datalake provisioned by cookbook doctest.',
158
- data_domain: 'healthcare',
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: 'healthcare-lake-unregulated' },
197
- regulated_cloud_storage: { ...S3, bucket: 'healthcare-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 (the regulated FHIR R4 patient + appointment
231
- tables and their join tables), deploys PostgREST roles, and
232
- provisions the service-account bot user. Downstream resource
233
- creates (tools, workflows) only need `datalakeId` to be persisted
234
- (which it is the moment §004 returns), but any scenario step that
235
- ingests into the regulated/unregulated DBs or runs a dataset
236
- search requires `status: 'ready'`. Polling here makes every
237
- healthcare scenario cookbook deterministic regardless of how long
238
- the cold migration takes on a given host; the loop exits the
239
- moment the datalake reaches `:ready`, so the 5-minute cap is paid
240
- only in failure mode.
241
-
242
- ```typescript
243
- const READY_TIMEOUT_MS = 5 * 60_000
244
- const READY_POLL_MS = 5_000
245
- const deadline = Date.now() + READY_TIMEOUT_MS
246
- let datalakeStatus: string | undefined
247
- while (Date.now() < deadline) {
248
- const { data } = await api.datalakes.get(tenantSlug, ctx.datalakeId)
249
- datalakeStatus = data.status
250
- if (datalakeStatus === 'ready') break
251
- await new Promise((r) => setTimeout(r, READY_POLL_MS))
252
- }
253
- if (datalakeStatus !== 'ready') {
254
- throw new Error(
255
- `datalake did not reach :ready within ${READY_TIMEOUT_MS}ms (last status: ${datalakeStatus})`,
256
- )
257
- }
258
- ```
259
-
260
- ## 007 — a reusable "wait until the run has fired" helper
261
-
262
- `workflows.run` only **schedules** a run. It records it and returns
263
- immediately with `workflow_run_id` / `status` / `scheduled_at`; the
264
- `workflow_run_log_id` and `batch_id` a scenario needs are written
265
- later, when the run actually fires, and are read back from
266
- `workflowRuns.get`.
267
-
268
- Two traps live in that gap, so every healthcare scenario shares one
269
- helper rather than re-deriving it:
270
-
271
- - **Poll until `workflow_run_log_id` is a string — NOT until `status`
272
- leaves `'scheduled'`.** Those are different moments: the run reaches
273
- `processing` first and writes the log id a beat later. A predicate on
274
- status alone releases you to read a `null`, and because
275
- `typeof null === 'object'` the symptom is a baffling *"expected
276
- string, got object"* rather than an obvious nil.
277
- - **Raise on `failed` carrying `failure_reason` rather than polling to
278
- the deadline.** A scenario blocked on a run that will never fire
279
- should say why on the first read, not thirty seconds later behind a
280
- generic timeout.
281
-
282
- The helper is stored on the shared `ctx` bag rather than declared as a
283
- plain function because each numbered step compiles into its own `it()`
284
- block — a bare `function` here would not be in scope for the steps that
285
- call it.
286
-
287
- ```typescript
288
- const FIRED_TIMEOUT_MS = 60_000
289
- const FIRED_POLL_MS = 1_000
290
-
291
- ctx.waitForFiredRun = async (
292
- runDatalakeSlug: string,
293
- runId: string,
294
- timeoutMs: number = FIRED_TIMEOUT_MS,
295
- ): Promise<{ workflowRunLogId: string; batchId: string | null }> => {
296
- const deadline = Date.now() + timeoutMs
297
- let lastStatus: string | undefined
298
- while (Date.now() < deadline) {
299
- const { data } = await api.workflowRuns.get(tenantSlug, runDatalakeSlug, runId)
300
- lastStatus = data.status
301
- if (data.status === 'failed') {
302
- throw new Error(
303
- `workflow run ${runId} failed: ${data.failure_reason ?? 'no failure_reason given'}`,
304
- )
305
- }
306
- if (typeof data.workflow_run_log_id === 'string') {
307
- return { workflowRunLogId: data.workflow_run_log_id, batchId: data.batch_id ?? null }
308
- }
309
- await new Promise((r) => setTimeout(r, FIRED_POLL_MS))
310
- }
311
- throw new Error(
312
- `workflow run ${runId} never fired within ${timeoutMs}ms (last status: ${lastStatus})`,
313
- )
314
- }
315
- ```
316
-
317
- # Rollback
318
-
319
- The cookbook doctest harness does not currently tear down the
320
- created tenant / datalake / user — each run mints fresh names via
321
- `runSuffix` so reruns do not collide, and the seeded local DB is
322
- cheap to reset (`mix ecto.reset` on the platform). When the
323
- validator graduates to continuous integration at Milestone 18
324
- (running against the mockoon-backed mock env at `ALVERA_ENV=mock`),
325
- no real state is created at all.
326
-
327
- # Outcome
328
-
329
- After this setup runs, the closure-scoped slots are populated as:
330
-
331
- - `api` — a tenant-scoped `PlatformApi` client authenticated as
332
- the industry-admin user
333
- - `tenantSlug` — server-derived slug of the fresh healthcare
334
- tenant
335
- - `datalakeSlug` — server-derived slug of the fresh healthcare
336
- datalake
337
- - `ctx.sarahEmail` / `ctx.sarahPassword` — the per-run admin
338
- credentials (kept on `ctx` because no scenario cookbook needs
339
- to re-authenticate by default)
340
- - `ctx.tenantApiKey` — the tenant's publishable API key (minted in
341
- setup via `admin.createTenantApiKey`); thread it into any
342
- additional tenant-scoped `createSession` a scenario performs
343
- - `ctx.waitForFiredRun(datalakeSlug, runId)` — polls `workflowRuns.get`
344
- until a scheduled run has actually fired, then returns its
345
- `{ workflowRunLogId, batchId }`. Use it after every `workflows.run`;
346
- reading `workflow_run_log_id` off the run response returns `null`
347
- because run-workflow only schedules (see §007)
348
-
349
- Scenario cookbooks under `industry: healthcare` start at their own
350
- `§001` with these slots already populated.
351
-
352
- # See also
353
-
354
- - `.agent/datalakes.md` — datalake create body shape (regulated +
355
- unregulated tier configuration)
356
- - `.agent/AGENTS.md` § SDK auth + client construction — root vs
357
- tenantless vs tenant-scoped session scopes
358
- - `integration-tests/tests/healthcare/bootstrap.test.ts` — the
359
- green test these snippets are lifted from (the §7b sibling
360
- datalake creation and the §10–§13 sanity probes are out of
361
- scope for this lean cookbook setup)