Class: Inquirex::LLM::Adapter

Inherits:
Object
  • Object
show all
Defined in:
lib/inquirex/llm/adapter.rb

Overview

Abstract interface for LLM adapters. Adapters bridge the gap between LLM::Node definitions and actual LLM API calls.

Implementations must:

1. Accept an LLM::Node and current answers
2. Construct the appropriate prompt (using node.prompt, node.from_steps, etc.)
3. Call the LLM API
4. Parse and validate the response against node.schema (if present)
5. Return a Hash or String result

The adapter is invoked server-side when the engine reaches an LLM step. It is never called on the frontend.

Examples:

Implementing a custom adapter

class MyLlmAdapter < Inquirex::LLM::Adapter
  def call(node, answers)
    prompt_text = build_prompt(node, answers)
    response = my_llm_client.complete(prompt_text, model: node.model)
    parse_response(response, node.schema)
  end
end

Direct Known Subclasses

AnthropicAdapter, NullAdapter, OpenAIAdapter

Instance Method Summary collapse

Instance Method Details

#call(node, answers) ⇒ Hash, String

Processes an LLM step and returns the result.

Parameters:

  • node (LLM::Node)

    the LLM step to process

  • answers (Hash)

    current collected answers

Returns:

  • (Hash, String)

    structured output (for extract) or text (when no schema)

Raises:



34
35
36
# File 'lib/inquirex/llm/adapter.rb', line 34

def call(node, answers)
  raise NotImplementedError, "#{self.class}#call must be implemented"
end

#normalize_output(node, output) ⇒ Hash, Object

Canonicalizes LLM output against the schema's value constraints — the regression guard for "the model answered with a label or a case variant". Every value-constrained field is matched against the allowed form values: an exact match passes through, a case-insensitive match is rewritten to the canonical value, and a value outside the list becomes nil (enum) or is dropped from the array (multi_enum) — "unknown, will ask" instead of junk that prefills the wrong option downstream. Unconstrained fields are untouched.

Parameters:

  • node (LLM::Node)
  • output (Hash, Object)

    parsed LLM response

Returns:

  • (Hash, Object)

    output with constrained fields canonicalized



113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
# File 'lib/inquirex/llm/adapter.rb', line 113

def normalize_output(node, output)
  schema = node.respond_to?(:schema) ? node.schema : nil
  return output unless schema && output.is_a?(Hash)

  output.to_h do |key, raw|
    values = schema.values_for(key)
    next [key, raw] unless values

    if schema.fields[key.to_sym] == :multi_enum
      [key, Array(raw).filter_map { |entry| canonical_value(values, entry) }]
    else
      [key, canonical_value(values, raw)]
    end
  end
end

#source_answers(node, answers) ⇒ Hash

Gathers the source answer data that feeds the LLM prompt.

Parameters:

Returns:

  • (Hash)

    relevant subset of answers



75
76
77
78
79
80
81
82
83
# File 'lib/inquirex/llm/adapter.rb', line 75

def source_answers(node, answers)
  if node.from_all
    answers.dup
  else
    node.from_steps.each_with_object({}) do |step_id, acc|
      acc[step_id] = answers[step_id] if answers.key?(step_id)
    end
  end
end

#summarize(node, transcript, answers = {}) ⇒ String

Produces the closing prose summary for a summarize step.

Separate from #call because nothing about it is the same: the prompt is the gem's rather than the node's, the input is the session transcript rather than selected answers, and the result is markdown rather than a schema-shaped Hash. Keeping it apart also means an existing custom adapter that only implements #call keeps working for extract and fails loudly — rather than subtly — if a flow starts asking it to summarise.

Parameters:

  • node (LLM::Node)

    the summarize step

  • transcript (String)

    everything the user was shown and answered

  • answers (Hash) (defaults to: {})

    collected answers, for adapters that want them

Returns:

  • (String)

    markdown summary

Raises:



53
54
55
# File 'lib/inquirex/llm/adapter.rb', line 53

def summarize(node, transcript, answers = {})
  raise NotImplementedError, "#{self.class}#summarize must be implemented"
end

#summary_input(transcript) ⇒ String

The user-side prompt for a summarize call: the transcript, and nothing else that could compete with it for the model's attention.

Parameters:

  • transcript (String)

Returns:

  • (String)

Raises:



63
64
65
66
67
68
# File 'lib/inquirex/llm/adapter.rb', line 63

def summary_input(transcript)
  text = transcript.to_s.strip
  raise Errors::AdapterError, "Cannot summarize an empty transcript" if text.empty?

  "Here is the session transcript.\n\n#{text}"
end

#validate_output!(node, output) ⇒ Object

Validates adapter output against the node's schema.

Parameters:

Raises:



90
91
92
93
94
95
96
97
98
# File 'lib/inquirex/llm/adapter.rb', line 90

def validate_output!(node, output)
  return unless node.schema

  missing = node.schema.missing_fields(output)
  return if missing.empty?

  raise Errors::SchemaViolationError,
    "LLM output for #{node.id.inspect} missing fields: #{missing.join(", ")}"
end