diff --git a/examples/azure_enterprise_auth.rb b/examples/azure_enterprise_auth.rb new file mode 100644 index 0000000..5152953 --- /dev/null +++ b/examples/azure_enterprise_auth.rb @@ -0,0 +1,57 @@ +require 'intelligence' + +# Example: Azure OpenAI with Enterprise Authentication (OAuth 2.0 Client Credentials) +# This demonstrates how to use Azure Active Directory for enterprise authentication + +# Configuration using enterprise credentials +azure_enterprise = Intelligence::Adapter.build! :azure do + base_uri ENV['AZURE_OPENAI_ENDPOINT'] + + # Enterprise authentication (preferred for enterprise environments) + tenant_id ENV['AZURE_TENANT_ID'] + client_id ENV['AZURE_CLIENT_ID'] + client_secret ENV['AZURE_CLIENT_SECRET'] + + api_version '2024-10-21' + + chat_options do + model 'gpt-5' # Azure deployment name + max_tokens 256 + temperature 0.7 + end +end + +# Alternative: Using API key authentication (fallback) +azure_api_key = Intelligence::Adapter.build! :azure do + base_uri ENV['AZURE_OPENAI_ENDPOINT'] + key ENV['AZURE_OPENAI_API_KEY'] + api_version '2024-10-21' + + chat_options do + model 'gpt-5' + max_tokens 256 + temperature 0.7 + end +end + +# Test the enterprise authentication +conversation = Intelligence::Conversation.build do + system_message do + content text: "You are a helpful AI assistant." + end + + message do + role :user + content text: "Explain the benefits of using enterprise authentication with Azure OpenAI." + end +end + +request = Intelligence::ChatRequest.new(adapter: azure_enterprise) +response = request.chat(conversation) + +if response.success? + puts "Enterprise Authentication Response:" + puts response.result.text +else + puts "Error: #{response.result.error_description}" +end \ No newline at end of file diff --git a/lib/intelligence/adapters/azure.rb b/lib/intelligence/adapters/azure.rb index 56d1453..9e2520c 100644 --- a/lib/intelligence/adapters/azure.rb +++ b/lib/intelligence/adapters/azure.rb @@ -1,90 +1,111 @@ -require_relative 'generic' +require_relative 'generic/adapter' +require 'net/http' +require 'uri' +require 'json' module Intelligence module Azure - class Adapter < Generic::Adapter - CHAT_COMPLETIONS_PATH = 'chat/completions' - - schema do - # normalized properties, used by all endpoints - base_uri String, required: true - key String - api_version String, required: true, default: '2025-01-01-preview' - - # properties for generative text endpoints - chat_options do - - # normalized properties for openai generative text endpoint - model String, requried: true - max_tokens Integer, in: (0...) - temperature Float, in: (0..1) - top_p Float, in: (0..1) - seed Integer - stop String, array: true - stream [ TrueClass, FalseClass ] - - frequency_penalty Float, in: (-2..2) - presence_penalty Float, in: (-2..2) - - modalities String, array: true - response_format do - # 'text' and 'json_schema' are the only supported types - type Symbol, in: [ :text, :json_schema ] - json_schema - end - - # tools - tool array: true, as: :tools, &Tool.schema - # tool choice configuration - # - # `tool_choice :none` - # or - # ``` - # tool_choice :function do - # function :my_function - # end - # ``` - tool_choice arguments: :type do - type Symbol, in: [ :none, :auto, :required ] - function arguments: :name do - name Symbol - end - end - # the parallel_tool_calls parameter is only allowed when 'tools' are specified - parallel_tool_calls [ TrueClass, FalseClass ] - - end - end + class Adapter < Generic::Adapter + + CHAT_COMPLETIONS_PATH = 'openai/deployments' + + schema do + base_uri String, required: true + key String + tenant_id String + client_id String + client_secret String + api_version String, required: true, default: '2024-10-21' + + chat_options do + model String, required: true + max_tokens Integer, as: :max_completion_tokens + temperature Float, in: (0..1) + top_p Float, in: (0..1) + seed Integer + stop String, array: true + stream [ TrueClass, FalseClass ] + + frequency_penalty Float, in: (-2..2) + presence_penalty Float, in: (-2..2) + + modalities String, array: true + response_format do + type Symbol, in: [ :text, :json_schema ] + json_schema + end + + max_completion_tokens Integer + + tool array: true, as: :tools, &Tool.schema + tool_choice arguments: :type do + type Symbol, in: [ :none, :auto, :required ] + function arguments: :name do + name Symbol + end + end + parallel_tool_calls [ TrueClass, FalseClass ] + + end + end + + def fetch_azure_access_token(tenant_id, client_id, client_secret) + uri = URI("https://login.microsoftonline.com/#{tenant_id}/oauth2/v2.0/token") + res = Net::HTTP.post_form(uri, { + 'client_id' => client_id, + 'client_secret' => client_secret, + 'scope' => 'https://cognitiveservices.azure.com/.default', + 'grant_type' => 'client_credentials' + }) + raise res.body unless res.is_a?(Net::HTTPSuccess) + JSON.parse(res.body)['access_token'] + end def chat_request_uri( options = nil ) options = merge_options( @options, build_options( options ) ) base_uri = options[ :base_uri ] - if base_uri - # because URI join is dumb - base_uri = ( base_uri.end_with?( '/' ) ? base_uri : base_uri + '/' ) - uri = URI.join( base_uri, CHAT_COMPLETIONS_PATH ) + model = options.dig( :chat_options, :model ) + api_version = options[ :api_version ] - api_version = options[ :api_version ] || options[ 'api-version' ] - uri.query = [ uri.query, "api-version=#{ api_version }" ].compact.join( '&' ) + raise ArgumentError.new( "An Azure base_uri is required to build an Azure chat request." ) \ + if base_uri.nil? + raise ArgumentError.new( "A model is required to build an Azure chat request." ) \ + if model.nil? - uri - else - raise 'The Azure adapter requires a base_uri.' - end + base_uri = ( base_uri.end_with?( '/' ) ? base_uri : base_uri + '/' ) + "#{base_uri}#{CHAT_COMPLETIONS_PATH}/#{model}/chat/completions?api-version=#{api_version}" end def chat_request_headers( options = {} ) options = merge_options( @options, build_options( options ) ) result = {} - key = options[ :key ] + tenant_id = options[ :tenant_id ] + client_id = options[ :client_id ] + client_secret = options[ :client_secret ] - raise ArgumentError.new( "An Azure key is required to build an Azure request." ) \ - if key.nil? + if tenant_id && client_id && client_secret + token = fetch_azure_access_token(tenant_id, client_id, client_secret) + result[ 'Authorization' ] = "Bearer #{token}" + else + key = options[ :key ] + raise ArgumentError.new( "An Azure key is required to build an Azure request." ) \ + if key.nil? + result[ 'api-key' ] = key + end result[ 'Content-Type' ] = 'application/json' - result[ 'api-key' ] = key - result + result + end + + def chat_request_body( conversation, options = nil ) + tools = options&.delete( :tools ) || [] + options = merge_options( @options, build_options( options ) ) + + chat_options = options[ :chat_options ]&.dup || {} + chat_options.delete( :model ) + + super( conversation, { tools: tools }.merge( options.merge( chat_options: chat_options ) ) ) end end