@alvera-ai/platform-sdk 0.11.0 → 0.12.1-next.420540
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 +7 -3
- package/.agent/ai_sandbox.md +25 -0
- package/.agent/cookbook/{custom-tables.md → generic-tables.md} +38 -35
- package/.agent/cookbook/talk-to-data.md +139 -0
- package/.agent/data_activation_clients.md +24 -0
- package/.agent/datalakes.md +55 -0
- package/LICENSE +21 -0
- package/README.md +2 -2
- package/dist/index.d.mts +167 -5
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +108 -6
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/.agent/AGENTS.md
CHANGED
|
@@ -357,7 +357,7 @@ The `cookbook/` directory holds two kinds of recipe, both validated by
|
|
|
357
357
|
an outcome.
|
|
358
358
|
- **Capability docs** (`<capability>.md`) — one platform capability each,
|
|
359
359
|
shown as the minimal call sequence that proves it (e.g. `bulk-ingest`,
|
|
360
|
-
`ai-agent-invoke`, `
|
|
360
|
+
`ai-agent-invoke`, `generic-tables`, `invite-team`). Read these to learn one
|
|
361
361
|
capability in isolation; cookbooks weave them into outcomes.
|
|
362
362
|
|
|
363
363
|
### Available cookbooks
|
|
@@ -430,14 +430,18 @@ full wire shape.
|
|
|
430
430
|
document/image and pull structured JSON out of it (`aiAgents.invoke` with
|
|
431
431
|
files). The correct file-vision path — the agent reads the file; the DAC
|
|
432
432
|
ingests it.
|
|
433
|
-
- [
|
|
434
|
-
built-in datasets don't model; deploy → ingest →
|
|
433
|
+
- [generic-tables](./cookbook/generic-tables.md) — stand up a generic table the
|
|
434
|
+
built-in datasets don't model; deploy → ingest → read back via executeSql.
|
|
435
435
|
- [action-status-updaters](./cookbook/action-status-updaters.md) — reconcile
|
|
436
436
|
the delivery status of messages you send, on a schedule.
|
|
437
437
|
- [system-templates](./cookbook/system-templates.md) — discover the platform's
|
|
438
438
|
built-in row-mapping Liquid templates.
|
|
439
439
|
- [invite-team](./cookbook/invite-team.md) — invite a teammate into your tenant
|
|
440
440
|
(root / tenantless / tenant-scoped sessions in one flow).
|
|
441
|
+
- [talk-to-data](./cookbook/talk-to-data.md) — turn a datalake conversational:
|
|
442
|
+
natural language → reviewable SQL (`datalakes.textToSql`, data-free by
|
|
443
|
+
construction) → read-only execution returning a `{ data, meta }` row page or a
|
|
444
|
+
CSV export (`datalakes.executeSql`).
|
|
441
445
|
|
|
442
446
|
## Utility namespaces
|
|
443
447
|
|
package/.agent/ai_sandbox.md
CHANGED
|
@@ -179,6 +179,31 @@ via consumer-authored fragments is structurally impossible: the
|
|
|
179
179
|
fragment is the WHERE *expression*, with values supplied via
|
|
180
180
|
positional bindings the platform builds.
|
|
181
181
|
|
|
182
|
+
### Datalake "talk to data" — `textToSql` + `executeSql`
|
|
183
|
+
|
|
184
|
+
The datalake BI surface (`api.datalakes.textToSql` /
|
|
185
|
+
`api.datalakes.executeSql`, see `datalakes.md` "Talk to data") is
|
|
186
|
+
the **full-statement** sibling of the WHERE-clause lane above, and
|
|
187
|
+
it sits inside the same Layer-3 boundary:
|
|
188
|
+
|
|
189
|
+
- **Generation is data-free by construction.** `textToSql` sends
|
|
190
|
+
only the natural-language `prompt` plus the datalake **schema**
|
|
191
|
+
to the LLM — never rows. The model returns SQL for review; no
|
|
192
|
+
datalake data ever crosses the LLM boundary, so generation is
|
|
193
|
+
safe in both `regulated` and `unregulated` mode. The two-call
|
|
194
|
+
split (generate, then execute) exists so a human or agent can
|
|
195
|
+
inspect and edit the SQL before any data is read.
|
|
196
|
+
- **Execution is read-only and mode-routed.** `executeSql` runs the
|
|
197
|
+
statement against the mode-appropriate schema with a read-only
|
|
198
|
+
connection: INSERT/UPDATE/DELETE/DDL are rejected (app-layer
|
|
199
|
+
`@deny` + a read-only DB transaction + an EXPLAIN preflight), and
|
|
200
|
+
`mode` (`regulated` vs `unregulated`) plus the session's tenant
|
|
201
|
+
(RLS) decide which schema and which rows are reachable — the same
|
|
202
|
+
Layer-1 compliance gates that govern every other read.
|
|
203
|
+
- **Pagination caps blast radius.** `page` / `page_size` map to a
|
|
204
|
+
server-side `LIMIT/OFFSET` window capped at the platform default,
|
|
205
|
+
so a single call cannot exfiltrate an unbounded result set.
|
|
206
|
+
|
|
182
207
|
## How the three layers compose
|
|
183
208
|
|
|
184
209
|
```
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
---
|
|
2
|
-
title: "Capability: stand up a
|
|
3
|
-
summary: A capability walk for generic tables. Create a
|
|
2
|
+
title: "Capability: stand up a generic table the platform doesn't model"
|
|
3
|
+
summary: A capability walk for generic tables. Create a generic table with typed, privacy-tagged columns (`api.genericTables.create`), wait for it to deploy, use the default ingestion client the platform auto-provisions for it, ingest a row (`api.dataActivationClients.ingest`), and read it back with read-only SQL (`api.datalakes.executeSql`). For data the built-in datasets don't cover.
|
|
4
4
|
industry: accounts_receivable
|
|
5
|
-
slug:
|
|
5
|
+
slug: generic-tables
|
|
6
6
|
vitest_source:
|
|
7
7
|
- integration-tests/tests/accounts_receivable/generic-tables.test.ts
|
|
8
8
|
- integration-tests/tests/accounts_receivable/bootstrap.test.ts
|
|
@@ -23,7 +23,7 @@ The lifecycle is:
|
|
|
23
23
|
**auto-provisions a default Data Activation Client** bound to an identity
|
|
24
24
|
contract — no tool, contract, or DAC to wire by hand.
|
|
25
25
|
3. `dataActivationClients.ingest(...)` a row through that default client.
|
|
26
|
-
4. `
|
|
26
|
+
4. `datalakes.executeSql(...)` reads it back by its unique column with read-only SQL.
|
|
27
27
|
|
|
28
28
|
This is **global** to every datalake — only the table's domain meaning differs.
|
|
29
29
|
See `generic_tables.md` for the column reference.
|
|
@@ -33,7 +33,7 @@ See `generic_tables.md` for the column reference.
|
|
|
33
33
|
The `_setup/accounts_receivable.md` bootstrap left `api`, `tenantSlug`, and
|
|
34
34
|
`datalakeSlug` populated.
|
|
35
35
|
|
|
36
|
-
## 001 — list existing
|
|
36
|
+
## 001 — list existing generic tables (sanity)
|
|
37
37
|
|
|
38
38
|
`genericTables.list` returns the paginated `{ data, meta }` envelope. A fresh
|
|
39
39
|
datalake has none; the call just confirms the surface is reachable.
|
|
@@ -45,7 +45,7 @@ if (!Array.isArray(existing.data)) {
|
|
|
45
45
|
}
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
-
## 002 — create the
|
|
48
|
+
## 002 — create the generic table
|
|
49
49
|
|
|
50
50
|
Declare the table with a `title` and `columns`. Each column has a `type`, an
|
|
51
51
|
`is_unique` flag, and — load-bearing — a `privacy_requirement` that decides how
|
|
@@ -115,7 +115,7 @@ while (Date.now() < deadline && !defaultDacSlug) {
|
|
|
115
115
|
if (!defaultDacSlug) await new Promise((r) => setTimeout(r, 1_000))
|
|
116
116
|
}
|
|
117
117
|
if (!defaultDacSlug) {
|
|
118
|
-
throw new Error('default DAC for the
|
|
118
|
+
throw new Error('default DAC for the generic table was not provisioned within 30s')
|
|
119
119
|
}
|
|
120
120
|
ctx.defaultDacSlug = defaultDacSlug
|
|
121
121
|
```
|
|
@@ -144,34 +144,34 @@ if (!ingest.batch_id) {
|
|
|
144
144
|
}
|
|
145
145
|
```
|
|
146
146
|
|
|
147
|
-
## 006 — read the row back
|
|
147
|
+
## 006 — read the row back with read-only SQL
|
|
148
148
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
149
|
+
`executeSql` runs a read-only statement against the datalake and returns a page
|
|
150
|
+
as `{ data, meta }` — `data` is an array-of-arrays aligned positionally to
|
|
151
|
+
`meta.columns`. `mode: 'unregulated'` runs it against the unregulated schema, so
|
|
152
|
+
you reference the table by its server-derived `name` (the `alvera_custom_…`
|
|
153
|
+
value from §002 — `mode` selects the schema, so no `regulated_`/`unregulated_`
|
|
154
|
+
prefix). Ingestion is async, so poll until the row lands.
|
|
153
155
|
|
|
154
156
|
```typescript
|
|
155
|
-
const { data: userSearch } = await api.datasets.createUserSearch(tenantSlug, datalakeSlug, 'generic_table', {
|
|
156
|
-
search_query: `regulated_${ctx.tableName}.submission_id = '${ctx.submissionId}'`,
|
|
157
|
-
generic_table_id: genericTableId,
|
|
158
|
-
})
|
|
159
|
-
if (userSearch.status !== 'completed') {
|
|
160
|
-
throw new Error(`user-search did not compile: ${userSearch.status}`)
|
|
161
|
-
}
|
|
162
|
-
|
|
163
157
|
const deadline = Date.now() + 45_000
|
|
164
|
-
let
|
|
165
|
-
while (Date.now() < deadline &&
|
|
166
|
-
const { data:
|
|
167
|
-
|
|
168
|
-
|
|
158
|
+
let row: Record<string, unknown> | undefined
|
|
159
|
+
while (Date.now() < deadline && !row) {
|
|
160
|
+
const { data: result } = await api.datalakes.executeSql(tenantSlug, datalakeSlug, {
|
|
161
|
+
sql: `SELECT submission_id, customer_name, source_channel
|
|
162
|
+
FROM ${ctx.tableName}
|
|
163
|
+
WHERE submission_id = '${ctx.submissionId}'`,
|
|
164
|
+
mode: 'unregulated',
|
|
169
165
|
})
|
|
170
|
-
|
|
171
|
-
|
|
166
|
+
// JSON envelope (a `string` only for `{ format: 'csv' }`); zip the positional
|
|
167
|
+
// row back into an object keyed by meta.columns.
|
|
168
|
+
if (typeof result !== 'string' && result.data.length > 0) {
|
|
169
|
+
row = Object.fromEntries(result.meta.columns.map((c, i) => [c, result.data[0][i]]))
|
|
170
|
+
}
|
|
171
|
+
if (!row) await new Promise((r) => setTimeout(r, 1_000))
|
|
172
172
|
}
|
|
173
|
-
if (!
|
|
174
|
-
throw new Error('ingested row not found in the
|
|
173
|
+
if (!row || row.submission_id !== ctx.submissionId) {
|
|
174
|
+
throw new Error('ingested row not found in the generic table within 45s')
|
|
175
175
|
}
|
|
176
176
|
```
|
|
177
177
|
|
|
@@ -182,15 +182,18 @@ if (!rows.some((r) => r.submission_id === ctx.submissionId)) {
|
|
|
182
182
|
free text is scrubbed, `none` passes through. Dropping it silently changes who
|
|
183
183
|
can see what.
|
|
184
184
|
- **The table name is server-derived, never set by you.** The platform slugifies
|
|
185
|
-
the title and prefixes `alvera_custom_`. Read `name` off the create response
|
|
186
|
-
the
|
|
185
|
+
the title and prefixes `alvera_custom_`. Read `name` off the create response and
|
|
186
|
+
reference it directly in `executeSql`; `mode` selects the schema, so you don't
|
|
187
|
+
add a `regulated_`/`unregulated_` prefix yourself.
|
|
187
188
|
- **You don't create the ingestion client.** Deploying the table auto-provisions
|
|
188
189
|
a default DAC + identity contract. Find it by name; don't build one.
|
|
189
|
-
- **Two async waits.** Wait for `:deployed` before ingesting, then poll
|
|
190
|
-
|
|
190
|
+
- **Two async waits.** Wait for `:deployed` before ingesting, then poll
|
|
191
|
+
`executeSql` until the row materializes — ingestion is async like every other
|
|
191
192
|
ingest path.
|
|
192
|
-
- **`
|
|
193
|
-
|
|
193
|
+
- **`mode` picks the schema, and `tokenize` columns come back masked.** In
|
|
194
|
+
`mode: 'unregulated'` the read runs against the tokenized view, so filter and
|
|
195
|
+
assert on a `none` column (here `submission_id`); a `tokenize` column like
|
|
196
|
+
`customer_name` returns its token, not the raw value.
|
|
194
197
|
|
|
195
198
|
# See also
|
|
196
199
|
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Capability: talk to your datalake — natural language → SQL → rows"
|
|
3
|
+
summary: A capability walk for datalake text-to-SQL. Generate a read-only SQL statement from a natural-language prompt (`api.datalakes.textToSql`) — only the prompt + schema cross the LLM boundary, never rows — then run it read-only and read back the page (`api.datalakes.executeSql`), with both the JSON `{ data, meta }` envelope and a CSV export. The BI surface on a datalake.
|
|
4
|
+
industry: foundation
|
|
5
|
+
slug: talk-to-data
|
|
6
|
+
vitest_source:
|
|
7
|
+
- integration-tests/tests/foundation/text-to-sql.test.ts
|
|
8
|
+
status: green
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Capability
|
|
12
|
+
|
|
13
|
+
**What you get:** a two-call BI surface on a datalake — describe what you want in
|
|
14
|
+
plain language, get back reviewable SQL, then run it read-only and read the rows.
|
|
15
|
+
|
|
16
|
+
The split is deliberate: `textToSql` *generates*, `executeSql` *runs*. A UI or
|
|
17
|
+
agent can show the SQL, let a human edit it, and re-run — and the generate step
|
|
18
|
+
is **data-free by construction** (only the prompt + the datalake schema reach the
|
|
19
|
+
LLM, never rows), so it is safe in both `regulated` and `unregulated` mode.
|
|
20
|
+
|
|
21
|
+
1. `datalakes.textToSql(...)` — natural language → `{ sql, model, provider, explanation }`.
|
|
22
|
+
2. `datalakes.executeSql(...)` — read-only execution → `{ data, meta }` (data is an
|
|
23
|
+
array-of-arrays row page aligned to `meta.columns`), or a CSV string with
|
|
24
|
+
`{ format: 'csv' }`.
|
|
25
|
+
|
|
26
|
+
See `datalakes.md` "Talk to data" for the wire reference and `ai_sandbox.md`
|
|
27
|
+
Layer 3 for why the boundary holds.
|
|
28
|
+
|
|
29
|
+
# Walkthrough
|
|
30
|
+
|
|
31
|
+
The `_setup/foundation.md` bootstrap left `api`, `tenantSlug`, and `datalakeSlug`
|
|
32
|
+
populated, pointing at a freshly-migrated foundation datalake.
|
|
33
|
+
|
|
34
|
+
## 001 — generate SQL from a natural-language prompt
|
|
35
|
+
|
|
36
|
+
`textToSql` runs ordered multi-provider LLM failover and returns the generated
|
|
37
|
+
`sql` plus the winning `model` / `provider` and a best-effort `explanation`
|
|
38
|
+
(`null` when the explainer is unavailable). It does **not** execute the SQL — it
|
|
39
|
+
hands it back for review. Nothing but the prompt and the datalake schema is sent
|
|
40
|
+
to the model, so no datalake rows leave the trust boundary.
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
const { data: gen } = await api.datalakes.textToSql(tenantSlug, datalakeSlug, {
|
|
44
|
+
prompt: 'how many legal entities are there?',
|
|
45
|
+
mode: 'unregulated',
|
|
46
|
+
})
|
|
47
|
+
if (typeof gen.sql !== 'string' || gen.sql.trim() === '') {
|
|
48
|
+
throw new Error(`textToSql returned no SQL (got: ${JSON.stringify(gen.sql)})`)
|
|
49
|
+
}
|
|
50
|
+
if (typeof gen.model !== 'string' || typeof gen.provider !== 'string') {
|
|
51
|
+
throw new Error('textToSql response missing model/provider attribution')
|
|
52
|
+
}
|
|
53
|
+
// gen.explanation is best-effort plain-language prose, or null.
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## 002 — run a read-only query and read back the page
|
|
57
|
+
|
|
58
|
+
`executeSql` runs the SQL **read-only** (INSERT/UPDATE/DELETE/DDL are rejected)
|
|
59
|
+
and returns `{ data, meta }`. `data` is an **array-of-arrays** row page — positional,
|
|
60
|
+
aligned to `meta.columns`, because arbitrary SQL can have duplicate / expression
|
|
61
|
+
column names. `meta` carries the structural + pagination fields. A deterministic
|
|
62
|
+
`SELECT 1 AS n` keeps this step independent of the generated SQL and seeded data.
|
|
63
|
+
|
|
64
|
+
The curated return is `ExecuteSqlResponse | string` (the CSV branch is a string),
|
|
65
|
+
so narrow with a `typeof` guard before reading the JSON envelope.
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
const jsonResult = await api.datalakes.executeSql(tenantSlug, datalakeSlug, {
|
|
69
|
+
sql: 'SELECT 1 AS n',
|
|
70
|
+
mode: 'unregulated',
|
|
71
|
+
})
|
|
72
|
+
if (typeof jsonResult.data === 'string') {
|
|
73
|
+
throw new Error('expected the JSON envelope, got a CSV string')
|
|
74
|
+
}
|
|
75
|
+
const page = jsonResult.data
|
|
76
|
+
if (!Array.isArray(page.data) || !Array.isArray(page.data[0])) {
|
|
77
|
+
throw new Error('executeSql data is not an array-of-arrays')
|
|
78
|
+
}
|
|
79
|
+
if (page.meta.columns[0] !== 'n' || page.data[0][0] !== 1 || page.meta.num_rows !== 1) {
|
|
80
|
+
throw new Error(`unexpected SELECT 1 result: ${JSON.stringify(page)}`)
|
|
81
|
+
}
|
|
82
|
+
// page.meta also carries total_count, page, page_size, total_pages, duration_ms, command.
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## 003 — export the same page as CSV
|
|
86
|
+
|
|
87
|
+
Pass `{ format: 'csv' }` to get the page as a CSV **string** instead of the JSON
|
|
88
|
+
envelope — the same read, content-negotiated for download.
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
const csvResult = await api.datalakes.executeSql(
|
|
92
|
+
tenantSlug,
|
|
93
|
+
datalakeSlug,
|
|
94
|
+
{ sql: 'SELECT 1 AS n', mode: 'unregulated' },
|
|
95
|
+
{ format: 'csv' },
|
|
96
|
+
)
|
|
97
|
+
if (typeof csvResult.data !== 'string') {
|
|
98
|
+
throw new Error('expected a CSV string for { format: "csv" }')
|
|
99
|
+
}
|
|
100
|
+
const csv = csvResult.data
|
|
101
|
+
if (!/\bn\b/.test(csv) || !csv.includes('1')) {
|
|
102
|
+
throw new Error(`unexpected CSV body: ${JSON.stringify(csv)}`)
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
# Branches
|
|
107
|
+
|
|
108
|
+
- **Generation failure.** If every configured LLM provider fails, `textToSql`
|
|
109
|
+
returns `422` (`AlveraApiError`) — `Text-to-SQL generation failed for all
|
|
110
|
+
providers (...)`. Surface it; do not retry blindly.
|
|
111
|
+
- **Write SQL rejected.** `executeSql` with a non-read-only statement
|
|
112
|
+
(`INSERT`/`UPDATE`/`DELETE`/DDL) is rejected `422` — the read-only boundary is
|
|
113
|
+
enforced at the app `@deny` regex, a read-only DB transaction, and an EXPLAIN
|
|
114
|
+
preflight.
|
|
115
|
+
- **Pagination.** Pass `page` / `page_size` to window large result sets;
|
|
116
|
+
`meta.total_count` / `meta.total_pages` describe the full set (`page_size` is
|
|
117
|
+
capped server-side).
|
|
118
|
+
|
|
119
|
+
# Rollback
|
|
120
|
+
|
|
121
|
+
Nothing to tear down — both calls are read-only (or generate-only). The
|
|
122
|
+
`_setup/foundation.md` tenant + datalake are reset with `mix ecto.reset` on the
|
|
123
|
+
platform; per-run `runSuffix` names avoid collisions across reruns.
|
|
124
|
+
|
|
125
|
+
# Outcome
|
|
126
|
+
|
|
127
|
+
A datalake becomes conversational: a prompt yields reviewable SQL with provider
|
|
128
|
+
attribution and a plain-language explanation, that SQL runs read-only, and the
|
|
129
|
+
page comes back either as a structured `{ data, meta }` envelope or a CSV export
|
|
130
|
+
— all without any datalake row ever reaching the LLM.
|
|
131
|
+
|
|
132
|
+
# See also
|
|
133
|
+
|
|
134
|
+
- `.agent/datalakes.md` — "Talk to data" (`textToSql` / `executeSql` wire shape)
|
|
135
|
+
and §6 gotcha on the array-of-arrays `data`.
|
|
136
|
+
- `.agent/ai_sandbox.md` — Layer 3 SQL boundary: how generation stays data-free
|
|
137
|
+
and execution stays read-only + mode-routed.
|
|
138
|
+
- `integration-tests/tests/foundation/text-to-sql.test.ts` — the green vitest
|
|
139
|
+
these snippets are lifted from.
|
|
@@ -386,6 +386,30 @@ durable" signal — not just the presence of the log row.
|
|
|
386
386
|
|
|
387
387
|
### 6.5 Verifying ingestion: the two-step dataset search
|
|
388
388
|
|
|
389
|
+
> **Simplest landing check — `executeSql`.** Ingestion is async (the row
|
|
390
|
+
> commits a moment after the `202`). When you just need to know a row
|
|
391
|
+
> *landed*, poll `datalakes.executeSql` with a read-only `SELECT … WHERE
|
|
392
|
+
> batch_id = '…'`: **each call is a fresh read, so there is no stale
|
|
393
|
+
> snapshot to recover from** — unlike a `createUserSearch` created before
|
|
394
|
+
> the row commits, which captures nothing and never updates. Gate on it,
|
|
395
|
+
> then run the two-step `createUserSearch` below **once** (race-free) only
|
|
396
|
+
> if you need the dataset's alias-join + tokenization semantics.
|
|
397
|
+
>
|
|
398
|
+
> ```typescript
|
|
399
|
+
> const { data: probe } = await api.datalakes.executeSql(tenantSlug, datalakeSlug, {
|
|
400
|
+
> sql: `SELECT 1 FROM patients WHERE batch_id = '${batchId}' LIMIT 1`,
|
|
401
|
+
> mode: 'unregulated',
|
|
402
|
+
> })
|
|
403
|
+
> // `data` is an array-of-arrays (a `string` only for { format: 'csv' });
|
|
404
|
+
> // a non-empty page proves the batch landed. Reference the unregulated
|
|
405
|
+
> // table by its base name — `mode: 'regulated'` uses the `regulated_` prefix.
|
|
406
|
+
> const landed = typeof probe !== 'string' && probe.data.length > 0
|
|
407
|
+
> ```
|
|
408
|
+
>
|
|
409
|
+
> `createUserSearch` is the heavier, workflow-oriented surface — reach for
|
|
410
|
+
> it when you need its alias joins (`ri.value`, tokenized read-back), not
|
|
411
|
+
> for a plain "did it land" check.
|
|
412
|
+
|
|
389
413
|
The canonical way to **prove a row landed in the datalake** is
|
|
390
414
|
to query the dataset directly. This is a two-step pattern:
|
|
391
415
|
|
package/.agent/datalakes.md
CHANGED
|
@@ -586,6 +586,52 @@ for (const name of catalog.datasets) {
|
|
|
586
586
|
}
|
|
587
587
|
```
|
|
588
588
|
|
|
589
|
+
### Talk to data — `textToSql` + `executeSql`
|
|
590
|
+
|
|
591
|
+
Two query methods turn the datalake into a BI surface: natural language → SQL →
|
|
592
|
+
rows. They are **split on purpose** so an agent or UI can show, edit, and re-run
|
|
593
|
+
the SQL between the two calls.
|
|
594
|
+
|
|
595
|
+
| Method | Returns |
|
|
596
|
+
|------------------------------------------------------------|-----------------------------------------------------------|
|
|
597
|
+
| `.textToSql(tenantSlug, datalakeSlug, body)` | `{ sql, model, provider, explanation }` |
|
|
598
|
+
| `.executeSql(tenantSlug, datalakeSlug, body, options?)` | `{ data, meta }` — or a CSV **string** with `{ format: 'csv' }` |
|
|
599
|
+
|
|
600
|
+
- **`textToSql`** body is `{ prompt, mode }` (`mode` ∈ `'regulated' | 'unregulated'`,
|
|
601
|
+
selecting which schema to target). It runs ordered multi-provider LLM failover and
|
|
602
|
+
returns the generated `sql`, the winning `model` + `provider`, and a best-effort
|
|
603
|
+
`explanation` (`null` when the explainer is unavailable). **Only the prompt + the
|
|
604
|
+
schema cross the LLM boundary — never datalake rows**, so it is safe in both modes.
|
|
605
|
+
It returns SQL for review; it does **not** execute it.
|
|
606
|
+
- **`executeSql`** body is `{ sql, mode, page?, page_size? }`. It runs the SQL
|
|
607
|
+
**read-only** (INSERT/UPDATE/DELETE/DDL are rejected) on the mode-appropriate
|
|
608
|
+
schema, with Flop-inspired pagination (`page_size` capped server-side). The JSON
|
|
609
|
+
shape is `{ data, meta }`; pass `options = { format: 'csv' }` to get the page as a
|
|
610
|
+
CSV string attachment instead.
|
|
611
|
+
|
|
612
|
+
```typescript
|
|
613
|
+
const { data: gen } = await api.datalakes.textToSql(tenantSlug, datalakeSlug, {
|
|
614
|
+
prompt: 'count contacts created this month',
|
|
615
|
+
mode: 'unregulated',
|
|
616
|
+
})
|
|
617
|
+
// gen.sql — review / edit before running
|
|
618
|
+
|
|
619
|
+
const { data: page } = await api.datalakes.executeSql(tenantSlug, datalakeSlug, {
|
|
620
|
+
sql: gen.sql,
|
|
621
|
+
mode: 'unregulated',
|
|
622
|
+
page: 1,
|
|
623
|
+
page_size: 100,
|
|
624
|
+
})
|
|
625
|
+
// page.data — array-of-arrays rows, aligned to page.meta.columns
|
|
626
|
+
// page.meta — { columns, num_rows, total_count, page, page_size, total_pages, duration_ms, command }
|
|
627
|
+
|
|
628
|
+
const { data: csv } = await api.datalakes.executeSql(
|
|
629
|
+
tenantSlug, datalakeSlug,
|
|
630
|
+
{ sql: gen.sql, mode: 'unregulated' },
|
|
631
|
+
{ format: 'csv' },
|
|
632
|
+
) // csv is a string, not the JSON envelope
|
|
633
|
+
```
|
|
634
|
+
|
|
589
635
|
## 6. Gotchas
|
|
590
636
|
|
|
591
637
|
1. **`create()` does NOT auto-enqueue migration.** The create response
|
|
@@ -712,3 +758,12 @@ for (const name of catalog.datasets) {
|
|
|
712
758
|
`"password"` — and consequently requires both `*_user` and
|
|
713
759
|
`*_pass` to be supplied. The IAM-role relaxation in §2 applies
|
|
714
760
|
only when `auth_method` is explicitly `"iam_role"`.
|
|
761
|
+
|
|
762
|
+
13. **`executeSql` rows are array-of-arrays, not objects.** `data` is
|
|
763
|
+
`Array<Array<unknown>>` — a positional row page with **no column
|
|
764
|
+
keys**, because arbitrary SQL can yield duplicate or expression
|
|
765
|
+
column names that have no safe object key. Read column names from
|
|
766
|
+
`meta.columns` and index rows positionally (`row[i]` ↔
|
|
767
|
+
`meta.columns[i]`); never assume `row.someColumn`. The `?format=csv`
|
|
768
|
+
branch returns a CSV **string** with no `meta` envelope — for the
|
|
769
|
+
structural metadata, use the JSON call.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alvera
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -132,7 +132,7 @@ Every resource is a typed namespace on the client (`api.<resource>.<verb>`):
|
|
|
132
132
|
| `tenants` | `list`, `create` |
|
|
133
133
|
| `invitations` | `list`, `create`, `accept` |
|
|
134
134
|
| `datasets` | `search`, `metadata`, `createUserSearch` |
|
|
135
|
-
| `datalakes` | `list`, `get`, `create`, `metadata`, `migrate`, `createUploadLink`, `createDownloadLink`
|
|
135
|
+
| `datalakes` | `list`, `get`, `create`, `metadata`, `migrate`, `createUploadLink`, `createDownloadLink`, `textToSql`, `executeSql` |
|
|
136
136
|
| `dataSources` | `list`, `create`, `update` |
|
|
137
137
|
| `tools` | `list`, `get`, `create`, `update`, `delete`, `testInvocation` |
|
|
138
138
|
| `genericTables` | `list`, `create` |
|
|
@@ -175,4 +175,4 @@ It writes an idempotent managed block; re-running replaces only that block.
|
|
|
175
175
|
|
|
176
176
|
## License
|
|
177
177
|
|
|
178
|
-
|
|
178
|
+
MIT — see [LICENSE](./LICENSE).
|