wizards 0.2.25 → 0.2.27
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/apps/runtime/dist/agents/architect.js +5 -0
- package/apps/runtime/dist/agents/project-assistant.js +10 -1
- package/apps/runtime/dist/authoring/guide.js +61 -8
- package/apps/runtime/dist/connectors/builtin.js +17 -1
- package/apps/runtime/dist/connectors/index.js +43 -14
- package/apps/runtime/dist/connectors/plugin-connections.js +248 -0
- package/apps/runtime/dist/connectors/via-account.js +63 -0
- package/apps/runtime/dist/credits/estimate.js +12 -2
- package/apps/runtime/dist/db/migrations/tenant/0010_plugin_connection.sql +15 -0
- package/apps/runtime/dist/db/migrations/tenant/meta/0010_snapshot.json +2942 -0
- package/apps/runtime/dist/db/migrations/tenant/meta/_journal.json +7 -0
- package/apps/runtime/dist/db/schema.js +21 -0
- package/apps/runtime/dist/engenty/connections-google/connectors/calendar.js +25 -3
- package/apps/runtime/dist/engenty/connections-microsoft/outlook.js +29 -1
- package/apps/runtime/dist/engine/decide.js +82 -0
- package/apps/runtime/dist/engine/input.js +21 -5
- package/apps/runtime/dist/engine/requirements.js +80 -15
- package/apps/runtime/dist/engine/runner.js +252 -6
- package/apps/runtime/dist/engine/steps.js +206 -18
- package/apps/runtime/dist/engine/surface.js +90 -0
- package/apps/runtime/dist/mcp/tools.js +3 -2
- package/apps/runtime/dist/plugins/loader.js +22 -1
- package/apps/runtime/dist/plugins/registry.js +10 -0
- package/apps/runtime/dist/render/downloads.js +7 -0
- package/apps/runtime/dist/routes/runs.js +28 -7
- package/apps/runtime/dist/routes/studio.js +3 -1
- package/apps/runtime/dist/services/marketplace.js +134 -3
- package/apps/runtime/dist/services/relevance.js +17 -49
- package/apps/runtime/dist/services/runs.js +43 -15
- package/apps/runtime/dist/services/wizards.js +4 -2
- package/apps/runtime/dist/tools/pages.js +31 -6
- package/apps/runtime/dist/tools/plugin.js +29 -1
- package/apps/web/dist/assets/Account-DW396ig5.js +1 -0
- package/apps/web/dist/assets/{ChangelogDialog-B7C0r9dF.js → ChangelogDialog-BdZ1SmEd.js} +1 -1
- package/apps/web/dist/assets/CreditsDialog-fOYdoev3.js +1 -0
- package/apps/web/dist/assets/EditorPage-CAARFsaI.js +3 -0
- package/apps/web/dist/assets/{FlowPreview-D0m5Qk6S.js → FlowPreview-U2AEnKlX.js} +1 -1
- package/apps/web/dist/assets/{Harness-bIL3pHB_.js → Harness-CtDlm_2b.js} +2 -2
- package/apps/web/dist/assets/HomePage-Bd-mkLxW.js +1 -0
- package/apps/web/dist/assets/ImportWizard-BMPGjd_t.js +1 -0
- package/apps/web/dist/assets/LocalRuntime-DjSNCRnV.js +1 -0
- package/apps/web/dist/assets/NewWizardPage-DZPcDR9i.js +2 -0
- package/apps/web/dist/assets/ProjectPage-_LAtG3tn.js +2 -0
- package/apps/web/dist/assets/PublicRunner-6Iqp0DmU.js +1 -0
- package/apps/web/dist/assets/RunDrawer-CtQoY3Ha.js +4 -0
- package/apps/web/dist/assets/RunnerView-BLOCNZaZ.js +2 -0
- package/apps/web/dist/assets/SettingsPage-Z6skBf6I.js +1 -0
- package/apps/web/dist/assets/SetupPage-4-Lic4pT.js +1 -0
- package/apps/web/dist/assets/SharePage-DfdhXxZm.js +1 -0
- package/apps/web/dist/assets/arrow-right-BOtqnsA1.js +1 -0
- package/apps/web/dist/assets/brand-CCAI9p0f.js +2 -0
- package/apps/web/dist/assets/changelog-DQM0oz9S.js +1 -0
- package/apps/web/dist/assets/circle-alert-BC6xmJ9r.js +1 -0
- package/apps/web/dist/assets/definition-D5V79jHA.js +1 -0
- package/apps/web/dist/assets/esm-FEWi61F_.js +7 -0
- package/apps/web/dist/assets/external-link-wKL1HE6G.js +1 -0
- package/apps/web/dist/assets/image-BMdSChOR.js +1 -0
- package/apps/web/dist/assets/index-BOp2fz-2.css +1 -0
- package/apps/web/dist/assets/index-YJMooz0J.js +132 -0
- package/apps/web/dist/assets/lock-BpqzLMuv.js +1 -0
- package/apps/web/dist/assets/meta-BqZ7SlZz.js +1 -0
- package/apps/web/dist/assets/pen-line-DM-u0dES.js +1 -0
- package/apps/web/dist/assets/refresh-cw-Bk7HkOIM.js +1 -0
- package/apps/web/dist/assets/settings-menu-BM_NfZfn.js +1 -0
- package/apps/web/dist/assets/star-JHzYYl5R.js +1 -0
- package/apps/web/dist/assets/trash-CdLg-Ela.js +1 -0
- package/apps/web/dist/assets/ui-8QmvuC9u.js +9 -0
- package/apps/web/dist/assets/upload-CL1R6shM.js +1 -0
- package/apps/web/dist/assets/video-BioLmXNO.js +1 -0
- package/apps/web/dist/assets/volume-2-_IOfPNCv.js +1 -0
- package/apps/web/dist/assets/wand-sparkles-DYtDQSJZ.js +1 -0
- package/apps/web/dist/index.html +4 -5
- package/apps/web/dist/mcp/flow.html +61 -61
- package/node_modules/@engenty-wizards/plugin-sdk/dist/index.d.ts +85 -0
- package/node_modules/@engenty-wizards/plugin-sdk/dist/studio.d.ts +23 -0
- package/node_modules/@engenty-wizards/shared/dist/decision.d.ts +76 -0
- package/node_modules/@engenty-wizards/shared/dist/decision.js +147 -0
- package/node_modules/@engenty-wizards/shared/dist/definition.d.ts +1076 -51
- package/node_modules/@engenty-wizards/shared/dist/definition.js +597 -44
- package/node_modules/@engenty-wizards/shared/dist/marketplace-entry.js +19 -2
- package/node_modules/@engenty-wizards/shared/dist/marketplace.d.ts +6 -1
- package/node_modules/@engenty-wizards/shared/dist/run.d.ts +55 -0
- package/node_modules/@engenty-wizards/shared/dist/run.js +3 -0
- package/node_modules/@engenty-wizards/shared/dist/surface.d.ts +89 -0
- package/node_modules/@engenty-wizards/shared/dist/surface.js +198 -0
- package/package.json +1 -1
- package/apps/web/dist/assets/Account-CpxsROpe.js +0 -1
- package/apps/web/dist/assets/CreditsDialog-Com2Y3g8.js +0 -1
- package/apps/web/dist/assets/EditorPage-BCagWjrz.js +0 -2
- package/apps/web/dist/assets/HomePage-DPE_Z9HB.js +0 -1
- package/apps/web/dist/assets/ImportWizard-Dz2uKznp.js +0 -1
- package/apps/web/dist/assets/LocalRuntime-wMKUVZOu.js +0 -1
- package/apps/web/dist/assets/NewWizardPage-BOxU2jWq.js +0 -2
- package/apps/web/dist/assets/ProjectPage-BJp2wqIk.js +0 -5
- package/apps/web/dist/assets/PublicRunner-BsrqVK0J.js +0 -1
- package/apps/web/dist/assets/RunDrawer-CLfJ0W_V.js +0 -1
- package/apps/web/dist/assets/RunnerView-BXMELFC4.js +0 -70
- package/apps/web/dist/assets/SettingsPage-BkuUm-bf.js +0 -1
- package/apps/web/dist/assets/SetupPage-CoyXpcvS.js +0 -1
- package/apps/web/dist/assets/SharePage-BuR-he1L.js +0 -1
- package/apps/web/dist/assets/api-rJBQEu1G.js +0 -10
- package/apps/web/dist/assets/arrow-right-6FyN3e7D.js +0 -1
- package/apps/web/dist/assets/changelog-Bmw6dg-T.js +0 -1
- package/apps/web/dist/assets/check-CUpby_J9.js +0 -1
- package/apps/web/dist/assets/chevron-down-DymW-2BP.js +0 -1
- package/apps/web/dist/assets/circle-alert-D2SCwFT-.js +0 -1
- package/apps/web/dist/assets/createLucideIcon-q3MZMmnf.js +0 -1
- package/apps/web/dist/assets/definition-BznNdlhf.js +0 -55
- package/apps/web/dist/assets/esm-BrGYYg2h.js +0 -7
- package/apps/web/dist/assets/external-link-MAL7vJYj.js +0 -1
- package/apps/web/dist/assets/image-CC_2mJd6.js +0 -1
- package/apps/web/dist/assets/index-BZs1IxAk.css +0 -1
- package/apps/web/dist/assets/index-Co5QOA9_.js +0 -10
- package/apps/web/dist/assets/lock-DZL2N67v.js +0 -1
- package/apps/web/dist/assets/meta-B3jzXP27.js +0 -1
- package/apps/web/dist/assets/pen-line-NK46C_nG.js +0 -1
- package/apps/web/dist/assets/plus-Du-Jx8aI.js +0 -1
- package/apps/web/dist/assets/refresh-cw-BNtHis1x.js +0 -1
- package/apps/web/dist/assets/settings-menu-CCNUb0Fd.js +0 -1
- package/apps/web/dist/assets/table-2-BdDuBcF2.js +0 -1
- package/apps/web/dist/assets/trash-0IafyhZB.js +0 -1
- package/apps/web/dist/assets/ui-DJOZSSKo.js +0 -1
- package/apps/web/dist/assets/upload-DNRENWzQ.js +0 -1
- package/apps/web/dist/assets/video-9fDPRPCv.js +0 -1
- package/apps/web/dist/assets/volume-2-DE67_rue.js +0 -1
- package/apps/web/dist/assets/wand-sparkles-Clj3VRB5.js +0 -1
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { assembleSurface, dataRef, listRef, } from "@engenty-wizards/shared/definition";
|
|
2
|
+
import { validateSurface } from "@engenty-wizards/shared/surface";
|
|
3
|
+
import { decide } from "./decide.js";
|
|
4
|
+
import { answersAsText, resolveRef } from "./template.js";
|
|
5
|
+
import { StepError } from "./types.js";
|
|
6
|
+
/**
|
|
7
|
+
* A surface step: its data read from the run (answers, outputs, stored lists; the pictures a
|
|
8
|
+
* step made as asset ids), its components written once — or, for a decided surface, the pieces
|
|
9
|
+
* one decision keeps. Checked against the catalog before it is shown.
|
|
10
|
+
*/
|
|
11
|
+
export async function runSurfaceStep(step, ctx) {
|
|
12
|
+
const data = surfaceData(step.data, ctx);
|
|
13
|
+
const components = step.components ?? (await decidedComponents(step, ctx));
|
|
14
|
+
const issues = validateSurface(components, Object.keys(data));
|
|
15
|
+
if (issues.length) {
|
|
16
|
+
throw new StepError(`Die Ansicht ist fehlerhaft: ${issues.map((i) => i.message).join(" ")}`);
|
|
17
|
+
}
|
|
18
|
+
return { surface: { components, data }, at: new Date().toISOString() };
|
|
19
|
+
}
|
|
20
|
+
/** What a surface reads: key → a field, a step's output or one of its fields, a stored list. */
|
|
21
|
+
export function surfaceData(refs, ctx) {
|
|
22
|
+
const out = {};
|
|
23
|
+
for (const [key, raw] of Object.entries(refs)) {
|
|
24
|
+
const ref = dataRef(raw);
|
|
25
|
+
const list = listRef(ref);
|
|
26
|
+
if (list) {
|
|
27
|
+
out[key] = (ctx.scope.lists?.[list]?.rows ?? []).map((r) => r.cells);
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
const source = ctx.def.steps.find((s) => `steps.${s.id}` === ref);
|
|
31
|
+
if (source && (source.type === "generate" || source.type === "widget")) {
|
|
32
|
+
// Pictures and clips by asset id: the runner shows them from the run's files.
|
|
33
|
+
const ids = (ctx.state.outputs[source.id]?.assets ?? []).map((a) => a.id);
|
|
34
|
+
out[key] = source.type === "generate" && source.each ? ids : (ids[0] ?? null);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
const output = source ? ctx.state.outputs[source.id] : undefined;
|
|
38
|
+
out[key] = output ? (output.json ?? output.text ?? null) : (resolveRef(ref, ctx.scope) ?? null);
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
/** The pieces of a decided surface one decision keeps: required ones always. */
|
|
43
|
+
async function decidedComponents(step, ctx) {
|
|
44
|
+
const candidates = step.candidates ?? [];
|
|
45
|
+
const questions = {};
|
|
46
|
+
for (const c of candidates) {
|
|
47
|
+
if (!c.required && !c.group) {
|
|
48
|
+
questions[`p_${c.id}`] = {
|
|
49
|
+
type: "noul",
|
|
50
|
+
instructions: `Should the view show this: ${c.description}`,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
for (const g of step.groups ?? []) {
|
|
55
|
+
const members = candidates.filter((c) => c.group === g.id && !c.required);
|
|
56
|
+
if (members.length) {
|
|
57
|
+
questions[`g_${g.id}`] = {
|
|
58
|
+
type: "choice",
|
|
59
|
+
instructions: g.instructions,
|
|
60
|
+
criteria: {
|
|
61
|
+
...Object.fromEntries(members.map((c) => [c.id, c.description])),
|
|
62
|
+
...(g.optional ? { none: "None of these." } : {}),
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const kept = new Set(candidates.filter((c) => c.required).map((c) => c.id));
|
|
68
|
+
if (Object.keys(questions).length) {
|
|
69
|
+
const results = Object.fromEntries(ctx.def.steps
|
|
70
|
+
.map((s) => [s.title, ctx.state.outputs[s.id]?.text ?? ""])
|
|
71
|
+
.filter(([, text]) => text)
|
|
72
|
+
.map(([title, text]) => [title, text.slice(0, 6000)]));
|
|
73
|
+
const decision = await decide({
|
|
74
|
+
state: { answers: answersAsText(ctx.scope), results },
|
|
75
|
+
questions,
|
|
76
|
+
call: ctx.call,
|
|
77
|
+
signal: ctx.signal,
|
|
78
|
+
charge: ctx.chargeUsd,
|
|
79
|
+
});
|
|
80
|
+
for (const [name, answer] of Object.entries(decision.answers)) {
|
|
81
|
+
if (answer.type === "noul" && answer.noul >= 0.5) {
|
|
82
|
+
kept.add(name.slice(2));
|
|
83
|
+
}
|
|
84
|
+
else if (answer.type === "choice" && answer.choice !== "none") {
|
|
85
|
+
kept.add(answer.choice);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return assembleSurface(candidates, kept);
|
|
90
|
+
}
|
|
@@ -143,7 +143,7 @@ export function registerTools(server, who) {
|
|
|
143
143
|
});
|
|
144
144
|
tool("list_starters", "wizards:read", {
|
|
145
145
|
title: "List starters",
|
|
146
|
-
description: "
|
|
146
|
+
description: "Ready-made wizards to start from: first those the space's plugins bring (marked with the plugin), then the marketplace's (tweet with image, video ad, research briefing, dashboard, invoice, offer …), with what each makes and needs. Give a query to search them; without one you get the starters.",
|
|
147
147
|
input: z.object({ query: z.string().max(200).optional() }),
|
|
148
148
|
readOnly: true,
|
|
149
149
|
}, async ({ query }) => (await searchMarketplace({ q: query ?? "", lang: "de", limit: 30 })).entries
|
|
@@ -155,6 +155,7 @@ export function registerTools(server, who) {
|
|
|
155
155
|
formats: s.formats,
|
|
156
156
|
useCases: s.useCases,
|
|
157
157
|
capabilities: s.capabilities,
|
|
158
|
+
...(s.plugin ? { plugin: s.plugin.name } : {}),
|
|
158
159
|
})));
|
|
159
160
|
tool("get_starter", "wizards:read", {
|
|
160
161
|
title: "Get starter",
|
|
@@ -401,7 +402,7 @@ While status is running, get_run with waitSeconds. Hosts with MCP Apps also show
|
|
|
401
402
|
}, async ({ runId, waitSeconds }) => runReport(who.userId, runId, waitSeconds));
|
|
402
403
|
tool("answer_page", "runs:test", {
|
|
403
404
|
title: "Answer page",
|
|
404
|
-
description: "Fill the page a run waits for: field id → value (items: an array of row objects; toggle: true/false; multiselect: an array). Returns the run, after waitSeconds.",
|
|
405
|
+
description: "Fill the page a run waits for: field id → value (items: an array of row objects; toggle: true/false; multiselect: an array; slot: one of the offered times, as given). Returns the run, after waitSeconds.",
|
|
405
406
|
input: z.object({
|
|
406
407
|
runId: z.string(),
|
|
407
408
|
stepId: z.string().describe("waitingFor.page"),
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, watch } from "node:fs";
|
|
2
2
|
import { join, resolve, sep } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
-
import { PLUGIN_TOOL_NAME } from "@engenty-wizards/shared/definition";
|
|
4
|
+
import { PLUGIN_ID, PLUGIN_TOOL_NAME, wizardSchema } from "@engenty-wizards/shared/definition";
|
|
5
5
|
import { generateText, Output } from "ai";
|
|
6
6
|
import { createJiti } from "jiti";
|
|
7
7
|
import { readPage } from "../agents/website.js";
|
|
8
8
|
import { packageRoot } from "../cli/home.js";
|
|
9
|
+
import { pluginConnections } from "../connectors/plugin-connections.js";
|
|
9
10
|
import { db, onTenantOpen } from "../db/client.js";
|
|
10
11
|
import { parseDocument } from "../documents/parse.js";
|
|
11
12
|
import { env } from "../env.js";
|
|
@@ -98,6 +99,9 @@ function apiFor(record) {
|
|
|
98
99
|
}
|
|
99
100
|
record.assistantTools.set(tool.name, tool);
|
|
100
101
|
},
|
|
102
|
+
registerAssistantContext(context) {
|
|
103
|
+
record.assistantPages.push(context);
|
|
104
|
+
},
|
|
101
105
|
registerPublicRoute(route) {
|
|
102
106
|
record.publicRoutes.push(compileRoute(route));
|
|
103
107
|
},
|
|
@@ -153,6 +157,21 @@ function apiFor(record) {
|
|
|
153
157
|
object: (request.schema ? result.output : undefined),
|
|
154
158
|
};
|
|
155
159
|
},
|
|
160
|
+
connections: pluginConnections(source.id),
|
|
161
|
+
registerStarter(starter) {
|
|
162
|
+
if (!PLUGIN_ID.test(starter.id)) {
|
|
163
|
+
throw new Error(`Starter "${starter.id}": an id is lower case, digits and "-".`);
|
|
164
|
+
}
|
|
165
|
+
if (record.starters.has(starter.id)) {
|
|
166
|
+
throw new Error(`Starter "${starter.id}" is registered twice.`);
|
|
167
|
+
}
|
|
168
|
+
const parsed = wizardSchema.safeParse(starter.definition);
|
|
169
|
+
if (!parsed.success) {
|
|
170
|
+
const issue = parsed.error.issues[0];
|
|
171
|
+
throw new Error(`Starter "${starter.id}": ${issue?.path.join(".") || "definition"}: ${issue?.message}`);
|
|
172
|
+
}
|
|
173
|
+
record.starters.set(starter.id, { ...starter, definition: parsed.data });
|
|
174
|
+
},
|
|
156
175
|
onUnload(dispose) {
|
|
157
176
|
record.disposers.push(dispose);
|
|
158
177
|
},
|
|
@@ -186,8 +205,10 @@ async function load(source) {
|
|
|
186
205
|
record.tools.clear();
|
|
187
206
|
record.contexts = [];
|
|
188
207
|
record.assistantTools.clear();
|
|
208
|
+
record.assistantPages = [];
|
|
189
209
|
record.publicRoutes = [];
|
|
190
210
|
record.jobs.clear();
|
|
211
|
+
record.starters.clear();
|
|
191
212
|
record.listeners = {};
|
|
192
213
|
record.migrations = [];
|
|
193
214
|
record.error = err?.message || String(err);
|
|
@@ -17,8 +17,10 @@ export function emptyRecord(source) {
|
|
|
17
17
|
tools: new Map(),
|
|
18
18
|
contexts: [],
|
|
19
19
|
assistantTools: new Map(),
|
|
20
|
+
assistantPages: [],
|
|
20
21
|
publicRoutes: [],
|
|
21
22
|
jobs: new Map(),
|
|
23
|
+
starters: new Map(),
|
|
22
24
|
listeners: {},
|
|
23
25
|
migrations: [],
|
|
24
26
|
disposers: [],
|
|
@@ -83,6 +85,14 @@ export async function pluginToolsOf(tenantId) {
|
|
|
83
85
|
description: tool.description,
|
|
84
86
|
})));
|
|
85
87
|
}
|
|
88
|
+
/** The starters the tenant's plugins bring, in the order the plugins registered them. */
|
|
89
|
+
export async function pluginStartersOf(tenantId) {
|
|
90
|
+
return (await pluginsOf(tenantId)).flatMap((plugin) => [...plugin.starters.values()].map((starter) => ({
|
|
91
|
+
id: `${plugin.source.id}.${starter.id}`,
|
|
92
|
+
plugin,
|
|
93
|
+
starter,
|
|
94
|
+
})));
|
|
95
|
+
}
|
|
86
96
|
/** The built studio half as it is on disk now; null when the plugin has none. */
|
|
87
97
|
export function pluginAsset(record, kind) {
|
|
88
98
|
const file = record.source[kind];
|
|
@@ -69,6 +69,13 @@ export async function renderDownload(step, output, format, baseName) {
|
|
|
69
69
|
? { data: new Uint8Array(found.data), mime: found.row.mime, filename: file(format) }
|
|
70
70
|
: null;
|
|
71
71
|
}
|
|
72
|
+
if (step.type === "surface") {
|
|
73
|
+
return {
|
|
74
|
+
data: JSON.stringify(output.surface ?? {}, null, 2),
|
|
75
|
+
mime: MIME.json,
|
|
76
|
+
filename: file("json"),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
72
79
|
if (step.type === "widget") {
|
|
73
80
|
const source = output.assets?.find((a) => a.mime === "text/html");
|
|
74
81
|
if (format === "json") {
|
|
@@ -9,6 +9,7 @@ import { nanoid } from "nanoid";
|
|
|
9
9
|
import { z } from "zod";
|
|
10
10
|
import { principalOf } from "../auth/index.js";
|
|
11
11
|
import { ConnectError, connectionViews, connectWithCredentials, disconnect, finishOAuth, oauthStateRun, startOAuth, } from "../connectors/index.js";
|
|
12
|
+
import { finishPluginOAuth, pluginStateTenant } from "../connectors/plugin-connections.js";
|
|
12
13
|
import { db, schema, withTenant } from "../db/client.js";
|
|
13
14
|
import { answerAsk, pendingAsk } from "../engine/asks.js";
|
|
14
15
|
import { signalChanged, subscribe } from "../engine/events.js";
|
|
@@ -558,8 +559,16 @@ export const runRoutes = new Hono()
|
|
|
558
559
|
if (!run || !connection) {
|
|
559
560
|
return c.json({ error: "not found" }, 404);
|
|
560
561
|
}
|
|
561
|
-
|
|
562
|
-
|
|
562
|
+
try {
|
|
563
|
+
const url = await startOAuth(scopeOf(run), await projectIdOf(run), run.id, connection, c.req.param("connectorId"));
|
|
564
|
+
return c.json({ url });
|
|
565
|
+
}
|
|
566
|
+
catch (err) {
|
|
567
|
+
if (err instanceof ConnectError) {
|
|
568
|
+
return c.json({ error: err.message }, 422);
|
|
569
|
+
}
|
|
570
|
+
throw err;
|
|
571
|
+
}
|
|
563
572
|
})
|
|
564
573
|
.post("/:id/connections/:connectionId/credentials/:connectorId", async (c) => {
|
|
565
574
|
const run = await accessibleRun(c, c.req.param("id"));
|
|
@@ -780,14 +789,26 @@ function sendDownload(c, download) {
|
|
|
780
789
|
*/
|
|
781
790
|
export const connectCallback = new Hono().get("/callback", async (c) => {
|
|
782
791
|
const code = c.req.query("code");
|
|
792
|
+
// Connected through the account's Manage-App: a ticket instead of a code.
|
|
793
|
+
const ticket = c.req.query("ticket");
|
|
783
794
|
const state = c.req.query("state");
|
|
784
795
|
let ok = false;
|
|
785
|
-
let message = "
|
|
786
|
-
|
|
787
|
-
|
|
796
|
+
let message = c.req.query("error") === "exchange_failed"
|
|
797
|
+
? "Verbinden hat nicht geklappt. Bitte noch einmal versuchen."
|
|
798
|
+
: "Verbinden wurde abgebrochen.";
|
|
799
|
+
// A plugin's sign-in (server.connections) names its tenant; a wizard's names its run.
|
|
800
|
+
const pluginTenant = state ? pluginStateTenant(state) : null;
|
|
801
|
+
const tenant = state ? (pluginTenant ?? (await tenantOfRun(oauthStateRun(state) ?? ""))) : null;
|
|
802
|
+
const answer = ticket ? { ticket } : code ? { code } : null;
|
|
803
|
+
if (answer && state && tenant) {
|
|
788
804
|
try {
|
|
789
|
-
|
|
790
|
-
|
|
805
|
+
if (pluginTenant) {
|
|
806
|
+
await withTenant(tenant, () => finishPluginOAuth(answer, state));
|
|
807
|
+
}
|
|
808
|
+
else {
|
|
809
|
+
const done = await withTenant(tenant, () => finishOAuth(answer, state));
|
|
810
|
+
signalChanged(done.runId);
|
|
811
|
+
}
|
|
791
812
|
ok = true;
|
|
792
813
|
message = "Verbunden. Du kannst dieses Fenster schließen.";
|
|
793
814
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { mkdirSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { MODEL_CLASSES, TEXT_CLASSES } from "@engenty-wizards/shared/definition";
|
|
3
|
+
import { MODEL_CLASSES, PLUGIN_ID, TEXT_CLASSES } from "@engenty-wizards/shared/definition";
|
|
4
4
|
import { CATEGORY_TYPES, whereSchema } from "@engenty-wizards/shared/knowledge";
|
|
5
5
|
import { PROJECT_FILE_KINDS } from "@engenty-wizards/shared/projects";
|
|
6
6
|
import { isCreditsRef } from "@engenty-wizards/shared/providers";
|
|
@@ -405,6 +405,7 @@ export const studio = new Hono()
|
|
|
405
405
|
.max(24)
|
|
406
406
|
.default([]),
|
|
407
407
|
part: z.enum(["info", "knowledge"]).default("info"),
|
|
408
|
+
plugin: z.string().regex(PLUGIN_ID).optional(),
|
|
408
409
|
})
|
|
409
410
|
.parse(await c.req.json());
|
|
410
411
|
if (!(await canSpend())) {
|
|
@@ -420,6 +421,7 @@ export const studio = new Hono()
|
|
|
420
421
|
message: body.message,
|
|
421
422
|
history: body.history,
|
|
422
423
|
part: body.part,
|
|
424
|
+
plugin: body.plugin,
|
|
423
425
|
signal: abort.signal,
|
|
424
426
|
onText: (delta) => {
|
|
425
427
|
void stream.writeSSE({ event: "text", data: JSON.stringify(delta) });
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { statSync } from "node:fs";
|
|
1
3
|
import { DEFINITION_VERSION, readable, wizardSchema, } from "@engenty-wizards/shared/definition";
|
|
2
|
-
import { MARKETPLACE_LANGS, } from "@engenty-wizards/shared/marketplace";
|
|
4
|
+
import { capabilitiesOf, costTierOf, effortOf, formatsOf, MARKETPLACE_LANGS, } from "@engenty-wizards/shared/marketplace";
|
|
3
5
|
import { wizardOutline } from "@engenty-wizards/shared/marketplace-entry";
|
|
4
6
|
import { searchEntries } from "@engenty-wizards/shared/marketplace-search";
|
|
5
7
|
import { and, eq, inArray, notInArray } from "drizzle-orm";
|
|
@@ -9,14 +11,19 @@ import { missingCapabilities, optionalCapabilities, stepsWithoutModel, } from ".
|
|
|
9
11
|
import { env } from "../env.js";
|
|
10
12
|
import { managed } from "../manage.js";
|
|
11
13
|
import { classCatalog, imageCostUsd, tokenCostUsd, videoCostUsd, WEB_SEARCH_COST_USD, } from "../models.js";
|
|
14
|
+
import { pluginStartersOf } from "../plugins/registry.js";
|
|
12
15
|
import { readSetting, writeSetting } from "../settings.js";
|
|
13
|
-
import { LOCAL_TENANT } from "../tenants/tenant.js";
|
|
16
|
+
import { currentTenant, LOCAL_TENANT } from "../tenants/tenant.js";
|
|
14
17
|
import { ServiceError } from "./errors.js";
|
|
15
18
|
/**
|
|
16
19
|
* The marketplace is an app of its own; this runtime is one of its clients
|
|
17
20
|
* (docs/marketplace-contract.md). It searches the marketplace live and starts wizards from its
|
|
18
21
|
* entries. Its starters and what people starred are kept in the control database, so the list
|
|
19
22
|
* and starting a wizard work without a connection too.
|
|
23
|
+
*
|
|
24
|
+
* Beside them stand the starters the tenant's plugins bring (docs/content/dev/plugins/starters.md),
|
|
25
|
+
* listed first and never fetched. They are known as `<plugin id>.<starter id>`; a marketplace
|
|
26
|
+
* entry's id has no dot, so the two never meet.
|
|
20
27
|
*/
|
|
21
28
|
export const asLang = (raw) => MARKETPLACE_LANGS.includes(raw ?? "") ? raw : "de";
|
|
22
29
|
/** A search waits this long for the marketplace; then it searches what is kept here. */
|
|
@@ -84,6 +91,69 @@ function keptItem(e, lang) {
|
|
|
84
91
|
const text = textIn(e, lang);
|
|
85
92
|
return text ? { ...keptSummary(e, lang), definition: text.definition, files: e.files } : null;
|
|
86
93
|
}
|
|
94
|
+
// --- the plugins' starters ------------------------------------------------------------------
|
|
95
|
+
/** One of the starters the tenant's plugins bring, by its id; undefined for any other id. */
|
|
96
|
+
async function pluginStarter(id) {
|
|
97
|
+
if (!id.includes(".")) {
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
return (await pluginStartersOf(currentTenant())).find((s) => s.id === id);
|
|
101
|
+
}
|
|
102
|
+
/** A plugin's starter as the marketplace lists an entry, read from its definition. */
|
|
103
|
+
function pluginSummary({ id, plugin, starter }) {
|
|
104
|
+
const definition = starter.definition;
|
|
105
|
+
const { name, version } = plugin.source.manifest;
|
|
106
|
+
const capabilities = capabilitiesOf(definition);
|
|
107
|
+
const changed = statSync(plugin.source.entry ?? plugin.source.root, { throwIfNoEntry: false });
|
|
108
|
+
return {
|
|
109
|
+
id,
|
|
110
|
+
revision: 0,
|
|
111
|
+
hash: createHash("sha256")
|
|
112
|
+
.update(`${version}\n${JSON.stringify(definition)}`)
|
|
113
|
+
.digest("hex")
|
|
114
|
+
.slice(0, 16),
|
|
115
|
+
updatedAt: (changed?.mtime ?? new Date()).toISOString(),
|
|
116
|
+
starter: true,
|
|
117
|
+
language: starter.language ?? "de",
|
|
118
|
+
title: starter.title,
|
|
119
|
+
pitch: starter.description,
|
|
120
|
+
// Found by the plugin's name as well: "Termine" finds the appointment plugin's starters.
|
|
121
|
+
terms: [name],
|
|
122
|
+
avatar: definition.avatar,
|
|
123
|
+
formats: formatsOf(definition),
|
|
124
|
+
industries: ["any"],
|
|
125
|
+
useCases: [],
|
|
126
|
+
capabilities,
|
|
127
|
+
effort: effortOf(definition),
|
|
128
|
+
costTier: costTierOf(capabilities),
|
|
129
|
+
steps: definition.steps.length,
|
|
130
|
+
version: definition.version,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
/** A plugin's starter as this app shows it: never starred, and marked with its plugin. */
|
|
134
|
+
function pluginEntry(found, unavailable) {
|
|
135
|
+
const { id, name } = found.plugin.source.manifest;
|
|
136
|
+
return {
|
|
137
|
+
...asEntry(pluginSummary(found), new Set(), unavailable, optionalOf(found.starter.definition)),
|
|
138
|
+
plugin: { id, name },
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
/** Two searches' counts per option, added up. */
|
|
142
|
+
function addFacets(a, b) {
|
|
143
|
+
const add = (x = {}, y = {}) => {
|
|
144
|
+
const out = { ...x };
|
|
145
|
+
for (const [key, n] of Object.entries(y)) {
|
|
146
|
+
out[key] = (out[key] ?? 0) + n;
|
|
147
|
+
}
|
|
148
|
+
return out;
|
|
149
|
+
};
|
|
150
|
+
return {
|
|
151
|
+
useCase: add(a.useCase, b.useCase),
|
|
152
|
+
industry: add(a.industry, b.industry),
|
|
153
|
+
format: add(a.format, b.format),
|
|
154
|
+
capability: add(a.capability, b.capability),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
87
157
|
// --- searching -----------------------------------------------------------------------------
|
|
88
158
|
/** A summary as this app shows it: whether it can read and run it, and whether it is starred. */
|
|
89
159
|
function asEntry(summary, starred, unavailable, optional = new Set()) {
|
|
@@ -120,8 +190,43 @@ async function keptOptional(ids, lang, unavailable) {
|
|
|
120
190
|
}
|
|
121
191
|
return out;
|
|
122
192
|
}
|
|
123
|
-
/**
|
|
193
|
+
/**
|
|
194
|
+
* Searches the tenant's plugins' starters and the marketplace, in that order: a page holds the
|
|
195
|
+
* plugins' starters the words and filters find, then the marketplace's entries.
|
|
196
|
+
*/
|
|
124
197
|
export async function searchMarketplace(search) {
|
|
198
|
+
const { q = "", starred, limit = 60, offset = 0, lang: _lang, ...filters } = search;
|
|
199
|
+
const own = starred ? [] : await pluginStartersOf(currentTenant());
|
|
200
|
+
if (!own.length) {
|
|
201
|
+
return searchListed(search);
|
|
202
|
+
}
|
|
203
|
+
const byId = new Map(own.map((s) => [s.id, s]));
|
|
204
|
+
const found = searchEntries(own.map(pluginSummary), q, filters, { limit: own.length });
|
|
205
|
+
const shown = found.entries.slice(offset, offset + limit);
|
|
206
|
+
// The marketplace fills what is left of the page, and counts from where its entries begin.
|
|
207
|
+
const room = limit - shown.length;
|
|
208
|
+
const listed = await searchListed({
|
|
209
|
+
...search,
|
|
210
|
+
limit: Math.max(1, room),
|
|
211
|
+
offset: Math.max(0, offset - found.total),
|
|
212
|
+
});
|
|
213
|
+
const unavailable = new Set(listed.unavailable);
|
|
214
|
+
return {
|
|
215
|
+
...listed,
|
|
216
|
+
entries: [
|
|
217
|
+
...shown.flatMap((e) => {
|
|
218
|
+
const entry = byId.get(e.id);
|
|
219
|
+
return entry ? [pluginEntry(entry, unavailable)] : [];
|
|
220
|
+
}),
|
|
221
|
+
...listed.entries.slice(0, room),
|
|
222
|
+
],
|
|
223
|
+
total: found.total + listed.total,
|
|
224
|
+
all: own.length + listed.all,
|
|
225
|
+
facets: addFacets(listed.facets, found.facets),
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
/** Searches the marketplace; without an answer, what is kept here, scored the same way. */
|
|
229
|
+
async function searchListed(search) {
|
|
125
230
|
const { q = "", lang, starred: onlyStarred, limit = 60, offset = 0, ...filters } = search;
|
|
126
231
|
const stars = await starredIds();
|
|
127
232
|
const starred = new Set(stars);
|
|
@@ -217,6 +322,15 @@ async function priced(definition) {
|
|
|
217
322
|
}
|
|
218
323
|
/** An entry in a language with its wizard read against this app; null where there is none. */
|
|
219
324
|
export async function marketplaceDetail(id, lang) {
|
|
325
|
+
const own = await pluginStarter(id);
|
|
326
|
+
if (own) {
|
|
327
|
+
const definition = own.starter.definition;
|
|
328
|
+
return {
|
|
329
|
+
...pluginEntry(own, await missingCapabilities()),
|
|
330
|
+
credits: await priced(definition),
|
|
331
|
+
...wizardOutline(definition, Object.keys(own.starter.files ?? {}), lang),
|
|
332
|
+
};
|
|
333
|
+
}
|
|
220
334
|
const found = await fetchItem(id, lang);
|
|
221
335
|
if (!found) {
|
|
222
336
|
return null;
|
|
@@ -242,6 +356,23 @@ export async function marketplaceDetail(id, lang) {
|
|
|
242
356
|
}
|
|
243
357
|
/** An entry's wizard and workspace in a language: what a wizard is made from. */
|
|
244
358
|
export async function marketplaceWizard(id, lang) {
|
|
359
|
+
const own = await pluginStarter(id);
|
|
360
|
+
if (own) {
|
|
361
|
+
// A plugin's starter is in its one language, whatever is asked for.
|
|
362
|
+
const { starter, plugin } = own;
|
|
363
|
+
return {
|
|
364
|
+
id,
|
|
365
|
+
revision: null,
|
|
366
|
+
plugin: plugin.source.id,
|
|
367
|
+
title: starter.title,
|
|
368
|
+
language: starter.language ?? "de",
|
|
369
|
+
definition: structuredClone(starter.definition),
|
|
370
|
+
files: Object.fromEntries(Object.entries(starter.files ?? {}).map(([path, data]) => [
|
|
371
|
+
path,
|
|
372
|
+
typeof data === "string" ? Buffer.from(data, "utf8") : Buffer.from(data),
|
|
373
|
+
])),
|
|
374
|
+
};
|
|
375
|
+
}
|
|
245
376
|
const found = await fetchItem(id, lang);
|
|
246
377
|
if (!found) {
|
|
247
378
|
return null;
|
|
@@ -1,32 +1,27 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { decide } from "../engine/decide.js";
|
|
2
2
|
/**
|
|
3
3
|
* The last step of a search of Wissen: does each candidate the index found help answer the
|
|
4
|
-
* question?
|
|
5
|
-
* on its own
|
|
6
|
-
*
|
|
4
|
+
* question? One yes/no question per candidate, all in one decision: a decision model (Jev)
|
|
5
|
+
* answers each on its own with P(yes); without one the classifier class's language model
|
|
6
|
+
* answers yes or no. What falls under MIN_RELEVANCE goes; the rest is ranked by it. As
|
|
7
|
+
* engenty-pro's knowledge base checks its hits.
|
|
7
8
|
*/
|
|
8
9
|
export const MIN_RELEVANCE = 0.5;
|
|
9
10
|
/** A search waits for a person: a classifier that cannot answer in this time is left out. */
|
|
10
11
|
const TIMEOUT_MS = 4000;
|
|
11
|
-
const ATTEMPTS = 2;
|
|
12
12
|
const TEXT_CHARS = 1200;
|
|
13
13
|
const clip = (value, max) => {
|
|
14
14
|
const s = value.replace(/\s+/g, " ").trim();
|
|
15
15
|
return s.length > max ? `${s.slice(0, max - 1)}…` : s;
|
|
16
16
|
};
|
|
17
17
|
/**
|
|
18
|
-
* P(relevant) for each candidate, in their order
|
|
19
|
-
*
|
|
20
|
-
* the order the index gave.
|
|
18
|
+
* P(relevant) for each candidate, in their order. Null for all of them when no way decided in
|
|
19
|
+
* time: the search then keeps the order the index gave.
|
|
21
20
|
*/
|
|
22
21
|
export async function judgeRelevance(query, candidates, signal) {
|
|
23
22
|
if (!candidates.length) {
|
|
24
23
|
return [];
|
|
25
24
|
}
|
|
26
|
-
const access = await systemOneAccess().catch(() => null);
|
|
27
|
-
if (!access) {
|
|
28
|
-
return null;
|
|
29
|
-
}
|
|
30
25
|
const questions = {};
|
|
31
26
|
const state = {
|
|
32
27
|
query: clip(query, 600),
|
|
@@ -49,42 +44,15 @@ export async function judgeRelevance(query, candidates, signal) {
|
|
|
49
44
|
};
|
|
50
45
|
}),
|
|
51
46
|
};
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
body,
|
|
63
|
-
signal: signal
|
|
64
|
-
? AbortSignal.any([signal, AbortSignal.timeout(TIMEOUT_MS)])
|
|
65
|
-
: AbortSignal.timeout(TIMEOUT_MS),
|
|
66
|
-
});
|
|
67
|
-
if (!res.ok) {
|
|
68
|
-
if ([429, 503, 529].includes(res.status) && attempt < ATTEMPTS) {
|
|
69
|
-
continue;
|
|
70
|
-
}
|
|
71
|
-
console.warn(`[relevance] the classifier answered ${res.status}`);
|
|
72
|
-
return null;
|
|
73
|
-
}
|
|
74
|
-
const answer = (await res.json());
|
|
75
|
-
return candidates.map((_, index) => {
|
|
76
|
-
const a = answer.answers?.[`c${index}`];
|
|
77
|
-
return a?.type === "noul" && typeof a.noul === "number" && Number.isFinite(a.noul)
|
|
78
|
-
? Math.round(a.noul * 1000) / 1000
|
|
79
|
-
: null;
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
catch (err) {
|
|
83
|
-
if (signal?.aborted || attempt >= ATTEMPTS) {
|
|
84
|
-
console.warn("[relevance] the classifier did not answer:", err.message);
|
|
85
|
-
return null;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
47
|
+
try {
|
|
48
|
+
const decision = await decide({ state, questions, signal, deadlineMs: TIMEOUT_MS });
|
|
49
|
+
return candidates.map((_, index) => {
|
|
50
|
+
const a = decision.answers[`c${index}`];
|
|
51
|
+
return a?.type === "noul" ? Math.round(a.noul * 1000) / 1000 : null;
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
console.warn("[relevance] no decision:", err.message);
|
|
56
|
+
return null;
|
|
88
57
|
}
|
|
89
|
-
return null;
|
|
90
58
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { formatsFor } from "@engenty-wizards/shared/definition";
|
|
1
|
+
import { formatsFor, optionsFromData, slotText, wizardLang, } from "@engenty-wizards/shared/definition";
|
|
2
2
|
import { and, count, desc, eq, gte, inArray, max, or, sql } from "drizzle-orm";
|
|
3
3
|
import { canSpend, MICROS_PER_CREDIT } from "../credits/credits.js";
|
|
4
4
|
import { db, schema } from "../db/client.js";
|
|
@@ -159,7 +159,31 @@ const CHAT_KINDS = new Set([
|
|
|
159
159
|
"color",
|
|
160
160
|
"items",
|
|
161
161
|
"location",
|
|
162
|
+
"slot",
|
|
162
163
|
]);
|
|
164
|
+
/** A field's choices in this run: what its `optionsFrom` finds in earlier steps, else the fixed ones. */
|
|
165
|
+
function choicesOf(run, field) {
|
|
166
|
+
const found = field.optionsFrom ? optionsFromData(field.optionsFrom, run.state.outputs) : [];
|
|
167
|
+
return found.length ? found : field.options;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* What a client needs besides the plain field: a slot's times as the person reads them, and the
|
|
171
|
+
* one value of them to send back.
|
|
172
|
+
*/
|
|
173
|
+
function slotHints(run, field, options) {
|
|
174
|
+
if (field.kind !== "slot") {
|
|
175
|
+
return {};
|
|
176
|
+
}
|
|
177
|
+
const lang = wizardLang(run.definition);
|
|
178
|
+
const value = run.state.values[field.id];
|
|
179
|
+
return {
|
|
180
|
+
times: (options ?? []).map((o) => ({ value: o, label: slotText(o, lang) })),
|
|
181
|
+
...(typeof value === "string" ? { valueText: slotText(value, lang) } : {}),
|
|
182
|
+
hint: options?.length
|
|
183
|
+
? "One appointment time: show the person the labels, answer with the value of the one they pick, exactly as given."
|
|
184
|
+
: "No times are free right now: tell the person.",
|
|
185
|
+
};
|
|
186
|
+
}
|
|
163
187
|
/** Where the person can go on in a browser: the run page of a live run, the studio for a test. */
|
|
164
188
|
/** The live run's page — the wizard itself, never the studio. A test run has none. */
|
|
165
189
|
async function browserUrl(run) {
|
|
@@ -197,20 +221,24 @@ function waitingFor(run, current) {
|
|
|
197
221
|
return {
|
|
198
222
|
page: current.id,
|
|
199
223
|
title: current.title,
|
|
200
|
-
fields: current.fields.map((f) =>
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
224
|
+
fields: current.fields.map((f) => {
|
|
225
|
+
const options = choicesOf(run, f);
|
|
226
|
+
return {
|
|
227
|
+
id: f.id,
|
|
228
|
+
label: f.label,
|
|
229
|
+
kind: f.kind,
|
|
230
|
+
required: Boolean(f.required),
|
|
231
|
+
options,
|
|
232
|
+
placeholder: f.placeholder,
|
|
233
|
+
help: f.help,
|
|
234
|
+
default: f.default,
|
|
235
|
+
value: run.state.values[f.id],
|
|
236
|
+
columns: f.columns,
|
|
237
|
+
// A file, a recording or a signature comes from the person's device: the run page.
|
|
238
|
+
inChat: CHAT_KINDS.has(f.kind),
|
|
239
|
+
...slotHints(run, f, options),
|
|
240
|
+
};
|
|
241
|
+
}),
|
|
214
242
|
};
|
|
215
243
|
}
|
|
216
244
|
if (current.type === "review") {
|