diff --git a/app/controllers/chats_controller.rb b/app/controllers/chats_controller.rb index 6b8b494a7..eb3e2c5c7 100644 --- a/app/controllers/chats_controller.rb +++ b/app/controllers/chats_controller.rb @@ -43,7 +43,7 @@ class ChatsController < ApplicationController def retry @chat.retry_last_message! - redirect_to chat_path(@chat, thinking: true) + redirect_to chat_path(@chat) end private diff --git a/app/jobs/assistant_response_job.rb b/app/jobs/assistant_response_job.rb index 70664f02b..36a6c0e84 100644 --- a/app/jobs/assistant_response_job.rb +++ b/app/jobs/assistant_response_job.rb @@ -1,7 +1,7 @@ class AssistantResponseJob < ApplicationJob queue_as :high_priority - def perform(message) - message.request_response + def perform(message, assistant_message = nil) + message.request_response(assistant_message: assistant_message) end end diff --git a/app/models/assistant/base.rb b/app/models/assistant/base.rb index 2b77671af..42bd69397 100644 --- a/app/models/assistant/base.rb +++ b/app/models/assistant/base.rb @@ -1,13 +1,11 @@ class Assistant::Base - include Assistant::Broadcastable - attr_reader :chat def initialize(chat) @chat = chat end - def respond_to(message) + def respond_to(message, assistant_message: nil) raise NotImplementedError, "#{self.class}#respond_to must be implemented" end end diff --git a/app/models/assistant/broadcastable.rb b/app/models/assistant/broadcastable.rb deleted file mode 100644 index 7fd2507b5..000000000 --- a/app/models/assistant/broadcastable.rb +++ /dev/null @@ -1,12 +0,0 @@ -module Assistant::Broadcastable - extend ActiveSupport::Concern - - private - def update_thinking(thought) - chat.broadcast_update target: "thinking-indicator", partial: "chats/thinking_indicator", locals: { chat: chat, message: thought } - end - - def stop_thinking - chat.broadcast_remove target: "thinking-indicator" - end -end diff --git a/app/models/assistant/builtin.rb b/app/models/assistant/builtin.rb index 14bc9c05d..6a1ae93c9 100644 --- a/app/models/assistant/builtin.rb +++ b/app/models/assistant/builtin.rb @@ -17,12 +17,8 @@ class Assistant::Builtin < Assistant::Base @functions = functions end - def respond_to(message) - assistant_message = AssistantMessage.new( - chat: chat, - content: "", - ai_model: message.ai_model - ) + def respond_to(message, assistant_message: nil) + assistant_message ||= AssistantMessage.new(chat: chat, content: "", ai_model: message.ai_model) llm_provider = get_model_provider(message.ai_model) unless llm_provider @@ -40,7 +36,6 @@ class Assistant::Builtin < Assistant::Base responder.on(:output_text) do |text| if assistant_message.content.blank? - stop_thinking Chat.transaction do assistant_message.append_text!(text) chat.update_latest_response!(latest_response_id) @@ -51,7 +46,6 @@ class Assistant::Builtin < Assistant::Base end responder.on(:response) do |data| - update_thinking("Analyzing your data...") if data[:function_tool_calls].present? assistant_message.tool_calls = data[:function_tool_calls] latest_response_id = data[:id] @@ -62,13 +56,13 @@ class Assistant::Builtin < Assistant::Base responder.respond(previous_response_id: latest_response_id) rescue => e - stop_thinking - # If we streamed any partial content before the error, the message was - # persisted with the default `complete` status. Demote it to `failed` so - # `Assistant::Responder#conversation_history` won't feed a broken turn - # back into future prompts. if assistant_message&.persisted? - assistant_message.update_columns(status: "failed") + if assistant_message.content.blank? + assistant_message.destroy + else + # Demote partially-streamed turns to `failed` so `Responder#conversation_history` excludes them. + assistant_message.update_columns(status: "failed") + end end chat.add_error(e) end diff --git a/app/models/assistant/external.rb b/app/models/assistant/external.rb index a64888a6e..f2e200641 100644 --- a/app/models/assistant/external.rb +++ b/app/models/assistant/external.rb @@ -33,8 +33,9 @@ class Assistant::External < Assistant::Base end end - def respond_to(message) + def respond_to(message, assistant_message: nil) response_completed = false + assistant_message ||= AssistantMessage.new(chat: chat, content: "", ai_model: "external-agent") unless self.class.configured? raise Assistant::Error, @@ -45,12 +46,6 @@ class Assistant::External < Assistant::Base raise Assistant::Error, "Your account is not authorized to use the external assistant." end - assistant_message = AssistantMessage.new( - chat: chat, - content: "", - ai_model: "external-agent" - ) - client = build_client messages = build_conversation_messages @@ -58,17 +53,10 @@ class Assistant::External < Assistant::Base messages: messages, user: "sure-family-#{chat.user.family_id}" ) do |text| - if assistant_message.content.blank? - stop_thinking - assistant_message.content = text - assistant_message.save! - else - assistant_message.append_text!(text) - end + assistant_message.append_text!(text) end - if assistant_message.new_record? - stop_thinking + if assistant_message.content.blank? raise Assistant::Error, "External assistant returned an empty response." end @@ -76,12 +64,10 @@ class Assistant::External < Assistant::Base assistant_message.update!(ai_model: model) if model.present? rescue Assistant::Error, ActiveRecord::ActiveRecordError => e cleanup_partial_response(assistant_message) unless response_completed - stop_thinking chat.add_error(e) rescue => e Rails.logger.error("[Assistant::External] Unexpected error: #{e.class} - #{e.message}") cleanup_partial_response(assistant_message) unless response_completed - stop_thinking chat.add_error(Assistant::Error.new("Something went wrong with the external assistant. Check server logs for details.")) end @@ -103,7 +89,7 @@ class Assistant::External < Assistant::Base end def build_conversation_messages - chat.conversation_messages.ordered.last(MAX_CONVERSATION_MESSAGES).map do |msg| + chat.conversation_messages.where(status: "complete").ordered.last(MAX_CONVERSATION_MESSAGES).map do |msg| { role: msg.role, content: msg.content } end end diff --git a/app/models/assistant_message.rb b/app/models/assistant_message.rb index 4b1a1404a..a40304d2c 100644 --- a/app/models/assistant_message.rb +++ b/app/models/assistant_message.rb @@ -7,6 +7,7 @@ class AssistantMessage < Message def append_text!(text) self.content += text + self.status = :complete if pending? save! end end diff --git a/app/models/chat.rb b/app/models/chat.rb index 9345c9916..1198ee4be 100644 --- a/app/models/chat.rb +++ b/app/models/chat.rb @@ -79,7 +79,7 @@ class Chat < ApplicationRecord def add_error(e) update!(error: build_error_payload(e).to_json) - broadcast_append target: "messages", partial: "chats/error", locals: { chat: self } + broadcast_append target: messages_target, partial: "chats/error", locals: { chat: self } end def presentable_error_message @@ -93,20 +93,29 @@ class Chat < ApplicationRecord def clear_error update! error: nil - broadcast_remove target: "chat-error" + broadcast_remove target: error_target end def conversation_messages messages.where(type: [ "UserMessage", "AssistantMessage" ]) end - def ask_assistant_later(message) - clear_error - AssistantResponseJob.perform_later(message) + def messages_target + ActionView::RecordIdentifier.dom_id(self, :messages) end - def ask_assistant(message) - assistant.respond_to(message) + def error_target + ActionView::RecordIdentifier.dom_id(self, :chat_error) + end + + def ask_assistant_later(message) + clear_error + pending = messages.create!(type: "AssistantMessage", content: "", ai_model: message.ai_model, status: :pending) + AssistantResponseJob.perform_later(message, pending) + end + + def ask_assistant(message, assistant_message: nil) + assistant.respond_to(message, assistant_message: assistant_message) end private diff --git a/app/models/message.rb b/app/models/message.rb index 4bf5e9c00..736ac8785 100644 --- a/app/models/message.rb +++ b/app/models/message.rb @@ -8,9 +8,9 @@ class Message < ApplicationRecord failed: "failed" } - validates :content, presence: true + validates :content, presence: true, unless: :pending? - after_create_commit -> { broadcast_append_to chat, target: "messages" }, if: :broadcast? + after_create_commit -> { broadcast_append_to chat, target: chat.messages_target }, if: :broadcast? after_update_commit -> { broadcast_update_to chat }, if: :broadcast? scope :ordered, -> { order(created_at: :asc) } diff --git a/app/models/user_message.rb b/app/models/user_message.rb index 5a123120d..865550a4a 100644 --- a/app/models/user_message.rb +++ b/app/models/user_message.rb @@ -11,7 +11,7 @@ class UserMessage < Message chat.ask_assistant_later(self) end - def request_response - chat.ask_assistant(self) + def request_response(assistant_message: nil) + chat.ask_assistant(self, assistant_message: assistant_message) end end diff --git a/app/views/assistant_messages/_assistant_message.html.erb b/app/views/assistant_messages/_assistant_message.html.erb index 59356a788..9768ee0d0 100644 --- a/app/views/assistant_messages/_assistant_message.html.erb +++ b/app/views/assistant_messages/_assistant_message.html.erb @@ -1,7 +1,12 @@ <%# locals: (assistant_message:) %>
<%= t("chats.thinking") %>
+Assistant reasoning
diff --git a/app/views/chats/_error.html.erb b/app/views/chats/_error.html.erb index 4aa20cb70..b1f590b88 100644 --- a/app/views/chats/_error.html.erb +++ b/app/views/chats/_error.html.erb @@ -1,6 +1,6 @@ <%# locals: (chat:) %> -<%= chat.technical_error_message %>
diff --git a/app/views/chats/_thinking_indicator.html.erb b/app/views/chats/_thinking_indicator.html.erb
deleted file mode 100644
index e1ba89217..000000000
--- a/app/views/chats/_thinking_indicator.html.erb
+++ /dev/null
@@ -1,6 +0,0 @@
-<%# locals: (chat:, message: "Thinking ...") -%>
-
-<%= message %>
-