zod-to-openai-tool
Version:
Easily create tools from zod schemas to use with OpenAI Assistants and Chat Completions
1 lines • 11.9 kB
Source Map (JSON)
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import type { OpenAI } from \"openai\";\nimport { z } from \"zod\";\nimport { zodToJsonSchema } from \"zod-to-json-schema\";\ninterface Steps<T = void, Omitted extends string = never> {\n /**\n * Adds a schema for the tool. This will be used to validate the input and to infer the type of the input in the `run()` function.\n * @param s The schema for the input. Must be a `z.object({})`\n * @returns A tool with the input schema set.\n */\n input<S extends z.AnyZodObject>(\n schema: S,\n ): Omit<Steps<z.infer<S>, Omitted | \"input\">, \"input\" | Omitted> &\n InternalTool;\n /**\n * The function to run when the model calls the tool. This is the only required builder step.\n * @param args The arguments for the `run()` function.\n * The type of the arguments will be inferred from the input schema. If there is no input schema, the type will be `void`.\n * @returns A tool with the `run()` function set.\n */\n run(\n func: (input: T extends void ? never : T) => unknown,\n ): Omit<Steps<T, Omitted | \"run\">, \"run\" | \"input\" | Omitted> & InternalTool;\n /**\n * Adds a description to the tool. This will be provided to the model to aid in understanding the tool.\n * @param d The description of the tool as a string.\n * It is good to explain what data the tool returns and what it does here.\n * @returns A tool with the description set.\n */\n describe(\n description: string,\n ): Omit<Steps<T, Omitted | \"describe\">, Omitted | \"describe\"> & InternalTool;\n}\n\ntype CheckHasSetRun<T> = T extends { run: any } ? never : T;\n\ninterface Data {\n func: (input: any) => unknown;\n schema: z.AnyZodObject;\n description: string | undefined;\n}\n\ninterface InternalTool {\n _data: Data;\n _parameters: OpenAI.Beta.FunctionTool[\"function\"][\"parameters\"];\n}\n\ntype OpenAIBuiltInTool = OpenAI.Beta.Assistant[\"tools\"][number];\n\nexport type Tool<T = void, O extends string = never> = Steps<T, O> &\n InternalTool;\n\nfunction tool<T = void>(): Tool<T> {\n const data: Data = {\n schema: z.object({}),\n func: () => {},\n description: undefined,\n };\n\n return {\n input<S extends z.AnyZodObject>(s: S) {\n data.schema = s;\n return this as Tool<z.infer<S>, \"input\">;\n },\n run(f) {\n data.func = f;\n return this;\n },\n describe(d) {\n data.description = d;\n return this;\n },\n /** @internal */\n get _data() {\n return data;\n },\n /** @internal */\n get _parameters() {\n const { $schema, ...parameters } = zodToJsonSchema(data.schema);\n return parameters;\n },\n };\n}\n\n/**\n * Creates a tool for use with openai assistants\n * @example\n * ```ts\n * const getWeather = t\n * .input(\n * z.object({\n * city: z.string(),\n * }))\n * .describe(\"Gets the weather\")\n * .run(async ({ city }) => ({\n * weather: \"sunny\",\n * }));\n * ```\n */\nexport const t: Steps<void> & {\n /**\n * Alias to the `file_search` tool\n * @see https://platform.openai.com/docs/assistants/tools/knowledge-retrieval\n */\n fileSearch: OpenAI.Beta.FileSearchTool;\n /**\n * Alias to the `code_interpreter` tool\n * @see https://platform.openai.com/docs/assistants/tools/code-interpreter\n */\n codeInterpreter: OpenAI.Beta.CodeInterpreterTool;\n} = {\n input<S extends z.AnyZodObject>(s: S) {\n return tool().input<S>(s);\n },\n run(...args: Parameters<Tool[\"run\"]>) {\n return tool().run(...args);\n },\n describe(d: string) {\n return tool().describe(d);\n },\n codeInterpreter: { type: \"code_interpreter\" },\n fileSearch: { type: \"file_search\" },\n};\n\n/**\n *\n * @param tools An object containing tools created with `t.run()`. Name them using the key.\n * @param onError A function that will be called when a tool throws an error. The error will be passed as the first argument.\n * If this function returns a value, that value will be used as the output of the tool.\n * If you do not provide a function, the error will be stringified and sent to the assistant.\n * If the function returns `undefined` or `null`, the error will be sent to the assistant.\n * @returns An object containing the tools and a function to process actions.\n * @example\n * ```ts\n * const { t, processAssistantActions, processChatActions } = createTools({\n * getWeather, // These are created with `t.run()` and `t.input()`, see the example for `t`\n * exponential,\n * });\n *\n * // Then use them like this:\n * const assistant = await openai.beta.assistants.create({\n * tools,\n * //...\n * });\n * ```\n */\nexport function createTools<T>(\n tools: { [K in keyof T]: InternalTool & CheckHasSetRun<T[K]> },\n onError?: (error: unknown) => any,\n) {\n type _Tool = (typeof tools)[keyof T];\n\n function _processActions(\n data: (OpenAI.Beta.Threads.Runs.RequiredActionFunctionToolCall &\n OpenAI.Chat.ChatCompletionMessageToolCall)[],\n ) {\n const results = Promise.all(\n data.map(async ({ function: { arguments: args, name }, id }, i) => {\n const tool = tools[name as keyof T];\n let output;\n try {\n const input = await tool._data.schema.parseAsync(JSON.parse(args));\n output = await tool._data.func(input);\n } catch (error) {\n error = onError?.(error) ?? error;\n if (error instanceof Error) {\n error = error.message;\n }\n output = { error };\n }\n return { id, output: JSON.stringify(output) };\n }),\n );\n return results;\n }\n\n return {\n tools: Object.entries<_Tool>(tools).map(\n ([name, tool]): OpenAI.Beta.FunctionTool &\n OpenAI.Chat.Completions.ChatCompletionTool => {\n const parameters = tool._parameters;\n return {\n type: \"function\",\n function: { name, description: tool._data.description, parameters },\n };\n },\n ),\n /**\n * Process the actions from the chat completion.\n * @param data The tool calls generated from the chat completion. (`message.tool_calls`)\n * @returns The message which should be sent with the messages to generate the result based on the tool calls\n */\n async processChatActions(\n data: OpenAI.Chat.ChatCompletionMessageToolCall[] = [],\n ) {\n return (await _processActions(data)).map(\n ({ id, output }) =>\n ({\n tool_call_id: id,\n role: \"tool\",\n content: output,\n }) as OpenAI.Chat.Completions.ChatCompletionToolMessageParam,\n );\n },\n /**\n * Process the actions from the assistant run.\n * @param data The tool calls generated from the assistant run. (`run.required_action.submit_tool_outputs.tool_calls`)\n * @returns The tool outputs which should be sent to `runs.submitToolOutputs()` to continue the run.\n */\n async processAssistantActions(\n data: OpenAI.Beta.Threads.Runs.RequiredActionFunctionToolCall[] = [],\n ) {\n return (await _processActions(data)).map(\n ({ id, output }) =>\n ({\n tool_call_id: id,\n output,\n }) as OpenAI.Beta.Threads.Runs.RunSubmitToolOutputsParams.ToolOutput,\n );\n },\n };\n}\n\ntype CreateToolsOutput = ReturnType<typeof createTools>;\ntype AnyTool = ReturnType<typeof createTools> | OpenAIBuiltInTool;\n\n/**\n * Combine multiple tools into one object that can be used with an assistant.\n * @param tools All tools to combine. You can provide tools created with `createTools()` or built in tools from the OpenAI API (CodeInterpreter and Retrieval).\n * @returns The same object as `createTools()`, but with all tools combined.\n * @see https://platform.openai.com/docs/assistants/tools - for more information on the OpenAI API tools.\n * @example\n * ```ts\n * const { tools, processAssistantActions } = combineTools(\n * createTools({\n * getWeather,\n * exponential,\n * }),\n * { type: \"code_interpreter\" },\n * { type: \"retrieval\" },\n * );\n * ```\n */\nexport function combineTools(...tools: AnyTool[]): Omit<\n CreateToolsOutput,\n \"tools\"\n> & {\n tools: OpenAIBuiltInTool[];\n} {\n const customTools = tools.filter(\n (t): t is Exclude<typeof t, OpenAIBuiltInTool> => \"tools\" in t,\n );\n\n const combinedCustomTools = {\n tools: customTools.flatMap(t => t.tools),\n async processChatActions(\n data: OpenAI.Chat.ChatCompletionMessageToolCall[] = [],\n ) {\n return (\n await Promise.all(customTools.map(t => t.processChatActions(data)))\n ).flat();\n },\n async processAssistantActions(\n data: OpenAI.Beta.Threads.Runs.RequiredActionFunctionToolCall[] = [],\n ) {\n return (\n await Promise.all(customTools.map(t => t.processAssistantActions(data)))\n ).flat();\n },\n };\n const builtInTools = tools.filter((t): t is OpenAIBuiltInTool => \"type\" in t);\n\n return {\n tools: [...combinedCustomTools.tools, ...builtInTools],\n processChatActions: combinedCustomTools.processChatActions,\n processAssistantActions: combinedCustomTools.processAssistantActions,\n };\n}\n"],"mappings":"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","names":["tool","t"]}