Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .changeset/project-group-name.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@

feat: Add `projectGroupName` to create projects inside a project group

`initLogger`, `init`, `initDataset`, and `Eval` now accept `projectGroupName`. When the named project does not exist yet, it is created inside that project group, which lets callers who only hold project-creation permission on a group (rather than on the whole organization) register projects. The option is ignored when a `projectId` is supplied.
`projects.create({ name, projectGroupName })` now accepts a project group name. When `braintrust push` or `project.publish()` registers a project that does not exist yet, it is created inside that project group, which lets callers who only hold project-creation permission on a group (rather than on the whole organization) register projects.
12 changes: 1 addition & 11 deletions js/src/framework.ts
Original file line number Diff line number Diff line change
Expand Up @@ -327,13 +327,6 @@
*/
projectId?: string;

/**
* If specified, creates the project inside the project group with this name when the project does
* not already exist. Requires permission to create projects in that group. Ignored if `projectId`
* is specified.
*/
projectGroupName?: string;

/**
* If specified, uses the logger state to initialize Braintrust objects. If unspecified, falls back
* to the global state (initialized using your API key).
Expand Down Expand Up @@ -508,7 +501,7 @@

export async function _internalInitEvaluatorExperiment(
projectName: string,
evaluator: Evaluator<any, any, any, any, any>,

Check warning on line 504 in js/src/framework.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
data: EvalData<any, any, any>,
options: {
disabled?: boolean;
Expand All @@ -522,10 +515,7 @@
return initExperiment(evaluator.state, {
...(evaluator.projectId
? { projectId: evaluator.projectId }
: {
project: projectName,
projectGroupName: evaluator.projectGroupName,
}),
: { project: projectName }),
experiment: options.experimentName ?? evaluator.experimentName,
description: evaluator.description,
metadata: evaluator.metadata,
Expand Down
47 changes: 46 additions & 1 deletion js/src/framework2.test.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,63 @@
import { beforeAll, expect, expectTypeOf, test } from "vitest";
import { afterEach, beforeAll, expect, expectTypeOf, test, vi } from "vitest";
import * as z3 from "zod/v3";
import * as z4 from "zod/v4";
import {
CodeFunction,
ProjectNameIdMap,
projects,
serializeEvalParametersToStaticParametersSchema,
} from "./framework2";
import { _internalGetGlobalState } from "./logger";
import { configureNode } from "./node/config";
import { zodToJsonSchema } from "./zod/utils";

beforeAll(() => {
configureNode();
});

afterEach(() => {
vi.restoreAllMocks();
});

function mockProjectRegister() {
const postJson = vi.fn(async () => ({
project: {
id: "00000000-0000-0000-0000-000000000001",
org_id: "00000000-0000-0000-0000-000000000002",
name: "project",
},
}));
vi.spyOn(_internalGetGlobalState(), "appConn").mockReturnValue({
post_json: postJson,
} as never);
return postJson;
}

test("project resolution creates the project in its project group", async () => {
const postJson = mockProjectRegister();
const project = projects.create({
name: "project",
projectGroupName: "my-group",
});

expect(await new ProjectNameIdMap().resolve(project)).toBe(
"00000000-0000-0000-0000-000000000001",
);
expect(postJson).toHaveBeenCalledWith("api/project/register", {
project_name: "project",
project_group_name: "my-group",
});
});

test("project resolution omits project_group_name when unspecified", async () => {
const postJson = mockProjectRegister();

await new ProjectNameIdMap().resolve(projects.create({ name: "project" }));
expect(postJson).toHaveBeenCalledWith("api/project/register", {
project_name: "project",
});
});

test.each([
{
version: "v3",
Expand Down
20 changes: 15 additions & 5 deletions js/src/framework2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,16 @@ export { toolFunctionDefinitionSchema };
const currentFilename =
typeof __filename !== "undefined" ? __filename : "unknown";

type NameOrId = { name: string } | { id: string };

export type CreateProjectOpts = NameOrId;
export type CreateProjectOpts =
| {
name: string;
/**
* If specified, creates the project inside the project group with this name when the project
* does not already exist. Requires permission to create projects in that group.
*/
projectGroupName?: string;
}
| { id: string };
class ProjectBuilder {
create(opts: CreateProjectOpts) {
return new Project(opts);
Expand All @@ -56,6 +63,7 @@ export const projects = new ProjectBuilder();
export class Project {
public readonly name?: string;
public readonly id?: string;
public readonly projectGroupName?: string;
public tools: ToolBuilder;
public prompts: PromptBuilder;
public parameters: ParametersBuilder;
Expand All @@ -77,6 +85,7 @@ export class Project {
_initializeSpanContext();
this.name = "name" in args ? args.name : undefined;
this.id = "id" in args ? args.id : undefined;
this.projectGroupName = "name" in args ? args.projectGroupName : undefined;
this.tools = new ToolBuilder(this);
this.prompts = new PromptBuilder(this);
this.parameters = new ParametersBuilder(this);
Expand Down Expand Up @@ -857,12 +866,13 @@ export class ProjectNameIdMap {
private nameToId: Record<string, string> = {};
private idToName: Record<string, string> = {};

async getId(projectName: string): Promise<string> {
async getId(projectName: string, projectGroupName?: string): Promise<string> {
if (!(projectName in this.nameToId)) {
const response = await _internalGetGlobalState()
.appConn()
.post_json("api/project/register", {
project_name: projectName,
...(projectGroupName ? { project_group_name: projectGroupName } : {}),
});

const result = z
Expand Down Expand Up @@ -898,6 +908,6 @@ export class ProjectNameIdMap {
if (project.id) {
return project.id;
}
return this.getId(project.name!);
return this.getId(project.name!, project.projectGroupName);
}
}
39 changes: 1 addition & 38 deletions js/src/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4159,7 +4159,6 @@ export type InitOptions<IsOpen extends boolean> = FullLoginOptions & {
tags?: string[];
gitMetadataSettings?: GitMetadataSettings;
projectId?: string;
projectGroupName?: string;
baseExperimentId?: string;
repoInfo?: RepoInfo;
setCurrent?: boolean;
Expand Down Expand Up @@ -4218,7 +4217,6 @@ type InitializedExperiment<IsOpen extends boolean | undefined> =
* @param setCurrent If true (the default), set the global current-experiment to the newly-created one.
* @param options.open If the experiment already exists, open it in read-only mode. Throws an error if the experiment does not already exist.
* @param options.projectId The id of the project to create the experiment in. This takes precedence over `project` if specified.
* @param options.projectGroupName (Optional) Create the project inside the project group with this name, if the project does not already exist. Requires permission to create projects in that group. Ignored if `projectId` is specified.
* @param options.baseExperimentId An optional experiment id to use as a base. If specified, the new experiment will be summarized and compared to this. This takes precedence over `baseExperiment` if specified.
* @param options.repoInfo (Optional) Explicitly specify the git metadata for this experiment. This takes precedence over `gitMetadataSettings` if specified.
* @param options.tags (Optional) A list of tags to attach to the experiment.
Expand Down Expand Up @@ -4278,7 +4276,6 @@ export function init<IsOpen extends boolean = false>(
tags,
gitMetadataSettings,
projectId,
projectGroupName,
baseExperimentId,
repoInfo,
state: stateArg,
Expand Down Expand Up @@ -4358,18 +4355,6 @@ export function init<IsOpen extends boolean = false>(
update,
};

if (!isEmpty(projectGroupName) && isEmpty(projectId)) {
// `api/experiment/register` cannot create a project inside a project
// group, so resolve the project (creating it in the group if needed)
// up front and register the experiment against its id.
const { project: groupProject } = await computeLoggerMetadata(state, {
project_name: project,
project_group_name: projectGroupName,
});
delete args["project_name"];
args["project_id"] = groupProject.id;
}

if (experiment) {
args["experiment_name"] = experiment;
}
Expand Down Expand Up @@ -4591,7 +4576,6 @@ export type InitDatasetOptions<IsLegacyDataset extends boolean> =
environment?: string;
snapshotName?: string;
projectId?: string;
projectGroupName?: string;
metadata?: Record<string, unknown>;
state?: BraintrustState;
_internal_btql?: Record<string, unknown>;
Expand Down Expand Up @@ -4883,7 +4867,6 @@ export function initDataset<
fetch,
forceLogin,
projectId,
projectGroupName,
metadata,
useOutput: legacy,
state: stateArg,
Expand Down Expand Up @@ -4968,19 +4951,6 @@ export function initDataset<
description,
metadata,
};

if (!isEmpty(projectGroupName) && isEmpty(projectId)) {
// `api/dataset/register` cannot create a project inside a project
// group, so resolve the project (creating it in the group if needed)
// up front and register the dataset against its id.
const { project: groupProject } = await computeLoggerMetadata(state, {
project_name: project,
project_group_name: projectGroupName,
});
delete args["project_name"];
args["project_id"] = groupProject.id;
}

const response = await state
.appConn()
.post_json("api/dataset/register", args);
Expand Down Expand Up @@ -5081,11 +5051,9 @@ async function computeLoggerMetadata(
{
project_name,
project_id,
project_group_name,
}: {
project_name?: string;
project_id?: string;
project_group_name?: string;
},
) {
await state.login({});
Expand All @@ -5103,7 +5071,7 @@ async function computeLoggerMetadata(
org_id,
state.loginToken,
isEmpty(project_id)
? ["name", project_name || GLOBAL_PROJECT, project_group_name ?? null]
? ["name", project_name || GLOBAL_PROJECT]
: ["id", project_id],
]);
const cached = cache.get(key);
Expand All @@ -5125,7 +5093,6 @@ async function computeLoggerMetadata(
{
project_name: project_name || GLOBAL_PROJECT,
org_id,
...(isEmpty(project_group_name) ? {} : { project_group_name }),
},
controller.signal,
);
Expand Down Expand Up @@ -5186,7 +5153,6 @@ type AsyncFlushArg<IsAsyncFlush> = {
export type InitLoggerOptions<IsAsyncFlush> = FullLoginOptions & {
projectName?: string;
projectId?: string;
projectGroupName?: string;
environment?: SpanOriginEnvironment;
setCurrent?: boolean;
state?: BraintrustState;
Expand All @@ -5199,7 +5165,6 @@ export type InitLoggerOptions<IsAsyncFlush> = FullLoginOptions & {
* @param options Additional options for configuring init().
* @param options.projectName The name of the project to log into. If unspecified, will default to the Global project.
* @param options.projectId The id of the project to log into. This takes precedence over projectName if specified.
* @param options.projectGroupName (Optional) Create the project inside the project group with this name, if the project does not already exist. Requires permission to create projects in that group. Ignored if `projectId` is specified.
* @param options.asyncFlush If true, will log asynchronously in the background. Otherwise, will log synchronously. (true by default)
* @param options.appUrl The URL of the Braintrust App. Defaults to https://www.braintrust.dev.
* @param options.apiKey The API key to use. If the parameter is not specified, will try to use the `BRAINTRUST_API_KEY` environment variable. In Node.js,
Expand All @@ -5216,7 +5181,6 @@ export function initLogger<IsAsyncFlush extends boolean = true>(
const {
projectName,
projectId,
projectGroupName,
asyncFlush: asyncFlushArg,
appUrl,
apiKey,
Expand All @@ -5234,7 +5198,6 @@ export function initLogger<IsAsyncFlush extends boolean = true>(
const computeMetadataArgs = {
project_name: projectName,
project_id: projectId,
project_group_name: projectGroupName,
};

const linkArgs = {
Expand Down
Loading
Loading