Skip to content
Open
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
90 changes: 90 additions & 0 deletions js/src/experimental-prompt-adapters/ai-sdk.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import type {
PromptMessage,
PromptMessageContentPart,
} from "../experimental-prompt-api";
import type { PromptJsonSchema } from "../experimental-prompt-api-schema-utils";
import { isImageFile, resolvePromptFile } from "./utils";
import type { AdapterOptions, PromptAdapterInput } from "./types";

type AISDKContentPart =
| { type: "text"; text: string }
| { type: "image"; image: string; mediaType?: string }
| { type: "file"; data: string; mediaType: string; filename?: string };

type AISDKMessage = Omit<PromptMessage, "content"> & {
content: string | AISDKContentPart[];
};

type AISDKGenerateObjectPromptArgs = {
model?: string;
messages: AISDKMessage[];
schema?: PromptJsonSchema;
experimental_telemetry: {
metadata: {
braintrustPrompt: PromptAdapterInput["dependencies"];
};
};
};

type AISDKGenerateObjectAdapterOptions = AdapterOptions;

export function aiSDKGenerateObjectAdapter(
options: AISDKGenerateObjectAdapterOptions = {},
): (builtPrompt: PromptAdapterInput) => Promise<AISDKGenerateObjectPromptArgs> {
return async (builtPrompt) => ({
model: builtPrompt.model,
messages: await Promise.all(
builtPrompt.messages.map((message) =>
renderAISDKMessage(message, options),
),
),
schema: builtPrompt.outputSchema?.toJSONSchema(),
experimental_telemetry: {
metadata: {
braintrustPrompt: builtPrompt.dependencies,
},
},
});
}

async function renderAISDKMessage(
message: PromptMessage,
options: AdapterOptions,
): Promise<AISDKMessage> {
if (typeof message.content === "string") {
return message as AISDKMessage;

Check warning on line 55 in js/src/experimental-prompt-adapters/ai-sdk.ts

View workflow job for this annotation

GitHub Actions / lint

Do not use any type assertions
}
return {
...message,
content: await Promise.all(
message.content.map((part) => renderAISDKContentPart(part, options)),
),
};
}

async function renderAISDKContentPart(
part: PromptMessageContentPart,
options: AdapterOptions,
): Promise<AISDKContentPart> {
if (part.type === "text") {
return part;
}

const resolved = await resolvePromptFile(part, options);
if (isImageFile(resolved)) {
return {
type: "image",
image: resolved.data,
...(resolved.contentType
? { mediaType: resolved.contentType }
: undefined),
};
}

return {
type: "file",
data: resolved.data,
mediaType: resolved.contentType ?? "application/octet-stream",
...(resolved.filename ? { filename: resolved.filename } : undefined),
};
}
9 changes: 9 additions & 0 deletions js/src/experimental-prompt-adapters/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { aiSDKGenerateObjectAdapter } from "./ai-sdk";
import { openAIChatAdapter } from "./openai";

export { aiSDKGenerateObjectAdapter, openAIChatAdapter };

export const adapters = {
openAIChat: openAIChatAdapter,
aiSDKGenerateObject: aiSDKGenerateObjectAdapter,
};
128 changes: 128 additions & 0 deletions js/src/experimental-prompt-adapters/openai.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import type {
PromptMessage,
PromptMessageContentPart,
} from "../experimental-prompt-api";
import type { PromptJsonSchema } from "../experimental-prompt-api-schema-utils";
import type { AdapterOptions, PromptAdapterInput } from "./types";
import { isImageFile, isLikelyFileId, resolvePromptFile } from "./utils";

type OpenAIContentPart =
| { type: "text"; text: string }
| {
type: "image_url";
image_url: { url: string; detail?: "auto" | "low" | "high" };
}
| {
type: "file";
file: { file_data?: string; file_id?: string; filename?: string };
};

type OpenAIChatMessage = Omit<PromptMessage, "content"> & {
content: string | OpenAIContentPart[];
};

type OpenAIChatPromptArgs = {
model?: string;
messages: OpenAIChatMessage[];
response_format?: {
type: "json_schema";
json_schema: {
name: string;
schema: PromptJsonSchema;
strict: true;
};
};
span_info: {
metadata: {
prompt: PromptAdapterInput["dependencies"];
};
};
};

type OpenAIChatAdapterOptions = AdapterOptions;

function schemaName(slug: string): string {
const name = slug.replace(/[^a-zA-Z0-9_-]/g, "_");
return name.length > 0 ? `${name}_output` : "prompt_output";
}

export function openAIChatAdapter(
options: OpenAIChatAdapterOptions = {},
): (builtPrompt: PromptAdapterInput) => Promise<OpenAIChatPromptArgs> {
return async (builtPrompt) => {
const outputSchema = builtPrompt.outputSchema?.toJSONSchema();
return {
model: builtPrompt.model,
messages: await Promise.all(
builtPrompt.messages.map((message) =>
renderOpenAIMessage(message, options),
),
),
...(outputSchema
? {
response_format: {
type: "json_schema" as const,
json_schema: {
name: schemaName(builtPrompt.dependencies.root.slug),
schema: outputSchema,
strict: true as const,
},
},
}
: undefined),
span_info: {
metadata: {
prompt: builtPrompt.dependencies,
},
},
};
};
}

async function renderOpenAIMessage(
message: PromptMessage,
options: AdapterOptions,
): Promise<OpenAIChatMessage> {
if (typeof message.content === "string") {
return message as OpenAIChatMessage;

Check warning on line 87 in js/src/experimental-prompt-adapters/openai.ts

View workflow job for this annotation

GitHub Actions / lint

Do not use any type assertions
}
return {
...message,
content: await Promise.all(
message.content.map((part) => renderOpenAIContentPart(part, options)),
),
};
}

async function renderOpenAIContentPart(
part: PromptMessageContentPart,
options: AdapterOptions,
): Promise<OpenAIContentPart> {
if (part.type === "text") {
return part;
}

const resolved = await resolvePromptFile(part, options);
if (isImageFile(resolved)) {
return {
type: "image_url",
image_url: {
url: resolved.data,
...(resolved.detail ? { detail: resolved.detail } : undefined),
},
};
}

return {
type: "file",
file: isLikelyFileId(resolved.data)
? {
file_id: resolved.data,
...(resolved.filename ? { filename: resolved.filename } : undefined),
}
: {
file_data: resolved.data,
...(resolved.filename ? { filename: resolved.filename } : undefined),
},
};
}
21 changes: 21 additions & 0 deletions js/src/experimental-prompt-adapters/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { PromptJsonSchema } from "../experimental-prompt-api-schema-utils";
import type {
PromptDependencies,
PromptMessage,
} from "../experimental-prompt-api";
import type { BraintrustState } from "../logger";

export type PromptAdapterInput = {
kind: "messages" | "string";
model?: string;
inputSchema: { toJSONSchema(): PromptJsonSchema };
outputSchema?: { toJSONSchema(): PromptJsonSchema };
input: unknown;
messages: PromptMessage[];
content?: string;
dependencies: PromptDependencies;
};

export type AdapterOptions = {
state?: BraintrustState;
};
Loading
Loading