Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions examples/azure_enterprise_auth.rb
Original file line number Diff line number Diff line change
@@ -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
159 changes: 90 additions & 69 deletions lib/intelligence/adapters/azure.rb
Original file line number Diff line number Diff line change
@@ -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
Expand Down