Module: PatientLLM

Defined in:
lib/patient_llm.rb,
lib/patient_llm/agent.rb,
lib/patient_llm/schema.rb,
lib/patient_llm/presets.rb,
lib/patient_llm/callback.rb,
lib/patient_llm/halt_error.rb,
lib/patient_llm/agent/failure.rb,
lib/patient_llm/configuration.rb,
lib/patient_llm/agent/response.rb,
lib/patient_llm/request_preview.rb,
lib/patient_llm/aws_request_signer.rb,
lib/patient_llm/structured_output_error.rb,
lib/patient_llm/max_tool_iterations_error.rb

Defined Under Namespace

Modules: Presets Classes: Agent, AwsRequestSigner, Callback, Configuration, HaltError, MaxToolIterationsError, RequestPreview, Schema, StructuredOutputError

Constant Summary collapse

VERSION =
File.read(File.join(__dir__, "../VERSION")).strip
SERIALIZER_PATHS =

Default API paths per serializer format. The Bedrock Converse and Gemini paths embed a {model} placeholder that is replaced with the session's model (percent-encoded as a single path segment) at dispatch time.

{
  chat_completion: "v1/chat/completions",
  open_responses: "v1/responses",
  messages: "v1/messages",
  converse: "model/{model}/converse",
  gemini: "v1beta/models/{model}:generateContent"
}.freeze
ANTHROPIC_VERSION =

Required version header for the Anthropic Messages API.

"2023-06-01"
VALID_SERIALIZERS =

Valid serializer format names.

SERIALIZER_PATHS.keys.freeze
SESSION_REF_KEY =

Key used in callback args to reference a session stored in a payload store.

"$session_ref"
AUTHENTICATION_HEADERS =

Headers that must be setup to use the secrets manager. If any of these headers are included in the provider configuration, an error will be raised unless their values are set up as secrets using PatientHttp.secret.

["authorization", "x-api-key", "x-goog-api-key", "api-key"].freeze

Class Attribute Summary collapse

Class Method Summary collapse

Class Attribute Details

.configurationConfiguration? (readonly)

The current configuration.

Returns:



56
57
58
# File 'lib/patient_llm.rb', line 56

def configuration
  @configuration
end

Class Method Details

.ask(session, provider:, callback:, callback_args: {}, url: nil, serializer: nil, path: nil, headers: nil, params: nil, preprocessors: nil, timeout: nil, max_tool_iterations: nil) ⇒ Object

Send an LLM request asynchronously using the given session and provider.

Parameters:

  • session (PromptBuilder::Session)

    The prompt session containing conversation state

  • provider (Symbol, String)

    Registered provider name

  • callback (Class, String)

    Callback class for handling completion/error

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

    Custom arguments passed through to the callback

  • url (String, nil) (defaults to: nil)

    Override the provider's base URL for this request

  • serializer (Symbol, nil) (defaults to: nil)

    Override the provider's serializer for this request

  • path (String, nil) (defaults to: nil)

    Override the endpoint path for this request

  • headers (Hash, nil) (defaults to: nil)

    Additional headers merged on top of provider headers

  • params (Hash, nil) (defaults to: nil)

    Additional params merged into the request payload

  • preprocessors (String, Symbol, Array<String, Symbol>, nil) (defaults to: nil)

    Names of request preprocessors to apply to this request. Replaces the provider's configured preprocessors; pass an empty array to clear them for this request.

  • timeout (Numeric, nil) (defaults to: nil)

    Request timeout in seconds for this request. Overrides the provider's timeout; defaults to the PatientHttp processor configuration.

  • max_tool_iterations (Integer, nil) (defaults to: nil)

    Maximum automatic tool-execution rounds for this request. Overrides the provider's setting; defaults to PatientLLM::Callback::MAX_TOOL_ITERATIONS.

Returns:

  • (Object)

    Handler-specific identifier for the enqueued request



160
161
162
163
164
165
166
167
168
169
170
171
172
173
# File 'lib/patient_llm.rb', line 160

def ask(session, provider:, callback:, callback_args: {}, url: nil, serializer: nil, path: nil, headers: nil, params: nil, preprocessors: nil, timeout: nil, max_tool_iterations: nil)
  request_options = build_request_options(
    url: url,
    serializer: serializer,
    path: path,
    headers: headers,
    params: params,
    preprocessors: preprocessors,
    timeout: timeout,
    max_tool_iterations: max_tool_iterations
  )

  dispatch(session, provider: provider, callback: callback, callback_args: callback_args, request_options: request_options)
end

.configure {|Configuration| ... } ⇒ void

This method returns an undefined value.

Configure providers for LLM requests.

Yields:



48
49
50
51
# File 'lib/patient_llm.rb', line 48

def configure
  @configuration ||= Configuration.new
  yield @configuration
end

.dispatch(session, provider:, callback:, callback_args:, request_options:, tool_iteration: 0, original_request_id: nil) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Internal dispatch used by ask and by Callback to re-issue requests during the automatic tool loop. Not part of the public API.



224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
# File 'lib/patient_llm.rb', line 224

def dispatch(session, provider:, callback:, callback_args:, request_options:, tool_iteration: 0, original_request_id: nil)
  provider_config = self.provider(provider) || {}
  provider_name = provider.to_s

  if tool_iteration.zero?
    PatientLLM::Callback.validate_callback_class!(PatientHttp::ClassHelper.resolve_class_name(callback.to_s))
  end

  resolved = resolve_request(session, provider_config, request_options)

  dispatch_callback_args = {
    session: session_payload(session),
    provider: provider_name,
    serializer: resolved.serializer.to_s,
    callback: callback.to_s,
    custom: PromptBuilder.jsonify(callback_args || {}),
    request_options: request_options,
    max_tool_iterations: resolved.max_tool_iterations,
    tool_iteration: tool_iteration,
    original_request_id: original_request_id
  }

  if inline?
    request = PatientHttp::Request.new(
      :post,
      resolved.url,
      json: resolved.payload,
      headers: resolved.headers,
      preprocessors: resolved.preprocessors,
      timeout: resolved.timeout
    )
    PatientHttp.execute_inline(
      request: request,
      callback: PatientLLM::Callback,
      callback_args: dispatch_callback_args,
      raise_error_responses: true
    )
  else
    PatientHttp.post(
      resolved.url,
      json: resolved.payload,
      headers: resolved.headers,
      preprocessors: resolved.preprocessors,
      timeout: resolved.timeout,
      raise_error_responses: true,
      callback: PatientLLM::Callback,
      callback_args: dispatch_callback_args
    )
  end
end

.inline { ... } ⇒ Object

Execute requests inline (synchronously, in-process) for the duration of the block instead of dispatching through the registered PatientHttp handler. Useful in consoles and tests; the automatic tool loop also runs inline since it re-enters on the same thread.

Yields:

  • the block during which requests execute inline

Returns:

  • (Object)

    the block's return value



123
124
125
126
127
128
129
130
131
# File 'lib/patient_llm.rb', line 123

def inline
  previous = Thread.current.thread_variable_get(:patient_llm_inline)
  Thread.current.thread_variable_set(:patient_llm_inline, true)
  begin
    yield
  ensure
    Thread.current.thread_variable_set(:patient_llm_inline, previous)
  end
end

.inline?Boolean

Check if requests are currently executing inline via inline.

Returns:

  • (Boolean)


136
137
138
# File 'lib/patient_llm.rb', line 136

def inline?
  !!Thread.current.thread_variable_get(:patient_llm_inline)
end

.preview_request(session, provider:, url: nil, serializer: nil, path: nil, headers: nil, params: nil, preprocessors: nil, timeout: nil, max_tool_iterations: nil) ⇒ RequestPreview

Build the request that ask would send without sending it. The same resolution logic as ask is applied: per-request overrides are merged over the provider configuration, the session is serialized with the resolved serializer, and provider params are merged into the payload.

Nothing is enqueued or executed and no callback is required. Request preprocessors (e.g. AWS SigV4 signing) run at send time in the request processor, so their changes are not reflected in the preview. Header values that reference registered secrets are replaced with placeholders and never resolved.

Parameters:

  • session (PromptBuilder::Session)

    The prompt session containing conversation state

  • provider (Symbol, String)

    Registered provider name

  • url (String, nil) (defaults to: nil)

    Override the provider's base URL for this request

  • serializer (Symbol, nil) (defaults to: nil)

    Override the provider's serializer for this request

  • path (String, nil) (defaults to: nil)

    Override the endpoint path for this request

  • headers (Hash, nil) (defaults to: nil)

    Additional headers merged on top of provider headers

  • params (Hash, nil) (defaults to: nil)

    Additional params merged into the request payload

  • preprocessors (String, Symbol, Array<String, Symbol>, nil) (defaults to: nil)

    Accepted for parity with ask; preprocessors are not applied to the preview.

  • timeout (Numeric, nil) (defaults to: nil)

    Accepted for parity with ask; does not affect the preview.

  • max_tool_iterations (Integer, nil) (defaults to: nil)

    Accepted for parity with ask; does not affect the preview.

Returns:

  • (RequestPreview)

    The url, headers, and JSON payload the request would send



199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
# File 'lib/patient_llm.rb', line 199

def preview_request(session, provider:, url: nil, serializer: nil, path: nil, headers: nil, params: nil, preprocessors: nil, timeout: nil, max_tool_iterations: nil)
  request_options = build_request_options(
    url: url,
    serializer: serializer,
    path: path,
    headers: headers,
    params: params,
    preprocessors: preprocessors,
    timeout: timeout,
    max_tool_iterations: max_tool_iterations
  )

  resolved = resolve_request(session, self.provider(provider) || {}, request_options)

  RequestPreview.new(
    url: resolved.url,
    headers: redact_secret_headers(resolved.headers),
    payload: resolved.payload
  )
end

.provider(name) ⇒ Hash?

Look up a registered provider by name.

Parameters:

  • name (Symbol, String)

    Provider name

Returns:

  • (Hash, nil)

    Provider config



69
70
71
# File 'lib/patient_llm.rb', line 69

def provider(name)
  @configuration&.lookup(name)
end

.reset!void

This method returns an undefined value.

Reset configuration. Primarily useful in tests.



61
62
63
# File 'lib/patient_llm.rb', line 61

def reset!
  @configuration = nil
end

.verify_configuration!true

Verify that the configuration is fully wired: a PatientHttp request handler is registered, every secret referenced by a provider is registered, and every preprocessor referenced by a provider is registered (when a default PatientHttp configuration is available to check against).

Call this at the end of your application initializer to surface wiring mistakes at boot time instead of at dispatch time inside a job.

Returns:

  • (true)

Raises:

  • (RuntimeError)

    if any configuration problem is found



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
# File 'lib/patient_llm.rb', line 83

def verify_configuration!
  errors = []

  unless PatientHttp.handler_registered?
    errors << "No PatientHttp request handler is registered. Add a job-system integration (patient_http-sidekiq or patient_http-solid_queue) or call PatientHttp.inline! for synchronous execution."
  end

  @configuration&.provider_names&.each do |name|
    provider_config = @configuration.lookup(name)

    provider_config[:headers].each do |header_name, value|
      next unless value.is_a?(PatientHttp::SecretReference)

      unless PatientHttp.secret_registered?(value.name)
        errors << "Provider #{name.inspect} header #{header_name.inspect} references secret #{value.name.inspect} but it is not registered with PatientHttp."
      end
    end

    patient_http_config = PatientHttp.default_configuration
    if patient_http_config
      Array(provider_config[:preprocessors]).each do |preprocessor_name|
        unless patient_http_config.preprocessor(preprocessor_name)
          errors << "Provider #{name.inspect} references preprocessor #{preprocessor_name.inspect} but it is not registered on the PatientHttp configuration."
        end
      end
    end
  end

  raise errors.join("\n") unless errors.empty?

  true
end