Module: Protege::Inference

Extended by:
Inference
Included in:
Inference
Defined in:
lib/protege/inference.rb,
lib/protege/inference/provider/request.rb,
lib/protege/inference/provider/response.rb,
lib/protege/inference/provider/tool_call.rb

Overview

Namespace for all inference concerns — providers, requests, responses, and the value objects they exchange.

Module methods here are the cross-namespace entry points. Other namespaces (Orchestrator, Gateway) and the Rails layer call these rather than reaching into Provider::* internals directly.

Defined Under Namespace

Modules: Provider

Instance Method Summary collapse

Instance Method Details

#build_request(user_content: nil, tools: [], context_messages: []) ⇒ Provider::Request

Build a Provider::Request for the first turn of a generation.

The message list is the resolver-supplied context_messages. When user_content is given, it is appended as a trailing role: :user message — used by the reply path to inject the live inbound email body (a stripped string, or multimodal parts when it has attachments). When user_content is nil the request is exactly the resolver messages: a scheduled run assembles its whole turn — instructions included — through its resolver chain, so the harness injects nothing. The request carries only the conversation and the tool classes; the active provider reads its own model / sampling / credentials from config.providers.

Parameters:

  • user_content (String, Array<Object>, nil) (defaults to: nil)

    the trailing user turn, or nil to add none.

  • tools (Array<Class>) (defaults to: [])

    tool classes to expose for this generation.

  • context_messages (Array<ModelMessage>) (defaults to: [])

    the resolver-assembled message list.

Returns:



26
27
28
29
30
31
# File 'lib/protege/inference.rb', line 26

def build_request(user_content: nil, tools: [], context_messages: [])
  messages  = Array(context_messages)
  messages += [ModelMessage.new(role: :user, content: user_content)] unless user_content.nil?

  Provider::Request.new(messages:, tools:)
end

#extend_request(previous:, response:, tool_results:, review_parts: []) ⇒ Provider::Request

Build the next-turn Provider::Request by appending the assistant turn and tool results.

Used by the Orchestrator tool loop after the model requested tools: it appends the assistant ModelMessage (carrying its tool_calls) followed by one role: :tool message per result, then re-issues the request with the same tools as previous (model and sampling come from the provider's config on each turn). Each tool result's payload is correlated back via tool_call.id and serialized as JSON.

When review_parts are given (a tool asked to show the model an image/PDF it can't return in a text-only tool result), a final role: :user message carrying those vision parts is appended after the tool messages — the only place the OpenAI schema lets visual content reach the model.

Parameters:

  • previous (Provider::Request)

    the request that produced response.

  • response (Provider::Response)

    the assistant response containing the requested tool calls.

  • tool_results (Array<Protege::ToolResult>)

    one result per executed tool call.

  • review_parts (Array<Object>) (defaults to: [])

    vision parts to show the model on a trailing user turn.

Returns:



51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# File 'lib/protege/inference.rb', line 51

def extend_request(previous:, response:, tool_results:, review_parts: [])
  assistant_msg = ModelMessage.new(
    role:       :assistant,
    content:    response.text || '',
    tool_calls: response.tool_calls
  )

  tool_messages = tool_results.map do |tr|
    ModelMessage.new(
      role:         :tool,
      tool_call_id: tr.tool_call.id,
      content:      tr.result.to_h.to_json
    )
  end

  messages  = previous.messages + [assistant_msg] + tool_messages
  messages += [review_message(review_parts)] if review_parts.any?

  Provider::Request.new(messages:, tools: previous.tools)
end

#generate(request) ⇒ Provider::Response

Generate against the configured provider — the seam that keeps the provider contract hash-based. Hands the provider the wire request hash (+Request#to_wire+) and wraps its returned response hash back into a Provider::Response, so providers never touch Protege's internal value types.

Parameters:

Returns:



94
95
96
# File 'lib/protege/inference.rb', line 94

def generate(request)
  Provider::Response.from_wire(provider.generate(request.to_wire))
end

#generate_stream(request) {|chunk| ... } ⇒ Provider::Response

Stream a generation against the configured provider, wrapping the final response hash as #generate does. Yielded chunks pass through untouched.

Parameters:

Yield Parameters:

  • chunk (Hash)

    a { type:, content: } token chunk

Returns:



104
105
106
# File 'lib/protege/inference.rb', line 104

def generate_stream(request, &)
  Provider::Response.from_wire(provider.generate_stream(request.to_wire, &))
end

#providerProviderMixin

Instantiate the provider class matching the configured provider_id.

Resolves against ProviderMixin.registered (every Provider descendant) by symbolic id, so the lookup stays correct across Zeitwerk reloads.

Returns:

  • (ProviderMixin)

    a fresh provider instance for the configured id.

Raises:



79
80
81
82
83
84
85
86
# File 'lib/protege/inference.rb', line 79

def provider
  id    = Protege.configuration.provider_id
  klass = ProviderMixin.registered.find { |k| k.id == id }

  raise Protege::ContractViolationError, "no provider registered for #{id.inspect}" unless klass

  klass.new
end