From b2e954c50cd2c60fffcd41f697d550c7550c8bb4 Mon Sep 17 00:00:00 2001 From: Jeremy Moseley Date: Mon, 16 Dec 2024 17:22:18 -0800 Subject: [PATCH 01/14] SAVEPOINT --- package.json | 4 +- playground/blog/BlogWritingWorkflow.tsx | 27 --- playground/index.tsx | 155 +++++++++-------- playground/shared/components/LLMEditor.tsx | 12 -- .../shared/components/LLMResearcher.tsx | 25 --- playground/shared/components/LLMWriter.tsx | 32 ---- playground/tweet/TweetWritingWorkflow.tsx | 21 --- pnpm-lock.yaml | 47 +++--- src/components/Step.ts | 5 - src/components/Workflow.tsx | 91 ---------- src/context/ExecutionContext.ts | 11 -- src/context/StepContext.ts | 9 - src/context/workflow-context.ts | 24 --- src/execute.tsx | 61 +++++++ src/hooks/useWorkflowOutput.ts | 63 ------- src/index.ts | 24 ++- src/registry/workflow-registry.ts | 14 -- src/utils/renderWorkflow.tsx | 104 ------------ src/utils/workflowBuilder.ts | 157 ------------------ src/withWorkflowStep.tsx | 38 +++++ tsconfig.json | 1 + 21 files changed, 226 insertions(+), 699 deletions(-) delete mode 100644 playground/blog/BlogWritingWorkflow.tsx delete mode 100644 playground/shared/components/LLMEditor.tsx delete mode 100644 playground/shared/components/LLMResearcher.tsx delete mode 100644 playground/shared/components/LLMWriter.tsx delete mode 100644 playground/tweet/TweetWritingWorkflow.tsx delete mode 100644 src/components/Step.ts delete mode 100644 src/components/Workflow.tsx delete mode 100644 src/context/ExecutionContext.ts delete mode 100644 src/context/StepContext.ts delete mode 100644 src/context/workflow-context.ts create mode 100644 src/execute.tsx delete mode 100644 src/hooks/useWorkflowOutput.ts delete mode 100644 src/registry/workflow-registry.ts delete mode 100644 src/utils/renderWorkflow.tsx delete mode 100644 src/utils/workflowBuilder.ts create mode 100644 src/withWorkflowStep.tsx diff --git a/package.json b/package.json index b7ef36d6..c6855ec0 100644 --- a/package.json +++ b/package.json @@ -44,8 +44,8 @@ }, "dependencies": { "openai": "^4.76.0", - "react": "^19.0.0", - "react-dom": "^19.0.0" + "preact": "^10.25.2", + "preact-render-to-string": "^6.5.12" }, "devDependencies": { "@swc/cli": "^0.5.2", diff --git a/playground/blog/BlogWritingWorkflow.tsx b/playground/blog/BlogWritingWorkflow.tsx deleted file mode 100644 index 9a5287f5..00000000 --- a/playground/blog/BlogWritingWorkflow.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { createWorkflow } from "@/src/utils/workflowBuilder"; - -import { LLMEditor } from "../shared/components/LLMEditor"; -import { LLMResearcher } from "../shared/components/LLMResearcher"; -import { LLMWriter } from "../shared/components/LLMWriter"; - -interface BlogWritingWorkflowInputs { - title: string; - prompt: string; -} - -export const BlogWritingWorkflow = createWorkflow< - BlogWritingWorkflowInputs, - string ->((props, render) => ( - - {({ research }) => ( - - {({ content }) => ( - - {editedContent => render(editedContent)} - - )} - - )} - -)); diff --git a/playground/index.tsx b/playground/index.tsx index 64a4780e..7a69b0d1 100644 --- a/playground/index.tsx +++ b/playground/index.tsx @@ -1,85 +1,98 @@ -import { Workflow } from "@/src/components/Workflow"; -import { WorkflowContext } from "@/src/components/Workflow"; -import { createWorkflowOutput } from "@/src/hooks/useWorkflowOutput"; +import { ComponentChild } from "preact"; -import { BlogWritingWorkflow } from "./blog/BlogWritingWorkflow"; -import { TweetWritingWorkflow } from "./tweet/TweetWritingWorkflow"; +import { + executeJsxWorkflow, + withWorkflowComponent, + withWorkflowFunction, +} from "@/src/index"; -async function runParallelWorkflow() { - const title = "Programmatic Secrets with ESC"; - const prompt = "Write an article..."; - - const [blogPost, setBlogPost] = createWorkflowOutput(""); - const [tweet, setTweet] = createWorkflowOutput(""); - - const workflow = ( - - - - +// Parallel execution component +const Collect = async ({ + children, +}: { + children: ComponentChild[]; +}): Promise => { + console.log("📦 Collect input:", children); + const results = await Promise.all( + (Array.isArray(children) ? children : [children]).map(child => { + console.log("📦 Collect executing child:", child); + return executeJsxWorkflow(child); + }), ); + console.log("📦 Collect results:", results); + return results; +}; - const context = new WorkflowContext(workflow); - await context.execute(); +// Pure workflow steps +const pureResearchBrainstorm = async ({ prompt }: { prompt: string }) => { + console.log("🔍 Starting research for:", prompt); + const topics = await Promise.resolve(["topic 1", "topic 2", "topic 3"]); + return topics; +}; - console.log("\n=== Parallel Workflow Results ==="); - console.log("Blog Post:", await blogPost); - console.log("Tweet:", await tweet); -} +const pureResearch = async ({ topic }: { topic: string }) => { + console.log("📚 Researching topic:", topic); + return await Promise.resolve(`research results for ${topic}`); +}; -async function runNestedWorkflow() { - const title = "Programmatic Secrets with ESC"; - const prompt = "Write an article..."; +const pureWriter = async ({ + research, + prompt, +}: { + research: string; + prompt: string; +}): Promise => { + console.log("✍️ Writing draft based on research"); + return await Promise.resolve(`**draft\n${research}\n${prompt}\n**end draft`); +}; - let blogPost = ""; - let tweet = ""; +const pureEditor = async ({ draft }: { draft: string }) => { + console.log("✨ Polishing final draft"); + return await Promise.resolve(`edited result: ${draft}`); +}; - const workflow = ( - - { - resolve(title); - }) - } - prompt={prompt} - > - {blogPostResult => ( - - {tweetResult => { - blogPost = blogPostResult; - tweet = tweetResult; - return null; - }} - - )} - - - ); +// Wrapped workflow steps +const LLMResearchBrainstorm = withWorkflowFunction(pureResearchBrainstorm); +const LLMResearch = withWorkflowFunction(pureResearch); +const LLMWriter = withWorkflowFunction(pureWriter); +const LLMEditor = withWorkflowFunction(pureEditor); - const context = new WorkflowContext(workflow); - await context.execute(); +// Research collection component +const ResearchCollection = withWorkflowComponent<{ prompt: string }, string[]>( + ({ prompt }) => ( + + {topics => ( + + {topics.map(topic => ( + + ))} + + )} + + ), +); - console.log("\n=== Nested Workflow Results ==="); - console.log("Blog Post:", blogPost); - console.log("Tweet:", tweet); -} +const BlogWritingWorkflow = ({ prompt }: { prompt: string }) => { + return ( + + {research => { + console.log("🧠 Research:", research, typeof research); + return ( + + {draft => } + + ); + }} + + ); +}; async function main() { - try { - await runParallelWorkflow(); - await runNestedWorkflow(); - } catch (error) { - console.error("Workflow execution failed:", error); - process.exit(1); - } + console.log("🚀 Starting blog writing workflow"); + const result = await executeJsxWorkflow( + , + ); + console.log("✅ Final result:", result); } -main().catch((error: unknown) => { - console.error("Unhandled error:", error); - process.exit(1); -}); +await main(); diff --git a/playground/shared/components/LLMEditor.tsx b/playground/shared/components/LLMEditor.tsx deleted file mode 100644 index a40e2bdd..00000000 --- a/playground/shared/components/LLMEditor.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { createWorkflow } from "@/src/utils/workflowBuilder"; - -interface EditorProps { - content: string; -} - -export const LLMEditor = createWorkflow( - async (props, render) => { - const editedContent = await Promise.resolve(`Edited: ${props.content}`); - return render(editedContent); - }, -); diff --git a/playground/shared/components/LLMResearcher.tsx b/playground/shared/components/LLMResearcher.tsx deleted file mode 100644 index 2e7cf663..00000000 --- a/playground/shared/components/LLMResearcher.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { createWorkflow } from "@/src/utils/workflowBuilder"; - -interface ResearcherProps { - title: string; - prompt: string; -} - -interface ResearcherOutput { - research: string; - sources: string[]; - summary: string; -} - -export const LLMResearcher = createWorkflow( - 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); - }, -); diff --git a/playground/shared/components/LLMWriter.tsx b/playground/shared/components/LLMWriter.tsx deleted file mode 100644 index b4a20114..00000000 --- a/playground/shared/components/LLMWriter.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { createWorkflow } from "@/src/utils/workflowBuilder"; - -interface WriterProps { - content: string; -} - -interface WriterOutput { - content: string; - metadata: { - wordCount: number; - readingTime: number; - keywords: string[]; - }; -} - -export const LLMWriter = createWorkflow( - async (props, render) => { - const processedContent = await Promise.resolve( - `Written content based on: ${props.content}`, - ); - const processedMetadata = { - wordCount: processedContent.split(" ").length, - readingTime: Math.ceil(processedContent.split(" ").length / 200), - keywords: ["sample", "content", "test"], - }; - - return render({ - content: processedContent, - metadata: processedMetadata, - }); - }, -); diff --git a/playground/tweet/TweetWritingWorkflow.tsx b/playground/tweet/TweetWritingWorkflow.tsx deleted file mode 100644 index 0354802f..00000000 --- a/playground/tweet/TweetWritingWorkflow.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { createWorkflow } from "@/src/utils/workflowBuilder"; - -import { LLMEditor } from "../shared/components/LLMEditor"; -import { LLMWriter } from "../shared/components/LLMWriter"; - -interface TweetWritingWorkflowInputs { - content: string | Promise; -} - -export const TweetWritingWorkflow = createWorkflow< - TweetWritingWorkflowInputs, - string ->((props, render) => ( - - {({ content }) => ( - - {editedContent => render(editedContent)} - - )} - -)); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 54148b2e..a67ba48a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,12 +11,12 @@ importers: openai: specifier: ^4.76.0 version: 4.76.3 - react: - specifier: ^19.0.0 - version: 19.0.0 - react-dom: - specifier: ^19.0.0 - version: 19.0.0(react@19.0.0) + preact: + specifier: ^10.25.2 + version: 10.25.2 + preact-render-to-string: + specifier: ^6.5.12 + version: 6.5.12(preact@10.25.2) optionalDependencies: '@rollup/rollup-linux-x64-gnu': specifier: ^4.28.1 @@ -2286,6 +2286,14 @@ packages: resolution: {integrity: sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==} engines: {node: ^10 || ^12 || >=14} + preact-render-to-string@6.5.12: + resolution: {integrity: sha512-FpU7/cRipZo4diSWQq7gZWVp+Px76CtVduJZNvQwVzynDsAIxKteMrjCCGPbM2oEasReoDffaeMCMlaur9ohIg==} + peerDependencies: + preact: '>=10' + + preact@10.25.2: + resolution: {integrity: sha512-GEts1EH3oMnqdOIeXhlbBSddZ9nrINd070WBOiPO2ous1orrKGUM4SMDbwyjSWD1iMS2dBvaDjAa5qUhz3TXqw==} + prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -2316,15 +2324,6 @@ packages: resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} engines: {node: '>=10'} - react-dom@19.0.0: - resolution: {integrity: sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==} - peerDependencies: - react: ^19.0.0 - - react@19.0.0: - resolution: {integrity: sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==} - engines: {node: '>=0.10.0'} - readdirp@3.6.0: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} @@ -2391,9 +2390,6 @@ packages: safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} - scheduler@0.25.0: - resolution: {integrity: sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==} - seek-bzip@2.0.0: resolution: {integrity: sha512-SMguiTnYrhpLdk3PwfzHeotrcwi8bNV4iemL9tx9poR/yeaMYwB9VzR1w7b57DuWpuqR8n6oZboi0hj3AxZxQg==} hasBin: true @@ -4875,6 +4871,12 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + preact-render-to-string@6.5.12(preact@10.25.2): + dependencies: + preact: 10.25.2 + + preact@10.25.2: {} + prelude-ls@1.2.1: {} prettier-linter-helpers@1.0.0: @@ -4893,13 +4895,6 @@ snapshots: quick-lru@5.1.1: {} - react-dom@19.0.0(react@19.0.0): - dependencies: - react: 19.0.0 - scheduler: 0.25.0 - - react@19.0.0: {} - readdirp@3.6.0: dependencies: picomatch: 2.3.1 @@ -4974,8 +4969,6 @@ snapshots: safe-buffer@5.2.1: {} - scheduler@0.25.0: {} - seek-bzip@2.0.0: dependencies: commander: 6.2.1 diff --git a/src/components/Step.ts b/src/components/Step.ts deleted file mode 100644 index f74c1d7e..00000000 --- a/src/components/Step.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { ExecutionContext } from "../context/ExecutionContext"; - -export interface Step { - execute(context: ExecutionContext): Promise; -} diff --git a/src/components/Workflow.tsx b/src/components/Workflow.tsx deleted file mode 100644 index e578cfef..00000000 --- a/src/components/Workflow.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import React from "react"; - -import { ExecutionContext } from "../context/ExecutionContext"; -import { renderWorkflow } from "../utils/renderWorkflow"; -import { Step } from "./Step"; - -export function Workflow({ children }: { children: React.ReactNode }) { - return React.createElement(React.Fragment, null, children); -} - -export class WorkflowContext { - static current: WorkflowContext | null = null; - private executionQueue = new Set(); - private steps: Step[] = []; - private dynamicSteps = new Map(); - private executedSteps = new Set(); - - constructor(workflow: React.ReactElement) { - const wrappedWorkflow = React.createElement(React.Fragment, null, workflow); - this.steps = renderWorkflow(wrappedWorkflow); - } - - notifyUpdate(componentId: string) { - if (!this.executedSteps.has(componentId)) { - this.executionQueue.add(componentId); - } - } - - private async executeStep(step: Step, stepId: string): Promise { - if (this.executedSteps.has(stepId)) { - return; - } - - const context = new ExecutionContext(); - const childSteps = await step.execute(context); - this.executedSteps.add(stepId); - - if (childSteps.length > 0) { - this.dynamicSteps.set(stepId, childSteps); - - // Execute all child steps in parallel - await Promise.all( - childSteps.map((childStep, index) => { - const childId = `${stepId}_${index}`; - if (!this.executedSteps.has(childId)) { - return this.executeStep(childStep, childId); - } - return Promise.resolve(); - }), - ); - } - } - - async execute() { - WorkflowContext.current = this; - - try { - // Execute all initial steps in parallel - await Promise.all( - this.steps.map((step, index) => - this.executeStep(step, index.toString()), - ), - ); - - // Process any remaining steps in parallel - while (this.executionQueue.size > 0) { - const queuedIds = Array.from(this.executionQueue); - this.executionQueue.clear(); - - await Promise.all( - queuedIds.map(async id => { - const step = this.steps[parseInt(id)] as Step | undefined; - if (step) { - return this.executeStep(step, id); - } - - // Check dynamic steps - for (const [, steps] of this.dynamicSteps.entries()) { - const dynamicIndex = parseInt(id.split("_")[1]); - if (!isNaN(dynamicIndex) && steps[dynamicIndex]) { - return this.executeStep(steps[dynamicIndex], id); - } - } - }), - ); - } - } finally { - WorkflowContext.current = null; - } - } -} diff --git a/src/context/ExecutionContext.ts b/src/context/ExecutionContext.ts deleted file mode 100644 index 0ff87a2c..00000000 --- a/src/context/ExecutionContext.ts +++ /dev/null @@ -1,11 +0,0 @@ -export class ExecutionContext { - private refs: Record = {}; - - setRef(key: string, value: unknown): void { - this.refs[key] = value; - } - - getRef(key: string): unknown { - return this.refs[key]; - } -} diff --git a/src/context/StepContext.ts b/src/context/StepContext.ts deleted file mode 100644 index 052ad3b7..00000000 --- a/src/context/StepContext.ts +++ /dev/null @@ -1,9 +0,0 @@ -import React from "react"; - -import { Step } from "../components/Step"; - -export interface StepContextValue { - steps: Step[]; -} - -export const StepContext = React.createContext({ steps: [] }); diff --git a/src/context/workflow-context.ts b/src/context/workflow-context.ts deleted file mode 100644 index 7bd10ccd..00000000 --- a/src/context/workflow-context.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { createContext } from "react"; - -export interface WorkflowContext { - current: { - notifyUpdate: (componentId: string) => void; - getCurrentComponentId: () => string | null; - } | null; -} - -class WorkflowContextImpl implements WorkflowContext { - current: WorkflowContext["current"] = { - notifyUpdate(_componentId: string) { - // Implementation to handle component updates - // This might involve triggering re-renders or updating dependent components - }, - getCurrentComponentId() { - return null; - }, - }; -} - -export const WorkflowContext = createContext( - new WorkflowContextImpl(), -); diff --git a/src/execute.tsx b/src/execute.tsx new file mode 100644 index 00000000..0b28177e --- /dev/null +++ b/src/execute.tsx @@ -0,0 +1,61 @@ +import { ComponentChild } from "preact"; + +interface Component { + type: ( + props: Record, + ) => ComponentChild | Promise; + props: { + children?: unknown; + [key: string]: unknown; + }; +} + +function isComponent(node: unknown): node is Component { + return ( + typeof node === "object" && + node !== null && + "type" in node && + typeof (node as Component).type === "function" + ); +} + +type RenderPropChild = ( + value: T, +) => ComponentChild | Promise; + +async function resolveNode( + node: ComponentChild | Promise, +): Promise { + if (typeof node !== "object" || node === null) { + return node; + } + + const resolvedNode = await node; + + if (!isComponent(resolvedNode)) { + return resolvedNode; + } + + // Handle render props first + if (typeof resolvedNode.props.children === "function") { + const renderProp = resolvedNode.props.children as RenderPropChild; + const { children, ...props } = resolvedNode.props; + const result = await resolvedNode.type(props); + const resolvedResult = await resolveNode(result); + return resolveNode(renderProp(resolvedResult)); + } + + // Otherwise just execute the component + const result = await resolvedNode.type(resolvedNode.props); + return resolveNode(result); +} + +export async function executeJsxWorkflow( + node: ComponentChild, +): Promise { + const result = await resolveNode(node); + if (result === undefined || result === null || typeof result === "boolean") { + throw new Error("Workflow result is null, undefined, or boolean"); + } + return result as T; +} diff --git a/src/hooks/useWorkflowOutput.ts b/src/hooks/useWorkflowOutput.ts deleted file mode 100644 index d56b2a62..00000000 --- a/src/hooks/useWorkflowOutput.ts +++ /dev/null @@ -1,63 +0,0 @@ -type WorkflowOutput = Map< - string, - { - promise: Promise; - resolve: (value: T) => void; - hasResolved: boolean; - } ->; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export const workflowOutputs: WorkflowOutput = new Map(); - -let counter = 0; -function generateStableId() { - return `output_${counter++}`; -} - -export function createWorkflowOutput( - _initialValue: T, -): [Promise, (value: T) => void] { - const outputId = generateStableId(); - - if (!workflowOutputs.has(outputId)) { - let resolvePromise: (value: T) => void; - let rejectPromise: (error: unknown) => void; - const promise = new Promise((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, - }); - } - - const output = workflowOutputs.get(outputId)!; - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return [output.promise, output.resolve] as const; -} diff --git a/src/index.ts b/src/index.ts index 187f1763..497eafbf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,21 @@ -import { Workflow, WorkflowContext } from "./components/Workflow"; -import { createWorkflowOutput } from "./hooks/useWorkflowOutput"; -import { createWorkflow } from "./utils/workflowBuilder"; +/** +# Requirements -export { createWorkflow, createWorkflowOutput, Workflow, WorkflowContext }; +1. Type safety with mininal boilerplate from the user +2. Fork/join patterns (Collect). +3. Users always deal with plain types, and we handle promise resolution under the hood +4. Components are purely functional, and don't have the be aware of how they are used. +5. Keep track of inputs and outputs of each workflow step so that we can: + 1. Cache the outputs + 2. Render the workflow as a graph in some sort of UI that enables debugging, seeing inputs, outputs, etc. +6. Dynamic children composition pattern - outputs of a component made available as a lambda within it's children +7. Support parallel execution of steps (either dynamic via something liek a collector, or static via a few explicitly defined siblings) + */ + +import { executeJsxWorkflow } from "./execute"; +import { + withWorkflowComponent, + withWorkflowFunction, +} from "./withWorkflowStep"; + +export { executeJsxWorkflow, withWorkflowComponent, withWorkflowFunction }; diff --git a/src/registry/workflow-registry.ts b/src/registry/workflow-registry.ts deleted file mode 100644 index cc9bda50..00000000 --- a/src/registry/workflow-registry.ts +++ /dev/null @@ -1,14 +0,0 @@ -interface WorkflowDefinition { - id: string; - component: React.ReactElement; -} - -const workflowRegistry = new Map(); - -export function registerWorkflow(id: string, component: React.ReactElement) { - workflowRegistry.set(id, { id, component }); -} - -export function getWorkflow(id: string): WorkflowDefinition | undefined { - return workflowRegistry.get(id); -} diff --git a/src/utils/renderWorkflow.tsx b/src/utils/renderWorkflow.tsx deleted file mode 100644 index c56429f7..00000000 --- a/src/utils/renderWorkflow.tsx +++ /dev/null @@ -1,104 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unsafe-call */ -/* eslint-disable @typescript-eslint/no-unsafe-argument */ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ -/* eslint-disable @typescript-eslint/no-explicit-any */ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -import React from "react"; - -import { Step } from "../components/Step"; - -type FunctionComponent = (props: unknown) => React.ReactElement | null; - -// Keep track of workflows that have been processed to avoid double execution -const processedWorkflows = new Set(); - -export function renderWorkflow(element: React.ReactElement): Step[] { - processedWorkflows.clear(); - const steps: Step[] = []; - - function processElement(el: React.ReactNode): void { - if (!React.isValidElement(el)) { - return; - } - - // If it's a function component, execute it and process its result - const type = el.type as any; - - if (typeof type === "function" && !type.prototype?.render) { - // For workflow components, use getWorkflowResult - if (type.getWorkflowResult) { - // Generate a unique key for this workflow instance - const workflowKey = `${type.name}_${ - type.displayName || "" - }_${Object.entries(el.props as Record) - .map( - ([key, value]) => - `${key}:${value instanceof Promise ? "Promise" : (value as string)}`, - ) - .join("_")}`; - - if (!processedWorkflows.has(workflowKey)) { - processedWorkflows.add(workflowKey); - - // Create a step for this workflow component - const step: Step = { - async execute(context) { - const resolvedProps = {} as any; - // Resolve any promise props - for (const [key, value] of Object.entries( - el.props as Record, - )) { - resolvedProps[key] = - value instanceof Promise ? await value : value; - } - // Execute the workflow with resolved props - const result = await type.getWorkflowResult(resolvedProps); - - // Process any nested elements - if (result) { - const nestedSteps = renderWorkflow(result); - // Execute nested steps sequentially - for (const nestedStep of nestedSteps) { - await nestedStep.execute(context); - } - } - - return []; - }, - }; - - steps.push(step); - } - } else { - // For regular components, execute them directly - const Component = type as FunctionComponent; - const result = Component(el.props); - if (result) { - processElement(result); - } - } - return; - } - - // Check if this is a WorkflowStep - const props = el.props as Record; - if (props["data-workflow-step"]) { - steps.push(props.step); - return; - } - - // Process children - if (props.children) { - if (Array.isArray(props.children)) { - props.children.forEach((child: React.ReactNode) => { - processElement(child); - }); - } else { - processElement(props.children); - } - } - } - - processElement(element); - return steps; -} diff --git a/src/utils/workflowBuilder.ts b/src/utils/workflowBuilder.ts deleted file mode 100644 index f390eceb..00000000 --- a/src/utils/workflowBuilder.ts +++ /dev/null @@ -1,157 +0,0 @@ -import React from "react"; - -import { Step } from "../components/Step"; -import { createWorkflowOutput } from "../hooks/useWorkflowOutput"; -import { renderWorkflow } from "../utils/renderWorkflow"; - -type WorkflowRenderFunction = (value: T) => React.ReactElement | null; - -type WorkflowImplementation = ( - props: ResolvedProps, - render: WorkflowRenderFunction, -) => - | React.ReactElement - | Promise - | null - | Promise; - -type WorkflowComponentProps = TProps & { - children?: (output: TOutput) => React.ReactNode; - setOutput?: (value: TOutput) => void; -}; - -// Type to convert a props type to allow promises -type PromiseProps = { - [K in keyof TProps]: TProps[K] | Promise; -}; - -// Type to ensure implementation gets resolved props -type ResolvedProps = { - [K in keyof TProps]: TProps[K] extends Promise ? U : TProps[K]; -}; - -// This function resolves value in a promise. -// You can await a promise or a plain value and the effect is the same. -// Even though this function might seem unnecessary, using it makes our intent more clear. -async function resolveValue(value: T | Promise): Promise { - return await value; -} - -// Keep track of processed results to prevent infinite loops -const processedResults = new Set(); - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function createWorkflow, TOutput>( - implementation: WorkflowImplementation, -): React.ComponentType, TOutput>> { - const WorkflowComponent = ( - props: WorkflowComponentProps, TOutput>, - ): React.ReactElement | null => { - const { children, setOutput, ...componentProps } = props; - const [, setWorkflowOutput] = createWorkflowOutput( - null as unknown as TOutput, - ); - - const step: Step = { - async execute(context) { - try { - // Resolve all props in parallel - const resolvedProps = {} as ResolvedProps; - await Promise.all( - Object.entries(componentProps).map(async ([key, value]) => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - resolvedProps[key as keyof TProps] = await resolveValue(value); - }), - ); - - // Create render function that sets output and returns element - const render: WorkflowRenderFunction = value => { - setWorkflowOutput(value); - if (setOutput) { - setOutput(value); - } - if (children) { - return children(value) as React.ReactElement; - } - return null; - }; - - // Get the workflow result with resolved props - const element = await Promise.resolve( - implementation(resolvedProps, render), - ); - - // Process the element chain - if (element) { - const elementSteps = renderWorkflow(element); - // Execute steps sequentially to ensure proper chaining - for (const step of elementSteps) { - await step.execute(context); - } - } - - return []; - } catch (error) { - console.error("Error in workflow step:", error); - throw error; - } - }, - }; - - return React.createElement("div", { - "data-workflow-step": true, - step, - }); - }; - - // For execution phase, we need a way to get the workflow result without React - WorkflowComponent.getWorkflowResult = async ( - props: WorkflowComponentProps, TOutput>, - ): Promise => { - const { children, setOutput, ...componentProps } = props; - - // Generate a unique key for this result - const resultKey = JSON.stringify(componentProps); - if (processedResults.has(resultKey)) { - return null; - } - processedResults.add(resultKey); - - const [, setWorkflowOutput] = createWorkflowOutput( - null as unknown as TOutput, - ); - - try { - // Resolve all props before passing to implementation - const resolvedProps = {} as ResolvedProps; - for (const [key, value] of Object.entries(componentProps)) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - resolvedProps[key as keyof TProps] = await resolveValue(value); - } - - // Create render function that sets output and returns element - const render: WorkflowRenderFunction = value => { - setWorkflowOutput(value); - if (setOutput) { - setOutput(value); - } - if (children) { - return children(value) as React.ReactElement; - } - return null; - }; - - // Get the workflow result - const implementationResult = await implementation(resolvedProps, render); - return implementationResult; - } catch (error) { - console.error("Error in getWorkflowResult:", error); - throw error; - } finally { - processedResults.delete(resultKey); - } - }; - - WorkflowComponent.displayName = "Workflow"; - return WorkflowComponent; -} diff --git a/src/withWorkflowStep.tsx b/src/withWorkflowStep.tsx new file mode 100644 index 00000000..67b5f083 --- /dev/null +++ b/src/withWorkflowStep.tsx @@ -0,0 +1,38 @@ +import { ComponentChild } from "preact"; + +import { executeJsxWorkflow } from "./execute"; + +type MaybePromise = T | Promise; + +// For wrapping pure functions into components +export function withWorkflowFunction( + fn: (input: TInput) => MaybePromise, +) { + return ({ + children, + ...input + }: TInput & { children?: (output: TOutput) => ComponentChild }): Promise< + ComponentChild | TOutput + > => { + const promise = fn(input as TInput); + return Promise.resolve(promise).then((result: TOutput) => + children ? children(result) : result, + ); + }; +} + +// For wrapping JSX trees that need execution +export function withWorkflowComponent( + component: (input: TInput) => ComponentChild, +) { + return async ({ + children, + ...input + }: TInput & { children?: (output: TOutput) => ComponentChild }): Promise< + ComponentChild | TOutput + > => { + const element = component(input as TInput); + const result = await executeJsxWorkflow(element); + return children ? children(result) : result; + }; +} diff --git a/tsconfig.json b/tsconfig.json index 314e65ee..c12db851 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,6 +16,7 @@ "resolveJsonModule": true, "declaration": true, "jsx": "react-jsx", + "jsxImportSource": "preact", "paths": { "@/src/*": [ "src/*" From 5612543a6a2fb4212b998bafd49abdc0ff3482cf Mon Sep 17 00:00:00 2001 From: Jeremy Moseley Date: Mon, 16 Dec 2024 17:43:25 -0800 Subject: [PATCH 02/14] Collect --- playground/index.tsx | 24 +++--------------------- src/components/Collect.tsx | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+), 21 deletions(-) create mode 100644 src/components/Collect.tsx diff --git a/playground/index.tsx b/playground/index.tsx index 7a69b0d1..9f4bbbcc 100644 --- a/playground/index.tsx +++ b/playground/index.tsx @@ -1,28 +1,10 @@ -import { ComponentChild } from "preact"; - +import { Collect } from "@/src/components/Collect"; import { executeJsxWorkflow, withWorkflowComponent, withWorkflowFunction, } from "@/src/index"; -// Parallel execution component -const Collect = async ({ - children, -}: { - children: ComponentChild[]; -}): Promise => { - console.log("📦 Collect input:", children); - const results = await Promise.all( - (Array.isArray(children) ? children : [children]).map(child => { - console.log("📦 Collect executing child:", child); - return executeJsxWorkflow(child); - }), - ); - console.log("📦 Collect results:", results); - return results; -}; - // Pure workflow steps const pureResearchBrainstorm = async ({ prompt }: { prompt: string }) => { console.log("🔍 Starting research for:", prompt); @@ -58,8 +40,8 @@ const LLMWriter = withWorkflowFunction(pureWriter); const LLMEditor = withWorkflowFunction(pureEditor); // Research collection component -const ResearchCollection = withWorkflowComponent<{ prompt: string }, string[]>( - ({ prompt }) => ( +const ResearchCollection = withWorkflowComponent( + ({ prompt }: { prompt: string }) => ( {topics => ( diff --git a/src/components/Collect.tsx b/src/components/Collect.tsx new file mode 100644 index 00000000..ff01c7e9 --- /dev/null +++ b/src/components/Collect.tsx @@ -0,0 +1,18 @@ +import { ComponentChild } from "preact"; + +import { executeJsxWorkflow } from "../execute"; + +export interface CollectProps { + children: (ComponentChild | Promise)[]; +} + +export function Collect( + props: CollectProps, +): Promise & { __output: T[] } { + const promise = Promise.all( + props.children.map(child => + child instanceof Promise ? child : executeJsxWorkflow(child), + ), + ); + return Object.assign(promise, { __output: [] as T[] }); +} From cd9849143c2516333700f05bdbe2791f5d4cd4f2 Mon Sep 17 00:00:00 2001 From: Jeremy Moseley Date: Tue, 17 Dec 2024 13:31:31 -0800 Subject: [PATCH 03/14] SAVEPOINT --- package.json | 9 ++--- playground/index.tsx | 29 ++++++++------- pnpm-lock.yaml | 32 ---------------- src/components/Collect.tsx | 2 +- src/execute.tsx | 61 ------------------------------- src/index.ts | 4 +- src/jsx-runtime.ts | 33 +++++++++++++++++ src/withWorkflowStep.tsx | 38 ------------------- src/workflow/execute.tsx | 16 ++++++++ src/workflow/withWorkflowStep.tsx | 36 ++++++++++++++++++ tsconfig.json | 28 +++++++++----- 11 files changed, 127 insertions(+), 161 deletions(-) delete mode 100644 src/execute.tsx create mode 100644 src/jsx-runtime.ts delete mode 100644 src/withWorkflowStep.tsx create mode 100644 src/workflow/execute.tsx create mode 100644 src/workflow/withWorkflowStep.tsx diff --git a/package.json b/package.json index c6855ec0..5a6ab045 100644 --- a/package.json +++ b/package.json @@ -43,9 +43,7 @@ "prepare": "[ -f .husky/install.mjs ] && node .husky/install.mjs || true" }, "dependencies": { - "openai": "^4.76.0", - "preact": "^10.25.2", - "preact-render-to-string": "^6.5.12" + "openai": "^4.76.0" }, "devDependencies": { "@swc/cli": "^0.5.2", @@ -53,7 +51,6 @@ "@types/fs-extra": "^11.0.4", "@types/node": "^22.10.2", "@types/react": "^19.0.1", - "@types/react-dom": "^18.3.2", "@typescript-eslint/eslint-plugin": "^8.18.1", "@typescript-eslint/parser": "^8.18.1", "@vitest/coverage-istanbul": "^2.1.8", @@ -94,6 +91,8 @@ "types": "./dist/index.d.cts", "default": "./dist/index.cjs" } - } + }, + "./jsx-runtime": "./dist/jsx-runtime.js" } } + diff --git a/playground/index.tsx b/playground/index.tsx index 9f4bbbcc..2141e0ae 100644 --- a/playground/index.tsx +++ b/playground/index.tsx @@ -1,9 +1,9 @@ -import { Collect } from "@/src/components/Collect"; +import { Collect } from "@/components/Collect"; import { executeJsxWorkflow, withWorkflowComponent, withWorkflowFunction, -} from "@/src/index"; +} from "@/index"; // Pure workflow steps const pureResearchBrainstorm = async ({ prompt }: { prompt: string }) => { @@ -12,11 +12,6 @@ const pureResearchBrainstorm = async ({ prompt }: { prompt: string }) => { return topics; }; -const pureResearch = async ({ topic }: { topic: string }) => { - console.log("📚 Researching topic:", topic); - return await Promise.resolve(`research results for ${topic}`); -}; - const pureWriter = async ({ research, prompt, @@ -35,7 +30,12 @@ const pureEditor = async ({ draft }: { draft: string }) => { // Wrapped workflow steps const LLMResearchBrainstorm = withWorkflowFunction(pureResearchBrainstorm); -const LLMResearch = withWorkflowFunction(pureResearch); +const LLMResearch = withWorkflowFunction( + async ({ topic }: { topic: string }) => { + console.log("📚 Researching topic:", topic); + return await Promise.resolve(`research results for ${topic}`); + }, +); const LLMWriter = withWorkflowFunction(pureWriter); const LLMEditor = withWorkflowFunction(pureEditor); @@ -54,8 +54,8 @@ const ResearchCollection = withWorkflowComponent( ), ); -const BlogWritingWorkflow = ({ prompt }: { prompt: string }) => { - return ( +const BlogWritingWorkflow = withWorkflowComponent( + ({ prompt }: { prompt: string }) => ( {research => { console.log("🧠 Research:", research, typeof research); @@ -66,13 +66,16 @@ const BlogWritingWorkflow = ({ prompt }: { prompt: string }) => { ); }} - ); -}; + ), +); async function main() { console.log("🚀 Starting blog writing workflow"); + // const comp = jsx(LLMResearch, { + // topic: "Write a blog post about the future of AI", + // }); const result = await executeJsxWorkflow( - , + , ); console.log("✅ Final result:", result); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a67ba48a..6e3de727 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,12 +11,6 @@ importers: openai: specifier: ^4.76.0 version: 4.76.3 - preact: - specifier: ^10.25.2 - version: 10.25.2 - preact-render-to-string: - specifier: ^6.5.12 - version: 6.5.12(preact@10.25.2) optionalDependencies: '@rollup/rollup-linux-x64-gnu': specifier: ^4.28.1 @@ -40,9 +34,6 @@ importers: '@types/react': specifier: ^19.0.1 version: 19.0.1 - '@types/react-dom': - specifier: ^18.3.2 - version: 18.3.5(@types/react@19.0.1) '@typescript-eslint/eslint-plugin': specifier: ^8.18.1 version: 8.18.1(@typescript-eslint/parser@8.18.1(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1)(typescript@5.7.2) @@ -1007,11 +998,6 @@ packages: '@types/node@22.10.2': resolution: {integrity: sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==} - '@types/react-dom@18.3.5': - resolution: {integrity: sha512-P4t6saawp+b/dFrUr2cvkVsfvPguwsxtH6dNIYRllMsefqFzkZk5UIjzyDOv5g1dXIPdG4Sp1yCR4Z6RCUsG/Q==} - peerDependencies: - '@types/react': ^18.0.0 - '@types/react@19.0.1': resolution: {integrity: sha512-YW6614BDhqbpR5KtUYzTA+zlA7nayzJRA9ljz9CQoxthR0sDisYZLuvSMsil36t4EH/uAt8T52Xb4sVw17G+SQ==} @@ -2286,14 +2272,6 @@ packages: resolution: {integrity: sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==} engines: {node: ^10 || ^12 || >=14} - preact-render-to-string@6.5.12: - resolution: {integrity: sha512-FpU7/cRipZo4diSWQq7gZWVp+Px76CtVduJZNvQwVzynDsAIxKteMrjCCGPbM2oEasReoDffaeMCMlaur9ohIg==} - peerDependencies: - preact: '>=10' - - preact@10.25.2: - resolution: {integrity: sha512-GEts1EH3oMnqdOIeXhlbBSddZ9nrINd070WBOiPO2ous1orrKGUM4SMDbwyjSWD1iMS2dBvaDjAa5qUhz3TXqw==} - prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -3499,10 +3477,6 @@ snapshots: dependencies: undici-types: 6.20.0 - '@types/react-dom@18.3.5(@types/react@19.0.1)': - dependencies: - '@types/react': 19.0.1 - '@types/react@19.0.1': dependencies: csstype: 3.1.3 @@ -4871,12 +4845,6 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 - preact-render-to-string@6.5.12(preact@10.25.2): - dependencies: - preact: 10.25.2 - - preact@10.25.2: {} - prelude-ls@1.2.1: {} prettier-linter-helpers@1.0.0: diff --git a/src/components/Collect.tsx b/src/components/Collect.tsx index ff01c7e9..6f626058 100644 --- a/src/components/Collect.tsx +++ b/src/components/Collect.tsx @@ -1,6 +1,6 @@ import { ComponentChild } from "preact"; -import { executeJsxWorkflow } from "../execute"; +import { executeJsxWorkflow } from "../workflow/execute"; export interface CollectProps { children: (ComponentChild | Promise)[]; diff --git a/src/execute.tsx b/src/execute.tsx deleted file mode 100644 index 0b28177e..00000000 --- a/src/execute.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { ComponentChild } from "preact"; - -interface Component { - type: ( - props: Record, - ) => ComponentChild | Promise; - props: { - children?: unknown; - [key: string]: unknown; - }; -} - -function isComponent(node: unknown): node is Component { - return ( - typeof node === "object" && - node !== null && - "type" in node && - typeof (node as Component).type === "function" - ); -} - -type RenderPropChild = ( - value: T, -) => ComponentChild | Promise; - -async function resolveNode( - node: ComponentChild | Promise, -): Promise { - if (typeof node !== "object" || node === null) { - return node; - } - - const resolvedNode = await node; - - if (!isComponent(resolvedNode)) { - return resolvedNode; - } - - // Handle render props first - if (typeof resolvedNode.props.children === "function") { - const renderProp = resolvedNode.props.children as RenderPropChild; - const { children, ...props } = resolvedNode.props; - const result = await resolvedNode.type(props); - const resolvedResult = await resolveNode(result); - return resolveNode(renderProp(resolvedResult)); - } - - // Otherwise just execute the component - const result = await resolvedNode.type(resolvedNode.props); - return resolveNode(result); -} - -export async function executeJsxWorkflow( - node: ComponentChild, -): Promise { - const result = await resolveNode(node); - if (result === undefined || result === null || typeof result === "boolean") { - throw new Error("Workflow result is null, undefined, or boolean"); - } - return result as T; -} diff --git a/src/index.ts b/src/index.ts index 497eafbf..9a3875bb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,10 +12,10 @@ 7. Support parallel execution of steps (either dynamic via something liek a collector, or static via a few explicitly defined siblings) */ -import { executeJsxWorkflow } from "./execute"; +import { executeJsxWorkflow } from "./workflow/execute"; import { withWorkflowComponent, withWorkflowFunction, -} from "./withWorkflowStep"; +} from "./workflow/withWorkflowStep"; export { executeJsxWorkflow, withWorkflowComponent, withWorkflowFunction }; diff --git a/src/jsx-runtime.ts b/src/jsx-runtime.ts new file mode 100644 index 00000000..0d16265e --- /dev/null +++ b/src/jsx-runtime.ts @@ -0,0 +1,33 @@ +/* eslint-disable @typescript-eslint/no-namespace */ +type Primitive = string | number | boolean | null | undefined; // TODO: Add array and object + +declare global { + namespace JSX { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + export type ElementType = (props: any) => Primitive | Element; + // eslint-disable-next-line @typescript-eslint/no-empty-object-type + interface IntrinsicElements {} + export type Element = Component; + interface ElementChildrenAttribute { + // eslint-disable-next-line @typescript-eslint/no-empty-object-type + handleOutput: {}; // specify what the name of the children prop is + } + } +} + +export type MaybePromise = T | Promise; + +export interface Component { + type: (props: TProps) => MaybePromise; + props: TProps; +} + +export const jsx = ( + component: (props: TProps) => MaybePromise, + props: TProps | null, +): JSX.Element => { + return { + type: component, + props: props ?? ({} as TProps), + }; +}; diff --git a/src/withWorkflowStep.tsx b/src/withWorkflowStep.tsx deleted file mode 100644 index 67b5f083..00000000 --- a/src/withWorkflowStep.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { ComponentChild } from "preact"; - -import { executeJsxWorkflow } from "./execute"; - -type MaybePromise = T | Promise; - -// For wrapping pure functions into components -export function withWorkflowFunction( - fn: (input: TInput) => MaybePromise, -) { - return ({ - children, - ...input - }: TInput & { children?: (output: TOutput) => ComponentChild }): Promise< - ComponentChild | TOutput - > => { - const promise = fn(input as TInput); - return Promise.resolve(promise).then((result: TOutput) => - children ? children(result) : result, - ); - }; -} - -// For wrapping JSX trees that need execution -export function withWorkflowComponent( - component: (input: TInput) => ComponentChild, -) { - return async ({ - children, - ...input - }: TInput & { children?: (output: TOutput) => ComponentChild }): Promise< - ComponentChild | TOutput - > => { - const element = component(input as TInput); - const result = await executeJsxWorkflow(element); - return children ? children(result) : result; - }; -} diff --git a/src/workflow/execute.tsx b/src/workflow/execute.tsx new file mode 100644 index 00000000..2ecf3ce6 --- /dev/null +++ b/src/workflow/execute.tsx @@ -0,0 +1,16 @@ +import { Component } from "../jsx-runtime"; + +export type { Component }; + +export async function executeJsxWorkflow( + node: Component, +): Promise { + console.log("executeJsxWorkflow", node); + const result = await node.type(node.props); + if (typeof result === "object" && result !== null && "type" in result) { + // If we got back another component, execute it + return executeJsxWorkflow(result as any); + } + console.log("result", result); + return result; +} diff --git a/src/workflow/withWorkflowStep.tsx b/src/workflow/withWorkflowStep.tsx new file mode 100644 index 00000000..7be49de1 --- /dev/null +++ b/src/workflow/withWorkflowStep.tsx @@ -0,0 +1,36 @@ +import { Component, MaybePromise } from "../jsx-runtime"; +import { executeJsxWorkflow } from "./execute"; + +export function withWorkflowFunction< + TInput extends Record, + TOutput, +>(fn: (input: TInput) => MaybePromise) { + function WorkflowFunction(props: TInput): Component { + return { + type: fn, + props, + }; + } + return WorkflowFunction; +} + +// For wrapping JSX trees that need execution +export function withWorkflowComponent< + TInput extends Record, + TOutput, +>(component: Component) { + console.log("withWorkflowComponent", { component }); + async function WorkflowComponent({ + children, + ...input + }: TInput & { children?: (output: TOutput) => ChildOutput }): Promise< + ChildOutput | TOutput + > { + console.log("withWorkflowComponent call ", { component, input, children }); + const element = component.type(input as TInput); + const result = await executeJsxWorkflow(element); + console.log("result", result); + return children ? children(result) : result; + } + return WorkflowComponent; +} diff --git a/tsconfig.json b/tsconfig.json index c12db851..062d2069 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,24 +1,34 @@ { "$schema": "https://json.schemastore.org/tsconfig", "compilerOptions": { - "baseUrl": ".", - "module": "esnext", "target": "esnext", + "useDefineForClassFields": true, + "module": "esnext", + "lib": [ + "ESNext", + ], + "skipLibCheck": true, + /* Bundler mode */ "moduleResolution": "bundler", "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, "emitDeclarationOnly": true, - "esModuleInterop": true, - "strict": true, + "declaration": true, "outDir": "dist", "sourceMap": true, "inlineSources": true, - "skipLibCheck": true, - "resolveJsonModule": true, - "declaration": true, + "esModuleInterop": true, + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, "jsx": "react-jsx", - "jsxImportSource": "preact", + "jsxImportSource": "@", + "baseUrl": ".", "paths": { - "@/src/*": [ + "@/*": [ "src/*" ], "@/tests/*": [ From 60d2954a4bf9f6725c9dd94ee8c6daa3a8170d68 Mon Sep 17 00:00:00 2001 From: Jeremy Moseley Date: Tue, 17 Dec 2024 17:12:32 -0800 Subject: [PATCH 04/14] Strings, everything is strings --- playground/index.tsx | 18 +++++---- src/components/Collect.tsx | 21 +++++------ src/jsx-runtime.ts | 61 ++++++++++++++++++++++--------- src/workflow/execute.tsx | 18 ++------- src/workflow/withWorkflowStep.tsx | 42 +++++++++------------ 5 files changed, 87 insertions(+), 73 deletions(-) diff --git a/playground/index.tsx b/playground/index.tsx index 2141e0ae..67e11491 100644 --- a/playground/index.tsx +++ b/playground/index.tsx @@ -9,7 +9,7 @@ import { const pureResearchBrainstorm = async ({ prompt }: { prompt: string }) => { console.log("🔍 Starting research for:", prompt); const topics = await Promise.resolve(["topic 1", "topic 2", "topic 3"]); - return topics; + return topics.join(","); }; const pureWriter = async ({ @@ -45,7 +45,7 @@ const ResearchCollection = withWorkflowComponent( {topics => ( - {topics.map(topic => ( + {topics.split(",").map(topic => ( ))} @@ -60,7 +60,7 @@ const BlogWritingWorkflow = withWorkflowComponent( {research => { console.log("🧠 Research:", research, typeof research); return ( - + {draft => } ); @@ -71,11 +71,15 @@ const BlogWritingWorkflow = withWorkflowComponent( async function main() { console.log("🚀 Starting blog writing workflow"); - // const comp = jsx(LLMResearch, { - // topic: "Write a blog post about the future of AI", - // }); + // const comp = jsx( + // LLMResearch, + // { + // topic: "Write a blog post about the future of AI", + // }, + // output => , + // ); const result = await executeJsxWorkflow( - , + , ); console.log("✅ Final result:", result); } diff --git a/src/components/Collect.tsx b/src/components/Collect.tsx index 6f626058..6431a99d 100644 --- a/src/components/Collect.tsx +++ b/src/components/Collect.tsx @@ -1,18 +1,17 @@ -import { ComponentChild } from "preact"; - import { executeJsxWorkflow } from "../workflow/execute"; -export interface CollectProps { - children: (ComponentChild | Promise)[]; -} - -export function Collect( - props: CollectProps, -): Promise & { __output: T[] } { +export function Collect(props: { + children: string[] | Promise[]; +}): Promise { const promise = Promise.all( props.children.map(child => - child instanceof Promise ? child : executeJsxWorkflow(child), + child instanceof Promise + ? child + : executeJsxWorkflow(Promise.resolve(child)), ), ); - return Object.assign(promise, { __output: [] as T[] }); + return Promise.resolve(promise).then(result => { + console.log("Collect result", result); + return result.join("\n"); + }); } diff --git a/src/jsx-runtime.ts b/src/jsx-runtime.ts index 0d16265e..15279e20 100644 --- a/src/jsx-runtime.ts +++ b/src/jsx-runtime.ts @@ -1,33 +1,60 @@ /* eslint-disable @typescript-eslint/no-namespace */ -type Primitive = string | number | boolean | null | undefined; // TODO: Add array and object +// type Primitive = string | number | boolean | null | undefined; // TODO: Add array and object declare global { namespace JSX { // eslint-disable-next-line @typescript-eslint/no-explicit-any - export type ElementType = (props: any) => Primitive | Element; + export type ElementType = (props: any) => Promise; // eslint-disable-next-line @typescript-eslint/no-empty-object-type interface IntrinsicElements {} - export type Element = Component; - interface ElementChildrenAttribute { - // eslint-disable-next-line @typescript-eslint/no-empty-object-type - handleOutput: {}; // specify what the name of the children prop is + export type Element = Promise; + export interface ElementChildrenAttribute { + children: (output: string) => MaybePromise; } } } export type MaybePromise = T | Promise; -export interface Component { - type: (props: TProps) => MaybePromise; - props: TProps; -} +// export interface Component { +// type: (props: TProps) => MaybePromise; +// props: TProps; +// } -export const jsx = ( - component: (props: TProps) => MaybePromise, +export const jsx = < + TProps extends Record & { + children?: (output: string) => MaybePromise; + }, +>( + component: (props: TProps) => MaybePromise, props: TProps | null, -): JSX.Element => { - return { - type: component, - props: props ?? ({} as TProps), - }; + children?: + | ((output: string) => MaybePromise) + | ((output: string) => MaybePromise)[], +): Promise => { + console.log("jsx", { component, props, children }); + if (!children && props?.children) { + children = props.children; + } + return Promise.resolve(component(props ?? ({} as TProps))).then(result => { + console.log("jsx result", { result, children }); + if (children) { + if (Array.isArray(children)) { + console.log("jsx children is array", { children }); + return Promise.all( + children.map(child => { + if (child instanceof Function) { + return child(result); + } + return child; + }), + ).then(result => { + console.log("jsx children is array result", { result }); + return result.join("\n"); + }); + } + return children(result); + } + return result; + }); }; diff --git a/src/workflow/execute.tsx b/src/workflow/execute.tsx index 2ecf3ce6..4a61bfbd 100644 --- a/src/workflow/execute.tsx +++ b/src/workflow/execute.tsx @@ -1,16 +1,6 @@ -import { Component } from "../jsx-runtime"; - -export type { Component }; - -export async function executeJsxWorkflow( - node: Component, -): Promise { +export async function executeJsxWorkflow( + node: Promise, +): Promise { console.log("executeJsxWorkflow", node); - const result = await node.type(node.props); - if (typeof result === "object" && result !== null && "type" in result) { - // If we got back another component, execute it - return executeJsxWorkflow(result as any); - } - console.log("result", result); - return result; + return await node; } diff --git a/src/workflow/withWorkflowStep.tsx b/src/workflow/withWorkflowStep.tsx index 7be49de1..23e392f5 100644 --- a/src/workflow/withWorkflowStep.tsx +++ b/src/workflow/withWorkflowStep.tsx @@ -1,36 +1,30 @@ -import { Component, MaybePromise } from "../jsx-runtime"; +import { MaybePromise } from "../jsx-runtime"; import { executeJsxWorkflow } from "./execute"; -export function withWorkflowFunction< - TInput extends Record, - TOutput, ->(fn: (input: TInput) => MaybePromise) { - function WorkflowFunction(props: TInput): Component { - return { - type: fn, - props, - }; +export function withWorkflowFunction>( + fn: (input: TInput) => MaybePromise, +) { + function WorkflowFunction( + props: TInput & { children?: (output: string) => MaybePromise }, + ): Promise { + return Promise.resolve(fn(props)); } return WorkflowFunction; } // For wrapping JSX trees that need execution -export function withWorkflowComponent< - TInput extends Record, - TOutput, ->(component: Component) { +export function withWorkflowComponent>( + component: (input: TInput) => MaybePromise, +) { console.log("withWorkflowComponent", { component }); - async function WorkflowComponent({ - children, - ...input - }: TInput & { children?: (output: TOutput) => ChildOutput }): Promise< - ChildOutput | TOutput - > { - console.log("withWorkflowComponent call ", { component, input, children }); - const element = component.type(input as TInput); + async function WorkflowComponent( + input: TInput & { children?: (output: string) => MaybePromise }, + ): Promise { + console.log("withWorkflowComponent call ", { component, input }); + const element = Promise.resolve(component(input)); const result = await executeJsxWorkflow(element); - console.log("result", result); - return children ? children(result) : result; + console.log("withWorkflowComponent result", result); + return result; } return WorkflowComponent; } From 69cc95533218d5b7597825ae18ab6e90c448c94b Mon Sep 17 00:00:00 2001 From: Jeremy Moseley Date: Wed, 18 Dec 2024 10:08:11 -0800 Subject: [PATCH 05/14] Use type annotations to pass around some type safety. --- playground/index.tsx | 50 ++++++++++++++------------ src/components/Collect.tsx | 9 ++--- src/jsx-runtime.ts | 59 +++++++++++++++++++++++-------- src/workflow/execute.tsx | 8 ++--- src/workflow/withWorkflowStep.tsx | 28 +++++++++------ 5 files changed, 99 insertions(+), 55 deletions(-) diff --git a/playground/index.tsx b/playground/index.tsx index 67e11491..7a529b37 100644 --- a/playground/index.tsx +++ b/playground/index.tsx @@ -1,4 +1,3 @@ -import { Collect } from "@/components/Collect"; import { executeJsxWorkflow, withWorkflowComponent, @@ -9,7 +8,7 @@ import { const pureResearchBrainstorm = async ({ prompt }: { prompt: string }) => { console.log("🔍 Starting research for:", prompt); const topics = await Promise.resolve(["topic 1", "topic 2", "topic 3"]); - return topics.join(","); + return topics; }; const pureWriter = async ({ @@ -38,29 +37,44 @@ const LLMResearch = withWorkflowFunction( ); const LLMWriter = withWorkflowFunction(pureWriter); const LLMEditor = withWorkflowFunction(pureEditor); +const WebResearcher = withWorkflowFunction( + async ({ prompt }: { prompt: string }) => { + console.log("🌐 Researching web for:", prompt); + const results = await Promise.resolve([ + "web result 1", + "web result 2", + "web result 3", + ]); + return results; + }, +); // Research collection component -const ResearchCollection = withWorkflowComponent( - ({ prompt }: { prompt: string }) => ( +const ResearchCollection = withWorkflowComponent< + [string[], string[]], + { prompt: string } +>(({ prompt }: { prompt: string }) => ( + <> {topics => ( - - {topics.split(",").map(topic => ( + <> + {topics.map(topic => ( ))} - + )} - ), -); + + +)); -const BlogWritingWorkflow = withWorkflowComponent( +const BlogWritingWorkflow = withWorkflowComponent( ({ prompt }: { prompt: string }) => ( - {research => { - console.log("🧠 Research:", research, typeof research); + {([catalogResearch, webResearch]) => { + console.log("🧠 Research:", { catalogResearch, webResearch }); return ( - + {draft => } ); @@ -68,17 +82,9 @@ const BlogWritingWorkflow = withWorkflowComponent( ), ); - async function main() { console.log("🚀 Starting blog writing workflow"); - // const comp = jsx( - // LLMResearch, - // { - // topic: "Write a blog post about the future of AI", - // }, - // output => , - // ); - const result = await executeJsxWorkflow( + const result = await executeJsxWorkflow( , ); console.log("✅ Final result:", result); diff --git a/src/components/Collect.tsx b/src/components/Collect.tsx index 6431a99d..32b7a953 100644 --- a/src/components/Collect.tsx +++ b/src/components/Collect.tsx @@ -1,8 +1,9 @@ +import { MaybePromise } from "../jsx-runtime"; import { executeJsxWorkflow } from "../workflow/execute"; -export function Collect(props: { - children: string[] | Promise[]; -}): Promise { +export function Collect(props: { + children: MaybePromise[]; +}): Promise { const promise = Promise.all( props.children.map(child => child instanceof Promise @@ -13,5 +14,5 @@ export function Collect(props: { return Promise.resolve(promise).then(result => { console.log("Collect result", result); return result.join("\n"); - }); + }) as Promise; } diff --git a/src/jsx-runtime.ts b/src/jsx-runtime.ts index 15279e20..1405d332 100644 --- a/src/jsx-runtime.ts +++ b/src/jsx-runtime.ts @@ -1,37 +1,52 @@ /* eslint-disable @typescript-eslint/no-namespace */ -// type Primitive = string | number | boolean | null | undefined; // TODO: Add array and object - declare global { namespace JSX { // eslint-disable-next-line @typescript-eslint/no-explicit-any - export type ElementType = (props: any) => Promise; + export type ElementType = (props: any) => Promise; // eslint-disable-next-line @typescript-eslint/no-empty-object-type interface IntrinsicElements {} - export type Element = Promise; + export type Element = Promise; export interface ElementChildrenAttribute { - children: (output: string) => MaybePromise; + children: (output: unknown) => MaybePromise; } } } export type MaybePromise = T | Promise; +export type Child = JSX.Element | ((output: T) => JSX.Element); +export type Children = Child | Child[]; + +export const Fragment = (props: { children: Children }): Promise => { + if (Array.isArray(props.children)) { + return Promise.all( + props.children.map(child => { + if (child instanceof Function) { + return child(null); + } + return child; + }), + ); + } + + return Promise.all([props.children]); +}; + // export interface Component { // type: (props: TProps) => MaybePromise; // props: TProps; // } export const jsx = < + TOutput, TProps extends Record & { - children?: (output: string) => MaybePromise; + children?: Children; }, >( - component: (props: TProps) => MaybePromise, + component: (props: TProps) => MaybePromise, props: TProps | null, - children?: - | ((output: string) => MaybePromise) - | ((output: string) => MaybePromise)[], -): Promise => { + children?: Children, +): Promise => { console.log("jsx", { component, props, children }); if (!children && props?.children) { children = props.children; @@ -50,11 +65,27 @@ export const jsx = < }), ).then(result => { console.log("jsx children is array result", { result }); - return result.join("\n"); + return result as TOutput[]; }); } - return children(result); + if (children instanceof Function) { + return children(result) as TOutput; + } + return children as TOutput; } - return result; + return result as TOutput; }); }; + +export const jsxs = < + TOutput, + TProps extends Record & { + children?: Children; + }, +>( + component: (props: TProps) => MaybePromise, + props: TProps | null, + children?: Children, +): Promise => { + return jsx(component, props, children); +}; diff --git a/src/workflow/execute.tsx b/src/workflow/execute.tsx index 4a61bfbd..cd817f38 100644 --- a/src/workflow/execute.tsx +++ b/src/workflow/execute.tsx @@ -1,6 +1,6 @@ -export async function executeJsxWorkflow( - node: Promise, -): Promise { +export async function executeJsxWorkflow( + node: JSX.Element, +): Promise { console.log("executeJsxWorkflow", node); - return await node; + return (await node) as TOutput; } diff --git a/src/workflow/withWorkflowStep.tsx b/src/workflow/withWorkflowStep.tsx index 23e392f5..d619d465 100644 --- a/src/workflow/withWorkflowStep.tsx +++ b/src/workflow/withWorkflowStep.tsx @@ -1,30 +1,36 @@ import { MaybePromise } from "../jsx-runtime"; import { executeJsxWorkflow } from "./execute"; -export function withWorkflowFunction>( - fn: (input: TInput) => MaybePromise, -) { +export function withWorkflowFunction< + TOutput, + TInput extends Record = Record, +>(fn: (input: TInput) => MaybePromise) { function WorkflowFunction( - props: TInput & { children?: (output: string) => MaybePromise }, - ): Promise { + props: TInput & { + children?: (output: TOutput) => MaybePromise | JSX.Element; + }, + ): Promise { return Promise.resolve(fn(props)); } return WorkflowFunction; } // For wrapping JSX trees that need execution -export function withWorkflowComponent>( - component: (input: TInput) => MaybePromise, -) { +export function withWorkflowComponent< + TOutput, + TInput extends Record, +>(component: (input: TInput) => MaybePromise | JSX.Element) { console.log("withWorkflowComponent", { component }); async function WorkflowComponent( - input: TInput & { children?: (output: string) => MaybePromise }, - ): Promise { + input: TInput & { + children?: (output: TOutput) => MaybePromise | JSX.Element; + }, + ): Promise { console.log("withWorkflowComponent call ", { component, input }); const element = Promise.resolve(component(input)); const result = await executeJsxWorkflow(element); console.log("withWorkflowComponent result", result); - return result; + return result as TOutput; } return WorkflowComponent; } From 86d1607ab83eb6b8d503e2e70caab7014547dff8 Mon Sep 17 00:00:00 2001 From: Jeremy Moseley Date: Fri, 20 Dec 2024 09:22:23 -0800 Subject: [PATCH 06/14] Simplify the GenSX api. --- playground/index.tsx | 123 ++++++++++++++++-------------- src/component.ts | 14 ++++ src/components/Collect.tsx | 18 ----- src/gensx.ts | 3 + src/index.ts | 9 +-- src/jsx-runtime.ts | 4 - src/workflow/execute.tsx | 6 -- src/workflow/withWorkflowStep.tsx | 36 --------- 8 files changed, 86 insertions(+), 127 deletions(-) create mode 100644 src/component.ts delete mode 100644 src/components/Collect.tsx create mode 100644 src/gensx.ts delete mode 100644 src/workflow/execute.tsx delete mode 100644 src/workflow/withWorkflowStep.tsx diff --git a/playground/index.tsx b/playground/index.tsx index 7a529b37..0abe00e2 100644 --- a/playground/index.tsx +++ b/playground/index.tsx @@ -1,8 +1,4 @@ -import { - executeJsxWorkflow, - withWorkflowComponent, - withWorkflowFunction, -} from "@/index"; +import { Component, gensx } from "@/index"; // Pure workflow steps const pureResearchBrainstorm = async ({ prompt }: { prompt: string }) => { @@ -28,66 +24,79 @@ const pureEditor = async ({ draft }: { draft: string }) => { }; // Wrapped workflow steps -const LLMResearchBrainstorm = withWorkflowFunction(pureResearchBrainstorm); -const LLMResearch = withWorkflowFunction( - async ({ topic }: { topic: string }) => { - console.log("📚 Researching topic:", topic); - return await Promise.resolve(`research results for ${topic}`); - }, -); -const LLMWriter = withWorkflowFunction(pureWriter); -const LLMEditor = withWorkflowFunction(pureEditor); -const WebResearcher = withWorkflowFunction( - async ({ prompt }: { prompt: string }) => { - console.log("🌐 Researching web for:", prompt); - const results = await Promise.resolve([ - "web result 1", - "web result 2", - "web result 3", - ]); - return results; - }, -); - -// Research collection component -const ResearchCollection = withWorkflowComponent< - [string[], string[]], - { prompt: string } ->(({ prompt }: { prompt: string }) => ( - <> - - {topics => ( - <> - {topics.map(topic => ( - - ))} - - )} - - - -)); +const LLMResearchBrainstorm = Component(pureResearchBrainstorm); +const LLMResearch = Component(async ({ topic }: { topic: string }) => { + console.log("📚 Researching topic:", topic); + return await Promise.resolve(`research results for ${topic}`); +}); +const LLMWriter = Component(pureWriter); +const LLMEditor = Component(pureEditor); +const WebResearcher = Component(async ({ prompt }: { prompt: string }) => { + console.log("🌐 Researching web for:", prompt); + const results = await Promise.resolve([ + "web result 1", + "web result 2", + "web result 3", + ]); + return results; +}); -const BlogWritingWorkflow = withWorkflowComponent( +// When building a workflow out of components, there are two options: +// 1. Use the Component function to wrap it and specify the input and output types. This gets you a function with type safe inputs and outputs (if you just call it as a function). +// 2. Don't wrap it in the Component function, and do not specify the output type (see BlogWritingWorkflow below). You get a function that is the same type as the JSX.Element signature, so it has an unknown output type. If you try to specify the output type on the function signature, you get a type error (unknown is not assignable to X). +const ParallelResearch = Component<{ prompt: string }, [string[], string[]]>( ({ prompt }: { prompt: string }) => ( - - {([catalogResearch, webResearch]) => { - console.log("🧠 Research:", { catalogResearch, webResearch }); - return ( - - {draft => } - - ); - }} - + <> + + {topics => ( + <> + {topics.map(topic => ( + + ))} + + )} + + + ), ); + +const BlogWritingWorkflow = async ({ prompt }: { prompt: string }) => ( + + {([catalogResearch, webResearch]) => { + console.log("🧠 Research:", { catalogResearch, webResearch }); + return ( + + {draft => } + + ); + }} + +); + async function main() { console.log("🚀 Starting blog writing workflow"); - const result = await executeJsxWorkflow( + + // Use the gensx function to execute the workflow and annotate with the output type. + const result = await gensx( , ); - console.log("✅ Final result:", result); + + // Or just call the workflow as a function, and cast to the output type. + const result2 = (await ( + + )) as string; + + // Still need to cast here, because we didn't use the Component helper to wrap the workflow. + const result3 = (await BlogWritingWorkflow({ + prompt: "Write a blog post about the future of AI", + })) as string; + + // Don't need to cast here, because we used the Component helper to wrap the workflow. + const researchResult = await ParallelResearch({ + prompt: "Write a blog post about the future of AI", + }); + console.log("✅ Final result:", { result, result2, result3, researchResult }); } await main(); diff --git a/src/component.ts b/src/component.ts new file mode 100644 index 00000000..e1bfa53c --- /dev/null +++ b/src/component.ts @@ -0,0 +1,14 @@ +import { MaybePromise } from "./jsx-runtime"; + +export function Component, TOutput>( + fn: (input: TInput) => MaybePromise | JSX.Element, +) { + function WorkflowFunction( + props: TInput & { + children?: (output: TOutput) => MaybePromise | JSX.Element; + }, + ): Promise { + return Promise.resolve(fn(props)) as Promise; + } + return WorkflowFunction; +} diff --git a/src/components/Collect.tsx b/src/components/Collect.tsx deleted file mode 100644 index 32b7a953..00000000 --- a/src/components/Collect.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { MaybePromise } from "../jsx-runtime"; -import { executeJsxWorkflow } from "../workflow/execute"; - -export function Collect(props: { - children: MaybePromise[]; -}): Promise { - const promise = Promise.all( - props.children.map(child => - child instanceof Promise - ? child - : executeJsxWorkflow(Promise.resolve(child)), - ), - ); - return Promise.resolve(promise).then(result => { - console.log("Collect result", result); - return result.join("\n"); - }) as Promise; -} diff --git a/src/gensx.ts b/src/gensx.ts new file mode 100644 index 00000000..29c6af1e --- /dev/null +++ b/src/gensx.ts @@ -0,0 +1,3 @@ +export async function gensx(node: JSX.Element): Promise { + return (await node) as TOutput; +} diff --git a/src/index.ts b/src/index.ts index 9a3875bb..1715e1c4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,10 +12,7 @@ 7. Support parallel execution of steps (either dynamic via something liek a collector, or static via a few explicitly defined siblings) */ -import { executeJsxWorkflow } from "./workflow/execute"; -import { - withWorkflowComponent, - withWorkflowFunction, -} from "./workflow/withWorkflowStep"; +import { Component } from "./component"; +import { gensx } from "./gensx"; -export { executeJsxWorkflow, withWorkflowComponent, withWorkflowFunction }; +export { Component, gensx }; diff --git a/src/jsx-runtime.ts b/src/jsx-runtime.ts index 1405d332..2c3e5b79 100644 --- a/src/jsx-runtime.ts +++ b/src/jsx-runtime.ts @@ -47,15 +47,12 @@ export const jsx = < props: TProps | null, children?: Children, ): Promise => { - console.log("jsx", { component, props, children }); if (!children && props?.children) { children = props.children; } return Promise.resolve(component(props ?? ({} as TProps))).then(result => { - console.log("jsx result", { result, children }); if (children) { if (Array.isArray(children)) { - console.log("jsx children is array", { children }); return Promise.all( children.map(child => { if (child instanceof Function) { @@ -64,7 +61,6 @@ export const jsx = < return child; }), ).then(result => { - console.log("jsx children is array result", { result }); return result as TOutput[]; }); } diff --git a/src/workflow/execute.tsx b/src/workflow/execute.tsx deleted file mode 100644 index cd817f38..00000000 --- a/src/workflow/execute.tsx +++ /dev/null @@ -1,6 +0,0 @@ -export async function executeJsxWorkflow( - node: JSX.Element, -): Promise { - console.log("executeJsxWorkflow", node); - return (await node) as TOutput; -} diff --git a/src/workflow/withWorkflowStep.tsx b/src/workflow/withWorkflowStep.tsx deleted file mode 100644 index d619d465..00000000 --- a/src/workflow/withWorkflowStep.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { MaybePromise } from "../jsx-runtime"; -import { executeJsxWorkflow } from "./execute"; - -export function withWorkflowFunction< - TOutput, - TInput extends Record = Record, ->(fn: (input: TInput) => MaybePromise) { - function WorkflowFunction( - props: TInput & { - children?: (output: TOutput) => MaybePromise | JSX.Element; - }, - ): Promise { - return Promise.resolve(fn(props)); - } - return WorkflowFunction; -} - -// For wrapping JSX trees that need execution -export function withWorkflowComponent< - TOutput, - TInput extends Record, ->(component: (input: TInput) => MaybePromise | JSX.Element) { - console.log("withWorkflowComponent", { component }); - async function WorkflowComponent( - input: TInput & { - children?: (output: TOutput) => MaybePromise | JSX.Element; - }, - ): Promise { - console.log("withWorkflowComponent call ", { component, input }); - const element = Promise.resolve(component(input)); - const result = await executeJsxWorkflow(element); - console.log("withWorkflowComponent result", result); - return result as TOutput; - } - return WorkflowComponent; -} From 8f9bcb1234668e64f07abf9598512787fca8da5d Mon Sep 17 00:00:00 2001 From: Jeremy Moseley Date: Fri, 20 Dec 2024 09:51:44 -0800 Subject: [PATCH 07/14] Support jsx-dev-runtime. --- .vscode/settings.json | 2 +- src/component.ts | 2 +- src/gensx.ts | 2 ++ src/jsx-dev-runtime.ts | 7 +++++++ src/jsx-runtime.ts | 23 ++++++++--------------- 5 files changed, 19 insertions(+), 17 deletions(-) create mode 100644 src/jsx-dev-runtime.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index f3c3acec..4b77464b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,5 +3,5 @@ "source.fixAll.eslint": "always", "source.removeUnusedImports": "always" }, - "cSpell.words": ["gensx"] + "cSpell.words": ["gensx", "jsxs"] } diff --git a/src/component.ts b/src/component.ts index e1bfa53c..fa522ba2 100644 --- a/src/component.ts +++ b/src/component.ts @@ -1,4 +1,4 @@ -import { MaybePromise } from "./jsx-runtime"; +import { JSX, MaybePromise } from "./jsx-runtime"; export function Component, TOutput>( fn: (input: TInput) => MaybePromise | JSX.Element, diff --git a/src/gensx.ts b/src/gensx.ts index 29c6af1e..d876fc46 100644 --- a/src/gensx.ts +++ b/src/gensx.ts @@ -1,3 +1,5 @@ +import { JSX } from "./jsx-runtime"; + export async function gensx(node: JSX.Element): Promise { return (await node) as TOutput; } diff --git a/src/jsx-dev-runtime.ts b/src/jsx-dev-runtime.ts new file mode 100644 index 00000000..4b0abaca --- /dev/null +++ b/src/jsx-dev-runtime.ts @@ -0,0 +1,7 @@ +import { Fragment, JSX, jsx } from "@/jsx-runtime"; + +export type { JSX }; + +const jsxDEV = jsx; + +export { Fragment, jsxDEV }; diff --git a/src/jsx-runtime.ts b/src/jsx-runtime.ts index 2c3e5b79..843df155 100644 --- a/src/jsx-runtime.ts +++ b/src/jsx-runtime.ts @@ -1,14 +1,12 @@ /* eslint-disable @typescript-eslint/no-namespace */ -declare global { - namespace JSX { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - export type ElementType = (props: any) => Promise; - // eslint-disable-next-line @typescript-eslint/no-empty-object-type - interface IntrinsicElements {} - export type Element = Promise; - export interface ElementChildrenAttribute { - children: (output: unknown) => MaybePromise; - } +export namespace JSX { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + export type ElementType = (props: any) => Promise; + // eslint-disable-next-line @typescript-eslint/no-empty-object-type, @typescript-eslint/no-unused-vars + // interface IntrinsicElements {} + export type Element = Promise; + export interface ElementChildrenAttribute { + children: (output: unknown) => MaybePromise; } } @@ -32,11 +30,6 @@ export const Fragment = (props: { children: Children }): Promise => { return Promise.all([props.children]); }; -// export interface Component { -// type: (props: TProps) => MaybePromise; -// props: TProps; -// } - export const jsx = < TOutput, TProps extends Record & { From 1648656eb795f79a40aa0eb0636e3fb847141969 Mon Sep 17 00:00:00 2001 From: Jeremy Moseley Date: Fri, 20 Dec 2024 09:51:52 -0800 Subject: [PATCH 08/14] Add exports for jsx-runtime. --- package.json | 13 ++++++++++--- tsconfig.prod.json | 7 ++++--- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 5a6ab045..90902d40 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "packageManager": "pnpm@9.14.2", "type": "module", "scripts": { + "build:watch": "pnpm build:clean && pnpm generate-dist --watch", "dev": "nodemon", "prepublishOnly": "pnpm i && pnpm build", "build": "pnpm validate-typescript && pnpm build:clean && pnpm generate-dist", @@ -38,7 +39,7 @@ "lint:fix": "eslint --ignore-path .gitignore . --ext .js,.ts --fix", "lint:file": "eslint --ignore-path .gitignore", "validate-typescript": "tsc -p tsconfig.prod.json --noEmit", - "generate-dist": "tsup src/index.ts --minify --tsconfig tsconfig.prod.json --dts --format cjs,esm --out-dir dist", + "generate-dist": "tsup src/index.ts --minify --tsconfig tsconfig.prod.json --dts --format cjs,esm --out-dir dist --entry.jsx-runtime=src/jsx-runtime.ts --entry.jsx-dev-runtime=src/jsx-dev-runtime.ts --entry.index=src/index.ts", "build:clean": "rimraf dist; exit 0", "prepare": "[ -f .husky/install.mjs ] && node .husky/install.mjs || true" }, @@ -92,7 +93,13 @@ "default": "./dist/index.cjs" } }, - "./jsx-runtime": "./dist/jsx-runtime.js" + "./jsx-runtime": { + "import": "./dist/jsx-runtime.js", + "require": "./dist/jsx-runtime.cjs" + }, + "./jsx-dev-runtime": { + "import": "./dist/jsx-dev-runtime.js", + "require": "./dist/jsx-dev-runtime.cjs" + } } } - diff --git a/tsconfig.prod.json b/tsconfig.prod.json index 1c35d66d..434bf887 100644 --- a/tsconfig.prod.json +++ b/tsconfig.prod.json @@ -1,8 +1,9 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "rootDir": "./src", "outDir": "./dist" }, - "include": ["src"] -} + "include": [ + "src" + ] +} \ No newline at end of file From b1a6138b4d087155b8574ae67bfc223364ee62d1 Mon Sep 17 00:00:00 2001 From: Jeremy Moseley Date: Fri, 20 Dec 2024 09:56:56 -0800 Subject: [PATCH 09/14] Clarify comment. --- playground/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/playground/index.tsx b/playground/index.tsx index 0abe00e2..6048a9ea 100644 --- a/playground/index.tsx +++ b/playground/index.tsx @@ -44,6 +44,7 @@ const WebResearcher = Component(async ({ prompt }: { prompt: string }) => { // When building a workflow out of components, there are two options: // 1. Use the Component function to wrap it and specify the input and output types. This gets you a function with type safe inputs and outputs (if you just call it as a function). // 2. Don't wrap it in the Component function, and do not specify the output type (see BlogWritingWorkflow below). You get a function that is the same type as the JSX.Element signature, so it has an unknown output type. If you try to specify the output type on the function signature, you get a type error (unknown is not assignable to X). +// If you choose not to wrap it in a Component, you can't pass children to it, but we could easily expose the types for that to enable it, similar to the React.PropsWithChildren type. const ParallelResearch = Component<{ prompt: string }, [string[], string[]]>( ({ prompt }: { prompt: string }) => ( <> From 997481d10ddd0e151ec02ad15e83e105456c87e4 Mon Sep 17 00:00:00 2001 From: Jeremy Moseley Date: Fri, 20 Dec 2024 09:59:38 -0800 Subject: [PATCH 10/14] Remove label workflow. --- .github/workflows/conventional-label.yml | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 .github/workflows/conventional-label.yml diff --git a/.github/workflows/conventional-label.yml b/.github/workflows/conventional-label.yml deleted file mode 100644 index 52161a86..00000000 --- a/.github/workflows/conventional-label.yml +++ /dev/null @@ -1,13 +0,0 @@ -name: "🏷️ Conventional release labels" - -on: - pull_request_target: - types: [opened, edited, reopened] - -jobs: - label: - runs-on: ubuntu-latest - steps: - - uses: bcoe/conventional-release-labels@v1 - with: - type_labels: '{"feat": "kind/feature", "fix": "kind/bug", "breaking": "kind/breaking-change"}' From 0e77b102bfcda19584d67e3a237b04a424d9e75b Mon Sep 17 00:00:00 2001 From: Jeremy Moseley Date: Fri, 20 Dec 2024 11:17:35 -0800 Subject: [PATCH 11/14] Improve the examples. --- playground/index.tsx | 161 ++++++++++++++++++++++++------------------- src/component.ts | 2 +- src/gensx.ts | 2 +- src/index.ts | 4 +- 4 files changed, 93 insertions(+), 76 deletions(-) diff --git a/playground/index.tsx b/playground/index.tsx index 6048a9ea..c589f861 100644 --- a/playground/index.tsx +++ b/playground/index.tsx @@ -1,68 +1,100 @@ -import { Component, gensx } from "@/index"; +import * as gsx from "@/index"; -// Pure workflow steps -const pureResearchBrainstorm = async ({ prompt }: { prompt: string }) => { +interface LLMResearchBrainstormProps { + prompt: string; +} +type LLMResearchBrainstormOutput = string[]; +const LLMResearchBrainstorm = gsx.Component< + LLMResearchBrainstormProps, + LLMResearchBrainstormOutput +>(async ({ prompt }) => { console.log("🔍 Starting research for:", prompt); const topics = await Promise.resolve(["topic 1", "topic 2", "topic 3"]); return topics; -}; +}); + +interface LLMResearchProps { + topic: string; +} +type LLMResearchOutput = string; +const LLMResearch = gsx.Component( + async ({ topic }) => { + console.log("📚 Researching topic:", topic); + return await Promise.resolve(`research results for ${topic}`); + }, +); -const pureWriter = async ({ - research, - prompt, -}: { +interface LLMWriterProps { research: string; prompt: string; -}): Promise => { - console.log("✍️ Writing draft based on research"); - return await Promise.resolve(`**draft\n${research}\n${prompt}\n**end draft`); -}; - -const pureEditor = async ({ draft }: { draft: string }) => { - console.log("✨ Polishing final draft"); - return await Promise.resolve(`edited result: ${draft}`); -}; +} +type LLMWriterOutput = string; +const LLMWriter = gsx.Component( + async ({ research, prompt }) => { + console.log("✍️ Writing draft based on research"); + return await Promise.resolve( + `**draft\n${research}\n${prompt}\n**end draft`, + ); + }, +); -// Wrapped workflow steps -const LLMResearchBrainstorm = Component(pureResearchBrainstorm); -const LLMResearch = Component(async ({ topic }: { topic: string }) => { - console.log("📚 Researching topic:", topic); - return await Promise.resolve(`research results for ${topic}`); -}); -const LLMWriter = Component(pureWriter); -const LLMEditor = Component(pureEditor); -const WebResearcher = Component(async ({ prompt }: { prompt: string }) => { - console.log("🌐 Researching web for:", prompt); - const results = await Promise.resolve([ - "web result 1", - "web result 2", - "web result 3", - ]); - return results; -}); +interface LLMEditorProps { + draft: string; +} +type LLMEditorOutput = string; +const LLMEditor = gsx.Component( + async ({ draft }) => { + console.log("✨ Polishing final draft"); + return await Promise.resolve(`edited result: ${draft}`); + }, +); -// When building a workflow out of components, there are two options: -// 1. Use the Component function to wrap it and specify the input and output types. This gets you a function with type safe inputs and outputs (if you just call it as a function). -// 2. Don't wrap it in the Component function, and do not specify the output type (see BlogWritingWorkflow below). You get a function that is the same type as the JSX.Element signature, so it has an unknown output type. If you try to specify the output type on the function signature, you get a type error (unknown is not assignable to X). -// If you choose not to wrap it in a Component, you can't pass children to it, but we could easily expose the types for that to enable it, similar to the React.PropsWithChildren type. -const ParallelResearch = Component<{ prompt: string }, [string[], string[]]>( - ({ prompt }: { prompt: string }) => ( - <> - - {topics => ( - <> - {topics.map(topic => ( - - ))} - - )} - - - - ), +interface WebResearcherProps { + prompt: string; +} +type WebResearcherOutput = string[]; +const WebResearcher = gsx.Component( + async ({ prompt }) => { + console.log("🌐 Researching web for:", prompt); + const results = await Promise.resolve([ + "web result 1", + "web result 2", + "web result 3", + ]); + return results; + }, ); -const BlogWritingWorkflow = async ({ prompt }: { prompt: string }) => ( +type ParallelResearchOutput = [string[], string[]]; +interface ParallelResearchComponentProps { + prompt: string; +} +const ParallelResearch = gsx.Component< + ParallelResearchComponentProps, + ParallelResearchOutput +>(({ prompt }) => ( + <> + + {topics => ( + <> + {topics.map(topic => ( + + ))} + + )} + + + +)); + +interface BlogWritingWorkflowProps { + prompt: string; +} +type BlogWritingWorkflowOutput = string; +const BlogWritingWorkflow = gsx.Component< + BlogWritingWorkflowProps, + BlogWritingWorkflowOutput +>(async ({ prompt }) => ( {([catalogResearch, webResearch]) => { console.log("🧠 Research:", { catalogResearch, webResearch }); @@ -73,31 +105,16 @@ const BlogWritingWorkflow = async ({ prompt }: { prompt: string }) => ( ); }} -); +)); async function main() { console.log("🚀 Starting blog writing workflow"); // Use the gensx function to execute the workflow and annotate with the output type. - const result = await gensx( + const result = await gsx.execute( , ); - - // Or just call the workflow as a function, and cast to the output type. - const result2 = (await ( - - )) as string; - - // Still need to cast here, because we didn't use the Component helper to wrap the workflow. - const result3 = (await BlogWritingWorkflow({ - prompt: "Write a blog post about the future of AI", - })) as string; - - // Don't need to cast here, because we used the Component helper to wrap the workflow. - const researchResult = await ParallelResearch({ - prompt: "Write a blog post about the future of AI", - }); - console.log("✅ Final result:", { result, result2, result3, researchResult }); + console.log("✅ Final result:", { result }); } await main(); diff --git a/src/component.ts b/src/component.ts index fa522ba2..2818e035 100644 --- a/src/component.ts +++ b/src/component.ts @@ -1,6 +1,6 @@ import { JSX, MaybePromise } from "./jsx-runtime"; -export function Component, TOutput>( +export function Component( fn: (input: TInput) => MaybePromise | JSX.Element, ) { function WorkflowFunction( diff --git a/src/gensx.ts b/src/gensx.ts index d876fc46..ccc6ddd2 100644 --- a/src/gensx.ts +++ b/src/gensx.ts @@ -1,5 +1,5 @@ import { JSX } from "./jsx-runtime"; -export async function gensx(node: JSX.Element): Promise { +export async function execute(node: JSX.Element): Promise { return (await node) as TOutput; } diff --git a/src/index.ts b/src/index.ts index 1715e1c4..32ef5597 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,6 +13,6 @@ */ import { Component } from "./component"; -import { gensx } from "./gensx"; +import { execute } from "./gensx"; -export { Component, gensx }; +export { Component, execute }; From 1db439342d8e116ca709bcf534c8f6e1aa77bfdd Mon Sep 17 00:00:00 2001 From: Jeremy Moseley Date: Fri, 20 Dec 2024 13:24:05 -0800 Subject: [PATCH 12/14] Support arrays of child elements returned from the child function without a fragment. --- src/component.ts | 9 ++++++- src/jsx-runtime.ts | 62 ++++++++++++++++++++++------------------------ 2 files changed, 37 insertions(+), 34 deletions(-) diff --git a/src/component.ts b/src/component.ts index 2818e035..9f470808 100644 --- a/src/component.ts +++ b/src/component.ts @@ -5,10 +5,17 @@ export function Component( ) { function WorkflowFunction( props: TInput & { - children?: (output: TOutput) => MaybePromise | JSX.Element; + children?: ( + output: TOutput, + ) => MaybePromise; }, ): Promise { return Promise.resolve(fn(props)) as Promise; } + if (fn.name) { + Object.defineProperty(WorkflowFunction, "name", { + value: `WorkflowFunction[${fn.name}]`, + }); + } return WorkflowFunction; } diff --git a/src/jsx-runtime.ts b/src/jsx-runtime.ts index 843df155..b63f4b8c 100644 --- a/src/jsx-runtime.ts +++ b/src/jsx-runtime.ts @@ -6,75 +6,71 @@ export namespace JSX { // interface IntrinsicElements {} export type Element = Promise; export interface ElementChildrenAttribute { - children: (output: unknown) => MaybePromise; + children: (output: unknown) => JSX.Element | JSX.Element[]; } } export type MaybePromise = T | Promise; -export type Child = JSX.Element | ((output: T) => JSX.Element); -export type Children = Child | Child[]; - -export const Fragment = (props: { children: Children }): Promise => { +export const Fragment = (props: { + children: JSX.Element[] | JSX.Element; +}): JSX.Element[] => { + console.log("🚀 ~ Fragment ~ props:", props); if (Array.isArray(props.children)) { - return Promise.all( - props.children.map(child => { - if (child instanceof Function) { - return child(null); - } - return child; - }), - ); + return props.children; } - return Promise.all([props.children]); + return [props.children]; }; export const jsx = < TOutput, TProps extends Record & { - children?: Children; + children?: (output: TOutput) => MaybePromise; }, >( component: (props: TProps) => MaybePromise, props: TProps | null, - children?: Children, + children?: (output: TOutput) => MaybePromise, ): Promise => { if (!children && props?.children) { children = props.children; } + console.log("🚀 ~ jsx ~ children:", { + children, + props, + component, + childrenIsArray: Array.isArray(children), + }); return Promise.resolve(component(props ?? ({} as TProps))).then(result => { if (children) { + // If its an array of elements, this is an edge case for a Fragment. if (Array.isArray(children)) { - return Promise.all( - children.map(child => { - if (child instanceof Function) { - return child(result); - } - return child; - }), - ).then(result => { - return result as TOutput[]; - }); + return Promise.all(children); } - if (children instanceof Function) { - return children(result) as TOutput; + if (typeof children === "function") { + // If the components child function returns an array of elements, we need to resolve them all + const childrenResult = children(result); + if (Array.isArray(childrenResult)) { + return Promise.all(childrenResult); + } + return Promise.resolve(childrenResult); } - return children as TOutput; + return Promise.resolve(children); } - return result as TOutput; - }); + return result; + }) as Promise; }; export const jsxs = < TOutput, TProps extends Record & { - children?: Children; + children?: (output: TOutput) => MaybePromise; }, >( component: (props: TProps) => MaybePromise, props: TProps | null, - children?: Children, + children?: (output: TOutput) => MaybePromise, ): Promise => { return jsx(component, props, children); }; From 87a7e978be8bf7d9d0d4b480897e0f1aa79cc97e Mon Sep 17 00:00:00 2001 From: Jeremy Moseley Date: Fri, 20 Dec 2024 13:25:14 -0800 Subject: [PATCH 13/14] Simplify the example to remove the endted fragment. --- playground/index.tsx | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/playground/index.tsx b/playground/index.tsx index c589f861..e5d9cb79 100644 --- a/playground/index.tsx +++ b/playground/index.tsx @@ -75,13 +75,7 @@ const ParallelResearch = gsx.Component< >(({ prompt }) => ( <> - {topics => ( - <> - {topics.map(topic => ( - - ))} - - )} + {topics => topics.map(topic => )} @@ -99,7 +93,12 @@ const BlogWritingWorkflow = gsx.Component< {([catalogResearch, webResearch]) => { console.log("🧠 Research:", { catalogResearch, webResearch }); return ( - + {draft => } ); From de14ac8bd2de8ef7172efbd69391b12c13c3751f Mon Sep 17 00:00:00 2001 From: Jeremy Moseley Date: Fri, 20 Dec 2024 13:28:18 -0800 Subject: [PATCH 14/14] Remove console.logs --- src/jsx-runtime.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/jsx-runtime.ts b/src/jsx-runtime.ts index b63f4b8c..96364e72 100644 --- a/src/jsx-runtime.ts +++ b/src/jsx-runtime.ts @@ -15,7 +15,6 @@ export type MaybePromise = T | Promise; export const Fragment = (props: { children: JSX.Element[] | JSX.Element; }): JSX.Element[] => { - console.log("🚀 ~ Fragment ~ props:", props); if (Array.isArray(props.children)) { return props.children; } @@ -36,12 +35,6 @@ export const jsx = < if (!children && props?.children) { children = props.children; } - console.log("🚀 ~ jsx ~ children:", { - children, - props, - component, - childrenIsArray: Array.isArray(children), - }); return Promise.resolve(component(props ?? ({} as TProps))).then(result => { if (children) { // If its an array of elements, this is an edge case for a Fragment. @@ -56,6 +49,8 @@ export const jsx = < } return Promise.resolve(childrenResult); } + + // If its a single element, this is an edge case for a Fragment. return Promise.resolve(children); } return result;