Class: Aireview::Config

Inherits:
Object
  • Object
show all
Includes:
ConfigFallbacks, ConfigJev, ConfigLayers, ConfigLimits
Defined in:
lib/aireview/config.rb

Overview

Answers questions about the merged settings: values, their source (layer), the routing plan, provider keys. How settings are read from files and the environment is ConfigLoader's business.

Constant Summary collapse

DEFAULT_SECRET_FILES =
[
  '.env',
  '.env.*',
  'config/secrets.yml',
  'config/credentials/*.key',
  'spec/fixtures/cassettes/*.yml',
  'spec/fixtures/cassettes/**/*.yml',
  'spec/cassettes/*.yml',
  'spec/cassettes/**/*.yml',
  'test/fixtures/cassettes/*.yml',
  'test/fixtures/cassettes/**/*.yml'
].freeze
REVIEW_MODES =
%w[update once].freeze
DEFAULTS =
{
  'review_language' => 'en',
  'review_mode' => 'update',
  'ignore_paths' => [],
  'secret_patterns' => [],
  'secret_files' => DEFAULT_SECRET_FILES,
  'review_instructions' => nil,
  'llm_api_base' => nil,
  'ollama_api_base' => 'http://localhost:11434/v1',
  'llm_http_proxy' => nil,
  'llm' => {
    'provider' => 'gemini',
    'temperature' => 0,
    'timeout' => 60,
    'max_prompt_chars' => ConfigLimits::DEFAULT_MAX_PROMPT_CHARS,
    'time_budget' => ConfigFallbacks::DEFAULT_TIME_BUDGET,
    'overloaded_quarantine' => ConfigFallbacks::DEFAULT_OVERLOADED_QUARANTINE
  },
  'context' => ConfigLimits::CONTEXT_DEFAULTS
}.freeze

Constants included from ConfigLayers

Aireview::ConfigLayers::BUILT_IN_LAYER, Aireview::ConfigLayers::CLI_LAYER, Aireview::ConfigLayers::DATA_LAYER, Aireview::ConfigLayers::ENV_LAYER, Aireview::ConfigLayers::FILE_LAYER, Aireview::ConfigLayers::IMAGE_LAYER

Constants included from ConfigJev

Aireview::ConfigJev::CRITIQUE_ENGINES, Aireview::ConfigJev::DEFAULT_JEV_MODEL, Aireview::ConfigJev::DEFAULT_JEV_TIMEOUT, Aireview::ConfigJev::JEV_ALIASES, Aireview::ConfigJev::JEV_FALLBACKS, Aireview::ConfigJev::JEV_THRESHOLD_DEFAULTS

Constants included from ConfigFallbacks

Aireview::ConfigFallbacks::DEFAULT_OVERLOADED_QUARANTINE, Aireview::ConfigFallbacks::DEFAULT_TIME_BUDGET, Aireview::ConfigFallbacks::KEYLESS_PROVIDERS, Aireview::ConfigFallbacks::SELF_HOSTED_PLACEHOLDER_KEY

Constants included from ConfigLimits

Aireview::ConfigLimits::CONTEXT_DEFAULTS, Aireview::ConfigLimits::DEFAULT_MAX_PROMPT_CHARS

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from ConfigLayers

#layer_paths, #source_of, #stage_fallbacks_source, #stage_model_source, #stage_provider_source, #stage_provider_warnings, #stage_setting, #stage_setting_layer, #start_inherited?, #warnings

Methods included from ConfigJev

#critique_disabled?, #critique_engine, #jev_api_key, #jev_critique?, #jev_fallback, #jev_model, #jev_shadow?, #jev_signature, #jev_thresholds, #jev_timeout, #jev_warnings, #llm_stages, #require_jev!

Methods included from ConfigFallbacks

#api_key_counts, #candidate_api_keys, #fallback_names, #fallbacks_disabled?, #llm_time_budget, #overloaded_quarantine, #provider_api_keys, #require_llm_configuration!, #require_models!, #routing, #stage_chain

Methods included from ConfigLimits

#max_diff_chars, #max_jira_comment_chars, #max_jira_description_chars, #max_mr_description_chars, #max_prompt_chars

Constructor Details

#initialize(data = nil, config_path: nil, logger: Logger.new($stderr), layers: nil) ⇒ Config

The layers are the single source of truth: the merged data is computed from them. A config built from a hash (without ConfigLoader) is one layer, otherwise the CLI layer from with_overrides would be the only one and the stage settings of the original hash would be lost.



72
73
74
75
76
77
# File 'lib/aireview/config.rb', line 72

def initialize(data = nil, config_path: nil, logger: Logger.new($stderr), layers: nil)
  @layers = layers || [ConfigLayers::Layer.new(name: ConfigLayers::DATA_LAYER, data: Utils.normalize_hash(data))]
  @data = @layers.map(&:data).reduce({}) { |merged, layer_data| Utils.deep_merge(merged, layer_data) }
  @config_path = config_path
  @logger = logger
end

Instance Attribute Details

#config_path ⇒ Object (readonly)

Returns the value of attribute config_path.



58
59
60
# File 'lib/aireview/config.rb', line 58

def config_path
  @config_path
end

#layers ⇒ Object (readonly)

Returns the value of attribute layers.



58
59
60
# File 'lib/aireview/config.rb', line 58

def layers
  @layers
end

Class Method Details

.env_names ⇒ Object



64
65
66
# File 'lib/aireview/config.rb', line 64

def self.env_names
  ConfigLoader.env_names
end

.load(**options) ⇒ Object



60
61
62
# File 'lib/aireview/config.rb', line 60

def self.load(**options)
  ConfigLoader.load(**options)
end

Instance Method Details

#critique_model ⇒ Object

nil when no LLM Critique can run (see llm_stages).



150
151
152
# File 'lib/aireview/config.rb', line 150

def critique_model
  routing.stage?('critique') ? routing.primary('critique').model : nil
end

#critique_provider ⇒ Object



171
172
173
# File 'lib/aireview/config.rb', line 171

def critique_provider
  routing.stage?('critique') ? routing.primary('critique').provider : nil
end

#critique_temperature ⇒ Object



186
187
188
# File 'lib/aireview/config.rb', line 186

def critique_temperature
  stage_setting('critique', 'temperature') || DEFAULTS.dig('llm', 'temperature')
end

#generate_model ⇒ Object

The primary model of a stage is the first in its chain: the start for Generate, the first of the pool for Critique. These go into the review key.



145
146
147
# File 'lib/aireview/config.rb', line 145

def generate_model
  routing.primary('generate').model
end

#generate_provider ⇒ Object



154
155
156
# File 'lib/aireview/config.rb', line 154

def generate_provider
  routing.primary('generate').provider
end

#generate_temperature ⇒ Object



182
183
184
# File 'lib/aireview/config.rb', line 182

def generate_temperature
  stage_setting('generate', 'temperature') || DEFAULTS.dig('llm', 'temperature')
end

#gitlab_token ⇒ Object



115
116
117
# File 'lib/aireview/config.rb', line 115

def gitlab_token
  @data['gitlab_token']
end

#gitlab_url ⇒ Object



111
112
113
# File 'lib/aireview/config.rb', line 111

def gitlab_url
  @data['gitlab_url']
end

#ignore_paths ⇒ Object



216
217
218
# File 'lib/aireview/config.rb', line 216

def ignore_paths
  Array(@data['ignore_paths']).compact
end

#jira_configured? ⇒ Boolean

Returns:

  • (Boolean)


240
241
242
243
244
# File 'lib/aireview/config.rb', line 240

def jira_configured?
  Aireview::Utils.present?(jira_url) &&
    Aireview::Utils.present?() &&
    Aireview::Utils.present?(jira_password)
end

#jira_login ⇒ Object



123
124
125
# File 'lib/aireview/config.rb', line 123

def 
  @data['jira_login']
end

#jira_password ⇒ Object



127
128
129
# File 'lib/aireview/config.rb', line 127

def jira_password
  @data['jira_password']
end

#jira_url ⇒ Object



119
120
121
# File 'lib/aireview/config.rb', line 119

def jira_url
  @data['jira_url']
end

#llm_api_base ⇒ Object



190
191
192
# File 'lib/aireview/config.rb', line 190

def llm_api_base
  @data['llm_api_base']
end

#llm_api_key ⇒ Object



232
233
234
# File 'lib/aireview/config.rb', line 232

def llm_api_key
  @data['llm_api_key']
end

#llm_http_proxy ⇒ Object



198
199
200
# File 'lib/aireview/config.rb', line 198

def llm_http_proxy
  @data['llm_http_proxy'] || dig('llm', 'http_proxy')
end

#llm_provider ⇒ Object



131
132
133
# File 'lib/aireview/config.rb', line 131

def llm_provider
  dig('llm', 'provider') || DEFAULTS.dig('llm', 'provider')
end

#llm_temperature ⇒ Object



135
136
137
# File 'lib/aireview/config.rb', line 135

def llm_temperature
  dig('llm', 'temperature') || DEFAULTS.dig('llm', 'temperature')
end

#llm_timeout ⇒ Object



139
140
141
# File 'lib/aireview/config.rb', line 139

def llm_timeout
  dig('llm', 'timeout') || DEFAULTS.dig('llm', 'timeout')
end

#ollama_api_base ⇒ Object



194
195
196
# File 'lib/aireview/config.rb', line 194

def ollama_api_base
  @data['ollama_api_base'] || DEFAULTS['ollama_api_base']
end

#provider_api_key(provider = llm_provider) ⇒ Object



236
237
238
# File 'lib/aireview/config.rb', line 236

def provider_api_key(provider = llm_provider)
  @data["#{provider}_api_key"] || llm_api_key
end

#require_gitlab_token! ⇒ Object

Raises:



246
247
248
249
250
# File 'lib/aireview/config.rb', line 246

def require_gitlab_token!
  return gitlab_token if Aireview::Utils.present?(gitlab_token)

  raise ConfigError, 'GITLAB_TOKEN is required'
end

#result_signature ⇒ Object

Everything besides the prompt that affects the review result goes into the note key (see ReviewMarker): provider, model and temperature of the stages, Jev as the critique engine, the shared pool with its critique policy. Reserves of per-stage chains do not change the result.



162
163
164
165
166
167
168
169
# File 'lib/aireview/config.rb', line 162

def result_signature
  {
    'generate' => stage_signature('generate', generate_temperature),
    'critique' => (routing.stage?('critique') ? stage_signature('critique', critique_temperature) : nil),
    'critique_engine' => jev_signature,
    'pool' => routing.signature
  }
end

#review_instructions ⇒ Object



228
229
230
# File 'lib/aireview/config.rb', line 228

def review_instructions
  @data['review_instructions']
end

#review_language ⇒ Object



202
203
204
# File 'lib/aireview/config.rb', line 202

def review_language
  @data['review_language'] || DEFAULTS['review_language']
end

#review_mode ⇒ Object

update — our note is updated when the diff or the settings changed, once — one automatic review; a job Retry updates the review on changes.



208
209
210
211
212
213
214
# File 'lib/aireview/config.rb', line 208

def review_mode
  mode = (@data['review_mode'] || DEFAULTS['review_mode']).to_s
  return mode if REVIEW_MODES.include?(mode)

  @logger.warn("Unknown review_mode #{mode.inspect}, using #{DEFAULTS['review_mode']}")
  DEFAULTS['review_mode']
end

#secret_files ⇒ Object



224
225
226
# File 'lib/aireview/config.rb', line 224

def secret_files
  (DEFAULT_SECRET_FILES + Array(@data['secret_files']).compact).uniq
end

#secret_patterns ⇒ Object



220
221
222
# File 'lib/aireview/config.rb', line 220

def secret_patterns
  Array(@data['secret_patterns']).compact
end

#stage_signature(stage, temperature) ⇒ Object

The primary of a stage on a server of your own goes into the review key with its address; without one the signature is what it was.



177
178
179
180
# File 'lib/aireview/config.rb', line 177

def stage_signature(stage, temperature)
  primary = routing.primary(stage)
  [primary.provider, primary.model, temperature, *primary.api_base]
end

#with_overrides(generate_model: nil, critique_model: nil, generate_temperature: nil, critique_temperature: nil, critique_engine: nil, no_fallbacks: false, no_critique: false) ⇒ Object

CLI overrides change only the primary model of a stage, the reserves from the config stay; no_fallbacks leaves one model and one key. no_critique — --no-critique: the run has no Critique, so its settings are neither routed nor validated (see llm_stages). Seven named flags of the CLI read better than a struct for its own sake.



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
# File 'lib/aireview/config.rb', line 84

def with_overrides( # rubocop:disable Metrics/ParameterLists
  generate_model: nil,
  critique_model: nil,
  generate_temperature: nil,
  critique_temperature: nil,
  critique_engine: nil,
  no_fallbacks: false,
  no_critique: false
)
  llm_config = {
    'generate' => stage_overrides(model: generate_model, temperature: generate_temperature),
    'critique' => stage_overrides(model: critique_model, temperature: critique_temperature)
      .merge({'engine' => critique_engine}.compact)
  }.reject { |_, overrides| overrides.empty? }
  overrides = {}
  overrides['llm'] = llm_config unless llm_config.empty?
  overrides['fallbacks_disabled'] = true if no_fallbacks
  overrides['critique_disabled'] = true if no_critique
  return self if overrides.empty?

  self.class.new(
    config_path: config_path,
    logger: @logger,
    layers: @layers + [ConfigLayers::Layer.new(name: ConfigLayers::CLI_LAYER, data: overrides)]
  )
end