Skip to content

Commit

Permalink
feat(playground): add tools ui
Browse files Browse the repository at this point in the history
  • Loading branch information
Parker-Stafford committed Oct 14, 2024
1 parent b15f2a4 commit 2d394a3
Show file tree
Hide file tree
Showing 13 changed files with 730 additions and 19 deletions.
2 changes: 2 additions & 0 deletions app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"@uiw/codemirror-theme-github": "^4.23.5",
"@uiw/codemirror-theme-nord": "^4.23.0",
"@uiw/react-codemirror": "^4.23.0",
"codemirror-json-schema": "^0.7.8",
"copy-to-clipboard": "^3.3.3",
"d3-format": "^3.1.0",
"d3-scale-chromatic": "^3.1.0",
Expand Down Expand Up @@ -55,6 +56,7 @@
"use-deep-compare-effect": "^1.8.1",
"use-zustand": "^0.0.4",
"zod": "^3.23.8",
"zod-to-json-schema": "^3.23.3",
"zustand": "^4.5.4"
},
"devDependencies": {
Expand Down
449 changes: 449 additions & 0 deletions app/pnpm-lock.yaml

Large diffs are not rendered by default.

45 changes: 45 additions & 0 deletions app/src/components/code/JSONToolEditor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import React from "react";
import { json, jsonLanguage, jsonParseLinter } from "@codemirror/lang-json";
import { linter } from "@codemirror/lint";
import { EditorView, hoverTooltip } from "@codemirror/view";
import { nord } from "@uiw/codemirror-theme-nord";
import CodeMirror, { ReactCodeMirrorProps } from "@uiw/react-codemirror";
import {
handleRefresh,
jsonCompletion,
jsonSchemaHover,
jsonSchemaLinter,
stateExtensions,
} from "codemirror-json-schema";

import { useTheme } from "@phoenix/contexts";
import { toolJSONSchema } from "@phoenix/schemas/toolSchema";

export type JSONToolEditorProps = Omit<
ReactCodeMirrorProps,
"theme" | "extensions" | "editable"
>;

export function JSONToolEditor(props: JSONToolEditorProps) {
const { theme } = useTheme();
const codeMirrorTheme = theme === "light" ? undefined : nord;
return (
<CodeMirror
value={props.value}
extensions={[
json(),
EditorView.lineWrapping,
linter(jsonParseLinter()),
linter(jsonSchemaLinter(), { needsRefresh: handleRefresh }),
jsonLanguage.data.of({
autocomplete: jsonCompletion(),
}),
hoverTooltip(jsonSchemaHover()),
stateExtensions(toolJSONSchema as JSONSchema7),
]}
editable
theme={codeMirrorTheme}
{...props}
/>
);
}
1 change: 1 addition & 0 deletions app/src/components/code/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ export * from "./CodeWrap";
export * from "./TypeScriptBlock";
export * from "./CodeEditorFieldWrapper";
export * from "./CodeLanguageRadioGroup";
export * from "./JSONToolEditor";
22 changes: 21 additions & 1 deletion app/src/pages/playground/PlaygroundChatTemplate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { usePlaygroundContext } from "@phoenix/contexts/PlaygroundContext";
import { useChatMessageStyles } from "@phoenix/hooks/useChatMessageStyles";
import {
ChatMessage,
createTool,
generateMessageId,
PlaygroundChatTemplate as PlaygroundChatTemplateType,
} from "@phoenix/store";
Expand Down Expand Up @@ -123,7 +124,26 @@ export function PlaygroundChatTemplate(props: PlaygroundChatTemplateProps) {
borderTopColor="dark"
borderTopWidth="thin"
>
<Flex direction="row" justifyContent="end">
<Flex direction="row" justifyContent="end" gap="size-50">
<Button
variant="default"
aria-label="add tool"
size="compact"
icon={<Icon svg={<Icons.PlusOutline />} />}
onClick={() => {
updateInstance({
instanceId: id,
patch: {
tools: [
...playgroundInstance.tools,
createTool(playgroundInstance.tools.length + 1),
],
},
});
}}
>
Tool
</Button>
<Button
variant="default"
aria-label="add message"
Expand Down
17 changes: 16 additions & 1 deletion app/src/pages/playground/PlaygroundInstance.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,27 @@ import { PlaygroundInstanceProps } from "./types";
export function PlaygroundInstance(props: PlaygroundInstanceProps) {
const numInstances = usePlaygroundContext((state) => state.instances.length);
const isSingleInstance = numInstances == 1;

const instance = usePlaygroundContext((state) =>
state.instances.find(
(instance) => instance.id === props.playgroundInstanceId
)
);

if (instance == null) {
throw new Error(
`Playground instance ${props.playgroundInstanceId} not found`
);
}

const hasTools = instance.tools.length > 0;

return (
<PanelGroup direction={isSingleInstance ? "horizontal" : "vertical"}>
<Panel defaultSize={50} order={1}>
<PanelContent>
<PlaygroundTemplate {...props} />
<PlaygroundTools {...props} />
{hasTools && <PlaygroundTools {...props} />}
</PanelContent>
</Panel>
<PanelResizeHandle
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 @@ -41,7 +41,7 @@ export function PlaygroundOutput(props: PlaygroundOutputProps) {
state.instances.findIndex((instance) => instance.id === instanceId)
);
if (!instance) {
throw new Error("Playground instance not found");
throw new Error(`Playground instance ${instanceId} not found`);
}

const runId = instance.activeRunId;
Expand Down
67 changes: 63 additions & 4 deletions app/src/pages/playground/PlaygroundTools.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,85 @@
import React from "react";

import { Card } from "@arizeai/components";
import { Button, Card, Flex, Icon, Icons } from "@arizeai/components";

import { JSONToolEditor } from "@phoenix/components/code";
import { usePlaygroundContext } from "@phoenix/contexts/PlaygroundContext";

import { TitleWithAlphabeticIndex } from "./TitleWithAlphabeticIndex";
import { PlaygroundInstanceProps } from "./types";

interface PlaygroundToolsProps extends PlaygroundInstanceProps {}

export function PlaygroundTools(props: PlaygroundToolsProps) {
const index = usePlaygroundContext((state) =>
state.instances.findIndex(
const instanceId = props.playgroundInstanceId;
const instance = usePlaygroundContext((state) =>
state.instances.find(
(instance) => instance.id === props.playgroundInstanceId
)
);
if (instance == null) {
throw new Error(`Playground instance ${instanceId} not found`);
}
const updateInstance = usePlaygroundContext((state) => state.updateInstance);
if (instance.tools == null) {
throw new Error(`Playground instance ${instanceId} does not have tools`);
}
const index = usePlaygroundContext((state) =>
state.instances.findIndex((instance) => instance.id === instanceId)
);

return (
<Card
title={<TitleWithAlphabeticIndex index={index} title="Tools" />}
collapsible
variant="compact"
>
Tools go here
<Flex direction="column" gap="size-50">
{instance.tools.map((tool) => (
<Card
collapsible
variant="compact"
key={tool.id}
title={tool.definition.function.name}
bodyStyle={{ padding: 0 }}
extra={
<Button
aria-label="Delete tool"
icon={<Icon svg={<Icons.TrashOutline />} />}
variant="default"
size="compact"
onClick={() => {
updateInstance({
instanceId,
patch: {
tools: instance.tools.filter((t) => t.id !== tool.id),
},
});
}}
/>
}
>
<JSONToolEditor
value={JSON.stringify(tool.definition, null, 2)}
onChange={(value) => {
updateInstance({
instanceId,
patch: {
tools: instance.tools.map((t) =>
t.id === tool.id
? {
...t,
definition: JSON.parse(value),
}
: t
),
},
});
}}
/>
</Card>
))}
</Flex>
</Card>
);
}
2 changes: 1 addition & 1 deletion app/src/pages/playground/__tests__/playgroundUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ const expectedPlaygroundInstanceWithIO: PlaygroundInstance = {
provider: "OPENAI",
modelName: "gpt-4o",
},
tools: {},
tools: [],
template: {
__type: "chat",
// These id's are not 0, 1, 2, because we create a playground instance (including messages) at the top of the transformSpanAttributesToPlaygroundInstance function
Expand Down
1 change: 1 addition & 0 deletions app/src/schemas/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./toolSchema";
70 changes: 70 additions & 0 deletions app/src/schemas/toolSchema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { z } from "zod";
import zodToJsonSchema from "zod-to-json-schema";

/**
* The schema for a tool definition
* @see https://platform.openai.com/docs/guides/structured-outputs/supported-schemas
*/
export const toolSchema = z.object({
type: z.literal("function"),
function: z.object({
name: z.string().describe("The name of the function"),
description: z
.string()
.optional()
.describe("A description of the function"),
parameters: z
.object({
type: z.literal("object"),
properties: z
.record(
z.object({
type: z
.enum([
"string",
"number",
"boolean",
"object",
"array",
"null",
"integer",
])
.describe("The type of the parameter"),
description: z
.string()
.optional()
.describe("A description of the parameter"),
enum: z
.array(z.string())
.optional()
.describe("The allowed values"),
})
)
.describe("A map of parameter names to their definitions"),
required: z
.array(z.string())
.optional()
.describe("The required parameters"),
additionalProperties: z
.boolean()
.optional()
.describe("Whether additional properties are allowed"),
strict: z
.boolean()
.optional()
.describe("Whether the object should be strict"),
})
.describe("The parameters that the function accepts"),
}),
});

/**
* The type of a tool definition
* @see https://platform.openai.com/docs/guides/structured-outputs/supported-schemas
*/
export type ToolDefinition = z.infer<typeof toolSchema>;

/**
* The JSON schema for a tool definition
*/
export const toolJSONSchema = zodToJsonSchema(toolSchema);
Loading

0 comments on commit 2d394a3

Please sign in to comment.