Skip to content

Commit

Permalink
feat: Framework overhaul - object returns, JSX objects, streaming pat…
Browse files Browse the repository at this point in the history
…tern, and context API (#36)

Major overhaul of the framework based on my experience building out the first robust example (`HackerNewsAnalyzer`). This PR includes the following: 

- Support for object returns and nested JSX elements in objects
- Adds a natural pattern for delineating between streaming and sync LLM output (`Component` vs `StreamComponent`)
- A context API that can accumulate and track state across subgraphs (used to make streaming work without global state)
-  Add a robust `HackerNewsAnalyzer` example that summarizes posts from the front page (n=500) and runs sentiment analysis over all of the comments to product a report and a tweet in the style of Paul Graham.

## Object returns & nested JSX

Add two features to streamline workflow authorship:

1. Support for returning objects instead of just Element Element[] and string
2. Add the ability to embed Elements inside of objects

### Direct object return

- Components can now return plain objects directly instead of only JSX elements
- Eliminates the need for wrapper components when combining multiple outputs
- Example: `return { summary, commentAnalysis }` instead of needing to define another component that manipulates the output shape from tuple -> object

### Nested JSX Resolution in Objects

- The execute function now recursively resolves JSX elements within returned objects
- Enables natural composition where object properties can be JSX elements

Example:

```tsx
     return {
       analyses: stories.map(story => ({
         summary: <PostSummarizer story={story} />,
         commentAnalysis: <CommentsAnalyzer comments={story.comments} />
       }))
     }
```

Together both of these changes eliminate the need for wrapper components, boilerplate, and reduces the awkwardness of having to deal with weakly typed tuples in fanout and parallel operations.

## Streaming Patterns

LLM components need to support both (1) synchronous output -- the 99% use case -- and (2) streaming output -- the 1% output that is typically only used during the last step when output is being rendered to users via a UI.

We want to optimize for sync output since this is the common use case. To support this we've added:

1. `gensx.StreamComponent`: a special kind of component that returns a standard formal with a plain text value and a stream to get the output
2. A standard `stream` prop available on all streaming components. This defaults to false, but can be set to true to trigger streaming for that component only. Further nested components revert back to sync invocation unless they specify `stream: true` as well. The final output can be streaming or not, depending on the types of the components that get returned and whether or not `stream` is set on those elements allowing you to mix/match if needed.

This enables writing any component to either return a text value or a stream. Authors don't have to worry about supporting both formats. Consumers get the prompt value in the 99% use case or can set `stream: true` and get the expected behavior for the 1% use case.

## Context API

A context API that can be used to accumulate and track state across different parts of the subgraph.

Streaming is intrinsic to the framework, and the framework is very aware of the streaming context values. However, the general API should support other use cases in the future such as:

- tracing
- loggers
- error boundaries
- caching
- configuration inheritence

## HN Example

Adds a more robust real world example that analyzes the front page of HN:

1. pulls the top 500 pages, and filters down to text posts
2. summarizes the posts, analyzes comments for sentiment
4. feeds all of this into a report writer
5. rewrite the report in the voice of Paul Graham
6. writes a tweet about the post in the style of PG

Powerful example in just 300 LOC!

Co-authored-by: Jeremy Moseley <[email protected]>
  • Loading branch information
EvanBoyle and jmoseley authored Dec 31, 2024
1 parent 881189f commit 196b0e5
Show file tree
Hide file tree
Showing 21 changed files with 1,549 additions and 743 deletions.
1 change: 0 additions & 1 deletion .husky/pre-commit

This file was deleted.

38 changes: 38 additions & 0 deletions hn_analysis_report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
When you sift through the vibrant exchanges on Hacker News, you start to see patterns in the chaos. It's like peering into a crystal ball, albeit one that's a bit cracked and smudged with fingerprints. The topics buzzing around tell us a lot about where technology is heading and what’s gnawing at the minds of those in the trenches.

### Exciting Waves in Tech

1. **AI and Machine Learning:**
AI is the new electricity, lighting up the imagination of everyone from garage tinkerers to corporate juggernauts. Take, for instance, the buzz around [Semantic Search for ArXiv Papers](https://news.ycombinator.com/item?id=42507116). It's a project that uses AI to sift through the academic haystack to find those elusive needles. The real excitement lies in AI's potential to revolutionize fields like healthcare by making diagnostics smarter, or code smarter by augmenting software development.

2. **Cutting-Edge Development Tools:**
Developers are always on the hunt for ways to do more with less hassle. Enter tools like [Cudair](https://news.ycombinator.com/item?id=42484994), which offers live-reloading for CUDA applications. Such innovations are like giving a chef a sharper knife—they can still chop faster without losing a finger. In high-performance computing, where every millisecond counts, these tools are nothing short of revolutionary.

3. **Decentralization and Privacy:**
Privacy is the new frontier, and decentralized systems are the pioneers staking their claim. Projects such as [TideCloak](https://news.ycombinator.com/item?id=42460131) are building the infrastructure for a world where control over personal data is not just a pipe dream. As our lives move increasingly online, the demand for privacy and user sovereignty is growing louder.

### Storm Clouds on the Horizon

1. **Tech Monopolies and Privacy Concerns:**
The tech giants are feeling more and more like Big Brother to the Hacker News crowd. Consider the uproar over [uBlock in Chrome](https://news.ycombinator.com/item?id=42506506). Users are frustrated by changes that seem to prioritize profits over privacy, making them feel like pawns rather than participants.

2. **AI-Induced Job Anxiety:**
The specter of AI-induced job loss is haunting the discussions, especially concerning the [future of programming jobs](https://news.ycombinator.com/item?id=42500926). There's a palpable tension between the promise of AI augmenting human work and the fear of it replacing entry-level positions. Yet, the optimists argue that AI will create more jobs than it destroys, akin to how ATMs didn't eliminate bank tellers.

3. **Cloud Dependency Issues:**
The reliance on cloud services is a double-edged sword. While they offer convenience, they also introduce risks, as seen with issues like those surrounding [Google Authenticator](https://news.ycombinator.com/item?id=42510300). The fragility of these systems can be unnerving, especially when they hold the keys to your digital kingdom.

### Surprising Currents

1. **AI's Cross-Disciplinary Ventures:**
AI is proving to be a versatile tool, popping up in unexpected places like [crossword generation](https://news.ycombinator.com/item?id=42496953). It's like discovering your hammer can also make a decent screwdriver. This cross-pollination of ideas is pushing AI into creative domains, sparking new forms of problem-solving.

2. **DIY and Open-Source Renaissance:**
There's a resurgence of interest in DIY and open-source projects, as demonstrated by [Musoq](https://news.ycombinator.com/item?id=42453650). It enables querying across diverse data sources with SQL, empowering developers to break free from the shackles of proprietary software. It's a reminder that innovation often thrives outside corporate walls.

3. **Tech’s Cultural and Environmental Impact:**
Projects like [Movie Iris](https://news.ycombinator.com/item?id=42462348) show a unique blend of art, tech, and environmental consciousness. By using color extraction for film analysis, they encourage us to ponder the deeper implications of our cultural consumption and its environmental footprint.

### The Collective Pulse

The atmosphere among the tech-savvy is one of cautious optimism. There's a tangible excitement about the transformative power of technology, tempered by a keen awareness of the ethical and societal challenges it brings. As we forge ahead, the focus remains on balancing innovation with responsibility, ensuring that the future we build is one we can be proud of.
1 change: 1 addition & 0 deletions hn_analysis_tweet.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
AI is shaking up fields you wouldn't expect, like crossword puzzles—proof that the real magic lies in AI's unexpected versatility, not just its raw power. #AI #Innovation #TechTrends
7 changes: 0 additions & 7 deletions lint-staged.config.mjs

This file was deleted.

34 changes: 15 additions & 19 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
"name": "gensx",
"version": "0.1.1",
"description": "Make LLMs work good",
"main": "dist/index.cjs",
"module": "lib/index.js",
"main": "dist/index.js",
"module": "dist/index.js",
"types": "dist/index.d.ts",
"author": "",
"license": "MIT",
Expand All @@ -20,19 +20,19 @@
"packageManager": "[email protected]",
"type": "module",
"scripts": {
"build:watch": "pnpm build:clean && pnpm generate-dist --watch",
"build:watch": "pnpm build:clean && tsc -p tsconfig.prod.json --watch",
"dev": "nodemon",
"prepublishOnly": "pnpm i && pnpm build",
"build": "pnpm validate-typescript && pnpm build:clean && pnpm generate-dist",
"build": "pnpm validate-typescript && pnpm build:clean && pnpm build:dist",
"test": "rimraf coverage && pnpm test:unit",
"test:watch": "vitest",
"test:unit": "vitest run --coverage",
"lint": "eslint --ignore-path .gitignore . --ext .js,.ts",
"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 --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",
"build:clean": "rimraf dist",
"build:dist": "tsc -p tsconfig.prod.json --outDir dist --declaration",
"prepare": "[ -f .husky/install.mjs ] && node .husky/install.mjs || true"
},
"devDependencies": {
Expand All @@ -56,7 +56,6 @@
"prettier": "^3.4.2",
"rimraf": "^6.0.1",
"tsconfig-paths": "^4.2.0",
"tsup": "^8.3.5",
"tsx": "^4.19.2",
"typescript": "^5.7.2",
"unplugin-swc": "^1.5.1",
Expand All @@ -72,22 +71,19 @@
],
"exports": {
".": {
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"require": {
"types": "./dist/index.d.cts",
"default": "./dist/index.cjs"
}
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"./jsx-runtime": {
"import": "./dist/jsx-runtime.js",
"require": "./dist/jsx-runtime.cjs"
"types": "./dist/jsx-runtime.d.ts",
"default": "./dist/jsx-runtime.js"
},
"./jsx-dev-runtime": {
"import": "./dist/jsx-dev-runtime.js",
"require": "./dist/jsx-dev-runtime.cjs"
"types": "./dist/jsx-dev-runtime.d.ts",
"default": "./dist/jsx-dev-runtime.js"
}
},
"dependencies": {
"openai": "^4.77.0"
}
}
107 changes: 107 additions & 0 deletions playground/blogWriter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import * as gsx from "@/index";

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<LLMResearchProps, LLMResearchOutput>(
async ({ topic }) => {
console.log("📚 Researching topic:", topic);
return await Promise.resolve(`research results for ${topic}`);
},
);

interface LLMWriterProps {
research: string;
prompt: string;
}
type LLMWriterOutput = string;
const LLMWriter = gsx.Component<LLMWriterProps, LLMWriterOutput>(
async ({ research, prompt }) => {
console.log("✍️ Writing draft based on research");
return await Promise.resolve(
`**draft\n${research}\n${prompt}\n**end draft`,
);
},
);

interface LLMEditorProps {
draft: string;
}
type LLMEditorOutput = string;
const LLMEditor = gsx.Component<LLMEditorProps, LLMEditorOutput>(
async ({ draft }) => {
console.log("✨ Polishing final draft");
return await Promise.resolve(`edited result: ${draft}`);
},
);

interface WebResearcherProps {
prompt: string;
}
type WebResearcherOutput = string[];
const WebResearcher = gsx.Component<WebResearcherProps, WebResearcherOutput>(
async ({ prompt }) => {
console.log("🌐 Researching web for:", prompt);
const results = await Promise.resolve([
"web result 1",
"web result 2",
"web result 3",
]);
return results;
},
);

type ParallelResearchOutput = [string[], string[]];
interface ParallelResearchComponentProps {
prompt: string;
}
const ParallelResearch = gsx.Component<
ParallelResearchComponentProps,
ParallelResearchOutput
>(({ prompt }) => (
<>
<LLMResearchBrainstorm prompt={prompt}>
{topics => topics.map(topic => <LLMResearch topic={topic} />)}
</LLMResearchBrainstorm>
<WebResearcher prompt={prompt} />
</>
));

interface BlogWritingWorkflowProps {
prompt: string;
}
type BlogWritingWorkflowOutput = string;
export const BlogWritingWorkflow = gsx.Component<
BlogWritingWorkflowProps,
BlogWritingWorkflowOutput
>(async ({ prompt }) => (
<ParallelResearch prompt={prompt}>
{([catalogResearch, webResearch]) => {
console.log("🧠 Research:", { catalogResearch, webResearch });
return (
<LLMWriter
research={[catalogResearch.join("\n"), webResearch.join("\n")].join(
"\n\n",
)}
prompt={prompt}
>
{draft => <LLMEditor draft={draft} />}
</LLMWriter>
);
}}
</ParallelResearch>
));
23 changes: 23 additions & 0 deletions playground/chatCompletion.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { gsx } from "@/index";
import { createLLMService } from "@/llm";

const llm = createLLMService({
model: "gpt-4",
temperature: 0.7,
});

interface ChatCompletionProps {
prompt: string;
}

export const ChatCompletion = gsx.StreamComponent<ChatCompletionProps, string>(
async ({ prompt }) => {
// Use the LLM service's streaming API
const result = await llm.completeStream(prompt);

return {
stream: () => result.stream(),
value: result.value,
};
},
);
Loading

0 comments on commit 196b0e5

Please sign in to comment.