Class: Boxcars::IntelligenceBase

Inherits:
Engine
  • Object
show all
Includes:
UnifiedObservability
Defined in:
lib/boxcars/engine/intelligence_base.rb

Overview

A Base class for all Intelligence Engines

Direct Known Subclasses

Cerebras, Google, Together

Instance Attribute Summary collapse

Attributes inherited from Engine

#batch_size, #prompts, #user_id

Instance Method Summary collapse

Methods inherited from Engine

#extract_answer, #generate, #generation_info, #get_num_tokens

Constructor Details

#initialize(provider:, description:, name:, prompts: [], batch_size: 20, **kwargs) ⇒ IntelligenceBase

The base Intelligence Engine is used by other engines to generate output from prompts

Parameters:

  • provider (String)

    The provider of the Engine implemented by the Intelligence gem.

  • name (String)

    The name of the Engine. Defaults to classname.

  • description (String)

    A description of the Engine.

  • prompts (Array<Prompt>) (defaults to: [])

    The prompts to use for the Engine.

  • batch_size (Integer) (defaults to: 20)

    The number of prompts to send to the Engine at a time.

  • kwargs (Hash)

    Additional parameters to pass to the Engine.



20
21
22
23
24
25
26
# File 'lib/boxcars/engine/intelligence_base.rb', line 20

def initialize(provider:, description:, name:, prompts: [], batch_size: 20, **kwargs)
  user_id = kwargs.delete(:user_id)
  @provider = provider
  # Start with defaults, merge other kwargs, then explicitly set model if provided in initialize
  @all_params = default_model_params.merge(kwargs)
  super(description:, name:, prompts:, batch_size:, user_id:)
end

Instance Attribute Details

#all_paramsObject (readonly)

Returns the value of attribute all_params.



11
12
13
# File 'lib/boxcars/engine/intelligence_base.rb', line 11

def all_params
  @all_params
end

#providerObject (readonly)

Returns the value of attribute provider.



11
12
13
# File 'lib/boxcars/engine/intelligence_base.rb', line 11

def provider
  @provider
end

Instance Method Details

#adapter(params:, api_key:) ⇒ Object



37
38
39
40
41
42
# File 'lib/boxcars/engine/intelligence_base.rb', line 37

def adapter(params:, api_key:)
  Intelligence::Adapter.build! @provider do |config|
    config.key api_key
    config.chat_options params
  end
end

#client(prompt:, inputs: {}, api_key: nil, **kwargs) ⇒ Object

Get an answer from the engine

Raises:



66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
# File 'lib/boxcars/engine/intelligence_base.rb', line 66

def client(prompt:, inputs: {}, api_key: nil, **kwargs)
  params = all_params.merge(kwargs)
  api_key ||= lookup_provider_api_key(params:)
  raise Error, "No API key found for #{provider}" unless api_key

  adapter = adapter(api_key:, params:)
  convo = prompt.as_intelligence_conversation(inputs:)
  request_context = { user_id:, prompt: prompt&.as_prompt(inputs:)&.[](:prompt), inputs:, conversation_for_api: convo.to_h }
  request = Intelligence::ChatRequest.new(adapter:)

  start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
  response_obj = nil

  begin
    response_obj = request.chat(convo)
    duration_ms = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time) * 1000

    if response_obj.success?
      success = true
      parsed_json_response = JSON.parse(response_obj.body)
      response_data = { success:, parsed_json: parsed_json_response, response_obj:,
                        status_code: response_obj.status }
      track_ai_generation(duration_ms:, current_params: params, request_context:, response_data:, provider:)
      parsed_json_response
    else
      success = false
      error_message = response_obj&.reason_phrase || "No response from API #{provider}"
      response_data = { success:, error: StandardError.new(error_message), response_obj:,
                        status_code: response_obj.status }
      track_ai_generation(duration_ms:, current_params: params, request_context:, response_data:, provider:)
      raise Error, error_message
    end
  rescue Error => e
    # Re-raise Error exceptions (like the one above) without additional tracking
    # since they were already tracked in the else branch
    Boxcars.error("#{provider} Error: #{e.message}", :red)
    raise
  rescue StandardError => e
    duration_ms = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time) * 1000
    success = false
    error_obj = e
    response_data = { success:, error: error_obj, response_obj:, status_code: response_obj&.status }
    track_ai_generation(duration_ms:, current_params: params, request_context:, response_data:, provider:)
    Boxcars.error("#{provider} Error: #{e.message}", :red)
    raise
  end
end

#default_model_paramsObject

can be overridden by provider subclass



29
30
31
# File 'lib/boxcars/engine/intelligence_base.rb', line 29

def default_model_params
  {}
end

#lookup_provider_api_key(params:) ⇒ Object

Raises:

  • (NotImplementedError)


33
34
35
# File 'lib/boxcars/engine/intelligence_base.rb', line 33

def lookup_provider_api_key(params:)
  raise NotImplementedError, "lookup_provider_api_key method must be implemented by subclass"
end

#process_content(content) ⇒ Object

Process different content types



45
46
47
48
49
50
51
52
53
54
55
56
# File 'lib/boxcars/engine/intelligence_base.rb', line 45

def process_content(content)
  case content
  when String
    { type: "text", text: content }
  when Hash
    validate_content(content)
  when Array
    content.map { |c| process_content(c) }
  else
    raise ArgumentError, "Unsupported content type: #{content.class}"
  end
end

#run(question) ⇒ Object

Run the engine with a question



115
116
117
118
119
# File 'lib/boxcars/engine/intelligence_base.rb', line 115

def run(question, **)
  prompt = Prompt.new(template: question)
  response = client(prompt:, **)
  extract_answer(response)
end

#validate_content(content) ⇒ Object

Validate content structure

Raises:



59
60
61
62
63
# File 'lib/boxcars/engine/intelligence_base.rb', line 59

def validate_content(content)
  raise ArgumentError, "Content must have type and text fields" unless content[:type] && content[:text]

  content
end