Tool calling tests and fixtures
This commit is contained in:
@@ -1,52 +1,98 @@
|
||||
class Assistant
|
||||
include Provided, ToolCallable
|
||||
include Provided
|
||||
|
||||
UnknownModelError = Class.new(StandardError)
|
||||
attr_reader :chat
|
||||
|
||||
class << self
|
||||
def for_chat(chat)
|
||||
new(chat)
|
||||
end
|
||||
|
||||
def available_functions
|
||||
[
|
||||
Assistant::Function::GetBalanceSheet,
|
||||
Assistant::Function::GetIncomeStatement,
|
||||
Assistant::Function::GetExpenseCategories,
|
||||
Assistant::Function::GetAccountBalances,
|
||||
Assistant::Function::GetTransactions,
|
||||
Assistant::Function::ComparePeriods
|
||||
]
|
||||
end
|
||||
|
||||
def instructions
|
||||
<<~PROMPT
|
||||
You are a helpful financial assistant for Maybe, a personal finance app.
|
||||
You help users understand their financial data by answering questions about their accounts, transactions, income, expenses, and net worth.
|
||||
|
||||
When users ask financial questions:
|
||||
1. Use the provided functions to retrieve the relevant data
|
||||
2. Provide ONLY the most important numbers and insights
|
||||
3. Eliminate all unnecessary words and context
|
||||
4. Use simple markdown for formatting
|
||||
5. Ask follow-up questions to keep the conversation going. Help educate the user about their own data and entice them to ask more questions.
|
||||
|
||||
DO NOT:
|
||||
- Add introductions or conclusions
|
||||
- Apologize or explain limitations
|
||||
|
||||
Present monetary values using the format provided by the functions.
|
||||
PROMPT
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(chat)
|
||||
@chat = chat
|
||||
end
|
||||
|
||||
def respond_to(message)
|
||||
model = get_model(message.ai_model)
|
||||
def respond_to_user
|
||||
latest_message = chat_history.last
|
||||
|
||||
raise UnknownModelError, "Unknown model: #{message.ai_model}" unless model.present?
|
||||
if latest_message.nil?
|
||||
Rails.logger.warn("Assistant skipped response because there are no messages to respond to in the chat")
|
||||
return
|
||||
end
|
||||
|
||||
response = model.provider.chat({
|
||||
model: model.name,
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: message.content
|
||||
}
|
||||
]
|
||||
})
|
||||
unless latest_message.user?
|
||||
Rails.logger.warn("Assistant skipped response because latest message is not a user message")
|
||||
return
|
||||
end
|
||||
|
||||
# test_tool_call = {
|
||||
# type: "function",
|
||||
# function_name: "get_balance_sheet",
|
||||
# function_params: { test: "param" }
|
||||
# }
|
||||
provider = provider_for_model(latest_message.ai_model)
|
||||
|
||||
# fn = available_functions[test_tool_call[:function_name]]
|
||||
response = provider.chat_response(
|
||||
model: latest_message.ai_model,
|
||||
instructions: instructions,
|
||||
messages: chat_history,
|
||||
functions: available_functions
|
||||
)
|
||||
|
||||
# raise "Function not found: #{test_tool_call[:function_name]}" unless fn.present?
|
||||
|
||||
# result = fn.executor.call(test_tool_call[:function_params])
|
||||
|
||||
# tool_call = ToolCall::Function.new(
|
||||
# function_name: test_tool_call[:function_name],
|
||||
# result: result
|
||||
# )
|
||||
|
||||
# tool_call.save!
|
||||
|
||||
# tool_call.chat_message
|
||||
if response.success?
|
||||
response.data.messages.each do |message|
|
||||
message.chat = chat
|
||||
message.save!
|
||||
end
|
||||
else
|
||||
Rails.logger.error("Assistant failed to respond to user: #{response.error}")
|
||||
chat.update!(error: response.error)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def chat_history
|
||||
chat.messages.ordered.where(role: [ :user, :assistant, :developer ], status: "complete", kind: "text")
|
||||
end
|
||||
|
||||
def call_function(name, params = {})
|
||||
fn = available_functions.find { |fn| fn.name == name }
|
||||
raise "Assistant does not implement function: #{name}" if fn.nil?
|
||||
fn.call(params)
|
||||
end
|
||||
|
||||
def instructions
|
||||
self.class.instructions
|
||||
end
|
||||
|
||||
def available_functions
|
||||
self.class.available_functions
|
||||
end
|
||||
end
|
||||
|
||||
@@ -12,7 +12,8 @@ class Assistant::Function
|
||||
{
|
||||
type: "object",
|
||||
properties: {},
|
||||
required: []
|
||||
required: [],
|
||||
additionalProperties: false
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -23,7 +23,8 @@ class Assistant::Function::ComparePeriods < Assistant::Function
|
||||
description: "Second period for comparison"
|
||||
}
|
||||
},
|
||||
required: [ "period1", "period2" ]
|
||||
required: [ "period1", "period2" ],
|
||||
additionalProperties: false
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -18,7 +18,8 @@ class Assistant::Function::GetAccountBalances < Assistant::Function
|
||||
description: "Type of accounts to get balances for"
|
||||
}
|
||||
},
|
||||
required: []
|
||||
required: [ "account_type" ],
|
||||
additionalProperties: false
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -19,11 +19,11 @@ class Assistant::Function::GetExpenseCategories < Assistant::Function
|
||||
},
|
||||
limit: {
|
||||
type: "integer",
|
||||
description: "Number of top categories to return",
|
||||
default: 5
|
||||
description: "Number of top categories to return"
|
||||
}
|
||||
},
|
||||
required: []
|
||||
required: [ "period", "limit" ],
|
||||
additionalProperties: false
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -18,7 +18,8 @@ class Assistant::Function::GetIncomeStatement < Assistant::Function
|
||||
description: "The time period for the income statement data"
|
||||
}
|
||||
},
|
||||
required: []
|
||||
required: [ "period" ],
|
||||
additionalProperties: false
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -14,13 +14,11 @@ class Assistant::Function::GetTransactions < Assistant::Function
|
||||
properties: {
|
||||
start_date: {
|
||||
type: "string",
|
||||
format: "date",
|
||||
description: "Start date for transactions (YYYY-MM-DD)"
|
||||
description: "Start date for transactions in YYYY-MM-DD format"
|
||||
},
|
||||
end_date: {
|
||||
type: "string",
|
||||
format: "date",
|
||||
description: "End date for transactions (YYYY-MM-DD)"
|
||||
description: "End date for transactions in YYYY-MM-DD format"
|
||||
},
|
||||
category_name: {
|
||||
type: "string",
|
||||
@@ -28,11 +26,11 @@ class Assistant::Function::GetTransactions < Assistant::Function
|
||||
},
|
||||
limit: {
|
||||
type: "integer",
|
||||
description: "Maximum number of transactions to return",
|
||||
default: 10
|
||||
description: "Maximum number of transactions to return (defaults to 10)"
|
||||
}
|
||||
},
|
||||
required: []
|
||||
required: [ "start_date", "end_date", "category_name", "limit" ],
|
||||
additionalProperties: false
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -3,11 +3,11 @@ module Assistant::Provideable
|
||||
|
||||
ChatResponse = Data.define(:messages)
|
||||
|
||||
def fetch_chat_response(params = {})
|
||||
raise NotImplementedError, "Subclasses must implement #chat"
|
||||
def supports_model?(model)
|
||||
raise NotImplementedError, "Subclasses must implement #supports_model?"
|
||||
end
|
||||
|
||||
def tools_config(assistant_functions = [])
|
||||
raise NotImplementedError, "Subclasses must implement #tools_config"
|
||||
def chat_response(messages:, model: nil, functions: [], instructions: nil)
|
||||
raise NotImplementedError, "Subclasses must implement #chat_response"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,15 +1,8 @@
|
||||
module Assistant::Provided
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
AiModel = Data.define(:id, :name, :provider)
|
||||
|
||||
def available_models
|
||||
[
|
||||
AiModel.new("openai-gpt-4o", "GPT-4o", Providers.openai)
|
||||
]
|
||||
end
|
||||
|
||||
def get_model(id)
|
||||
available_models.find { |model| model.id == id }
|
||||
def provider_for_model(model)
|
||||
available_providers = [ Providers.openai ].compact
|
||||
available_providers.find { |provider| provider.supports_model?(model) }
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
module Assistant::ToolCallable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
class_methods do
|
||||
def available_functions
|
||||
[
|
||||
Assistant::Function::GetBalanceSheet,
|
||||
Assistant::Function::GetIncomeStatement,
|
||||
Assistant::Function::GetExpenseCategories,
|
||||
Assistant::Function::GetAccountBalances,
|
||||
Assistant::Function::GetTransactions,
|
||||
Assistant::Function::ComparePeriods
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
def get_function(name)
|
||||
fn = self.class.available_functions.find { |fn| fn.name == name }
|
||||
raise "Assistant does not implement function: #{name}" if fn.nil?
|
||||
fn
|
||||
end
|
||||
end
|
||||
@@ -14,32 +14,9 @@ class Chat < ApplicationRecord
|
||||
def create_from_prompt!(prompt, developer_prompt: nil)
|
||||
create!(
|
||||
title: prompt.first(20),
|
||||
messages: [
|
||||
Message.new(kind: "text", role: "developer", content: developer_prompt || default_developer_prompt),
|
||||
Message.new(kind: "text", role: "user", content: prompt)
|
||||
]
|
||||
messages: [ Message.new(kind: "text", role: "user", content: prompt) ]
|
||||
)
|
||||
end
|
||||
|
||||
def default_developer_prompt
|
||||
<<~PROMPT
|
||||
You are a helpful financial assistant for Maybe, a personal finance app.
|
||||
You help users understand their financial data by answering questions about their accounts, transactions, income, expenses, and net worth.
|
||||
|
||||
When users ask financial questions:
|
||||
1. Use the provided functions to retrieve the relevant data
|
||||
2. Provide ONLY the most important numbers and insights
|
||||
3. Eliminate all unnecessary words and context
|
||||
4. Use simple markdown for formatting
|
||||
5. Ask follow-up questions to keep the conversation going. Help educate the user about their own data and entice them to ask more questions.
|
||||
|
||||
DO NOT:
|
||||
- Add introductions or conclusions
|
||||
- Apologize or explain limitations
|
||||
|
||||
Present monetary values using the format provided by the functions.
|
||||
PROMPT
|
||||
end
|
||||
end
|
||||
|
||||
def assistant
|
||||
|
||||
@@ -34,7 +34,7 @@ class Message < ApplicationRecord
|
||||
after_update_commit -> { broadcast_update_to chat }, if: :visible?
|
||||
|
||||
scope :ordered, -> { order(created_at: :asc) }
|
||||
scope :conversation, -> { Chat.debug_mode_enabled? ? all : where(role: [ :user, :assistant ], kind: [ "text", "reasoning" ]) }
|
||||
scope :conversation, -> { Chat.debug_mode_enabled? ? ordered : ordered.where(role: [ :user, :assistant ], kind: [ "text", "reasoning" ]) }
|
||||
|
||||
private
|
||||
def visible?
|
||||
@@ -44,9 +44,7 @@ class Message < ApplicationRecord
|
||||
def handle_create
|
||||
broadcast_append_to chat
|
||||
|
||||
if user?
|
||||
chat.assistant.respond_to(self)
|
||||
end
|
||||
chat.assistant.respond_to_user if user?
|
||||
end
|
||||
|
||||
def status_valid_for_role
|
||||
|
||||
@@ -26,6 +26,13 @@ class Provider
|
||||
error: nil,
|
||||
)
|
||||
rescue StandardError => error
|
||||
Sentry.capture_exception(error)
|
||||
|
||||
unless Rails.env.production?
|
||||
Rails.logger.error(error)
|
||||
Rails.logger.error(error.backtrace.join("\n"))
|
||||
end
|
||||
|
||||
ProviderResponse.new(
|
||||
success?: false,
|
||||
data: nil,
|
||||
|
||||
@@ -1,20 +1,35 @@
|
||||
class Provider::OpenAI < Provider
|
||||
include Assistant::Provideable
|
||||
|
||||
AVAILABLE_MODELS = %w[gpt-4o]
|
||||
|
||||
def initialize(access_token)
|
||||
@client = ::OpenAI::Client.new(access_token: access_token)
|
||||
end
|
||||
|
||||
def fetch_chat_response(params = {})
|
||||
def supports_model?(model)
|
||||
AVAILABLE_MODELS.include?(model)
|
||||
end
|
||||
|
||||
def chat_response(messages:, model: nil, functions: [], instructions: nil)
|
||||
provider_response do
|
||||
response = client.responses.create(parameters: params)
|
||||
validate_model!(model)
|
||||
|
||||
response = client.responses.create(
|
||||
parameters: {
|
||||
model: model,
|
||||
input: messages.map { |msg| { role: msg.role, content: msg.content } },
|
||||
tools: build_tools(functions),
|
||||
instructions: instructions
|
||||
}
|
||||
)
|
||||
|
||||
Assistant::Provideable::ChatResponse.new(
|
||||
messages: response.dig("output").filter { |item| item.dig("type") == "message" }.map do |item|
|
||||
Message.new(
|
||||
ai_model: response.dig("model"),
|
||||
provider_id: item.dig("id"),
|
||||
status: normalized_status(item.dig("status")),
|
||||
status: normalize_status(item.dig("status")),
|
||||
role: "assistant",
|
||||
content: item.dig("content").map { |content| content.dig("text") }.join("\n")
|
||||
)
|
||||
@@ -23,24 +38,29 @@ class Provider::OpenAI < Provider
|
||||
end
|
||||
end
|
||||
|
||||
def tools_config(assistant_functions)
|
||||
assistant_functions.map do |fn|
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
private
|
||||
attr_reader :client, :model, :functions
|
||||
|
||||
def validate_model!(model)
|
||||
raise "Model #{model} not supported for Provider::OpenAI" unless AVAILABLE_MODELS.include?(model)
|
||||
model
|
||||
end
|
||||
|
||||
def build_tools(functions = [])
|
||||
functions.map do |fn|
|
||||
{
|
||||
type: "function",
|
||||
name: fn.name,
|
||||
description: fn.description,
|
||||
parameters: fn.parameters
|
||||
parameters: fn.parameters,
|
||||
strict: true
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
attr_reader :client
|
||||
|
||||
def normalized_status(openai_status)
|
||||
case openai_status
|
||||
# Normalize to our internal message status values
|
||||
def normalize_status(status)
|
||||
case status
|
||||
when "in_progress"
|
||||
"pending"
|
||||
when "completed"
|
||||
|
||||
@@ -4,6 +4,7 @@ class CreateAiChats < ActiveRecord::Migration[7.2]
|
||||
t.references :user, null: false, foreign_key: true, type: :uuid
|
||||
t.string :title, null: false
|
||||
t.string :instructions
|
||||
t.string :error
|
||||
t.timestamps
|
||||
end
|
||||
|
||||
|
||||
1
db/schema.rb
generated
1
db/schema.rb
generated
@@ -200,6 +200,7 @@ ActiveRecord::Schema[7.2].define(version: 2025_03_19_212839) do
|
||||
t.uuid "user_id", null: false
|
||||
t.string "title", null: false
|
||||
t.string "instructions"
|
||||
t.string "error"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["user_id"], name: "index_chats_on_user_id"
|
||||
|
||||
8
test/fixtures/messages.yml
vendored
8
test/fixtures/messages.yml
vendored
@@ -33,4 +33,10 @@ assistant:
|
||||
chat: one
|
||||
created_at: 2025-03-20 12:02:00
|
||||
|
||||
|
||||
chat2_user:
|
||||
kind: text
|
||||
content: Can you help me understand my spending habits?
|
||||
role: user
|
||||
ai_model: gpt-4o
|
||||
chat: two
|
||||
created_at: 2025-03-20 12:00:01
|
||||
|
||||
5
test/interfaces/llm_interface_test.rb
Normal file
5
test/interfaces/llm_interface_test.rb
Normal file
@@ -0,0 +1,5 @@
|
||||
require "test_helper"
|
||||
|
||||
module LLMInterfaceTest
|
||||
extend ActiveSupport::Testing::Declarative
|
||||
end
|
||||
@@ -1,9 +1,33 @@
|
||||
require "test_helper"
|
||||
|
||||
class AssistantTest < ActiveSupport::TestCase
|
||||
include ProviderTestHelper
|
||||
|
||||
setup do
|
||||
@chat = chats(:one)
|
||||
@chat = chats(:two)
|
||||
@assistant = Assistant.for_chat(@chat)
|
||||
@provider = mock
|
||||
end
|
||||
|
||||
test "responds to basic prompt without tools" do
|
||||
@assistant.expects(:provider_for_model).with("gpt-4o").returns(@provider)
|
||||
@provider.expects(:chat_response).returns(
|
||||
provider_success_response(
|
||||
Assistant::Provideable::ChatResponse.new(
|
||||
messages: [
|
||||
Message.new(
|
||||
role: "assistant",
|
||||
content: "Hello from assistant",
|
||||
ai_model: "gpt-4o"
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
assert_difference "Message.count", 1 do
|
||||
@assistant.respond_to_user
|
||||
end
|
||||
end
|
||||
|
||||
test "can execute get_balance_sheet function" do
|
||||
|
||||
@@ -1,23 +1,61 @@
|
||||
require "test_helper"
|
||||
|
||||
class Provider::OpenAITest < ActiveSupport::TestCase
|
||||
include LLMInterfaceTest
|
||||
|
||||
setup do
|
||||
@openai = Provider::OpenAI.new(ENV.fetch("OPENAI_ACCESS_TOKEN"))
|
||||
@subject = @openai = Provider::OpenAI.new(ENV.fetch("OPENAI_ACCESS_TOKEN"))
|
||||
end
|
||||
|
||||
test "verifies model support" do
|
||||
assert_not @subject.supports_model?("unknown-for-test-that-returns-false")
|
||||
end
|
||||
|
||||
test "provides basic chat response" do
|
||||
VCR.use_cassette("openai/chat/basic_response") do
|
||||
response = @openai.fetch_chat_response({
|
||||
model: "gpt-4o-2024-08-06",
|
||||
input: [
|
||||
{ role: "user", content: "This is a chat test. Can you confirm it worked?", type: "message" }
|
||||
VCR.use_cassette("#{vcr_key_prefix}/chat/basic_response") do
|
||||
response = @subject.chat_response(
|
||||
model: "gpt-4o",
|
||||
messages: [
|
||||
Message.new(
|
||||
role: "user",
|
||||
content: "This is a chat test. If it's working, respond with a single word: Yes"
|
||||
)
|
||||
]
|
||||
})
|
||||
)
|
||||
|
||||
assert response.success?
|
||||
assert response.data.messages.size > 0
|
||||
assert_equal "gpt-4o-2024-08-06", response.data.messages.first.ai_model
|
||||
assert_kind_of String, response.data.messages.first.content
|
||||
assert_equal "Yes", response.data.messages.first.content
|
||||
end
|
||||
end
|
||||
|
||||
test "provides response with tool calls" do
|
||||
VCR.use_cassette("#{vcr_key_prefix}/chat/tool_calls") do
|
||||
# A prompt that should use multiple tools
|
||||
prompt = <<~PROMPT
|
||||
Can you show me a breakdown of the following?
|
||||
|
||||
- My net worth over the last 30 days
|
||||
- My income over the last 30 days
|
||||
- My spending over the last 30 days
|
||||
PROMPT
|
||||
|
||||
response = @subject.chat_response(
|
||||
model: "gpt-4o",
|
||||
instructions: Assistant.instructions,
|
||||
functions: Assistant.available_functions,
|
||||
messages: [
|
||||
Message.new(role: "user", content: prompt)
|
||||
]
|
||||
)
|
||||
|
||||
assert response.success?
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def vcr_key_prefix
|
||||
@subject.class.name.demodulize.underscore
|
||||
end
|
||||
end
|
||||
|
||||
@@ -5,8 +5,8 @@ http_interactions:
|
||||
uri: https://api.openai.com/v1/responses
|
||||
body:
|
||||
encoding: UTF-8
|
||||
string: '{"model":"gpt-4o-2024-08-06","input":[{"role":"user","content":"This
|
||||
is a chat test. Can you confirm it worked?","type":"message"}]}'
|
||||
string: '{"model":"gpt-4o","input":[{"role":"user","content":"This is a chat
|
||||
test. If it''s working, respond with a single word: Yes"}],"tools":[],"instructions":null}'
|
||||
headers:
|
||||
Content-Type:
|
||||
- application/json
|
||||
@@ -24,7 +24,7 @@ http_interactions:
|
||||
message: OK
|
||||
headers:
|
||||
Date:
|
||||
- Fri, 21 Mar 2025 01:57:49 GMT
|
||||
- Fri, 21 Mar 2025 13:50:21 GMT
|
||||
Content-Type:
|
||||
- application/json
|
||||
Transfer-Encoding:
|
||||
@@ -36,34 +36,34 @@ http_interactions:
|
||||
Openai-Organization:
|
||||
- "<OPENAI_ORGANIZATION_ID>"
|
||||
X-Request-Id:
|
||||
- req_9602335bd3d4fc028e943b73568c744a
|
||||
- req_8a37322ef4257b41a01e525acc01b0ae
|
||||
Openai-Processing-Ms:
|
||||
- '498'
|
||||
- '631'
|
||||
Strict-Transport-Security:
|
||||
- max-age=31536000; includeSubDomains; preload
|
||||
Cf-Cache-Status:
|
||||
- DYNAMIC
|
||||
Set-Cookie:
|
||||
- __cf_bm=mk53hP2iYq67QLxPffHK7_g20.q7qePE3uhGT5UlZug-1742522269-1.0.1.1-11uFuXhy2XgMCl6eR8T63b4GE.Ze2YKiPzjvBfWdVboNp4vcggYrq9ZALAou5_WSDH0Y.dxQLGSjWYQYLUKSXCUncNUROtV_FpP_NdShmGI;
|
||||
path=/; expires=Fri, 21-Mar-25 02:27:49 GMT; domain=.api.openai.com; HttpOnly;
|
||||
- __cf_bm=kh8YdfmY4GGPlJcVDtHeTTmvZa0JKNsma0u22EhS.Ms-1742565021-1.0.1.1-UF_dozBNpIBJpmduXIrqqOt7pRAnlnYc1f2N9Lr0eNcPs7M6nphc6CT87z2t6bIYc4zd2pftWv.qP9.hVvdOKOM79gGAMKkZuis7Apa2bWY;
|
||||
path=/; expires=Fri, 21-Mar-25 14:20:21 GMT; domain=.api.openai.com; HttpOnly;
|
||||
Secure; SameSite=None
|
||||
- _cfuvid=vgV2Dax5RdtVv7gucgvfTvHMU1dkXLdMjQL5jgoT_DA-1742522269973-0.0.1.1-604800000;
|
||||
- _cfuvid=zDCnrFu7rIUPBocD7Q66kQfH7grNGFM1vgONYpM5nys-1742565021589-0.0.1.1-604800000;
|
||||
path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None
|
||||
X-Content-Type-Options:
|
||||
- nosniff
|
||||
Server:
|
||||
- cloudflare
|
||||
Cf-Ray:
|
||||
- 9239d73749cecf57-CMH
|
||||
- 923deaf45bcfcf57-CMH
|
||||
Alt-Svc:
|
||||
- h3=":443"; ma=86400
|
||||
body:
|
||||
encoding: ASCII-8BIT
|
||||
string: |-
|
||||
{
|
||||
"id": "resp_67dcc79d66348192bf1ca3c4a0319225048ecd0f5dc17426",
|
||||
"id": "resp_67dd6e9cedfc819280d0ad9bd77b3f730701768d8a4cc0c9",
|
||||
"object": "response",
|
||||
"created_at": 1742522269,
|
||||
"created_at": 1742565020,
|
||||
"status": "completed",
|
||||
"error": null,
|
||||
"incomplete_details": null,
|
||||
@@ -73,13 +73,13 @@ http_interactions:
|
||||
"output": [
|
||||
{
|
||||
"type": "message",
|
||||
"id": "msg_67dcc79db4cc8192ade54805290f6943048ecd0f5dc17426",
|
||||
"id": "msg_67dd6e9d66dc8192a00dedea14b4ab080701768d8a4cc0c9",
|
||||
"status": "completed",
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{
|
||||
"type": "output_text",
|
||||
"text": "Yes, it's working! How can I assist you today?",
|
||||
"text": "Yes",
|
||||
"annotations": []
|
||||
}
|
||||
]
|
||||
@@ -103,18 +103,18 @@ http_interactions:
|
||||
"top_p": 1.0,
|
||||
"truncation": "disabled",
|
||||
"usage": {
|
||||
"input_tokens": 38,
|
||||
"input_tokens": 43,
|
||||
"input_tokens_details": {
|
||||
"cached_tokens": 0
|
||||
},
|
||||
"output_tokens": 13,
|
||||
"output_tokens": 2,
|
||||
"output_tokens_details": {
|
||||
"reasoning_tokens": 0
|
||||
},
|
||||
"total_tokens": 51
|
||||
"total_tokens": 45
|
||||
},
|
||||
"user": null,
|
||||
"metadata": {}
|
||||
}
|
||||
recorded_at: Fri, 21 Mar 2025 01:57:49 GMT
|
||||
recorded_at: Fri, 21 Mar 2025 13:50:21 GMT
|
||||
recorded_with: VCR 6.3.1
|
||||
320
test/vcr_cassettes/open_ai/chat/tool_calls.yml
Normal file
320
test/vcr_cassettes/open_ai/chat/tool_calls.yml
Normal file
@@ -0,0 +1,320 @@
|
||||
---
|
||||
http_interactions:
|
||||
- request:
|
||||
method: post
|
||||
uri: https://api.openai.com/v1/responses
|
||||
body:
|
||||
encoding: UTF-8
|
||||
string: '{"model":"gpt-4o","input":[{"role":"user","content":"Can you show me
|
||||
a breakdown of the following?\n\n- My net worth over the last 30 days\n- My
|
||||
income over the last 30 days\n- My spending over the last 30 days\n"}],"tools":[{"type":"function","name":"get_balance_sheet","description":"Get
|
||||
current balance sheet information including net worth, assets, and liabilities","parameters":{"type":"object","properties":{},"required":[],"additionalProperties":false},"strict":true},{"type":"function","name":"get_income_statement","description":"Get
|
||||
income statement data for a specific time period","parameters":{"type":"object","properties":{"period":{"type":"string","enum":["current_month","previous_month","year_to_date","previous_year"],"description":"The
|
||||
time period for the income statement data"}},"required":["period"],"additionalProperties":false},"strict":true},{"type":"function","name":"get_expense_categories","description":"Get
|
||||
top expense categories for a specific time period","parameters":{"type":"object","properties":{"period":{"type":"string","enum":["current_month","previous_month","year_to_date","previous_year"],"description":"The
|
||||
time period for the expense categories data"},"limit":{"type":"integer","description":"Number
|
||||
of top categories to return"}},"required":["period","limit"],"additionalProperties":false},"strict":true},{"type":"function","name":"get_account_balances","description":"Get
|
||||
balances for all accounts or by account type","parameters":{"type":"object","properties":{"account_type":{"type":"string","enum":["asset","liability","all"],"description":"Type
|
||||
of accounts to get balances for"}},"required":["account_type"],"additionalProperties":false},"strict":true},{"type":"function","name":"get_transactions","description":"Get
|
||||
transactions filtered by date range and/or category","parameters":{"type":"object","properties":{"start_date":{"type":"string","description":"Start
|
||||
date for transactions in YYYY-MM-DD format"},"end_date":{"type":"string","description":"End
|
||||
date for transactions in YYYY-MM-DD format"},"category_name":{"type":"string","description":"Filter
|
||||
transactions by category name"},"limit":{"type":"integer","description":"Maximum
|
||||
number of transactions to return (defaults to 10)"}},"required":["start_date","end_date","category_name","limit"],"additionalProperties":false},"strict":true},{"type":"function","name":"compare_periods","description":"Compare
|
||||
financial data between two periods","parameters":{"type":"object","properties":{"period1":{"type":"string","enum":["current_month","previous_month","year_to_date","previous_year"],"description":"First
|
||||
period for comparison"},"period2":{"type":"string","enum":["current_month","previous_month","year_to_date","previous_year"],"description":"Second
|
||||
period for comparison"}},"required":["period1","period2"],"additionalProperties":false},"strict":true}],"instructions":"You
|
||||
are a helpful financial assistant for Maybe, a personal finance app.\nYou
|
||||
help users understand their financial data by answering questions about their
|
||||
accounts, transactions, income, expenses, and net worth.\n\nWhen users ask
|
||||
financial questions:\n1. Use the provided functions to retrieve the relevant
|
||||
data\n2. Provide ONLY the most important numbers and insights\n3. Eliminate
|
||||
all unnecessary words and context\n4. Use simple markdown for formatting\n5.
|
||||
Ask follow-up questions to keep the conversation going. Help educate the user
|
||||
about their own data and entice them to ask more questions.\n\nDO NOT:\n-
|
||||
Add introductions or conclusions\n- Apologize or explain limitations\n\nPresent
|
||||
monetary values using the format provided by the functions.\n"}'
|
||||
headers:
|
||||
Content-Type:
|
||||
- application/json
|
||||
Authorization:
|
||||
- Bearer <OPENAI_ACCESS_TOKEN>
|
||||
Accept-Encoding:
|
||||
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
|
||||
Accept:
|
||||
- "*/*"
|
||||
User-Agent:
|
||||
- Ruby
|
||||
response:
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
headers:
|
||||
Date:
|
||||
- Fri, 21 Mar 2025 14:18:42 GMT
|
||||
Content-Type:
|
||||
- application/json
|
||||
Transfer-Encoding:
|
||||
- chunked
|
||||
Connection:
|
||||
- keep-alive
|
||||
Openai-Version:
|
||||
- '2020-10-01'
|
||||
Openai-Organization:
|
||||
- "<OPENAI_ORGANIZATION_ID>"
|
||||
X-Request-Id:
|
||||
- req_434f08736b7c91b1ea07bf81cef21507
|
||||
Openai-Processing-Ms:
|
||||
- '11758'
|
||||
Strict-Transport-Security:
|
||||
- max-age=31536000; includeSubDomains; preload
|
||||
Cf-Cache-Status:
|
||||
- DYNAMIC
|
||||
Set-Cookie:
|
||||
- __cf_bm=TDrrkraYJtMJkBDpwTFRHszMh5.44PObkaR_DPTn7bs-1742566722-1.0.1.1-5yNYBgBtwECMJLuLhfogtiDW1.XdsVBSnit.VpOjHitkB4OOfQLLaHO_dLR4d4HDG8zkNL1RNXI07vOirY10o1FxcZ1oR8YR0UMUrNJ2_nw;
|
||||
path=/; expires=Fri, 21-Mar-25 14:48:42 GMT; domain=.api.openai.com; HttpOnly;
|
||||
Secure; SameSite=None
|
||||
- _cfuvid=OKVFDrnX270p7.zncd1f.vstTBf11_gfDGJVz6qq_lQ-1742566722134-0.0.1.1-604800000;
|
||||
path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None
|
||||
X-Content-Type-Options:
|
||||
- nosniff
|
||||
Server:
|
||||
- cloudflare
|
||||
Cf-Ray:
|
||||
- 923e14334bbc633e-ORD
|
||||
Alt-Svc:
|
||||
- h3=":443"; ma=86400
|
||||
body:
|
||||
encoding: ASCII-8BIT
|
||||
string: |-
|
||||
{
|
||||
"id": "resp_67dd753662dc8192b5b25f9666f06d940cbbaa24c6c80cad",
|
||||
"object": "response",
|
||||
"created_at": 1742566710,
|
||||
"status": "completed",
|
||||
"error": null,
|
||||
"incomplete_details": null,
|
||||
"instructions": "You are a helpful financial assistant for Maybe, a personal finance app.\nYou help users understand their financial data by answering questions about their accounts, transactions, income, expenses, and net worth.\n\nWhen users ask financial questions:\n1. Use the provided functions to retrieve the relevant data\n2. Provide ONLY the most important numbers and insights\n3. Eliminate all unnecessary words and context\n4. Use simple markdown for formatting\n5. Ask follow-up questions to keep the conversation going. Help educate the user about their own data and entice them to ask more questions.\n\nDO NOT:\n- Add introductions or conclusions\n- Apologize or explain limitations\n\nPresent monetary values using the format provided by the functions.\n",
|
||||
"max_output_tokens": null,
|
||||
"model": "gpt-4o-2024-08-06",
|
||||
"output": [
|
||||
{
|
||||
"type": "function_call",
|
||||
"id": "fc_67dd75416cf08192b67279b3c06bb84b0cbbaa24c6c80cad",
|
||||
"call_id": "call_Be2crDZFo1QBcyPpo9SivVVk",
|
||||
"name": "get_balance_sheet",
|
||||
"arguments": "{}",
|
||||
"status": "completed"
|
||||
},
|
||||
{
|
||||
"type": "function_call",
|
||||
"id": "fc_67dd754197b881929875c127b46daea20cbbaa24c6c80cad",
|
||||
"call_id": "call_ncxXWtAIV0lGpgIeiHUcquAS",
|
||||
"name": "get_income_statement",
|
||||
"arguments": "{\"period\":\"current_month\"}",
|
||||
"status": "completed"
|
||||
},
|
||||
{
|
||||
"type": "function_call",
|
||||
"id": "fc_67dd7541b3ec8192bf57bbcf406aeaad0cbbaa24c6c80cad",
|
||||
"call_id": "call_tgWrZssOt6UE2BOoQMkul3lg",
|
||||
"name": "get_expense_categories",
|
||||
"arguments": "{\"period\":\"current_month\",\"limit\":5}",
|
||||
"status": "completed"
|
||||
}
|
||||
],
|
||||
"parallel_tool_calls": true,
|
||||
"previous_response_id": null,
|
||||
"reasoning": {
|
||||
"effort": null,
|
||||
"generate_summary": null
|
||||
},
|
||||
"store": true,
|
||||
"temperature": 1.0,
|
||||
"text": {
|
||||
"format": {
|
||||
"type": "text"
|
||||
}
|
||||
},
|
||||
"tool_choice": "auto",
|
||||
"tools": [
|
||||
{
|
||||
"type": "function",
|
||||
"description": "Get current balance sheet information including net worth, assets, and liabilities",
|
||||
"name": "get_balance_sheet",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": [],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"strict": true
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"description": "Get income statement data for a specific time period",
|
||||
"name": "get_income_statement",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"period": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"current_month",
|
||||
"previous_month",
|
||||
"year_to_date",
|
||||
"previous_year"
|
||||
],
|
||||
"description": "The time period for the income statement data"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"period"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"strict": true
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"description": "Get top expense categories for a specific time period",
|
||||
"name": "get_expense_categories",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"period": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"current_month",
|
||||
"previous_month",
|
||||
"year_to_date",
|
||||
"previous_year"
|
||||
],
|
||||
"description": "The time period for the expense categories data"
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "Number of top categories to return"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"period",
|
||||
"limit"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"strict": true
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"description": "Get balances for all accounts or by account type",
|
||||
"name": "get_account_balances",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"account_type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"asset",
|
||||
"liability",
|
||||
"all"
|
||||
],
|
||||
"description": "Type of accounts to get balances for"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"account_type"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"strict": true
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"description": "Get transactions filtered by date range and/or category",
|
||||
"name": "get_transactions",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"start_date": {
|
||||
"type": "string",
|
||||
"description": "Start date for transactions in YYYY-MM-DD format"
|
||||
},
|
||||
"end_date": {
|
||||
"type": "string",
|
||||
"description": "End date for transactions in YYYY-MM-DD format"
|
||||
},
|
||||
"category_name": {
|
||||
"type": "string",
|
||||
"description": "Filter transactions by category name"
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "Maximum number of transactions to return (defaults to 10)"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"start_date",
|
||||
"end_date",
|
||||
"category_name",
|
||||
"limit"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"strict": true
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"description": "Compare financial data between two periods",
|
||||
"name": "compare_periods",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"period1": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"current_month",
|
||||
"previous_month",
|
||||
"year_to_date",
|
||||
"previous_year"
|
||||
],
|
||||
"description": "First period for comparison"
|
||||
},
|
||||
"period2": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"current_month",
|
||||
"previous_month",
|
||||
"year_to_date",
|
||||
"previous_year"
|
||||
],
|
||||
"description": "Second period for comparison"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"period1",
|
||||
"period2"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"strict": true
|
||||
}
|
||||
],
|
||||
"top_p": 1.0,
|
||||
"truncation": "disabled",
|
||||
"usage": {
|
||||
"input_tokens": 0,
|
||||
"input_tokens_details": {
|
||||
"cached_tokens": 0
|
||||
},
|
||||
"output_tokens": 0,
|
||||
"output_tokens_details": {
|
||||
"reasoning_tokens": 0
|
||||
},
|
||||
"total_tokens": 0
|
||||
},
|
||||
"user": null,
|
||||
"metadata": {}
|
||||
}
|
||||
recorded_at: Fri, 21 Mar 2025 14:18:42 GMT
|
||||
recorded_with: VCR 6.3.1
|
||||
Reference in New Issue
Block a user