Class: LanguageOperator::Client::Base
- Inherits:
-
Object
- Object
- LanguageOperator::Client::Base
- Includes:
- Agent::Instrumentation, CostCalculator, MCPConnector, Loggable, Retryable
- Defined in:
- lib/language_operator/client/base.rb
Overview
Core MCP client that connects to multiple servers and manages LLM chat
This class handles all the backend logic for connecting to MCP servers, configuring the LLM, and managing chat sessions. It's designed to be UI-agnostic and reusable across different interfaces (CLI, web, headless).
Direct Known Subclasses
Constant Summary
Constants included from CostCalculator
Constants included from Retryable
Retryable::DEFAULT_BASE_DELAY, Retryable::DEFAULT_MAX_ATTEMPTS, Retryable::DEFAULT_MAX_DELAY
Instance Attribute Summary collapse
-
#chat ⇒ Object
readonly
Returns the value of attribute chat.
-
#clients ⇒ Object
readonly
Returns the value of attribute clients.
-
#config ⇒ Object
readonly
Returns the value of attribute config.
Instance Method Summary collapse
-
#cleanup_connections ⇒ void
Cleanup all MCP connections and reset client state.
-
#clear_history! ⇒ void
Clear chat history while keeping MCP connections.
-
#connect! ⇒ Hash
Connect to all enabled MCP servers and configure LLM.
-
#connected? ⇒ Boolean
Check if the client is connected.
-
#debug? ⇒ Boolean
Get debug mode status.
-
#initialize(config) ⇒ Base
constructor
Initialize the client with configuration.
-
#send_message(message) ⇒ String
Send a message and get the full response.
-
#servers_info ⇒ Array<Hash>
Get information about connected servers.
-
#stream_message(message) {|String| ... } ⇒ Object
Stream a message and yield each chunk.
-
#tools ⇒ Array
Get all available tools from connected servers.
Methods included from CostCalculator
Methods included from Retryable
#with_retry, #with_retry_or_nil
Methods included from Loggable
Constructor Details
#initialize(config) ⇒ Base
Initialize the client with configuration
44 45 46 47 48 49 50 51 52 53 54 |
# File 'lib/language_operator/client/base.rb', line 44 def initialize(config) @config = config.is_a?(String) ? Config.load(config) : config @clients = [] @chat = nil @debug = @config['debug'] || false logger.debug('Client initialized', debug: @debug, llm_provider: @config.dig('llm', 'provider'), llm_model: @config.dig('llm', 'model')) end |
Instance Attribute Details
#chat ⇒ Object (readonly)
Returns the value of attribute chat.
39 40 41 |
# File 'lib/language_operator/client/base.rb', line 39 def chat @chat end |
#clients ⇒ Object (readonly)
Returns the value of attribute clients.
39 40 41 |
# File 'lib/language_operator/client/base.rb', line 39 def clients @clients end |
#config ⇒ Object (readonly)
Returns the value of attribute config.
39 40 41 |
# File 'lib/language_operator/client/base.rb', line 39 def config @config end |
Instance Method Details
#cleanup_connections ⇒ void
This method returns an undefined value.
Cleanup all MCP connections and reset client state
This method properly closes all MCP client connections and clears the @clients array to prevent resource leaks. Should be called when the client is no longer needed.
178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 |
# File 'lib/language_operator/client/base.rb', line 178 def cleanup_connections return if @clients.empty? logger.debug('Cleaning up MCP connections', client_count: @clients.length) @clients.each do |client| # Close the client connection if it responds to close client.close if client.respond_to?(:close) rescue StandardError => e logger.warn('Error closing MCP client connection', client: client.class.name, error: e.) end # Clear the clients array and chat session @clients.clear @chat = nil logger.debug('MCP connections cleanup completed') end |
#clear_history! ⇒ void
This method returns an undefined value.
Clear chat history while keeping MCP connections
148 149 150 151 152 153 154 155 |
# File 'lib/language_operator/client/base.rb', line 148 def clear_history! llm_config = @config['llm'] chat_params = build_chat_params(llm_config) @chat = RubyLLM.chat(**chat_params) all_tools = tools @chat.with_tools(*all_tools) unless all_tools.empty? end |
#connect! ⇒ Hash
Connect to all enabled MCP servers and configure LLM
60 61 62 63 |
# File 'lib/language_operator/client/base.rb', line 60 def connect! configure_llm connect_mcp_servers end |
#connected? ⇒ Boolean
Check if the client is connected
160 161 162 |
# File 'lib/language_operator/client/base.rb', line 160 def connected? !@clients.empty? && !@chat.nil? end |
#debug? ⇒ Boolean
Get debug mode status
167 168 169 |
# File 'lib/language_operator/client/base.rb', line 167 def debug? @debug end |
#send_message(message) ⇒ String
Send a message and get the full response
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 |
# File 'lib/language_operator/client/base.rb', line 70 def () raise 'Not connected. Call #connect! first.' unless @chat model = @config.dig('llm', 'model') provider = @config.dig('llm', 'provider') with_span('agent.llm.request', attributes: { 'llm.model' => model, 'llm.provider' => provider, 'llm.message_count' => @chat.respond_to?(:messages) ? @chat..length : nil }) do |span| result = @chat.ask() # Add token usage and cost attributes if available if result.respond_to?(:input_tokens) input_tokens = result.input_tokens || 0 output_tokens = result.output_tokens || 0 cost = calculate_cost(model, input_tokens, output_tokens) span.set_attribute('llm.input_tokens', input_tokens) span.set_attribute('llm.output_tokens', output_tokens) span.set_attribute('llm.cost_usd', cost.round(6)) if cost end result end end |
#servers_info ⇒ Array<Hash>
Get information about connected servers
135 136 137 138 139 140 141 142 143 |
# File 'lib/language_operator/client/base.rb', line 135 def servers_info @clients.map do |client| { name: client.name, tool_count: client.tools.length, tools: client.tools.map(&:name) } end end |
#stream_message(message) {|String| ... } ⇒ Object
Stream a message and yield each chunk
103 104 105 106 107 108 109 110 111 112 113 114 |
# File 'lib/language_operator/client/base.rb', line 103 def (, &block) raise 'Not connected. Call #connect! first.' unless @chat # NOTE: RubyLLM may not support streaming yet, so we'll call ask and yield the full response response = @chat.ask() # Convert response to string if it's a RubyLLM::Message object response_text = response.respond_to?(:content) ? response.content : response.to_s block.call(response_text) if block_given? response_text end |
#tools ⇒ Array
Get all available tools from connected servers
Wraps MCP tools with OpenTelemetry instrumentation to trace tool executions.
121 122 123 124 125 126 127 128 129 130 |
# File 'lib/language_operator/client/base.rb', line 121 def tools raw_tools = @clients.flat_map(&:tools) # Wrap each tool with instrumentation if telemetry is enabled if ENV.fetch('OTEL_EXPORTER_OTLP_ENDPOINT', nil) raw_tools.map { |tool| wrap_tool_with_instrumentation(tool) } else raw_tools end end |