ruby_llm-agents 0.5.0 → 1.0.0.beta.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.
Files changed (190) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +189 -31
  3. data/app/controllers/ruby_llm/agents/agents_controller.rb +136 -16
  4. data/app/controllers/ruby_llm/agents/dashboard_controller.rb +29 -9
  5. data/app/controllers/ruby_llm/agents/workflows_controller.rb +355 -0
  6. data/app/helpers/ruby_llm/agents/application_helper.rb +25 -0
  7. data/app/models/ruby_llm/agents/execution.rb +3 -0
  8. data/app/models/ruby_llm/agents/tenant_budget.rb +58 -15
  9. data/app/services/ruby_llm/agents/agent_registry.rb +51 -12
  10. data/app/views/layouts/ruby_llm/agents/application.html.erb +2 -29
  11. data/app/views/ruby_llm/agents/agents/_agent.html.erb +13 -1
  12. data/app/views/ruby_llm/agents/agents/_config_agent.html.erb +235 -0
  13. data/app/views/ruby_llm/agents/agents/_config_embedder.html.erb +70 -0
  14. data/app/views/ruby_llm/agents/agents/_config_image_generator.html.erb +152 -0
  15. data/app/views/ruby_llm/agents/agents/_config_moderator.html.erb +63 -0
  16. data/app/views/ruby_llm/agents/agents/_config_speaker.html.erb +108 -0
  17. data/app/views/ruby_llm/agents/agents/_config_transcriber.html.erb +91 -0
  18. data/app/views/ruby_llm/agents/agents/_workflow.html.erb +1 -1
  19. data/app/views/ruby_llm/agents/agents/index.html.erb +74 -9
  20. data/app/views/ruby_llm/agents/agents/show.html.erb +18 -378
  21. data/app/views/ruby_llm/agents/dashboard/_agent_comparison.html.erb +269 -15
  22. data/app/views/ruby_llm/agents/executions/show.html.erb +16 -0
  23. data/app/views/ruby_llm/agents/shared/_agent_type_badge.html.erb +93 -0
  24. data/app/views/ruby_llm/agents/workflows/_step_performance.html.erb +236 -0
  25. data/app/views/ruby_llm/agents/workflows/_structure_parallel.html.erb +76 -0
  26. data/app/views/ruby_llm/agents/workflows/_structure_pipeline.html.erb +74 -0
  27. data/app/views/ruby_llm/agents/workflows/_structure_router.html.erb +108 -0
  28. data/app/views/ruby_llm/agents/workflows/show.html.erb +442 -0
  29. data/config/routes.rb +1 -0
  30. data/lib/generators/ruby_llm_agents/agent_generator.rb +56 -7
  31. data/lib/generators/ruby_llm_agents/background_remover_generator.rb +110 -0
  32. data/lib/generators/ruby_llm_agents/embedder_generator.rb +107 -0
  33. data/lib/generators/ruby_llm_agents/image_analyzer_generator.rb +115 -0
  34. data/lib/generators/ruby_llm_agents/image_editor_generator.rb +108 -0
  35. data/lib/generators/ruby_llm_agents/image_generator_generator.rb +116 -0
  36. data/lib/generators/ruby_llm_agents/image_pipeline_generator.rb +178 -0
  37. data/lib/generators/ruby_llm_agents/image_transformer_generator.rb +109 -0
  38. data/lib/generators/ruby_llm_agents/image_upscaler_generator.rb +103 -0
  39. data/lib/generators/ruby_llm_agents/image_variator_generator.rb +102 -0
  40. data/lib/generators/ruby_llm_agents/install_generator.rb +76 -4
  41. data/lib/generators/ruby_llm_agents/restructure_generator.rb +292 -0
  42. data/lib/generators/ruby_llm_agents/speaker_generator.rb +121 -0
  43. data/lib/generators/ruby_llm_agents/templates/add_execution_type_migration.rb.tt +8 -0
  44. data/lib/generators/ruby_llm_agents/templates/agent.rb.tt +99 -84
  45. data/lib/generators/ruby_llm_agents/templates/application_agent.rb.tt +42 -40
  46. data/lib/generators/ruby_llm_agents/templates/application_background_remover.rb.tt +26 -0
  47. data/lib/generators/ruby_llm_agents/templates/application_embedder.rb.tt +50 -0
  48. data/lib/generators/ruby_llm_agents/templates/application_image_analyzer.rb.tt +26 -0
  49. data/lib/generators/ruby_llm_agents/templates/application_image_editor.rb.tt +20 -0
  50. data/lib/generators/ruby_llm_agents/templates/application_image_generator.rb.tt +38 -0
  51. data/lib/generators/ruby_llm_agents/templates/application_image_pipeline.rb.tt +139 -0
  52. data/lib/generators/ruby_llm_agents/templates/application_image_transformer.rb.tt +21 -0
  53. data/lib/generators/ruby_llm_agents/templates/application_image_upscaler.rb.tt +20 -0
  54. data/lib/generators/ruby_llm_agents/templates/application_image_variator.rb.tt +20 -0
  55. data/lib/generators/ruby_llm_agents/templates/application_speaker.rb.tt +49 -0
  56. data/lib/generators/ruby_llm_agents/templates/application_transcriber.rb.tt +53 -0
  57. data/lib/generators/ruby_llm_agents/templates/background_remover.rb.tt +44 -0
  58. data/lib/generators/ruby_llm_agents/templates/embedder.rb.tt +41 -0
  59. data/lib/generators/ruby_llm_agents/templates/image_analyzer.rb.tt +45 -0
  60. data/lib/generators/ruby_llm_agents/templates/image_editor.rb.tt +35 -0
  61. data/lib/generators/ruby_llm_agents/templates/image_generator.rb.tt +47 -0
  62. data/lib/generators/ruby_llm_agents/templates/image_pipeline.rb.tt +50 -0
  63. data/lib/generators/ruby_llm_agents/templates/image_transformer.rb.tt +44 -0
  64. data/lib/generators/ruby_llm_agents/templates/image_upscaler.rb.tt +38 -0
  65. data/lib/generators/ruby_llm_agents/templates/image_variator.rb.tt +33 -0
  66. data/lib/generators/ruby_llm_agents/templates/skills/AGENTS.md.tt +228 -0
  67. data/lib/generators/ruby_llm_agents/templates/skills/BACKGROUND_REMOVERS.md.tt +131 -0
  68. data/lib/generators/ruby_llm_agents/templates/skills/EMBEDDERS.md.tt +255 -0
  69. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_ANALYZERS.md.tt +120 -0
  70. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_EDITORS.md.tt +102 -0
  71. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_GENERATORS.md.tt +282 -0
  72. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_PIPELINES.md.tt +228 -0
  73. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_TRANSFORMERS.md.tt +120 -0
  74. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_UPSCALERS.md.tt +110 -0
  75. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_VARIATORS.md.tt +120 -0
  76. data/lib/generators/ruby_llm_agents/templates/skills/SPEAKERS.md.tt +212 -0
  77. data/lib/generators/ruby_llm_agents/templates/skills/TOOLS.md.tt +227 -0
  78. data/lib/generators/ruby_llm_agents/templates/skills/TRANSCRIBERS.md.tt +251 -0
  79. data/lib/generators/ruby_llm_agents/templates/skills/WORKFLOWS.md.tt +300 -0
  80. data/lib/generators/ruby_llm_agents/templates/speaker.rb.tt +56 -0
  81. data/lib/generators/ruby_llm_agents/templates/transcriber.rb.tt +51 -0
  82. data/lib/generators/ruby_llm_agents/transcriber_generator.rb +107 -0
  83. data/lib/generators/ruby_llm_agents/upgrade_generator.rb +152 -1
  84. data/lib/ruby_llm/agents/audio/speaker.rb +553 -0
  85. data/lib/ruby_llm/agents/audio/transcriber.rb +669 -0
  86. data/lib/ruby_llm/agents/base_agent.rb +675 -0
  87. data/lib/ruby_llm/agents/core/base/moderation_dsl.rb +181 -0
  88. data/lib/ruby_llm/agents/core/base/moderation_execution.rb +274 -0
  89. data/lib/ruby_llm/agents/core/base.rb +135 -0
  90. data/lib/ruby_llm/agents/core/configuration.rb +981 -0
  91. data/lib/ruby_llm/agents/core/errors.rb +150 -0
  92. data/lib/ruby_llm/agents/{instrumentation.rb → core/instrumentation.rb} +22 -1
  93. data/lib/ruby_llm/agents/core/llm_tenant.rb +358 -0
  94. data/lib/ruby_llm/agents/{version.rb → core/version.rb} +1 -1
  95. data/lib/ruby_llm/agents/dsl/base.rb +110 -0
  96. data/lib/ruby_llm/agents/dsl/caching.rb +142 -0
  97. data/lib/ruby_llm/agents/dsl/reliability.rb +307 -0
  98. data/lib/ruby_llm/agents/dsl.rb +41 -0
  99. data/lib/ruby_llm/agents/image/analyzer/dsl.rb +130 -0
  100. data/lib/ruby_llm/agents/image/analyzer/execution.rb +402 -0
  101. data/lib/ruby_llm/agents/image/analyzer.rb +90 -0
  102. data/lib/ruby_llm/agents/image/background_remover/dsl.rb +154 -0
  103. data/lib/ruby_llm/agents/image/background_remover/execution.rb +240 -0
  104. data/lib/ruby_llm/agents/image/background_remover.rb +89 -0
  105. data/lib/ruby_llm/agents/image/concerns/image_operation_dsl.rb +91 -0
  106. data/lib/ruby_llm/agents/image/concerns/image_operation_execution.rb +165 -0
  107. data/lib/ruby_llm/agents/image/editor/dsl.rb +56 -0
  108. data/lib/ruby_llm/agents/image/editor/execution.rb +207 -0
  109. data/lib/ruby_llm/agents/image/editor.rb +92 -0
  110. data/lib/ruby_llm/agents/image/generator/active_storage_support.rb +127 -0
  111. data/lib/ruby_llm/agents/image/generator/content_policy.rb +95 -0
  112. data/lib/ruby_llm/agents/image/generator/pricing.rb +353 -0
  113. data/lib/ruby_llm/agents/image/generator/templates.rb +124 -0
  114. data/lib/ruby_llm/agents/image/generator.rb +455 -0
  115. data/lib/ruby_llm/agents/image/pipeline/dsl.rb +213 -0
  116. data/lib/ruby_llm/agents/image/pipeline/execution.rb +382 -0
  117. data/lib/ruby_llm/agents/image/pipeline.rb +97 -0
  118. data/lib/ruby_llm/agents/image/transformer/dsl.rb +148 -0
  119. data/lib/ruby_llm/agents/image/transformer/execution.rb +223 -0
  120. data/lib/ruby_llm/agents/image/transformer.rb +95 -0
  121. data/lib/ruby_llm/agents/image/upscaler/dsl.rb +83 -0
  122. data/lib/ruby_llm/agents/image/upscaler/execution.rb +219 -0
  123. data/lib/ruby_llm/agents/image/upscaler.rb +81 -0
  124. data/lib/ruby_llm/agents/image/variator/dsl.rb +62 -0
  125. data/lib/ruby_llm/agents/image/variator/execution.rb +189 -0
  126. data/lib/ruby_llm/agents/image/variator.rb +80 -0
  127. data/lib/ruby_llm/agents/{alert_manager.rb → infrastructure/alert_manager.rb} +17 -22
  128. data/lib/ruby_llm/agents/infrastructure/budget/budget_query.rb +145 -0
  129. data/lib/ruby_llm/agents/infrastructure/budget/config_resolver.rb +149 -0
  130. data/lib/ruby_llm/agents/infrastructure/budget/forecaster.rb +68 -0
  131. data/lib/ruby_llm/agents/infrastructure/budget/spend_recorder.rb +279 -0
  132. data/lib/ruby_llm/agents/infrastructure/budget_tracker.rb +275 -0
  133. data/lib/ruby_llm/agents/{execution_logger_job.rb → infrastructure/execution_logger_job.rb} +17 -1
  134. data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/executor.rb +2 -1
  135. data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/retry_strategy.rb +9 -3
  136. data/lib/ruby_llm/agents/{reliability.rb → infrastructure/reliability.rb} +11 -21
  137. data/lib/ruby_llm/agents/pipeline/builder.rb +215 -0
  138. data/lib/ruby_llm/agents/pipeline/context.rb +255 -0
  139. data/lib/ruby_llm/agents/pipeline/executor.rb +86 -0
  140. data/lib/ruby_llm/agents/pipeline/middleware/base.rb +124 -0
  141. data/lib/ruby_llm/agents/pipeline/middleware/budget.rb +95 -0
  142. data/lib/ruby_llm/agents/pipeline/middleware/cache.rb +171 -0
  143. data/lib/ruby_llm/agents/pipeline/middleware/instrumentation.rb +415 -0
  144. data/lib/ruby_llm/agents/pipeline/middleware/reliability.rb +276 -0
  145. data/lib/ruby_llm/agents/pipeline/middleware/tenant.rb +196 -0
  146. data/lib/ruby_llm/agents/pipeline.rb +68 -0
  147. data/lib/ruby_llm/agents/{engine.rb → rails/engine.rb} +79 -11
  148. data/lib/ruby_llm/agents/results/background_removal_result.rb +286 -0
  149. data/lib/ruby_llm/agents/{result.rb → results/base.rb} +73 -1
  150. data/lib/ruby_llm/agents/results/embedding_result.rb +243 -0
  151. data/lib/ruby_llm/agents/results/image_analysis_result.rb +314 -0
  152. data/lib/ruby_llm/agents/results/image_edit_result.rb +250 -0
  153. data/lib/ruby_llm/agents/results/image_generation_result.rb +346 -0
  154. data/lib/ruby_llm/agents/results/image_pipeline_result.rb +399 -0
  155. data/lib/ruby_llm/agents/results/image_transform_result.rb +251 -0
  156. data/lib/ruby_llm/agents/results/image_upscale_result.rb +255 -0
  157. data/lib/ruby_llm/agents/results/image_variation_result.rb +237 -0
  158. data/lib/ruby_llm/agents/results/moderation_result.rb +158 -0
  159. data/lib/ruby_llm/agents/results/speech_result.rb +338 -0
  160. data/lib/ruby_llm/agents/results/transcription_result.rb +408 -0
  161. data/lib/ruby_llm/agents/text/embedder.rb +444 -0
  162. data/lib/ruby_llm/agents/text/moderator.rb +237 -0
  163. data/lib/ruby_llm/agents/workflow/async.rb +220 -0
  164. data/lib/ruby_llm/agents/workflow/async_executor.rb +156 -0
  165. data/lib/ruby_llm/agents/{workflow.rb → workflow/orchestrator.rb} +6 -5
  166. data/lib/ruby_llm/agents/workflow/parallel.rb +34 -17
  167. data/lib/ruby_llm/agents/workflow/thread_pool.rb +185 -0
  168. data/lib/ruby_llm/agents.rb +86 -20
  169. metadata +172 -34
  170. data/lib/ruby_llm/agents/base/caching.rb +0 -40
  171. data/lib/ruby_llm/agents/base/cost_calculation.rb +0 -105
  172. data/lib/ruby_llm/agents/base/dsl.rb +0 -324
  173. data/lib/ruby_llm/agents/base/execution.rb +0 -366
  174. data/lib/ruby_llm/agents/base/reliability_dsl.rb +0 -82
  175. data/lib/ruby_llm/agents/base/reliability_execution.rb +0 -136
  176. data/lib/ruby_llm/agents/base/response_building.rb +0 -86
  177. data/lib/ruby_llm/agents/base/tool_tracking.rb +0 -57
  178. data/lib/ruby_llm/agents/base.rb +0 -210
  179. data/lib/ruby_llm/agents/budget_tracker.rb +0 -733
  180. data/lib/ruby_llm/agents/configuration.rb +0 -394
  181. /data/lib/ruby_llm/agents/{deprecations.rb → core/deprecations.rb} +0 -0
  182. /data/lib/ruby_llm/agents/{inflections.rb → core/inflections.rb} +0 -0
  183. /data/lib/ruby_llm/agents/{resolved_config.rb → core/resolved_config.rb} +0 -0
  184. /data/lib/ruby_llm/agents/{attempt_tracker.rb → infrastructure/attempt_tracker.rb} +0 -0
  185. /data/lib/ruby_llm/agents/{cache_helper.rb → infrastructure/cache_helper.rb} +0 -0
  186. /data/lib/ruby_llm/agents/{circuit_breaker.rb → infrastructure/circuit_breaker.rb} +0 -0
  187. /data/lib/ruby_llm/agents/{redactor.rb → infrastructure/redactor.rb} +0 -0
  188. /data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/breaker_manager.rb +0 -0
  189. /data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/execution_constraints.rb +0 -0
  190. /data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/fallback_routing.rb +0 -0
@@ -1,324 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "reliability_dsl"
4
-
5
- module RubyLLM
6
- module Agents
7
- class Base
8
- # Class-level DSL for configuring agents
9
- #
10
- # Provides methods for setting model, temperature, timeout, caching,
11
- # reliability, streaming, tools, and parameters.
12
- module DSL
13
- # @!visibility private
14
- VERSION = "1.0"
15
- # @!visibility private
16
- CACHE_TTL = 1.hour
17
-
18
- # @!group Configuration DSL
19
-
20
- # Sets or returns the LLM model for this agent class
21
- #
22
- # @param value [String, nil] The model identifier to set
23
- # @return [String] The current model setting
24
- # @example
25
- # model "gpt-4o"
26
- def model(value = nil)
27
- @model = value if value
28
- @model || inherited_or_default(:model, RubyLLM::Agents.configuration.default_model)
29
- end
30
-
31
- # Sets or returns the temperature for LLM responses
32
- #
33
- # @param value [Float, nil] Temperature value (0.0-2.0)
34
- # @return [Float] The current temperature setting
35
- # @example
36
- # temperature 0.7
37
- def temperature(value = nil)
38
- @temperature = value if value
39
- @temperature || inherited_or_default(:temperature, RubyLLM::Agents.configuration.default_temperature)
40
- end
41
-
42
- # Sets or returns the version string for cache invalidation
43
- #
44
- # @param value [String, nil] Version string
45
- # @return [String] The current version
46
- # @example
47
- # version "2.0"
48
- def version(value = nil)
49
- @version = value if value
50
- @version || inherited_or_default(:version, VERSION)
51
- end
52
-
53
- # Sets or returns the description for this agent class
54
- #
55
- # @param value [String, nil] The description text
56
- # @return [String, nil] The current description
57
- # @example
58
- # description "Searches the knowledge base for relevant documents"
59
- def description(value = nil)
60
- @description = value if value
61
- @description || inherited_or_default(:description, nil)
62
- end
63
-
64
- # Sets or returns the timeout in seconds for LLM requests
65
- #
66
- # @param value [Integer, nil] Timeout in seconds
67
- # @return [Integer] The current timeout setting
68
- # @example
69
- # timeout 30
70
- def timeout(value = nil)
71
- @timeout = value if value
72
- @timeout || inherited_or_default(:timeout, RubyLLM::Agents.configuration.default_timeout)
73
- end
74
-
75
- # @!endgroup
76
-
77
- # @!group Reliability DSL
78
-
79
- # Configures reliability features using a block syntax
80
- #
81
- # Groups all reliability configuration in a single block for clarity.
82
- # Individual methods (retries, fallback_models, etc.) remain available
83
- # for backward compatibility.
84
- #
85
- # @yield Block containing reliability configuration
86
- # @return [void]
87
- # @example
88
- # reliability do
89
- # retries max: 3, backoff: :exponential
90
- # fallback_models "gpt-4o-mini"
91
- # total_timeout 30
92
- # circuit_breaker errors: 5
93
- # end
94
- def reliability(&block)
95
- builder = ReliabilityDSL.new
96
- builder.instance_eval(&block)
97
-
98
- @retries_config = builder.retries_config if builder.retries_config
99
- @fallback_models = builder.fallback_models_list if builder.fallback_models_list.any?
100
- @total_timeout = builder.total_timeout_value if builder.total_timeout_value
101
- @circuit_breaker_config = builder.circuit_breaker_config if builder.circuit_breaker_config
102
- end
103
-
104
- # Configures retry behavior for this agent
105
- #
106
- # @param max [Integer] Maximum number of retry attempts (default: 0)
107
- # @param backoff [Symbol] Backoff strategy (:constant or :exponential)
108
- # @param base [Float] Base delay in seconds
109
- # @param max_delay [Float] Maximum delay between retries
110
- # @param on [Array<Class>] Error classes to retry on (extends defaults)
111
- # @return [Hash] The current retry configuration
112
- # @example
113
- # retries max: 2, backoff: :exponential, base: 0.4, max_delay: 3.0, on: [Timeout::Error]
114
- def retries(max: nil, backoff: nil, base: nil, max_delay: nil, on: nil)
115
- if max || backoff || base || max_delay || on
116
- @retries_config ||= RubyLLM::Agents.configuration.default_retries.dup
117
- @retries_config[:max] = max if max
118
- @retries_config[:backoff] = backoff if backoff
119
- @retries_config[:base] = base if base
120
- @retries_config[:max_delay] = max_delay if max_delay
121
- @retries_config[:on] = on if on
122
- end
123
- @retries_config || inherited_or_default(:retries_config, RubyLLM::Agents.configuration.default_retries)
124
- end
125
-
126
- # Returns the retry configuration for this agent
127
- #
128
- # @return [Hash, nil] The retry configuration
129
- def retries_config
130
- @retries_config || (superclass.respond_to?(:retries_config) ? superclass.retries_config : nil)
131
- end
132
-
133
- # Sets or returns fallback models to try when primary model fails
134
- #
135
- # @param models [Array<String>, nil] Model identifiers to use as fallbacks
136
- # @return [Array<String>] The current fallback models
137
- # @example
138
- # fallback_models ["gpt-4o-mini", "gpt-4o"]
139
- def fallback_models(models = nil)
140
- @fallback_models = models if models
141
- @fallback_models || inherited_or_default(:fallback_models, RubyLLM::Agents.configuration.default_fallback_models)
142
- end
143
-
144
- # Sets or returns the total timeout for all retry/fallback attempts
145
- #
146
- # @param seconds [Integer, nil] Total timeout in seconds
147
- # @return [Integer, nil] The current total timeout
148
- # @example
149
- # total_timeout 20
150
- def total_timeout(seconds = nil)
151
- @total_timeout = seconds if seconds
152
- @total_timeout || inherited_or_default(:total_timeout, RubyLLM::Agents.configuration.default_total_timeout)
153
- end
154
-
155
- # Configures circuit breaker for this agent
156
- #
157
- # @param errors [Integer] Number of errors to trigger open state
158
- # @param within [Integer] Rolling window in seconds
159
- # @param cooldown [Integer] Cooldown period in seconds when open
160
- # @return [Hash, nil] The current circuit breaker configuration
161
- # @example
162
- # circuit_breaker errors: 10, within: 60, cooldown: 300
163
- def circuit_breaker(errors: nil, within: nil, cooldown: nil)
164
- if errors || within || cooldown
165
- @circuit_breaker_config ||= { errors: 10, within: 60, cooldown: 300 }
166
- @circuit_breaker_config[:errors] = errors if errors
167
- @circuit_breaker_config[:within] = within if within
168
- @circuit_breaker_config[:cooldown] = cooldown if cooldown
169
- end
170
- @circuit_breaker_config || (superclass.respond_to?(:circuit_breaker_config) ? superclass.circuit_breaker_config : nil)
171
- end
172
-
173
- # Returns the circuit breaker configuration for this agent
174
- #
175
- # @return [Hash, nil] The circuit breaker configuration
176
- def circuit_breaker_config
177
- @circuit_breaker_config || (superclass.respond_to?(:circuit_breaker_config) ? superclass.circuit_breaker_config : nil)
178
- end
179
-
180
- # @!endgroup
181
-
182
- # @!group Parameter DSL
183
-
184
- # Defines a parameter for the agent
185
- #
186
- # Creates an accessor method for the parameter that retrieves values
187
- # from the options hash, falling back to the default value.
188
- #
189
- # @param name [Symbol] The parameter name
190
- # @param required [Boolean] Whether the parameter is required
191
- # @param default [Object, nil] Default value if not provided
192
- # @param type [Class, nil] Optional type for validation (e.g., String, Integer, Array)
193
- # @return [void]
194
- # @example Without type (accepts anything)
195
- # param :query, required: true
196
- # param :data, default: {}
197
- # @example With type validation
198
- # param :limit, default: 10, type: Integer
199
- # param :name, type: String
200
- # param :tags, type: Array
201
- def param(name, required: false, default: nil, type: nil)
202
- @params ||= {}
203
- @params[name] = { required: required, default: default, type: type }
204
- define_method(name) do
205
- @options[name] || @options[name.to_s] || self.class.params.dig(name, :default)
206
- end
207
- end
208
-
209
- # Returns all defined parameters including inherited ones
210
- #
211
- # @return [Hash{Symbol => Hash}] Parameter definitions
212
- def params
213
- parent = superclass.respond_to?(:params) ? superclass.params : {}
214
- parent.merge(@params || {})
215
- end
216
-
217
- # @!endgroup
218
-
219
- # @!group Caching DSL
220
-
221
- # Enables caching for this agent with explicit TTL
222
- #
223
- # This is the preferred method for enabling caching.
224
- #
225
- # @param ttl [ActiveSupport::Duration] Time-to-live for cached responses
226
- # @return [void]
227
- # @example
228
- # cache_for 1.hour
229
- # cache_for 30.minutes
230
- def cache_for(ttl)
231
- @cache_enabled = true
232
- @cache_ttl = ttl
233
- end
234
-
235
- # Enables caching for this agent with optional TTL
236
- #
237
- # @deprecated Use {#cache_for} instead for clarity.
238
- # This method will be removed in version 1.0.
239
- # @param ttl [ActiveSupport::Duration] Time-to-live for cached responses
240
- # @return [void]
241
- # @example
242
- # cache 1.hour # deprecated
243
- # cache_for 1.hour # preferred
244
- def cache(ttl = CACHE_TTL)
245
- RubyLLM::Agents::Deprecations.warn(
246
- "cache(ttl) is deprecated. Use cache_for(ttl) instead for clarity.",
247
- caller
248
- )
249
- cache_for(ttl)
250
- end
251
-
252
- # Returns whether caching is enabled for this agent
253
- #
254
- # @return [Boolean] true if caching is enabled
255
- def cache_enabled?
256
- @cache_enabled || false
257
- end
258
-
259
- # Returns the cache TTL for this agent
260
- #
261
- # @return [ActiveSupport::Duration] The cache TTL
262
- def cache_ttl
263
- @cache_ttl || CACHE_TTL
264
- end
265
-
266
- # @!endgroup
267
-
268
- # @!group Streaming DSL
269
-
270
- # Enables or returns streaming mode for this agent
271
- #
272
- # When streaming is enabled and a block is passed to call,
273
- # chunks will be yielded to the block as they arrive.
274
- #
275
- # @param value [Boolean, nil] Whether to enable streaming
276
- # @return [Boolean] The current streaming setting
277
- # @example
278
- # streaming true
279
- def streaming(value = nil)
280
- @streaming = value unless value.nil?
281
- return @streaming unless @streaming.nil?
282
-
283
- inherited_or_default(:streaming, RubyLLM::Agents.configuration.default_streaming)
284
- end
285
-
286
- # @!endgroup
287
-
288
- # @!group Tools DSL
289
-
290
- # Sets or returns the tools available to this agent
291
- #
292
- # Tools are RubyLLM::Tool classes that the model can invoke.
293
- # The agent will automatically execute tool calls and continue
294
- # until the model produces a final response.
295
- #
296
- # @param tool_classes [Array<Class>] Tool classes to make available
297
- # @return [Array<Class>] The current tools
298
- # @example With array (preferred)
299
- # tools [WeatherTool, SearchTool, CalculatorTool]
300
- # @example Single tool
301
- # tools [WeatherTool]
302
- def tools(tool_classes = nil)
303
- if tool_classes
304
- @tools = Array(tool_classes)
305
- end
306
- @tools || inherited_or_default(:tools, RubyLLM::Agents.configuration.default_tools)
307
- end
308
-
309
- # @!endgroup
310
-
311
- private
312
-
313
- # Looks up setting from superclass or uses default
314
- #
315
- # @param method [Symbol] The method to call on superclass
316
- # @param default [Object] Default value if not found
317
- # @return [Object] The resolved value
318
- def inherited_or_default(method, default)
319
- superclass.respond_to?(method) ? superclass.send(method) : default
320
- end
321
- end
322
- end
323
- end
324
- end
@@ -1,366 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RubyLLM
4
- module Agents
5
- class Base
6
- # Main execution flow for agents
7
- #
8
- # Handles the core execution logic including caching, streaming,
9
- # client building, and parameter validation.
10
- module Execution
11
- # Executes the agent and returns the processed response
12
- #
13
- # Handles caching, dry-run mode, and delegates to uncached_call
14
- # for actual LLM execution.
15
- #
16
- # @yield [chunk] Yields chunks when streaming is enabled
17
- # @yieldparam chunk [RubyLLM::Chunk] A streaming chunk with content
18
- # @return [Object] The processed LLM response
19
- def call(&block)
20
- # Resolve tenant configuration before execution
21
- resolve_tenant_context!
22
-
23
- return dry_run_response if @options[:dry_run]
24
- return uncached_call(&block) if @options[:skip_cache] || !self.class.cache_enabled?
25
-
26
- cache_key = agent_cache_key
27
-
28
- # Check for cache hit BEFORE fetch to record it
29
- if cache_store.exist?(cache_key)
30
- started_at = Time.current
31
- cached_result = cache_store.read(cache_key)
32
- record_cache_hit_execution(cache_key, cached_result, started_at) if cached_result
33
- return cached_result
34
- end
35
-
36
- # Cache miss - execute and store
37
- cache_store.fetch(cache_key, expires_in: self.class.cache_ttl) do
38
- uncached_call(&block)
39
- end
40
- end
41
-
42
- # Resolves tenant context from the :tenant option
43
- #
44
- # The tenant option can be:
45
- # - String: Just the tenant_id (uses resolver or DB for config)
46
- # - Hash: Full config { id:, name:, daily_limit:, daily_token_limit:, ... }
47
- #
48
- # @return [void]
49
- def resolve_tenant_context!
50
- # Idempotency guard - only resolve once
51
- return if defined?(@tenant_context_resolved) && @tenant_context_resolved
52
-
53
- tenant_option = @options[:tenant]
54
- return unless tenant_option
55
-
56
- if tenant_option.is_a?(Hash)
57
- # Full config passed - extract id and store config
58
- @tenant_id = tenant_option[:id]&.to_s
59
- @tenant_config = tenant_option.except(:id)
60
- else
61
- # Just tenant_id passed
62
- @tenant_id = tenant_option.to_s
63
- @tenant_config = nil
64
- end
65
-
66
- @tenant_context_resolved = true
67
- end
68
-
69
- # Returns the resolved tenant ID
70
- #
71
- # @return [String, nil] The tenant identifier
72
- def resolved_tenant_id
73
- return @tenant_id if defined?(@tenant_id) && @tenant_id.present?
74
-
75
- config = RubyLLM::Agents.configuration
76
- return nil unless config.multi_tenancy_enabled?
77
-
78
- config.current_tenant_id
79
- end
80
-
81
- # Returns the runtime tenant config (if passed via :tenant option)
82
- #
83
- # @return [Hash, nil] Runtime tenant configuration
84
- def runtime_tenant_config
85
- @tenant_config if defined?(@tenant_config)
86
- end
87
-
88
- # Executes the agent without caching
89
- #
90
- # Routes to reliability-enabled execution if configured, otherwise
91
- # uses simple single-attempt execution.
92
- #
93
- # @yield [chunk] Yields chunks when streaming is enabled
94
- # @return [Object] The processed response
95
- def uncached_call(&block)
96
- if reliability_enabled?
97
- execute_with_reliability(&block)
98
- else
99
- instrument_execution { execute_single_attempt(&block) }
100
- end
101
- end
102
-
103
- # Executes a single LLM attempt with timeout
104
- #
105
- # @param model_override [String, nil] Optional model to use instead of default
106
- # @yield [chunk] Yields chunks when streaming is enabled
107
- # @return [Result] A Result object with processed content and metadata
108
- def execute_single_attempt(model_override: nil, &block)
109
- current_client = model_override ? build_client_with_model(model_override) : client
110
- @execution_started_at ||= Time.current
111
- reset_accumulated_tool_calls!
112
-
113
- Timeout.timeout(self.class.timeout) do
114
- if streaming_enabled? && block_given?
115
- execute_with_streaming(current_client, &block)
116
- else
117
- response = current_client.ask(user_prompt, **ask_options)
118
- extract_tool_calls_from_client(current_client)
119
- capture_response(response)
120
- build_result(process_response(response), response)
121
- end
122
- end
123
- end
124
-
125
- # Executes an LLM request with streaming enabled
126
- #
127
- # Yields chunks to the provided block as they arrive and tracks
128
- # time to first token for latency analysis.
129
- #
130
- # @param current_client [RubyLLM::Chat] The configured client
131
- # @yield [chunk] Yields each chunk as it arrives
132
- # @yieldparam chunk [RubyLLM::Chunk] A streaming chunk
133
- # @return [Result] A Result object with processed content and metadata
134
- def execute_with_streaming(current_client, &block)
135
- first_chunk_at = nil
136
-
137
- response = current_client.ask(user_prompt, **ask_options) do |chunk|
138
- first_chunk_at ||= Time.current
139
- yield chunk if block_given?
140
- end
141
-
142
- if first_chunk_at && @execution_started_at
143
- @time_to_first_token_ms = ((first_chunk_at - @execution_started_at) * 1000).to_i
144
- end
145
-
146
- extract_tool_calls_from_client(current_client)
147
- capture_response(response)
148
- build_result(process_response(response), response)
149
- end
150
-
151
- # Returns prompt info without making an API call (debug mode)
152
- #
153
- # @return [Result] A Result with dry run configuration info
154
- def dry_run_response
155
- Result.new(
156
- content: {
157
- dry_run: true,
158
- agent: self.class.name,
159
- model: model,
160
- temperature: temperature,
161
- timeout: self.class.timeout,
162
- system_prompt: system_prompt,
163
- user_prompt: user_prompt,
164
- attachments: @options[:with],
165
- schema: schema&.class&.name,
166
- streaming: self.class.streaming,
167
- tools: resolved_tools.map { |t| t.respond_to?(:name) ? t.name : t.to_s }
168
- },
169
- model_id: model,
170
- temperature: temperature,
171
- streaming: self.class.streaming
172
- )
173
- end
174
-
175
- # Resolves tools for this execution
176
- #
177
- # Checks for instance method override first (for dynamic tools),
178
- # then falls back to class-level DSL configuration. This allows
179
- # agents to define tools dynamically based on runtime context.
180
- #
181
- # @return [Array<Class>] Tool classes to use
182
- def resolved_tools
183
- # Check if instance defines tools method (not inherited from class singleton)
184
- if self.class.instance_methods(false).include?(:tools)
185
- tools
186
- else
187
- self.class.tools
188
- end
189
- end
190
-
191
- # Resolves messages for this execution
192
- #
193
- # Priority order:
194
- # 1. @override_messages (set via with_messages)
195
- # 2. :messages option passed at call time
196
- # 3. messages template method defined in subclass
197
- #
198
- # @return [Array<Hash>] Messages to apply to conversation
199
- def resolved_messages
200
- return @override_messages if @override_messages&.any?
201
- return @options[:messages] if @options[:messages]&.any?
202
-
203
- messages
204
- end
205
-
206
- # Returns the consolidated reliability configuration for this agent instance
207
- #
208
- # @return [Hash] Reliability config with :retries, :fallback_models, :total_timeout, :circuit_breaker
209
- def reliability_config
210
- default_retries = RubyLLM::Agents.configuration.default_retries
211
- {
212
- retries: self.class.retries || default_retries,
213
- fallback_models: self.class.fallback_models,
214
- total_timeout: self.class.total_timeout,
215
- circuit_breaker: self.class.circuit_breaker_config
216
- }
217
- end
218
-
219
- # Returns whether any reliability features are enabled for this agent
220
- #
221
- # @return [Boolean] true if retries, fallbacks, or circuit breaker is configured
222
- def reliability_enabled?
223
- config = reliability_config
224
- (config[:retries]&.dig(:max) || 0) > 0 ||
225
- config[:fallback_models]&.any? ||
226
- config[:circuit_breaker].present?
227
- end
228
-
229
- # Returns whether streaming is enabled for this execution
230
- #
231
- # Checks both class-level DSL setting and instance-level override
232
- # (set by the stream class method).
233
- #
234
- # @return [Boolean] true if streaming is enabled
235
- def streaming_enabled?
236
- @force_streaming || self.class.streaming
237
- end
238
-
239
- # Returns options to pass to the ask method
240
- #
241
- # Currently supports :with for attachments (images, PDFs, etc.)
242
- #
243
- # @return [Hash] Options for the ask call
244
- def ask_options
245
- opts = {}
246
- opts[:with] = @options[:with] if @options[:with]
247
- opts
248
- end
249
-
250
- # Validates that all required parameters are present and types match
251
- #
252
- # @raise [ArgumentError] If required parameters are missing or types don't match
253
- # @return [void]
254
- def validate_required_params!
255
- self.class.params.each do |name, config|
256
- value = @options[name] || @options[name.to_s]
257
- has_value = @options.key?(name) || @options.key?(name.to_s)
258
-
259
- # Check required
260
- if config[:required] && !has_value
261
- raise ArgumentError, "#{self.class} missing required param: #{name}"
262
- end
263
-
264
- # Check type if specified and value is present (not nil)
265
- if config[:type] && has_value && !value.nil?
266
- unless value.is_a?(config[:type])
267
- raise ArgumentError,
268
- "#{self.class} expected #{config[:type]} for :#{name}, got #{value.class}"
269
- end
270
- end
271
- end
272
- end
273
-
274
- # Builds and configures the RubyLLM client
275
- #
276
- # @return [RubyLLM::Chat] Configured chat client
277
- def build_client
278
- # Apply database-backed API configuration if available
279
- apply_api_configuration!
280
-
281
- client = RubyLLM.chat
282
- .with_model(model)
283
- .with_temperature(temperature)
284
- client = client.with_instructions(system_prompt) if system_prompt
285
- client = client.with_schema(schema) if schema
286
- client = client.with_tools(*resolved_tools) if resolved_tools.any?
287
- client = apply_messages(client, resolved_messages) if resolved_messages.any?
288
- client
289
- end
290
-
291
- # Applies database-backed API configuration to RubyLLM
292
- #
293
- # Resolution priority: per-tenant DB > global DB > RubyLLM.configure
294
- # Only applies if the api_configurations table exists.
295
- #
296
- # @return [void]
297
- def apply_api_configuration!
298
- return unless api_configuration_available?
299
-
300
- resolved_config = ApiConfiguration.resolve(tenant_id: resolved_tenant_id)
301
- resolved_config.apply_to_ruby_llm!
302
- rescue StandardError => e
303
- Rails.logger.warn("[RubyLLM::Agents] Failed to apply API config: #{e.message}")
304
- end
305
-
306
- # Checks if API configuration table is available
307
- #
308
- # @return [Boolean] true if table exists and is accessible
309
- def api_configuration_available?
310
- return @api_config_available if defined?(@api_config_available)
311
-
312
- @api_config_available = begin
313
- ApiConfiguration.table_exists?
314
- rescue StandardError
315
- false
316
- end
317
- end
318
-
319
- # Builds a client with a specific model
320
- #
321
- # @param model_id [String] The model identifier
322
- # @return [RubyLLM::Chat] Configured chat client
323
- def build_client_with_model(model_id)
324
- # Apply database-backed API configuration if available
325
- apply_api_configuration!
326
-
327
- client = RubyLLM.chat
328
- .with_model(model_id)
329
- .with_temperature(temperature)
330
- client = client.with_instructions(system_prompt) if system_prompt
331
- client = client.with_schema(schema) if schema
332
- client = client.with_tools(*resolved_tools) if resolved_tools.any?
333
- client = apply_messages(client, resolved_messages) if resolved_messages.any?
334
- client
335
- end
336
-
337
- # Applies conversation history to the client
338
- #
339
- # @param client [RubyLLM::Chat] The chat client
340
- # @param msgs [Array<Hash>] Messages with :role and :content keys
341
- # @return [RubyLLM::Chat] Client with messages applied
342
- def apply_messages(client, msgs)
343
- msgs.each do |message|
344
- client.add_message(role: message[:role].to_sym, content: message[:content])
345
- end
346
- client
347
- end
348
-
349
- # Builds a client with pre-populated conversation history
350
- #
351
- # @deprecated Use resolved_messages and apply_messages instead.
352
- # Override the messages template method or pass messages: option to call.
353
- # @param messages [Array<Hash>] Messages with :role and :content keys
354
- # @return [RubyLLM::Chat] Client with messages added
355
- # @example
356
- # build_client_with_messages([
357
- # { role: "user", content: "Hello" },
358
- # { role: "assistant", content: "Hi there!" }
359
- # ])
360
- def build_client_with_messages(messages)
361
- apply_messages(build_client, messages)
362
- end
363
- end
364
- end
365
- end
366
- end