Add back thinking messages, clean up error handling for chat
This commit is contained in:
@@ -21,7 +21,11 @@ class ChatsController < ApplicationController
|
||||
def create
|
||||
@chat = Current.user.chats.start!(chat_params[:content], model: chat_params[:ai_model])
|
||||
set_last_viewed_chat(@chat)
|
||||
redirect_to chat_path(@chat)
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_to chat_path(@chat) }
|
||||
format.turbo_stream
|
||||
end
|
||||
end
|
||||
|
||||
def edit
|
||||
@@ -45,7 +49,14 @@ class ChatsController < ApplicationController
|
||||
|
||||
def retry
|
||||
@chat.retry_last_message!
|
||||
redirect_to chat_path(@chat)
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_to chat_path(@chat) }
|
||||
format.turbo_stream { render turbo_stream: [
|
||||
turbo_stream.append("messages", partial: "chats/thinking_indicator", locals: { chat: @chat }),
|
||||
turbo_stream.remove("chat-error")
|
||||
] }
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -2,15 +2,17 @@ module Account::Chartable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
class_methods do
|
||||
def balance_series(currency:, period: Period.last_30_days, favorable_direction: "up", view: :balance)
|
||||
def balance_series(currency:, period: Period.last_30_days, favorable_direction: "up", view: :balance, interval: nil)
|
||||
raise ArgumentError, "Invalid view type" unless [ :balance, :cash_balance, :holdings_balance ].include?(view.to_sym)
|
||||
|
||||
series_interval = interval || period.interval
|
||||
|
||||
balances = Account::Balance.find_by_sql([
|
||||
balance_series_query,
|
||||
{
|
||||
start_date: period.start_date,
|
||||
end_date: period.end_date,
|
||||
interval: period.interval,
|
||||
interval: series_interval,
|
||||
target_currency: currency
|
||||
}
|
||||
])
|
||||
@@ -33,7 +35,7 @@ module Account::Chartable
|
||||
Series.new(
|
||||
start_date: period.start_date,
|
||||
end_date: period.end_date,
|
||||
interval: period.interval,
|
||||
interval: series_interval,
|
||||
trend: Trend.new(
|
||||
current: Money.new(balance_value_for(balances.last, view) || 0, currency),
|
||||
previous: Money.new(balance_value_for(balances.first, view) || 0, currency),
|
||||
@@ -124,11 +126,12 @@ module Account::Chartable
|
||||
classification == "asset" ? "up" : "down"
|
||||
end
|
||||
|
||||
def balance_series(period: Period.last_30_days, view: :balance)
|
||||
def balance_series(period: Period.last_30_days, view: :balance, interval: nil)
|
||||
self.class.where(id: self.id).balance_series(
|
||||
currency: currency,
|
||||
period: period,
|
||||
view: view,
|
||||
interval: interval,
|
||||
favorable_direction: favorable_direction
|
||||
)
|
||||
end
|
||||
|
||||
@@ -14,30 +14,31 @@ class Assistant
|
||||
end
|
||||
|
||||
def respond_to(message)
|
||||
sleep artificial_thinking_delay
|
||||
|
||||
provider = get_model_provider(message.ai_model)
|
||||
|
||||
response = provider.chat_response(message, instructions: instructions, available_functions: functions)
|
||||
|
||||
if response.success?
|
||||
Chat.transaction do
|
||||
process_response_artifacts(response.data)
|
||||
stop_thinking
|
||||
|
||||
chat.update!(
|
||||
error: nil,
|
||||
latest_assistant_response_id: response.data.id
|
||||
)
|
||||
end
|
||||
|
||||
chat.broadcast_remove target: "chat-error"
|
||||
else
|
||||
chat.update!(error: "#{response.error.class}: #{response.error.message}")
|
||||
chat.broadcast_append target: "messages", partial: "chats/error", locals: { chat: chat }
|
||||
unless response.success?
|
||||
chat.add_error("#{response.error.class}: #{response.error.message}")
|
||||
end
|
||||
|
||||
Chat.transaction do
|
||||
chat.clear_error
|
||||
process_response_artifacts(response.data)
|
||||
chat.update!(latest_assistant_response_id: response.data.id)
|
||||
end
|
||||
rescue => e
|
||||
chat.add_error("#{e.class}: #{e.message}")
|
||||
end
|
||||
|
||||
private
|
||||
def chat_history
|
||||
chat.messages.where.not(debug: true).ordered
|
||||
def stop_thinking
|
||||
sleep artificial_thinking_delay
|
||||
chat.broadcast_remove target: "thinking-indicator"
|
||||
end
|
||||
|
||||
def process_response_artifacts(data)
|
||||
@@ -123,17 +124,21 @@ class Assistant
|
||||
|
||||
def functions
|
||||
[
|
||||
Assistant::Function::GetTransactions.new(chat.user)
|
||||
Assistant::Function::GetTransactions.new(chat.user),
|
||||
Assistant::Function::GetAccounts.new(chat.user)
|
||||
]
|
||||
|
||||
# Assistant::Function::GetBalanceSheet.new(chat),
|
||||
# Assistant::Function::GetIncomeStatement.new(chat),
|
||||
# Assistant::Function::GetExpenseCategories.new(chat),
|
||||
# Assistant::Function::GetAccountBalances.new(chat),
|
||||
# Assistant::Function::ComparePeriods.new(chat)
|
||||
end
|
||||
|
||||
def preferred_currency
|
||||
Money::Currency.new(chat.user.family.currency)
|
||||
end
|
||||
|
||||
def artificial_thinking_delay
|
||||
1
|
||||
end
|
||||
end
|
||||
|
||||
@@ -10,58 +10,33 @@ class Assistant::Function::GetAccounts < Assistant::Function
|
||||
end
|
||||
|
||||
def call(params = {})
|
||||
account_type = params["account_type"] || "all"
|
||||
balance_sheet = BalanceSheet.new(family)
|
||||
|
||||
{
|
||||
as_of_date: Date.today.to_s,
|
||||
currency: family.currency,
|
||||
accounts: get_accounts_data(balance_sheet, account_type)
|
||||
}
|
||||
end
|
||||
as_of_date: Date.current,
|
||||
accounts: family.accounts.includes(:balances).map do |account|
|
||||
series_start_date = [ account.start_date, 5.years.ago.to_date ].max
|
||||
all_dates = Period.custom(start_date: series_start_date, end_date: Date.current)
|
||||
balance_series = account.balance_series(period: all_dates, interval: "1 month")
|
||||
|
||||
def params_schema
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
account_type: {
|
||||
type: "string",
|
||||
enum: [ "asset", "liability", "all" ],
|
||||
description: "Type of accounts to get balances for"
|
||||
}
|
||||
},
|
||||
required: [ "account_type" ],
|
||||
additionalProperties: false
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def get_accounts_data(balance_sheet, account_type)
|
||||
accounts = case account_type
|
||||
when "asset"
|
||||
balance_sheet.account_groups("asset")
|
||||
when "liability"
|
||||
balance_sheet.account_groups("liability")
|
||||
else
|
||||
balance_sheet.account_groups
|
||||
end
|
||||
|
||||
accounts.flat_map { |group| format_accounts(group.accounts) }
|
||||
end
|
||||
|
||||
def format_accounts(accounts)
|
||||
accounts.map do |account|
|
||||
{
|
||||
name: account.name,
|
||||
balance: account.balance,
|
||||
currency: account.currency,
|
||||
balance_formatted: account.balance_money.format,
|
||||
classification: account.classification,
|
||||
type: account.accountable_type,
|
||||
balance: format_currency(account.balance),
|
||||
classification: account.classification
|
||||
start_date: account.start_date,
|
||||
is_plaid_linked: account.plaid_account_id.present?,
|
||||
is_active: account.is_active,
|
||||
historical_balances: {
|
||||
start_date: balance_series.start_date,
|
||||
end_date: balance_series.end_date,
|
||||
currency: account.currency,
|
||||
interval: balance_series.interval,
|
||||
order: "chronological",
|
||||
balances: balance_series.values.map { |value| { date: value.date, balance_formatted: value.trend.current.format } }
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def format_currency(amount)
|
||||
Money.new(amount, family.currency).format
|
||||
end
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2,6 +2,10 @@ class Assistant::Function::GetTransactions < Assistant::Function
|
||||
include Pagy::Backend
|
||||
|
||||
class << self
|
||||
def default_page_size
|
||||
50
|
||||
end
|
||||
|
||||
def name
|
||||
"get_transactions"
|
||||
end
|
||||
@@ -16,7 +20,7 @@ class Assistant::Function::GetTransactions < Assistant::Function
|
||||
|
||||
- `total_pages`: The total number of pages of results
|
||||
- `page`: The current page of results
|
||||
- `page_size`: The number of results per page (this will always be 50)
|
||||
- `page_size`: The number of results per page (this will always be #{default_page_size})
|
||||
- `total_results`: The total number of results for the given filters
|
||||
|
||||
Simple example (transactions from the last 30 days):
|
||||
@@ -24,7 +28,6 @@ class Assistant::Function::GetTransactions < Assistant::Function
|
||||
```
|
||||
get_transactions({
|
||||
page: 1,
|
||||
page_size: 50,
|
||||
start_date: "#{30.days.ago.to_date}",
|
||||
end_date: "#{Date.current}"
|
||||
})
|
||||
@@ -35,7 +38,6 @@ class Assistant::Function::GetTransactions < Assistant::Function
|
||||
```
|
||||
get_transactions({
|
||||
page: 1,
|
||||
page_size: 50,
|
||||
search: "mcdonalds",
|
||||
accounts: ["Checking", "Savings"],
|
||||
start_date: "#{30.days.ago.to_date}",
|
||||
@@ -59,10 +61,6 @@ class Assistant::Function::GetTransactions < Assistant::Function
|
||||
build_schema(
|
||||
required: [ "order", "page", "page_size" ],
|
||||
properties: {
|
||||
page_size: {
|
||||
const: 50,
|
||||
description: "Number of transactions per page (always 50)"
|
||||
},
|
||||
page: {
|
||||
type: "integer",
|
||||
description: "Page number"
|
||||
@@ -125,7 +123,7 @@ class Assistant::Function::GetTransactions < Assistant::Function
|
||||
end
|
||||
|
||||
def call(params = {})
|
||||
search_params = params.except("order", "page", "page_size")
|
||||
search_params = params.except("order", "page")
|
||||
|
||||
transactions = family.transactions.active.search(search_params).includes(
|
||||
{ entry: :account },
|
||||
@@ -137,7 +135,7 @@ class Assistant::Function::GetTransactions < Assistant::Function
|
||||
ordered_transactions = params["order"] == "asc" ? transactions.chronological : transactions.reverse_chronological
|
||||
|
||||
# By default, we give a small page size to force the AI to use filters effectively and save on tokens
|
||||
pagy, transactions = pagy(ordered_transactions, page: params["page"] || 1, limit: 10)
|
||||
pagy, transactions = pagy(ordered_transactions, page: params["page"] || 1, limit: default_page_size)
|
||||
|
||||
normalized_transactions = transactions.map do |txn|
|
||||
entry = txn.entry
|
||||
@@ -159,8 +157,13 @@ class Assistant::Function::GetTransactions < Assistant::Function
|
||||
transactions: normalized_transactions,
|
||||
total_results: pagy.count,
|
||||
page: pagy.page,
|
||||
page_size: 10,
|
||||
page_size: default_page_size,
|
||||
total_pages: pagy.pages
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
def default_page_size
|
||||
self.class.default_page_size
|
||||
end
|
||||
end
|
||||
|
||||
@@ -27,10 +27,21 @@ class Chat < ApplicationRecord
|
||||
last_message = conversation_messages.ordered.last
|
||||
|
||||
if last_message.present? && last_message.role == "user"
|
||||
update!(error: nil)
|
||||
ask_assistant_later(last_message)
|
||||
end
|
||||
end
|
||||
|
||||
def add_error(message)
|
||||
update! error: message
|
||||
broadcast_append target: "messages", partial: "chats/error", locals: { chat: self }
|
||||
end
|
||||
|
||||
def clear_error
|
||||
update! error: nil
|
||||
broadcast_remove target: "chat-error"
|
||||
end
|
||||
|
||||
def assistant
|
||||
@assistant ||= Assistant.for_chat(self)
|
||||
end
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
class ToolCall::Function < ToolCall
|
||||
validates :function_name, :function_arguments, :function_result, presence: true
|
||||
validates :function_name, :function_result, presence: true
|
||||
validates :function_arguments, presence: true, allow_blank: true
|
||||
end
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<%# locals: (message: "Thinking ...") -%>
|
||||
<%# locals: (chat:, message: "Thinking ...") -%>
|
||||
|
||||
<div id="thinking-message" class="flex items-start">
|
||||
<div id="thinking-indicator" class="flex items-start">
|
||||
<%= render "chats/ai_avatar" %>
|
||||
<p class="text-sm text-secondary animate-pulse"><%= message %></p>
|
||||
</div>
|
||||
5
app/views/chats/create.turbo_stream.erb
Normal file
5
app/views/chats/create.turbo_stream.erb
Normal file
@@ -0,0 +1,5 @@
|
||||
<%= turbo_stream.replace "chat-form" do %>
|
||||
<%= render "messages/chat_form", chat: @chat %>
|
||||
<% end %>
|
||||
|
||||
<%= turbo_stream.append "messages", partial: "chats/thinking_indicator", locals: { chat: @chat } %>
|
||||
@@ -1,26 +0,0 @@
|
||||
<%# locals: (content:) %>
|
||||
|
||||
<div class="bg-gray-50 border border-gray-200 rounded-lg p-3 w-full text-gray-700 text-xs font-mono">
|
||||
<div class="flex items-center mb-1">
|
||||
<div class="mr-1"><%= icon("terminal", size: 14) %></div>
|
||||
<div class="font-semibold"><%= content.split("\n").first %></div>
|
||||
</div>
|
||||
<% if content.include?("```json") %>
|
||||
<%
|
||||
json_start = content.index("```json")
|
||||
json_end = content.index("```", json_start + 7)
|
||||
if json_start && json_end
|
||||
json_content = content[(json_start + 7)...json_end].strip
|
||||
begin
|
||||
parsed_json = JSON.parse(json_content)
|
||||
formatted_json = JSON.pretty_generate(parsed_json)
|
||||
rescue
|
||||
formatted_json = json_content
|
||||
end
|
||||
end
|
||||
%>
|
||||
<div class="mt-2 overflow-x-auto">
|
||||
<pre class="text-xs"><%= formatted_json %></pre>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
@@ -1,3 +1,5 @@
|
||||
<%= turbo_stream.replace "chat-form" do %>
|
||||
<%= render "messages/chat_form", chat: @chat %>
|
||||
<% end %>
|
||||
|
||||
<%= turbo_stream.append "messages", partial: "chats/thinking_indicator", locals: { chat: @chat } %>
|
||||
|
||||
Reference in New Issue
Block a user