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
4 changes: 3 additions & 1 deletion lib/braintrust/api/functions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,15 @@ def initialize(api)
# List functions with optional filters
# GET /v1/function?project_name=X&...
# @param project_name [String, nil] Filter by project name
# @param project_id [String, nil] Filter by project ID (UUID)
# @param function_name [String, nil] Filter by function name
# @param slug [String, nil] Filter by slug
# @param limit [Integer, nil] Limit number of results
# @return [Hash] Response with "objects" array
def list(project_name: nil, function_name: nil, slug: nil, limit: nil)
def list(project_name: nil, project_id: nil, function_name: nil, slug: nil, limit: nil)
params = {}
params["project_name"] = project_name if project_name
params["project_id"] = project_id if project_id
params["function_name"] = function_name if function_name
params["slug"] = slug if slug
params["limit"] = limit if limit
Expand Down
16 changes: 11 additions & 5 deletions lib/braintrust/prompt.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,28 @@ module Braintrust
# params = prompt.build(text: "Article to summarize...")
# client.messages.create(**params)
class Prompt
attr_reader :id, :name, :slug, :project_id
attr_reader :id, :name, :slug, :project_id, :version

# Load a prompt from Braintrust
#
# @param project [String] Project name
# @param project [String, nil] Project name (provide either project or project_id)
# @param project_id [String, nil] Project ID (UUID, provide either project or project_id)
# @param slug [String] Prompt slug
# @param version [String, nil] Specific version (default: latest)
# @param defaults [Hash] Default variable values for build()
# @param api [API, nil] Braintrust API client (default: creates one using global state)
# @return [Prompt]
def self.load(project:, slug:, version: nil, defaults: {}, api: nil)
def self.load(slug:, project: nil, project_id: nil, version: nil, defaults: {}, api: nil)
raise ArgumentError, "Either project or project_id is required" unless project || project_id

api ||= API.new

# Find the function by project + slug
result = api.functions.list(project_name: project, slug: slug)
result = api.functions.list(project_name: project, project_id: project_id, slug: slug)
function = result.dig("objects")&.first
raise Error, "Prompt '#{slug}' not found in project '#{project}'" unless function

identifier = project ? "project '#{project}'" : "project_id '#{project_id}'"
raise Error, "Prompt '#{slug}' not found in #{identifier}" unless function

# Fetch full function data including prompt_data
full_data = api.functions.get(id: function["id"], version: version)
Expand All @@ -47,6 +52,7 @@ def initialize(data, defaults: {})
@name = data["name"]
@slug = data["slug"]
@project_id = data["project_id"]
@version = data["_xact_id"]
end

# Get the raw prompt definition
Expand Down
35 changes: 35 additions & 0 deletions test/braintrust/api/functions_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,41 @@ def test_functions_list_with_project_name
end
end

def test_functions_list_with_project_id
VCR.use_cassette("functions/list_with_project_id") do
api = get_test_api
function_slug = "test-ruby-sdk-list-by-project-id"

# Create a function so we always have something to list
created = api.functions.create(
project_name: @project_name,
slug: function_slug,
function_data: {type: "prompt"},
prompt_data: {
prompt: {
type: "chat",
messages: [{role: "user", content: "Test"}]
},
options: {model: "gpt-4o-mini"}
}
)
project_id = created["project_id"]
assert project_id, "Expected project_id in create response"

begin
# List using project_id — must always exercise this code path
id_result = api.functions.list(project_id: project_id)
assert_instance_of Hash, id_result
assert id_result.key?("objects")
assert_instance_of Array, id_result["objects"]
assert id_result["objects"].any?, "Expected at least one function"
assert id_result["objects"].all? { |f| f["project_id"] == project_id }
ensure
api.functions.delete(id: created["id"])
end
end
end

def test_functions_create_new_function
VCR.use_cassette("functions/create") do
api = get_test_api
Expand Down
67 changes: 67 additions & 0 deletions test/braintrust/prompt_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,19 @@ def test_prompt_initialization
assert_equal "proj-456", prompt.project_id
end

def test_prompt_version
data = @function_data.merge("_xact_id" => "xact-789")
prompt = Braintrust::Prompt.new(data)

assert_equal "xact-789", prompt.version
end

def test_prompt_version_nil_when_not_present
prompt = Braintrust::Prompt.new(@function_data)

assert_nil prompt.version
end

def test_prompt_messages
prompt = Braintrust::Prompt.new(@function_data)
messages = prompt.messages
Expand Down Expand Up @@ -330,6 +343,57 @@ def test_prompt_load_not_found
end
end

def test_prompt_load_requires_project_or_project_id
error = assert_raises(ArgumentError) do
Braintrust::Prompt.load(slug: "some-prompt")
end

assert_match(/either project or project_id/i, error.message)
end

def test_prompt_load_with_project_id
VCR.use_cassette("prompt/load_with_project_id") do
api = get_test_api
slug = "test-prompt-project-id"

# Create a prompt to get a valid project_id
created = api.functions.create(
project_name: @project_name,
slug: slug,
function_data: {type: "prompt"},
prompt_data: {
prompt: {
type: "chat",
messages: [
{role: "user", content: "Project ID test: {{name}}"}
]
},
options: {
model: "gpt-4o-mini"
}
}
)

project_id = created["project_id"]
assert project_id, "Expected project_id in response"

# Load the prompt using project_id instead of project name
prompt = Braintrust::Prompt.load(project_id: project_id, slug: slug, api: api)

assert_instance_of Braintrust::Prompt, prompt
assert_equal slug, prompt.slug
assert_equal project_id, prompt.project_id
assert_equal "gpt-4o-mini", prompt.model

# Build and verify content
result = prompt.build(name: "World")
assert_equal "Project ID test: World", result[:messages][0][:content]

# Clean up
api.functions.delete(id: prompt.id)
end
end

def test_prompt_load_with_version
VCR.use_cassette("prompt/load_with_version") do
api = get_test_api
Expand Down Expand Up @@ -368,6 +432,9 @@ def test_prompt_load_with_version
assert_equal slug, prompt.slug
assert_equal "gpt-4o-mini", prompt.model

# Verify version accessor returns the exact _xact_id that was requested
assert_equal version_id, prompt.version

# Build and verify content
result = prompt.build(name: "World")
assert_equal "Version test: World", result[:messages][0][:content]
Expand Down
Loading
Loading