Skip to content

feat: Add execute to createWorkflow context #17

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 10 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 0 additions & 13 deletions .github/workflows/conventional-label.yml

This file was deleted.

1 change: 0 additions & 1 deletion .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1 +0,0 @@
npx lint-staged
5 changes: 4 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,8 @@
"source.fixAll.eslint": "always",
"source.removeUnusedImports": "always"
},
"cSpell.words": ["gensx"]
"cSpell.words": [
"gensx"
],
"typescript.tsdk": "node_modules/typescript/lib"
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"packageManager": "[email protected]",
"type": "module",
"scripts": {
"run": "tsx playground/index.tsx",
"dev": "nodemon",
"prepublishOnly": "pnpm i && pnpm build",
"build": "pnpm validate-typescript && pnpm build:clean && pnpm generate-dist",
Expand Down
4 changes: 2 additions & 2 deletions playground/blog/BlogWritingWorkflow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ interface BlogWritingWorkflowInputs {
export const BlogWritingWorkflow = createWorkflow<
BlogWritingWorkflowInputs,
string
>((props, render) => (
>((props, { resolve }) => (
<LLMResearcher title={props.title} prompt={props.prompt}>
{({ research }) => (
<LLMWriter content={research}>
{({ content }) => (
<LLMEditor content={content}>
{editedContent => render(editedContent)}
{editedContent => resolve(editedContent)}
</LLMEditor>
)}
</LLMWriter>
Expand Down
80 changes: 80 additions & 0 deletions playground/fanout/FanoutExample.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { createWorkflow } from "@/src/utils/workflowBuilder";

interface FanoutWorkflowInputs {
numbers: number[];
strings: string[];
}

interface FanoutWorkflowOutputs {
num: number;
str: string;
}

export const FanoutWorkflow = createWorkflow<
FanoutWorkflowInputs,
FanoutWorkflowOutputs
>(async (props, { resolve, execute }) => {
const doubledNums = props.numbers.map(n =>
execute(DoubleNumber, { input: n }),
);
const doubledStrings = props.strings.map(s =>
execute(DoubleString, { input: s }),
);

const resolvedNums = await Promise.all(doubledNums);
const resolvedStrings = await Promise.all(doubledStrings);

return (
<SumNumbers numbers={resolvedNums}>
{sum => (
<ConcatStrings strings={resolvedStrings}>
{str => resolve({ num: sum, str })}
</ConcatStrings>
)}
</SumNumbers>
);
});

interface ConcatStringsInputs {
strings: string[];
}

export const ConcatStrings = createWorkflow<ConcatStringsInputs, string>(
(props, { resolve }) => {
return resolve(props.strings.join(""));
},
);

interface SumNumbersInputs {
numbers: number[];
}

export const SumNumbers = createWorkflow<SumNumbersInputs, number>(
(props, { resolve }) => {
return resolve(props.numbers.reduce((a, b) => a + b, 0));
},
);

// Component for doubling a number
export interface DoubleNumberProps {
input: number;
}

export const DoubleNumber = createWorkflow<DoubleNumberProps, number>(
(props, { resolve }) => {
const doubled = props.input * 2;
return resolve(doubled);
},
);

// Component for doubling a string
export interface DoubleStringProps {
input: string;
}

export const DoubleString = createWorkflow<DoubleStringProps, string>(
(props, { resolve }) => {
const doubled = props.input + props.input;
return resolve(doubled);
},
);
38 changes: 30 additions & 8 deletions playground/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { WorkflowContext } from "@/src/components/Workflow";
import { createWorkflowOutput } from "@/src/hooks/useWorkflowOutput";

import { BlogWritingWorkflow } from "./blog/BlogWritingWorkflow";
import { FanoutWorkflow } from "./fanout/FanoutExample";
import { TweetWritingWorkflow } from "./tweet/TweetWritingWorkflow";

async function runParallelWorkflow() {
Expand Down Expand Up @@ -40,14 +41,7 @@ async function runNestedWorkflow() {

const workflow = (
<Workflow>
<BlogWritingWorkflow
title={
new Promise(resolve => {
resolve(title);
})
}
prompt={prompt}
>
<BlogWritingWorkflow title={title} prompt={prompt}>
{blogPostResult => (
<TweetWritingWorkflow content={blogPostResult}>
{tweetResult => {
Expand All @@ -69,10 +63,38 @@ async function runNestedWorkflow() {
console.log("Tweet:", tweet);
}

async function runFanoutWorkflow() {
const nums = [1, 2, 3, 4, 5];
const strs = ["a", "b", "c", "d", "e"];

let numResult = 0;
let strResult = "";

const workflow = (
<Workflow>
<FanoutWorkflow numbers={nums} strings={strs}>
{({ num, str }) => {
numResult = num;
strResult = str;
return null;
}}
</FanoutWorkflow>
</Workflow>
);

const context = new WorkflowContext(workflow);
await context.execute();

console.log("\n=== Fanout Workflow Results ===");
console.log("Number:", numResult);
console.log("String:", strResult);
}

async function main() {
try {
await runParallelWorkflow();
await runNestedWorkflow();
await runFanoutWorkflow();
} catch (error) {
console.error("Workflow execution failed:", error);
process.exit(1);
Expand Down
4 changes: 2 additions & 2 deletions playground/shared/components/LLMEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ interface EditorProps {
}

export const LLMEditor = createWorkflow<EditorProps, string>(
async (props, render) => {
async (props, { resolve }) => {
const editedContent = await Promise.resolve(`Edited: ${props.content}`);
return render(editedContent);
return resolve(editedContent);
},
);
21 changes: 12 additions & 9 deletions playground/shared/components/LLMResearcher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,17 @@ interface ResearcherOutput {
}

export const LLMResearcher = createWorkflow<ResearcherProps, ResearcherOutput>(
async (props, render) => {
const result = {
research: await Promise.resolve(
`Research based on title: ${props.title}, prompt: ${props.prompt}`,
),
sources: ["source1.com", "source2.com"],
summary: "Brief summary of findings",
};
return render(result);
async (props, { resolve }) => {
const processedResearch = await Promise.resolve(
`Research based on title: ${props.title}, prompt: ${props.prompt}`,
);
const processedSources = ["source1.com", "source2.com"];
const processedSummary = "Brief summary of findings";

return resolve({
research: processedResearch,
sources: processedSources,
summary: processedSummary,
});
},
);
4 changes: 2 additions & 2 deletions playground/shared/components/LLMWriter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ interface WriterOutput {
}

export const LLMWriter = createWorkflow<WriterProps, WriterOutput>(
async (props, render) => {
async (props, { resolve }) => {
const processedContent = await Promise.resolve(
`Written content based on: ${props.content}`,
);
Expand All @@ -24,7 +24,7 @@ export const LLMWriter = createWorkflow<WriterProps, WriterOutput>(
keywords: ["sample", "content", "test"],
};

return render({
return resolve({
content: processedContent,
metadata: processedMetadata,
});
Expand Down
20 changes: 11 additions & 9 deletions playground/tweet/TweetWritingWorkflow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@ interface TweetWritingWorkflowInputs {
export const TweetWritingWorkflow = createWorkflow<
TweetWritingWorkflowInputs,
string
>((props, render) => (
<LLMWriter content={props.content}>
{({ content }) => (
<LLMEditor content={content}>
{editedContent => render(editedContent)}
</LLMEditor>
)}
</LLMWriter>
));
>(async (props, { resolve, execute }) => {
return (
<LLMWriter content={props.content}>
{({ content }) => (
<LLMEditor content={content}>
{editedContent => resolve(editedContent)}
</LLMEditor>
)}
</LLMWriter>
);
});
5 changes: 3 additions & 2 deletions src/components/Workflow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,13 @@ export class WorkflowContext {
private steps: Step[] = [];
private dynamicSteps = new Map<string, Step[]>();
private executedSteps = new Set<string>();
readonly id: string;

constructor(workflow: React.ReactElement) {
this.id = `workflow_${Math.random().toString(36).slice(2)}`;
const wrappedWorkflow = React.createElement(React.Fragment, null, workflow);
this.steps = renderWorkflow(wrappedWorkflow);
WorkflowContext.current = this;
}

notifyUpdate(componentId: string) {
Expand Down Expand Up @@ -52,8 +55,6 @@ export class WorkflowContext {
}

async execute() {
WorkflowContext.current = this;

try {
// Execute all initial steps in parallel
await Promise.all(
Expand Down
71 changes: 27 additions & 44 deletions src/hooks/useWorkflowOutput.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
type WorkflowOutput<T> = Map<
import { WorkflowContext } from "../components/Workflow";

type WorkflowOutput = Map<
string,
{
promise: Promise<T>;
resolve: (value: T) => void;
promise: Promise<unknown>;
resolve: (value: unknown) => void;
hasResolved: boolean;
}
>;

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const workflowOutputs: WorkflowOutput<any> = new Map();
// Map of workflow context ID to its outputs
const contextOutputs = new Map<string, WorkflowOutput>();

let counter = 0;
function generateStableId() {
Expand All @@ -18,46 +20,27 @@ function generateStableId() {
export function createWorkflowOutput<T>(
_initialValue: T,
): [Promise<T>, (value: T) => void] {
const outputId = generateStableId();
const context = WorkflowContext.current ?? {
id: generateStableId(),
};

if (!workflowOutputs.has(outputId)) {
let resolvePromise: (value: T) => void;
let rejectPromise: (error: unknown) => void;
const promise = new Promise<T>((resolve, reject) => {
resolvePromise = resolve;
rejectPromise = reject;
});

// Only add timeout if WORKFLOW_TIMEOUT is set
let timeoutId: NodeJS.Timeout | undefined;
if (process.env.WORKFLOW_TIMEOUT === "true") {
timeoutId = setTimeout(() => {
if (!workflowOutputs.get(outputId)?.hasResolved) {
console.error(`Output ${outputId} timed out without being resolved`);
rejectPromise(
new Error(`Output ${outputId} timed out waiting for resolution`),
);
}
}, 5000);
}

workflowOutputs.set(outputId, {
promise,
resolve: (value: T) => {
if (timeoutId) {
clearTimeout(timeoutId);
}
if (workflowOutputs.get(outputId)?.hasResolved) {
throw new Error("Cannot set value multiple times");
}
resolvePromise(value);
workflowOutputs.get(outputId)!.hasResolved = true;
},
hasResolved: false,
});
if (!contextOutputs.has(context.id)) {
contextOutputs.set(context.id, new Map());
}

const output = workflowOutputs.get(outputId)!;
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return [output.promise, output.resolve] as const;
const outputs = contextOutputs.get(context.id)!;
const outputId = generateStableId();

let resolvePromise!: (value: T) => void;
const promise = new Promise<T>(resolve => {
resolvePromise = resolve;
});

outputs.set(outputId, {
promise: promise as Promise<unknown>,
resolve: resolvePromise as (value: unknown) => void,
hasResolved: false,
});

return [promise, resolvePromise];
}
Loading
Loading