Class: RubyLLM::SemanticRouter::Router

Inherits:
Object
  • Object
show all
Defined in:
lib/rubyllm/semantic_router/router.rb

Overview

Main router class that routes messages to specialized agents

Examples:

router = RubyLLM::SemanticRouter.new(
  agents: {
    product: RubyLLM.chat(model: "gpt-4o-mini")
                    .with_instructions("You're a product expert..."),
    support: RubyLLM.chat(model: "gpt-4o")
                    .with_instructions("You help with issues...")
                    .with_tools(DiagnosticTool)
  },
  default_agent: :product
)
router.add_example("Show me products", agent: :product)
router.ask("What laptops do you have?")

Defined Under Namespace

Classes: InMemoryExample

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(agents:, default_agent:, fallback: nil, similarity_threshold: nil, embedding_model: nil, k_neighbors: nil, scope: nil, strategy: nil, examples: nil, find_examples: nil, max_words: nil, logger: nil, cache_ttl: nil, max_retries: nil, retry_base_delay: nil) ⇒ Router

Returns a new instance of Router.



46
47
48
49
50
51
52
53
54
55
56
57
58
59
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
# File 'lib/rubyllm/semantic_router/router.rb', line 46

def initialize(
  agents:,
  default_agent:,
  fallback: nil,
  similarity_threshold: nil,
  embedding_model: nil,
  k_neighbors: nil,
  scope: nil,
  strategy: nil,
  examples: nil,
  find_examples: nil,
  max_words: nil,
  logger: nil,
  cache_ttl: nil,
  max_retries: nil,
  retry_base_delay: nil
)
  @agents = normalize_agents(agents)
  @default_agent = default_agent.to_sym
  @current_agent = @default_agent
  @strategy = strategy || Strategies::Semantic.new
  @examples = examples || []
  @scope = scope
  @find_examples = find_examples

  validate_default_agent!

  global_config = SemanticRouter.configuration || Configuration.new

  @logger = logger || global_config.logger
  @max_retries = max_retries || global_config.max_retries
  @retry_base_delay = retry_base_delay || global_config.retry_base_delay

  # Set up embedding cache if TTL is configured
  ttl = cache_ttl || global_config.cache_ttl
  @embedding_cache = ttl ? EmbeddingCache.new(ttl: ttl) : nil

  @config = build_config(
    embedding_model: embedding_model,
    similarity_threshold: similarity_threshold,
    k_neighbors: k_neighbors,
    fallback: fallback,
    max_words: max_words
  )

  @chat = nil
  @last_routing_decision = nil

  log(:debug, "Router initialized with agents: #{@agents.keys.join(', ')}")
end

Instance Attribute Details

#agentsObject (readonly)

Returns the value of attribute agents.



41
42
43
# File 'lib/rubyllm/semantic_router/router.rb', line 41

def agents
  @agents
end

#current_agentObject (readonly)

Returns the value of attribute current_agent.



41
42
43
# File 'lib/rubyllm/semantic_router/router.rb', line 41

def current_agent
  @current_agent
end

#embedding_cacheObject (readonly)

Returns the value of attribute embedding_cache.



41
42
43
# File 'lib/rubyllm/semantic_router/router.rb', line 41

def embedding_cache
  @embedding_cache
end

#last_routing_decisionObject (readonly)

Returns the value of attribute last_routing_decision.



41
42
43
# File 'lib/rubyllm/semantic_router/router.rb', line 41

def last_routing_decision
  @last_routing_decision
end

Instance Method Details

#add_example(text, agent:) ⇒ self

Add a routing example

Parameters:

  • text (String)

    Example user message

  • agent (Symbol, String)

    Name of the agent this should route to

Returns:

  • (self)


159
160
161
162
163
164
165
166
167
168
169
170
171
# File 'lib/rubyllm/semantic_router/router.rb', line 159

def add_example(text, agent:)
  agent_name = agent.to_sym
  validate_agent_exists!(agent_name)

  embedding = generate_embedding(text)

  @examples << InMemoryExample.new(
    agent_name: agent_name,
    example_text: text,
    embedding: embedding
  )
  self
end

#agent(name) ⇒ Object

Get an agent config by name



250
251
252
# File 'lib/rubyllm/semantic_router/router.rb', line 250

def agent(name)
  @agents[name.to_sym]
end

#agent_namesObject

Get the names of all registered agents



245
246
247
# File 'lib/rubyllm/semantic_router/router.rb', line 245

def agent_names
  @agents.keys
end

#ask(message) {|chunk| ... } ⇒ RubyLLM::Message

Send a message to the router and get a response

Parameters:

  • message (String)

    The user’s message

Yields:

  • (chunk)

    Optional block for streaming responses

Returns:

  • (RubyLLM::Message)

    The response from the selected agent



102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
# File 'lib/rubyllm/semantic_router/router.rb', line 102

def ask(message, &block)
  log(:debug, "Routing message: #{message[0..100]}...")

  @last_routing_decision = route(message)

  log(:info, "Routed to :#{@last_routing_decision.agent} " \
             "(confidence: #{@last_routing_decision.confidence.round(3)}, " \
             "reason: #{@last_routing_decision.reason})")

  target_agent = @last_routing_decision.agent
  if target_agent != @current_agent
    log(:debug, "Switching from :#{@current_agent} to :#{target_agent}")
    switch_to(target_agent)
  end

  if @last_routing_decision.needs_clarification?
    inject_clarification_prompt
  end

  current_chat.ask(message, &block)
end

#ask_batch(messages) ⇒ Array<RoutingDecision>

Route multiple messages and return their routing decisions Useful for batch analysis or pre-routing without conversation

Parameters:

  • messages (Array<String>)

    Messages to route

Returns:



129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
# File 'lib/rubyllm/semantic_router/router.rb', line 129

def ask_batch(messages)
  log(:debug, "Batch routing #{messages.size} messages")

  # Generate embeddings for all messages at once
  truncated = messages.map { |m| truncate_to_max_words(m) }
  embeddings = generate_embeddings_batch_with_retry(truncated)

  # Route each message using its pre-computed embedding
  messages.each_with_index.map do |message, i|
    decision = @strategy.route(
      message,
      agents: @agents,
      examples: scoped_examples,
      current_agent: @current_agent,
      config: @config,
      find_examples: @find_examples,
      precomputed_embedding: embeddings[i]
    )

    log(:debug, "Batch[#{i}] -> :#{decision.agent} (confidence: #{decision.confidence.round(3)})")
    emit(:on_route, decision)
    decision
  end
end

#clear_examples!Object

Clear all routing examples



260
261
262
263
# File 'lib/rubyllm/semantic_router/router.rb', line 260

def clear_examples!
  @examples = []
  self
end

#current_chatObject

Get the current chat object



235
236
237
# File 'lib/rubyllm/semantic_router/router.rb', line 235

def current_chat
  @chat ||= build_chat_for_agent(@current_agent)
end

#debug_routing(message) ⇒ Hash

Get detailed routing info for debugging

Parameters:

  • message (String)

    The message to analyze

Returns:

  • (Hash)


208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
# File 'lib/rubyllm/semantic_router/router.rb', line 208

def debug_routing(message)
  embedding = generate_embedding(message)

  matches = if @examples.respond_to?(:nearest_neighbors)
    @examples.nearest_neighbors(:embedding, embedding, distance: :cosine)
             .limit(@config.k_neighbors * 2)
             .to_a
  else
    find_nearest_in_memory(@examples.to_a, embedding, @config.k_neighbors * 2)
  end

  {
    message: message,
    threshold: @config.similarity_threshold,
    current_agent: @current_agent,
    would_route_to: match(message).agent,
    top_matches: matches.map do |m|
      {
        agent: extract_agent_name(m),
        example: extract_example_text(m),
        confidence: calculate_confidence(m)
      }
    end
  }
end

#examplesObject

Get all routing examples



255
256
257
# File 'lib/rubyllm/semantic_router/router.rb', line 255

def examples
  @examples
end

#import_examples(examples) ⇒ self

Import multiple routing examples at once (batch embedding)

Parameters:

  • examples (Array<Hash>)

    Array of agent: hashes

Returns:

  • (self)


177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
# File 'lib/rubyllm/semantic_router/router.rb', line 177

def import_examples(examples)
  return self if examples.empty?

  examples.each { |e| validate_agent_exists!(e[:agent].to_sym) }

  texts = examples.map { |e| e[:text] }
  embeddings = generate_embeddings_batch(texts)

  examples.each_with_index do |example, i|
    @examples << InMemoryExample.new(
      agent_name: example[:agent].to_sym,
      example_text: example[:text],
      embedding: embeddings[i]
    )
  end

  self
end

#match(message) ⇒ RoutingDecision

Preview routing without sending the message

Parameters:

  • message (String)

    The message to test

Returns:



200
201
202
# File 'lib/rubyllm/semantic_router/router.rb', line 200

def match(message)
  route(message)
end

#messagesObject

Get all messages in the conversation



240
241
242
# File 'lib/rubyllm/semantic_router/router.rb', line 240

def messages
  current_chat.messages
end

#on(event, &block) ⇒ Object

Register event callbacks



292
293
294
295
296
# File 'lib/rubyllm/semantic_router/router.rb', line 292

def on(event, &block)
  @callbacks ||= {}
  @callbacks[event] = block
  self
end

#switch_to(agent_name) ⇒ Object

Manually switch to a specific agent



272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
# File 'lib/rubyllm/semantic_router/router.rb', line 272

def switch_to(agent_name)
  agent_name = agent_name.to_sym
  validate_agent_exists!(agent_name)

  return self if agent_name == @current_agent

  agent = @agents[agent_name]

  if @chat
    @chat.with_instructions(agent.instructions, replace: true)
    @chat.with_tools(*agent.tools, replace: true) if agent.tools.any?
    @chat.with_model(agent.model) if agent.model
    @chat.with_temperature(agent.temperature) if agent.temperature
  end

  @current_agent = agent_name
  self
end

#with_examples(source) ⇒ Object

Use an external examples source (e.g., ActiveRecord model)



266
267
268
269
# File 'lib/rubyllm/semantic_router/router.rb', line 266

def with_examples(source)
  @examples = source
  self
end