diff --git a/README.md b/README.md index fc374c5d..4437ab3f 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,7 @@ This repo contains a number of [examples](./examples) to help you get up and run | šŸ”„ [Reflection](./examples/reflection) | Shows how to use a self-reflection pattern with GenSX | | šŸŒŠ [Streaming](./examples/streaming) | Shows how to handle streaming responses with GenSX | | šŸ”Œ [Providers](./examples/providers) | Shows how to create a custom provider for GenSX | +| šŸ—ƒļø [Contexts](./examples/contexts) | Shows how to use contexts to manage state in GenSX | ### Full Examples diff --git a/docs/src/content/docs/basic-concepts.mdx b/docs/src/content/docs/basic-concepts.mdx index 061debaa..84303486 100644 --- a/docs/src/content/docs/basic-concepts.mdx +++ b/docs/src/content/docs/basic-concepts.mdx @@ -149,7 +149,7 @@ Rather than having to run an entire workflow to test a change to a single compon ## Contexts -Contexts provide a way to share values across components without explicitly passing them through props. They're similar to React's Context system but adapted for GenSX workflows. Contexts are particularly useful for: +Contexts provide a way to share data across components without explicitly passing them through props. They're similar to React's Context API but adapted for GenSX workflows. Contexts are particularly useful for: - Sharing configuration across multiple components - Providing dependencies to deeply nested components @@ -158,27 +158,30 @@ Contexts provide a way to share values across components without explicitly pass Here's how to create and use a context: ```tsx +interface User { + name: string; +} + // Create a context with a default value -const UserContext = gsx.createContext({ name: "default", age: 0 }); +const UserContext = gsx.createContext({ name: "" }); // Use the context in a component -const Greeting = gsx.Component( - "Greeting", - () => { - const user = gsx.useContext(UserContext); - return `Hello, ${user.name}!`; - }, -); +const Greeting = gsx.Component, string>("Greeting", () => { + const user = gsx.useContext(UserContext); + return `Hello, ${user.name}!`; +}); // Provide a value to the context const result = await gsx.execute( - + , ); ``` -Contexts are a useful way to pass configuration without prop drilling. However, they do make your components less reusable so it is recommended that you use them sparingly. +Contexts are a useful way to pass configuration without prop drilling. However, they do make your components less reusable so we recommend that you use them sparingly. + +For more details on providers, see the [Contexts](/docs/concepts/context) page. ## Providers diff --git a/docs/src/content/docs/concepts/context.mdx b/docs/src/content/docs/concepts/context.mdx new file mode 100644 index 00000000..516d183b --- /dev/null +++ b/docs/src/content/docs/concepts/context.mdx @@ -0,0 +1,199 @@ +--- +title: Context and providers +description: Learn how to use contexts and providers in GenSX to share data across components. +sidebar: + order: 3 +--- + +Contexts and providers are powerful tools in GenSX for sharing data and managing configuration across components without explicitly passing props through every level of your component tree. They work similarly to [React's Context API](https://react.dev/reference/react/useContext) but are adapted to work with GenSX workflows. + +## What are contexts and providers? + +Contexts and providers work together to share data and manage dependencies across components. + +- **Contexts** give you a way to share data (like state, configuration, or dependencies) across components without manually passing props down the component tree. +- **Providers** are components that supply data or services to a context. Any component within a provider's subtree can access the context. + +The two concepts are interdependent so you can't use one without the other. Combined, they're great for: + +- Providing data to components without prop drilling +- Sharing configuration and dependencies, such as clients, for your workflow +- Managing state that needs to be accessed by multiple components + +The remainder of this document will show you how to create and use both contexts and providers in GenSX. + +## Creating and using contexts + +This next section walks through the steps needed to create and use a context in your GenSX workflow. + +### Step 1: Create a context + +To create a context, start by defining its interface and then use `gsx.createContext()` to initialize it along with a default value. For example, here's how to create a `User` context: + +```tsx +import { gsx } from "gensx"; + +// Define the interface +interface User { + name: string; +} + +// Create a context with a default value +const UserContext = gsx.createContext({ + name: "", +}); +``` + +### Step 2: Use the context in a component + +To use the context, call the `gsx.useContext(context)` hook inside of a component. Here a `Greeting` component is created that uses the `UserContext` to get the user's name: + +```tsx +const Greeting = gsx.Component, GreetingOutput>( + "Greeting", + () => { + const user = gsx.useContext(UserContext); + return `Hello, ${user.name}!`; + }, +); +``` + +### Step 3: Provide the context value + +To make the context value available to your components, you need to wrap your component in a `Provider` component and pass in a value via the `value` prop: + +```tsx +const result = await gsx.execute( + + + , +); +``` + +## Using providers for configuration and dependencies + +Providers are a specialized way to use contexts that focus on managing configuration and dependencies for your workflow. They simplify the process of sharing data like API keys, client instances, or feature flags across your components. + +### Built-in providers + +The main provider available today is the `OpenAIProvider`, which manages your OpenAI API key and client: + +```tsx +const result = await gsx.execute( + + + , +); +``` + +### Creating a Custom Provider + +If you need a provider that isn't available out of the box, you can easily create your own. The example below shows how to create a provider for the [Firecrawl](https://www.firecrawl.dev/) API. + +#### Step 1: Create a context + +Start by importing `gsx` and the package you want to use: + +```tsx +import { gsx } from "gensx"; +import FirecrawlApp, { FirecrawlAppConfig } from "@mendable/firecrawl-js"; +``` + +Then, create the context: + +```tsx +// Create a context +export const FirecrawlContext = gsx.createContext<{ + client?: FirecrawlApp; +}>({}); +``` + +The context contains the `client` that you'll use to interact with the Firecrawl API. + +#### Step 2: Create the provider + +Next, wrap your context in a provider component: + +```tsx +// Create the provider +export const FirecrawlProvider = gsx.Component( + "FirecrawlProvider", + (args: FirecrawlAppConfig) => { + const client = new FirecrawlApp({ + apiKey: args.apiKey, + }); + return ; + }, +); +``` + +The provider will take in the `apiKey` as a prop and use it to initialize the Firecrawl client. + +#### Step 3: Use the provider in a component + +Finally, you can build components that consume the context supplied by the provider: + +```tsx +export const ScrapePage = gsx.Component( + "ScrapePage", + async ({ url }) => { + const context = gsx.useContext(FirecrawlContext); + + if (!context.client) { + throw new Error( + "Firecrawl client not found. Please wrap your component with FirecrawlProvider.", + ); + } + const result = await context.client.scrapeUrl(url, { + formats: ["markdown"], + timeout: 30000, + }); + + if (!result.success || !result.markdown) { + throw new Error(`Failed to scrape url: ${url}`); + } + + return result.markdown; + }, +); +``` + +#### Step 4: Use the provider in your workflow + +Now when you use the `ScrapePage` component in your workflow, you'll wrap it in the `FirecrawlProvider` and pass in the `apiKey`: + +```tsx +const markdown = await gsx.execute( + + + , +); +``` + +## Nesting Providers + +You can nest multiple providers to combine different services or configurations in your workflow. This is useful when a component needs access to multiple contexts. Here's an example that combines the OpenAI provider with our custom Firecrawl provider: + +```tsx +const result = await gsx.execute( + + + + + , +); +``` + +In this example, the `WebPageSummarizer` component can access both the OpenAI client and Firecrawl client through their respective contexts. + +The order of nesting doesn't matter as long as the component using a context is wrapped by its corresponding provider somewhere up the tree. + +## Additional Resources + +You can find the full example code demonstrating these concepts on GitHub: + +- [Context examples](https://github.com/gensx-inc/gensx/tree/main/examples/contexts) +- [Provider examples](https://github.com/gensx-inc/gensx/tree/main/examples/providers) diff --git a/docs/src/content/docs/concepts/providers.mdx b/docs/src/content/docs/concepts/providers.mdx deleted file mode 100644 index 252f9086..00000000 --- a/docs/src/content/docs/concepts/providers.mdx +++ /dev/null @@ -1,96 +0,0 @@ ---- -title: Providers -description: Learn how to manage API keys and configuration dependencies in GenSX using providers -sidebar: - order: 3 ---- - -Providers are a specialized use of [contexts](../contexts) that focus on managing configuration and dependencies for your workflow. They're built using the context system, but provide a more focused API for configuration management. - -The main provider available today is the `OpenAIProvider`, which manages your OpenAI API key and client: - -```tsx -const result = await gsx.execute( - - - , -); -``` - -Providers offer several benefits: - -- **Centralized Configuration Management** - providers manage settings like API keys and feature flags keeping configuration separate from business logic. -- **Data Sharing** - providers give you an easy way to share data between components without prop drilling to simplify your code. -- **Reusability** - providers and their associated components can easily be reused in different parts of your application. -- **Scoped State Management** - providers can be used to manage state for parts of your workflow or the entire thing. - -## Creating a Provider - -If you want to use a provide that isn't available out of the box, you can easily create your own. The example below shows how to create a provider for the [Firecrawl](https://www.firecrawl.dev/) API. - -Start by importing `gsx` and the package you want to use: - -```tsx -import { gsx } from "gensx"; -import FirecrawlApp, { FirecrawlAppConfig } from "@mendable/firecrawl-js"; -``` - -Next, create a context and then the provider: - -```tsx -// Create a context -export const FirecrawlContext = gsx.createContext<{ - client?: FirecrawlApp; -}>({}); - -// Create the provider -export const FirecrawlProvider = gsx.Component( - "FirecrawlProvider", - (args: FirecrawlAppConfig) => { - const client = new FirecrawlApp({ - apiKey: args.apiKey, - }); - return ; - }, -); -``` - -Finally, you can build components that use the provider. - -```tsx -export const ScrapePage = gsx.Component( - "ScrapePage", - async ({ url }) => { - const context = gsx.useContext(FirecrawlContext); - - if (!context.client) { - throw new Error( - "Firecrawl client not found. Please wrap your component with FirecrawlProvider.", - ); - } - const result = await context.client.scrapeUrl(url, { - formats: ["markdown"], - timeout: 30000, - }); - - if (!result.success || !result.markdown) { - throw new Error(`Failed to scrape url: ${url}`); - } - - return result.markdown; - }, -); -``` - -To use the `FirecrawlProvider` and `ScrapePage` component, you would then do the following: - -```tsx -const markdown = await gsx.execute( - - - , -); -``` diff --git a/examples/README.md b/examples/README.md index 192ace23..2d8d37e4 100644 --- a/examples/README.md +++ b/examples/README.md @@ -30,6 +30,7 @@ Make sure to check what environment variables are required for each example. | šŸ”„ [Reflection](./reflection) | Shows how to use a self-reflection pattern with GenSX | | šŸŒŠ [Streaming](./streaming) | Demonstrates how to handle streaming responses with GenSX | | šŸ”Œ [Providers](./providers) | Shows how to create a custom provider for GenSX | +| šŸ—ƒļø [Contexts](./contexts) | Shows how to use contexts to manage state in GenSX | ## Full Examples diff --git a/examples/contexts/README.md b/examples/contexts/README.md new file mode 100644 index 00000000..bf9789ad --- /dev/null +++ b/examples/contexts/README.md @@ -0,0 +1,13 @@ +# Contexts Example + +This example shows how to create and use contexts in GenSX. For more details on contexts, see the [Contexts](https://gensx.dev/concepts/context) page. + +## Usage + +```bash +# Install dependencies +pnpm install + +# Run the example +pnpm run start +``` diff --git a/examples/contexts/eslint.config.mjs b/examples/contexts/eslint.config.mjs new file mode 100644 index 00000000..1e822356 --- /dev/null +++ b/examples/contexts/eslint.config.mjs @@ -0,0 +1,22 @@ +import rootConfig from "../../eslint.config.mjs"; + +export default [ + ...rootConfig, + { + files: ["**/*.{js,mjs,cjs,jsx,ts,tsx}"], + languageOptions: { + parserOptions: { + tsconfigRootDir: import.meta.dirname, + project: "./tsconfig.json", + }, + }, + rules: { + "@typescript-eslint/no-unsafe-assignment": "off", + "@typescript-eslint/no-unsafe-call": "off", + "@typescript-eslint/no-unsafe-member-access": "off", + "@typescript-eslint/no-unsafe-return": "off", + "@typescript-eslint/no-unsafe-argument": "off", + "node/no-unsupported-features/es-syntax": "off", + }, + }, +]; diff --git a/examples/contexts/index.tsx b/examples/contexts/index.tsx new file mode 100644 index 00000000..08950dd9 --- /dev/null +++ b/examples/contexts/index.tsx @@ -0,0 +1,37 @@ +import { gsx } from "gensx"; + +interface User { + name: string; +} + +// Create a context with a default value +const UserContext = gsx.createContext({ + name: "", +}); + +type GreetingOutput = string; + +// Use the context in a component +const Greeting = gsx.Component, GreetingOutput>( + "Greeting", + () => { + const user = gsx.useContext(UserContext); + return `Hello, ${user.name}!`; + }, +); + +async function main() { + // Provide a value to the context + const result = await gsx.execute( + + + , + ); + console.log(result); +} + +main().catch(console.error); diff --git a/examples/contexts/nodemon.json b/examples/contexts/nodemon.json new file mode 100644 index 00000000..ab6199f7 --- /dev/null +++ b/examples/contexts/nodemon.json @@ -0,0 +1,5 @@ +{ + "watch": [".", ".env", "../../packages/**/dist/**"], + "exec": "NODE_OPTIONS='--enable-source-maps' tsx --inspect=0.0.0.0:9229 ./index.tsx", + "ext": "ts, tsx, js, json" +} diff --git a/examples/contexts/package.json b/examples/contexts/package.json new file mode 100644 index 00000000..7928662f --- /dev/null +++ b/examples/contexts/package.json @@ -0,0 +1,25 @@ +{ + "name": "@gensx-examples/contexts", + "private": true, + "version": "0.0.0", + "type": "module", + "engines": { + "node": ">=18.0.0" + }, + "scripts": { + "dev": "nodemon", + "start": "NODE_OPTIONS='--enable-source-maps' tsx ./index.tsx", + "build": "tsc", + "lint": "eslint .", + "lint:fix": "eslint . --fix" + }, + "dependencies": { + "gensx": "workspace:*" + }, + "devDependencies": { + "@types/node": "^20.17.11", + "nodemon": "^3.1.9", + "tsx": "^4.19.2", + "typescript": "^5.0.0" + } +} diff --git a/examples/contexts/tsconfig.json b/examples/contexts/tsconfig.json new file mode 100644 index 00000000..3ee9986d --- /dev/null +++ b/examples/contexts/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "NodeNext", + "lib": ["ESNext", "DOM"], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "moduleResolution": "NodeNext", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "react-jsx", + "jsxImportSource": "gensx", + "outDir": "./dist" + }, + "include": ["./*.ts", "./*.tsx", "./**/*.ts", "./**/*.tsx"], + "exclude": ["node_modules"] +} diff --git a/examples/providers/index.tsx b/examples/providers/index.tsx index 68e5b70b..4b692ab4 100644 --- a/examples/providers/index.tsx +++ b/examples/providers/index.tsx @@ -3,7 +3,7 @@ import { gsx } from "gensx"; import { FirecrawlProvider, ScrapePage } from "./firecrawlProvider.js"; async function main() { - const url = "https://gensx.dev/overview/"; + const url = "https://gensx.com/overview/"; console.log("\nšŸš€ Scraping page from url:", url); const markdown = await gsx.execute( diff --git a/examples/providers/package.json b/examples/providers/package.json index 29aa03b4..f27ade1b 100644 --- a/examples/providers/package.json +++ b/examples/providers/package.json @@ -14,10 +14,8 @@ "lint:fix": "eslint . --fix" }, "dependencies": { - "@gensx/openai": "workspace:*", "@mendable/firecrawl-js": "^1.15.7", - "gensx": "workspace:*", - "openai": "^4.77.0" + "gensx": "workspace:*" }, "devDependencies": { "@types/node": "^20.17.11", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7cc593e6..c42164f0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -110,6 +110,25 @@ importers: specifier: ^5.0.0 version: 5.7.2 + examples/contexts: + dependencies: + gensx: + specifier: workspace:* + version: link:../../packages/gensx + devDependencies: + '@types/node': + specifier: ^20.17.11 + version: 20.17.11 + nodemon: + specifier: ^3.1.9 + version: 3.1.9 + tsx: + specifier: ^4.19.2 + version: 4.19.2 + typescript: + specifier: ^5.0.0 + version: 5.7.2 + examples/deepResearch: dependencies: '@gensx/openai': @@ -177,18 +196,12 @@ importers: examples/providers: dependencies: - '@gensx/openai': - specifier: workspace:* - version: link:../../packages/gensx-openai '@mendable/firecrawl-js': specifier: ^1.15.7 version: 1.15.7(ws@8.18.0) gensx: specifier: workspace:* version: link:../../packages/gensx - openai: - specifier: ^4.77.0 - version: 4.77.4(zod@3.24.1) devDependencies: '@types/node': specifier: ^20.17.11