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
27 changes: 27 additions & 0 deletions js/src/framework2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,15 @@ import {
Prompt,
PromptRowWithId,
} from "./logger";
import {
clearPromptEnvironment,
listPromptEnvironments,
setPromptEnvironment,
type ClearPromptEnvironmentOptions,
type ListPromptEnvironmentsOptions,
type PromptEnvironmentAssociation,
type SetPromptEnvironmentOptions,
} from "./prompt-environments";
import { GenericFunction } from "./framework-types";

interface BaseFnOpts {
Expand Down Expand Up @@ -581,6 +590,24 @@ export class PromptBuilder {

return prompt;
}

public listPromptEnvironments(
options: ListPromptEnvironmentsOptions,
): Promise<PromptEnvironmentAssociation[]> {
return listPromptEnvironments(options);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd expect an Environments class with a list, set, clear.

something like

prompt.environments.list() and so on

This would follow existing prompt.project etc.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd imagine this environment class would be used in logger instead of the function based approach

}

public setPromptEnvironment(
options: SetPromptEnvironmentOptions,
): Promise<PromptEnvironmentAssociation> {
return setPromptEnvironment(options);
}

public clearPromptEnvironment(
options: ClearPromptEnvironmentOptions,
): Promise<PromptEnvironmentAssociation> {
return clearPromptEnvironment(options);
}
}

export function promptDefinitionToPromptData(
Expand Down
49 changes: 49 additions & 0 deletions js/src/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,15 @@ import iso, { IsoAsyncLocalStorage } from "./isomorph";
import { canUseDiskCache, DiskCache } from "./prompt-cache/disk-cache";
import { LRUCache } from "./prompt-cache/lru-cache";
import { PromptCache } from "./prompt-cache/prompt-cache";
import {
clearPromptEnvironment,
listPromptEnvironments,
setPromptEnvironment,
type ClearPromptEnvironmentOptions,
type ListPromptEnvironmentsOptions,
type PromptEnvironmentAssociation,
type SetPromptEnvironmentOptions,
} from "./prompt-environments";
import {
addAzureBlobHeaders,
getCurrentUnixTimestamp,
Expand Down Expand Up @@ -6543,6 +6552,46 @@ export class Prompt<
return this.getParsedPromptData()!;
}

private requireId(): string {
const id = this.id as string | undefined;
if (!id) {
throw new Error(
"Prompt id is required to manage environments. Load the prompt from the API first.",
);
}
return id;
}

public listPromptEnvironments(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure i'd expect these methods here if the framework2 already has this. 🤔

if we want to keep them would imagine something like:

getEnvironments()
setEnvironments()
clearEnvironments()

(Prompt seemed duplicative)

this: Prompt<true, HasVersion>,
options: Omit<ListPromptEnvironmentsOptions, "promptId"> = {},
): Promise<PromptEnvironmentAssociation[]> {
return listPromptEnvironments({
...options,
promptId: this.requireId(),
});
}

public setPromptEnvironment(
this: Prompt<true, HasVersion>,
options: Omit<SetPromptEnvironmentOptions, "promptId">,
): Promise<PromptEnvironmentAssociation> {
return setPromptEnvironment({
...options,
promptId: this.requireId(),
});
}

public clearPromptEnvironment(
this: Prompt<true, HasVersion>,
options: Omit<ClearPromptEnvironmentOptions, "promptId">,
): Promise<PromptEnvironmentAssociation> {
return clearPromptEnvironment({
...options,
promptId: this.requireId(),
});
}

/**
* Build the prompt with the given formatting options. The args you pass in will
* be forwarded to the mustache template that defines the prompt and rendered with
Expand Down
209 changes: 209 additions & 0 deletions js/src/prompt-environments.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
import {
BraintrustState,
FullLoginOptions,
_internalGetGlobalState,
} from "./logger";

export interface PromptEnvironmentAssociation {
id: string;
object_type: string;
object_id: string;
object_version: string;
environment_slug: string;
created: string;
}

export type ListPromptEnvironmentsOptions = FullLoginOptions & {
promptId: string;
state?: BraintrustState;
};

export type SetPromptEnvironmentOptions = FullLoginOptions & {
promptId: string;
environmentSlug: string;
version: string;
state?: BraintrustState;
};

export type ClearPromptEnvironmentOptions = FullLoginOptions & {
promptId: string;
environmentSlug: string;
state?: BraintrustState;
};

/**
* List all environment associations for a prompt.
*
* @param options Options for the request
* @param options.promptId The ID of the prompt to list environment associations for
* @param options.apiKey The API key to use. If not specified, will use the `BRAINTRUST_API_KEY` environment variable.
* @param options.appUrl The URL of the Braintrust App. Defaults to https://www.braintrust.dev.
* @param options.orgName (Optional) The name of a specific organization to connect to.
* @returns A list of environment associations for the prompt
*
* @example
* ```javascript
* const associations = await listPromptEnvironments({
* promptId: "prompt-uuid",
* });
* console.log(associations);
* // [{ environment_slug: "production", object_version: "123", ... }]
* ```
*/
export async function listPromptEnvironments({
promptId,
appUrl,
apiKey,
orgName,
fetch,
forceLogin,
state: stateArg,
}: ListPromptEnvironmentsOptions): Promise<PromptEnvironmentAssociation[]> {
const state = stateArg ?? _internalGetGlobalState();

await state.login({
orgName,
apiKey,
appUrl,
fetch,
forceLogin,
});

const response = await state
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing try catch? perhaps better error message

.apiConn()
.get_json(
`environment-object/prompt/${promptId}`,
orgName ? { org_name: orgName } : {},
);

return (response.objects ?? []) as PromptEnvironmentAssociation[];
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would this hide an upstream error?

}

/**
* Set (or update) the prompt version associated with an environment.
* If an association already exists, it will be updated. Otherwise, a new one will be created.
*
* @param options Options for the request
* @param options.promptId The ID of the prompt
* @param options.environmentSlug The environment slug to set (e.g., "production", "staging")
* @param options.version The version (xact_id) of the prompt to associate with this environment
* @param options.apiKey The API key to use. If not specified, will use the `BRAINTRUST_API_KEY` environment variable.
* @param options.appUrl The URL of the Braintrust App. Defaults to https://www.braintrust.dev.
* @param options.orgName (Optional) The name of a specific organization to connect to.
* @returns The created or updated environment association
*
* @example
* ```javascript
* // Associate prompt version "456" with the "production" environment
* const association = await setPromptEnvironment({
* promptId: "prompt-uuid",
* environmentSlug: "production",
* version: "456",
* });
* ```
*/
export async function setPromptEnvironment({
promptId,
environmentSlug,
version,
appUrl,
apiKey,
orgName,
fetch,
forceLogin,
state: stateArg,
}: SetPromptEnvironmentOptions): Promise<PromptEnvironmentAssociation> {
const state = stateArg ?? _internalGetGlobalState();

await state.login({
orgName,
apiKey,
appUrl,
fetch,
forceLogin,
});

// Use PUT for upsert behavior - need to use state.fetch since HTTPConnection doesn't have put method
const apiConn = state.apiConn();
const url = `${apiConn.base_url}/environment-object/prompt/${promptId}/${environmentSlug}`;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we just fix this at the apiConn level? i.e. add a put_json()

const resp = await state.fetch(url, {
method: "PUT",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
...(apiConn.token ? { Authorization: `Bearer ${apiConn.token}` } : {}),
},
body: JSON.stringify({
object_version: version,
...(orgName && { org_name: orgName }),
}),
});

if (!resp.ok) {
const text = await resp.text();
throw new Error(`${resp.status}: ${resp.statusText} (${text})`);
}

return (await resp.json()) as PromptEnvironmentAssociation;
}

/**
* Clear an environment association for a prompt.
*
* @param options Options for the request
* @param options.promptId The ID of the prompt
* @param options.environmentSlug The environment slug to delete (e.g., "production", "staging")
* @param options.apiKey The API key to use. If not specified, will use the `BRAINTRUST_API_KEY` environment variable.
* @param options.appUrl The URL of the Braintrust App. Defaults to https://www.braintrust.dev.
* @param options.orgName (Optional) The name of a specific organization to connect to.
* @returns The cleared environment association
* @throws If no association exists for the prompt and environment
*
* @example
* ```javascript
* // Remove the "staging" environment association
* await clearPromptEnvironment({
* promptId: "prompt-uuid",
* environmentSlug: "staging",
* });
* ```
*/
export async function clearPromptEnvironment({
promptId,
environmentSlug,
appUrl,
apiKey,
orgName,
fetch,
forceLogin,
state: stateArg,
}: ClearPromptEnvironmentOptions): Promise<PromptEnvironmentAssociation> {
const state = stateArg ?? _internalGetGlobalState();

await state.login({
orgName,
apiKey,
appUrl,
fetch,
forceLogin,
});

// Need to use state.fetch since HTTPConnection doesn't have delete method
const apiConn = state.apiConn();
const queryString = orgName ? `?org_name=${encodeURIComponent(orgName)}` : "";
const url = `${apiConn.base_url}/environment-object/prompt/${promptId}/${environmentSlug}${queryString}`;
const resp = await state.fetch(url, {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto here

method: "DELETE",
headers: {
Accept: "application/json",
...(apiConn.token ? { Authorization: `Bearer ${apiConn.token}` } : {}),
},
});

if (!resp.ok) {
const text = await resp.text();
throw new Error(`${resp.status}: ${resp.statusText} (${text})`);
}

return (await resp.json()) as PromptEnvironmentAssociation;
}
Loading