Tool calling tests and fixtures

This commit is contained in:
Zach Gollwitzer
2025-03-21 10:30:46 -04:00
parent 31aca27173
commit a68e5fffdc
22 changed files with 569 additions and 153 deletions

View File

@@ -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

View File

@@ -12,7 +12,8 @@ class Assistant::Function
{
type: "object",
properties: {},
required: []
required: [],
additionalProperties: false
}
end
end

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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,

View File

@@ -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"

View File

@@ -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
View File

@@ -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"

View File

@@ -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

View File

@@ -0,0 +1,5 @@
require "test_helper"
module LLMInterfaceTest
extend ActiveSupport::Testing::Declarative
end

View File

@@ -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

View File

@@ -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

View File

@@ -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

View 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