Skip to Content
Overview

GenSX Overview

GenSX is a simple typescript framework for building complex LLM applications. It’s a workflow engine designed for building agents, chatbot APIs, and more using common patterns like RAG and reflection.

It uses jsx to define and orchestrate workflows with functional, reusable components:

const WriteBlog = gensx.Component<WriteBlogProps, string>( "WriteBlog", async ({ prompt }) => { return ( <WriteDraft prompt={prompt}> {(blog) => ( <EditDraft draft={blog}> {(draft) => console.log(draft)} </WriteDraft> )} </WriteBlog>, ); const workflow = gensx.Workflow("WriteBlogWorkflow", WriteBlog); const result = await workflow.run({ prompt: "Write a blog post about AI developer tools" });

Most LLM frameworks are graph oriented, inspired by popular python tools like Airflow. You express nodes, edges, and a global state object for your workflow. While graph APIs are highly expressive, they are also cumbersome:

  • Building a mental model and visualizing the execution of a workflow from a node/edge builder is difficult.
  • Global state makes refactoring difficult.
  • All of this leads to low velocity when experimenting with and evolving your LLM workflows.

LLM workflows are fundamentally about function composition and data flow - exactly what JSX was designed to express. There is no need for a graph DSL. GenSX expresses control flow, loops, and recursion using plain old typescript language primitives. To learn more about why GenSX uses JSX, read Why JSX?.

While GenSX uses JSX, it does not share the same execution model as React, and has zero dependencies.

Reusable by default

GenSX components are pure functions, depend on zero global state, and are reusable by default. Components accept props and return an output.

interface ResearchTopicProps { topic: string; } type ResearchTopicOutput = string; const ResearchTopic = gensx.Component<ResearchTopicProps, ResearchTopicOutput>( "ResearchTopic", async ({ topic }) => { console.log("📚 Researching topic:", topic); const systemPrompt = `You are a helpful assistant that researches topics...`; return ( <ChatCompletion model="gpt-4o-mini" temperature={0} messages={[ { role: "system", content: systemPrompt }, { role: "user", content: topic }, ]} /> ); }, );

Because components are pure functions, they are easy to test and eval in isolation. This enables you to move quickly and experiment with the structure of your workflow.

A second order benefit of reusability is that components can be shared and published to package managers like npm. If you build a functional component, you can make it available to the community - something that frameworks that depend on global state preclude by default.

Composition

All GenSX components support nesting, a pattern to access component outputs via a child function. This creates a natural way to pass data between steps:

export const WriteBlog = gensx.StreamComponent<WriteBlogProps>( "WriteBlog", async ({ prompt }) => { return ( <OpenAIProvider apiKey={process.env.OPENAI_API_KEY}> <Research prompt={prompt}> {(research) => ( <WriteDraft prompt={prompt} research={research.flat()}> {(draft) => <EditDraft draft={draft} stream={true} />} </WriteDraft> )} </Research> </OpenAIProvider> ); }, );

There is no need for a DSL or graph API to define the structure of your workflow. More complex patterns like cycles and agents can be encapsulated in components that use standard loops and conditionals. Typescript and JSX unifies workflow definition and execution in plain old typescript, with the ability to express all of the same patterns.

Visual clarity

Workflow composition with JSX reads naturally from top to bottom like a standard programming language.

<FetchHNPosts limit={postCount}> {(stories) => ( <AnalyzeHNPosts stories={stories}> {({ analyses }) => ( <GenerateReport analyses={analyses}> {(report) => ( <EditReport content={report}> {(editedReport) => ( <WriteTweet context={editedReport} prompt="Summarize the HN trends in a tweet" /> )} </EditReport> )} </GenerateReport> )} </AnalyzeHNPosts> )} </FetchHNPosts>

Contrast this with graph APIs, where you need to build a mental model of the workflow from a node/edge builder:

const graph = new Graph() .addNode("fetchHNPosts", fetchHNPosts) .addNode("analyzeHNPosts", analyzePosts) .addNode("generateReport", generateReport) .addNode("editReport", editReport) .addNode("writeTweet", writeTweet); graph .addEdge(START, "fetchHNPosts") .addEdge("fetchHNPosts", "analyzeHNPosts") .addEdge("analyzeHNPosts", "generateReport") .addEdge("generateReport", "editReport") .addEdge("editReport", "writeTweet") .addEdge("writeTweet", END);

Nesting makes dependencies explicit, and it is easy to see the data flow between steps. No graph DSL required.

Streaming baked in

LLMs are unique in that any given call can return a stream or a prompt response, but streaming is typically applied to just the last step. You shouldn’t have to touch the innards of your components to experiment with the shape of your workflow.

GenSX makes this easy to handle with StreamComponent.

A single component implementation can be used in both streaming and non-streaming contexts by setting the stream prop:

const EditDraft = gensx.StreamComponent<EditDraftProps>( "EditDraft", async ({ draft }) => { console.log("🔍 Editing draft"); const systemPrompt = `You are a helpful assistant that edits blog posts...`; return ( <ChatCompletion stream={true} model="gpt-4o-mini" temperature={0} messages={[ { role: "system", content: systemPrompt }, { role: "user", content: draft }, ]} /> ); }, );

From there you can use the component in a streaming context:

const stream = await gensx .Workflow("EditDraftWorkflow", EditDraft) .run({ draft, stream: true }); for await (const chunk of stream) { process.stdout.write(chunk); }

And you can use the component in a non-streaming context:

const result = await gensx .Workflow("EditDraftWorkflow", EditDraft) .run({ draft, stream: false }); console.log(result);

Designed for velocity

The GenSX programming model is optimized for speed of iteration in the long run.

The typical journey building an LLM application looks like:

  1. Ship a prototype that uses a single LLM prompt.
  2. Add in evals to measure progress.
  3. Add in external context via RAG.
  4. Break tasks down into smaller discrete LLM calls chained together to improve quality.
  5. Add advanced patterns like reflection, and agents.

Experimentation speed depends on the ability to refactor, rearrange, and inject new steps. In our experience, this is something that frameworks that revolve around global state and a graph model slow down.

The functional component model in GenSX support an iterative loop that is fast on day one through day 1000.

Last updated on