Class: Ace::LLM::QueryInterface

Inherits:
Object
  • Object
show all
Defined in:
lib/ace/llm/query_interface.rb

Overview

QueryInterface provides a simple Ruby API with named parameters matching the CLI This allows direct Ruby calls to LLM providers without subprocess overhead.

Class Method Summary collapse

Class Method Details

.execute_with_fallback(provider:, model:, messages:, generation_opts:, registry:, fallback_config:, timeout:, debug:, role_fallbacks: nil, preset: nil, thinking_level: nil, option_builder:) ⇒ Object



360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
# File 'lib/ace/llm/query_interface.rb', line 360

def self.execute_with_fallback(provider:, model:, messages:, generation_opts:,
  registry:, fallback_config:, timeout:, debug:, role_fallbacks: nil, preset: nil, thinking_level: nil, option_builder:)
  if fallback_config.disabled?
    client = registry.get_client(provider, model: model, timeout: timeout)
    return client.generate(messages, **generation_opts)
  end

  primary_provider_string = model ? "#{provider}:#{model}" : provider

  # Inject remaining role candidates ahead of the global fallback chain
  normalized_role_fallbacks = normalize_fallback_providers(role_fallbacks, Molecules::ProviderModelParser.new(registry: registry))
  if role_fallbacks&.any?
    existing_chain = fallback_config.providers_for(primary_provider_string)
    merged_chain = normalized_role_fallbacks + (existing_chain - normalized_role_fallbacks)
    fallback_config = fallback_config.merge(chains: {primary_provider_string => merged_chain})
  end

  status_callback = ->(msg) { warn msg }

  orchestrator = Molecules::FallbackOrchestrator.new(
    config: fallback_config,
    status_callback: status_callback,
    timeout: timeout
  )

  orchestrator.execute(primary_provider: primary_provider_string, registry: registry) do |client, target_selector|
    opts = if target_selector.to_s == primary_provider_string
      generation_opts
    else
      option_builder.call(target_selector)
    end
    client.generate(messages, **opts)
  end
end

.extract_text_content(response) ⇒ Object



395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
# File 'lib/ace/llm/query_interface.rb', line 395

def self.extract_text_content(response)
  if response[:text]
    response[:text]
  elsif response[:content]
    response[:content]
  elsif response[:choices] && response[:choices].is_a?(Array) && !response[:choices].empty?
    choice = response[:choices].first
    if choice[:message] && choice[:message][:content]
      choice[:message][:content]
    elsif choice[:text]
      choice[:text]
    else
      ""
    end
  elsif response[:candidates] && response[:candidates].is_a?(Array) && !response[:candidates].empty?
    candidate = response[:candidates].first
    if candidate[:content] && candidate[:content][:parts] && !candidate[:content][:parts].empty?
      candidate[:content][:parts].first[:text] || ""
    else
      ""
    end
  else
    response.to_s
  end
end

.load_fallback_config(fallback, fallback_providers, parser: nil) ⇒ Object



333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
# File 'lib/ace/llm/query_interface.rb', line 333

def self.load_fallback_config(fallback, fallback_providers, parser: nil)
  parser ||= Molecules::ProviderModelParser.new

  config_fallback = Molecules::ConfigLoader.get("llm.fallback")
  normalized_config_fallback = normalize_fallback_provider_hash(config_fallback, parser)
  config = Models::FallbackConfig.from_hash(normalized_config_fallback)

  env_overrides = load_fallback_env_overrides
  env_overrides = normalize_fallback_provider_hash(env_overrides, parser)
  config = config.merge(env_overrides) unless env_overrides.empty?

  explicit_overrides = {}
  explicit_overrides[:enabled] = fallback unless fallback.nil?
  explicit_overrides[:providers] = fallback_providers if fallback_providers
  explicit_overrides = normalize_fallback_provider_hash(explicit_overrides, parser)
  config = config.merge(explicit_overrides) unless explicit_overrides.empty?

  normalized_chains = config.chains.transform_values do |chain|
    normalize_fallback_providers(chain, parser)
  end

  config.merge(
    providers: normalize_fallback_providers(config.providers, parser),
    chains: normalized_chains
  )
end

.query(provider_model, prompt = nil, output: nil, format: "text", temperature: nil, max_tokens: nil, system: nil, timeout: nil, force: false, debug: false, model: nil, prompt_override: nil, fallback: nil, fallback_providers: nil, system_file: nil, prompt_file: nil, cli_args: nil, system_append: nil, preset: nil, sandbox: nil, working_dir: nil, subprocess_env: nil, subprocess_command_prefix: nil, last_message_file: nil) ⇒ Object

Raises:



17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
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
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
# File 'lib/ace/llm/query_interface.rb', line 17

def self.query(provider_model, prompt = nil,
  output: nil,
  format: "text",
  temperature: nil,
  max_tokens: nil,
  system: nil,
  timeout: nil,
  force: false,
  debug: false,
  model: nil,
  prompt_override: nil,
  fallback: nil,
  fallback_providers: nil,
  system_file: nil,
  prompt_file: nil,
  cli_args: nil,
  system_append: nil,
  preset: nil,
  sandbox: nil,
  working_dir: nil,
  subprocess_env: nil,
  subprocess_command_prefix: nil,
  last_message_file: nil)
  registry = Molecules::ClientRegistry.new
  parser = Molecules::ProviderModelParser.new(registry: registry)

  parse_result = parser.parse(provider_model)
  raise Error, parse_result.error unless parse_result.valid?

  resolved_preset = resolve_preset_name(parse_result.preset, preset)
  final_model = model || parse_result.model
  if final_model.nil? || final_model.empty?
    raise Error, "No model specified and no default available for #{parse_result.provider}"
  end

  final_prompt = prompt_override || prompt
  if final_prompt.nil? || final_prompt.empty?
    raise Error, "No prompt specified. Use positional prompt or prompt_override: parameter"
  end

  messages = []
  messages << {role: "system", content: system} if system && !system.empty?
  messages << {role: "user", content: final_prompt}

  generation_opts = build_generation_opts(
    provider: parse_result.provider,
    preset: resolved_preset,
    thinking_level: parse_result.thinking_level,
    temperature: temperature,
    max_tokens: max_tokens,
    system_file: system_file,
    prompt_file: prompt_file,
    cli_args: cli_args,
    system_append: system_append,
    sandbox: sandbox,
    working_dir: working_dir,
    subprocess_env: subprocess_env,
    subprocess_command_prefix: subprocess_command_prefix,
    last_message_file: last_message_file
  )
  execution_overrides = load_execution_overrides(
    provider: parse_result.provider,
    preset: resolved_preset,
    thinking_level: parse_result.thinking_level
  )

  if debug
    warn "Provider: #{parse_result.provider}"
    warn "Model: #{final_model}"
    warn "Preset: #{resolved_preset}" if resolved_preset
    warn "Thinking level: #{parse_result.thinking_level}" if parse_result.thinking_level
    warn "Temperature: #{generation_opts[:temperature]}" unless generation_opts[:temperature].nil?
    warn "Max tokens: #{generation_opts[:max_tokens]}" unless generation_opts[:max_tokens].nil?
  end

  fallback_config = load_fallback_config(fallback, fallback_providers, parser: parser)
  timeout_value = first_non_nil(timeout, execution_overrides["timeout"], Molecules::ConfigLoader.get("llm.timeout"), 120)
  resolved_timeout = normalize_timeout(timeout_value)
  parser_for_options = Molecules::ProviderModelParser.new(registry: registry)

  response = execute_with_fallback(
    provider: parse_result.provider,
    model: final_model,
    messages: messages,
    generation_opts: generation_opts,
    registry: registry,
    fallback_config: fallback_config,
    timeout: resolved_timeout,
    debug: debug,
    role_fallbacks: parse_result.role_fallbacks,
    preset: resolved_preset,
    thinking_level: parse_result.thinking_level,
    option_builder: lambda { |target_selector|
      parsed_target = parser_for_options.parse(target_selector.to_s)
      target_provider = parsed_target.valid? ? parsed_target.provider : parse_result.provider
      target_preset = if parsed_target.valid? && parsed_target.preset
        parsed_target.preset
      else
        resolved_preset
      end
      target_thinking = if parsed_target.valid?
        parsed_target.thinking_level
      else
        parse_result.thinking_level
      end
      build_generation_opts(
        provider: target_provider,
        preset: target_preset,
        thinking_level: target_thinking,
        temperature: temperature,
        max_tokens: max_tokens,
        system_file: system_file,
        prompt_file: prompt_file,
        cli_args: cli_args,
        system_append: system_append,
        sandbox: sandbox,
        working_dir: working_dir,
        subprocess_env: subprocess_env,
        subprocess_command_prefix: subprocess_command_prefix,
        last_message_file: last_message_file
      )
    }
  )

  text_content = extract_text_content(response)

  result = {
    text: text_content,
    model: final_model,
    provider: parse_result.provider,
    preset: resolved_preset,
    thinking_level: parse_result.thinking_level,
    usage: response[:usage],
    metadata: response[:metadata]
  }

  if output && !output.empty?
    handler = Molecules::FormatHandlers.get_handler(format)

    formatted_content = case format
    when "json"
      handler.format(result)
    when "yaml"
      handler.format(result)
    when "raw"
      handler.format(response)
    else
      text_content
    end

    file_handler = Molecules::FileIoHandler.new
    file_handler.write_content(formatted_content, output, format: format, force: force)

    warn "Output written to: #{output}" if debug
  end

  result
end