discourse-ai/lib/completions/dialects/claude.rb

171 lines
4.9 KiB
Ruby

# frozen_string_literal: true
module DiscourseAi
module Completions
module Dialects
class Claude < Dialect
class << self
def can_translate?(llm_model)
llm_model.provider == "anthropic" ||
(llm_model.provider == "aws_bedrock") &&
(llm_model.name.include?("anthropic") || llm_model.name.include?("claude"))
end
end
class ClaudePrompt
attr_reader :system_prompt
attr_reader :messages
attr_reader :tools
def initialize(system_prompt, messages, tools)
@system_prompt = system_prompt
@messages = messages
@tools = tools
end
def has_tools?
tools.present?
end
end
def translate
messages = super
system_prompt = messages.shift[:content] if messages.first[:role] == "system"
if !system_prompt && !native_tool_support?
system_prompt = tools_dialect.instructions.presence
end
interleving_messages = []
previous_message = nil
messages.each do |message|
if previous_message
if previous_message[:role] == "user" && message[:role] == "user"
interleving_messages << { role: "assistant", content: "OK" }
elsif previous_message[:role] == "assistant" && message[:role] == "assistant"
interleving_messages << { role: "user", content: "OK" }
end
end
interleving_messages << message
previous_message = message
end
tools = nil
tools = tools_dialect.translated_tools if native_tool_support?
ClaudePrompt.new(system_prompt.presence, interleving_messages, tools)
end
def max_prompt_tokens
llm_model.max_prompt_tokens
end
def native_tool_support?
!llm_model.lookup_custom_param("disable_native_tools")
end
private
def tools_dialect
if native_tool_support?
@tools_dialect ||= DiscourseAi::Completions::Dialects::ClaudeTools.new(prompt.tools)
else
super
end
end
def tool_call_msg(msg)
translated = tools_dialect.from_raw_tool_call(msg)
{ role: "assistant", content: translated }
end
def tool_msg(msg)
translated = tools_dialect.from_raw_tool(msg)
{ role: "user", content: translated }
end
def model_msg(msg)
content_array = []
if msg[:thinking] || msg[:redacted_thinking_signature]
if msg[:thinking]
content_array << {
type: "thinking",
thinking: msg[:thinking],
signature: msg[:thinking_signature],
}
end
if msg[:redacted_thinking_signature]
content_array << {
type: "redacted_thinking",
data: msg[:redacted_thinking_signature],
}
end
end
# other encoder is used to pass through thinking
content_array =
to_encoded_content_array(
content: [content_array, msg[:content]].flatten,
image_encoder: ->(details) {},
text_encoder: ->(text) { { type: "text", text: text } },
other_encoder: ->(details) { details },
allow_vision: false,
)
{ role: "assistant", content: no_array_if_only_text(content_array) }
end
def system_msg(msg)
msg = { role: "system", content: msg[:content] }
if tools_dialect.instructions.present?
msg[:content] = msg[:content].dup << "\n\n#{tools_dialect.instructions}"
end
msg
end
def user_msg(msg)
content_array = []
content_array << "#{msg[:id]}: " if msg[:id]
content_array.concat([msg[:content]].flatten)
content_array =
to_encoded_content_array(
content: content_array,
image_encoder: ->(details) { image_node(details) },
text_encoder: ->(text) { { type: "text", text: text } },
allow_vision: vision_support?,
)
{ role: "user", content: no_array_if_only_text(content_array) }
end
# keeping our payload as backward compatible as possible
def no_array_if_only_text(content_array)
if content_array.length == 1 && content_array.first[:type] == "text"
content_array.first[:text]
else
content_array
end
end
def image_node(details)
{
source: {
type: "base64",
data: details[:base64],
media_type: details[:mime_type],
},
type: "image",
}
end
end
end
end
end