wizards 0.2.15 → 0.2.17
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/credits/estimate.js +14 -8
- package/apps/runtime/dist/db/migrations/tenant/0008_run_event_note.sql +1 -0
- package/apps/runtime/dist/db/migrations/tenant/meta/0008_snapshot.json +2019 -0
- package/apps/runtime/dist/db/migrations/tenant/meta/_journal.json +7 -0
- package/apps/runtime/dist/db/schema.js +2 -0
- package/apps/runtime/dist/engine/events.js +14 -2
- package/apps/runtime/dist/engine/prepare.js +5 -2
- package/apps/runtime/dist/engine/requirements.js +248 -12
- package/apps/runtime/dist/engine/runner.js +28 -5
- package/apps/runtime/dist/engine/steps.js +18 -10
- package/apps/runtime/dist/engine/template.js +13 -10
- package/apps/runtime/dist/routes/studio.js +2 -2
- package/apps/runtime/dist/services/marketplace.js +34 -7
- package/apps/runtime/dist/tools/browser.js +12 -6
- package/apps/runtime/dist/tools/index.js +12 -6
- package/apps/runtime/dist/tools/mail.js +5 -2
- package/apps/runtime/dist/tools/pages.js +1 -1
- package/apps/runtime/dist/tools/project.js +5 -2
- package/apps/runtime/dist/tools/store.js +4 -4
- package/apps/runtime/dist/widgets/step.js +10 -4
- package/apps/web/dist/assets/{Account-B_FCuidz.js → Account-LVXhagaS.js} +1 -1
- package/apps/web/dist/assets/{ChangelogDialog-BKvNJIrA.js → ChangelogDialog-DM0KAoy6.js} +1 -1
- package/apps/web/dist/assets/ChatPanel-CxOQmtv-.js +1 -0
- package/apps/web/dist/assets/CreditsDialog-DkW3YT44.js +1 -0
- package/apps/web/dist/assets/EditorPage-hbisE1Wf.js +2 -0
- package/apps/web/dist/assets/{FlowPreview-De3ZyIer.js → FlowPreview-v7or29ku.js} +1 -1
- package/apps/web/dist/assets/{Harness-3kuJaJoj.js → Harness-RZF-bBmb.js} +2 -2
- package/apps/web/dist/assets/{HomePage-DWDT64vg.js → HomePage-mxG-a_KU.js} +1 -1
- package/apps/web/dist/assets/{ImportWizard-CTGmBDI8.js → ImportWizard-CiPyUt3E.js} +1 -1
- package/apps/web/dist/assets/LocalRuntime-Dqp9wkN3.js +1 -0
- package/apps/web/dist/assets/NewWizardPage-DHwRsi0_.js +2 -0
- package/apps/web/dist/assets/ProjectPage-Bb7jN_6n.js +4 -0
- package/apps/web/dist/assets/PublicRunner-TGkOwbAP.js +1 -0
- package/apps/web/dist/assets/RunnerView-DzHGshZj.js +70 -0
- package/apps/web/dist/assets/SettingsPage-CG3Xtfej.js +1 -0
- package/apps/web/dist/assets/{SetupPage-DHQYO7zB.js → SetupPage-O1GfPlqX.js} +1 -1
- package/apps/web/dist/assets/SharePage-CCNUhtry.js +1 -0
- package/apps/web/dist/assets/{api-RJbFM_oc.js → api-DB22_B7G.js} +2 -2
- package/apps/web/dist/assets/changelog-BkIJdZ7M.js +1 -0
- package/apps/web/dist/assets/{esm-CgnVZhIQ.js → esm-3TT9i_rv.js} +1 -1
- package/apps/web/dist/assets/{index-9maLuK9R.css → index-BPpKz65x.css} +1 -1
- package/apps/web/dist/assets/index-CLe9Wrpu.js +10 -0
- package/apps/web/dist/assets/{meta-CchZXdWH.js → meta-CEsBCPfA.js} +1 -1
- package/apps/web/dist/assets/{settings-menu-CrRWSrNL.js → settings-menu-pOcjznGJ.js} +1 -1
- package/apps/web/dist/assets/ui-I7QYMX0F.js +1 -0
- package/apps/web/dist/index.html +4 -4
- package/apps/web/dist/mcp/flow.html +54 -54
- package/node_modules/@engenty-wizards/shared/dist/definition.d.ts +11 -0
- package/node_modules/@engenty-wizards/shared/dist/definition.js +27 -2
- package/node_modules/@engenty-wizards/shared/dist/marketplace.d.ts +2 -0
- package/node_modules/@engenty-wizards/shared/dist/run.d.ts +150 -0
- package/node_modules/@engenty-wizards/shared/dist/run.js +72 -1
- package/package.json +1 -1
- package/apps/web/dist/assets/ChatPanel-CPgh0skF.js +0 -1
- package/apps/web/dist/assets/CreditsDialog-CbFq_7-m.js +0 -1
- package/apps/web/dist/assets/EditorPage-8liZwYCK.js +0 -2
- package/apps/web/dist/assets/LocalRuntime-CVdU8RtD.js +0 -1
- package/apps/web/dist/assets/NewWizardPage-CqFlo0b-.js +0 -2
- package/apps/web/dist/assets/ProjectPage-CDw2Jcxx.js +0 -4
- package/apps/web/dist/assets/PublicRunner-BYnX2OmB.js +0 -1
- package/apps/web/dist/assets/RunnerView-Civq59Tk.js +0 -70
- package/apps/web/dist/assets/SettingsPage-Bj0TxbiM.js +0 -1
- package/apps/web/dist/assets/SharePage-CY6bwfTI.js +0 -1
- package/apps/web/dist/assets/changelog-BWtvjeKs.js +0 -1
- package/apps/web/dist/assets/index-LmRYEd-y.js +0 -10
- package/apps/web/dist/assets/ui-HV_XbHjw.js +0 -1
|
@@ -129,6 +129,8 @@ export const runEvent = sqliteTable("run_event", {
|
|
|
129
129
|
stepId: text("step_id"),
|
|
130
130
|
type: text("type", { enum: ["step_started", "step_done", "tool", "info", "error"] }).notNull(),
|
|
131
131
|
message: text("message").notNull(),
|
|
132
|
+
/** The message as a note of `RUN_NOTES`, for a runner in another language. */
|
|
133
|
+
note: text("note", { mode: "json" }).$type(),
|
|
132
134
|
/** A picture of the run the event is about; shown while the step works. */
|
|
133
135
|
assetId: text("asset_id"),
|
|
134
136
|
createdAt: createdAt(),
|
|
@@ -1,13 +1,23 @@
|
|
|
1
1
|
import { AsyncResource } from "node:async_hooks";
|
|
2
2
|
import { EventEmitter } from "node:events";
|
|
3
|
+
import { noteText } from "@engenty-wizards/shared/run";
|
|
3
4
|
import { and, desc, eq, gt } from "drizzle-orm";
|
|
4
5
|
import { db, schema } from "../db/client.js";
|
|
5
6
|
const bus = new EventEmitter();
|
|
6
7
|
bus.setMaxListeners(0);
|
|
7
|
-
export async function emitEvent(runId, stepId, type,
|
|
8
|
+
export async function emitEvent(runId, stepId, type, said, asset) {
|
|
9
|
+
const note = typeof said === "string" ? null : said;
|
|
10
|
+
const message = note ? noteText(note, "de") : said;
|
|
8
11
|
const [row] = await db
|
|
9
12
|
.insert(schema.runEvent)
|
|
10
|
-
.values({
|
|
13
|
+
.values({
|
|
14
|
+
runId,
|
|
15
|
+
stepId,
|
|
16
|
+
type,
|
|
17
|
+
message: message.slice(0, 2000),
|
|
18
|
+
note,
|
|
19
|
+
assetId: asset ?? null,
|
|
20
|
+
})
|
|
11
21
|
.returning();
|
|
12
22
|
const event = {
|
|
13
23
|
id: row.id,
|
|
@@ -15,6 +25,7 @@ export async function emitEvent(runId, stepId, type, message, asset) {
|
|
|
15
25
|
stepId: row.stepId,
|
|
16
26
|
type: row.type,
|
|
17
27
|
message: row.message,
|
|
28
|
+
note: row.note ?? null,
|
|
18
29
|
asset: row.assetId,
|
|
19
30
|
};
|
|
20
31
|
bus.emit(runId, { runId, event });
|
|
@@ -41,6 +52,7 @@ export async function recentEvents(runId, afterId = 0, limit = 40) {
|
|
|
41
52
|
stepId: r.stepId,
|
|
42
53
|
type: r.type,
|
|
43
54
|
message: r.message,
|
|
55
|
+
note: r.note ?? null,
|
|
44
56
|
asset: r.assetId,
|
|
45
57
|
}));
|
|
46
58
|
}
|
|
@@ -28,7 +28,7 @@ export async function prepareInputs(ctx) {
|
|
|
28
28
|
delete values[field.id];
|
|
29
29
|
continue;
|
|
30
30
|
}
|
|
31
|
-
await ctx.emit("info",
|
|
31
|
+
await ctx.emit("info", { code: "listen", params: { label: field.label } });
|
|
32
32
|
try {
|
|
33
33
|
const transcript = await transcribeAudio({
|
|
34
34
|
bytes: new Uint8Array(found.data),
|
|
@@ -45,7 +45,10 @@ export async function prepareInputs(ctx) {
|
|
|
45
45
|
}
|
|
46
46
|
// The step goes on without the words; the next one tries again.
|
|
47
47
|
console.error(`[run ${ctx.runId}] transcribe ${field.id}`, err);
|
|
48
|
-
await ctx.emit("info",
|
|
48
|
+
await ctx.emit("info", {
|
|
49
|
+
code: "listenFailed",
|
|
50
|
+
params: { label: field.label },
|
|
51
|
+
});
|
|
49
52
|
}
|
|
50
53
|
}
|
|
51
54
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isDecisionStep, } from "@engenty-wizards/shared/definition";
|
|
1
|
+
import { isDecisionStep, MODEL_CLASSES, ruleMatches, } from "@engenty-wizards/shared/definition";
|
|
2
2
|
import { classProblem } from "../models.js";
|
|
3
3
|
/**
|
|
4
4
|
* What a wizard needs of the models before a run starts: each class its steps call, and whether
|
|
@@ -32,28 +32,58 @@ export function stepClasses(step) {
|
|
|
32
32
|
default:
|
|
33
33
|
return [step.model ?? "high"];
|
|
34
34
|
}
|
|
35
|
+
case "page":
|
|
36
|
+
// A voice note the page asks for is listened to before the next step reads it.
|
|
37
|
+
return step.fields.some((f) => f.kind === "audio" && f.required) ? ["audio"] : [];
|
|
35
38
|
default:
|
|
36
|
-
//
|
|
39
|
+
// Reviews (they redo a generate step counted already), widgets, results.
|
|
37
40
|
return [];
|
|
38
41
|
}
|
|
39
42
|
}
|
|
43
|
+
/** The classes a step can do without: a voice note the person may leave out. */
|
|
44
|
+
export function optionalStepClasses(step) {
|
|
45
|
+
return step.type === "page" &&
|
|
46
|
+
step.fields.some((f) => f.kind === "audio" && !f.required) &&
|
|
47
|
+
!stepClasses(step).includes("audio")
|
|
48
|
+
? ["audio"]
|
|
49
|
+
: [];
|
|
50
|
+
}
|
|
40
51
|
/** The steps every run passes through: without one of them the end is not reachable. */
|
|
41
52
|
export function unavoidableSteps(def) {
|
|
53
|
+
return def.steps.length ? unavoidableFrom(def, def.steps[0].id, {}) : new Set();
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The steps every path from `from` to the end passes through. A rule on an answer in `known`
|
|
57
|
+
* goes one way; a rule on anything else may go either way.
|
|
58
|
+
*/
|
|
59
|
+
export function unavoidableFrom(def, from, known) {
|
|
42
60
|
const index = new Map(def.steps.map((s, i) => [s.id, i]));
|
|
61
|
+
const start = index.get(from) ?? 0;
|
|
43
62
|
const successors = (i) => {
|
|
44
|
-
const out =
|
|
45
|
-
|
|
46
|
-
.
|
|
63
|
+
const out = [];
|
|
64
|
+
for (const rule of def.steps[i].next ?? []) {
|
|
65
|
+
const target = rule.goto === "end" ? def.steps.length : (index.get(rule.goto) ?? -1);
|
|
66
|
+
if (!(rule.when.field in known)) {
|
|
67
|
+
if (target >= 0) {
|
|
68
|
+
out.push(target);
|
|
69
|
+
}
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (ruleMatches(rule, known)) {
|
|
73
|
+
// The first rule that matches decides; nothing after it is tried.
|
|
74
|
+
return target >= 0 ? [...out, target] : out;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
47
77
|
// No rule matching goes on to the next step (or ends after the last).
|
|
48
78
|
out.push(i + 1);
|
|
49
79
|
return out;
|
|
50
80
|
};
|
|
51
81
|
const reachesEnd = (without) => {
|
|
52
|
-
if (without ===
|
|
82
|
+
if (without === start) {
|
|
53
83
|
return false;
|
|
54
84
|
}
|
|
55
|
-
const seen = new Set([
|
|
56
|
-
const queue = [
|
|
85
|
+
const seen = new Set([start]);
|
|
86
|
+
const queue = [start];
|
|
57
87
|
while (queue.length) {
|
|
58
88
|
const i = queue.shift();
|
|
59
89
|
if (i >= def.steps.length) {
|
|
@@ -70,24 +100,135 @@ export function unavoidableSteps(def) {
|
|
|
70
100
|
};
|
|
71
101
|
return new Set(def.steps.filter((_, i) => !reachesEnd(i)).map((s) => s.id));
|
|
72
102
|
}
|
|
103
|
+
/** Whether a rule matches given what is known: true, false, or null = it may go either way. */
|
|
104
|
+
function ruleHolds(rule, facts) {
|
|
105
|
+
const k = facts[rule.when.field];
|
|
106
|
+
const value = rule.when.value;
|
|
107
|
+
switch (rule.when.op) {
|
|
108
|
+
case "empty":
|
|
109
|
+
return k?.filled === undefined ? (k?.is !== undefined ? false : null) : !k.filled;
|
|
110
|
+
case "notEmpty":
|
|
111
|
+
return k?.filled === undefined ? (k?.is !== undefined ? true : null) : k.filled;
|
|
112
|
+
case "equals":
|
|
113
|
+
return k?.is !== undefined
|
|
114
|
+
? k.is === String(value)
|
|
115
|
+
: k?.not.includes(String(value))
|
|
116
|
+
? false
|
|
117
|
+
: null;
|
|
118
|
+
case "notEquals":
|
|
119
|
+
return k?.is !== undefined
|
|
120
|
+
? k.is !== String(value)
|
|
121
|
+
: k?.not.includes(String(value))
|
|
122
|
+
? true
|
|
123
|
+
: null;
|
|
124
|
+
case "in": {
|
|
125
|
+
const list = Array.isArray(value) ? value.map(String) : [];
|
|
126
|
+
if (k?.is !== undefined) {
|
|
127
|
+
return list.includes(k.is);
|
|
128
|
+
}
|
|
129
|
+
return list.every((v) => k?.not.includes(v)) ? false : null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/** What is known once a rule did (`held`) or did not match. */
|
|
134
|
+
function learn(rule, held, facts) {
|
|
135
|
+
const field = rule.when.field;
|
|
136
|
+
const k = { ...(facts[field] ?? { not: [] }) };
|
|
137
|
+
k.not = [...k.not];
|
|
138
|
+
const value = String(rule.when.value);
|
|
139
|
+
const op = rule.when.op;
|
|
140
|
+
if ((op === "equals" && held) || (op === "notEquals" && !held)) {
|
|
141
|
+
k.is = value;
|
|
142
|
+
k.filled = true;
|
|
143
|
+
}
|
|
144
|
+
else if ((op === "equals" && !held) || (op === "notEquals" && held)) {
|
|
145
|
+
k.not.push(value);
|
|
146
|
+
}
|
|
147
|
+
else if (op === "in" && !held && Array.isArray(rule.when.value)) {
|
|
148
|
+
k.not.push(...rule.when.value.map(String));
|
|
149
|
+
}
|
|
150
|
+
else if (op === "empty" || op === "notEmpty") {
|
|
151
|
+
k.filled = (op === "notEmpty") === held;
|
|
152
|
+
}
|
|
153
|
+
return { ...facts, [field]: k };
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* The most a run can add up to along one way through the wizard. A branch remembers what it
|
|
157
|
+
* implies of the answers, so ways that contradict themselves (the 720p clips and the 480p
|
|
158
|
+
* ones) are not added up. Steps in `closed` are not passed (no model for them here); where that
|
|
159
|
+
* leaves no way to the end, they count again. A branch back to an earlier step is not followed
|
|
160
|
+
* twice.
|
|
161
|
+
*/
|
|
162
|
+
export function mostAlongOnePath(def, cost, closed = new Set()) {
|
|
163
|
+
const index = new Map(def.steps.map((s, i) => [s.id, i]));
|
|
164
|
+
const end = def.steps.length;
|
|
165
|
+
const walk = (avoid) => {
|
|
166
|
+
const best = (i, facts, seen) => {
|
|
167
|
+
if (i >= end) {
|
|
168
|
+
return 0;
|
|
169
|
+
}
|
|
170
|
+
const step = def.steps[i];
|
|
171
|
+
if (i < 0 || avoid.has(step.id) || seen.has(i)) {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
const passed = new Set(seen).add(i);
|
|
175
|
+
let most = null;
|
|
176
|
+
const consider = (j, known) => {
|
|
177
|
+
const rest = best(j, known, passed);
|
|
178
|
+
if (rest !== null && (most === null || rest > most)) {
|
|
179
|
+
most = rest;
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
// The rules are tried in order; the first that matches decides.
|
|
183
|
+
let known = facts;
|
|
184
|
+
let decided = false;
|
|
185
|
+
for (const rule of step.next ?? []) {
|
|
186
|
+
const holds = ruleHolds(rule, known);
|
|
187
|
+
if (holds === false) {
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
consider(rule.goto === "end" ? end : (index.get(rule.goto) ?? -1), learn(rule, true, known));
|
|
191
|
+
if (holds === true) {
|
|
192
|
+
decided = true;
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
known = learn(rule, false, known);
|
|
196
|
+
}
|
|
197
|
+
if (!decided) {
|
|
198
|
+
consider(i + 1, known);
|
|
199
|
+
}
|
|
200
|
+
return most === null ? null : most + cost(step);
|
|
201
|
+
};
|
|
202
|
+
return best(0, {}, new Set());
|
|
203
|
+
};
|
|
204
|
+
return walk(closed) ?? walk(new Set()) ?? def.steps.reduce((sum, s) => sum + cost(s), 0);
|
|
205
|
+
}
|
|
206
|
+
/** The steps that call a class this runtime cannot serve. */
|
|
207
|
+
export async function stepsWithoutModel(def) {
|
|
208
|
+
const missing = await missingClasses();
|
|
209
|
+
return new Set(def.steps.filter((s) => stepClasses(s).some((cls) => missing.has(cls))).map((s) => s.id));
|
|
210
|
+
}
|
|
73
211
|
/** The classes this wizard needs that this runtime cannot serve; empty = every step can run. */
|
|
74
212
|
export async function missingModels(def) {
|
|
75
213
|
const unavoidable = unavoidableSteps(def);
|
|
76
214
|
const needs = new Map();
|
|
77
215
|
for (const step of def.steps) {
|
|
78
216
|
for (const cls of new Set(stepClasses(step))) {
|
|
79
|
-
needs.set(cls, [...(needs.get(cls) ?? []), step]);
|
|
217
|
+
needs.set(cls, [...(needs.get(cls) ?? []), { step, required: true }]);
|
|
218
|
+
}
|
|
219
|
+
for (const cls of optionalStepClasses(step)) {
|
|
220
|
+
needs.set(cls, [...(needs.get(cls) ?? []), { step, required: false }]);
|
|
80
221
|
}
|
|
81
222
|
}
|
|
82
223
|
const missing = [];
|
|
83
|
-
for (const [cls,
|
|
224
|
+
for (const [cls, uses] of needs) {
|
|
84
225
|
const problem = await classProblem(cls);
|
|
85
226
|
if (problem) {
|
|
86
227
|
missing.push({
|
|
87
228
|
cls,
|
|
88
229
|
problem,
|
|
89
|
-
steps:
|
|
90
|
-
blocking:
|
|
230
|
+
steps: uses.map(({ step }) => ({ id: step.id, title: step.title })),
|
|
231
|
+
blocking: uses.some(({ step, required }) => required && unavoidable.has(step.id)),
|
|
91
232
|
});
|
|
92
233
|
}
|
|
93
234
|
}
|
|
@@ -122,3 +263,98 @@ export async function missingCapabilities() {
|
|
|
122
263
|
}
|
|
123
264
|
return missing;
|
|
124
265
|
}
|
|
266
|
+
/** The classes this runtime cannot serve. */
|
|
267
|
+
export async function missingClasses() {
|
|
268
|
+
const missing = new Set();
|
|
269
|
+
for (const cls of MODEL_CLASSES) {
|
|
270
|
+
if (await classProblem(cls)) {
|
|
271
|
+
missing.add(cls);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return missing;
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* The capabilities of a wizard that only some of its paths need: without them it still starts,
|
|
278
|
+
* on the other paths (animated stills instead of video clips, no voice-over).
|
|
279
|
+
*/
|
|
280
|
+
export function optionalCapabilities(def) {
|
|
281
|
+
const unavoidable = unavoidableSteps(def);
|
|
282
|
+
const required = new Set();
|
|
283
|
+
const any = new Set();
|
|
284
|
+
for (const step of def.steps) {
|
|
285
|
+
for (const cls of stepClasses(step)) {
|
|
286
|
+
any.add(cls);
|
|
287
|
+
if (unavoidable.has(step.id)) {
|
|
288
|
+
required.add(cls);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
for (const cls of optionalStepClasses(step)) {
|
|
292
|
+
any.add(cls);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
const optional = new Set();
|
|
296
|
+
for (const [capability, cls] of Object.entries(CAPABILITY_CLASS)) {
|
|
297
|
+
if (any.has(cls) && !required.has(cls)) {
|
|
298
|
+
optional.add(capability);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return optional;
|
|
302
|
+
}
|
|
303
|
+
function choicesOf(step) {
|
|
304
|
+
return step.fields.flatMap((f) => {
|
|
305
|
+
if (f.kind === "select" && f.options?.length) {
|
|
306
|
+
return [{ field: f.id, values: f.options }];
|
|
307
|
+
}
|
|
308
|
+
if (f.kind === "toggle") {
|
|
309
|
+
return [{ field: f.id, values: [true, false] }];
|
|
310
|
+
}
|
|
311
|
+
return [];
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* The answers on a page that lead to a step this runtime has no model for: with them the run
|
|
316
|
+
* would stop there, after what came before was paid for. The answers given already count; the
|
|
317
|
+
* page's other fields may still go either way. A choice whose every value leads there is left
|
|
318
|
+
* open: the run cannot go around it, and the start already warned. A voice note nothing here
|
|
319
|
+
* can listen to is closed as a whole (no values).
|
|
320
|
+
*/
|
|
321
|
+
export async function closedChoices(def, step, values) {
|
|
322
|
+
const missing = await missingClasses();
|
|
323
|
+
if (!missing.size) {
|
|
324
|
+
return {};
|
|
325
|
+
}
|
|
326
|
+
const byId = new Map(def.steps.map((s) => [s.id, s]));
|
|
327
|
+
const known = Object.fromEntries(Object.entries(values).filter(([id]) => !step.fields.some((f) => f.id === id)));
|
|
328
|
+
const out = {};
|
|
329
|
+
// A voice note nothing here can listen to: the field is closed as a whole.
|
|
330
|
+
if (missing.has("audio")) {
|
|
331
|
+
for (const field of step.fields) {
|
|
332
|
+
if (field.kind === "audio") {
|
|
333
|
+
out[field.id] = { values: [], classes: ["audio"] };
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
for (const choice of choicesOf(step)) {
|
|
338
|
+
const closed = { values: [], classes: [] };
|
|
339
|
+
for (const value of choice.values) {
|
|
340
|
+
const ahead = unavoidableFrom(def, step.id, { ...known, [choice.field]: value });
|
|
341
|
+
const lacking = [...ahead].flatMap((id) => {
|
|
342
|
+
const s = byId.get(id);
|
|
343
|
+
return s ? stepClasses(s).filter((cls) => missing.has(cls)) : [];
|
|
344
|
+
});
|
|
345
|
+
if (lacking.length) {
|
|
346
|
+
closed.values.push(value);
|
|
347
|
+
closed.classes = [...new Set([...closed.classes, ...lacking])];
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
if (closed.values.length && closed.values.length < choice.values.length) {
|
|
351
|
+
out[choice.field] = closed;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return out;
|
|
355
|
+
}
|
|
356
|
+
/** The answer a closed choice gets instead: its first value that stays open. */
|
|
357
|
+
export function openValue(step, field, closed) {
|
|
358
|
+
const choice = choicesOf(step).find((c) => c.field === field);
|
|
359
|
+
return choice?.values.find((v) => !closed.values.some((c) => String(c) === String(v)));
|
|
360
|
+
}
|
|
@@ -20,7 +20,7 @@ import { askPerson, clearStaleAsk, unattended } from "./asks.js";
|
|
|
20
20
|
import { emitEvent, recentEvents, signalChanged } from "./events.js";
|
|
21
21
|
import { readPageInput } from "./input.js";
|
|
22
22
|
import { pushRun } from "./push.js";
|
|
23
|
-
import { blockingMessage, missingModels } from "./requirements.js";
|
|
23
|
+
import { blockingMessage, closedChoices, missingModels, openValue, stepsWithoutModel, } from "./requirements.js";
|
|
24
24
|
import { releaseResources, resourcesFor } from "./resources.js";
|
|
25
25
|
import { runAutomaticStep } from "./steps.js";
|
|
26
26
|
import { resolveRef } from "./template.js";
|
|
@@ -161,7 +161,7 @@ export async function createRun(input) {
|
|
|
161
161
|
throw new ModelUnavailableError(blocked);
|
|
162
162
|
}
|
|
163
163
|
// The run's expected cost is held before it starts; what it really costs is booked per call.
|
|
164
|
-
const estimate = await estimateRun(input.wizardId, input.version, input.definition);
|
|
164
|
+
const estimate = await estimateRun(input.wizardId, input.version, input.definition, await stepsWithoutModel(input.definition));
|
|
165
165
|
if (!(await reserveForRun(id, estimate.reserve))) {
|
|
166
166
|
throw new NoCreditsError();
|
|
167
167
|
}
|
|
@@ -358,6 +358,15 @@ export async function submitPage(runId, stepId, input) {
|
|
|
358
358
|
throw new RunConflict("Not a page");
|
|
359
359
|
}
|
|
360
360
|
const { values, errors } = readPageInput(step, input);
|
|
361
|
+
// An answer that leads to a step without a model here would stop the run there, later.
|
|
362
|
+
const closed = await closedChoices(run.definition, step, run.state.values);
|
|
363
|
+
for (const [id, choice] of Object.entries(closed)) {
|
|
364
|
+
const answer = values[id];
|
|
365
|
+
if (answer !== undefined &&
|
|
366
|
+
(choice.values.length === 0 || choice.values.some((v) => String(v) === String(answer)))) {
|
|
367
|
+
errors.push({ field: id, message: "Das ist hier gerade nicht verfügbar." });
|
|
368
|
+
}
|
|
369
|
+
}
|
|
361
370
|
if (errors.length) {
|
|
362
371
|
throw new RunInputError(errors);
|
|
363
372
|
}
|
|
@@ -483,8 +492,11 @@ export async function availableFormats(step, run, wanted) {
|
|
|
483
492
|
}
|
|
484
493
|
return formats;
|
|
485
494
|
}
|
|
486
|
-
/**
|
|
487
|
-
|
|
495
|
+
/**
|
|
496
|
+
* What the page's fields start with: an earlier step's result where they name one, and for a
|
|
497
|
+
* choice whose default leads to a step without a model here, the first value that does not.
|
|
498
|
+
*/
|
|
499
|
+
function prefillOf(step, run, closed) {
|
|
488
500
|
const scope = { def: run.definition, state: run.state, brand: {} };
|
|
489
501
|
const out = {};
|
|
490
502
|
for (const field of step.fields) {
|
|
@@ -496,11 +508,21 @@ function prefillOf(step, run) {
|
|
|
496
508
|
out[field.id] = value;
|
|
497
509
|
}
|
|
498
510
|
}
|
|
511
|
+
for (const [id, choice] of Object.entries(closed)) {
|
|
512
|
+
const field = step.fields.find((f) => f.id === id);
|
|
513
|
+
const start = out[id] ?? field?.default ?? (field?.kind === "toggle" ? false : undefined);
|
|
514
|
+
if (field && start !== undefined && choice.values.some((v) => String(v) === String(start))) {
|
|
515
|
+
out[id] = openValue(step, id, choice);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
499
518
|
return out;
|
|
500
519
|
}
|
|
501
520
|
export async function runView(run, brand) {
|
|
502
521
|
const def = run.definition;
|
|
503
522
|
const step = stepOf(def, run.cursor) ?? null;
|
|
523
|
+
const closed = step?.type === "page" && run.status === "waiting_input"
|
|
524
|
+
? await closedChoices(def, step, run.state.values)
|
|
525
|
+
: {};
|
|
504
526
|
const index = step ? def.steps.indexOf(step) : def.steps.length;
|
|
505
527
|
const refs = step?.type === "review"
|
|
506
528
|
? step.show
|
|
@@ -549,7 +571,8 @@ export async function runView(run, brand) {
|
|
|
549
571
|
},
|
|
550
572
|
step,
|
|
551
573
|
values: run.state.values,
|
|
552
|
-
prefill: step?.type === "page" ? prefillOf(step, run) : {},
|
|
574
|
+
prefill: step?.type === "page" ? prefillOf(step, run, closed) : {},
|
|
575
|
+
closed,
|
|
553
576
|
outputs: Object.fromEntries(shownIds.map((id) => [id, run.state.outputs[id]]).filter(([, o]) => o)),
|
|
554
577
|
shown,
|
|
555
578
|
progress: { done: run.status === "done" ? def.steps.length : index, total: def.steps.length },
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { allFields, dataRef, isDecisionStep, templateRefs, } from "@engenty-wizards/shared/definition";
|
|
1
|
+
import { allFields, dataRef, isDecisionStep, LOCALES, templateRefs, wizardLang, } from "@engenty-wizards/shared/definition";
|
|
2
2
|
import { Agent } from "@mastra/core/agent";
|
|
3
3
|
import { generateText, Output } from "ai";
|
|
4
4
|
import { z } from "zod";
|
|
@@ -13,8 +13,13 @@ import { runWidgetStep } from "../widgets/step.js";
|
|
|
13
13
|
import { prepareInputs } from "./prepare.js";
|
|
14
14
|
import { answersAsText, colorLine, factLines, renderTemplate, resolveRef, } from "./template.js";
|
|
15
15
|
import { StepError } from "./types.js";
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
/** Today's date as the wizard's people write it. */
|
|
17
|
+
function today(ctx) {
|
|
18
|
+
return new Date().toLocaleDateString(LOCALES[wizardLang(ctx.def)], {
|
|
19
|
+
year: "numeric",
|
|
20
|
+
month: "long",
|
|
21
|
+
day: "numeric",
|
|
22
|
+
});
|
|
18
23
|
}
|
|
19
24
|
function brandBlock(ctx) {
|
|
20
25
|
const b = ctx.scope.brand;
|
|
@@ -166,7 +171,7 @@ export async function runAgentStep(step, ctx) {
|
|
|
166
171
|
: "Finish with everything the result needs in your final answer; it is turned into structured data afterwards.";
|
|
167
172
|
const system = [
|
|
168
173
|
GROUND_RULES,
|
|
169
|
-
`Today is ${today()}.`,
|
|
174
|
+
`Today is ${today(ctx)}.`,
|
|
170
175
|
brandBlock(ctx),
|
|
171
176
|
documentsBlock(ctx),
|
|
172
177
|
// What the space's plugins hold for its wizards: a wiki, questions and answers.
|
|
@@ -221,7 +226,7 @@ export async function runAgentStep(step, ctx) {
|
|
|
221
226
|
// Working through a mailbox or a row of portals takes many small tool calls.
|
|
222
227
|
const maxSteps = step.tools.includes("browser") || step.connections?.length ? 60 : 20;
|
|
223
228
|
for (const [i, photo] of photos.entries()) {
|
|
224
|
-
await ctx.emit("info",
|
|
229
|
+
await ctx.emit("info", { code: "photo", params: { n: i + 1, total: photos.length } }, photo.id);
|
|
225
230
|
}
|
|
226
231
|
const message = photos.length
|
|
227
232
|
? [
|
|
@@ -381,7 +386,7 @@ async function writeHtml(step, ctx, kind) {
|
|
|
381
386
|
].join("\n");
|
|
382
387
|
const system = [
|
|
383
388
|
GROUND_RULES,
|
|
384
|
-
`Today is ${today()}.`,
|
|
389
|
+
`Today is ${today(ctx)}.`,
|
|
385
390
|
brandBlock(ctx),
|
|
386
391
|
guide,
|
|
387
392
|
images.length
|
|
@@ -474,8 +479,8 @@ async function runMediaStep(step, ctx, asset) {
|
|
|
474
479
|
const todo = entries.filter((e) => !keep(e));
|
|
475
480
|
if (asset === "video") {
|
|
476
481
|
await ctx.emit("info", todo.length > 1
|
|
477
|
-
?
|
|
478
|
-
:
|
|
482
|
+
? { code: "clipsRendering", params: { count: todo.length } }
|
|
483
|
+
: { code: "videoRendering" });
|
|
479
484
|
}
|
|
480
485
|
let finished = 0;
|
|
481
486
|
const note = ctx.state.notes[step.id];
|
|
@@ -496,7 +501,10 @@ async function runMediaStep(step, ctx, asset) {
|
|
|
496
501
|
.join("\n")
|
|
497
502
|
: await visualPrompt(step, ctx, entry);
|
|
498
503
|
if (entry.reference && todo.length > 1) {
|
|
499
|
-
await ctx.emit("info",
|
|
504
|
+
await ctx.emit("info", {
|
|
505
|
+
code: asset === "image" ? "editing" : "filming",
|
|
506
|
+
params: { n: entry.index + 1, total: entries.length },
|
|
507
|
+
}, entry.reference);
|
|
500
508
|
}
|
|
501
509
|
const reference = await loadReference(entry.reference, ctx);
|
|
502
510
|
const media = asset === "image"
|
|
@@ -529,7 +537,7 @@ async function runMediaStep(step, ctx, asset) {
|
|
|
529
537
|
});
|
|
530
538
|
finished++;
|
|
531
539
|
if (todo.length > 1) {
|
|
532
|
-
await ctx.emit("info",
|
|
540
|
+
await ctx.emit("info", { code: "batchDone", params: { done: finished, total: todo.length } }, asset === "image" ? ref.id : undefined);
|
|
533
541
|
}
|
|
534
542
|
return { index: entry.index, prompt, ref };
|
|
535
543
|
});
|
|
@@ -1,16 +1,17 @@
|
|
|
1
|
-
import { allFields, isAudioValue, isLocationValue, itemsTotals, locationText, } from "@engenty-wizards/shared/definition";
|
|
1
|
+
import { allFields, isAudioValue, isLocationValue, itemsTotals, LOCALES, locationText, wizardLang, } from "@engenty-wizards/shared/definition";
|
|
2
2
|
import { listAsMarkdown } from "@engenty-wizards/shared/store";
|
|
3
3
|
const TEMPLATE_RE = /\{\{\s*([^}]+?)\s*\}\}/g;
|
|
4
|
-
function money(n, currency) {
|
|
4
|
+
function money(n, currency, locale) {
|
|
5
5
|
try {
|
|
6
|
-
return new Intl.NumberFormat(
|
|
6
|
+
return new Intl.NumberFormat(locale, { style: "currency", currency }).format(n);
|
|
7
7
|
}
|
|
8
8
|
catch {
|
|
9
9
|
return `${n.toFixed(2)} ${currency}`;
|
|
10
10
|
}
|
|
11
11
|
}
|
|
12
12
|
/** Line items as a Markdown table with their totals — what a model needs to write a document. */
|
|
13
|
-
export function itemsAsMarkdown(field, value, values) {
|
|
13
|
+
export function itemsAsMarkdown(field, value, values, lang = "de") {
|
|
14
|
+
const locale = LOCALES[lang];
|
|
14
15
|
const totals = itemsTotals(field, value, values);
|
|
15
16
|
const cols = field.columns ?? [];
|
|
16
17
|
if (!totals.rows.length) {
|
|
@@ -18,12 +19,14 @@ export function itemsAsMarkdown(field, value, values) {
|
|
|
18
19
|
}
|
|
19
20
|
const head = `| ${cols.map((c) => c.label).join(" | ")} | Amount |\n|${cols.map(() => " --- ").join("|")}| ---: |`;
|
|
20
21
|
const body = totals.rows.map((r) => `| ${cols
|
|
21
|
-
.map((c) => c.kind === "money"
|
|
22
|
-
|
|
22
|
+
.map((c) => c.kind === "money"
|
|
23
|
+
? money(Number(r[c.id]) || 0, totals.currency, locale)
|
|
24
|
+
: String(r[c.id] ?? ""))
|
|
25
|
+
.join(" | ")} | ${money(r.amount, totals.currency, locale)} |`);
|
|
23
26
|
const sums = [
|
|
24
|
-
`Net: ${money(totals.net, totals.currency)}`,
|
|
25
|
-
totals.vatRate ? `VAT ${totals.vatRate}%: ${money(totals.vat, totals.currency)}` : null,
|
|
26
|
-
`Total: ${money(totals.gross, totals.currency)}`,
|
|
27
|
+
`Net: ${money(totals.net, totals.currency, locale)}`,
|
|
28
|
+
totals.vatRate ? `VAT ${totals.vatRate}%: ${money(totals.vat, totals.currency, locale)}` : null,
|
|
29
|
+
`Total: ${money(totals.gross, totals.currency, locale)}`,
|
|
27
30
|
].filter(Boolean);
|
|
28
31
|
return [head, ...body, "", ...sums].join("\n");
|
|
29
32
|
}
|
|
@@ -118,7 +121,7 @@ export function resolveRef(ref, scope) {
|
|
|
118
121
|
const value = state.values[head];
|
|
119
122
|
if (field?.kind === "items") {
|
|
120
123
|
if (!rest.length) {
|
|
121
|
-
return itemsAsMarkdown(field, value, state.values);
|
|
124
|
+
return itemsAsMarkdown(field, value, state.values, wizardLang(def));
|
|
122
125
|
}
|
|
123
126
|
const totals = itemsTotals(field, value, state.values);
|
|
124
127
|
return totals[rest[0]];
|
|
@@ -16,7 +16,7 @@ import { layout } from "../cli/home.js";
|
|
|
16
16
|
import { connectorImportSchema, importConnector, listConnectors, previewSource, refreshConnector, registryService, removeConnector, searchRegistry, } from "../connectors/external.js";
|
|
17
17
|
import { balanceCredits, canSpend } from "../credits/credits.js";
|
|
18
18
|
import { estimateRun } from "../credits/estimate.js";
|
|
19
|
-
import { missingModels } from "../engine/requirements.js";
|
|
19
|
+
import { missingModels, stepsWithoutModel } from "../engine/requirements.js";
|
|
20
20
|
import { env } from "../env.js";
|
|
21
21
|
import { freshAuth, loginEnv } from "../harness/env.js";
|
|
22
22
|
import { detectHarness, detectHarnesses, HARNESS_IDS, harness, installSpec, } from "../harness/index.js";
|
|
@@ -639,7 +639,7 @@ export const studio = new Hono()
|
|
|
639
639
|
// What a run of the draft is expected to cost, per step and in total.
|
|
640
640
|
.get("/wizards/:id/estimate", async (c) => {
|
|
641
641
|
const w = await ownedWizard(c.get("user").id, c.req.param("id"));
|
|
642
|
-
return c.json(await estimateRun(w.id, null, w.draft));
|
|
642
|
+
return c.json(await estimateRun(w.id, null, w.draft, await stepsWithoutModel(w.draft)));
|
|
643
643
|
})
|
|
644
644
|
// The models the draft needs that this runtime cannot serve, by class, with the steps that call them.
|
|
645
645
|
.get("/wizards/:id/models", async (c) => {
|