@softr/sdk 0.0.78-alpha.2 → 0.0.78
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/dist/action.d.ts +1 -34
- package/dist/action.js +1 -12
- package/dist/assertions.js +1 -1
- package/dist/auth/methods/profile.d.ts +5 -5
- package/dist/auth/methods/profile.js +2 -2
- package/dist/auth/platform.d.ts +0 -1
- package/dist/auth/platform.js +36 -14
- package/dist/auth/session.js +1 -1
- package/dist/auth/user.d.ts +8 -1
- package/dist/auth/user.js +2 -1
- package/dist/cli.js +38 -12
- package/dist/codegen/datasource-docs.js +92 -1
- package/dist/codegen/datasource-types.js +16 -1
- package/dist/codegen/fetch-metadata.js +27 -11
- package/dist/codegen/softr-env.js +2 -1
- package/dist/cookies.js +10 -2
- package/dist/formatting.d.ts +1 -1
- package/dist/formatting.js +1 -1
- package/dist/index.d.ts +3 -4
- package/dist/index.js +2 -2
- package/dist/mcp.d.ts +2 -2
- package/dist/mcp.js +2 -2
- package/dist/package.js +1 -1
- package/dist/pwa.js +1 -1
- package/dist/route/route.js +1 -1
- package/dist/server.d.ts +1 -2
- package/dist/server.js +1 -2
- package/dist/softr-config.schema.js +52 -24
- package/dist/vite.js +1 -19
- package/package.json +1 -1
- package/schema/softr.schema.json +13 -0
- package/skills/softr-sdk/SKILL.md +4 -12
- package/skills/softr-sdk/auth/SKILL.md +1 -1
- package/skills/softr-sdk/charts-and-metrics/SKILL.md +1 -1
- package/skills/softr-sdk/comments/SKILL.md +1 -1
- package/skills/softr-sdk/current-user/SKILL.md +1 -1
- package/skills/softr-sdk/file-uploads/SKILL.md +1 -1
- package/skills/softr-sdk/formatting/SKILL.md +1 -1
- package/skills/softr-sdk/integrations/SKILL.md +4 -4
- package/skills/softr-sdk/mcp/SKILL.md +1 -1
- package/skills/softr-sdk/pwa/SKILL.md +1 -1
- package/skills/softr-sdk/record-mutations/SKILL.md +1 -1
- package/skills/softr-sdk/record-queries/SKILL.md +1 -1
- package/skills/softr-sdk/region-visibility/SKILL.md +1 -1
- package/skills/softr-sdk/routes-and-visibility/SKILL.md +1 -1
- package/skills/softr-sdk/workflows/SKILL.md +1 -1
- package/dist/core/ai.d.ts +0 -76
- package/dist/core/ai.js +0 -64
- package/skills/softr-sdk/ai/SKILL.md +0 -161
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: ai
|
|
3
|
-
description: >-
|
|
4
|
-
Run an AI model from the app on Softr's own keys, billed to the workspace's
|
|
5
|
-
AI credits, with nothing to connect: defineAiAction from @softr/sdk declares
|
|
6
|
-
the action with the catalog model it runs on and its permissions;
|
|
7
|
-
ai.generate from @softr/sdk/server runs it with a system text, a prompt, an
|
|
8
|
-
optional JSON schema for an answer that is data, and optional files;
|
|
9
|
-
ai.stream runs it for an answer the user watches being written. Load this
|
|
10
|
-
for a summary, an extraction, a classification, a draft, or a chat the app
|
|
11
|
-
asks a model for.
|
|
12
|
-
metadata:
|
|
13
|
-
type: sub-skill
|
|
14
|
-
library: softr-sdk
|
|
15
|
-
library_version: 0.0.78-alpha.2
|
|
16
|
-
---
|
|
17
|
-
|
|
18
|
-
**Boundary.** `ai` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler — importing it into a component fails the build. `defineAiAction` and every type come from `@softr/sdk` and can be imported anywhere.
|
|
19
|
-
|
|
20
|
-
# AI
|
|
21
|
-
|
|
22
|
-
An app asks a model through an AI action. Softr runs the call on its own provider keys and bills the workspace's AI credits, so there is no key to ask the user for and nothing to connect. This is the path for every AI feature of the app's own — a summary of a record, fields extracted from a document, a classification, a draft, a chat — unless the user explicitly asked to use their own provider account, which is the `integrations` sub-skill's path.
|
|
23
|
-
|
|
24
|
-
## Choose the model first
|
|
25
|
-
|
|
26
|
-
The model is chosen when the app is built and frozen in its code. Before you declare an action, read Softr's catalog with the `ai_list_models` tool. Each entry carries the `id` the action names, the `provider`, and `bestFor`, what to pick it for:
|
|
27
|
-
|
|
28
|
-
- a summary, a briefing, a draft, reasoning over many facts: the strongest model listed;
|
|
29
|
-
- an extraction or a classification over one document: the cheapest model that reads that document's kind — a PDF needs a model whose `bestFor` says so.
|
|
30
|
-
|
|
31
|
-
Copy the `id` exactly. An id outside the catalog is refused when the action is saved, and a model spelled from memory never is in it.
|
|
32
|
-
|
|
33
|
-
## Declare the action
|
|
34
|
-
|
|
35
|
-
```ts
|
|
36
|
-
const summarizeDeal = defineAiAction({
|
|
37
|
-
id: 'summarizeDeal',
|
|
38
|
-
model: 'anthropic-claude-sonnet-4-6',
|
|
39
|
-
permissions: { userGroupTypes: ['LOGGED_IN_USERS'] },
|
|
40
|
-
})
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
Write the object inline in `defineAiAction`. Passing it through a variable widens the types.
|
|
44
|
-
|
|
45
|
-
**Every call spends the workspace's credits.** Keep the audience to `['LOGGED_IN_USERS']`, narrowed with `customGroups` where the feature is for some users only. Open an AI action to visitors only when a public page really needs it, and say so to the user: anyone on the internet can then bill the workspace.
|
|
46
|
-
|
|
47
|
-
Declare one action per feature — the summary and the extraction are two actions, each with its own `id`, its own model and its own audience.
|
|
48
|
-
|
|
49
|
-
## Core pattern: an answer the server reads
|
|
50
|
-
|
|
51
|
-
`ai.generate(action, init)` runs the action and answers when the model is done. `init` takes:
|
|
52
|
-
|
|
53
|
-
- `prompt` — what the model is asked, with the facts it is given. Required.
|
|
54
|
-
- `system` — what the model is and the rules it answers under.
|
|
55
|
-
- `schema` — a JSON schema the answer has to match. With it the model is asked for JSON only, and the answer comes back parsed in `json` as well as written in `text`.
|
|
56
|
-
- `files` — documents and images the model reads, each as `{ url, mimeType? }`: an `https` URL Softr may fetch, one of the app's own uploads as a rule.
|
|
57
|
-
- `maxOutputTokens` — a cap on the answer's length. Softr caps it further.
|
|
58
|
-
- `messages` — the earlier turns of a conversation, oldest first, each `{ role: 'user' | 'assistant', content }`. The prompt is the newest turn. Leave out for a one-shot call.
|
|
59
|
-
- `reasoning` — `'low'`, `'medium'` or `'high'`: how hard a reasoning model thinks before it answers. The catalog says which models reason; the others ignore it. Leave out for the provider's default.
|
|
60
|
-
- `temperature` — 0 to 1: how varied the answer is. `0` for an extraction or a classification, around `0.7` for a draft. A low default when left out.
|
|
61
|
-
|
|
62
|
-
The answer is `{ text, json, usage, model }`: `text` as the model wrote it, `json` parsed when a schema was given and the model kept to it, `null` otherwise.
|
|
63
|
-
|
|
64
|
-
```ts
|
|
65
|
-
const summarize = createServerFn({ method: 'POST' })
|
|
66
|
-
.validator(z.object({ recordId: z.string() }))
|
|
67
|
-
.handler(async ({ data }) => {
|
|
68
|
-
const deal = await ds.get(dealQuery, { id: data.recordId })
|
|
69
|
-
if (deal === null) throw new Error('This deal no longer exists')
|
|
70
|
-
const facts = [`Name: ${fieldText(deal.fields.name)}`, `Stage: ${fieldText(deal.fields.stage)}`].join('\n')
|
|
71
|
-
const answer = await ai.generate(summarizeDeal, {
|
|
72
|
-
system: 'You brief a sales team. Use only the facts given; say what is missing rather than invent it.',
|
|
73
|
-
prompt: facts,
|
|
74
|
-
maxOutputTokens: 800,
|
|
75
|
-
})
|
|
76
|
-
return answer.text
|
|
77
|
-
})
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
## Extraction: an answer that is data
|
|
81
|
-
|
|
82
|
-
A document dropped on the app, fields pulled out of it, a record created — the model reads the file and answers in the shape the record needs. Give it the schema and the file; validate the parsed answer before writing it, since a model may stray from the shape.
|
|
83
|
-
|
|
84
|
-
```ts
|
|
85
|
-
const InvoiceFields = z.object({ vendor: z.string(), total: z.number(), issuedOn: z.string() })
|
|
86
|
-
|
|
87
|
-
const extractInvoice = createServerFn({ method: 'POST' })
|
|
88
|
-
.validator(z.object({ fileUrl: z.string().url() }))
|
|
89
|
-
.handler(async ({ data }) => {
|
|
90
|
-
const answer = await ai.generate(readInvoice, {
|
|
91
|
-
system: 'Read the invoice and answer with its fields. Dates as YYYY-MM-DD, amounts as numbers.',
|
|
92
|
-
prompt: 'Extract the vendor, the total and the issue date.',
|
|
93
|
-
schema: {
|
|
94
|
-
type: 'object',
|
|
95
|
-
properties: { vendor: { type: 'string' }, total: { type: 'number' }, issuedOn: { type: 'string' } },
|
|
96
|
-
required: ['vendor', 'total', 'issuedOn'],
|
|
97
|
-
},
|
|
98
|
-
files: [{ url: data.fileUrl, mimeType: 'application/pdf' }],
|
|
99
|
-
})
|
|
100
|
-
const parsed = InvoiceFields.safeParse(answer.json)
|
|
101
|
-
if (!parsed.success) throw new Error('The invoice could not be read. Try a clearer scan.')
|
|
102
|
-
return ds.create(createInvoice, { data: parsed.data })
|
|
103
|
-
})
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
The file's URL comes from the app's own upload: load the Softr SDK `file-uploads` guidance for that half. `readInvoice` names a model whose `bestFor` reads PDF documents.
|
|
107
|
-
|
|
108
|
-
## A conversation
|
|
109
|
-
|
|
110
|
-
A chat sends its earlier turns in `messages` and the new one as `prompt`. The app keeps the turns; the model holds nothing between calls.
|
|
111
|
-
|
|
112
|
-
```ts
|
|
113
|
-
const reply = createServerFn({ method: 'POST' })
|
|
114
|
-
.validator(z.object({ turns: z.array(z.object({ role: z.enum(['user', 'assistant']), content: z.string() })), prompt: z.string() }))
|
|
115
|
-
.handler(async ({ data }) => {
|
|
116
|
-
const answer = await ai.generate(supportChat, {
|
|
117
|
-
system: 'You answer questions about the order the user is looking at.',
|
|
118
|
-
messages: data.turns,
|
|
119
|
-
prompt: data.prompt,
|
|
120
|
-
temperature: 0.5,
|
|
121
|
-
})
|
|
122
|
-
return answer.text
|
|
123
|
-
})
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
A chat the user watches being written streams instead, with the same `messages`: see the next section.
|
|
127
|
-
|
|
128
|
-
## Streaming: an answer the user watches being written
|
|
129
|
-
|
|
130
|
-
A chat, an assistant, a long draft shown as it is typed — never buffer these: the user would watch a blank panel until the last token. `ai.stream(action, init)` takes the same `init` and answers with a `Response` whose body is a stream of Server-Sent Events. Hand it to the browser from the server function, unchanged:
|
|
131
|
-
|
|
132
|
-
```ts
|
|
133
|
-
const draftReply = createServerFn({ method: 'POST' })
|
|
134
|
-
.validator(z.object({ prompt: z.string() }))
|
|
135
|
-
.handler(async ({ data }) => {
|
|
136
|
-
const res = await ai.stream(draftAction, { system: 'You write replies for a support team.', prompt: data.prompt })
|
|
137
|
-
return new Response(res.body, { headers: { 'Content-Type': 'text/event-stream' } })
|
|
138
|
-
})
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
Each event is one JSON object on a `data:` line:
|
|
142
|
-
|
|
143
|
-
- `{ "text": "..." }` — the next piece of the answer; append it.
|
|
144
|
-
- `{ "done": true, "usage": { "inputTokens", "outputTokens" }, "model": "..." }` — the answer is complete.
|
|
145
|
-
- `{ "error": "..." }` — in place of `done`, when the call ended before its answer: show the message.
|
|
146
|
-
|
|
147
|
-
On the client, call the server function's URL with `fetch`, read `response.body` with a stream reader, split on blank lines, and parse the JSON after `data:`.
|
|
148
|
-
|
|
149
|
-
## Failure modes
|
|
150
|
-
|
|
151
|
-
**Calling from client code.** `ai` is server-only. Importing it into a component or calling it outside a `createServerFn` handler fails the build or has no request context to act on.
|
|
152
|
-
|
|
153
|
-
**Spelling a model id.** `model` is copied from `ai_list_models`, never written from memory. An id outside the catalog fails the save of the action, so the app cannot be published with it.
|
|
154
|
-
|
|
155
|
-
**No credits left.** `ai.generate` and `ai.stream` throw an `AiError` whose `status` is `402` when the workspace has no AI credits. Show its message to the user; the workspace owner tops the credits up in Softr. Any other refusal carries its status and reason the same way.
|
|
156
|
-
|
|
157
|
-
**Trusting `json` without checking it.** A model may stray from the schema; `json` is then `null`, or a shape close to the one asked. Validate it with `zod` before writing it to a record, and keep `text` as the fallback the user can read.
|
|
158
|
-
|
|
159
|
-
**Reading a streamed body with `res.json()`.** The body of `ai.stream` is an event stream, not one JSON document. Return `res.body` in a `Response` or read it with a stream reader.
|
|
160
|
-
|
|
161
|
-
**Asking for the user's API key.** An AI feature of the app's own needs none. Only when the user asks to run the app on their own provider account does the call go through a connected integration — load the `integrations` sub-skill then.
|