Class: PatientLLM::Agent
- Inherits:
-
Object
- Object
- PatientLLM::Agent
- Defined in:
- lib/patient_llm/agent.rb,
lib/patient_llm/agent/failure.rb,
lib/patient_llm/agent/response.rb
Overview
Base class for declarative LLM agents. An agent bundles everything about one LLM integration in a single class: the provider, model, generation settings, tools (schema and handler together), structured output schema, and completion handling. The agent class itself is the callback identity, so its name is what travels through the job queue — everything else stays in code and is re-resolved in the worker process.
Subclasses inherit every declaration from their parent agent class,
including tools and the output schema. Override a setting by redeclaring
it, or remove an inherited scalar setting by passing an explicit nil
(e.g. temperature nil).
The completed/failed/tool_round hooks can be redirected to another class for one request by passing it as the :callback option to Agent.ask; the agent still supplies the configuration and the tools.
Defined Under Namespace
Constant Summary collapse
- PLUMBING_METHODS =
Methods that implement the callback plumbing contract. Subclasses must override the completed/failed/tool_round hooks instead.
%i[on_complete on_tool_use on_error prepare handles_tool? invoke_tool].freeze
- HOOKS =
The user-facing hooks that a class passed as the :callback option to ask may implement to take over from the agent's own hooks.
%i[completed failed tool_round].freeze
Instance Attribute Summary collapse
-
#provider ⇒ String?
readonly
The provider name for the current invocation.
-
#session ⇒ PromptBuilder::Session?
readonly
The session for the current invocation.
Class Method Summary collapse
-
.apply_configuration(session, except: []) ⇒ PromptBuilder::Session
Apply the agent's declarations to a session.
-
.ask(message = nil, context: {}, session: nil, callback: nil, **options) ⇒ Object
Send a message to the agent asynchronously.
-
.ask!(message = nil, context: {}, session: nil, **options) ⇒ Agent::Response
Send a message and execute the request inline (synchronously, in-process), returning the final Response.
-
.build_session(**options) ⇒ PromptBuilder::Session
Build a new session from the agent's declarations.
-
.continue(state, message = nil, context: {}, **options) ⇒ Object
Continue a persisted conversation.
-
.extra(value = NOT_SPECIFIED) { ... } ⇒ Hash?
DSL: get or set provider-specific extra data set on sessions built by this agent (e.g. the Bedrock Converse :guardrail_config).
-
.instructions(value = NOT_SPECIFIED) { ... } ⇒ String?
DSL: get or set instructions applied to every request.
-
.max_output_tokens(value = NOT_SPECIFIED) { ... } ⇒ Integer?
DSL: get or set the maximum output tokens.
-
.max_tool_iterations(value = NOT_SPECIFIED) { ... } ⇒ Integer?
DSL: get or set the maximum automatic tool-execution rounds.
-
.model(value = NOT_SPECIFIED) { ... } ⇒ String?
DSL: get or set the model.
-
.output(schema: nil, name: "response", strict: nil) { ... } ⇒ void
DSL: declare a structured output schema.
-
.output_schema ⇒ Hash?
The declared output schema, inherited from the parent agent class unless declared with Agent.output.
-
.preview_request(message = nil, session: nil, context: nil, callback: nil, **options) ⇒ PatientLLM::RequestPreview
Build the request that Agent.ask would send without sending it.
-
.provider(name = NOT_SPECIFIED) { ... } ⇒ Symbol?
DSL: get or set the provider name for this agent.
-
.reasoning(value = NOT_SPECIFIED, effort: nil, budget_tokens: nil) ⇒ Hash?
DSL: get or set the reasoning configuration.
-
.system(value = NOT_SPECIFIED) { ... } ⇒ String?
DSL: get or set the system message.
-
.temperature(value = NOT_SPECIFIED) { ... } ⇒ Numeric?
DSL: get or set the sampling temperature.
-
.tool(name, description = nil, parameters: nil, strict: nil) { ... } ⇒ void
DSL: declare a tool.
-
.tool_declared?(name) ⇒ Boolean
Check if a tool name is declared on this agent.
-
.tools ⇒ Hash<String, Hash>
The declared tools, including tools inherited from parent agent classes.
Instance Method Summary collapse
-
#completed(response) ⇒ void
Hook: invoked with the final Response after any automatic tool rounds complete.
-
#failed(failure) ⇒ void
Hook: invoked when a request fails.
-
#handles_tool?(name) ⇒ Boolean
private
Plumbing: whether this agent handles the named tool with an instance method.
-
#invoke_tool(name, arguments, callback_args: nil) ⇒ Object
private
Plumbing: invoke the tool's instance method with the LLM-provided arguments as keywords.
-
#on_complete(session:, provider:, llm_response:, callback_args:, http_response:, request_id:) ⇒ Object
private
Plumbing: adapter for the Callback contract.
-
#on_error(session:, provider:, callback_args:, error:, http_response:, request_id:) ⇒ Object
private
Plumbing: adapter for the Callback contract.
-
#on_tool_use(session:, provider:, llm_response:, callback_args:, http_response:, request_id:) ⇒ Object
private
Plumbing: adapter for the Callback contract.
-
#prepare(session: nil, provider: nil, callback_args: nil, http_response: nil, request_id: nil) ⇒ Object
private
Plumbing: called by Callback before hooks and tool execution to make the invocation state available to instance methods.
-
#tool_round(response) ⇒ void
Hook: invoked once per automatic tool round, after the tools run and before the next request is issued.
Instance Attribute Details
#provider ⇒ String? (readonly)
Returns the provider name for the current invocation.
550 551 552 |
# File 'lib/patient_llm/agent.rb', line 550 def provider @provider end |
#session ⇒ PromptBuilder::Session? (readonly)
Returns the session for the current invocation.
547 548 549 |
# File 'lib/patient_llm/agent.rb', line 547 def session @session end |
Class Method Details
.apply_configuration(session, except: []) ⇒ PromptBuilder::Session
Apply the agent's declarations to a session. Used for both new sessions
and sessions restored from persisted state. Fields listed in except
are left untouched (used by build_session so per-request options win
over the agent's declarations).
406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 |
# File 'lib/patient_llm/agent.rb', line 406 def apply_configuration(session, except: []) = system model_value = model instructions_value = instructions temperature_value = temperature max_output_tokens_value = max_output_tokens extra_value = extra (session, ) if && !except.include?(:system) session.model = model_value if model_value && !except.include?(:model) session.instructions = instructions_value if instructions_value && !except.include?(:instructions) session.temperature = temperature_value if temperature_value && !except.include?(:temperature) session.max_output_tokens = max_output_tokens_value if max_output_tokens_value && !except.include?(:max_output_tokens) session.extra = extra_value if extra_value && !except.include?(:extra) session.think(**reasoning.transform_keys(&:to_sym)) if reasoning && !except.include?(:reasoning) if output_schema && !except.include?(:text) session.json_output(output_schema[:schema], name: output_schema[:name], strict: output_schema[:strict]) end tools.each do |name, declaration| session.register_tool( name, description: declaration[:description], parameters: declaration[:parameters], strict: declaration[:strict] || false ) end session end |
.ask(message = nil, context: {}, session: nil, callback: nil, **options) ⇒ Object
Send a message to the agent asynchronously. When the final response
arrives (after any automatic tool rounds), the agent's completed hook
is invoked in the worker; errors invoke failed.
284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 |
# File 'lib/patient_llm/agent.rb', line 284 def ask( = nil, context: {}, session: nil, callback: nil, **) = .slice(*PromptBuilder::Session::INITIALIZE_OPTIONS) raise ArgumentError.new("session options cannot be passed when a session is provided") if session && .any? session ||= build_session(**) session.user() if = .except(*PromptBuilder::Session::INITIALIZE_OPTIONS) iterations = max_tool_iterations [:max_tool_iterations] ||= iterations if iterations agent_callback_args = {"context" => PromptBuilder.jsonify(context || {})} callback_name = callback_class_name(callback) agent_callback_args["callback"] = callback_name if callback_name PatientLLM.ask( session, provider: provider_name!, callback: self, callback_args: agent_callback_args, ** ) end |
.ask!(message = nil, context: {}, session: nil, **options) ⇒ Agent::Response
Send a message and execute the request inline (synchronously, in-process), returning the final Response. The automatic tool loop runs inline too. Intended for consoles, development, and tests. The completed/failed hooks still run, so this exercises real code paths.
339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 |
# File 'lib/patient_llm/agent.rb', line 339 def ask!( = nil, context: {}, session: nil, **) capture = {} previous = Thread.current.thread_variable_get(:patient_llm_agent_capture) Thread.current.thread_variable_set(:patient_llm_agent_capture, capture) begin PatientLLM.inline do ask(, context: context, session: session, **) end ensure Thread.current.thread_variable_set(:patient_llm_agent_capture, previous) end raise capture[:error] if capture[:error] capture[:response] || raise("No response was captured; the request did not complete") end |
.build_session(**options) ⇒ PromptBuilder::Session
Build a new session from the agent's declarations. Options passed by the caller take precedence over the agent's declarations for the same fields (pass an explicit nil to unset a declared value for one request).
392 393 394 395 396 |
# File 'lib/patient_llm/agent.rb', line 392 def build_session(**) session = PromptBuilder::Session.new(**) apply_configuration(session, except: .keys) session end |
.continue(state, message = nil, context: {}, **options) ⇒ Object
Continue a persisted conversation. The session is restored from the
state hash (as returned by response.state), the agent's current
configuration is re-applied to it (so model/instruction/tool/schema
changes deploy cleanly to older conversations), and the message is sent.
320 321 322 323 324 |
# File 'lib/patient_llm/agent.rb', line 320 def continue(state, = nil, context: {}, **) session = PromptBuilder::Session.from_h(state) apply_configuration(session) ask(, context: context, session: session, **) end |
.extra(value = NOT_SPECIFIED) { ... } ⇒ Hash?
DSL: get or set provider-specific extra data set on sessions built by this agent (e.g. the Bedrock Converse :guardrail_config). The recognized keys depend on the serializer used for the request. The value is inherited from the parent agent class unless set; redeclaring replaces the whole hash. Pass an explicit nil to remove an inherited value.
197 198 199 200 201 202 |
# File 'lib/patient_llm/agent.rb', line 197 def extra(value = NOT_SPECIFIED, &block) get_or_set_setting(:extra, value, block) do |resolved| raise ArgumentError, "extra must be a Hash" unless resolved.nil? || resolved.is_a?(Hash) resolved.nil? ? nil : PromptBuilder.jsonify(resolved) end end |
.instructions(value = NOT_SPECIFIED) { ... } ⇒ String?
DSL: get or set instructions applied to every request. These are appended to the system prompt on APIs that don't support instructions as a separate field. The value is inherited from the parent agent class unless set; pass an explicit nil to remove an inherited value.
118 119 120 |
# File 'lib/patient_llm/agent.rb', line 118 def instructions(value = NOT_SPECIFIED, &block) get_or_set_setting(:instructions, value, block) end |
.max_output_tokens(value = NOT_SPECIFIED) { ... } ⇒ Integer?
DSL: get or set the maximum output tokens. The value is inherited from the parent agent class unless set; pass an explicit nil to remove an inherited value.
168 169 170 |
# File 'lib/patient_llm/agent.rb', line 168 def max_output_tokens(value = NOT_SPECIFIED, &block) get_or_set_setting(:max_output_tokens, value, block) end |
.max_tool_iterations(value = NOT_SPECIFIED) { ... } ⇒ Integer?
DSL: get or set the maximum automatic tool-execution rounds. The value is inherited from the parent agent class unless set; pass an explicit nil to remove an inherited value.
180 181 182 |
# File 'lib/patient_llm/agent.rb', line 180 def max_tool_iterations(value = NOT_SPECIFIED, &block) get_or_set_setting(:max_tool_iterations, value, block) end |
.model(value = NOT_SPECIFIED) { ... } ⇒ String?
DSL: get or set the model. The value is inherited from the parent agent class unless set; pass an explicit nil to remove an inherited value.
93 94 95 |
# File 'lib/patient_llm/agent.rb', line 93 def model(value = NOT_SPECIFIED, &block) get_or_set_setting(:model, value, block) end |
.output(schema: nil, name: "response", strict: nil) { ... } ⇒ void
This method returns an undefined value.
DSL: declare a structured output schema. The model's response is parsed
into a Hash available as response.object in the completed hook. Use a
Schema block or pass a raw JSON Schema hash with schema:.
242 243 244 245 246 247 248 249 250 251 |
# File 'lib/patient_llm/agent.rb', line 242 def output(schema: nil, name: "response", strict: nil, &block) raise ArgumentError, "pass either schema: or a schema block, not both" if schema && block raise ArgumentError, "output requires schema: or a schema block" if schema.nil? && block.nil? @output_schema = { name: name.to_s, schema: schema ? PromptBuilder.jsonify(schema) : Schema.build(&block), strict: strict } end |
.output_schema ⇒ Hash?
The declared output schema, inherited from the parent agent class unless declared with output.
257 258 259 |
# File 'lib/patient_llm/agent.rb', line 257 def output_schema inherited_setting(:output_schema) end |
.preview_request(message = nil, session: nil, context: nil, callback: nil, **options) ⇒ PatientLLM::RequestPreview
Build the request that ask would send without sending it. The session is built from the agent's declarations exactly as ask builds it (including any per-request session options and the user message, which is appended to a passed session), and the request is resolved through the same logic as a dispatched request. Nothing is enqueued or executed. See PatientLLM.preview_request for details on what the preview does and does not reflect.
373 374 375 376 377 378 379 380 381 382 383 384 385 |
# File 'lib/patient_llm/agent.rb', line 373 def preview_request( = nil, session: nil, context: nil, callback: nil, **) = .slice(*PromptBuilder::Session::INITIALIZE_OPTIONS) raise ArgumentError.new("session options cannot be passed when a session is provided") if session && .any? session ||= build_session(**) session.user() if PatientLLM.preview_request( session, provider: provider_name!, **.except(*PromptBuilder::Session::INITIALIZE_OPTIONS) ) end |
.provider(name = NOT_SPECIFIED) { ... } ⇒ Symbol?
DSL: get or set the provider name for this agent. The value is inherited from the parent agent class unless set; pass an explicit nil to remove an inherited value.
82 83 84 |
# File 'lib/patient_llm/agent.rb', line 82 def provider(name = NOT_SPECIFIED, &block) get_or_set_setting(:provider, name, block) { |value| value&.to_sym } end |
.reasoning(value = NOT_SPECIFIED, effort: nil, budget_tokens: nil) ⇒ Hash?
DSL: get or set the reasoning configuration. Accepts a portable effort
level (:minimal, :low, :medium, :high, :xhigh, :max) or explicit options
(effort: or budget_tokens:) which are applied with session.think.
The value is inherited from the parent agent class unless set; pass an
explicit nil to remove an inherited value.
145 146 147 148 149 150 151 152 153 154 155 156 157 158 |
# File 'lib/patient_llm/agent.rb', line 145 def reasoning(value = NOT_SPECIFIED, effort: nil, budget_tokens: nil) return inherited_setting(:reasoning) if value.equal?(NOT_SPECIFIED) && effort.nil? && budget_tokens.nil? passed = [(value.equal?(NOT_SPECIFIED) ? nil : value), effort, budget_tokens].compact raise ArgumentError, "pass a level, effort:, or budget_tokens: — not more than one" if passed.size > 1 @reasoning = if passed.empty? nil elsif effort || budget_tokens {effort: effort&.to_s, budget_tokens: budget_tokens}.compact else {effort: value.to_s} end end |
.system(value = NOT_SPECIFIED) { ... } ⇒ String?
DSL: get or set the system message. The value is inherited from the parent agent class unless set; pass an explicit nil to remove an inherited value.
105 106 107 |
# File 'lib/patient_llm/agent.rb', line 105 def system(value = NOT_SPECIFIED, &block) get_or_set_setting(:system, value, block) end |
.temperature(value = NOT_SPECIFIED) { ... } ⇒ Numeric?
DSL: get or set the sampling temperature. The value is inherited from the parent agent class unless set; pass an explicit nil to remove an inherited value.
130 131 132 |
# File 'lib/patient_llm/agent.rb', line 130 def temperature(value = NOT_SPECIFIED, &block) get_or_set_setting(:temperature, value, block) end |
.tool(name, description = nil, parameters: nil, strict: nil) { ... } ⇒ void
This method returns an undefined value.
DSL: declare a tool. The tool's handler is the instance method with the same name; define it in the class body with keyword arguments matching the declared parameters. The schema can be declared with a Schema block or passed as a raw JSON Schema hash with parameters:.
215 216 217 218 219 220 |
# File 'lib/patient_llm/agent.rb', line 215 def tool(name, description = nil, parameters: nil, strict: nil, &block) raise ArgumentError, "pass either parameters: or a schema block, not both" if parameters && block schema = parameters ? PromptBuilder.jsonify(parameters) : Schema.build(&block) own_tools[name.to_s] = {description: description, parameters: schema, strict: strict} end |
.tool_declared?(name) ⇒ Boolean
Check if a tool name is declared on this agent.
438 439 440 |
# File 'lib/patient_llm/agent.rb', line 438 def tool_declared?(name) tools.key?(name.to_s) end |
.tools ⇒ Hash<String, Hash>
The declared tools, including tools inherited from parent agent classes. A tool redeclared in a subclass replaces the inherited declaration. The returned hash is a copy; declare tools with the tool DSL method rather than mutating it.
228 229 230 231 |
# File 'lib/patient_llm/agent.rb', line 228 def tools merged = (self == PatientLLM::Agent) ? {} : superclass.tools merged.merge!(deep_dup(own_tools)) end |
Instance Method Details
#completed(response) ⇒ void
666 667 |
# File 'lib/patient_llm/agent.rb', line 666 def completed(response) end |
#failed(failure) ⇒ void
This method returns an undefined value.
Hook: invoked when a request fails. Override in your agent, or in a class passed as the :callback option to ask. The default implementation re-raises the error so unhandled failures are never silently lost — under a job system this fails the callback job, making the error visible to its retry and error reporting. During inline execution the raise is skipped because ask! raises the captured error itself (raising here as well would make the executor invoke the error callback a second time with a wrapped error).
681 682 683 |
# File 'lib/patient_llm/agent.rb', line 681 def failed(failure) raise failure.error unless Thread.current.thread_variable_get(:patient_llm_agent_capture) end |
#handles_tool?(name) ⇒ Boolean
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.
Plumbing: whether this agent handles the named tool with an instance method. Used by Callback to route tool execution. Only public methods defined by the agent class hierarchy (or modules mixed into it) count as handlers — a declared tool name that collides with an inherited Object method must not route LLM-controlled invocations to it. A declared tool with no valid handler raises rather than returning false so a private handler or a typo'd method name fails loudly instead of producing a bogus completion.
634 635 636 637 638 639 640 641 642 643 |
# File 'lib/patient_llm/agent.rb', line 634 def handles_tool?(name) return false unless self.class.tool_declared?(name) method_name = name.to_sym unless respond_to?(method_name) && tool_method_owned_by_agent?(method_name) raise NoMethodError, "#{self.class} declares tool #{name.to_s.inspect} but does not define a public instance method ##{method_name} to handle it" end true end |
#invoke_tool(name, arguments, callback_args: 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.
Plumbing: invoke the tool's instance method with the LLM-provided
arguments as keywords. A tool method that declares a context: keyword
also receives the context passed to ask/continue, extracted from the
callback args supplied by Callback.
651 652 653 654 655 656 657 |
# File 'lib/patient_llm/agent.rb', line 651 def invoke_tool(name, arguments, callback_args: nil) raise ArgumentError, "#{self.class} does not handle tool #{name.inspect}" unless handles_tool?(name) kwargs = (arguments || {}).transform_keys(&:to_sym) kwargs[:context] = extract_context(callback_args) if tool_accepts_context?(name) public_send(name.to_sym, **kwargs) end |
#on_complete(session:, provider:, llm_response:, callback_args:, http_response:, request_id:) ⇒ 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.
Plumbing: adapter for the Callback contract. Do not override; implement
completed(response) instead.
575 576 577 578 579 580 581 582 583 584 585 586 587 |
# File 'lib/patient_llm/agent.rb', line 575 def on_complete(session:, provider:, llm_response:, callback_args:, http_response:, request_id:) prepare(session: session, provider: provider, callback_args: callback_args, http_response: http_response, request_id: request_id) response = Response.new( llm_response, session: session, output_schema: self.class.output_schema, http_response: http_response, http_request_id: http_response&.request_id, context: extract_context(callback_args) ) capture_result(:response, response) dispatch_hook(:completed, response) end |
#on_error(session:, provider:, callback_args:, error:, http_response:, request_id:) ⇒ 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.
Plumbing: adapter for the Callback contract. Do not override; implement
failed(failure) instead.
609 610 611 612 613 614 615 616 617 618 619 620 621 622 |
# File 'lib/patient_llm/agent.rb', line 609 def on_error(session:, provider:, callback_args:, error:, http_response:, request_id:) prepare(session: session, provider: provider, callback_args: callback_args, http_response: http_response, request_id: request_id) http_request_id = http_response&.request_id http_request_id ||= error.request_id if error.respond_to?(:request_id) failure = Failure.new( error, session: session, http_response: http_response, http_request_id: http_request_id, context: extract_context(callback_args) ) capture_result(:error, error) dispatch_hook(:failed, failure) end |
#on_tool_use(session:, provider:, llm_response:, callback_args:, http_response:, request_id:) ⇒ 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.
Plumbing: adapter for the Callback contract. Do not override; implement
tool_round(response) instead.
593 594 595 596 597 598 599 600 601 602 603 |
# File 'lib/patient_llm/agent.rb', line 593 def on_tool_use(session:, provider:, llm_response:, callback_args:, http_response:, request_id:) prepare(session: session, provider: provider, callback_args: callback_args, http_response: http_response, request_id: request_id) response = Response.new( llm_response, session: session, http_response: http_response, http_request_id: http_response&.request_id, context: extract_context(callback_args) ) dispatch_hook(:tool_round, response) end |
#prepare(session: nil, provider: nil, callback_args: nil, http_response: nil, 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.
Plumbing: called by Callback before hooks and tool execution to make the invocation state available to instance methods. Do not override.
556 557 558 559 560 561 562 563 564 565 566 567 568 569 |
# File 'lib/patient_llm/agent.rb', line 556 def prepare(session: nil, provider: nil, callback_args: nil, http_response: nil, request_id: nil) @session = session @provider = provider @callback_delegate = build_callback_delegate(callback_args) if @callback_delegate.is_a?(PatientLLM::Agent) @callback_delegate.prepare( session: session, provider: provider, callback_args: callback_args, http_response: http_response, request_id: request_id ) end end |
#tool_round(response) ⇒ void
This method returns an undefined value.
Hook: invoked once per automatic tool round, after the tools run and before the next request is issued. Override in your agent (or in a class passed as the :callback option to ask) to observe intermediate progress.
691 692 |
# File 'lib/patient_llm/agent.rb', line 691 def tool_round(response) end |