mirror of
https://github.com/we-promise/sure.git
synced 2026-06-01 16:59:03 +00:00
Introduces Provider::Anthropic alongside Provider::Openai, implementing the LlmConcept chat_response contract over the official anthropic Ruby SDK. Batch ops, PDF, and RAG land in follow-up PRs. - Provider::Anthropic uses Messages API for sync and streaming responses - ChatConfig builds requests with ephemeral prompt-cache markers on the system prompt and the last tool definition - MessageFormatter reconstructs multi-turn history (text + tool_use + tool_result blocks) from raw Message records, including the paired user-role tool_result turn Anthropic requires after every tool_use - ChatParser maps Anthropic Message into the shared ChatResponse Data - Registry, Setting, User, Chat default model wired for ANTHROPIC_* envs and Setting.anthropic_*; LLM_PROVIDER selects between providers - Responder forwards raw conversation_history (Array<Message>) so providers without hosted conversation state can rebuild context - OpenAI provider accepts and ignores the new kwarg (no behavior change) Tests cover provider init, model gating, MessageFormatter for all turn shapes, ChatConfig request building (max_tokens, system cache, tool conversion), ChatParser for text / tool_use / mixed blocks, Registry discovery, and mocked chat_response success / error / function_request paths. Live VCR cassettes recorded in a follow-up with a real key. Stacked PRs: 2/5 batch ops + cost ledger, 3/5 PDF, 4/5 pgvector RAG, 5/5 settings UI + disclosure.
75 lines
1.7 KiB
Ruby
75 lines
1.7 KiB
Ruby
class Provider::Anthropic::ChatParser
|
|
Error = Class.new(StandardError)
|
|
|
|
def initialize(message)
|
|
@message = message
|
|
end
|
|
|
|
def parsed
|
|
ChatResponse.new(
|
|
id: response_id,
|
|
model: response_model,
|
|
messages: messages,
|
|
function_requests: function_requests
|
|
)
|
|
end
|
|
|
|
private
|
|
ChatResponse = Provider::LlmConcept::ChatResponse
|
|
ChatMessage = Provider::LlmConcept::ChatMessage
|
|
ChatFunctionRequest = Provider::LlmConcept::ChatFunctionRequest
|
|
|
|
attr_reader :message
|
|
|
|
def response_id
|
|
message.id
|
|
end
|
|
|
|
def response_model
|
|
message.model.to_s
|
|
end
|
|
|
|
def messages
|
|
text_blocks = content_blocks.select { |block| block_type(block) == :text }
|
|
return [] if text_blocks.empty?
|
|
|
|
[
|
|
ChatMessage.new(
|
|
id: response_id,
|
|
output_text: text_blocks.map { |b| block_value(b, :text) }.compact.join("\n")
|
|
)
|
|
]
|
|
end
|
|
|
|
def function_requests
|
|
content_blocks
|
|
.select { |block| block_type(block) == :tool_use }
|
|
.map do |block|
|
|
input = block_value(block, :input)
|
|
ChatFunctionRequest.new(
|
|
id: block_value(block, :id),
|
|
call_id: block_value(block, :id),
|
|
function_name: block_value(block, :name),
|
|
function_args: input.is_a?(String) ? input : input.to_json
|
|
)
|
|
end
|
|
end
|
|
|
|
def content_blocks
|
|
Array(message.content)
|
|
end
|
|
|
|
def block_type(block)
|
|
raw = block.respond_to?(:type) ? block.type : block[:type] || block["type"]
|
|
raw.to_s.to_sym
|
|
end
|
|
|
|
def block_value(block, key)
|
|
if block.respond_to?(key)
|
|
block.public_send(key)
|
|
elsif block.is_a?(Hash)
|
|
block[key] || block[key.to_s]
|
|
end
|
|
end
|
|
end
|