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:) ⇒ Object



292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
# File 'lib/ace/llm/query_interface.rb', line 292

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

  status_callback = ->(msg) { warn msg }

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

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

  orchestrator.execute(primary_provider: primary_provider_string, registry: registry) do |client|
    client.generate(messages, **generation_opts)
  end
end

.extract_text_content(response) ⇒ Object



314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
# File 'lib/ace/llm/query_interface.rb', line 314

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



265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
# File 'lib/ace/llm/query_interface.rb', line 265

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, 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
# 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,
  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)
  execution_overrides = load_execution_overrides(
    provider: parse_result.provider,
    preset: resolved_preset,
    thinking_level: parse_result.thinking_level
  )

  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 = {}
  resolved_temperature = first_non_nil(temperature, execution_overrides["temperature"])
  resolved_max_tokens = first_non_nil(max_tokens, execution_overrides["max_tokens"])
  resolved_cli_args = first_non_nil(cli_args, execution_overrides["cli_args"])
  resolved_system_append = first_non_empty(system_append, execution_overrides["system_append"])
  resolved_sandbox = first_non_nil(sandbox, execution_overrides["sandbox"])
  resolved_working_dir = first_non_nil(working_dir, execution_overrides["working_dir"])
  resolved_subprocess_env = merge_hash_values(execution_overrides["subprocess_env"], subprocess_env)

  generation_opts[:temperature] = resolved_temperature unless resolved_temperature.nil?
  generation_opts[:max_tokens] = resolved_max_tokens unless resolved_max_tokens.nil?
  generation_opts[:system_file] = system_file if system_file
  generation_opts[:prompt_file] = prompt_file if prompt_file
  generation_opts[:cli_args] = resolved_cli_args unless blank_value?(resolved_cli_args)
  generation_opts[:system_append] = resolved_system_append unless blank_value?(resolved_system_append)
  generation_opts[:sandbox] = resolved_sandbox if resolved_sandbox
  generation_opts[:working_dir] = resolved_working_dir unless blank_value?(resolved_working_dir)
  generation_opts[:subprocess_env] = resolved_subprocess_env unless resolved_subprocess_env.nil?
  generation_opts[:last_message_file] = last_message_file if last_message_file

  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: #{resolved_temperature}" unless resolved_temperature.nil?
    warn "Max tokens: #{resolved_max_tokens}" unless resolved_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)

  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
  )

  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