diff --git a/.prettierignore b/.prettierignore index 76f70e0c9..d2ca38d05 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,9 +1,10 @@ .pnp* .yarn* .vscode* -*data* +packages/examples/src/bakeoff/*/data/* dist .next .docusaurus -*build* packages/docs/docs/api +packages/docs/build +packages/create-react-app-demo/build/ diff --git a/packages/ai-jsx/package.json b/packages/ai-jsx/package.json index 0a6685379..f40d9126d 100644 --- a/packages/ai-jsx/package.json +++ b/packages/ai-jsx/package.json @@ -4,7 +4,7 @@ "repository": "fixie-ai/ai-jsx", "bugs": "https://github.com/fixie-ai/ai-jsx/issues", "homepage": "https://ai-jsx.com", - "version": "0.5.14", + "version": "0.5.15", "volta": { "extends": "../../package.json" }, @@ -245,6 +245,15 @@ "default": "./dist/cjs/react/completion.cjs" } }, + "./react/jit-ui/mdx": { + "import": { + "types": "./dist/esm/react/jit-ui/mdx.d.ts", + "default": "./dist/esm/react/jit-ui/mdx.js" + }, + "require": { + "default": "./dist/cjs/react/jit-ui/mdx.cjs" + } + }, "./experimental/next": { "import": { "types": "./dist/esm/experimental/next/index.d.ts", diff --git a/packages/ai-jsx/src/react/completion.tsx b/packages/ai-jsx/src/react/completion.tsx index 3827fdf92..2c732c396 100644 --- a/packages/ai-jsx/src/react/completion.tsx +++ b/packages/ai-jsx/src/react/completion.tsx @@ -7,44 +7,13 @@ import { isJsxBoundary } from './jsx-boundary.js'; import { AIJSXError, ErrorCode } from '../core/errors.js'; import z from 'zod'; -function reactComponentName(component: React.JSXElementConstructor | string) { - return typeof component === 'string' ? component : component.name; -} - export async function* UICompletion( { example, children }: { example: React.ReactNode; children: AI.Node }, { render, logger }: AI.ComponentContext ) { yield ''; - const reactComponents = new Set | string>(); - function collectComponents(node: React.ReactNode | AI.Node, inReact: boolean) { - if (Array.isArray(node)) { - node.forEach((node) => collectComponents(node, inReact)); - } - - if (React.isValidElement(node)) { - if (inReact) { - reactComponents.add(node.type); - } - - const childrenAreReact = (inReact || node.type === AI.React) && !isJsxBoundary(node.type); - if ('children' in node.props) { - collectComponents(node.props.children, childrenAreReact); - } - } - - if (AI.isElement(node)) { - const childrenAreReact = (inReact || node.tag === AI.React) && !isJsxBoundary(node.tag); - if ('children' in node.props) { - collectComponents(node.props.children, childrenAreReact); - } - } - } - - collectComponents(example, true); - - const validComponents = Object.fromEntries(Array.from(reactComponents).map((c) => [reactComponentName(c), c])); + const validComponents = collectComponents(example); interface SerializedComponent { name: string; children: (string | SerializedComponent)[]; @@ -115,3 +84,40 @@ export async function* UICompletion( yield component; } } + +/** + * Bug: this function doesn't handle symbols, so if `Component` is a fragment, it'll return + * `undefined`. + */ +function reactComponentName(component: React.JSXElementConstructor | string) { + return typeof component === 'string' ? component : component.name; +} + +export function collectComponents(node: React.ReactNode | AI.Node) { + const reactComponents = new Set | string>(); + function collectComponentsRec(node: React.ReactNode | AI.Node, inReact: boolean) { + if (Array.isArray(node)) { + node.forEach((node) => collectComponentsRec(node, inReact)); + } + + if (React.isValidElement(node)) { + if (inReact) { + reactComponents.add(node.type); + } + + const childrenAreReact = (inReact || node.type === AI.React) && !isJsxBoundary(node.type); + if ('children' in node.props) { + collectComponentsRec(node.props.children, childrenAreReact); + } + } + + if (AI.isElement(node)) { + const childrenAreReact = (inReact || node.tag === AI.React) && !isJsxBoundary(node.tag); + if ('children' in node.props) { + collectComponentsRec(node.props.children, childrenAreReact); + } + } + } + collectComponentsRec(node, true); + return Object.fromEntries(Array.from(reactComponents).map((c) => [reactComponentName(c), c])); +} diff --git a/packages/ai-jsx/src/react/jit-ui/mdx.tsx b/packages/ai-jsx/src/react/jit-ui/mdx.tsx new file mode 100644 index 000000000..67fa051d6 --- /dev/null +++ b/packages/ai-jsx/src/react/jit-ui/mdx.tsx @@ -0,0 +1,115 @@ +import * as AI from '../core.js'; +import { ChatCompletion, SystemMessage } from '../../core/completion.js'; +import React from 'react'; +import { collectComponents } from '../completion.js'; + +/** + * Use GPT-4 with this. + */ +export function MdxChatCompletion({ children, usageExamples }: { children: AI.Node; usageExamples: React.ReactNode }) { + const components = collectComponents(usageExamples); + /* prettier-ignore */ + return + + You are an assistant who can use React components to work with the user. By default, you use markdown. However, if it's useful, you can also mix in the following React components: {Object.keys(components).join(', ')}. + All your responses + should be in MDX, which is Markdown For the Component Era. Here are instructions for how to use MDX: + === Begin instructions + {/* Snipped from https://github.com/mdx-js/mdx/blob/main/docs/docs/what-is-mdx.server.mdx. */} + MDX allows you to use JSX in your markdown content. + You can import components, such as interactive charts or alerts, and embed them + within your content. + This makes writing long-form content with components a blast. + + More practically MDX can be explained as a format that combines markdown with + JSX and looks as follows: + + === Begin example + {` + Here is some markdown text + + + # Here is more markdown text + + } + /> + + * Markdown list item 1 + * Markdown list item 2 + * Markdown list item 3 + `} + === end example + === end instructions + + Do not include a starting ```mdx and closing ``` line. Just respond with the MDX itself. + + Do not include extra whitespace that is not needed for the markdown interpretation. For instance, if your component has a prop that's a JSON object, put it all on one line: + + {''} + + This doc tells you the differences between MDX and markdown. + + {/* Adapted from https://github.com/micromark/mdx-state-machine#72-deviations-from-markdown */} + === Start doc + ### 7.2 Deviations from Markdown + + MDX adds constructs to Markdown but also prohibits certain normal Markdown + constructs. + + #### 7.2.2 Indented code + + Indentation to create code blocks is not supported. + Instead, use fenced code blocks. + + The reason for this change is so that elements can be indented. + + {/* Commenting out the negative examples because they seem to confuse the LLM. */} + {/* + Incorrect: + + ```markdown + console.log(1) + ``` */} + + Correct: + + ```js + console.log(1) + ``` + + #### 7.2.3 Autolinks + + Autolinks are not supported. + Instead, use links or references. + + The reason for this change is because whether something is an element (whether + HTML or JSX) or an autolink is ambiguous {'(Markdown normally treats ``, ``, or `` as links).'} + + {/* Incorrect: + + ```markdown + See for more information + ``` */} + + Correct: + + See [example.com](https://example.com) for more information. + + #### 7.2.4 Errors + + Whereas all Markdown is valid, incorrect MDX will crash. + === end doc + + Here are the components you have available, and how to use them: + + === Begin components + {usageExamples} + === end components + + {children} + ; +} diff --git a/packages/docs/docs/changelog.md b/packages/docs/docs/changelog.md index 8c8968a1a..99c910cab 100644 --- a/packages/docs/docs/changelog.md +++ b/packages/docs/docs/changelog.md @@ -1,6 +1,10 @@ # Changelog -## 0.5.14 +## 0.5.15 + +- Add [`MdxChatCompletion`](./guides/mdx.md), so your model calls can now output [MDX](https://mdxjs.com/) using your components. + +## [0.5.14](https://github.com/fixie-ai/ai-jsx/commit/5971243) - Add [Llama2 support](./guides/models.md#llama2). diff --git a/packages/docs/docs/guides/mdx.md b/packages/docs/docs/guides/mdx.md new file mode 100644 index 000000000..be623ff3f --- /dev/null +++ b/packages/docs/docs/guides/mdx.md @@ -0,0 +1,81 @@ +# MDX Output + +By default, models emit text. If you ask them to, they'll emit markdown, which is a robust, easy way to provide more structured output to your user. + +To take it a step further, you can have the model emit [MDX](https://mdxjs.com/), which combines Markdown and your components: + +```mdx +# Last year’s snowfall + +In {year}, the snowfall was above average. +It was followed by a warm spring which caused +flood conditions in many of the nearby rivers. + + +``` + +To do this, use the `MdxChatCompletion` component: + +```tsx + + Tell me a children's story. Summarize the key characters at the end. + +``` + +[(See the examples project for a full working demo.)](https://github.com/fixie-ai/ai-jsx/blob/main/packages/examples/src/mdx.tsx) + +## Telling the model which components are available + +The API for `MdxChatCompletion` is the same as `ChatCompletion`, except it adds a `usageExamples` prop. That prop is an AI.JSX component that tells the model which components are available: + + +```tsx +const usageExample = ( + <> + Use a Card to display collected information to the user. The children can be markdown. Only use the card if you have + a logically-grouped set of information to show the user, in the context of a larger response. Generally, your entire + response should not be a card. A card takes optional header and footer props. + + Example 1 of how you might use this + component: Here's the best candidate I found: + + **Skills**: React, TypeScript, Node.js **Location**: Seattle, WA **Years of experience**: 5 **Availability**: + Full-time + + {/* ... you may wish to add more examples */} + +); + + + Tell me a children's story. Summarize the key characters at the end. + +``` + +In that example, `Card` may refer to a real component in scope, or you can just write it out as a string: + + +```tsx +function Card({ header, footer, children }) { + return ( +
+
{header}
+
{children}
+
{footer}
+
+ ); +} + +const usageExample = ( + <> + {/* Reference a component in scope */} + Here's how you use a Card: content + + {/* Just write out a string */} + You can also use a ButtonGroup: {``} + +); +``` + +## Using the output + +The output will be MDX, as a string. It will not have any import statements. If you want to render it into your UI, it's up to you to parse/compile it in some way, using the [MDX APIs](https://mdxjs.com/). diff --git a/packages/examples/.eslintrc.cjs b/packages/examples/.eslintrc.cjs index a396c1e61..f90fc1963 100644 --- a/packages/examples/.eslintrc.cjs +++ b/packages/examples/.eslintrc.cjs @@ -26,7 +26,7 @@ module.exports = { 'no-use-before-define': 'off', '@typescript-eslint/no-use-before-define': ['error', { functions: false, variables: true }], - 'no-trailing-spaces': 'warn', + 'no-trailing-spaces': 'off', 'no-else-return': ['warn', { allowElseIf: false }], // Disable style rules to let prettier own it diff --git a/packages/examples/package.json b/packages/examples/package.json index 21b0fa478..356c83987 100644 --- a/packages/examples/package.json +++ b/packages/examples/package.json @@ -35,6 +35,7 @@ "demo:multi-model-chat": "yarn build && node dist/multi-model-chat.js", "demo:simple-completion": "yarn build && node dist/simple-completion.js", "demo:getting-started": "yarn build && cp src/getting-started/data.json dist/getting-started/ && node dist/getting-started/index.js", + "demo:mdx-jit": "yarn build && node dist/mdx.js", "demo:inspector": "yarn build && node dist/inspector.js", "demo:router": "yarn build && node dist/router.js", "demo:replicate-llama2": "yarn build && node dist/replicate-llama2.js", diff --git a/packages/examples/src/bakeoff/health-agent/user-data.json b/packages/examples/src/bakeoff/health-agent/user-data.json index 05b5de2f6..e3ff10f55 100644 --- a/packages/examples/src/bakeoff/health-agent/user-data.json +++ b/packages/examples/src/bakeoff/health-agent/user-data.json @@ -1,49 +1,48 @@ { "sleep_satisfaction": 2, "daily_function": 3, - "nightly_records": - [ - { - "Date (m/d/yyyy)": "5/1/2023", - "Total Time in Bed (hrs)": 8, - "Sleep Onset Duration (hrs)": 0.5, - "Wake Before Rise Time (hrs)": 0.25 - }, - { - "Date (m/d/yyyy)": "5/2/2023", - "Total Time in Bed (hrs)": 7, - "Sleep Onset Duration (hrs)": 0.3, - "Wake Before Rise Time (hrs)": 0.20 - }, - { - "Date (m/d/yyyy)": "5/3/2023", - "Sleep Onset Duration (hrs)": 0.32, - "Total Time in Bed (hrs)": 7.5, - "Wake Before Rise Time (hrs)": 0.10 - }, - { - "Date (m/d/yyyy)": "5/4/2023", - "Total Time in Bed (hrs)": 6, - "Sleep Onset Duration (hrs)": 0.27, - "Wake Before Rise Time (hrs)": 0.30 - }, - { - "Date (m/d/yyyy)": "5/5/2023", - "Sleep Onset Duration (hrs)": 1.5, - "Total Time in Bed (hrs)": 7.5, - "Wake Before Rise Time (hrs)": 0.10 - }, - { - "Date (m/d/yyyy)": "5/6/2023", - "Total Time in Bed (hrs)": 7.5, - "Sleep Onset Duration (hrs)": 0.12, - "Wake Before Rise Time (hrs)": 0.25 - }, - { - "Date (m/d/yyyy)": "5/7/2023", - "Total Time in Bed (hrs)": 7.5, - "Sleep Onset Duration (hrs)": 0.6, - "Wake Before Rise Time (hrs)": 0.25 - } - ] - } \ No newline at end of file + "nightly_records": [ + { + "Date (m/d/yyyy)": "5/1/2023", + "Total Time in Bed (hrs)": 8, + "Sleep Onset Duration (hrs)": 0.5, + "Wake Before Rise Time (hrs)": 0.25 + }, + { + "Date (m/d/yyyy)": "5/2/2023", + "Total Time in Bed (hrs)": 7, + "Sleep Onset Duration (hrs)": 0.3, + "Wake Before Rise Time (hrs)": 0.2 + }, + { + "Date (m/d/yyyy)": "5/3/2023", + "Sleep Onset Duration (hrs)": 0.32, + "Total Time in Bed (hrs)": 7.5, + "Wake Before Rise Time (hrs)": 0.1 + }, + { + "Date (m/d/yyyy)": "5/4/2023", + "Total Time in Bed (hrs)": 6, + "Sleep Onset Duration (hrs)": 0.27, + "Wake Before Rise Time (hrs)": 0.3 + }, + { + "Date (m/d/yyyy)": "5/5/2023", + "Sleep Onset Duration (hrs)": 1.5, + "Total Time in Bed (hrs)": 7.5, + "Wake Before Rise Time (hrs)": 0.1 + }, + { + "Date (m/d/yyyy)": "5/6/2023", + "Total Time in Bed (hrs)": 7.5, + "Sleep Onset Duration (hrs)": 0.12, + "Wake Before Rise Time (hrs)": 0.25 + }, + { + "Date (m/d/yyyy)": "5/7/2023", + "Total Time in Bed (hrs)": 7.5, + "Sleep Onset Duration (hrs)": 0.6, + "Wake Before Rise Time (hrs)": 0.25 + } + ] +} diff --git a/packages/examples/src/getting-started/data.json b/packages/examples/src/getting-started/data.json index ee44b0f17..1b0c8a7f0 100644 --- a/packages/examples/src/getting-started/data.json +++ b/packages/examples/src/getting-started/data.json @@ -1,7 +1,5 @@ { "name": "sam", "age": 42, - "hobbies": [ - "painting" - ] -} \ No newline at end of file + "hobbies": ["painting"] +} diff --git a/packages/examples/src/mdx.tsx b/packages/examples/src/mdx.tsx new file mode 100644 index 000000000..38b35fb10 --- /dev/null +++ b/packages/examples/src/mdx.tsx @@ -0,0 +1,186 @@ +/** @jsxImportSource ai-jsx/react */ +import * as AI from 'ai-jsx'; +import { SystemMessage, UserMessage } from 'ai-jsx/core/completion'; +// import { showInspector } from 'ai-jsx/core/inspector'; +import { MdxChatCompletion } from 'ai-jsx/react/jit-ui/mdx'; +import { JsonChatCompletion } from 'ai-jsx/batteries/constrained-output'; +import z from 'zod'; + +import { OpenAI } from 'ai-jsx/lib/openai'; +import { PinoLogger } from 'ai-jsx/core/log'; +import { pino } from 'pino'; +import { memo } from 'ai-jsx/core/memoize'; + +/* eslint-disable @typescript-eslint/no-unused-vars */ +function Card({ header, footer, children }: { header?: string; footer?: string; children: string }) { + return null; +} +function ButtonGroup({ labels }: { labels: string[] }) { + return null; +} +function BookFlight({ flights }: { flights: any }) { + return null; +} +function BookHotel({ hotels }: { hotels: any }) { + return null; +} +/* eslint-enable @typescript-eslint/no-unused-vars */ + +/* prettier-ignore */ +const usageExample = <> + Use a Card to display collected information to the user. The children can be markdown. + + Only use the card if you have a logically-grouped set of information to show the user, in the context of a larger response. Generally, your entire response should not be a card. + + A card takes optional header and footer props. + + Example 1 of how you might use this component: + Here's the best candidate I found: + + **Skills**: React, TypeScript, Node.js + **Location**: Seattle, WA + **Years of experience**: 5 + **Availability**: Full-time + + + Example 2 of how you might use this component: + + **Leaves** at 4:15p and **arrives** at 6:20p. + + + Example 3 of how you might use this component (using with surrounding markdown): + Sure, I'd be happy to help you find a car wash. Here are some options: + + + $50 for a quick car wash. + + + $155 for a detailing + + + $10 for some guy to spray your car with a hose. + + + Example 4 of how you might use this component, after writing out a report on economics: + ... and that concludes the report on economics. + + * Price is determined by supply and demand + * Setting price floors or ceilings cause deadweight loss. + * Interfering with the natural price can also cause shortages. + + + Use a button group when the user needs to make a choice. A ButtonGroup requires a labels prop. + + Example 1 of how you might use this component: + + + Example 2 of how you might use this component (using with surrounding markdown): + The system is configured. How would you like to proceed? + + + Use a BookFlight component to let the user book a flight. BookFlight requires a flights prop. + + {/* We may be able to remove all the whitespace from the object literals in these + prop examples and thus convince the model to skip the whitespace as well, + improving performance. + */} + Example 1 of how you might use this component: + + + Example 2 of how you might use this component (using with surrounding markdown): + Here are some flights: + + + Use a BookHotel component to let the user book a hotel. BookHotel requires a hotels prop. + + Example 1 of how you might use this component: + +; + +function QuestionAndAnswer({ children }: { children: AI.Node }) { + const question = memo(children); + return ( + <> + + Q: {question} + {'\n'} + A: {question} + {'\n\n'} + + + ); +} + +export function App() { + return ( + <> + + + You are an AI that helps users book flights. The user's reservation:{' '} + + Generate a sample flight reservation. + + + Tell me about the flight reservation I just made. + + {'\n\n'} + + + You are an AI that helps users book hotels. Hotels:{' '} + + + Generate some information about hotels, including each hotel's name and how many stars it is + + + + I'd like to book a hotel + + {'\n\n'} + + You are an AI who tells stories. + Tell me a children's story. Summarize the key characters at the end. + + {'\n\n'} + + + You are an agent that can help the user buy cars. You have a few different workflows to do this. If the user + asks what they are, make some up. + + What can you help me with? + + {'\n\n'} + + ); +} + +// showInspector(); + +const logger = pino({ + name: 'ai-jsx', + level: process.env.loglevel ?? 'trace', + transport: { + target: 'pino-pretty', + options: { + colorize: true, + }, + }, +}); + +let lastValue = ''; +const rendering = AI.createRenderContext({ logger: new PinoLogger(logger) }).render(, { appendOnly: true }); +for await (const frame of rendering) { + process.stdout.write(frame.slice(lastValue.length)); + lastValue = frame; +}