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.
-
.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.
519 520 521 |
# File 'lib/patient_llm/agent.rb', line 519 def provider @provider end |
#session ⇒ PromptBuilder::Session? (readonly)
Returns the session for the current invocation.
516 517 518 |
# File 'lib/patient_llm/agent.rb', line 516 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).
375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 |
# File 'lib/patient_llm/agent.rb', line 375 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).
361 362 363 364 365 |
# File 'lib/patient_llm/agent.rb', line 361 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 |
.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.
407 408 409 |
# File 'lib/patient_llm/agent.rb', line 407 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
635 636 |
# File 'lib/patient_llm/agent.rb', line 635 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).
650 651 652 |
# File 'lib/patient_llm/agent.rb', line 650 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.
603 604 605 606 607 608 609 610 611 612 |
# File 'lib/patient_llm/agent.rb', line 603 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.
620 621 622 623 624 625 626 |
# File 'lib/patient_llm/agent.rb', line 620 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.
544 545 546 547 548 549 550 551 552 553 554 555 556 |
# File 'lib/patient_llm/agent.rb', line 544 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.
578 579 580 581 582 583 584 585 586 587 588 589 590 591 |
# File 'lib/patient_llm/agent.rb', line 578 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.
562 563 564 565 566 567 568 569 570 571 572 |
# File 'lib/patient_llm/agent.rb', line 562 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.
525 526 527 528 529 530 531 532 533 534 535 536 537 538 |
# File 'lib/patient_llm/agent.rb', line 525 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.
660 661 |
# File 'lib/patient_llm/agent.rb', line 660 def tool_round(response) end |