ruby_llm-agents 3.4.0 → 3.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Agents
5
+ module Pricing
6
+ # Normalizes OpenRouter bulk model list into the common pricing format.
7
+ #
8
+ # OpenRouter prices are **strings** representing USD per token.
9
+ # This adapter converts them to Float.
10
+ #
11
+ # Coverage: 400+ text LLM models, some audio-capable chat models.
12
+ # No transcription, image generation, or embedding models.
13
+ #
14
+ # @example
15
+ # OpenRouterAdapter.find_model("openai/gpt-4o")
16
+ # # => { input_cost_per_token: 0.0000025, output_cost_per_token: 0.00001, source: :openrouter }
17
+ #
18
+ module OpenRouterAdapter
19
+ extend self
20
+
21
+ # Find and normalize pricing for a model
22
+ #
23
+ # @param model_id [String] The model identifier
24
+ # @return [Hash, nil] Normalized pricing hash or nil
25
+ def find_model(model_id)
26
+ models = DataStore.openrouter_data
27
+ return nil unless models.is_a?(Array) && models.any?
28
+
29
+ entry = find_by_id(models, model_id)
30
+ return nil unless entry
31
+
32
+ normalize(entry)
33
+ end
34
+
35
+ private
36
+
37
+ def find_by_id(models, model_id)
38
+ normalized = model_id.to_s.downcase
39
+
40
+ # Exact match by id field
41
+ entry = models.find { |m| m["id"]&.downcase == normalized }
42
+ return entry if entry
43
+
44
+ # Try without provider prefix (e.g., "gpt-4o" matches "openai/gpt-4o")
45
+ entry = models.find do |m|
46
+ id = m["id"].to_s.downcase
47
+ id.end_with?("/#{normalized}") || id == normalized
48
+ end
49
+ return entry if entry
50
+
51
+ # Try with common provider prefixes
52
+ prefixes = %w[openai anthropic google meta-llama mistralai cohere deepseek]
53
+ prefixes.each do |prefix|
54
+ entry = models.find { |m| m["id"]&.downcase == "#{prefix}/#{normalized}" }
55
+ return entry if entry
56
+ end
57
+
58
+ nil
59
+ end
60
+
61
+ def normalize(entry)
62
+ pricing = entry["pricing"]
63
+ return nil unless pricing.is_a?(Hash)
64
+
65
+ result = {source: :openrouter}
66
+
67
+ prompt_cost = safe_float(pricing["prompt"])
68
+ completion_cost = safe_float(pricing["completion"])
69
+
70
+ result[:input_cost_per_token] = prompt_cost if prompt_cost&.positive?
71
+ result[:output_cost_per_token] = completion_cost if completion_cost&.positive?
72
+
73
+ if pricing["image"]
74
+ image_cost = safe_float(pricing["image"])
75
+ result[:image_cost_raw] = image_cost if image_cost&.positive?
76
+ end
77
+
78
+ (result.keys.size > 1) ? result : nil
79
+ end
80
+
81
+ def safe_float(value)
82
+ return nil if value.nil?
83
+ Float(value)
84
+ rescue ArgumentError, TypeError
85
+ nil
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Agents
5
+ module Pricing
6
+ # Normalizes Portkey AI per-model pricing into the common format.
7
+ #
8
+ # Portkey prices are in **cents per token**. This adapter converts to
9
+ # USD per token for consistency with other adapters.
10
+ #
11
+ # Requires knowing the provider for a model, resolved via PROVIDER_MAP.
12
+ #
13
+ # @example
14
+ # PortkeyAdapter.find_model("gpt-4o")
15
+ # # => { input_cost_per_token: 0.0000025, output_cost_per_token: 0.00001, source: :portkey }
16
+ #
17
+ module PortkeyAdapter
18
+ extend self
19
+
20
+ PROVIDER_MAP = [
21
+ [/^(gpt-|o1|o3|o4|whisper|dall-e|tts-|chatgpt)/, "openai"],
22
+ [/^claude/, "anthropic"],
23
+ [/^gemini/, "google"],
24
+ [/^(mistral|codestral|pixtral|ministral)/, "mistralai"],
25
+ [/^llama/, "meta"],
26
+ [/^(command|embed)/, "cohere"],
27
+ [/^deepseek/, "deepseek"],
28
+ [/^(nova|titan)/, "amazon"]
29
+ ].freeze
30
+
31
+ # Find and normalize pricing for a model
32
+ #
33
+ # @param model_id [String] The model identifier
34
+ # @return [Hash, nil] Normalized pricing hash or nil
35
+ def find_model(model_id)
36
+ provider, model_name = resolve_provider(model_id)
37
+ return nil unless provider
38
+
39
+ raw = DataStore.portkey_data(provider, model_name)
40
+ return nil unless raw.is_a?(Hash) && raw["pay_as_you_go"]
41
+
42
+ normalize(raw)
43
+ end
44
+
45
+ private
46
+
47
+ def resolve_provider(model_id)
48
+ id = model_id.to_s
49
+
50
+ # Handle prefixed model IDs like "azure/gpt-4o" or "groq/llama-3"
51
+ if id.include?("/")
52
+ parts = id.split("/", 2)
53
+ return [parts[0], parts[1]]
54
+ end
55
+
56
+ PROVIDER_MAP.each do |pattern, provider|
57
+ return [provider, id] if id.match?(pattern)
58
+ end
59
+
60
+ nil
61
+ end
62
+
63
+ def normalize(raw)
64
+ pag = raw["pay_as_you_go"]
65
+ return nil unless pag
66
+
67
+ result = {source: :portkey}
68
+
69
+ # Main text token pricing (cents → USD)
70
+ req_price = dig_price(pag, "request_token", "price")
71
+ resp_price = dig_price(pag, "response_token", "price")
72
+ result[:input_cost_per_token] = req_price / 100.0 if req_price&.positive?
73
+ result[:output_cost_per_token] = resp_price / 100.0 if resp_price&.positive?
74
+
75
+ # Additional units (audio tokens, etc.)
76
+ additional = pag["additional_units"]
77
+ if additional.is_a?(Hash)
78
+ audio_in = dig_price(additional, "request_audio_token", "price")
79
+ audio_out = dig_price(additional, "response_audio_token", "price")
80
+ result[:input_cost_per_audio_token] = audio_in / 100.0 if audio_in&.positive?
81
+ result[:output_cost_per_audio_token] = audio_out / 100.0 if audio_out&.positive?
82
+ end
83
+
84
+ (result.keys.size > 1) ? result : nil
85
+ end
86
+
87
+ def dig_price(hash, *keys)
88
+ value = hash.dig(*keys)
89
+ value.is_a?(Numeric) ? value : nil
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Agents
5
+ module Pricing
6
+ # Extracts pricing from the ruby_llm gem's built-in model registry.
7
+ #
8
+ # This is a local, zero-HTTP-cost source that provides pricing for
9
+ # models that ruby_llm knows about. It's the fastest adapter since
10
+ # all data is already loaded in-process.
11
+ #
12
+ # Uses RubyLLM::Models.find(model_id) which returns pricing as
13
+ # USD per million tokens.
14
+ #
15
+ # @example
16
+ # RubyLLMAdapter.find_model("gpt-4o")
17
+ # # => { input_cost_per_token: 0.0000025, output_cost_per_token: 0.00001, source: :ruby_llm }
18
+ #
19
+ module RubyLLMAdapter
20
+ extend self
21
+
22
+ # Find and normalize pricing for a model from ruby_llm's registry
23
+ #
24
+ # @param model_id [String] The model identifier
25
+ # @return [Hash, nil] Normalized pricing hash or nil
26
+ def find_model(model_id)
27
+ return nil unless defined?(::RubyLLM::Models)
28
+
29
+ model_info = ::RubyLLM::Models.find(model_id)
30
+ return nil unless model_info
31
+
32
+ normalize(model_info)
33
+ rescue
34
+ nil
35
+ end
36
+
37
+ private
38
+
39
+ def normalize(model_info)
40
+ pricing = model_info.pricing
41
+ return nil unless pricing
42
+
43
+ result = {source: :ruby_llm}
44
+
45
+ # Text tokens (per million → per token)
46
+ text_tokens = pricing.respond_to?(:text_tokens) ? pricing.text_tokens : nil
47
+ if text_tokens
48
+ input_per_million = text_tokens.respond_to?(:input) ? text_tokens.input : nil
49
+ output_per_million = text_tokens.respond_to?(:output) ? text_tokens.output : nil
50
+
51
+ if input_per_million.is_a?(Numeric) && input_per_million.positive?
52
+ result[:input_cost_per_token] = input_per_million / 1_000_000.0
53
+ end
54
+
55
+ if output_per_million.is_a?(Numeric) && output_per_million.positive?
56
+ result[:output_cost_per_token] = output_per_million / 1_000_000.0
57
+ end
58
+ end
59
+
60
+ # Audio tokens (per million → per token) if available
61
+ audio_tokens = pricing.respond_to?(:audio_tokens) ? pricing.audio_tokens : nil
62
+ if audio_tokens
63
+ audio_input = audio_tokens.respond_to?(:input) ? audio_tokens.input : nil
64
+ audio_output = audio_tokens.respond_to?(:output) ? audio_tokens.output : nil
65
+
66
+ if audio_input.is_a?(Numeric) && audio_input.positive?
67
+ result[:input_cost_per_audio_token] = audio_input / 1_000_000.0
68
+ end
69
+
70
+ if audio_output.is_a?(Numeric) && audio_output.positive?
71
+ result[:output_cost_per_audio_token] = audio_output / 1_000_000.0
72
+ end
73
+ end
74
+
75
+ # Image pricing if available
76
+ images = pricing.respond_to?(:images) ? pricing.images : nil
77
+ if images
78
+ per_image = images.respond_to?(:input) ? images.input : nil
79
+ if per_image.is_a?(Numeric) && per_image.positive?
80
+ result[:input_cost_per_image] = per_image
81
+ end
82
+ end
83
+
84
+ # Mode from model type if available
85
+ if model_info.respond_to?(:type)
86
+ result[:mode] = model_info.type.to_s
87
+ end
88
+
89
+ (result.keys.size > 1) ? result : nil
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Agents
5
+ module Routing
6
+ # Class-level DSL for defining routes and classification categories.
7
+ #
8
+ # Extended into any BaseAgent subclass that includes the Routing concern.
9
+ # Provides `route`, `default_route`, and accessor methods for route definitions.
10
+ #
11
+ # @example
12
+ # class SupportRouter < RubyLLM::Agents::BaseAgent
13
+ # include RubyLLM::Agents::Routing
14
+ #
15
+ # route :billing, "Billing, charges, refunds"
16
+ # route :technical, "Bugs, errors, crashes"
17
+ # default_route :general
18
+ # end
19
+ #
20
+ module ClassMethods
21
+ # Define a classification route.
22
+ #
23
+ # @param name [Symbol] Route identifier
24
+ # @param description [String] What messages belong to this route
25
+ # @param agent [Class, nil] Optional agent class to map to this route
26
+ #
27
+ # @example Simple route
28
+ # route :billing, "Billing, charges, refunds"
29
+ #
30
+ # @example Route with agent mapping
31
+ # route :billing, "Billing questions", agent: BillingAgent
32
+ #
33
+ def route(name, description, agent: nil)
34
+ @routes ||= {}
35
+ @routes[name.to_sym] = {
36
+ description: description,
37
+ agent: agent
38
+ }
39
+ end
40
+
41
+ # Set the default route for unmatched classifications.
42
+ #
43
+ # @param name [Symbol] Default route name
44
+ # @param agent [Class, nil] Optional default agent class
45
+ #
46
+ def default_route(name, agent: nil)
47
+ @default_route_name = name.to_sym
48
+ @routes ||= {}
49
+ @routes[name.to_sym] ||= {
50
+ description: "Default / general category",
51
+ agent: agent
52
+ }
53
+ end
54
+
55
+ # Returns all defined routes (including inherited).
56
+ #
57
+ # @return [Hash{Symbol => Hash}] Route definitions
58
+ def routes
59
+ parent = superclass.respond_to?(:routes) ? superclass.routes : {}
60
+ parent.merge(@routes || {})
61
+ end
62
+
63
+ # Returns the default route name (including inherited).
64
+ #
65
+ # @return [Symbol] The default route name
66
+ def default_route_name
67
+ @default_route_name || (superclass.respond_to?(:default_route_name) ? superclass.default_route_name : :general)
68
+ end
69
+
70
+ # Returns :router for instrumentation/tracking.
71
+ #
72
+ # @return [Symbol]
73
+ def agent_type
74
+ :router
75
+ end
76
+
77
+ # Override call to accept message: as a named param.
78
+ #
79
+ # @param message [String, nil] The message to classify
80
+ # @param kwargs [Hash] Additional options
81
+ # @return [RoutingResult] The classification result
82
+ def call(message: nil, **kwargs, &block)
83
+ if message
84
+ super(**kwargs.merge(message: message), &block)
85
+ else
86
+ super(**kwargs, &block)
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Agents
5
+ module Routing
6
+ # Wraps a standard Result with routing-specific accessors.
7
+ #
8
+ # Delegates all standard Result methods (tokens, cost, timing, etc.)
9
+ # to the underlying result, adding only the route-specific interface.
10
+ #
11
+ # @example
12
+ # result = SupportRouter.call(message: "I was charged twice")
13
+ # result.route # => :billing
14
+ # result.agent_class # => BillingAgent (if mapped)
15
+ # result.success? # => true
16
+ # result.total_cost # => 0.0001
17
+ #
18
+ class RoutingResult < Result
19
+ # @return [Symbol] The classified route name
20
+ attr_reader :route
21
+
22
+ # @return [Class, nil] The mapped agent class (if defined via `agent:`)
23
+ attr_reader :agent_class
24
+
25
+ # @return [String] The raw text response from the LLM
26
+ attr_reader :raw_response
27
+
28
+ # Creates a new RoutingResult by wrapping a base Result with route data.
29
+ #
30
+ # @param base_result [Result] The standard Result from BaseAgent execution
31
+ # @param route_data [Hash] Parsed route information
32
+ # @option route_data [Symbol] :route The classified route name
33
+ # @option route_data [Class, nil] :agent_class Mapped agent class
34
+ # @option route_data [String] :raw_response Raw LLM text
35
+ def initialize(base_result:, route_data:)
36
+ super(
37
+ content: route_data,
38
+ input_tokens: base_result.input_tokens,
39
+ output_tokens: base_result.output_tokens,
40
+ input_cost: base_result.input_cost,
41
+ output_cost: base_result.output_cost,
42
+ total_cost: base_result.total_cost,
43
+ model_id: base_result.model_id,
44
+ chosen_model_id: base_result.chosen_model_id,
45
+ temperature: base_result.temperature,
46
+ started_at: base_result.started_at,
47
+ completed_at: base_result.completed_at,
48
+ duration_ms: base_result.duration_ms,
49
+ finish_reason: base_result.finish_reason,
50
+ streaming: base_result.streaming,
51
+ error_class: base_result.error_class,
52
+ error_message: base_result.error_message,
53
+ attempts_count: base_result.attempts_count
54
+ )
55
+
56
+ @route = route_data[:route]
57
+ @agent_class = route_data[:agent_class]
58
+ @raw_response = route_data[:raw_response]
59
+ end
60
+
61
+ # Converts the result to a hash including routing fields.
62
+ #
63
+ # @return [Hash] All result data plus route, agent_class, raw_response
64
+ def to_h
65
+ super.merge(
66
+ route: route,
67
+ agent_class: agent_class&.name,
68
+ raw_response: raw_response
69
+ )
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "routing/class_methods"
4
+ require_relative "routing/result"
5
+
6
+ module RubyLLM
7
+ module Agents
8
+ # Adds classification & routing capabilities to any BaseAgent.
9
+ #
10
+ # Include this module in a BaseAgent subclass to get:
11
+ # - `route` DSL for defining classification categories
12
+ # - `default_route` for fallback classification
13
+ # - Auto-generated system/user prompts from route definitions
14
+ # - Structured output parsing to return a RoutingResult
15
+ #
16
+ # All existing BaseAgent features (caching, reliability, retries,
17
+ # fallback models, instrumentation, multi-tenancy) work unchanged.
18
+ #
19
+ # @example Class-based router
20
+ # class SupportRouter < RubyLLM::Agents::BaseAgent
21
+ # include RubyLLM::Agents::Routing
22
+ #
23
+ # model "gpt-4o-mini"
24
+ # temperature 0.0
25
+ #
26
+ # route :billing, "Billing, charges, refunds"
27
+ # route :technical, "Bugs, errors, crashes"
28
+ # default_route :general
29
+ # end
30
+ #
31
+ # result = SupportRouter.call(message: "I was charged twice")
32
+ # result.route # => :billing
33
+ #
34
+ # @example Inline classification
35
+ # route = RubyLLM::Agents::Routing.classify(
36
+ # message: "I was charged twice",
37
+ # routes: { billing: "Billing issues", technical: "Tech issues" },
38
+ # default: :general
39
+ # )
40
+ # # => :billing
41
+ #
42
+ module Routing
43
+ def self.included(base)
44
+ unless base < BaseAgent
45
+ raise ArgumentError, "#{base} must inherit from RubyLLM::Agents::BaseAgent to include Routing"
46
+ end
47
+
48
+ base.extend(ClassMethods)
49
+ base.param(:message, required: false)
50
+ end
51
+
52
+ # Classify a message without defining a router class.
53
+ #
54
+ # Creates an anonymous BaseAgent subclass with Routing included,
55
+ # calls it, and returns just the route symbol.
56
+ #
57
+ # @param message [String] The message to classify
58
+ # @param routes [Hash{Symbol => String}] Route names to descriptions
59
+ # @param default [Symbol] Default route (:general)
60
+ # @param model [String] LLM model to use ("gpt-4o-mini")
61
+ # @param options [Hash] Extra options passed to .call
62
+ # @return [Symbol] The classified route name
63
+ def self.classify(message:, routes:, default: :general, model: "gpt-4o-mini", **options)
64
+ router_model = model
65
+ router = Class.new(BaseAgent) do
66
+ include Routing
67
+
68
+ self.model router_model
69
+ temperature 0.0
70
+
71
+ routes.each do |name, desc|
72
+ route name, desc
73
+ end
74
+ default_route default
75
+ end
76
+
77
+ result = router.call(message: message, **options)
78
+ result.route
79
+ end
80
+
81
+ # Helper to get the auto-generated system prompt for routing.
82
+ # Use this in custom system_prompt overrides to include route definitions.
83
+ #
84
+ # @return [String] The auto-generated routing system prompt
85
+ def routing_system_prompt
86
+ default = self.class.default_route_name
87
+
88
+ <<~PROMPT.strip
89
+ You are a message classifier. Classify the user's message into exactly one of the following categories:
90
+
91
+ #{routing_categories_text}
92
+
93
+ If none of the categories clearly match, classify as: #{default}
94
+
95
+ Respond with ONLY the category name, nothing else.
96
+ PROMPT
97
+ end
98
+
99
+ # Helper to get formatted route categories for use in custom prompts.
100
+ #
101
+ # @return [String] Formatted list of route categories
102
+ def routing_categories_text
103
+ self.class.routes.map do |name, config|
104
+ "- #{name}: #{config[:description]}"
105
+ end.join("\n")
106
+ end
107
+
108
+ # Auto-generated system_prompt (used if subclass doesn't override).
109
+ def system_prompt
110
+ super || routing_system_prompt
111
+ end
112
+
113
+ # Auto-generated user_prompt from the :message param.
114
+ def user_prompt
115
+ @ask_message || options[:message] || super
116
+ end
117
+
118
+ # Override process_response to parse the route from LLM output.
119
+ def process_response(response)
120
+ raw = response.content.to_s.strip.downcase.gsub(/[^a-z0-9_]/, "")
121
+ route_name = raw.to_sym
122
+
123
+ valid_routes = self.class.routes.keys
124
+ route_name = self.class.default_route_name unless valid_routes.include?(route_name)
125
+
126
+ {
127
+ route: route_name,
128
+ agent_class: self.class.routes.dig(route_name, :agent),
129
+ raw_response: response.content.to_s.strip
130
+ }
131
+ end
132
+
133
+ # Override build_result to return a RoutingResult.
134
+ def build_result(content, response, context)
135
+ base = super
136
+ RoutingResult.new(base_result: base, route_data: content)
137
+ end
138
+ end
139
+ end
140
+ end
@@ -57,6 +57,9 @@ require_relative "agents/results/image_pipeline_result"
57
57
  require_relative "agents/image/concerns/image_operation_dsl"
58
58
  require_relative "agents/image/concerns/image_operation_execution"
59
59
 
60
+ # Routing concern (classification & routing)
61
+ require_relative "agents/routing"
62
+
60
63
  # Text agents
61
64
  require_relative "agents/text/embedder"
62
65
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby_llm-agents
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.4.0
4
+ version: 3.5.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - adham90
@@ -102,6 +102,7 @@ files:
102
102
  - app/views/ruby_llm/agents/agents/_config_embedder.html.erb
103
103
  - app/views/ruby_llm/agents/agents/_config_image_generator.html.erb
104
104
  - app/views/ruby_llm/agents/agents/_config_moderator.html.erb
105
+ - app/views/ruby_llm/agents/agents/_config_router.html.erb
105
106
  - app/views/ruby_llm/agents/agents/_config_speaker.html.erb
106
107
  - app/views/ruby_llm/agents/agents/_config_transcriber.html.erb
107
108
  - app/views/ruby_llm/agents/agents/_empty_state.html.erb
@@ -215,6 +216,7 @@ files:
215
216
  - lib/ruby_llm/agents/audio/speech_client.rb
216
217
  - lib/ruby_llm/agents/audio/speech_pricing.rb
217
218
  - lib/ruby_llm/agents/audio/transcriber.rb
219
+ - lib/ruby_llm/agents/audio/transcription_pricing.rb
218
220
  - lib/ruby_llm/agents/base_agent.rb
219
221
  - lib/ruby_llm/agents/core/base.rb
220
222
  - lib/ruby_llm/agents/core/base/callbacks.rb
@@ -282,6 +284,13 @@ files:
282
284
  - lib/ruby_llm/agents/pipeline/middleware/instrumentation.rb
283
285
  - lib/ruby_llm/agents/pipeline/middleware/reliability.rb
284
286
  - lib/ruby_llm/agents/pipeline/middleware/tenant.rb
287
+ - lib/ruby_llm/agents/pricing/data_store.rb
288
+ - lib/ruby_llm/agents/pricing/helicone_adapter.rb
289
+ - lib/ruby_llm/agents/pricing/litellm_adapter.rb
290
+ - lib/ruby_llm/agents/pricing/llmpricing_adapter.rb
291
+ - lib/ruby_llm/agents/pricing/openrouter_adapter.rb
292
+ - lib/ruby_llm/agents/pricing/portkey_adapter.rb
293
+ - lib/ruby_llm/agents/pricing/ruby_llm_adapter.rb
285
294
  - lib/ruby_llm/agents/rails/engine.rb
286
295
  - lib/ruby_llm/agents/results/background_removal_result.rb
287
296
  - lib/ruby_llm/agents/results/base.rb
@@ -295,6 +304,9 @@ files:
295
304
  - lib/ruby_llm/agents/results/image_variation_result.rb
296
305
  - lib/ruby_llm/agents/results/speech_result.rb
297
306
  - lib/ruby_llm/agents/results/transcription_result.rb
307
+ - lib/ruby_llm/agents/routing.rb
308
+ - lib/ruby_llm/agents/routing/class_methods.rb
309
+ - lib/ruby_llm/agents/routing/result.rb
298
310
  - lib/ruby_llm/agents/text/embedder.rb
299
311
  - lib/tasks/ruby_llm_agents.rake
300
312
  homepage: https://github.com/adham90/ruby_llm-agents