ruby_llm-agents 0.4.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 (208) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +225 -34
  3. data/app/controllers/ruby_llm/agents/agents_controller.rb +136 -16
  4. data/app/controllers/ruby_llm/agents/api_configurations_controller.rb +214 -0
  5. data/app/controllers/ruby_llm/agents/dashboard_controller.rb +29 -9
  6. data/app/controllers/ruby_llm/agents/{settings_controller.rb → system_config_controller.rb} +3 -3
  7. data/app/controllers/ruby_llm/agents/tenants_controller.rb +109 -0
  8. data/app/controllers/ruby_llm/agents/workflows_controller.rb +355 -0
  9. data/app/helpers/ruby_llm/agents/application_helper.rb +25 -0
  10. data/app/models/ruby_llm/agents/api_configuration.rb +386 -0
  11. data/app/models/ruby_llm/agents/execution.rb +3 -0
  12. data/app/models/ruby_llm/agents/tenant_budget.rb +112 -14
  13. data/app/services/ruby_llm/agents/agent_registry.rb +51 -12
  14. data/app/views/layouts/ruby_llm/agents/application.html.erb +5 -30
  15. data/app/views/ruby_llm/agents/agents/_agent.html.erb +13 -1
  16. data/app/views/ruby_llm/agents/agents/_config_agent.html.erb +235 -0
  17. data/app/views/ruby_llm/agents/agents/_config_embedder.html.erb +70 -0
  18. data/app/views/ruby_llm/agents/agents/_config_image_generator.html.erb +152 -0
  19. data/app/views/ruby_llm/agents/agents/_config_moderator.html.erb +63 -0
  20. data/app/views/ruby_llm/agents/agents/_config_speaker.html.erb +108 -0
  21. data/app/views/ruby_llm/agents/agents/_config_transcriber.html.erb +91 -0
  22. data/app/views/ruby_llm/agents/agents/_workflow.html.erb +1 -1
  23. data/app/views/ruby_llm/agents/agents/index.html.erb +74 -9
  24. data/app/views/ruby_llm/agents/agents/show.html.erb +18 -378
  25. data/app/views/ruby_llm/agents/api_configurations/_api_key_field.html.erb +34 -0
  26. data/app/views/ruby_llm/agents/api_configurations/_form.html.erb +288 -0
  27. data/app/views/ruby_llm/agents/api_configurations/edit.html.erb +95 -0
  28. data/app/views/ruby_llm/agents/api_configurations/edit_tenant.html.erb +97 -0
  29. data/app/views/ruby_llm/agents/api_configurations/show.html.erb +211 -0
  30. data/app/views/ruby_llm/agents/api_configurations/tenant.html.erb +179 -0
  31. data/app/views/ruby_llm/agents/dashboard/_action_center.html.erb +1 -1
  32. data/app/views/ruby_llm/agents/dashboard/_agent_comparison.html.erb +269 -15
  33. data/app/views/ruby_llm/agents/executions/show.html.erb +98 -0
  34. data/app/views/ruby_llm/agents/shared/_agent_type_badge.html.erb +93 -0
  35. data/app/views/ruby_llm/agents/{settings → system_config}/show.html.erb +1 -1
  36. data/app/views/ruby_llm/agents/tenants/_form.html.erb +150 -0
  37. data/app/views/ruby_llm/agents/tenants/edit.html.erb +13 -0
  38. data/app/views/ruby_llm/agents/tenants/index.html.erb +129 -0
  39. data/app/views/ruby_llm/agents/tenants/show.html.erb +374 -0
  40. data/app/views/ruby_llm/agents/workflows/_step_performance.html.erb +236 -0
  41. data/app/views/ruby_llm/agents/workflows/_structure_parallel.html.erb +76 -0
  42. data/app/views/ruby_llm/agents/workflows/_structure_pipeline.html.erb +74 -0
  43. data/app/views/ruby_llm/agents/workflows/_structure_router.html.erb +108 -0
  44. data/app/views/ruby_llm/agents/workflows/show.html.erb +442 -0
  45. data/config/routes.rb +13 -1
  46. data/lib/generators/ruby_llm_agents/agent_generator.rb +56 -7
  47. data/lib/generators/ruby_llm_agents/api_configuration_generator.rb +100 -0
  48. data/lib/generators/ruby_llm_agents/background_remover_generator.rb +110 -0
  49. data/lib/generators/ruby_llm_agents/embedder_generator.rb +107 -0
  50. data/lib/generators/ruby_llm_agents/image_analyzer_generator.rb +115 -0
  51. data/lib/generators/ruby_llm_agents/image_editor_generator.rb +108 -0
  52. data/lib/generators/ruby_llm_agents/image_generator_generator.rb +116 -0
  53. data/lib/generators/ruby_llm_agents/image_pipeline_generator.rb +178 -0
  54. data/lib/generators/ruby_llm_agents/image_transformer_generator.rb +109 -0
  55. data/lib/generators/ruby_llm_agents/image_upscaler_generator.rb +103 -0
  56. data/lib/generators/ruby_llm_agents/image_variator_generator.rb +102 -0
  57. data/lib/generators/ruby_llm_agents/install_generator.rb +76 -4
  58. data/lib/generators/ruby_llm_agents/restructure_generator.rb +292 -0
  59. data/lib/generators/ruby_llm_agents/speaker_generator.rb +121 -0
  60. data/lib/generators/ruby_llm_agents/templates/add_execution_type_migration.rb.tt +8 -0
  61. data/lib/generators/ruby_llm_agents/templates/agent.rb.tt +99 -84
  62. data/lib/generators/ruby_llm_agents/templates/application_agent.rb.tt +42 -40
  63. data/lib/generators/ruby_llm_agents/templates/application_background_remover.rb.tt +26 -0
  64. data/lib/generators/ruby_llm_agents/templates/application_embedder.rb.tt +50 -0
  65. data/lib/generators/ruby_llm_agents/templates/application_image_analyzer.rb.tt +26 -0
  66. data/lib/generators/ruby_llm_agents/templates/application_image_editor.rb.tt +20 -0
  67. data/lib/generators/ruby_llm_agents/templates/application_image_generator.rb.tt +38 -0
  68. data/lib/generators/ruby_llm_agents/templates/application_image_pipeline.rb.tt +139 -0
  69. data/lib/generators/ruby_llm_agents/templates/application_image_transformer.rb.tt +21 -0
  70. data/lib/generators/ruby_llm_agents/templates/application_image_upscaler.rb.tt +20 -0
  71. data/lib/generators/ruby_llm_agents/templates/application_image_variator.rb.tt +20 -0
  72. data/lib/generators/ruby_llm_agents/templates/application_speaker.rb.tt +49 -0
  73. data/lib/generators/ruby_llm_agents/templates/application_transcriber.rb.tt +53 -0
  74. data/lib/generators/ruby_llm_agents/templates/background_remover.rb.tt +44 -0
  75. data/lib/generators/ruby_llm_agents/templates/create_api_configurations_migration.rb.tt +90 -0
  76. data/lib/generators/ruby_llm_agents/templates/embedder.rb.tt +41 -0
  77. data/lib/generators/ruby_llm_agents/templates/image_analyzer.rb.tt +45 -0
  78. data/lib/generators/ruby_llm_agents/templates/image_editor.rb.tt +35 -0
  79. data/lib/generators/ruby_llm_agents/templates/image_generator.rb.tt +47 -0
  80. data/lib/generators/ruby_llm_agents/templates/image_pipeline.rb.tt +50 -0
  81. data/lib/generators/ruby_llm_agents/templates/image_transformer.rb.tt +44 -0
  82. data/lib/generators/ruby_llm_agents/templates/image_upscaler.rb.tt +38 -0
  83. data/lib/generators/ruby_llm_agents/templates/image_variator.rb.tt +33 -0
  84. data/lib/generators/ruby_llm_agents/templates/skills/AGENTS.md.tt +228 -0
  85. data/lib/generators/ruby_llm_agents/templates/skills/BACKGROUND_REMOVERS.md.tt +131 -0
  86. data/lib/generators/ruby_llm_agents/templates/skills/EMBEDDERS.md.tt +255 -0
  87. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_ANALYZERS.md.tt +120 -0
  88. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_EDITORS.md.tt +102 -0
  89. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_GENERATORS.md.tt +282 -0
  90. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_PIPELINES.md.tt +228 -0
  91. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_TRANSFORMERS.md.tt +120 -0
  92. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_UPSCALERS.md.tt +110 -0
  93. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_VARIATORS.md.tt +120 -0
  94. data/lib/generators/ruby_llm_agents/templates/skills/SPEAKERS.md.tt +212 -0
  95. data/lib/generators/ruby_llm_agents/templates/skills/TOOLS.md.tt +227 -0
  96. data/lib/generators/ruby_llm_agents/templates/skills/TRANSCRIBERS.md.tt +251 -0
  97. data/lib/generators/ruby_llm_agents/templates/skills/WORKFLOWS.md.tt +300 -0
  98. data/lib/generators/ruby_llm_agents/templates/speaker.rb.tt +56 -0
  99. data/lib/generators/ruby_llm_agents/templates/transcriber.rb.tt +51 -0
  100. data/lib/generators/ruby_llm_agents/transcriber_generator.rb +107 -0
  101. data/lib/generators/ruby_llm_agents/upgrade_generator.rb +152 -1
  102. data/lib/ruby_llm/agents/audio/speaker.rb +553 -0
  103. data/lib/ruby_llm/agents/audio/transcriber.rb +669 -0
  104. data/lib/ruby_llm/agents/base_agent.rb +675 -0
  105. data/lib/ruby_llm/agents/core/base/moderation_dsl.rb +181 -0
  106. data/lib/ruby_llm/agents/core/base/moderation_execution.rb +274 -0
  107. data/lib/ruby_llm/agents/core/base.rb +135 -0
  108. data/lib/ruby_llm/agents/core/configuration.rb +981 -0
  109. data/lib/ruby_llm/agents/core/errors.rb +150 -0
  110. data/lib/ruby_llm/agents/{instrumentation.rb → core/instrumentation.rb} +93 -4
  111. data/lib/ruby_llm/agents/core/llm_tenant.rb +358 -0
  112. data/lib/ruby_llm/agents/core/resolved_config.rb +348 -0
  113. data/lib/ruby_llm/agents/{version.rb → core/version.rb} +1 -1
  114. data/lib/ruby_llm/agents/dsl/base.rb +110 -0
  115. data/lib/ruby_llm/agents/dsl/caching.rb +142 -0
  116. data/lib/ruby_llm/agents/dsl/reliability.rb +307 -0
  117. data/lib/ruby_llm/agents/dsl.rb +41 -0
  118. data/lib/ruby_llm/agents/image/analyzer/dsl.rb +130 -0
  119. data/lib/ruby_llm/agents/image/analyzer/execution.rb +402 -0
  120. data/lib/ruby_llm/agents/image/analyzer.rb +90 -0
  121. data/lib/ruby_llm/agents/image/background_remover/dsl.rb +154 -0
  122. data/lib/ruby_llm/agents/image/background_remover/execution.rb +240 -0
  123. data/lib/ruby_llm/agents/image/background_remover.rb +89 -0
  124. data/lib/ruby_llm/agents/image/concerns/image_operation_dsl.rb +91 -0
  125. data/lib/ruby_llm/agents/image/concerns/image_operation_execution.rb +165 -0
  126. data/lib/ruby_llm/agents/image/editor/dsl.rb +56 -0
  127. data/lib/ruby_llm/agents/image/editor/execution.rb +207 -0
  128. data/lib/ruby_llm/agents/image/editor.rb +92 -0
  129. data/lib/ruby_llm/agents/image/generator/active_storage_support.rb +127 -0
  130. data/lib/ruby_llm/agents/image/generator/content_policy.rb +95 -0
  131. data/lib/ruby_llm/agents/image/generator/pricing.rb +353 -0
  132. data/lib/ruby_llm/agents/image/generator/templates.rb +124 -0
  133. data/lib/ruby_llm/agents/image/generator.rb +455 -0
  134. data/lib/ruby_llm/agents/image/pipeline/dsl.rb +213 -0
  135. data/lib/ruby_llm/agents/image/pipeline/execution.rb +382 -0
  136. data/lib/ruby_llm/agents/image/pipeline.rb +97 -0
  137. data/lib/ruby_llm/agents/image/transformer/dsl.rb +148 -0
  138. data/lib/ruby_llm/agents/image/transformer/execution.rb +223 -0
  139. data/lib/ruby_llm/agents/image/transformer.rb +95 -0
  140. data/lib/ruby_llm/agents/image/upscaler/dsl.rb +83 -0
  141. data/lib/ruby_llm/agents/image/upscaler/execution.rb +219 -0
  142. data/lib/ruby_llm/agents/image/upscaler.rb +81 -0
  143. data/lib/ruby_llm/agents/image/variator/dsl.rb +62 -0
  144. data/lib/ruby_llm/agents/image/variator/execution.rb +189 -0
  145. data/lib/ruby_llm/agents/image/variator.rb +80 -0
  146. data/lib/ruby_llm/agents/{alert_manager.rb → infrastructure/alert_manager.rb} +17 -22
  147. data/lib/ruby_llm/agents/infrastructure/budget/budget_query.rb +145 -0
  148. data/lib/ruby_llm/agents/infrastructure/budget/config_resolver.rb +149 -0
  149. data/lib/ruby_llm/agents/infrastructure/budget/forecaster.rb +68 -0
  150. data/lib/ruby_llm/agents/infrastructure/budget/spend_recorder.rb +279 -0
  151. data/lib/ruby_llm/agents/infrastructure/budget_tracker.rb +275 -0
  152. data/lib/ruby_llm/agents/{execution_logger_job.rb → infrastructure/execution_logger_job.rb} +17 -1
  153. data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/executor.rb +2 -1
  154. data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/retry_strategy.rb +9 -3
  155. data/lib/ruby_llm/agents/{reliability.rb → infrastructure/reliability.rb} +11 -21
  156. data/lib/ruby_llm/agents/pipeline/builder.rb +215 -0
  157. data/lib/ruby_llm/agents/pipeline/context.rb +255 -0
  158. data/lib/ruby_llm/agents/pipeline/executor.rb +86 -0
  159. data/lib/ruby_llm/agents/pipeline/middleware/base.rb +124 -0
  160. data/lib/ruby_llm/agents/pipeline/middleware/budget.rb +95 -0
  161. data/lib/ruby_llm/agents/pipeline/middleware/cache.rb +171 -0
  162. data/lib/ruby_llm/agents/pipeline/middleware/instrumentation.rb +415 -0
  163. data/lib/ruby_llm/agents/pipeline/middleware/reliability.rb +276 -0
  164. data/lib/ruby_llm/agents/pipeline/middleware/tenant.rb +196 -0
  165. data/lib/ruby_llm/agents/pipeline.rb +68 -0
  166. data/lib/ruby_llm/agents/{engine.rb → rails/engine.rb} +79 -10
  167. data/lib/ruby_llm/agents/results/background_removal_result.rb +286 -0
  168. data/lib/ruby_llm/agents/{result.rb → results/base.rb} +73 -1
  169. data/lib/ruby_llm/agents/results/embedding_result.rb +243 -0
  170. data/lib/ruby_llm/agents/results/image_analysis_result.rb +314 -0
  171. data/lib/ruby_llm/agents/results/image_edit_result.rb +250 -0
  172. data/lib/ruby_llm/agents/results/image_generation_result.rb +346 -0
  173. data/lib/ruby_llm/agents/results/image_pipeline_result.rb +399 -0
  174. data/lib/ruby_llm/agents/results/image_transform_result.rb +251 -0
  175. data/lib/ruby_llm/agents/results/image_upscale_result.rb +255 -0
  176. data/lib/ruby_llm/agents/results/image_variation_result.rb +237 -0
  177. data/lib/ruby_llm/agents/results/moderation_result.rb +158 -0
  178. data/lib/ruby_llm/agents/results/speech_result.rb +338 -0
  179. data/lib/ruby_llm/agents/results/transcription_result.rb +408 -0
  180. data/lib/ruby_llm/agents/text/embedder.rb +444 -0
  181. data/lib/ruby_llm/agents/text/moderator.rb +237 -0
  182. data/lib/ruby_llm/agents/workflow/async.rb +220 -0
  183. data/lib/ruby_llm/agents/workflow/async_executor.rb +156 -0
  184. data/lib/ruby_llm/agents/{workflow.rb → workflow/orchestrator.rb} +6 -5
  185. data/lib/ruby_llm/agents/workflow/parallel.rb +34 -17
  186. data/lib/ruby_llm/agents/workflow/thread_pool.rb +185 -0
  187. data/lib/ruby_llm/agents.rb +86 -20
  188. metadata +189 -35
  189. data/lib/ruby_llm/agents/base/caching.rb +0 -40
  190. data/lib/ruby_llm/agents/base/cost_calculation.rb +0 -105
  191. data/lib/ruby_llm/agents/base/dsl.rb +0 -324
  192. data/lib/ruby_llm/agents/base/execution.rb +0 -283
  193. data/lib/ruby_llm/agents/base/reliability_dsl.rb +0 -82
  194. data/lib/ruby_llm/agents/base/reliability_execution.rb +0 -136
  195. data/lib/ruby_llm/agents/base/response_building.rb +0 -86
  196. data/lib/ruby_llm/agents/base/tool_tracking.rb +0 -57
  197. data/lib/ruby_llm/agents/base.rb +0 -209
  198. data/lib/ruby_llm/agents/budget_tracker.rb +0 -471
  199. data/lib/ruby_llm/agents/configuration.rb +0 -357
  200. /data/lib/ruby_llm/agents/{deprecations.rb → core/deprecations.rb} +0 -0
  201. /data/lib/ruby_llm/agents/{inflections.rb → core/inflections.rb} +0 -0
  202. /data/lib/ruby_llm/agents/{attempt_tracker.rb → infrastructure/attempt_tracker.rb} +0 -0
  203. /data/lib/ruby_llm/agents/{cache_helper.rb → infrastructure/cache_helper.rb} +0 -0
  204. /data/lib/ruby_llm/agents/{circuit_breaker.rb → infrastructure/circuit_breaker.rb} +0 -0
  205. /data/lib/ruby_llm/agents/{redactor.rb → infrastructure/redactor.rb} +0 -0
  206. /data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/breaker_manager.rb +0 -0
  207. /data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/execution_constraints.rb +0 -0
  208. /data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/fallback_routing.rb +0 -0
@@ -1,136 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RubyLLM
4
- module Agents
5
- class Base
6
- # Reliability execution with retry/fallback/circuit breaker support
7
- #
8
- # Handles executing agents with automatic retries, model fallbacks,
9
- # circuit breaker protection, and budget enforcement.
10
- module ReliabilityExecution
11
- # Executes the agent with retry/fallback/circuit breaker support
12
- #
13
- # @yield [chunk] Yields chunks when streaming is enabled
14
- # @return [Object] The processed response
15
- # @raise [Reliability::AllModelsExhaustedError] If all models fail
16
- # @raise [Reliability::BudgetExceededError] If budget limits exceeded
17
- # @raise [Reliability::TotalTimeoutError] If total timeout exceeded
18
- def execute_with_reliability(&block)
19
- config = reliability_config
20
- models_to_try = [model, *config[:fallback_models]].uniq
21
- total_deadline = config[:total_timeout] ? Time.current + config[:total_timeout] : nil
22
- started_at = Time.current
23
-
24
- # Get current tenant_id for multi-tenancy support
25
- global_config = RubyLLM::Agents.configuration
26
- tenant_id = global_config.multi_tenancy_enabled? ? global_config.current_tenant_id : nil
27
-
28
- # Pre-check budget (tenant_id is resolved automatically if not passed)
29
- BudgetTracker.check_budget!(self.class.name, tenant_id: tenant_id) if global_config.budgets_enabled?
30
-
31
- instrument_execution_with_attempts(models_to_try: models_to_try) do |attempt_tracker|
32
- last_error = nil
33
-
34
- models_to_try.each do |current_model|
35
- # Check circuit breaker (with tenant isolation if enabled)
36
- breaker = get_circuit_breaker(current_model, tenant_id: tenant_id)
37
- if breaker&.open?
38
- attempt_tracker.record_short_circuit(current_model)
39
- next
40
- end
41
-
42
- retries_remaining = config[:retries]&.dig(:max) || 0
43
- attempt_index = 0
44
-
45
- loop do
46
- # Check total timeout
47
- if total_deadline && Time.current > total_deadline
48
- elapsed = Time.current - started_at
49
- raise Reliability::TotalTimeoutError.new(config[:total_timeout], elapsed)
50
- end
51
-
52
- attempt = attempt_tracker.start_attempt(current_model)
53
-
54
- begin
55
- result = execute_single_attempt(model_override: current_model, &block)
56
- attempt_tracker.complete_attempt(attempt, success: true, response: @last_response)
57
-
58
- # Record success in circuit breaker
59
- breaker&.record_success!
60
-
61
- # Record budget spend (with tenant isolation if enabled)
62
- if @last_response && global_config.budgets_enabled?
63
- record_attempt_cost(attempt_tracker, tenant_id: tenant_id)
64
- end
65
-
66
- # Use throw instead of return to allow instrument_execution_with_attempts
67
- # to properly complete the execution record before returning
68
- throw :execution_success, result
69
-
70
- rescue *retryable_errors(config) => e
71
- last_error = e
72
- attempt_tracker.complete_attempt(attempt, success: false, error: e)
73
- breaker&.record_failure!
74
-
75
- if retries_remaining > 0 && !past_deadline?(total_deadline)
76
- retries_remaining -= 1
77
- attempt_index += 1
78
- retries_config = config[:retries] || {}
79
- delay = Reliability.calculate_backoff(
80
- strategy: retries_config[:backoff] || :exponential,
81
- base: retries_config[:base] || 0.4,
82
- max_delay: retries_config[:max_delay] || 3.0,
83
- attempt: attempt_index
84
- )
85
- sleep(delay)
86
- else
87
- break # Move to next model
88
- end
89
-
90
- rescue StandardError => e
91
- # Non-retryable error - record and move to next model
92
- last_error = e
93
- attempt_tracker.complete_attempt(attempt, success: false, error: e)
94
- breaker&.record_failure!
95
- break
96
- end
97
- end
98
- end
99
-
100
- # All models exhausted
101
- raise Reliability::AllModelsExhaustedError.new(models_to_try, last_error)
102
- end
103
- end
104
-
105
- # Returns the list of retryable error classes
106
- #
107
- # @param config [Hash] Reliability configuration
108
- # @return [Array<Class>] Error classes to retry on
109
- def retryable_errors(config)
110
- custom_errors = config[:retries]&.dig(:on) || []
111
- Reliability.default_retryable_errors + custom_errors
112
- end
113
-
114
- # Checks if the total deadline has passed
115
- #
116
- # @param deadline [Time, nil] The deadline
117
- # @return [Boolean] true if past deadline
118
- def past_deadline?(deadline)
119
- deadline && Time.current > deadline
120
- end
121
-
122
- # Gets or creates a circuit breaker for a model
123
- #
124
- # @param model_id [String] The model identifier
125
- # @param tenant_id [String, nil] Optional tenant identifier for multi-tenant isolation
126
- # @return [CircuitBreaker, nil] The circuit breaker or nil if not configured
127
- def get_circuit_breaker(model_id, tenant_id: nil)
128
- config = reliability_config[:circuit_breaker]
129
- return nil unless config
130
-
131
- CircuitBreaker.from_config(self.class.name, model_id, config, tenant_id: tenant_id)
132
- end
133
- end
134
- end
135
- end
136
- end
@@ -1,86 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RubyLLM
4
- module Agents
5
- class Base
6
- # Result object construction from LLM responses
7
- #
8
- # Handles building Result objects with full execution metadata
9
- # including tokens, costs, timing, and tool calls.
10
- module ResponseBuilding
11
- # Builds a Result object from processed content and response metadata
12
- #
13
- # @param content [Hash, String] The processed response content
14
- # @param response [RubyLLM::Message] The raw LLM response
15
- # @return [Result] A Result object with full execution metadata
16
- def build_result(content, response)
17
- completed_at = Time.current
18
- input_tokens = result_response_value(response, :input_tokens)
19
- output_tokens = result_response_value(response, :output_tokens)
20
- response_model_id = result_response_value(response, :model_id)
21
-
22
- Result.new(
23
- content: content,
24
- input_tokens: input_tokens,
25
- output_tokens: output_tokens,
26
- cached_tokens: result_response_value(response, :cached_tokens, 0),
27
- cache_creation_tokens: result_response_value(response, :cache_creation_tokens, 0),
28
- model_id: model,
29
- chosen_model_id: response_model_id || model,
30
- temperature: temperature,
31
- started_at: @execution_started_at,
32
- completed_at: completed_at,
33
- duration_ms: result_duration_ms(completed_at),
34
- time_to_first_token_ms: @time_to_first_token_ms,
35
- finish_reason: result_finish_reason(response),
36
- streaming: self.class.streaming,
37
- input_cost: result_input_cost(input_tokens, response_model_id),
38
- output_cost: result_output_cost(output_tokens, response_model_id),
39
- total_cost: result_total_cost(input_tokens, output_tokens, response_model_id),
40
- tool_calls: @accumulated_tool_calls,
41
- tool_calls_count: @accumulated_tool_calls.size
42
- )
43
- end
44
-
45
- # Safely extracts a value from the response object
46
- #
47
- # @param response [Object] The response object
48
- # @param method [Symbol] The method to call
49
- # @param default [Object] Default value if method doesn't exist
50
- # @return [Object] The extracted value or default
51
- def result_response_value(response, method, default = nil)
52
- return default unless response.respond_to?(method)
53
- response.send(method) || default
54
- end
55
-
56
- # Calculates execution duration in milliseconds
57
- #
58
- # @param completed_at [Time] When execution completed
59
- # @return [Integer, nil] Duration in ms or nil
60
- def result_duration_ms(completed_at)
61
- return nil unless @execution_started_at
62
- ((completed_at - @execution_started_at) * 1000).to_i
63
- end
64
-
65
- # Extracts finish reason from response
66
- #
67
- # @param response [Object] The response object
68
- # @return [String, nil] Normalized finish reason
69
- def result_finish_reason(response)
70
- reason = result_response_value(response, :finish_reason) ||
71
- result_response_value(response, :stop_reason)
72
- return nil unless reason
73
-
74
- # Normalize to standard values
75
- case reason.to_s.downcase
76
- when "stop", "end_turn" then "stop"
77
- when "length", "max_tokens" then "length"
78
- when "content_filter", "safety" then "content_filter"
79
- when "tool_calls", "tool_use" then "tool_calls"
80
- else "other"
81
- end
82
- end
83
- end
84
- end
85
- end
86
- end
@@ -1,57 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RubyLLM
4
- module Agents
5
- class Base
6
- # Tool call tracking for agent executions
7
- #
8
- # Handles accumulating and serializing tool calls made during
9
- # an agent's execution cycle.
10
- module ToolTracking
11
- # Resets accumulated tool calls for a new execution
12
- #
13
- # @return [void]
14
- def reset_accumulated_tool_calls!
15
- @accumulated_tool_calls = []
16
- end
17
-
18
- # Extracts tool calls from all assistant messages in the conversation
19
- #
20
- # RubyLLM handles tool call loops internally. After ask() completes,
21
- # the conversation history contains all intermediate assistant messages
22
- # that had tool_calls. This method extracts those tool calls.
23
- #
24
- # @param client [RubyLLM::Chat] The chat client with conversation history
25
- # @return [void]
26
- def extract_tool_calls_from_client(client)
27
- return unless client.respond_to?(:messages)
28
-
29
- client.messages.each do |message|
30
- next unless message.role == :assistant
31
- next unless message.respond_to?(:tool_calls) && message.tool_calls.present?
32
-
33
- message.tool_calls.each_value do |tool_call|
34
- @accumulated_tool_calls << serialize_tool_call(tool_call)
35
- end
36
- end
37
- end
38
-
39
- # Serializes a single tool call to a hash
40
- #
41
- # @param tool_call [Object] The tool call object
42
- # @return [Hash] Serialized tool call
43
- def serialize_tool_call(tool_call)
44
- if tool_call.respond_to?(:to_h)
45
- tool_call.to_h.transform_keys(&:to_s)
46
- else
47
- {
48
- "id" => tool_call.id,
49
- "name" => tool_call.name,
50
- "arguments" => tool_call.arguments
51
- }
52
- end
53
- end
54
- end
55
- end
56
- end
57
- end
@@ -1,209 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "base/dsl"
4
- require_relative "base/caching"
5
- require_relative "base/cost_calculation"
6
- require_relative "base/tool_tracking"
7
- require_relative "base/response_building"
8
- require_relative "base/execution"
9
- require_relative "base/reliability_execution"
10
-
11
- module RubyLLM
12
- module Agents
13
- # Base class for LLM-powered agents
14
- #
15
- # Provides a DSL for configuring and executing agents that interact with
16
- # large language models. Includes built-in support for caching, timeouts,
17
- # structured output, and execution tracking.
18
- #
19
- # @example Creating an agent
20
- # class SearchAgent < ApplicationAgent
21
- # model "gpt-4o"
22
- # temperature 0.0
23
- # version "1.0"
24
- # timeout 30
25
- # cache 1.hour
26
- #
27
- # param :query, required: true
28
- # param :limit, default: 10
29
- #
30
- # def system_prompt
31
- # "You are a search assistant..."
32
- # end
33
- #
34
- # def user_prompt
35
- # "Search for: #{query}"
36
- # end
37
- # end
38
- #
39
- # @example Calling an agent
40
- # SearchAgent.call(query: "red dress")
41
- # SearchAgent.call(query: "red dress", dry_run: true) # Debug mode
42
- # SearchAgent.call(query: "red dress", skip_cache: true) # Bypass cache
43
- #
44
- # @see RubyLLM::Agents::Instrumentation
45
- # @api public
46
- class Base
47
- include Instrumentation
48
- include Caching
49
- include CostCalculation
50
- include ToolTracking
51
- include ResponseBuilding
52
- include Execution
53
- include ReliabilityExecution
54
-
55
- extend DSL
56
-
57
- class << self
58
- # Factory method to instantiate and execute an agent
59
- #
60
- # @param args [Array] Positional arguments (reserved for future use)
61
- # @param kwargs [Hash] Named parameters for the agent
62
- # @option kwargs [Boolean] :dry_run Return prompt info without API call
63
- # @option kwargs [Boolean] :skip_cache Bypass caching even if enabled
64
- # @option kwargs [String, Array<String>] :with Attachments (files, URLs) to send with the prompt
65
- # @yield [chunk] Yields chunks when streaming is enabled
66
- # @yieldparam chunk [RubyLLM::Chunk] A streaming chunk with content
67
- # @return [Object] The processed response from the agent
68
- #
69
- # @example Basic usage
70
- # SearchAgent.call(query: "red dress")
71
- #
72
- # @example Debug mode
73
- # SearchAgent.call(query: "red dress", dry_run: true)
74
- #
75
- # @example Streaming mode
76
- # ChatAgent.call(message: "Hello") do |chunk|
77
- # print chunk.content
78
- # end
79
- #
80
- # @example With attachments
81
- # VisionAgent.call(query: "Describe this image", with: "photo.jpg")
82
- # VisionAgent.call(query: "Compare these", with: ["a.png", "b.png"])
83
- def call(*args, **kwargs, &block)
84
- new(*args, **kwargs).call(&block)
85
- end
86
-
87
- # Streams agent execution, yielding chunks as they arrive
88
- #
89
- # A more explicit alternative to passing a block to call.
90
- # Forces streaming mode for this invocation regardless of class setting.
91
- #
92
- # @param kwargs [Hash] Agent parameters
93
- # @yield [chunk] Yields each chunk as it arrives
94
- # @yieldparam chunk [RubyLLM::Chunk] Streaming chunk with content
95
- # @return [Result] The final result after streaming completes
96
- # @raise [ArgumentError] If no block is provided
97
- #
98
- # @example Basic streaming
99
- # MyAgent.stream(query: "test") do |chunk|
100
- # print chunk.content
101
- # end
102
- #
103
- # @example With result metadata
104
- # result = MyAgent.stream(query: "test") { |c| print c.content }
105
- # puts "\nTokens: #{result.total_tokens}"
106
- def stream(**kwargs, &block)
107
- raise ArgumentError, "Block required for streaming" unless block_given?
108
-
109
- instance = new(**kwargs)
110
- instance.instance_variable_set(:@force_streaming, true)
111
- instance.call(&block)
112
- end
113
- end
114
-
115
- # @!attribute [r] model
116
- # @return [String] The LLM model being used
117
- # @!attribute [r] temperature
118
- # @return [Float] The temperature setting
119
- # @!attribute [r] client
120
- # @return [RubyLLM::Chat] The configured RubyLLM client
121
- # @!attribute [r] time_to_first_token_ms
122
- # @return [Integer, nil] Time to first token in milliseconds (streaming only)
123
- # @!attribute [r] accumulated_tool_calls
124
- # @return [Array<Hash>] Tool calls accumulated during execution
125
- attr_reader :model, :temperature, :client, :time_to_first_token_ms, :accumulated_tool_calls
126
-
127
- # Creates a new agent instance
128
- #
129
- # @param model [String] Override the class-level model setting
130
- # @param temperature [Float] Override the class-level temperature
131
- # @param options [Hash] Agent parameters defined via the param DSL
132
- # @raise [ArgumentError] If required parameters are missing
133
- def initialize(model: self.class.model, temperature: self.class.temperature, **options)
134
- @model = model
135
- @temperature = temperature
136
- @options = options
137
- @accumulated_tool_calls = []
138
- validate_required_params!
139
- @client = build_client
140
- end
141
-
142
- # @!group Template Methods (override in subclasses)
143
-
144
- # User prompt to send to the LLM
145
- #
146
- # @abstract Subclasses must implement this method
147
- # @return [String] The user prompt
148
- # @raise [NotImplementedError] If not overridden in subclass
149
- def user_prompt
150
- raise NotImplementedError, "#{self.class} must implement #user_prompt"
151
- end
152
-
153
- # System prompt for LLM instructions
154
- #
155
- # @return [String, nil] System instructions, or nil for none
156
- def system_prompt
157
- nil
158
- end
159
-
160
- # Response schema for structured output
161
- #
162
- # @return [RubyLLM::Schema, nil] Schema definition, or nil for free-form
163
- def schema
164
- nil
165
- end
166
-
167
- # Conversation history for multi-turn conversations
168
- #
169
- # Override in subclass to provide conversation history.
170
- # Messages will be added to the chat before the user_prompt.
171
- #
172
- # @return [Array<Hash>] Array of messages with :role and :content keys
173
- # @example
174
- # def messages
175
- # [{ role: :user, content: "Hello" }, { role: :assistant, content: "Hi!" }]
176
- # end
177
- def messages
178
- []
179
- end
180
-
181
- # Post-processes the LLM response
182
- #
183
- # Override to transform the response before returning to the caller.
184
- # Default implementation symbolizes hash keys.
185
- #
186
- # @param response [RubyLLM::Message] The raw response from the LLM
187
- # @return [Object] The processed result
188
- def process_response(response)
189
- content = response.content
190
- return content unless content.is_a?(Hash)
191
- content.transform_keys(&:to_sym)
192
- end
193
-
194
- # @!endgroup
195
-
196
- # Sets conversation history and rebuilds the client
197
- #
198
- # @param msgs [Array<Hash>] Messages with :role and :content keys
199
- # @return [self] Returns self for chaining
200
- # @example
201
- # agent.with_messages([{ role: :user, content: "Hi" }]).call
202
- def with_messages(msgs)
203
- @override_messages = msgs
204
- @client = build_client
205
- self
206
- end
207
- end
208
- end
209
- end