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/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, 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:



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

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



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

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 = {}
  request_options["url"] = url if url
  request_options["serializer"] = serializer.to_s if serializer
  request_options["path"] = path if path
  request_options["headers"] = headers if headers && !headers.empty?
  request_options["params"] = params if params && !params.empty?
  request_options["preprocessors"] = preprocessors if preprocessors
  request_options["timeout"] = timeout if timeout
  request_options["max_tool_iterations"] = max_tool_iterations if max_tool_iterations

  # The request options travel through the job queue in the callback args,
  # which only permit JSON-native values; convert Symbols (e.g. serializer
  # names, preprocessor names, header/param values) to Strings up front.
  request_options = PromptBuilder.jsonify(request_options)

  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:



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

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.

Raises:

  • (ArgumentError)


182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
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
# File 'lib/patient_llm.rb', line 182

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_url = request_options["url"] || provider_config[:url]
  raise ArgumentError, "No API base URL configured. Set url: or register a provider with a url." unless resolved_url

  resolved_serializer = (request_options["serializer"] || provider_config[:serializer] || :chat_completion).to_sym
  validate_serializer!(resolved_serializer)

  resolved_path = request_options["path"] || provider_config[:path] || SERIALIZER_PATHS[resolved_serializer]
  if resolved_path.include?("{model}")
    raise ArgumentError, "The endpoint path #{resolved_path.inspect} includes a {model} placeholder but session.model is not set" if session.model.nil?

    # Encode the model as a single path segment; Bedrock model ids can be
    # ARNs containing ":" and "/" that would otherwise splice extra path
    # segments into the URL and break SigV4 signing.
    resolved_path = resolved_path.gsub("{model}", URI.encode_uri_component(session.model.to_s))
  end

  resolved_headers = (provider_config[:headers] || {}).merge(request_options["headers"] || {})
  if resolved_serializer == :messages && !resolved_headers.key?("anthropic-version")
    resolved_headers = {"anthropic-version" => ANTHROPIC_VERSION}.merge(resolved_headers)
  end

  resolved_params = (provider_config[:params] || {}).merge(request_options["params"] || {})
  resolved_preprocessors = request_options["preprocessors"] || provider_config[:preprocessors]
  resolved_timeout = request_options["timeout"] || provider_config[:timeout]
  resolved_max_tool_iterations = (request_options["max_tool_iterations"] || provider_config[:max_tool_iterations] || Callback::MAX_TOOL_ITERATIONS).to_i

  payload = session.request_payload(resolved_serializer)
  payload = deep_merge(payload, deep_stringify_keys(resolved_params)) unless resolved_params.empty?

  request_url = join_url(resolved_url, resolved_path)

  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,
      request_url,
      json: 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(
      request_url,
      json: 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



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

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)


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

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

.provider(name) ⇒ Hash?

Look up a registered provider by name.

Parameters:

  • name (Symbol, String)

    Provider name

Returns:

  • (Hash, nil)

    Provider config



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

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

.reset!void

This method returns an undefined value.

Reset configuration. Primarily useful in tests.



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

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



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

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