mirror of
https://github.com/we-promise/sure.git
synced 2026-05-30 15:59:02 +00:00
- 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.
97 lines
3.0 KiB
Ruby
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
|