lib/ruby_conversations/client.rb
# frozen_string_literal: true require 'faraday' require 'jwt' require 'logger' module RubyConversations # HTTP client for interacting with the conversations API class Client attr_reader :client PROMPT_ATTRIBUTES = %w[ id name message role temperature valid_placeholders created_at updated_at latest_version_id llm ].freeze # Initialize a new API client # @param url [String] The base URL for the API # @param jwt_secret [String, #call] The JWT secret or a callable that returns a JWT token def initialize(url:, jwt_secret:) @url = url @jwt_secret = jwt_secret @jwt_token = nil @jwt_expiration = nil @logger = Logger.new($stdout) @client = build_client end # Store a conversation or add a message to an existing conversation # @param conversation [Conversation] The conversation to store # @param message [Message] The message to store # @return [Hash] The API response data def store_conversation_or_message(conversation, message) if conversation.id.present? store_message(conversation, message) else store_conversation(conversation) end end # Store a new conversation # @param conversation [Conversation] The conversation to store # @return [Hash] The API response data # @raise [Error] If the API response is missing a conversation ID def store_conversation(conversation) response = client.post('api/ai_conversations', conversation.conversation_attributes_for_storage) data = handle_response(response) raise RubyConversations::Error, 'API response missing conversation ID' unless data['id'].present? conversation.id = data['id'] data end # Store a message in an existing conversation # @param conversation [Conversation] The conversation to add the message to # @param message [Message] The message to store # @return [Hash] The API response data def store_message(conversation, message) response = client.post("api/ai_conversations/#{conversation.id}/ai_messages", { ai_message: message.attributes_for_api }) handle_response(response) end # Fetch a conversation template by name # @param template_name [String] The name of the template to fetch # @return [Hash] The template data including messages and prompts def fetch_conversation_template(template_name) response = client.get("api/ai_conversations/#{template_name}") handle_response(response) end # Fetch all available conversation templates # @return [Array<Hash>] Array of template data including messages and prompts def fetch_all_conversation_templates response = client.get('api/ai_conversations') handle_response(response) end # Fetch a prompt by name # @param name [String] The name of the prompt to fetch # @return [Hash] The prompt attributes def fetch_prompt(name) response = client.get("api/prompts/#{name}") data = handle_response(response) map_prompt_attributes(data) end private def map_prompt_attributes(data) PROMPT_ATTRIBUTES.each_with_object({}) do |attr_name, attrs| attrs[attr_name.to_sym] = data[attr_name] end end def build_client Faraday.new(url: @url) do |faraday| faraday.request :json faraday.response :json faraday.request :authorization, 'Bearer', -> { current_jwt } faraday.adapter Faraday.default_adapter end end def current_jwt refresh_jwt if token_expired? @jwt_token end def token_expired? [@jwt_token, @jwt_expiration].any?(&:nil?) || Time.now.to_i >= @jwt_expiration end def refresh_jwt if @jwt_secret.respond_to?(:call) refresh_callable_jwt else refresh_static_jwt end end def refresh_callable_jwt @jwt_token = @jwt_secret.call decoded_token = JWT.decode(@jwt_token, nil, false).first @jwt_expiration = decoded_token['exp'] end def refresh_static_jwt @jwt_expiration = Time.now.to_i + 3600 # 1 hour payload = { exp: @jwt_expiration, iat: Time.now.to_i } @jwt_token = JWT.encode(payload, @jwt_secret, 'HS256') end def handle_response(response) return response.body if response.success? @logger.error("API request failed: #{response.body.inspect}") raise RubyConversations::ClientError.new("API request failed: #{response.body}", status_code: response.status) end end end