diff --git a/apps/hub/src/ai/data/storage.ts b/apps/hub/src/ai/data/storage.ts index 8172ea9e1e..281040197c 100644 --- a/apps/hub/src/ai/data/storage.ts +++ b/apps/hub/src/ai/data/storage.ts @@ -31,10 +31,18 @@ export function decodeDbWorkflowToClientWorkflow( return { id: row.id, timestamp: row.createdAt.getTime(), - status: "error", + status: "finished", inputs: {}, steps: [], - result: `Invalid workflow format in the database: ${e}`, + result: { + code: "", + isValid: false, + totalPrice: 0, + runTimeMs: 0, + llmRunCount: 0, + logSummary: "", + error: `Invalid workflow format in the database: ${e}`, + }, } satisfies ClientWorkflow; } } diff --git a/apps/hub/src/ai/data/v1_0.ts b/apps/hub/src/ai/data/v1_0.ts index e034b009e1..9bd8e71278 100644 --- a/apps/hub/src/ai/data/v1_0.ts +++ b/apps/hub/src/ai/data/v1_0.ts @@ -94,16 +94,15 @@ export const v1WorkflowSchema = z.discriminatedUnion("status", [ }), ]); +// upgrading legacy workflow to new client workflow shape export function decodeV1_0JsonToClientWorkflow( json: Prisma.JsonValue ): ClientWorkflow { // `input` doesn't exist in the new ClientWorkflow shape, so we need to pull it out const { input, ...v1Workflow } = v1WorkflowSchema.parse(json); - // upgrading legacy workflow to new client workflow shape - return { - ...v1Workflow, - steps: v1Workflow.steps.map(({ outputs, ...step }) => ({ + const steps: ClientWorkflow["steps"] = v1Workflow.steps.map( + ({ outputs, ...step }) => ({ ...step, // modern steps in ClientWorkflow store state as an object state: @@ -122,22 +121,47 @@ export function decodeV1_0JsonToClientWorkflow( } : { kind: "PENDING" }, startTime: v1Workflow.timestamp, // old workflow steps don't have start times - })), - inputs: - input.type === "Create" - ? { - prompt: { - value: input.prompt, - kind: "prompt", - id: `${v1Workflow.id}-prompt`, - }, - } - : { - source: { - value: input.source, - kind: "source", - id: `${v1Workflow.id}-source`, - }, + }) + ); + + const inputs: ClientWorkflow["inputs"] = + input.type === "Create" + ? { + prompt: { + value: input.prompt, + kind: "prompt", + id: `${v1Workflow.id}-prompt`, + }, + } + : { + source: { + value: input.source, + kind: "source", + id: `${v1Workflow.id}-source`, }, + }; + + if (v1Workflow.status === "error") { + return { + ...v1Workflow, + status: "finished", + result: { + code: "", + isValid: false, + totalPrice: 0, + runTimeMs: 0, + llmRunCount: 0, + logSummary: "", + error: v1Workflow.result, + }, + steps, + inputs, + }; + } + + return { + ...v1Workflow, + steps, + inputs, }; } diff --git a/apps/hub/src/app/ai/WorkflowSummaryList/WorkflowStatusIcon.tsx b/apps/hub/src/app/ai/WorkflowSummaryList/WorkflowStatusIcon.tsx index 21e9818d0b..7e6261261e 100644 --- a/apps/hub/src/app/ai/WorkflowSummaryList/WorkflowStatusIcon.tsx +++ b/apps/hub/src/app/ai/WorkflowSummaryList/WorkflowStatusIcon.tsx @@ -12,7 +12,7 @@ export function isWorkflowOutdated(workflow: ClientWorkflow): boolean { function getWorkflowStatusForIcon( workflow: ClientWorkflow -): ClientWorkflow["status"] { +): "loading" | "finished" | "error" { if (workflow.status === "loading") { return isWorkflowOutdated(workflow) ? "error" : "loading"; } @@ -36,5 +36,7 @@ export const WorkflowStatusIcon: FC<{ workflow: ClientWorkflow }> = ({ return ; case "error": return ; + default: + throw new Error(status satisfies never); } }; diff --git a/apps/hub/src/app/ai/WorkflowViewer/index.tsx b/apps/hub/src/app/ai/WorkflowViewer/index.tsx index d225064ea7..52134bbf41 100644 --- a/apps/hub/src/app/ai/WorkflowViewer/index.tsx +++ b/apps/hub/src/app/ai/WorkflowViewer/index.tsx @@ -3,7 +3,7 @@ import { format } from "date-fns"; import { Children, FC } from "react"; import { ClientWorkflow } from "@quri/squiggle-ai"; -import { ErrorIcon, StyledTab } from "@quri/ui"; +import { ErrorIcon, StyledTab, TextTooltip } from "@quri/ui"; import { commonDateFormat } from "@/lib/constants"; import { useAvailableHeight } from "@/lib/hooks/useAvailableHeight"; @@ -53,12 +53,26 @@ const FinishedWorkflowViewer: FC> = ({
( - - {(workflow.result.runTimeMs / 1000).toFixed(2)}s - ${workflow.result.totalPrice.toFixed(2)} - {workflow.result.llmRunCount} LLM runs - - +
+ + {(workflow.result.runTimeMs / 1000).toFixed(2)}s + ${workflow.result.totalPrice.toFixed(2)} + {workflow.result.llmRunCount} LLM runs + + + {workflow.result.error ? ( +
+ + + + + +
+ ) : null} +
)} renderRight={() => (
@@ -137,18 +151,6 @@ export const WorkflowViewer: FC = ({ return ; case "loading": return ; - case "error": - return ( -
-

- Server Error -

-

{workflow.result}

-

- Please try refreshing the page or attempt your action again. -

-
- ); default: throw workflow satisfies never; } diff --git a/apps/hub/src/app/ai/api/create/route.ts b/apps/hub/src/app/ai/api/create/route.ts index a3702062ca..8d584531ce 100644 --- a/apps/hub/src/app/ai/api/create/route.ts +++ b/apps/hub/src/app/ai/api/create/route.ts @@ -105,7 +105,7 @@ function aiRequestToWorkflow(request: AiRequestBody) { const llmConfig: LlmConfig = { llmId: request.model ?? "Claude-Sonnet", priceLimit: 0.3, - durationLimitMinutes: 2, + durationLimitMinutes: 0.01, messagesInHistoryToKeep: 4, numericSteps: request.numericSteps, styleGuideSteps: request.styleGuideSteps, diff --git a/apps/hub/src/app/ai/useSquiggleWorkflows.tsx b/apps/hub/src/app/ai/useSquiggleWorkflows.tsx index 30423fc18f..b464b716b3 100644 --- a/apps/hub/src/app/ai/useSquiggleWorkflows.tsx +++ b/apps/hub/src/app/ai/useSquiggleWorkflows.tsx @@ -109,8 +109,16 @@ export function useSquiggleWorkflows(preloadedWorkflows: AiWorkflow[]) { } catch (error) { updateWorkflow(id, (workflow) => ({ ...workflow, - status: "error", - result: `Server error: ${error instanceof Error ? error.toString() : "Unknown error"}.`, + status: "finished", + result: { + code: "", + isValid: false, + totalPrice: 0, + runTimeMs: 0, + llmRunCount: 0, + logSummary: "", + error: `Server error: ${error instanceof Error ? error.toString() : "Unknown error"}.`, + }, })); } }, diff --git a/internal-packages/ai/src/types.ts b/internal-packages/ai/src/types.ts index 069bed0ebd..0941b59892 100644 --- a/internal-packages/ai/src/types.ts +++ b/internal-packages/ai/src/types.ts @@ -93,6 +93,7 @@ const stepUpdatedSchema = stepSchema.partial().required({ export const clientWorkflowResultSchema = z.object({ code: z.string().describe("Squiggle code snippet"), isValid: z.boolean(), + error: z.string().optional(), totalPrice: z.number(), runTimeMs: z.number(), llmRunCount: z.number(), @@ -142,11 +143,6 @@ export const clientWorkflowSchema = z.discriminatedUnion("status", [ status: z.literal("finished"), result: clientWorkflowResultSchema, }), - z.object({ - ...commonClientWorkflowFields, - status: z.literal("error"), - result: z.string(), - }), ]); export type ClientWorkflow = z.infer; diff --git a/internal-packages/ai/src/workflows/Workflow.ts b/internal-packages/ai/src/workflows/Workflow.ts index 1172efcfcd..e0397caea9 100644 --- a/internal-packages/ai/src/workflows/Workflow.ts +++ b/internal-packages/ai/src/workflows/Workflow.ts @@ -111,10 +111,13 @@ export class Workflow { public llmClient: LLMClient; + public error?: string; + constructor(params: WorkflowInstanceParams) { this.id = params.id ?? crypto.randomUUID(); this.template = params.template; this.inputs = params.inputs; + this.error = params.error ?? undefined; this.llmConfig = params.llmConfig ?? llmConfigDefault; this.startTime = params.steps.at(0)?.startTime ?? Date.now(); @@ -201,9 +204,9 @@ export class Workflow { payload: { step }, }); - // apply the transition rule, produce the next step in PENDING state - // this code is inlined in this method, which guarantees that we always have one pending step - // (until the transition rule decides to finish) + // Apply the transition rule, produce the next step in PENDING state or stop the workflow. + // This code is inlined in this method, which guarantees that we always have one pending step + // (until the transition rule decides to finish). const result = this.transitionRule(step, new WorkflowGuardHelpers(step)); switch (result.kind) { case "repeat": @@ -213,10 +216,11 @@ export class Workflow { this.addStep(result.step.prepare(result.inputs)); break; case "finish": - // no new steps to add + // no new steps to add, we're done break; case "fatal": - throw new Error(result.message); + this.error = result.message; + break; } } @@ -299,7 +303,7 @@ export class Workflow { getFinalResult(): ClientWorkflowResult { const finalStep = this.getRecentStepWithCode(); - const isValid = finalStep?.step.getState().kind === "DONE"; + const isValid = finalStep?.step.getState().kind === "DONE" && !this.error; // compute run time let runTimeMs: number; @@ -326,6 +330,7 @@ export class Workflow { return { code: finalStep?.code ?? "", isValid, + error: this.error, totalPrice, runTimeMs, llmRunCount, @@ -427,6 +432,7 @@ export class Workflow { return { id: this.id, templateName: this.template.name, + error: this.error ?? null, inputIds: Object.fromEntries( Object.entries(this.inputs).map(([key, input]) => [ key, @@ -454,6 +460,7 @@ export class Workflow { }): Workflow { const workflow = new Workflow({ id: node.id, + error: node.error ?? undefined, template: getWorkflowTemplateByName(node.templateName), inputs: Object.fromEntries( Object.entries(node.inputIds).map(([key, id]) => [ @@ -509,4 +516,5 @@ export type SerializedWorkflow = { inputIds: Record; llmConfig: LlmConfig; stepIds: number[]; + error: string | null; }; diff --git a/internal-packages/ai/src/workflows/WorkflowTemplate.ts b/internal-packages/ai/src/workflows/WorkflowTemplate.ts index ceccaacdb1..dc8890f272 100644 --- a/internal-packages/ai/src/workflows/WorkflowTemplate.ts +++ b/internal-packages/ai/src/workflows/WorkflowTemplate.ts @@ -11,6 +11,7 @@ export type WorkflowInstanceParams = { llmConfig?: LlmConfig; openaiApiKey?: string; anthropicApiKey?: string; + error?: string; }; /** diff --git a/internal-packages/ai/src/workflows/controllers.ts b/internal-packages/ai/src/workflows/controllers.ts index ad8cc6d2d3..af26b7c77b 100644 --- a/internal-packages/ai/src/workflows/controllers.ts +++ b/internal-packages/ai/src/workflows/controllers.ts @@ -145,6 +145,6 @@ export function getDefaultTransitionRule( } } - return h.fatal("Unknown step"); + return h.fatal(ERROR_MESSAGES.UNKNOWN_STEP); }; }