Files
sure/app/models/assistant/builtin.rb
Guillem Arias 66753319a7 fix(ai): address local review on Anthropic foundation
- Provider::Anthropic#supports_pdf_processing? bypasses prefix gate for
  custom endpoints, mirroring supports_model?
- Provider::Anthropic#initialize raises Error when custom_endpoint? AND
  model.blank?, parity with Provider::Openai
- stream_chat_response captures partial usage on mid-stream errors and
  records it via the new on_partial callback so chat_response can skip
  the duplicate error row in the outer rescue
- safe_accumulated_message swallows the secondary failure when the SDK
  cannot reconstruct a snapshot
- langfuse_client memoizes properly (||= instead of =) so repeated calls
  don't churn Langfuse instances
- MessageFormatter sorts tool_calls by created_at then id so the
  message array is deterministic across replays; skips tool_calls
  missing both provider_call_id and provider_id rather than sending
  `id: nil` and getting rejected by Anthropic
- Setting.anthropic_access_token default falls back through
  ENV["ANTHROPIC_API_KEY"].presence (was missing .presence, so an
  empty-string env value bled through)
- User#openai_configured? / #anthropic_configured? delegate to the
  Provider::* class methods — single source of truth
- Assistant::Responder renames the OpenAI-shape history builder
  conversation_history → openai_messages_payload so the kwarg name
  matches the local method name (messages: openai_messages_payload,
  conversation_history: chat_message_records)
- Assistant::Builtin stale-history comment updated to reference both
  builders

Adds a streaming chat_response test using ad-hoc subclasses of the
SDK event types so the case/when dispatch matches via is_a? without
stubbing class-level === behavior.
2026-05-25 20:27:59 +02:00

97 lines
3.0 KiB
Ruby

class Assistant::Builtin < Assistant::Base
include Assistant::Provided
include Assistant::Configurable
attr_reader :instructions
class << self
def for_chat(chat)
config = config_for(chat)
new(chat, instructions: config[:instructions], functions: config[:functions])
end
end
def initialize(chat, instructions: nil, functions: [])
super(chat)
@instructions = instructions
@functions = functions
end
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
raise StandardError, build_no_provider_error_message(message.ai_model)
end
responder = Assistant::Responder.new(
message: message,
instructions: instructions,
function_tool_caller: function_tool_caller,
llm: llm_provider
)
latest_response_id = chat.latest_assistant_response_id
responder.on(:output_text) do |text|
if assistant_message.content.blank?
Chat.transaction do
assistant_message.append_text!(text)
chat.update_latest_response!(latest_response_id)
end
else
assistant_message.append_text!(text)
end
end
responder.on(:response) do |data|
if data[:function_tool_calls].present?
assistant_message.tool_calls = data[:function_tool_calls]
latest_response_id = data[:id]
else
chat.update_latest_response!(data[:id])
end
end
responder.respond(previous_response_id: latest_response_id)
rescue => e
if assistant_message&.persisted?
if assistant_message.content.blank?
assistant_message.destroy
else
# Demote partially-streamed turns to `failed` so the responder's history builders (`#openai_messages_payload`, `#chat_message_records`) exclude them.
assistant_message.update_columns(status: "failed")
end
end
chat.add_error(e)
end
private
attr_reader :functions
def function_tool_caller
@function_tool_caller ||= Assistant::FunctionToolCaller.new(
functions.map { |fn| fn.new(chat.user) }
)
end
def build_no_provider_error_message(requested_model)
available_providers = registry.providers
if available_providers.empty?
"No LLM provider configured that supports model '#{requested_model}'. " \
"Please configure an LLM provider (e.g., OpenAI) in settings."
else
provider_details = available_providers.map do |provider|
" - #{provider.provider_name}: #{provider.supported_models_description}"
end.join("\n")
"No LLM provider configured that supports model '#{requested_model}'.\n\n" \
"Available providers:\n#{provider_details}\n\n" \
"Please either:\n" \
" 1. Use a supported model from the list above, or\n" \
" 2. Configure a provider that supports '#{requested_model}' in settings."
end
end
end