Skip to content

Commit 1260ec6

Browse files
committed
start adding option for new iterator
1 parent 8f8d768 commit 1260ec6

File tree

8 files changed

+830
-0
lines changed

8 files changed

+830
-0
lines changed

examples/tool-calls-beta-zod.ts

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
#!/usr/bin/env -S npm run tsn -T
2+
3+
import OpenAI from 'openai';
4+
import { betaZodTool } from 'openai/helpers/beta/zod';
5+
// import { BetaToolUseBlock } from 'openai/helpers/beta';
6+
import { z } from 'zod';
7+
8+
const client = new OpenAI();
9+
10+
async function main() {
11+
const runner = client.beta.chat.completions.toolRunner({
12+
messages: [
13+
{
14+
role: 'user',
15+
content: `I'm planning a trip to San Francisco and I need some information. Can you help me with the weather, current time, and currency exchange rates (from EUR)? Please use parallel tool use`,
16+
},
17+
],
18+
tools: [
19+
betaZodTool({
20+
name: 'getWeather',
21+
description: 'Get the weather at a specific location',
22+
inputSchema: z.object({
23+
location: z.string().describe('The city and state, e.g. San Francisco, CA'),
24+
}),
25+
run: ({ location }) => {
26+
return `The weather is sunny with a temperature of 20°C in ${location}.`;
27+
},
28+
}),
29+
betaZodTool({
30+
name: 'getTime',
31+
description: 'Get the current time in a specific timezone',
32+
inputSchema: z.object({
33+
timezone: z.string().describe('The timezone, e.g. America/Los_Angeles'),
34+
}),
35+
run: ({ timezone }) => {
36+
return `The current time in ${timezone} is 3:00 PM.`;
37+
},
38+
}),
39+
betaZodTool({
40+
name: 'getCurrencyExchangeRate',
41+
description: 'Get the exchange rate between two currencies',
42+
inputSchema: z.object({
43+
from_currency: z.string().describe('The currency to convert from, e.g. USD'),
44+
to_currency: z.string().describe('The currency to convert to, e.g. EUR'),
45+
}),
46+
run: ({ from_currency, to_currency }) => {
47+
return `The exchange rate from ${from_currency} to ${to_currency} is 0.85.`;
48+
},
49+
}),
50+
],
51+
model: 'gpt-4o',
52+
max_tokens: 1024,
53+
// This limits the conversation to at most 10 back and forth between the API.
54+
max_iterations: 10,
55+
});
56+
57+
console.log(`\n🚀 Running tools...\n`);
58+
59+
for await (const message of runner) {
60+
console.log(`┌─ Message ${message.id} `.padEnd(process.stdout.columns, '─'));
61+
console.log();
62+
63+
const { choices } = message;
64+
const firstChoice = choices.at(0)!;
65+
66+
console.log(`${firstChoice.message.content}\n`); // text
67+
// each tool call (could be many)
68+
for (const toolCall of firstChoice.message.tool_calls ?? []) {
69+
if (toolCall.type === 'function') {
70+
console.log(`${toolCall.function.name}(${JSON.stringify(toolCall.function.arguments, null, 2)})\n`);
71+
}
72+
}
73+
74+
console.log(`└─`.padEnd(process.stdout.columns, '─'));
75+
console.log();
76+
console.log();
77+
78+
// const defaultResponse = await runner.generateToolResponse();
79+
// if (defaultResponse && typeof defaultResponse.content !== 'string') {
80+
// console.log(`┌─ Response `.padEnd(process.stdout.columns, '─'));
81+
// console.log();
82+
83+
// for (const block of defaultResponse.content) {
84+
// if (block.type === 'tool_result') {
85+
// const toolUseBlock = message.content.find((b): b is BetaToolUseBlock => {
86+
// return b.type === 'tool_use' && b.id === block.tool_use_id;
87+
// })!;
88+
// console.log(`${toolUseBlock.name}(): ${block.content}`);
89+
// }
90+
// }
91+
92+
// console.log();
93+
// console.log(`└─`.padEnd(process.stdout.columns, '─'));
94+
// console.log();
95+
// console.log();
96+
// }
97+
}
98+
}
99+
100+
main();

src/helpers/beta/zod.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { transformJSONSchema } from '../..//lib/transform-json-schema';
2+
import type { infer as zodInfer, ZodType } from 'zod';
3+
import * as z from 'zod';
4+
import { OpenAIError } from '../../core/error';
5+
// import { AutoParseableBetaOutputFormat } from '../../lib/beta-parser';
6+
// import { BetaRunnableTool, Promisable } from '../../lib/tools/BetaRunnableTool';
7+
// import { BetaToolResultContentBlockParam } from '../../resources/beta';
8+
/**
9+
* Creates a JSON schema output format object from the given Zod schema.
10+
*
11+
* If this is passed to the `.parse()` method then the response message will contain a
12+
* `.parsed` property that is the result of parsing the content with the given Zod object.
13+
*
14+
* This can be passed directly to the `.create()` method but will not
15+
* result in any automatic parsing, you'll have to parse the response yourself.
16+
*/
17+
export function betaZodOutputFormat<ZodInput extends ZodType>(
18+
zodObject: ZodInput,
19+
): AutoParseableBetaOutputFormat<zodInfer<ZodInput>> {
20+
let jsonSchema = z.toJSONSchema(zodObject, { reused: 'ref' });
21+
22+
jsonSchema = transformJSONSchema(jsonSchema);
23+
24+
return {
25+
type: 'json_schema',
26+
schema: {
27+
...jsonSchema,
28+
},
29+
parse: (content) => {
30+
const output = zodObject.safeParse(JSON.parse(content));
31+
32+
if (!output.success) {
33+
throw new OpenAIError(
34+
`Failed to parse structured output: ${output.error.message} cause: ${output.error.issues}`,
35+
);
36+
}
37+
38+
return output.data;
39+
},
40+
};
41+
}
42+
43+
/**
44+
* Creates a tool using the provided Zod schema that can be passed
45+
* into the `.toolRunner()` method. The Zod schema will automatically be
46+
* converted into JSON Schema when passed to the API. The provided function's
47+
* input arguments will also be validated against the provided schema.
48+
*/
49+
export function betaZodTool<InputSchema extends ZodType>(options: {
50+
name: string;
51+
inputSchema: InputSchema;
52+
description: string;
53+
run: (args: zodInfer<InputSchema>) => Promisable<string | Array<BetaToolResultContentBlockParam>>;
54+
}): BetaRunnableTool<zodInfer<InputSchema>> {
55+
const jsonSchema = z.toJSONSchema(options.inputSchema, { reused: 'ref' });
56+
57+
if (jsonSchema.type !== 'object') {
58+
throw new Error(`Zod schema for tool "${options.name}" must be an object, but got ${jsonSchema.type}`);
59+
}
60+
61+
// TypeScript doesn't narrow the type after the runtime check, so we need to assert it
62+
const objectSchema = jsonSchema as typeof jsonSchema & { type: 'object' };
63+
64+
return {
65+
type: 'custom',
66+
name: options.name,
67+
input_schema: objectSchema,
68+
description: options.description,
69+
run: options.run,
70+
parse: (args: unknown) => options.inputSchema.parse(args) as zodInfer<InputSchema>,
71+
};
72+
}

src/internal/utils/values.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,3 +103,10 @@ export const safeJSON = (text: string) => {
103103
return undefined;
104104
}
105105
};
106+
107+
// Gets a value from an object, deletes the key, and returns the value (or undefined if not found)
108+
export const pop = <T extends Record<string, any>, K extends string>(obj: T, key: K): T[K] => {
109+
const value = obj[key];
110+
delete obj[key];
111+
return value;
112+
};

src/lib/beta-parser.ts

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { OpenAIError } from '../core/error';
2+
import {
3+
BetaContentBlock,
4+
BetaJSONOutputFormat,
5+
BetaMessage,
6+
BetaTextBlock,
7+
MessageCreateParams,
8+
} from '../resources/beta/messages/messages';
9+
10+
// vendored from typefest just to make things look a bit nicer on hover
11+
type Simplify<T> = { [KeyType in keyof T]: T[KeyType] } & {};
12+
13+
export type BetaParseableMessageCreateParams = Simplify<
14+
Omit<MessageCreateParams, 'output_format'> & {
15+
output_format?: BetaJSONOutputFormat | AutoParseableBetaOutputFormat<any> | null;
16+
}
17+
>;
18+
19+
export type ExtractParsedContentFromBetaParams<Params extends BetaParseableMessageCreateParams> =
20+
Params['output_format'] extends AutoParseableBetaOutputFormat<infer P> ? P : null;
21+
22+
export type AutoParseableBetaOutputFormat<ParsedT> = BetaJSONOutputFormat & {
23+
parse(content: string): ParsedT;
24+
};
25+
26+
export type ParsedBetaMessage<ParsedT> = BetaMessage & {
27+
content: Array<ParsedBetaContentBlock<ParsedT>>;
28+
parsed_output: ParsedT | null;
29+
};
30+
31+
export type ParsedBetaContentBlock<ParsedT> =
32+
| (BetaTextBlock & { parsed: ParsedT | null })
33+
| Exclude<BetaContentBlock, BetaTextBlock>;
34+
35+
export function maybeParseBetaMessage<Params extends BetaParseableMessageCreateParams | null>(
36+
message: BetaMessage,
37+
params: Params,
38+
): ParsedBetaMessage<ExtractParsedContentFromBetaParams<NonNullable<Params>>> {
39+
if (!params || !('parse' in (params.output_format ?? {}))) {
40+
return {
41+
...message,
42+
content: message.content.map((block) => {
43+
if (block.type === 'text') {
44+
return {
45+
...block,
46+
parsed: null,
47+
};
48+
}
49+
return block;
50+
}),
51+
parsed_output: null,
52+
} as ParsedBetaMessage<ExtractParsedContentFromBetaParams<NonNullable<Params>>>;
53+
}
54+
55+
return parseBetaMessage(message, params);
56+
}
57+
58+
export function parseBetaMessage<Params extends BetaParseableMessageCreateParams>(
59+
message: BetaMessage,
60+
params: Params,
61+
): ParsedBetaMessage<ExtractParsedContentFromBetaParams<Params>> {
62+
let firstParsed: ReturnType<typeof parseBetaOutputFormat<Params>> | null = null;
63+
64+
const content: Array<ParsedBetaContentBlock<ExtractParsedContentFromBetaParams<Params>>> =
65+
message.content.map((block) => {
66+
if (block.type === 'text') {
67+
const parsed = parseBetaOutputFormat(params, block.text);
68+
69+
if (firstParsed === null) {
70+
firstParsed = parsed;
71+
}
72+
73+
return {
74+
...block,
75+
parsed,
76+
};
77+
}
78+
return block;
79+
});
80+
81+
return {
82+
...message,
83+
content,
84+
parsed_output: firstParsed,
85+
} as ParsedBetaMessage<ExtractParsedContentFromBetaParams<Params>>;
86+
}
87+
88+
function parseBetaOutputFormat<Params extends BetaParseableMessageCreateParams>(
89+
params: Params,
90+
content: string,
91+
): ExtractParsedContentFromBetaParams<Params> | null {
92+
if (params.output_format?.type !== 'json_schema') {
93+
return null;
94+
}
95+
96+
try {
97+
if ('parse' in params.output_format) {
98+
return params.output_format.parse(content);
99+
}
100+
101+
return JSON.parse(content);
102+
} catch (error) {
103+
throw new OpenAIError(`Failed to parse structured output: ${error}`);
104+
}
105+
}

src/lib/beta/BetaRunnableTool.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import type { ChatCompletionTool } from '../../resources';
2+
3+
export type Promisable<T> = T | Promise<T>;
4+
5+
// this type is just an extension of BetaTool with a run and parse method
6+
// that will be called by `toolRunner()` helpers
7+
export type BetaRunnableTool<Input = any> = ChatCompletionTool & {
8+
run: (args: Input) => Promisable<string | Array<BetaToolResultContentBlockParam>>;
9+
parse: (content: unknown) => Input;
10+
};

0 commit comments

Comments
 (0)