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
-
.configuration ⇒ Configuration?
readonly
The current configuration.
Class Method Summary collapse
-
.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.
-
.configure {|Configuration| ... } ⇒ void
Configure providers for LLM requests.
-
.dispatch(session, provider:, callback:, callback_args:, request_options:, tool_iteration: 0, original_request_id: nil) ⇒ Object
private
Internal dispatch used by PatientLLM.ask and by Callback to re-issue requests during the automatic tool loop.
-
.inline { ... } ⇒ Object
Execute requests inline (synchronously, in-process) for the duration of the block instead of dispatching through the registered PatientHttp handler.
-
.inline? ⇒ Boolean
Check if requests are currently executing inline via PatientLLM.inline.
-
.provider(name) ⇒ Hash?
Look up a registered provider by name.
-
.reset! ⇒ void
Reset configuration.
-
.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).
Class Attribute Details
.configuration ⇒ Configuration? (readonly)
The current configuration.
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.
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) = {} ["url"] = url if url ["serializer"] = serializer.to_s if serializer ["path"] = path if path ["headers"] = headers if headers && !headers.empty? ["params"] = params if params && !params.empty? ["preprocessors"] = preprocessors if preprocessors ["timeout"] = timeout if timeout ["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. = PromptBuilder.jsonify() dispatch(session, provider: provider, callback: callback, callback_args: callback_args, request_options: ) end |
.configure {|Configuration| ... } ⇒ void
This method returns an undefined value.
Configure providers for LLM requests.
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
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 = ["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 = (["serializer"] || provider_config[:serializer] || :chat_completion).to_sym validate_serializer!(resolved_serializer) resolved_path = ["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(["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(["params"] || {}) resolved_preprocessors = ["preprocessors"] || provider_config[:preprocessors] resolved_timeout = ["timeout"] || provider_config[:timeout] resolved_max_tool_iterations = (["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: , 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.
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.
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.
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.
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 |