Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: convert tool call schemas between providers #5206

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions app/src/pages/playground/ChatMessageToolCallsEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,14 @@ import { JSONSchema7 } from "json-schema";

import { JSONEditor } from "@phoenix/components/code";
import { usePlaygroundContext } from "@phoenix/contexts/PlaygroundContext";
import {
openAIToolCallsJSONSchema,
openAIToolCallsSchema,
} from "@phoenix/schemas";
import { toolCallSchemas } from "@phoenix/schemas";
import { ChatMessage } from "@phoenix/store";
import { safelyParseJSON } from "@phoenix/utils/jsonUtils";

import { PlaygroundInstanceProps } from "./types";

const { openAIToolCallsSchema, openAIToolCallsJSONSchema } = toolCallSchemas;

/**
* Editor for message tool calls
*/
Expand Down
2 changes: 1 addition & 1 deletion app/src/pages/playground/PlaygroundOutput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { useNotifyError } from "@phoenix/contexts";
import { useCredentialsContext } from "@phoenix/contexts/CredentialsContext";
import { usePlaygroundContext } from "@phoenix/contexts/PlaygroundContext";
import { useChatMessageStyles } from "@phoenix/hooks/useChatMessageStyles";
import { OpenAIToolCall } from "@phoenix/schemas";
import { OpenAIToolCall } from "@phoenix/schemas/toolCallSchemas";
import { ChatMessage, generateMessageId } from "@phoenix/store";
import { assertUnreachable } from "@phoenix/typeUtils";

Expand Down
9 changes: 6 additions & 3 deletions app/src/pages/playground/PlaygroundTool.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ import { JSONEditor } from "@phoenix/components/code";
import { LazyEditorWrapper } from "@phoenix/components/code/LazyEditorWrapper";
import { SpanKindIcon } from "@phoenix/components/trace";
import { usePlaygroundContext } from "@phoenix/contexts/PlaygroundContext";
import { openAIToolJSONSchema, openAIToolSchema } from "@phoenix/schemas";
import {
openAIToolDefinitionJSONSchema,
openAIToolDefinitionSchema,
} from "@phoenix/schemas";
import { OpenAITool } from "@phoenix/store";
import { safelyParseJSON } from "@phoenix/utils/jsonUtils";

Expand Down Expand Up @@ -45,7 +48,7 @@ export function PlaygroundTool({
// there is no "deepPassthrough" to allow for extra keys
// at all levels of the schema, so we just use the json parsed value here,
// knowing that it is valid with potentially extra keys
const { success } = openAIToolSchema.safeParse(definition);
const { success } = openAIToolDefinitionSchema.safeParse(definition);
if (!success) {
return;
}
Expand Down Expand Up @@ -106,7 +109,7 @@ export function PlaygroundTool({
<JSONEditor
value={toolDefinition}
onChange={onChange}
jsonSchema={openAIToolJSONSchema as JSONSchema7}
jsonSchema={openAIToolDefinitionJSONSchema as JSONSchema7}
/>
</LazyEditorWrapper>
</Card>
Expand Down
5 changes: 3 additions & 2 deletions app/src/pages/playground/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import {
ToolAttributePostfixes,
} from "@arizeai/openinference-semantic-conventions";

import { openAIToolCallSchema, openAIToolSchema } from "@phoenix/schemas";
import { openAIToolDefinitionSchema } from "@phoenix/schemas";
import { openAIToolCallSchema } from "@phoenix/schemas/toolCallSchemas";
import { ChatMessage } from "@phoenix/store";
import { isObject, Mutable, schemaForType } from "@phoenix/typeUtils";
import { safelyParseJSON } from "@phoenix/utils/jsonUtils";
Expand Down Expand Up @@ -220,7 +221,7 @@ export const toolJSONSchemaSchema = z
})
// TODO(parker / apowell) - adjust this transformation with anthropic tool support https://github.com/Arize-ai/phoenix/issues/5100
.transform((o, ctx) => {
const { data, success } = openAIToolSchema.safeParse(o);
const { data, success } = openAIToolDefinitionSchema.safeParse(o);

if (!success) {
ctx.addIssue({
Expand Down
144 changes: 144 additions & 0 deletions app/src/pages/playground/toolCallSchemas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import { z } from "zod";

import { assertUnreachable } from "@phoenix/typeUtils";

export const jsonSchema = z
.object({
type: z.string(),
Parker-Stafford marked this conversation as resolved.
Show resolved Hide resolved
// content changes based on the type
// see https://json-schema.org/understanding-json-schema/reference/type
})
.passthrough();

/**
* --------------------------------
* Provider Schemas
* --------------------------------
*/

/**
* OpenAI tool call format
*/
export const openAIToolCallSchema = z.object({
id: z.string().nullish(),
type: z.literal("function"),
function: z.object({
name: z.string(),
description: z.string().nullish(),
Parker-Stafford marked this conversation as resolved.
Show resolved Hide resolved
parameters: jsonSchema,
}),
});

export type OpenAIToolCall = z.infer<typeof openAIToolCallSchema>;

/**
* Anthropic tool call format
*/
export const anthropicToolCallSchema = z.object({
name: z.string(),
description: z.string(),
input_schema: jsonSchema,
});

export type AnthropicToolCall = z.infer<typeof anthropicToolCallSchema>;

/**
* --------------------------------
* Conversion Schemas
* --------------------------------
*/

/**
* Parse incoming object as an Anthropic tool call and immediately convert to OpenAI format
*/
export const anthropicToOpenAI = anthropicToolCallSchema.transform(
Parker-Stafford marked this conversation as resolved.
Show resolved Hide resolved
(anthropic): OpenAIToolCall => ({
id: null, // generate?
Parker-Stafford marked this conversation as resolved.
Show resolved Hide resolved
type: "function",
function: {
name: anthropic.name,
description: anthropic.description,
parameters: anthropic.input_schema,
},
})
);

/**
* Parse incoming object as an OpenAI tool call and immediately convert to Anthropic format
*/
export const openAIToAnthropic = openAIToolCallSchema.transform(
(openai): AnthropicToolCall => ({
name: openai.function.name,
description: openai.function.description ?? openai.function.name,
input_schema: openai.function.parameters,
})
);

/**
* --------------------------------
* Conversion Helpers
* --------------------------------
*/

// Helper type for provider identification
export const providerType = z.enum(["openai", "anthropic"]);
export type ProviderType = z.infer<typeof providerType>;

/**
* Union of all tool call formats
*
* This is useful for functions that need to accept any tool call format
*/
export const anyToolCallSchema = z.union([
Parker-Stafford marked this conversation as resolved.
Show resolved Hide resolved
openAIToolCallSchema,
anthropicToolCallSchema,
]);

export type AnyToolCall = z.infer<typeof anyToolCallSchema>;

/**
* Convert from any tool call format to OpenAI format
*/
export const toOpenAIFormat = (
Parker-Stafford marked this conversation as resolved.
Show resolved Hide resolved
toolCall: AnyToolCall,
provider: ProviderType
): OpenAIToolCall => {
switch (provider) {
case "openai":
return toolCall as OpenAIToolCall;
case "anthropic":
return anthropicToOpenAI.parse(toolCall as AnthropicToolCall);
default:
assertUnreachable(provider);
}
Parker-Stafford marked this conversation as resolved.
Show resolved Hide resolved
};

/**
* Convert from OpenAI tool call format to any other format
*/
export const fromOpenAIFormat = (
toolCall: OpenAIToolCall,
targetProvider: ProviderType
): AnyToolCall => {
switch (targetProvider) {
case "openai":
return toolCall;
case "anthropic":
return openAIToAnthropic.parse(toolCall);
default:
assertUnreachable(targetProvider);
}
};

/**
* Detect the provider of a tool call object
*/
export const detectProvider = (toolCall: unknown): ProviderType => {
Parker-Stafford marked this conversation as resolved.
Show resolved Hide resolved
if (openAIToolCallSchema.safeParse(toolCall).success) {
return "openai";
}
if (anthropicToolCallSchema.safeParse(toolCall).success) {
return "anthropic";
}
throw new Error("Unknown tool call format");
};
1 change: 1 addition & 0 deletions app/src/schemas/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from "./toolSchemas";
export * as toolCallSchemas from "./toolCallSchemas";
Loading
Loading