Class: LanguageOperator::Client::Base

Inherits:
Object
  • Object
show all
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).

Examples:

Basic usage

config = Config.load('config.yaml')
client = Base.new(config)
client.connect!
response = client.send_message("What tools are available?")

Streaming responses

client.stream_message("Search for Ruby news") do |chunk|
  print chunk
end

Direct Known Subclasses

Agent::Base

Constant Summary

Constants included from CostCalculator

CostCalculator::MODEL_PRICING

Constants included from Retryable

Retryable::DEFAULT_BASE_DELAY, Retryable::DEFAULT_MAX_ATTEMPTS, Retryable::DEFAULT_MAX_DELAY

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from CostCalculator

#calculate_cost

Methods included from Retryable

#with_retry, #with_retry_or_nil

Methods included from Loggable

#logger

Constructor Details

#initialize(config) ⇒ Base

Initialize the client with configuration

Parameters:

  • Configuration hash or path to YAML file



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

#chatObject (readonly)

Returns the value of attribute chat.



39
40
41
# File 'lib/language_operator/client/base.rb', line 39

def chat
  @chat
end

#clientsObject (readonly)

Returns the value of attribute clients.



39
40
41
# File 'lib/language_operator/client/base.rb', line 39

def clients
  @clients
end

#configObject (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_connectionsvoid

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.message)
  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

Returns:

  • Connection results with status and tool counts

Raises:

  • If LLM configuration fails



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

Returns:

  • True if connected to at least one server



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

Returns:

  • True if debug mode is enabled



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

Parameters:

  • User message

Returns:

  • Assistant response

Raises:

  • If message fails



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 send_message(message)
  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.messages.length : nil
            }) do |span|
    result = @chat.ask(message)

    # 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_infoArray<Hash>

Get information about connected servers

Returns:

  • Server information (name, url, tool_count)



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

Parameters:

  • User message

Yields:

  • (String)

    Each chunk of the response

Raises:

  • If streaming fails



103
104
105
106
107
108
109
110
111
112
113
114
# File 'lib/language_operator/client/base.rb', line 103

def stream_message(message, &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(message)

  # 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

#toolsArray

Get all available tools from connected servers

Wraps MCP tools with OpenTelemetry instrumentation to trace tool executions.

Returns:

  • Array of instrumented tool objects



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