├── apps
├── web
│ ├── README.md
│ ├── .eslintrc.cjs
│ ├── src
│ │ ├── lib
│ │ │ ├── enums.ts
│ │ │ ├── format-functions.ts
│ │ │ ├── utils.ts
│ │ │ ├── is-function-call.ts
│ │ │ └── parse-function-call.ts
│ │ ├── app
│ │ │ ├── page.tsx
│ │ │ ├── api
│ │ │ │ ├── inngest
│ │ │ │ │ └── route.ts
│ │ │ │ └── chat
│ │ │ │ │ └── route.ts
│ │ │ ├── layout.tsx
│ │ │ └── globals.css
│ │ ├── inngest
│ │ │ ├── inngest.server.client.ts
│ │ │ ├── ai-flow.ts
│ │ │ ├── message-writer.ts
│ │ │ └── function-invoker.ts
│ │ ├── components
│ │ │ ├── ui
│ │ │ │ ├── textarea.tsx
│ │ │ │ └── button.tsx
│ │ │ ├── icons.tsx
│ │ │ └── chat-box.tsx
│ │ ├── types.ts
│ │ └── hooks
│ │ │ └── use-backend-chat.ts
│ ├── next.config.js
│ ├── postcss.config.js
│ ├── .env.production
│ ├── .env.template
│ ├── .env.development
│ ├── next-env.d.ts
│ ├── .gitignore
│ ├── tsconfig.json
│ ├── package.json
│ └── tailwind.config.js
└── openai-party
│ ├── partykit.json
│ ├── public
│ ├── logo.png
│ ├── favicon.ico
│ ├── favicon-16x16.png
│ ├── favicon-32x32.png
│ ├── apple-touch-icon.png
│ ├── android-chrome-192x192.png
│ ├── android-chrome-512x512.png
│ ├── site.webmanifest
│ ├── index.html
│ └── normalize.css
│ ├── package.json
│ ├── src
│ ├── styles.css
│ └── server.ts
│ ├── README.md
│ ├── .gitignore
│ └── tsconfig.json
├── .npmrc
├── pnpm-workspace.yaml
├── tsconfig.json
├── packages
├── eslint-config-custom
│ ├── README.md
│ ├── package.json
│ ├── library.js
│ ├── react-internal.js
│ └── next.js
└── tsconfig
│ ├── package.json
│ ├── react-library.json
│ ├── base.json
│ └── nextjs.json
├── turbo.json
├── package.json
├── .gitignore
├── LICENSE
└── README.md
/apps/web/README.md:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | auto-install-peers = true
2 |
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | packages:
2 | - "packages/*"
3 | - "apps/*"
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "tsconfig/base.json",
3 | }
4 |
--------------------------------------------------------------------------------
/apps/web/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 |
3 | extends: ["custom/next"],
4 | };
5 |
--------------------------------------------------------------------------------
/apps/web/src/lib/enums.ts:
--------------------------------------------------------------------------------
1 | export const DONE = "\\ok";
2 | export const CONFIRM = "\\confirm";
--------------------------------------------------------------------------------
/apps/openai-party/partykit.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "openai-party",
3 | "main": "src/server.ts"
4 | }
5 |
--------------------------------------------------------------------------------
/apps/web/next.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | reactStrictMode: true,
3 | transpilePackages: [],
4 | };
5 |
--------------------------------------------------------------------------------
/packages/eslint-config-custom/README.md:
--------------------------------------------------------------------------------
1 | # `@turbo/eslint-config`
2 |
3 | Collection of internal eslint configurations.
4 |
--------------------------------------------------------------------------------
/apps/web/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/apps/web/.env.production:
--------------------------------------------------------------------------------
1 | NEXT_PUBLIC_PARTY_KIT_URL=https://my-party.joelhooks.partykit.dev
2 | OPENAI_MODEL_NAME=gpt-3.5-turbo-16k-0613
--------------------------------------------------------------------------------
/apps/openai-party/public/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joelhooks/inngest-partykit-nextjs-openai/HEAD/apps/openai-party/public/logo.png
--------------------------------------------------------------------------------
/apps/openai-party/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joelhooks/inngest-partykit-nextjs-openai/HEAD/apps/openai-party/public/favicon.ico
--------------------------------------------------------------------------------
/apps/web/.env.template:
--------------------------------------------------------------------------------
1 | NEXT_PUBLIC_PARTY_KIT_URL=http://127.0.0.1:1999
2 | OPENAI_API_KEY=
3 | LINEAR_API_KEY=
4 | INNGEST_EVENT_KEY=
5 | INNGEST_SIGNING_KEY=
--------------------------------------------------------------------------------
/apps/openai-party/public/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joelhooks/inngest-partykit-nextjs-openai/HEAD/apps/openai-party/public/favicon-16x16.png
--------------------------------------------------------------------------------
/apps/openai-party/public/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joelhooks/inngest-partykit-nextjs-openai/HEAD/apps/openai-party/public/favicon-32x32.png
--------------------------------------------------------------------------------
/apps/openai-party/public/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joelhooks/inngest-partykit-nextjs-openai/HEAD/apps/openai-party/public/apple-touch-icon.png
--------------------------------------------------------------------------------
/apps/web/src/app/page.tsx:
--------------------------------------------------------------------------------
1 | import Chat from "@/components/chat-box";
2 |
3 | export default function Page(): JSX.Element {
4 | return (
5 |
6 | );
7 | }
8 |
--------------------------------------------------------------------------------
/apps/openai-party/public/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joelhooks/inngest-partykit-nextjs-openai/HEAD/apps/openai-party/public/android-chrome-192x192.png
--------------------------------------------------------------------------------
/apps/openai-party/public/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joelhooks/inngest-partykit-nextjs-openai/HEAD/apps/openai-party/public/android-chrome-512x512.png
--------------------------------------------------------------------------------
/apps/web/src/lib/format-functions.ts:
--------------------------------------------------------------------------------
1 | import type { APIDocs, Functions } from "../types";
2 |
3 | export const formatFunctions = (f: Functions): APIDocs[] => {
4 | return Object.values(f).map((g) => g.docs);
5 | };
--------------------------------------------------------------------------------
/apps/web/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { type ClassValue, clsx } from "clsx";
2 | import { twMerge } from "tailwind-merge";
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs));
6 | }
7 |
--------------------------------------------------------------------------------
/apps/web/.env.development:
--------------------------------------------------------------------------------
1 | NEXT_PUBLIC_PARTY_KIT_URL=http://127.0.0.1:1999
2 | NEXT_PUBLIC_PARTYKIT_ROOM_NAME=linear-issues-manager
3 | INNGEST_EVENT_KEY=local_1
4 | INNGEST_SIGNING_KEY=inn_1_sk
5 | OPENAI_MODEL_NAME=gpt-3.5-turbo-16k-0613
6 |
--------------------------------------------------------------------------------
/apps/web/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/basic-features/typescript for more information.
6 |
--------------------------------------------------------------------------------
/apps/web/src/lib/is-function-call.ts:
--------------------------------------------------------------------------------
1 | import type { AIMessage } from "../types";
2 |
3 | export const isFunctionCall = (o: AIMessage[]): boolean => {
4 | const last = o[o.length - 1];
5 | return !!(last as AIMessage)?.function_call?.name;
6 | };
--------------------------------------------------------------------------------
/apps/web/src/app/api/inngest/route.ts:
--------------------------------------------------------------------------------
1 | import { serve } from "inngest/next";
2 | import { inngest } from "@/inngest/inngest.server.client";
3 | import { aibot } from "@/inngest/ai-flow";
4 |
5 | export const { GET, POST, PUT } = serve(inngest, [aibot]);
6 |
--------------------------------------------------------------------------------
/packages/tsconfig/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tsconfig",
3 | "version": "0.0.0",
4 | "private": true,
5 | "license": "MIT",
6 | "files": [
7 | "base.json",
8 | "nextjs.json"
9 | ],
10 | "publishConfig": {
11 | "access": "public"
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/packages/tsconfig/react-library.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "display": "React Library",
4 | "extends": "./base.json",
5 | "compilerOptions": {
6 | "jsx": "react-jsx",
7 | "lib": ["ES2015", "DOM"],
8 | "module": "ESNext",
9 | "target": "esnext"
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/packages/eslint-config-custom/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "eslint-config-custom",
3 | "license": "MIT",
4 | "version": "0.0.0",
5 | "private": true,
6 | "devDependencies": {
7 | "@next/eslint-plugin-next": "^13.4.19",
8 | "@vercel/style-guide": "^4.0.2",
9 | "eslint-config-turbo": "^1.10.12"
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/apps/web/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import "./globals.css";
2 | import { Inter } from "next/font/google";
3 |
4 | const inter = Inter({ subsets: ["latin"] });
5 |
6 | export default function RootLayout({
7 | children,
8 | }: {
9 | children: React.ReactNode;
10 | }): JSX.Element {
11 | return (
12 |
13 |
{children}
14 |
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/apps/openai-party/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "openai-party",
3 | "version": "0.0.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "partykit dev",
7 | "deploy": "partykit deploy"
8 | },
9 | "dependencies": {
10 |
11 | "inngest": "^2.5.2",
12 | "partysocket": "0.0.0-3228764"
13 |
14 | },
15 | "devDependencies": {
16 | "partykit": "0.0.0-3228764",
17 | "typescript": "^5.1.6"
18 | }
19 | }
--------------------------------------------------------------------------------
/turbo.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://turbo.build/schema.json",
3 | "globalDependencies": ["**/.env.*local"],
4 | "pipeline": {
5 | "build": {
6 | "dependsOn": ["^build"],
7 | "env": ["OPENAI_API_KEY", "NEXT_PUBLIC_PARTY_KIT_URL", "OPENAI_MODEL_NAME"],
8 | "outputs": [".next/**", "!.next/cache/**"]
9 | },
10 | "lint": {},
11 | "dev": {
12 | "cache": false,
13 | "persistent": true
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/apps/openai-party/public/site.webmanifest:
--------------------------------------------------------------------------------
1 | {
2 | "name": "PartyKit",
3 | "short_name": "PartyKit",
4 | "icons": [
5 | {
6 | "src": "/android-chrome-192x192.png",
7 | "sizes": "192x192",
8 | "type": "image/png"
9 | },
10 | {
11 | "src": "/android-chrome-512x512.png",
12 | "sizes": "512x512",
13 | "type": "image/png"
14 | }
15 | ],
16 | "theme_color": "#ffffff",
17 | "background_color": "#ffffff",
18 | "display": "standalone"
19 | }
20 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "scripts": {
4 | "build": "turbo run build",
5 | "dev": "turbo run dev",
6 | "dev-turbo": "turbo run dev",
7 | "lint": "turbo run lint",
8 | "format": "prettier --write \"**/*.{ts,tsx,md}\""
9 | },
10 | "devDependencies": {
11 | "eslint": "^8.47.0",
12 | "prettier": "^3.0.2",
13 | "tsconfig": "workspace:*",
14 | "turbo": "latest",
15 | "typescript": "^5.1.6"
16 | },
17 | "name": "inngest-partykit-nextjs-openai"
18 | }
19 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | node_modules
5 | .pnp
6 | .pnp.js
7 |
8 | # testing
9 | coverage
10 |
11 | # next.js
12 | .next/
13 | out/
14 | build
15 |
16 | # misc
17 | .DS_Store
18 | *.pem
19 |
20 | # debug
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
25 | # local env files
26 | .env
27 | .env.local
28 | .env.development.local
29 | .env.test.local
30 | .env.production.local
31 |
32 | # turbo
33 | .turbo
34 |
35 | # vercel
36 | .vercel
37 |
--------------------------------------------------------------------------------
/apps/web/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # local env files
28 | .env.local
29 | .env.development.local
30 | .env.test.local
31 | .env.production.local
32 |
33 | # vercel
34 | .vercel
35 |
--------------------------------------------------------------------------------
/apps/openai-party/src/styles.css:
--------------------------------------------------------------------------------
1 | /*
2 | We've already included normalize.css.
3 |
4 | But we'd like a modern looking boilerplate.
5 | Clean type, sans-serif, and a nice color palette.
6 |
7 | */
8 |
9 | body {
10 | font-family: sans-serif;
11 | font-size: 16px;
12 | line-height: 1.5;
13 | color: #333;
14 | }
15 |
16 | h1,
17 | h2,
18 | h3,
19 | h4,
20 | h5,
21 | h6 {
22 | font-family: sans-serif;
23 | font-weight: 600;
24 | line-height: 1.25;
25 | margin-top: 0;
26 | margin-bottom: 0.5rem;
27 | }
28 |
29 | #app {
30 | padding: 1rem;
31 | }
32 |
--------------------------------------------------------------------------------
/apps/web/src/lib/parse-function-call.ts:
--------------------------------------------------------------------------------
1 | import type {ChatCompletionRequestMessage} from "openai-edge";
2 | import type { AIMessage, FunctionCall } from "../types";
3 |
4 | export const parseFunctionCall = (o: AIMessage): FunctionCall => {
5 | if (!!(o as ChatCompletionRequestMessage).function_call) {
6 | const fn = o as ChatCompletionRequestMessage;
7 | return {
8 | name: fn.function_call?.name || "",
9 | arguments: JSON.parse(fn.function_call?.arguments || "{}"),
10 | };
11 | }
12 | throw new Error("no function call available");
13 | };
14 |
--------------------------------------------------------------------------------
/packages/tsconfig/base.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "display": "Default",
4 | "compilerOptions": {
5 | "composite": false,
6 | "declaration": true,
7 | "declarationMap": true,
8 | "esModuleInterop": true,
9 | "forceConsistentCasingInFileNames": true,
10 | "inlineSources": false,
11 | "isolatedModules": true,
12 | "moduleResolution": "node",
13 | "noUnusedLocals": false,
14 | "noUnusedParameters": false,
15 | "preserveWatchOutput": true,
16 | "skipLibCheck": true,
17 | "strict": true,
18 | "strictNullChecks": true
19 | },
20 | "exclude": ["node_modules"]
21 | }
22 |
--------------------------------------------------------------------------------
/apps/web/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "forceConsistentCasingInFileNames": true,
9 | "noEmit": true,
10 | "esModuleInterop": true,
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "jsx": "preserve",
16 | "incremental": true,
17 | "plugins": [
18 | {
19 | "name": "next"
20 | }
21 | ],
22 | "paths": {
23 | "@/*": ["./src/*"]
24 | }
25 | },
26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
27 | "exclude": ["node_modules"]
28 | }
29 |
--------------------------------------------------------------------------------
/apps/web/src/inngest/inngest.server.client.ts:
--------------------------------------------------------------------------------
1 | import { EventSchemas, Inngest } from "inngest";
2 | import type { Message } from "ai";
3 |
4 | export const inngest = new Inngest({
5 | name: "Inngest + PartyKit: OpenAI Function invocation app",
6 | schemas: new EventSchemas().fromRecord(),
7 | });
8 |
9 | type ChatStarted = {
10 | data: {
11 | requestId: string;
12 | messages: Message[];
13 | };
14 | };
15 |
16 | type ChatCancelled = {
17 | data: {
18 | requestId: string;
19 | };
20 | };
21 |
22 | type ChatConfirmed = {
23 | data: {
24 | requestId: string;
25 | confirm: boolean;
26 | };
27 | };
28 |
29 | type Events = {
30 | "api/chat.started": ChatStarted;
31 | "api/chat.cancelled": ChatCancelled;
32 | "api/chat.confirmed": ChatConfirmed;
33 | };
34 |
--------------------------------------------------------------------------------
/packages/eslint-config-custom/library.js:
--------------------------------------------------------------------------------
1 | const { resolve } = require("node:path");
2 |
3 | const project = resolve(process.cwd(), "tsconfig.json");
4 |
5 | /*
6 | * This is a custom ESLint configuration for use with
7 | * typescript packages.
8 | *
9 | * This config extends the Vercel Engineering Style Guide.
10 | * For more information, see https://github.com/vercel/style-guide
11 | *
12 | */
13 |
14 | module.exports = {
15 | extends: [
16 | "@vercel/style-guide/eslint/node",
17 | "@vercel/style-guide/eslint/typescript",
18 | ].map(require.resolve),
19 | parserOptions: {
20 | project,
21 | },
22 | globals: {
23 | React: true,
24 | JSX: true,
25 | },
26 | settings: {
27 | "import/resolver": {
28 | typescript: {
29 | project,
30 | },
31 | },
32 | },
33 | ignorePatterns: ["node_modules/", "dist/"],
34 | };
35 |
--------------------------------------------------------------------------------
/apps/openai-party/README.md:
--------------------------------------------------------------------------------
1 | ## 🎈 openai-party
2 |
3 | Welcome to the party, pal!
4 |
5 | This is a [Partykit](https://partykit.io) project, which lets you create real-time collaborative applications with minimal coding effort.
6 |
7 | [`server.ts`](./src/server.ts) is the server-side code, which is responsible for handling WebSocket events and HTTP requests. [`client.tsx`](./src/client.tsx) is the client-side code, which connects to the server and listens for events.
8 |
9 | You can start developing by running `npm run dev` and opening [http://localhost:1999](http://localhost:1999) in your browser. When you're ready, you can deploy your application on to the PartyKit cloud with `npm run deploy`.
10 |
11 | Refer to our docs for more information: https://github.com/partykit/partykit/blob/main/README.md. For more help, reach out to us on [Discord](https://discord.gg/g5uqHQJc3z), [GitHub](https://github.com/partykit/partykit), or [Twitter](https://twitter.com/partykit_io).
12 |
--------------------------------------------------------------------------------
/packages/tsconfig/nextjs.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "display": "Next.js",
4 | "compilerOptions": {
5 | "composite": false,
6 | "esModuleInterop": true,
7 | "forceConsistentCasingInFileNames": true,
8 | "inlineSources": false,
9 | "isolatedModules": true,
10 | "moduleResolution": "node",
11 | "noUnusedLocals": false,
12 | "noUnusedParameters": false,
13 | "preserveWatchOutput": true,
14 | "skipLibCheck": true,
15 | "strict": true,
16 | "strictNullChecks": true,
17 | "plugins": [{ "name": "next" }],
18 | "allowJs": true,
19 | "declaration": false,
20 | "declarationMap": false,
21 | "incremental": true,
22 | "jsx": "preserve",
23 | "lib": ["dom", "dom.iterable", "esnext"],
24 | "module": "esnext",
25 | "noEmit": true,
26 | "resolveJsonModule": true,
27 | "target": "esnext"
28 | },
29 | "include": ["src", "next-env.d.ts"],
30 | "exclude": ["node_modules"]
31 | }
32 |
--------------------------------------------------------------------------------
/apps/web/src/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { cn } from "@/lib/utils";
3 |
4 |
5 | export interface TextareaProps
6 | extends React.TextareaHTMLAttributes {}
7 |
8 | const Textarea = React.forwardRef(
9 | ({ className, ...props }, ref) => {
10 | return (
11 |
19 | );
20 | }
21 | );
22 | Textarea.displayName = "Textarea";
23 |
24 | export { Textarea };
25 |
--------------------------------------------------------------------------------
/packages/eslint-config-custom/react-internal.js:
--------------------------------------------------------------------------------
1 | const { resolve } = require("node:path");
2 |
3 | const project = resolve(process.cwd(), "tsconfig.json");
4 |
5 | /*
6 | * This is a custom ESLint configuration for use with
7 | * internal (bundled by their consumer) libraries
8 | * that utilize React.
9 | *
10 | * This config extends the Vercel Engineering Style Guide.
11 | * For more information, see https://github.com/vercel/style-guide
12 | *
13 | */
14 |
15 | module.exports = {
16 | extends: [
17 | "@vercel/style-guide/eslint/browser",
18 | "@vercel/style-guide/eslint/typescript",
19 | "@vercel/style-guide/eslint/react",
20 | ].map(require.resolve),
21 | parserOptions: {
22 | project,
23 | },
24 | globals: {
25 | JSX: true,
26 | },
27 | settings: {
28 | "import/resolver": {
29 | typescript: {
30 | project,
31 | },
32 | },
33 | },
34 | ignorePatterns: ["node_modules/", "dist/", ".eslintrc.js"],
35 | // add rules configurations here
36 | rules: {
37 | "import/no-default-export": "off",
38 | },
39 | };
40 |
--------------------------------------------------------------------------------
/apps/web/src/app/api/chat/route.ts:
--------------------------------------------------------------------------------
1 | import type { Message } from 'ai'
2 | import { customAlphabet } from 'nanoid';
3 | import { inngest } from '@/inngest/inngest.server.client';
4 |
5 | const nanoid = customAlphabet(
6 | "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
7 | 7
8 | );
9 |
10 | export const runtime = 'edge'
11 |
12 | export async function POST(req: Request) {
13 | // if no requestId is provided we generate one
14 | const body = await req.json()
15 | const { messages, requestId = nanoid(), confirm } = body
16 |
17 | if (confirm !== undefined) {
18 | // Confirm API
19 | await inngest.send({
20 | name: "api/chat.confirmed",
21 | data: {
22 | requestId,
23 | confirm,
24 | },
25 | });
26 | return new Response(requestId, { status: 200 });
27 | }
28 |
29 | console.log('start the chat')
30 |
31 | await inngest.send({
32 | name: "api/chat.started",
33 | data: {
34 | messages: messages as Message[],
35 | requestId,
36 | },
37 | });
38 |
39 | return new Response(requestId as string, { status: 200 });
40 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Joel Hooks
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/packages/eslint-config-custom/next.js:
--------------------------------------------------------------------------------
1 | const { resolve } = require("node:path");
2 |
3 | const project = resolve(process.cwd(), "tsconfig.json");
4 |
5 | /*
6 | * This is a custom ESLint configuration for use with
7 | * Next.js apps.
8 | *
9 | * This config extends the Vercel Engineering Style Guide.
10 | * For more information, see https://github.com/vercel/style-guide
11 | *
12 | */
13 |
14 | module.exports = {
15 | extends: [
16 | "eslint-config-turbo",
17 | 'next'
18 | ].map(require.resolve),
19 | parserOptions: {
20 | project,
21 | },
22 | globals: {
23 | React: true,
24 | JSX: true,
25 | },
26 | settings: {
27 | "import/resolver": {
28 | typescript: {
29 | project,
30 | },
31 | },
32 | },
33 | ignorePatterns: ["node_modules/", "dist/"],
34 | // add rules configurations here
35 | rules: {
36 | "import/no-default-export": "off",
37 | "no-console": "off",
38 | "@typescript-eslint/explicit-function-return-type": "off",
39 | "import/no-named-as-default": "off",
40 | "@typescript-eslint/no-unsafe-assignment": "off",
41 | "@typescript-eslint/no-unsafe-call": "off",
42 | "@typescript-eslint/no-unsafe-member-access": "off",
43 | "no-undef": "off",
44 | },
45 | };
46 |
--------------------------------------------------------------------------------
/apps/openai-party/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
9 |
10 | PartyKit: Everything's better with friends!
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
🎈 Welcome to PartyKit!
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/apps/web/src/types.ts:
--------------------------------------------------------------------------------
1 | import type {ChatCompletionRequestMessage} from "openai-edge";
2 | import type { JSONSchema4 } from "json-schema";
3 |
4 | /**
5 | * Functions represents functions which are callable by OpenAI. These are added in
6 | * to each LLM call via the `functions` parameter.
7 | *
8 | */
9 | export type Functions = Record;
10 |
11 | export type FunctionDefinition = {
12 | invoke: (f: FunctionCall, m: ChatCompletionRequestMessage[]) => Promise;
13 |
14 | docs: APIDocs;
15 | /**
16 | * confirm indicates whether this function call requires confirmation from the
17 | * user to be executed.
18 | */
19 | confirm?: boolean;
20 | };
21 |
22 | export type APIDocs = {
23 | name: string;
24 | description: string;
25 | parameters: JSONSchema4;
26 | };
27 |
28 | export type FunctionCall = {
29 | arguments: Record;
30 | name: string; // function name.
31 | };
32 |
33 | export type AIMessage = ChatCompletionRequestMessage & {
34 | content: null | string;
35 | createdAt?: Date;
36 | id?: string;
37 | };
38 |
39 | export type AIError = { error: string };
40 |
41 | export type AIOutput = AIMessage | AIError;
42 |
43 | export interface ProgressWriter {
44 | write(resp: Response): Promise;
45 | }
--------------------------------------------------------------------------------
/apps/web/src/components/icons.tsx:
--------------------------------------------------------------------------------
1 | export const Send = ({ className }: { className?: string }) => {
2 | return (
3 |
10 |
17 |
18 | );
19 | };
20 | export const Loading = ({ className }: { className?: string }) => {
21 | return (
22 |
23 |
24 |
25 | );
26 | };
27 |
--------------------------------------------------------------------------------
/apps/web/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "web",
3 | "version": "1.0.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "concurrently \"npm:dev-*\"",
7 | "dev-next": "next dev",
8 | "dev-inngest": "inngest dev",
9 | "build": "next build",
10 | "start": "next start",
11 | "lint": "next lint"
12 | },
13 | "dependencies": {
14 | "@linear/sdk": "^7.0.1",
15 | "@radix-ui/react-slot": "^1.0.2",
16 | "@tailwindcss/typography": "^0.5.9",
17 | "ai": "^2.2.8",
18 | "autoprefixer": "^10.4.15",
19 | "class-variance-authority": "^0.7.0",
20 | "clsx": "^2.0.0",
21 | "inngest": "^2.5.2",
22 | "json-schema": "^0.4.0",
23 | "nanoid": "^4.0.2",
24 | "next": "^13.4.19",
25 | "openai": "^4.2.0",
26 | "openai-edge": "^1.2.2",
27 | "partykit": "0.0.0-3228764",
28 | "partysocket": "0.0.0-3228764",
29 | "postcss": "^8.4.28",
30 | "react": "^18.2.0",
31 | "react-dom": "^18.2.0",
32 | "react-markdown": "^8.0.7",
33 | "tailwind-merge": "^1.13.2",
34 | "tailwindcss": "3.3.2",
35 | "tailwindcss-animate": "^1.0.6"
36 | },
37 | "devDependencies": {
38 | "@next/eslint-plugin-next": "^13.4.19",
39 | "@types/json-schema": "^7.0.12",
40 | "@types/node": "^18",
41 | "@types/react": "^18.0.22",
42 | "@types/react-dom": "^18.0.7",
43 | "concurrently": "^8.2.1",
44 | "encoding": "^0.1.13",
45 | "eslint-config-custom": "workspace:*",
46 | "inngest-cli": "^0.16.6",
47 | "tsconfig": "workspace:*",
48 | "typescript": "^5.1.6"
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/apps/web/src/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer base {
6 | :root {
7 | --background: 0 0% 100%;
8 | --foreground: 20 14.3% 4.1%;
9 |
10 | --muted: 60 4.8% 95.9%;
11 | --muted-foreground: 25 5.3% 44.7%;
12 |
13 | --popover: 0 0% 100%;
14 | --popover-foreground: 20 14.3% 4.1%;
15 |
16 | --card: 0 0% 100%;
17 | --card-foreground: 20 14.3% 4.1%;
18 |
19 | --border: 20 5.9% 90%;
20 | --input: 20 5.9% 90%;
21 |
22 | --primary: 24 9.8% 10%;
23 | --primary-foreground: 60 9.1% 97.8%;
24 |
25 | --secondary: 60 4.8% 95.9%;
26 | --secondary-foreground: 24 9.8% 10%;
27 |
28 | --accent: 60 4.8% 95.9%;
29 | --accent-foreground: 24 9.8% 10%;
30 |
31 | --destructive: 0 84.2% 60.2%;
32 | --destructive-foreground: 60 9.1% 97.8%;
33 |
34 | --ring: 24 5.4% 63.9%;
35 |
36 | --radius: 0.5rem;
37 |
38 | --tw-ring-offset-width: 0;
39 | }
40 |
41 | .dark {
42 | --background: 20 14.3% 4.1%;
43 | --foreground: 60 9.1% 97.8%;
44 |
45 | --muted: 12 6.5% 15.1%;
46 | --muted-foreground: 24 5.4% 63.9%;
47 |
48 | --popover: 20 14.3% 4.1%;
49 | --popover-foreground: 60 9.1% 97.8%;
50 |
51 | --card: 20 14.3% 4.1%;
52 | --card-foreground: 60 9.1% 97.8%;
53 |
54 | --border: 12 6.5% 15.1%;
55 | --input: 12 6.5% 15.1%;
56 |
57 | --primary: 60 9.1% 97.8%;
58 | --primary-foreground: 24 9.8% 10%;
59 |
60 | --secondary: 12 6.5% 15.1%;
61 | --secondary-foreground: 60 9.1% 97.8%;
62 |
63 | --accent: 12 6.5% 15.1%;
64 | --accent-foreground: 60 9.1% 97.8%;
65 |
66 | --destructive: 0 62.8% 30.6%;
67 | --destructive-foreground: 0 85.7% 97.3%;
68 |
69 | --ring: 12 6.5% 15.1%;
70 | }
71 | }
72 |
73 | @layer base {
74 | * {
75 | @apply border-border;
76 | }
77 |
78 | body {
79 | @apply bg-background text-foreground;
80 | }
81 | }
--------------------------------------------------------------------------------
/apps/web/src/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { Slot } from "@radix-ui/react-slot";
3 | import { cva, type VariantProps } from "class-variance-authority";
4 | import { cn } from "@/lib/utils";
5 |
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-white transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-stone-400 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 dark:ring-offset-stone-950 dark:focus-visible:ring-stone-800",
9 | {
10 | variants: {
11 | variant: {
12 | default:
13 | "bg-stone-900 text-stone-50 hover:bg-stone-900/90 dark:bg-stone-50 dark:text-stone-900 dark:hover:bg-stone-50/90",
14 | destructive:
15 | "bg-red-500 text-stone-50 hover:bg-red-500/90 dark:bg-red-900 dark:text-red-50 dark:hover:bg-red-900/90",
16 | outline:
17 | "border border-stone-200 bg-white hover:bg-stone-100 hover:text-stone-900 dark:border-stone-800 dark:bg-stone-950 dark:hover:bg-stone-800 dark:hover:text-stone-50",
18 | secondary:
19 | "bg-stone-100 text-stone-900 hover:bg-stone-100/80 dark:bg-stone-800 dark:text-stone-50 dark:hover:bg-stone-800/80",
20 | ghost:
21 | "hover:bg-stone-100 hover:text-stone-900 dark:hover:bg-stone-800 dark:hover:text-stone-50",
22 | link: "text-stone-900 underline-offset-4 hover:underline dark:text-stone-50",
23 | },
24 | size: {
25 | default: "h-10 px-4 py-2",
26 | sm: "h-9 rounded-md px-3",
27 | lg: "h-11 rounded-md px-8",
28 | icon: "h-10 w-10",
29 | },
30 | },
31 | defaultVariants: {
32 | variant: "default",
33 | size: "default",
34 | },
35 | }
36 | );
37 |
38 | export interface ButtonProps
39 | extends React.ButtonHTMLAttributes,
40 | VariantProps {
41 | asChild?: boolean;
42 | }
43 |
44 | const Button = React.forwardRef(
45 | ({ className, variant, size, asChild = false, ...props }, ref) => {
46 | const Comp = asChild ? Slot : "button";
47 | return (
48 |
53 | );
54 | }
55 | );
56 | Button.displayName = "Button";
57 |
58 | export { Button, buttonVariants };
59 |
--------------------------------------------------------------------------------
/apps/web/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | darkMode: ["class"],
4 | content: [
5 | './pages/**/*.{ts,tsx}',
6 | './components/**/*.{ts,tsx}',
7 | './app/**/*.{ts,tsx}',
8 | './src/**/*.{ts,tsx}',
9 | ],
10 | theme: {
11 | container: {
12 | center: true,
13 | padding: "2rem",
14 | screens: {
15 | "2xl": "1400px",
16 | },
17 | },
18 | extend: {
19 | colors: {
20 | border: "hsl(var(--border))",
21 | input: "hsl(var(--input))",
22 | ring: "hsl(var(--ring))",
23 | background: "hsl(var(--background))",
24 | foreground: "hsl(var(--foreground))",
25 | primary: {
26 | DEFAULT: "hsl(var(--primary))",
27 | foreground: "hsl(var(--primary-foreground))",
28 | },
29 | secondary: {
30 | DEFAULT: "hsl(var(--secondary))",
31 | foreground: "hsl(var(--secondary-foreground))",
32 | },
33 | destructive: {
34 | DEFAULT: "hsl(var(--destructive))",
35 | foreground: "hsl(var(--destructive-foreground))",
36 | },
37 | muted: {
38 | DEFAULT: "hsl(var(--muted))",
39 | foreground: "hsl(var(--muted-foreground))",
40 | },
41 | accent: {
42 | DEFAULT: "hsl(var(--accent))",
43 | foreground: "hsl(var(--accent-foreground))",
44 | },
45 | popover: {
46 | DEFAULT: "hsl(var(--popover))",
47 | foreground: "hsl(var(--popover-foreground))",
48 | },
49 | card: {
50 | DEFAULT: "hsl(var(--card))",
51 | foreground: "hsl(var(--card-foreground))",
52 | },
53 | },
54 | borderRadius: {
55 | lg: "var(--radius)",
56 | md: "calc(var(--radius) - 2px)",
57 | sm: "calc(var(--radius) - 4px)",
58 | },
59 | keyframes: {
60 | "accordion-down": {
61 | from: { height: 0 },
62 | to: { height: "var(--radix-accordion-content-height)" },
63 | },
64 | "accordion-up": {
65 | from: { height: "var(--radix-accordion-content-height)" },
66 | to: { height: 0 },
67 | },
68 | },
69 | animation: {
70 | "accordion-down": "accordion-down 0.2s ease-out",
71 | "accordion-up": "accordion-up 0.2s ease-out",
72 | },
73 | },
74 | },
75 | plugins: [
76 | require("tailwindcss-animate"),
77 | require('@tailwindcss/typography'),
78 | ],
79 | }
80 |
--------------------------------------------------------------------------------
/apps/web/src/inngest/ai-flow.ts:
--------------------------------------------------------------------------------
1 | import {
2 | type ChatCompletionRequestMessage,
3 | Configuration,
4 | OpenAIApi,
5 | } from "openai-edge"
6 | import {inngest} from "./inngest.server.client";
7 | import type { AIMessage, FunctionCall, Functions } from "@/types";
8 | import { FunctionInvoker } from "./function-invoker";
9 | import { LinearClient } from "@linear/sdk";
10 |
11 | const config = new Configuration({apiKey: process.env.OPENAI_API_KEY})
12 | const openai = new OpenAIApi(config)
13 | const linear = new LinearClient({ apiKey: process.env.LINEAR_API_KEY });
14 |
15 | export const aibot = inngest.createFunction(
16 | {
17 | name: "OpenAI Linear Bot",
18 | cancelOn: [
19 | // Cancel this function if we receive a cancellation event with the same request ID can .
20 | // This prevents wasted execution and increased costs.
21 | {
22 | event: "api/chat.cancelled",
23 | if: "event.data.requestId == async.data.requestId",
24 | },
25 | ],
26 | },
27 | {event: "api/chat.started"},
28 | async ({event, step}) => {
29 | const invoker = new FunctionInvoker({
30 | openai,
31 | functions,
32 | requestId: event.data.requestId,
33 | });
34 |
35 | const messages = await invoker.start(event.data.messages as AIMessage[], step);
36 | return messages;
37 | }
38 | );
39 |
40 |
41 | // All available functions for Linear.
42 | const functions: Functions = {
43 | search_issues: {
44 | docs: {
45 | name: "search_issues",
46 | description: "Search all issues for the given text",
47 | parameters: {
48 | type: "object",
49 | properties: {
50 | search: {
51 | type: "string",
52 | description: "The search term",
53 | },
54 | },
55 | required: ["search"],
56 | },
57 | },
58 | invoke: async (f: FunctionCall, _m: ChatCompletionRequestMessage[]) => {
59 | if (typeof f.arguments.search !== "string") {
60 | throw new Error("No search term provided");
61 | }
62 | return linear.issues({
63 | last: 5,
64 | filter: { searchableContent: { contains: f.arguments.search } },
65 | });
66 | },
67 | },
68 | delete_issue: {
69 | docs: {
70 | name: "delete_issue",
71 | description: "Delete an issue by ID",
72 | parameters: {
73 | type: "object",
74 | properties: {
75 | id: {
76 | type: "string",
77 | description: "ID of the issue to delete",
78 | },
79 | },
80 | required: ["id"],
81 | },
82 | },
83 | confirm: true,
84 | invoke: async (f: FunctionCall, _m: ChatCompletionRequestMessage[]) => {
85 | console.log("🤡 Not actually deleting issues!", f.arguments.id);
86 | return true;
87 | },
88 | },
89 | };
90 |
91 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Collaborative AI-Assisted Workflows with Inngest and Partykit
2 |
3 | This is an example using [Inngest](https://inngest.com) and
4 | [Partykit](https://github.com/partykit/partykit) to build a collaborative
5 | AI-assisted workflow.
6 |
7 | 📼 [quick loom video](https://www.loom.com/share/c43aa34205854096bcec0a96e7ba5634?sid=839b1adc-ad39-4540-9995-88967f2b6da9)
8 |
9 | Inngest initiates and orchestrates the workflow, and Partykit is used
10 | as a proxy to the OpenAI API. We need a proxy because the OpenAI API **streams**
11 | responses, and we want to orchestrate actions based on suggestions and function
12 | calls that the OpenAI API returns.
13 |
14 | This is important because we aren't building a simple chat box. We want a UI that allows
15 | us to do actual work, and we **cannot trust the LLM to make appropriate business decisions**.
16 |
17 | We **can** trust the LLM to make suggestions, and we can use those suggestions to
18 | make human-level decisions.
19 |
20 | ## How it works
21 |
22 | The user has a text box they can type into and ask for a particular task. This
23 | initiates a workflow with Inngest. As OpenAI streams back its responses, we can use Partykit
24 | as a proxy for the streams and broadcast those responses to **every subscribed user**.
25 |
26 | **This means that we are able to create multiplayer workflows that are assisted by AI.**
27 |
28 | ## How to use
29 |
30 | Get everything installed:
31 |
32 | ```shell
33 | pnpm install
34 | ```
35 |
36 | Start everything like this:
37 |
38 | ```shell
39 | pnpm dev
40 | ```
41 |
42 | `pnpm dev` starts the web application server, the PartyKit dev server, and the Inngest dev server.
43 |
44 | Inngest runs at http://localhost:8288, and the web app server runs at http://localhost:3000. Open both of these in your browser!
45 |
46 | Partykit doesn't have a UI but is running at http://127.0.0.1:1999/
47 |
48 | `OPENAI_API_KEY` and `LINEAR_API_KEY` need to be set in `/apps/web/.env.local` for the web app to work.
49 |
50 | 👋 Note that the linear function `delete` does NOT actually delete issues. You can create a Linear account for free to test it out.
51 |
52 | ## What's inside?
53 |
54 | This example includes the following packages/apps:
55 |
56 | ### Apps and Packages
57 |
58 | - `web`: simple [Next.js](https://nextjs.org/) app
59 | - `openai-party`: a [PartyKit](https://github.com/partykit/partykit) server
60 | - `ui`: a stub React component library shared by both `web` applications
61 | - `eslint-config-custom`: `eslint` configurations (includes `eslint-config-next` and `eslint-config-prettier`)
62 | - `tsconfig`: `tsconfig.json`s used throughout the monorepo
63 |
64 | Each package/app is 100% [TypeScript](https://www.typescriptlang.org/).
65 |
66 | ### Build
67 |
68 | To build all apps and packages, run the following command:
69 |
70 | ```
71 | pnpm build
72 | ```
73 |
74 | ### Develop
75 |
76 | To develop all apps and packages, run the following command:
77 |
78 | ```
79 | pnpm dev
80 | ```
81 |
--------------------------------------------------------------------------------
/apps/openai-party/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 |
3 | logs
4 | _.log
5 | npm-debug.log_
6 | yarn-debug.log*
7 | yarn-error.log*
8 | lerna-debug.log*
9 | .pnpm-debug.log*
10 |
11 | # Diagnostic reports (https://nodejs.org/api/report.html)
12 |
13 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
14 |
15 | # Runtime data
16 |
17 | pids
18 | _.pid
19 | _.seed
20 | \*.pid.lock
21 |
22 | # Directory for instrumented libs generated by jscoverage/JSCover
23 |
24 | lib-cov
25 |
26 | # Coverage directory used by tools like istanbul
27 |
28 | coverage
29 | \*.lcov
30 |
31 | # nyc test coverage
32 |
33 | .nyc_output
34 |
35 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
36 |
37 | .grunt
38 |
39 | # Bower dependency directory (https://bower.io/)
40 |
41 | bower_components
42 |
43 | # node-waf configuration
44 |
45 | .lock-wscript
46 |
47 | # Compiled binary addons (https://nodejs.org/api/addons.html)
48 |
49 | build/Release
50 |
51 | # Dependency directories
52 |
53 | node_modules/
54 | jspm_packages/
55 |
56 | # Snowpack dependency directory (https://snowpack.dev/)
57 |
58 | web_modules/
59 |
60 | # TypeScript cache
61 |
62 | \*.tsbuildinfo
63 |
64 | # Optional npm cache directory
65 |
66 | .npm
67 |
68 | # Optional eslint cache
69 |
70 | .eslintcache
71 |
72 | # Optional stylelint cache
73 |
74 | .stylelintcache
75 |
76 | # Microbundle cache
77 |
78 | .rpt2_cache/
79 | .rts2_cache_cjs/
80 | .rts2_cache_es/
81 | .rts2_cache_umd/
82 |
83 | # Optional REPL history
84 |
85 | .node_repl_history
86 |
87 | # Output of 'npm pack'
88 |
89 | \*.tgz
90 |
91 | # Yarn Integrity file
92 |
93 | .yarn-integrity
94 |
95 | # dotenv environment variable files
96 |
97 | .env
98 | .env.development.local
99 | .env.test.local
100 | .env.production.local
101 | .env.local
102 |
103 | # parcel-bundler cache (https://parceljs.org/)
104 |
105 | .cache
106 | .parcel-cache
107 |
108 | # Next.js build output
109 |
110 | .next
111 | out
112 |
113 | # Nuxt.js build / generate output
114 |
115 | .nuxt
116 | dist
117 |
118 | # Gatsby files
119 |
120 | .cache/
121 |
122 | # Comment in the public line in if your project uses Gatsby and not Next.js
123 |
124 | # https://nextjs.org/blog/next-9-1#public-directory-support
125 |
126 | # public
127 |
128 | # vuepress build output
129 |
130 | .vuepress/dist
131 |
132 | # vuepress v2.x temp and cache directory
133 |
134 | .temp
135 | .cache
136 |
137 | # Docusaurus cache and generated files
138 |
139 | .docusaurus
140 |
141 | # Serverless directories
142 |
143 | .serverless/
144 |
145 | # FuseBox cache
146 |
147 | .fusebox/
148 |
149 | # DynamoDB Local files
150 |
151 | .dynamodb/
152 |
153 | # TernJS port file
154 |
155 | .tern-port
156 |
157 | # Stores VSCode versions used for testing VSCode extensions
158 |
159 | .vscode-test
160 |
161 | # yarn v2
162 |
163 | .yarn/cache
164 | .yarn/unplugged
165 | .yarn/build-state.yml
166 | .yarn/install-state.gz
167 | .pnp.\*
168 |
169 | .partykit
170 | .DS_Store
171 |
--------------------------------------------------------------------------------
/apps/openai-party/src/server.ts:
--------------------------------------------------------------------------------
1 | import type {
2 | Party,
3 | PartyConnection,
4 | PartyRequest,
5 | PartyServer,
6 | PartyWorker,
7 | PartyServerOptions,
8 | PartyConnectionContext,
9 | } from "partykit/server";
10 | import { Inngest } from "inngest";
11 |
12 | // PartyKit servers now implement PartyServer interface
13 | export default class Main implements PartyServer {
14 | // onBefore* handlers that run in the worker nearest the user are now
15 | // explicitly marked static, because they have no access to Party state
16 | static async onBeforeRequest(req: PartyRequest) {
17 | return req;
18 | }
19 | static async onBeforeConnect(req: PartyRequest) {
20 | return req;
21 | }
22 | // onFetch is now stable. No more unstable_onFetch
23 | static async onFetch(req: PartyRequest) {
24 | return new Response("Unrecognized request: " + req.url, { status: 404 });
25 | }
26 |
27 | // Opting into hibernation is now an explicit option
28 | readonly options: PartyServerOptions = {
29 | hibernate: true,
30 | };
31 |
32 | // Servers can now keep state in class instance variables
33 | messages: string[] = [];
34 | inngest: Inngest
35 |
36 | // PartyServer receives the Party (previous PartyKitRoom) as a constructor argument
37 | // instead of receiving the `room` argument in each method.
38 | readonly party: Party;
39 | constructor(party: Party) {
40 | this.party = party;
41 | this.inngest = new Inngest({
42 | name: "Inngest + PartyKit: OpenAI Function invocation app",
43 | })
44 | }
45 |
46 | // There's now a new lifecycle method `onStart` which fires before first connection
47 | // or request to the room. You can use this to load data from storage and perform other
48 | // asynchronous initialization. The Party will wait until `onStart` completes before
49 | // processing any connections or requests.
50 | async onStart() {
51 | this.messages = (await this.party.storage.get("messages")) ?? [];
52 | }
53 |
54 | // onConnect, onRequest, onAlarm no longer receive the room argument.
55 | async onRequest(_req: PartyRequest) {
56 | const messageBody: {requestId: string, body: string} = await _req.json();
57 |
58 | this.party.broadcast(messageBody.body);
59 |
60 | return new Response(
61 | `Party ${this.party.id} has received ${this.messages.length} messages`
62 | );
63 | }
64 | async onConnect(connection: PartyConnection, ctx: PartyConnectionContext) {}
65 |
66 | // Previously onMessage, onError, onClose were only called for hibernating parties.
67 | // They're now available for all parties, so you no longer need to manually
68 | // manage event handlers in onConnect!
69 | async onMessage(message: string, connection: PartyConnection) {
70 | this.party.broadcast(message, [connection.id]);
71 | }
72 | async onError(connection: PartyConnection, err: Error) {
73 | console.log("Error from " + connection.id, err.message);
74 | }
75 | async onClose(connection: PartyConnection) {
76 | console.log("Closed " + connection.id);
77 | this.inngest.send({
78 | name: "api/chat.cancelled",
79 | data: {
80 | requestId: connection.id,
81 | },
82 | });
83 | }
84 | }
85 |
86 | // Optional: Typecheck the static methods with a `satisfies` statement.
87 | Main satisfies PartyWorker;
--------------------------------------------------------------------------------
/apps/web/src/inngest/message-writer.ts:
--------------------------------------------------------------------------------
1 | import { OpenAIStream, StreamingTextResponse } from "ai";
2 | import type { AIMessage, AIOutput } from "@/types";
3 |
4 | export class WriteStrategyManyRequests {
5 | requestId: string;
6 |
7 | interval = 250;
8 |
9 | buffer: {
10 | contents: string;
11 | // signal is a blocking signal which resolves when the buffer has been written.
12 | signal?: Promise;
13 | };
14 |
15 | constructor(requestId: string) {
16 | this.requestId = requestId;
17 | this.buffer = {
18 | contents: "",
19 | };
20 | }
21 |
22 | async write(resp: Response): Promise {
23 | const applyChunk = this.chunk.bind(this);
24 |
25 | const pipe = new TransformStream({
26 | async transform(chunk, controller) {
27 | // When we receive a chunk, publish this as a new request.
28 | const text = new TextDecoder().decode(chunk);
29 | await applyChunk(text);
30 | // await publish(text, requestId);
31 | // Continue with the standard stream.
32 | controller.enqueue(chunk);
33 | },
34 | });
35 | // Publish via our writing pipe.
36 | const stream = OpenAIStream(resp).pipeThrough(pipe);
37 | const result = await parse(stream, this.requestId);
38 | await this.buffer.signal;
39 | return result;
40 | }
41 |
42 | async chunk(text: string) {
43 | let resolve = (_val?: any) => {};
44 |
45 | this.buffer.contents += text;
46 |
47 | if (this.buffer.signal) {
48 | // Already enqueued.
49 | return;
50 | }
51 |
52 | (this.buffer.signal = new Promise((r) => {
53 | resolve = r;
54 | }))
55 | setTimeout(() => {
56 | if (this.buffer.contents.length === 0) {
57 | // No need to write
58 | resolve();
59 | return;
60 | }
61 | publish(this.buffer.contents, this.requestId);
62 | resolve();
63 | this.buffer = {
64 | contents: "",
65 | };
66 | }, this.interval);
67 | }
68 | }
69 |
70 |
71 | /**
72 | * 🥳 Publish a message to the party. Sends a POST request to the partykit server.
73 | * The server then broadcasts it to all connected clients.
74 | *
75 | * @param body
76 | * @param requestId
77 | */
78 | export const publish = async (body: string, requestId: string) => {
79 | const partyUrl = `${process.env.NEXT_PUBLIC_PARTY_KIT_URL!}/party/${process.env.NEXT_PUBLIC_PARTYKIT_ROOM_NAME}`
80 | await fetch(partyUrl, {
81 | method: "POST",
82 | body: JSON.stringify({
83 | requestId,
84 | body,
85 | }),
86 | }).catch((e) => {
87 | console.error(e);
88 | })
89 | };
90 |
91 | const parse = async (
92 | stream: ReadableStream,
93 | requestId: string
94 | ): Promise => {
95 | // And then pass this through the standard text response
96 | const text = await new StreamingTextResponse(stream).text();
97 | try {
98 | const raw = JSON.parse(text) as Record;
99 | const output = {
100 | role: "assistant",
101 | content: null,
102 | ...raw,
103 | } as unknown;
104 | return output as AIMessage;
105 | } catch (e) {
106 | // This may not be JSON
107 | return { role: "assistant", content: text };
108 | }
109 | };
--------------------------------------------------------------------------------
/apps/web/src/inngest/function-invoker.ts:
--------------------------------------------------------------------------------
1 | import type { ChatCompletionRequestMessage, OpenAIApi } from "openai-edge";
2 | import type { AIMessage, AIOutput, Functions, ProgressWriter } from "@/types";
3 | import { isFunctionCall } from "@/lib/is-function-call";
4 | import { formatFunctions } from "@/lib/format-functions";
5 | import { parseFunctionCall } from "@/lib/parse-function-call";
6 | import { WriteStrategyManyRequests, publish } from "./message-writer";
7 | import { CONFIRM, DONE } from "@/lib/enums";
8 |
9 | /**
10 | * The invoker dynamically creates steps to call OpenAI and functions.
11 | *
12 | * 😅 There's a fair bit of complexity below, but it's mostly to handle
13 | * the various edge cases of calling OpenAI and functions.
14 | */
15 | export class FunctionInvoker {
16 | #ai: OpenAIApi;
17 | #fns: Functions;
18 | #messages: AIMessage[];
19 | #requestId: string;
20 | #writer: ProgressWriter;
21 |
22 | constructor({
23 | openai,
24 | functions,
25 | requestId,
26 | }: {
27 | openai: OpenAIApi;
28 | functions: Functions;
29 | requestId: string;
30 | }) {
31 | this.#ai = openai;
32 | this.#fns = functions;
33 | this.#requestId = requestId;
34 | this.#messages = [];
35 | this.#writer = new WriteStrategyManyRequests(requestId);
36 | }
37 |
38 | /**
39 | * This returns messages as input for ChatGPT
40 | *
41 | */
42 | get input() {
43 | return this.#messages.map((m) => {
44 | const input: ChatCompletionRequestMessage = {
45 | role: m.role,
46 | content: m.content,
47 | };
48 | if (m.name) {
49 | input.name = m.name;
50 | }
51 | if (m.function_call) {
52 | input.function_call = m.function_call;
53 | }
54 | return input;
55 | })
56 | }
57 |
58 | /**
59 | * Start runs the chain and should be called from your Inngest function.
60 | */
61 | async start(messages: AIMessage[], step: any): Promise {
62 | this.#messages = messages || [];
63 |
64 | // Call OpenAI reliably, proxying the content to the browser.
65 | const output = await step.run(
66 | "Call OpenAI",
67 | async (): Promise => {
68 | const resp = await this.#ai.createChatCompletion({
69 | model: process.env.OPENAI_MODEL_NAME!,
70 | stream: true,
71 | messages: this.input,
72 | functions: formatFunctions(this.#fns),
73 | });
74 | return this.#handleResponse(resp);
75 | }
76 | );
77 |
78 | this.#messages.push(output as AIMessage);
79 | while (isFunctionCall(this.#messages)) {
80 | // Continue to run the chain autonomously.
81 | await this.invoke(step);
82 | }
83 | return this.#messages;
84 | }
85 |
86 | /**
87 | * Invoke runs the chain, continually invoking OpenAI and functions
88 | * correctly.
89 | */
90 | async invoke(step: any): Promise {
91 | if (!isFunctionCall(this.#messages)) {
92 | return this.#messages;
93 | }
94 |
95 | // Invoke the fn
96 | const call = parseFunctionCall(
97 | this.#messages[this.#messages.length - 1]
98 | ) ;
99 |
100 | // If this function requires confirmation, wait for the user to confirm.
101 | if (this.#fns[call.name]?.confirm) {
102 | // Publish a confirm request, then wait for confirmation.
103 | await step.run("Publish confirmation", async () => {
104 | await publish(CONFIRM, this.#requestId);
105 | });
106 |
107 | const confirm = await step.waitForEvent(`api/chat.confirmed`, {
108 | timeout: "5m",
109 | match: "data.requestId",
110 | });
111 |
112 | if (!confirm || !confirm.data.confirm) {
113 | await step.run("Publish deny", async () => {
114 | await publish("You haven't given me permission to call this function. I'll ignore the last function call request", this.#requestId);
115 | await publish(DONE, this.#requestId);
116 | });
117 |
118 | this.#messages.push({
119 | role: "assistant",
120 | content: "You haven't given me permission to call this function. I'll ignore the last function call request",
121 | });
122 | return this.#messages;
123 | }
124 | }
125 |
126 | const content = await step.run(
127 | `Call function ${call.name}`,
128 | async (): Promise => {
129 | return this.#fns[call.name].invoke(call, this.#messages);
130 | }
131 | );
132 |
133 | const stringified = JSON.stringify(content);
134 | this.#messages.push({
135 | role: "function",
136 | name: call.name,
137 | content: stringified,
138 | });
139 |
140 | // Call the LLM one more time with the function output.
141 | const output = await step.run(
142 | "Call OpenAI",
143 | async (): Promise => {
144 | const resp = await this.#ai.createChatCompletion({
145 | model: process.env.OPENAI_MODEL_NAME!,
146 | stream: true,
147 | messages: this.input,
148 | functions: formatFunctions(this.#fns),
149 | });
150 | return this.#handleResponse(resp);
151 | }
152 | );
153 |
154 | this.#messages.push(output as AIMessage);
155 | return this.#messages;
156 | }
157 |
158 | /**
159 | * This processes a response from OpenAI.
160 | */
161 | async #handleResponse(response: Response): Promise {
162 | let result;
163 |
164 | if (response.status >= 400) {
165 | result = await response.json();
166 | throw new Error(
167 | result?.error?.message ? result.error.message as string : "There was an error with openAI",
168 | {
169 | cause: result,
170 | }
171 | );
172 | }
173 |
174 | try {
175 | result = await this.#writer.write(response);
176 | } catch (e) {
177 | console.warn((e as Error).message, e);
178 | } finally {
179 | await publish(DONE, this.#requestId);
180 | }
181 | return result as AIOutput;
182 | }
183 |
184 | }
--------------------------------------------------------------------------------
/apps/web/src/hooks/use-backend-chat.ts:
--------------------------------------------------------------------------------
1 | import { useRef, useEffect, useState, useCallback } from "react";
2 | import { customAlphabet } from "nanoid";
3 | import type { UseChatOptions } from "ai/react";
4 | import type {
5 | ChatRequestOptions,
6 | CreateMessage,
7 | Message,
8 | } from "ai";
9 | import usePartySocket from "partysocket/react";
10 |
11 | const nanoid = customAlphabet(
12 | "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
13 | 7
14 | );
15 |
16 | /**
17 | * 👋 We can tune function calls and avoid hallucinations with a system message.
18 | * @see https://platform.openai.com/docs/guides/gpt/function-calling
19 | */
20 | const DEFAULT_SYSTEM_MESSAGE: Message = {
21 | id: nanoid(),
22 | role: "system",
23 | content: "Don't make assumptions about what values to plug into functions. Ask for clarification if a user request is ambiguous."
24 | }
25 |
26 | /**
27 | * 👋 This is a hook that provides a chat interface to the OpenAI API. The primary
28 | * different between this and the `useChat` hook is that this hook will receive
29 | * messages from the backend via a socket connection.
30 | *
31 | * This is useful for our process where we want to run functions async BUT we
32 | * want to be able to confirm the action first so simply stream chat responses won't
33 | * work.
34 | * @param chatOptions
35 | * @returns
36 | */
37 | export function useBackendChat({
38 | id,
39 | initialMessages = [DEFAULT_SYSTEM_MESSAGE],
40 | initialInput = "",
41 | }: UseChatOptions = {}) {
42 |
43 | // Generate a unique id for the chat if not provided
44 |
45 | const [lastMessage, setLastMessage] = useState(null);
46 | const [messages, setMessages] = useState<(Message | CreateMessage)[]>(initialMessages);
47 | const [isLoading, setIsLoading] = useState(false);
48 | const [isConfirmRequired, setIsConfirmRequired] = useState(false);
49 | // 👋 open a socket so that we can broadcast streaming responses from
50 | // openai api to any connected clients.
51 | const socket = usePartySocket({
52 | room: process.env.NEXT_PUBLIC_PARTYKIT_ROOM_NAME!,
53 | host: process.env.NEXT_PUBLIC_PARTY_KIT_URL!,
54 | onMessage: (message) => {
55 | setLastMessage(message);
56 | }
57 | });
58 |
59 | // We use the socket id as the `requestId` that is used to track the workflow
60 | const requestId = useRef(socket.id).current;
61 |
62 | // Keep a mutable buffer of incoming messages
63 | const [tokenBuffer, setTokenBuffer] = useState([]);
64 |
65 | // Keep the latest messages in a ref.
66 | const messagesRef = useRef<(Message | CreateMessage)[]>(messages);
67 | useEffect(() => {
68 | messagesRef.current = messages;
69 | }, [messages]);
70 |
71 | // Abort controller to cancel the current API call.
72 | //
73 | // TODO: Send cancellation event.
74 | const abortControllerRef = useRef(null);
75 |
76 | const mutate = useCallback(
77 | async (currentMessages?: (CreateMessage | Message)[]) => {
78 | setIsLoading(true);
79 | await fetch("/api/chat", {
80 | method: "POST",
81 | body: JSON.stringify({
82 | messages: currentMessages || messages,
83 | requestId,
84 | }),
85 | });
86 | },
87 | [messages, requestId]
88 | );
89 |
90 | const stop = useCallback(() => {
91 | if (abortControllerRef.current) {
92 | abortControllerRef.current.abort();
93 | abortControllerRef.current = null;
94 |
95 | }
96 | console.log('cancel::' + requestId, socket)
97 | socket.send(`cancel::${requestId}`)
98 | }, [socket]);
99 |
100 | const append = useCallback(
101 | async (message: Message | CreateMessage) => {
102 | if (!message.id) {
103 | message.id = nanoid();
104 | }
105 | if (!message.createdAt) {
106 | message.createdAt = new Date();
107 | }
108 | const history = messages.concat([message]);
109 | setMessages(history);
110 | await mutate(history);
111 | },
112 | [messages]
113 | );
114 |
115 | // State to update chat UI
116 | useEffect(() => {
117 | if (lastMessage?.data === `\\confirm`) {
118 | setIsLoading(false);
119 | setIsConfirmRequired(true);
120 | return;
121 | }
122 |
123 | if (lastMessage?.data === `\\ok`) {
124 | setIsLoading(false);
125 | if (tokenBuffer.length === 0) {
126 | return;
127 | }
128 |
129 | // If the first tokenBuffer contains "function_call", parse this entire
130 | // item as a function call.
131 | let history = messages;
132 | if (tokenBuffer[0].includes("function_call")) {
133 | const call = JSON.parse(tokenBuffer.join(""));
134 | history = messages.concat([
135 | {
136 | id: lastMessage.id,
137 | role: "assistant",
138 | content: '',
139 | function_call: call?.function_call || call,
140 | createdAt: new Date(),
141 | },
142 | ]);
143 | } else {
144 | history = messages.concat([
145 | {
146 | id: lastMessage.id,
147 | role: "assistant",
148 | content: tokenBuffer.join(""),
149 | createdAt: new Date(),
150 | },
151 | ]);
152 | }
153 |
154 | // This is the end of our token stream.
155 | setTokenBuffer([]);
156 | setMessages(history);
157 | return;
158 | }
159 |
160 | if (lastMessage !== null) {
161 | setTokenBuffer(tokenBuffer.concat([lastMessage.data]));
162 | }
163 | }, [lastMessage]);
164 |
165 | const handleInputChange = (e: any) => {
166 | setInput(e.target.value);
167 | };
168 |
169 | // Input state and handlers.
170 | const [input, setInput] = useState(initialInput);
171 |
172 | const handleSubmit = useCallback(
173 | (
174 | e: React.FormEvent,
175 | { options, functions, function_call }: ChatRequestOptions = {},
176 | metadata?: Object
177 | ) => {
178 | e.preventDefault();
179 | if (!input) return;
180 |
181 | append({
182 | content: input,
183 | role: "user",
184 | createdAt: new Date(),
185 | });
186 | setInput("");
187 | },
188 | [input, append]
189 | );
190 |
191 | const onConfirm = useCallback(
192 | async (confirm: boolean) => {
193 | setIsLoading(true);
194 | setIsConfirmRequired(false);
195 | await fetch("/api/chat", {
196 | method: "POST",
197 | body: JSON.stringify({
198 | requestId,
199 | confirm
200 | }),
201 | });
202 |
203 | },
204 | []
205 | );
206 |
207 | return {
208 | messages,
209 | buffer: tokenBuffer.join(""),
210 | requestId,
211 | isConfirmRequired,
212 | onConfirm,
213 | append,
214 | stop,
215 | setMessages,
216 | input,
217 | setInput,
218 | handleInputChange,
219 | handleSubmit,
220 | isLoading,
221 | socket
222 | };
223 | }
224 |
225 |
--------------------------------------------------------------------------------
/apps/openai-party/public/normalize.css:
--------------------------------------------------------------------------------
1 | /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
2 |
3 | /* Document
4 | ========================================================================== */
5 |
6 | /**
7 | * 1. Correct the line height in all browsers.
8 | * 2. Prevent adjustments of font size after orientation changes in iOS.
9 | */
10 |
11 | html {
12 | line-height: 1.15; /* 1 */
13 | -webkit-text-size-adjust: 100%; /* 2 */
14 | }
15 |
16 | /* Sections
17 | ========================================================================== */
18 |
19 | /**
20 | * Remove the margin in all browsers.
21 | */
22 |
23 | body {
24 | margin: 0;
25 | }
26 |
27 | /**
28 | * Render the `main` element consistently in IE.
29 | */
30 |
31 | main {
32 | display: block;
33 | }
34 |
35 | /**
36 | * Correct the font size and margin on `h1` elements within `section` and
37 | * `article` contexts in Chrome, Firefox, and Safari.
38 | */
39 |
40 | h1 {
41 | font-size: 2em;
42 | margin: 0.67em 0;
43 | }
44 |
45 | /* Grouping content
46 | ========================================================================== */
47 |
48 | /**
49 | * 1. Add the correct box sizing in Firefox.
50 | * 2. Show the overflow in Edge and IE.
51 | */
52 |
53 | hr {
54 | box-sizing: content-box; /* 1 */
55 | height: 0; /* 1 */
56 | overflow: visible; /* 2 */
57 | }
58 |
59 | /**
60 | * 1. Correct the inheritance and scaling of font size in all browsers.
61 | * 2. Correct the odd `em` font sizing in all browsers.
62 | */
63 |
64 | pre {
65 | font-family: monospace, monospace; /* 1 */
66 | font-size: 1em; /* 2 */
67 | }
68 |
69 | /* Text-level semantics
70 | ========================================================================== */
71 |
72 | /**
73 | * Remove the gray background on active links in IE 10.
74 | */
75 |
76 | a {
77 | background-color: transparent;
78 | }
79 |
80 | /**
81 | * 1. Remove the bottom border in Chrome 57-
82 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
83 | */
84 |
85 | abbr[title] {
86 | border-bottom: none; /* 1 */
87 | text-decoration: underline; /* 2 */
88 | text-decoration: underline dotted; /* 2 */
89 | }
90 |
91 | /**
92 | * Add the correct font weight in Chrome, Edge, and Safari.
93 | */
94 |
95 | b,
96 | strong {
97 | font-weight: bolder;
98 | }
99 |
100 | /**
101 | * 1. Correct the inheritance and scaling of font size in all browsers.
102 | * 2. Correct the odd `em` font sizing in all browsers.
103 | */
104 |
105 | code,
106 | kbd,
107 | samp {
108 | font-family: monospace, monospace; /* 1 */
109 | font-size: 1em; /* 2 */
110 | }
111 |
112 | /**
113 | * Add the correct font size in all browsers.
114 | */
115 |
116 | small {
117 | font-size: 80%;
118 | }
119 |
120 | /**
121 | * Prevent `sub` and `sup` elements from affecting the line height in
122 | * all browsers.
123 | */
124 |
125 | sub,
126 | sup {
127 | font-size: 75%;
128 | line-height: 0;
129 | position: relative;
130 | vertical-align: baseline;
131 | }
132 |
133 | sub {
134 | bottom: -0.25em;
135 | }
136 |
137 | sup {
138 | top: -0.5em;
139 | }
140 |
141 | /* Embedded content
142 | ========================================================================== */
143 |
144 | /**
145 | * Remove the border on images inside links in IE 10.
146 | */
147 |
148 | img {
149 | border-style: none;
150 | }
151 |
152 | /* Forms
153 | ========================================================================== */
154 |
155 | /**
156 | * 1. Change the font styles in all browsers.
157 | * 2. Remove the margin in Firefox and Safari.
158 | */
159 |
160 | button,
161 | input,
162 | optgroup,
163 | select,
164 | textarea {
165 | font-family: inherit; /* 1 */
166 | font-size: 100%; /* 1 */
167 | line-height: 1.15; /* 1 */
168 | margin: 0; /* 2 */
169 | }
170 |
171 | /**
172 | * Show the overflow in IE.
173 | * 1. Show the overflow in Edge.
174 | */
175 |
176 | button,
177 | input {
178 | /* 1 */
179 | overflow: visible;
180 | }
181 |
182 | /**
183 | * Remove the inheritance of text transform in Edge, Firefox, and IE.
184 | * 1. Remove the inheritance of text transform in Firefox.
185 | */
186 |
187 | button,
188 | select {
189 | /* 1 */
190 | text-transform: none;
191 | }
192 |
193 | /**
194 | * Correct the inability to style clickable types in iOS and Safari.
195 | */
196 |
197 | button,
198 | [type="button"],
199 | [type="reset"],
200 | [type="submit"] {
201 | -webkit-appearance: button;
202 | }
203 |
204 | /**
205 | * Remove the inner border and padding in Firefox.
206 | */
207 |
208 | button::-moz-focus-inner,
209 | [type="button"]::-moz-focus-inner,
210 | [type="reset"]::-moz-focus-inner,
211 | [type="submit"]::-moz-focus-inner {
212 | border-style: none;
213 | padding: 0;
214 | }
215 |
216 | /**
217 | * Restore the focus styles unset by the previous rule.
218 | */
219 |
220 | button:-moz-focusring,
221 | [type="button"]:-moz-focusring,
222 | [type="reset"]:-moz-focusring,
223 | [type="submit"]:-moz-focusring {
224 | outline: 1px dotted ButtonText;
225 | }
226 |
227 | /**
228 | * Correct the padding in Firefox.
229 | */
230 |
231 | fieldset {
232 | padding: 0.35em 0.75em 0.625em;
233 | }
234 |
235 | /**
236 | * 1. Correct the text wrapping in Edge and IE.
237 | * 2. Correct the color inheritance from `fieldset` elements in IE.
238 | * 3. Remove the padding so developers are not caught out when they zero out
239 | * `fieldset` elements in all browsers.
240 | */
241 |
242 | legend {
243 | box-sizing: border-box; /* 1 */
244 | color: inherit; /* 2 */
245 | display: table; /* 1 */
246 | max-width: 100%; /* 1 */
247 | padding: 0; /* 3 */
248 | white-space: normal; /* 1 */
249 | }
250 |
251 | /**
252 | * Add the correct vertical alignment in Chrome, Firefox, and Opera.
253 | */
254 |
255 | progress {
256 | vertical-align: baseline;
257 | }
258 |
259 | /**
260 | * Remove the default vertical scrollbar in IE 10+.
261 | */
262 |
263 | textarea {
264 | overflow: auto;
265 | }
266 |
267 | /**
268 | * 1. Add the correct box sizing in IE 10.
269 | * 2. Remove the padding in IE 10.
270 | */
271 |
272 | [type="checkbox"],
273 | [type="radio"] {
274 | box-sizing: border-box; /* 1 */
275 | padding: 0; /* 2 */
276 | }
277 |
278 | /**
279 | * Correct the cursor style of increment and decrement buttons in Chrome.
280 | */
281 |
282 | [type="number"]::-webkit-inner-spin-button,
283 | [type="number"]::-webkit-outer-spin-button {
284 | height: auto;
285 | }
286 |
287 | /**
288 | * 1. Correct the odd appearance in Chrome and Safari.
289 | * 2. Correct the outline style in Safari.
290 | */
291 |
292 | [type="search"] {
293 | -webkit-appearance: textfield; /* 1 */
294 | outline-offset: -2px; /* 2 */
295 | }
296 |
297 | /**
298 | * Remove the inner padding in Chrome and Safari on macOS.
299 | */
300 |
301 | [type="search"]::-webkit-search-decoration {
302 | -webkit-appearance: none;
303 | }
304 |
305 | /**
306 | * 1. Correct the inability to style clickable types in iOS and Safari.
307 | * 2. Change font properties to `inherit` in Safari.
308 | */
309 |
310 | ::-webkit-file-upload-button {
311 | -webkit-appearance: button; /* 1 */
312 | font: inherit; /* 2 */
313 | }
314 |
315 | /* Interactive
316 | ========================================================================== */
317 |
318 | /*
319 | * Add the correct display in Edge, IE 10+, and Firefox.
320 | */
321 |
322 | details {
323 | display: block;
324 | }
325 |
326 | /*
327 | * Add the correct display in all browsers.
328 | */
329 |
330 | summary {
331 | display: list-item;
332 | }
333 |
334 | /* Misc
335 | ========================================================================== */
336 |
337 | /**
338 | * Add the correct display in IE 10+.
339 | */
340 |
341 | template {
342 | display: none;
343 | }
344 |
345 | /**
346 | * Add the correct display in IE 10.
347 | */
348 |
349 | [hidden] {
350 | display: none;
351 | }
352 |
--------------------------------------------------------------------------------
/apps/web/src/components/chat-box.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import * as React from 'react'
3 | import ReactMarkdown from 'react-markdown'
4 | import { useBackendChat } from '@/hooks/use-backend-chat';
5 | import { Button } from '@/components/ui/button';
6 | import { Textarea } from '@/components/ui/textarea';
7 | import { Send, Loading } from "@/components/icons";
8 | import type {CreateMessage, Message} from "ai";
9 |
10 | export default function Chat() {
11 | const formRef = React.useRef(null);
12 | const inputRef = React.useRef(null);
13 | const {input, setInput, handleSubmit, isLoading, messages, buffer, isConfirmRequired, onConfirm, stop} = useBackendChat()
14 | const disabled = input.length === 0 || isLoading;
15 |
16 | React.useEffect(() => {
17 | window.scrollTo({
18 | top: window.innerHeight*2,
19 | behavior: "smooth"
20 | });
21 | }, [buffer]);
22 | return (
23 |
24 |
25 |
InngestAI
26 |
27 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | Welcome to Inngest + Vercel AI!
37 |
38 |
39 | An open-source framework that calls OpenAI functions in the
40 | background securely, with everything handled for you:
41 |
42 |
43 | Function state automatically managed
44 | Automatic retries
45 | Audit trails and logging
46 | Auto-cancellation on navigation change
47 | Background to browser streaming
48 | User confirmations built in
49 |
50 |
51 |
52 |
53 |
54 |
55 | {messages.filter((m) => m.role !== 'system').map((m) => (
56 |
57 | ))}
58 |
59 | {isConfirmRequired && }
60 |
61 | {isLoading && (
62 |
67 | )}
68 |
69 |
113 |
117 | Stop
118 |
119 |
120 | )
121 | }
122 |
123 |
124 | const MessageUI = ({ message, planning = false }: { message: Message | CreateMessage, planning?: boolean }) => {
125 | let classes = "";
126 | switch (message.role) {
127 | case "system" || "assistant":
128 | classes = "border-neutral-400 border-l-[3px] text-neutral-900 bg-neutral-100";
129 | break;
130 | default:
131 | classes = "border-blue-300 border-l-[3px] text-neutral-900 bg-white py-6";
132 | break;
133 | }
134 |
135 | if (message.function_call) {
136 | return
137 | }
138 |
139 | return (
140 |
146 |
147 |
148 | {message.role === "user" ? "You" : "AI"} {message.createdAt && ` · ${message.createdAt?.toLocaleString()}` }
149 |
150 |
151 |
152 |
153 | );
154 | };
155 |
156 | const CallUI = ({ message, planning = false }: { message: Message | CreateMessage, planning?: boolean }) => {
157 | const call = message.function_call === "string" ? JSON.parse(message.function_call) : message.function_call;
158 | return (
159 |
170 |
171 | Function call
172 | {call?.name}{!planning && ({call.arguments}) }
173 |
174 |
175 | );
176 | }
177 |
178 | const ConfirmUI = ({ onConfirm } : {onConfirm: (ok:boolean) => void}) => {
179 | React.useEffect(() => {
180 | window.scrollTo({
181 | top: window.innerHeight*2,
182 | behavior: "smooth"
183 | });
184 | }, []);
185 |
186 | return (
187 |
198 |
199 |
Are you sure you want to call this function?
200 |
This function requires confirmation before being invoked.
201 |
202 |
203 |
204 | onConfirm(true)}
207 | >
208 | Yes
209 |
210 | onConfirm(false)}
213 | >
214 | No
215 |
216 |
217 |
218 | );
219 | }
220 |
221 |
222 | const BufferUI = ({ buffer }: { buffer: string }) => {
223 | if (!buffer || buffer.length === 0) {
224 | return null;
225 | }
226 |
227 | if (buffer.indexOf("{") === 0) {
228 | let call = {
229 | name: "Planning function...",
230 | arguments: ''
231 | };
232 | try { call = JSON.parse(buffer) } catch(e) {};
233 | return (
234 |
243 | )
244 | }
245 |
246 | return (
247 |
255 | )
256 | };
257 |
--------------------------------------------------------------------------------
/apps/openai-party/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Visit https://aka.ms/tsconfig to read more about this file */
4 |
5 | /* Projects */
6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
12 |
13 | /* Language and Environment */
14 | "target": "es2020" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
16 | // "jsx": "preserve", /* Specify what JSX code is generated. */
17 | // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
26 |
27 | /* Modules */
28 | "module": "ES2020" /* Specify what module code is generated. */,
29 | // "rootDir": "./", /* Specify the root folder within your source files. */
30 | "moduleResolution": "Node" /* Specify how TypeScript looks up a file from a given module specifier. */,
31 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
34 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
35 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */
36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
37 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
38 | // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
39 | // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
40 | // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
41 | // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
42 | "resolveJsonModule": true /* Enable importing .json files. */,
43 | // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
44 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */
45 |
46 | /* JavaScript Support */
47 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
48 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
49 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
50 |
51 | /* Emit */
52 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
53 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */
54 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
55 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */
56 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
57 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
58 | // "outDir": "./", /* Specify an output folder for all emitted files. */
59 | // "removeComments": true, /* Disable emitting comments. */
60 | "noEmit": true /* Disable emitting files from a compilation. */,
61 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
62 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
63 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
64 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
65 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
66 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
67 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
68 | // "newLine": "crlf", /* Set the newline character for emitting files. */
69 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
70 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
71 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
72 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
73 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */
74 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
75 |
76 | /* Interop Constraints */
77 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
78 | "verbatimModuleSyntax": true /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */,
79 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
80 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */,
81 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
82 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
83 |
84 | /* Type Checking */
85 | "strict": true /* Enable all strict type-checking options. */,
86 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
87 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
88 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
89 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
90 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
91 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
92 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
93 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
94 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
95 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
96 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
97 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
98 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
99 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
100 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
101 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
102 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
103 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
104 |
105 | /* Completeness */
106 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
107 | "skipLibCheck": true /* Skip type checking all .d.ts files. */
108 | }
109 | }
110 |
--------------------------------------------------------------------------------