Skip to content

Commit

Permalink
feat: Implement OpenAI helper (#47)
Browse files Browse the repository at this point in the history
## Proposed changes

This builds the initial implementation for `@gensx/openai`.

A few additions to the framework were required:

* Plumbing through context everywhere so that children inherit context
* Add `ContextProvider` wrapper that allows you to inject context

Also update the BlogWriter example and the HackerNews example to use the
`@gensx/openai` package.
  • Loading branch information
jmoseley authored Jan 3, 2025
1 parent 4041a30 commit df6856b
Show file tree
Hide file tree
Showing 37 changed files with 785 additions and 394 deletions.
3 changes: 2 additions & 1 deletion .vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"recommendations": [
"dbaeumer.vscode-eslint",
"streetsidesoftware.code-spell-checker"
"streetsidesoftware.code-spell-checker",
"vitest.explorer"
]
}
28 changes: 28 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Debug Gensx Tests",
"autoAttachChildProcesses": true,
"skipFiles": [
"<node_internals>/**",
"**/node_modules/**"
],
"program": "${workspaceRoot}/packages/gensx/node_modules/vitest/vitest.mjs",
"args": [
"--config",
"${workspaceRoot}/packages/gensx/vitest.config.ts",
"run",
"${file}"
],
"smartStep": true,
"console": "integratedTerminal",
"cwd": "${workspaceRoot}/packages/gensx",
"env": {
"NODE_ENV": "test"
}
}
]
}
120 changes: 83 additions & 37 deletions examples/blogWriter/blogWriter.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,36 @@
import { ChatCompletion, OpenAIProvider } from "@gensx/openai";
import { gsx } from "gensx";

interface LLMResearchBrainstormProps {
prompt: string;
}
type LLMResearchBrainstormOutput = string[];
interface LLMResearchBrainstormOutput {
topics: 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;
const systemPrompt = `You are a helpful assistant that brainstorms topics for a researching a blog post. The user will provide a prompt and you will brainstorm topics based on the prompt. You should return 3 - 5 topics, as a JSON array.
Here is an example of the JSON output: { "topics": ["topic 1", "topic 2", "topic 3"] }`;
return (
<ChatCompletion
model="gpt-4o-mini"
temperature={0.5}
messages={[
{ role: "system", content: systemPrompt },
{ role: "user", content: prompt },
]}
response_format={{ type: "json_object" }}
>
{
// TODO: Figure out why this needs a type annotation, but other components do not.
(completion: string) => JSON.parse(completion ?? '{ "topics": [] }')
}
</ChatCompletion>
);
});

interface LLMResearchProps {
Expand All @@ -20,34 +40,65 @@ type LLMResearchOutput = string;
const LLMResearch = gsx.Component<LLMResearchProps, LLMResearchOutput>(
async ({ topic }) => {
console.log("📚 Researching topic:", topic);
return await Promise.resolve(`research results for ${topic}`);
const systemPrompt = `You are a helpful assistant that researches topics. The user will provide a topic and you will research the topic. You should return a summary of the research, summarizing the most important points in a few sentences at most.`;

return (
<ChatCompletion
model="gpt-4o-mini"
temperature={0}
messages={[
{ role: "system", content: systemPrompt },
{ role: "user", content: topic },
]}
/>
);
},
);

interface LLMWriterProps {
research: string;
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`,
async ({ prompt, research }) => {
const systemPrompt = `You are a helpful assistant that writes blog posts. The user will provide a prompt and you will write a blog post based on the prompt. Unless specified by the user, the blog post should be 200 words.
Here is the research for the blog post: ${research.join("\n")}`;

console.log("🚀 Writing blog post for:", { prompt, research });
return (
<ChatCompletion
model="gpt-4o-mini"
temperature={0}
messages={[
{ role: "system", content: systemPrompt },
{ role: "user", content: prompt },
]}
/>
);
},
);

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}`);
},
);
const LLMEditor = gsx.StreamComponent<LLMEditorProps>(async ({ draft }) => {
console.log("🔍 Editing draft");
const systemPrompt = `You are a helpful assistant that edits blog posts. The user will provide a draft and you will edit it to make it more engaging and interesting.`;

return (
<ChatCompletion
stream={true}
model="gpt-4o-mini"
temperature={0}
messages={[
{ role: "system", content: systemPrompt },
{ role: "user", content: draft },
]}
/>
);
});

interface WebResearcherProps {
prompt: string;
Expand Down Expand Up @@ -75,7 +126,9 @@ const ParallelResearch = gsx.Component<
>(({ prompt }) => (
<>
<LLMResearchBrainstorm prompt={prompt}>
{(topics) => topics.map((topic) => <LLMResearch topic={topic} />)}
{({ topics }) => {
return topics.map((topic) => <LLMResearch topic={topic} />);
}}
</LLMResearchBrainstorm>
<WebResearcher prompt={prompt} />
</>
Expand All @@ -84,24 +137,17 @@ const ParallelResearch = gsx.Component<
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",
export const BlogWritingWorkflow =
gsx.StreamComponent<BlogWritingWorkflowProps>(async ({ prompt }) => {
return (
<OpenAIProvider apiKey={process.env.OPENAI_API_KEY}>
<ParallelResearch prompt={prompt}>
{(research) => (
<LLMWriter prompt={prompt} research={research.flat()}>
{(draft) => <LLMEditor draft={draft} stream={true} />}
</LLMWriter>
)}
prompt={prompt}
>
{(draft) => <LLMEditor draft={draft} />}
</LLMWriter>
);
}}
</ParallelResearch>
));
</ParallelResearch>
</OpenAIProvider>
);
});
14 changes: 10 additions & 4 deletions examples/blogWriter/index.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import { gsx } from "gensx";
import { gsx, Streamable } from "gensx";

import { BlogWritingWorkflow } from "./blogWriter.js";

async function main() {
console.log("\n🚀 Starting blog writing workflow");
const result = await gsx.execute<string>(
<BlogWritingWorkflow prompt="Write a blog post about the future of AI" />,
const stream = await gsx.execute<Streamable>(
<BlogWritingWorkflow
stream={true}
prompt="Write a blog post about the future of AI"
/>,
);
console.log("✅ Blog writing complete:", { result });
for await (const chunk of stream) {
process.stdout.write(chunk);
}
console.log("\n✅ Blog writing complete");
}

main().catch(console.error);
4 changes: 2 additions & 2 deletions examples/blogWriter/nodemon.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"watch": [".", ".env"],
"exec": "tsx --inspect=0.0.0.0:9229 ./index.tsx",
"watch": [".", ".env", "../../packages/**/dist/**"],
"exec": "NODE_OPTIONS='--enable-source-maps' tsx --inspect=0.0.0.0:9229 ./index.tsx",
"ext": "ts, tsx, js, json"
}

5 changes: 3 additions & 2 deletions examples/blogWriter/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@
},
"scripts": {
"dev": "nodemon",
"run": "tsx ./index.tsx",
"run": "NODE_OPTIONS='--enable-source-maps' tsx ./index.tsx",
"build": "tsc",
"lint": "eslint . --ext .ts,.tsx",
"lint:fix": "eslint . --ext .ts,.tsx --fix",
"test": "tsx ./index.tsx"
"test": "NODE_OPTIONS='--enable-source-maps' tsx ./index.tsx"
},
"dependencies": {
"gensx": "workspace:*",
"@gensx/openai": "workspace:*",
"openai": "^4.77.0"
},
"devDependencies": {
Expand Down
3 changes: 2 additions & 1 deletion examples/blogWriter/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
"target": "ESNext",
"module": "NodeNext",
"lib": [
"ESNext"
"ESNext",
"DOM"
],
"strict": true,
"esModuleInterop": true,
Expand Down
Loading

0 comments on commit df6856b

Please sign in to comment.