Class: LLM::Client
- Inherits:
-
Object
- Object
- LLM::Client
- Defined in:
- lib/llm/client.rb
Overview
Convenience layer over Providers::Anthropic for sending messages and handling tool execution loops.
Constant Summary collapse
- INTERRUPT_MESSAGE =
Synthetic tool_result when a tool is skipped because the human pressed Escape.
"Your human wants your attention"
Instance Attribute Summary collapse
-
#max_tokens ⇒ Integer
readonly
Maximum tokens in the response.
-
#model ⇒ String
readonly
The model identifier used for API calls.
-
#provider ⇒ Providers::Anthropic
readonly
The underlying API provider.
Instance Method Summary collapse
-
#chat_with_tools(messages, registry:, session_id:, first_response: nil, between_rounds: nil, **options) ⇒ Hash?
Send messages with tool support.
-
#initialize(model: Anima::Settings.model, max_tokens: Anima::Settings.max_tokens, provider: nil, logger: nil) ⇒ Client
constructor
A new instance of Client.
Constructor Details
#initialize(model: Anima::Settings.model, max_tokens: Anima::Settings.max_tokens, provider: nil, logger: nil) ⇒ Client
Returns a new instance of Client.
29 30 31 32 33 34 |
# File 'lib/llm/client.rb', line 29 def initialize(model: Anima::Settings.model, max_tokens: Anima::Settings.max_tokens, provider: nil, logger: nil) @provider = build_provider(provider) @model = model @max_tokens = max_tokens @logger = logger end |
Instance Attribute Details
#max_tokens ⇒ Integer (readonly)
Returns maximum tokens in the response.
22 23 24 |
# File 'lib/llm/client.rb', line 22 def max_tokens @max_tokens end |
#model ⇒ String (readonly)
Returns the model identifier used for API calls.
19 20 21 |
# File 'lib/llm/client.rb', line 19 def model @model end |
#provider ⇒ Providers::Anthropic (readonly)
Returns the underlying API provider.
16 17 18 |
# File 'lib/llm/client.rb', line 16 def provider @provider end |
Instance Method Details
#chat_with_tools(messages, registry:, session_id:, first_response: nil, between_rounds: nil, **options) ⇒ Hash?
Send messages with tool support. Runs the full tool execution loop: call LLM, execute any requested tools, feed results back, repeat until the LLM produces a final text response.
Emits Events::ToolCall and Events::ToolResponse events for each tool interaction so they’re persisted and visible in the event stream.
When the user interrupts via Escape, remaining tools receive synthetic “Your human wants your attention” results and the loop exits without another LLM call.
60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 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 114 115 116 117 118 |
# File 'lib/llm/client.rb', line 60 def chat_with_tools(, registry:, session_id:, first_response: nil, between_rounds: nil, **) = .dup rounds = 0 last_api_metrics = nil loop do rounds += 1 max_rounds = Anima::Settings.max_tool_rounds if rounds > max_rounds return {text: "[Tool loop exceeded #{max_rounds} rounds — halting]", api_metrics: last_api_metrics} end response = if first_response && rounds == 1 first_response else broadcast_session_state(session_id, "llm_generating") provider.( model: model, messages: , max_tokens: max_tokens, tools: registry.schemas, include_metrics: true, ** ) end # Capture api_metrics from ApiResponse wrapper (nil for pre-fetched first_response) last_api_metrics = response.api_metrics if response.respond_to?(:api_metrics) log(:debug, "stop_reason=#{response["stop_reason"]} content_types=#{(response["content"] || []).map { |b| b["type"] }.join(",")}") if response["stop_reason"] == "tool_use" tool_results = execute_tools(response, registry, session_id) promoted = promote_between_rounds(between_rounds) # Dual injection: user messages go as text blocks within the current # tool_results turn (same speaker); sub-agent messages append as # separate assistant→user turn pairs (distinct tool invocations). promoted[:texts].each { |text| tool_results << {type: "text", text: text} } += [ {role: "assistant", content: response["content"]}, {role: "user", content: tool_results} ] .concat(promoted[:pairs]) return nil if handle_interrupt!(session_id) else # Discard the text response if the user pressed Escape while # the API was generating it. Without this check the interrupt # flag set during the blocking API call would be silently # cleared by the ensure block in AgentRequestJob. return nil if handle_interrupt!(session_id) return {text: extract_text(response), api_metrics: last_api_metrics} end end end |