kopia lustrzana https://github.com/transitive-bullshit/chatgpt-api
Merge pull request #693 from transitive-bullshit/feature/update-march-2025
Add support for OpenAI Responses tools formatold-agentic
commit
eaad2109d4
|
@ -11,8 +11,6 @@ jobs:
|
||||||
matrix:
|
matrix:
|
||||||
node-version:
|
node-version:
|
||||||
- 18
|
- 18
|
||||||
- 20
|
|
||||||
- 21
|
|
||||||
- 22
|
- 22
|
||||||
- 23
|
- 23
|
||||||
|
|
||||||
|
@ -23,7 +21,7 @@ jobs:
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@v4
|
uses: pnpm/action-setup@v4
|
||||||
with:
|
with:
|
||||||
version: 10.5.2
|
version: 10.6.3
|
||||||
run_install: false
|
run_install: false
|
||||||
|
|
||||||
- name: Install Node.js
|
- name: Install Node.js
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
import 'dotenv/config'
|
||||||
|
|
||||||
|
import { createAISDKTools } from '@agentic/ai-sdk'
|
||||||
|
import { WeatherClient } from '@agentic/weather'
|
||||||
|
import { createOpenAI } from '@ai-sdk/openai'
|
||||||
|
import { generateText } from 'ai'
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const weather = new WeatherClient()
|
||||||
|
const openai = createOpenAI({ compatibility: 'strict' })
|
||||||
|
|
||||||
|
const result = await generateText({
|
||||||
|
model: openai('gpt-4o-mini'),
|
||||||
|
tools: createAISDKTools(weather),
|
||||||
|
experimental_activeTools: Array.from(weather.functions).map(
|
||||||
|
(fn) => fn.spec.name
|
||||||
|
),
|
||||||
|
toolChoice: 'required',
|
||||||
|
temperature: 0,
|
||||||
|
system: 'You are a helpful assistant. Be as concise as possible.',
|
||||||
|
prompt: 'What is the weather in San Francisco?'
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log(result.toolResults[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
await main()
|
|
@ -10,9 +10,9 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@agentic/ai-sdk": "workspace:*",
|
"@agentic/ai-sdk": "workspace:*",
|
||||||
"@agentic/weather": "workspace:*",
|
"@agentic/weather": "workspace:*",
|
||||||
"@ai-sdk/openai": "^1.1.13",
|
"@ai-sdk/openai": "^1.2.5",
|
||||||
"ai": "^4.1.42",
|
"ai": "^4.1.61",
|
||||||
"openai": "^4.85.2",
|
"openai": "^4.87.3",
|
||||||
"zod": "^3.24.2"
|
"zod": "^3.24.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
@ -0,0 +1,62 @@
|
||||||
|
import 'dotenv/config'
|
||||||
|
|
||||||
|
import type { ResponseInput } from 'openai/resources/responses/responses.mjs'
|
||||||
|
import { assert } from '@agentic/core'
|
||||||
|
import { WeatherClient } from '@agentic/stdlib'
|
||||||
|
import OpenAI from 'openai'
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const weather = new WeatherClient()
|
||||||
|
const openai = new OpenAI()
|
||||||
|
|
||||||
|
const messages: ResponseInput = [
|
||||||
|
{
|
||||||
|
role: 'system',
|
||||||
|
content: 'You are a helpful assistant. Be as concise as possible.'
|
||||||
|
},
|
||||||
|
{ role: 'user', content: 'What is the weather in San Francisco?' }
|
||||||
|
]
|
||||||
|
|
||||||
|
{
|
||||||
|
// First call to OpenAI to invoke the weather tool
|
||||||
|
const res = await openai.responses.create({
|
||||||
|
model: 'gpt-4o-mini',
|
||||||
|
temperature: 0,
|
||||||
|
tools: weather.functions.responsesToolSpecs,
|
||||||
|
tool_choice: 'required',
|
||||||
|
input: messages
|
||||||
|
})
|
||||||
|
|
||||||
|
const message = res.output[0]
|
||||||
|
console.log(JSON.stringify(message, null, 2))
|
||||||
|
assert(message?.type === 'function_call')
|
||||||
|
assert(message.name === 'get_current_weather')
|
||||||
|
|
||||||
|
const fn = weather.functions.get('get_current_weather')!
|
||||||
|
assert(fn)
|
||||||
|
const toolResult = await fn(message.arguments)
|
||||||
|
|
||||||
|
messages.push(message)
|
||||||
|
messages.push({
|
||||||
|
type: 'function_call_output',
|
||||||
|
call_id: message.call_id,
|
||||||
|
output: JSON.stringify(toolResult)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log()
|
||||||
|
|
||||||
|
{
|
||||||
|
// Second call to OpenAI to generate a text response
|
||||||
|
const res = await openai.responses.create({
|
||||||
|
model: 'gpt-4o-mini',
|
||||||
|
temperature: 0,
|
||||||
|
tools: weather.functions.responsesToolSpecs,
|
||||||
|
input: messages
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log(res.output_text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await main()
|
|
@ -43,6 +43,8 @@ async function main() {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log()
|
||||||
|
|
||||||
{
|
{
|
||||||
// Second call to OpenAI to generate a text response
|
// Second call to OpenAI to generate a text response
|
||||||
const res = await openai.chat.completions.create({
|
const res = await openai.chat.completions.create({
|
||||||
|
@ -52,7 +54,7 @@ async function main() {
|
||||||
tools: weather.functions.toolSpecs
|
tools: weather.functions.toolSpecs
|
||||||
})
|
})
|
||||||
const message = res.choices?.[0]?.message
|
const message = res.choices?.[0]?.message
|
||||||
console.log(JSON.stringify(message, null, 2))
|
console.log(message?.content)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@agentic/core": "workspace:*",
|
"@agentic/core": "workspace:*",
|
||||||
"@agentic/stdlib": "workspace:*",
|
"@agentic/stdlib": "workspace:*",
|
||||||
"openai": "^4.85.2",
|
"openai": "^4.87.3",
|
||||||
"zod": "^3.24.2"
|
"zod": "^3.24.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/transitive-bullshit/agentic.git"
|
"url": "git+https://github.com/transitive-bullshit/agentic.git"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.5.2",
|
"packageManager": "pnpm@10.6.3",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
},
|
},
|
||||||
|
@ -37,21 +37,21 @@
|
||||||
"@changesets/cli": "^2.28.1",
|
"@changesets/cli": "^2.28.1",
|
||||||
"@fisch0920/eslint-config": "^1.4.0",
|
"@fisch0920/eslint-config": "^1.4.0",
|
||||||
"@total-typescript/ts-reset": "^0.6.1",
|
"@total-typescript/ts-reset": "^0.6.1",
|
||||||
"@types/node": "^22.13.8",
|
"@types/node": "^22.13.10",
|
||||||
"del-cli": "^6.0.0",
|
"del-cli": "^6.0.0",
|
||||||
"dotenv": "^16.4.7",
|
"dotenv": "^16.4.7",
|
||||||
"eslint": "^8.57.1",
|
"eslint": "^8.57.1",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"lint-staged": "^15.4.3",
|
"lint-staged": "^15.5.0",
|
||||||
"npm-run-all2": "^7.0.2",
|
"npm-run-all2": "^7.0.2",
|
||||||
"only-allow": "^1.2.1",
|
"only-allow": "^1.2.1",
|
||||||
"prettier": "^3.5.2",
|
"prettier": "^3.5.3",
|
||||||
"syncpack": "14.0.0-alpha.10",
|
"syncpack": "14.0.0-alpha.10",
|
||||||
"tsup": "^8.4.0",
|
"tsup": "^8.4.0",
|
||||||
"tsx": "^4.19.3",
|
"tsx": "^4.19.3",
|
||||||
"turbo": "^2.4.4",
|
"turbo": "^2.4.4",
|
||||||
"typescript": "^5.8.2",
|
"typescript": "^5.8.2",
|
||||||
"vitest": "3.0.7",
|
"vitest": "3.0.8",
|
||||||
"zod": "^3.24.2",
|
"zod": "^3.24.2",
|
||||||
"zoominfo-api-auth-client": "^1.0.1"
|
"zoominfo-api-auth-client": "^1.0.1"
|
||||||
},
|
},
|
||||||
|
|
|
@ -39,7 +39,7 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@agentic/tsconfig": "workspace:*",
|
"@agentic/tsconfig": "workspace:*",
|
||||||
"ai": "^4.1.47"
|
"ai": "^4.1.61"
|
||||||
},
|
},
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public"
|
"access": "public"
|
||||||
|
|
|
@ -2,6 +2,10 @@ import type * as types from './types.ts'
|
||||||
import { AIFunctionsProvider } from './fns'
|
import { AIFunctionsProvider } from './fns'
|
||||||
import { isAIFunction } from './utils'
|
import { isAIFunction } from './utils'
|
||||||
|
|
||||||
|
export type AIFunctionSetOptions = {
|
||||||
|
transformNameKeysFn?: (name: string) => string
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A set of AI functions intended to make it easier to work with large sets of
|
* A set of AI functions intended to make it easier to work with large sets of
|
||||||
* AI functions across different clients.
|
* AI functions across different clients.
|
||||||
|
@ -14,8 +18,14 @@ import { isAIFunction } from './utils'
|
||||||
*/
|
*/
|
||||||
export class AIFunctionSet implements Iterable<types.AIFunction> {
|
export class AIFunctionSet implements Iterable<types.AIFunction> {
|
||||||
protected readonly _map: Map<string, types.AIFunction>
|
protected readonly _map: Map<string, types.AIFunction>
|
||||||
|
protected readonly _transformNameKeysFn: (name: string) => string
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
aiFunctionLikeObjects?: types.AIFunctionLike[],
|
||||||
|
{ transformNameKeysFn = transformName }: AIFunctionSetOptions = {}
|
||||||
|
) {
|
||||||
|
this._transformNameKeysFn = transformNameKeysFn
|
||||||
|
|
||||||
constructor(aiFunctionLikeObjects?: types.AIFunctionLike[]) {
|
|
||||||
// TODO: these `instanceof` checks seem to be failing on some platforms,
|
// TODO: these `instanceof` checks seem to be failing on some platforms,
|
||||||
// so for now we're using an uglier, but more reliable approach to parsing
|
// so for now we're using an uglier, but more reliable approach to parsing
|
||||||
// the AIFunctionLike objects.
|
// the AIFunctionLike objects.
|
||||||
|
@ -64,7 +74,9 @@ export class AIFunctionSet implements Iterable<types.AIFunction> {
|
||||||
}
|
}
|
||||||
|
|
||||||
this._map = new Map(
|
this._map = new Map(
|
||||||
fns ? fns.map((fn) => [transformName(fn.spec.name), fn]) : null
|
fns
|
||||||
|
? fns.map((fn) => [this._transformNameKeysFn(fn.spec.name), fn])
|
||||||
|
: null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -73,21 +85,21 @@ export class AIFunctionSet implements Iterable<types.AIFunction> {
|
||||||
}
|
}
|
||||||
|
|
||||||
add(fn: types.AIFunction): this {
|
add(fn: types.AIFunction): this {
|
||||||
this._map.set(transformName(fn.spec.name), fn)
|
this._map.set(this._transformNameKeysFn(fn.spec.name), fn)
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
get(name: string): types.AIFunction | undefined {
|
get(name: string): types.AIFunction | undefined {
|
||||||
return this._map.get(transformName(name))
|
return this._map.get(this._transformNameKeysFn(name))
|
||||||
}
|
}
|
||||||
|
|
||||||
set(name: string, fn: types.AIFunction): this {
|
set(name: string, fn: types.AIFunction): this {
|
||||||
this._map.set(transformName(name), fn)
|
this._map.set(this._transformNameKeysFn(name), fn)
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
has(name: string): boolean {
|
has(name: string): boolean {
|
||||||
return this._map.has(transformName(name))
|
return this._map.has(this._transformNameKeysFn(name))
|
||||||
}
|
}
|
||||||
|
|
||||||
clear(): void {
|
clear(): void {
|
||||||
|
@ -95,23 +107,23 @@ export class AIFunctionSet implements Iterable<types.AIFunction> {
|
||||||
}
|
}
|
||||||
|
|
||||||
delete(name: string): boolean {
|
delete(name: string): boolean {
|
||||||
return this._map.delete(transformName(name))
|
return this._map.delete(this._transformNameKeysFn(name))
|
||||||
}
|
}
|
||||||
|
|
||||||
pick(...keys: string[]): AIFunctionSet {
|
pick(...keys: string[]): AIFunctionSet {
|
||||||
const keysToIncludeSet = new Set(keys.map(transformName))
|
const keysToIncludeSet = new Set(keys.map(this._transformNameKeysFn))
|
||||||
return new AIFunctionSet(
|
return new AIFunctionSet(
|
||||||
Array.from(this).filter((fn) =>
|
Array.from(this).filter((fn) =>
|
||||||
keysToIncludeSet.has(transformName(fn.spec.name))
|
keysToIncludeSet.has(this._transformNameKeysFn(fn.spec.name))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
omit(...keys: string[]): AIFunctionSet {
|
omit(...keys: string[]): AIFunctionSet {
|
||||||
const keysToExcludeSet = new Set(keys.map(transformName))
|
const keysToExcludeSet = new Set(keys.map(this._transformNameKeysFn))
|
||||||
return new AIFunctionSet(
|
return new AIFunctionSet(
|
||||||
Array.from(this).filter(
|
Array.from(this).filter(
|
||||||
(fn) => !keysToExcludeSet.has(transformName(fn.spec.name))
|
(fn) => !keysToExcludeSet.has(this._transformNameKeysFn(fn.spec.name))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -120,10 +132,18 @@ export class AIFunctionSet implements Iterable<types.AIFunction> {
|
||||||
return [...this.entries].map(fn)
|
return [...this.entries].map(fn)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the functions in this set as an array compatible with OpenAI's
|
||||||
|
* chat completions `functions`.
|
||||||
|
*/
|
||||||
get specs(): types.AIFunctionSpec[] {
|
get specs(): types.AIFunctionSpec[] {
|
||||||
return this.map((fn) => fn.spec)
|
return this.map((fn) => fn.spec)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the functions in this set as an array compatible with OpenAI's
|
||||||
|
* chat completions `tools`.
|
||||||
|
*/
|
||||||
get toolSpecs(): types.AIToolSpec[] {
|
get toolSpecs(): types.AIToolSpec[] {
|
||||||
return this.map((fn) => ({
|
return this.map((fn) => ({
|
||||||
type: 'function' as const,
|
type: 'function' as const,
|
||||||
|
@ -131,6 +151,17 @@ export class AIFunctionSet implements Iterable<types.AIFunction> {
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the tools in this set compatible with OpenAI's `responses` API.
|
||||||
|
*
|
||||||
|
* Note that this is currently the same type as `AIFunctionSet.specs`, but
|
||||||
|
* they are separate APIs which may diverge over time, so if you're using the
|
||||||
|
* OpenAI `responses` API, you should reference this property.
|
||||||
|
*/
|
||||||
|
get responsesToolSpecs(): types.AIFunctionSpec[] {
|
||||||
|
return this.specs
|
||||||
|
}
|
||||||
|
|
||||||
get entries(): IterableIterator<types.AIFunction> {
|
get entries(): IterableIterator<types.AIFunction> {
|
||||||
return this._map.values()
|
return this._map.values()
|
||||||
}
|
}
|
||||||
|
|
|
@ -64,6 +64,12 @@ export function createAIFunction<InputSchema extends z.ZodObject<any>, Output>(
|
||||||
return implementation(parsedInput)
|
return implementation(parsedInput)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Override the default function name with the intended name.
|
||||||
|
Object.defineProperty(aiFunction, 'name', {
|
||||||
|
value: spec.name,
|
||||||
|
writable: false
|
||||||
|
})
|
||||||
|
|
||||||
const strict = !!spec.strict
|
const strict = !!spec.strict
|
||||||
|
|
||||||
aiFunction.inputSchema = spec.inputSchema
|
aiFunction.inputSchema = spec.inputSchema
|
||||||
|
@ -72,6 +78,7 @@ export function createAIFunction<InputSchema extends z.ZodObject<any>, Output>(
|
||||||
name: spec.name,
|
name: spec.name,
|
||||||
description: spec.description?.trim() ?? '',
|
description: spec.description?.trim() ?? '',
|
||||||
parameters: zodToJsonSchema(spec.inputSchema, { strict }),
|
parameters: zodToJsonSchema(spec.inputSchema, { strict }),
|
||||||
|
type: 'function',
|
||||||
strict
|
strict
|
||||||
}
|
}
|
||||||
aiFunction.impl = implementation
|
aiFunction.impl = implementation
|
||||||
|
|
|
@ -34,12 +34,17 @@ export interface AIFunctionSpec {
|
||||||
/** JSON schema spec of the function's input parameters */
|
/** JSON schema spec of the function's input parameters */
|
||||||
parameters: JSONSchema
|
parameters: JSONSchema
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of the function tool. Always `function`.
|
||||||
|
*/
|
||||||
|
type: 'function'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether to enable strict schema adherence when generating the function
|
* Whether to enable strict schema adherence when generating the function
|
||||||
* parameters. Currently only supported by OpenAI's
|
* parameters. Currently only supported by OpenAI's
|
||||||
* [Structured Outputs](https://platform.openai.com/docs/guides/structured-outputs).
|
* [Structured Outputs](https://platform.openai.com/docs/guides/structured-outputs).
|
||||||
*/
|
*/
|
||||||
strict?: boolean
|
strict: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AIToolSpec {
|
export interface AIToolSpec {
|
||||||
|
@ -91,6 +96,7 @@ export interface AIFunction<
|
||||||
// TODO: this `any` shouldn't be necessary, but it is for `createAIFunction` results to be assignable to `AIFunctionLike`
|
// TODO: this `any` shouldn't be necessary, but it is for `createAIFunction` results to be assignable to `AIFunctionLike`
|
||||||
impl: (params: z.infer<InputSchema> | any) => MaybePromise<Output>
|
impl: (params: z.infer<InputSchema> | any) => MaybePromise<Output>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SafeParseResult<TData> =
|
export type SafeParseResult<TData> =
|
||||||
| {
|
| {
|
||||||
success: true
|
success: true
|
||||||
|
|
|
@ -37,8 +37,8 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@agentic/core": "workspace:*",
|
"@agentic/core": "workspace:*",
|
||||||
"@agentic/tsconfig": "workspace:*",
|
"@agentic/tsconfig": "workspace:*",
|
||||||
"@ai-sdk/openai": "^1.1.13",
|
"@ai-sdk/openai": "^1.2.5",
|
||||||
"ai": "^4.1.42"
|
"ai": "^4.1.61"
|
||||||
},
|
},
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public"
|
"access": "public"
|
||||||
|
|
|
@ -55,6 +55,7 @@ export namespace jina {
|
||||||
withGeneratedAlt: z.boolean().optional(),
|
withGeneratedAlt: z.boolean().optional(),
|
||||||
withLinksSummary: z.boolean().optional(),
|
withLinksSummary: z.boolean().optional(),
|
||||||
withImagesSummary: z.boolean().optional(),
|
withImagesSummary: z.boolean().optional(),
|
||||||
|
withFavicon: z.boolean().optional(),
|
||||||
setCookie: z.string().optional(),
|
setCookie: z.string().optional(),
|
||||||
proxyUrl: z.string().optional(),
|
proxyUrl: z.string().optional(),
|
||||||
noCache: z.boolean().optional(),
|
noCache: z.boolean().optional(),
|
||||||
|
@ -95,6 +96,7 @@ export namespace jina {
|
||||||
content: string
|
content: string
|
||||||
description?: string
|
description?: string
|
||||||
publishedTime?: string
|
publishedTime?: string
|
||||||
|
favicon?: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -249,6 +251,7 @@ export class JinaClient extends AIFunctionsProvider {
|
||||||
withGeneratedAlt: 'x-with-generated-alt',
|
withGeneratedAlt: 'x-with-generated-alt',
|
||||||
withLinksSummary: 'x-with-links-summary',
|
withLinksSummary: 'x-with-links-summary',
|
||||||
withImagesSummary: 'x-with-images-summary',
|
withImagesSummary: 'x-with-images-summary',
|
||||||
|
withFavicon: 'x-with-favicon',
|
||||||
setCookie: 'x-set-cookie',
|
setCookie: 'x-set-cookie',
|
||||||
proxyUrl: 'x-proxy-url',
|
proxyUrl: 'x-proxy-url',
|
||||||
noCache: 'x-no-cache',
|
noCache: 'x-no-cache',
|
||||||
|
|
Plik diff jest za duży
Load Diff
Ładowanie…
Reference in New Issue