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.
- checksums.yaml +4 -4
- data/README.md +189 -31
- data/app/controllers/ruby_llm/agents/agents_controller.rb +136 -16
- data/app/controllers/ruby_llm/agents/dashboard_controller.rb +29 -9
- data/app/controllers/ruby_llm/agents/workflows_controller.rb +355 -0
- data/app/helpers/ruby_llm/agents/application_helper.rb +25 -0
- data/app/models/ruby_llm/agents/execution.rb +3 -0
- data/app/models/ruby_llm/agents/tenant_budget.rb +58 -15
- data/app/services/ruby_llm/agents/agent_registry.rb +51 -12
- data/app/views/layouts/ruby_llm/agents/application.html.erb +2 -29
- data/app/views/ruby_llm/agents/agents/_agent.html.erb +13 -1
- data/app/views/ruby_llm/agents/agents/_config_agent.html.erb +235 -0
- data/app/views/ruby_llm/agents/agents/_config_embedder.html.erb +70 -0
- data/app/views/ruby_llm/agents/agents/_config_image_generator.html.erb +152 -0
- data/app/views/ruby_llm/agents/agents/_config_moderator.html.erb +63 -0
- data/app/views/ruby_llm/agents/agents/_config_speaker.html.erb +108 -0
- data/app/views/ruby_llm/agents/agents/_config_transcriber.html.erb +91 -0
- data/app/views/ruby_llm/agents/agents/_workflow.html.erb +1 -1
- data/app/views/ruby_llm/agents/agents/index.html.erb +74 -9
- data/app/views/ruby_llm/agents/agents/show.html.erb +18 -378
- data/app/views/ruby_llm/agents/dashboard/_agent_comparison.html.erb +269 -15
- data/app/views/ruby_llm/agents/executions/show.html.erb +16 -0
- data/app/views/ruby_llm/agents/shared/_agent_type_badge.html.erb +93 -0
- data/app/views/ruby_llm/agents/workflows/_step_performance.html.erb +236 -0
- data/app/views/ruby_llm/agents/workflows/_structure_parallel.html.erb +76 -0
- data/app/views/ruby_llm/agents/workflows/_structure_pipeline.html.erb +74 -0
- data/app/views/ruby_llm/agents/workflows/_structure_router.html.erb +108 -0
- data/app/views/ruby_llm/agents/workflows/show.html.erb +442 -0
- data/config/routes.rb +1 -0
- data/lib/generators/ruby_llm_agents/agent_generator.rb +56 -7
- data/lib/generators/ruby_llm_agents/background_remover_generator.rb +110 -0
- data/lib/generators/ruby_llm_agents/embedder_generator.rb +107 -0
- data/lib/generators/ruby_llm_agents/image_analyzer_generator.rb +115 -0
- data/lib/generators/ruby_llm_agents/image_editor_generator.rb +108 -0
- data/lib/generators/ruby_llm_agents/image_generator_generator.rb +116 -0
- data/lib/generators/ruby_llm_agents/image_pipeline_generator.rb +178 -0
- data/lib/generators/ruby_llm_agents/image_transformer_generator.rb +109 -0
- data/lib/generators/ruby_llm_agents/image_upscaler_generator.rb +103 -0
- data/lib/generators/ruby_llm_agents/image_variator_generator.rb +102 -0
- data/lib/generators/ruby_llm_agents/install_generator.rb +76 -4
- data/lib/generators/ruby_llm_agents/restructure_generator.rb +292 -0
- data/lib/generators/ruby_llm_agents/speaker_generator.rb +121 -0
- data/lib/generators/ruby_llm_agents/templates/add_execution_type_migration.rb.tt +8 -0
- data/lib/generators/ruby_llm_agents/templates/agent.rb.tt +99 -84
- data/lib/generators/ruby_llm_agents/templates/application_agent.rb.tt +42 -40
- data/lib/generators/ruby_llm_agents/templates/application_background_remover.rb.tt +26 -0
- data/lib/generators/ruby_llm_agents/templates/application_embedder.rb.tt +50 -0
- data/lib/generators/ruby_llm_agents/templates/application_image_analyzer.rb.tt +26 -0
- data/lib/generators/ruby_llm_agents/templates/application_image_editor.rb.tt +20 -0
- data/lib/generators/ruby_llm_agents/templates/application_image_generator.rb.tt +38 -0
- data/lib/generators/ruby_llm_agents/templates/application_image_pipeline.rb.tt +139 -0
- data/lib/generators/ruby_llm_agents/templates/application_image_transformer.rb.tt +21 -0
- data/lib/generators/ruby_llm_agents/templates/application_image_upscaler.rb.tt +20 -0
- data/lib/generators/ruby_llm_agents/templates/application_image_variator.rb.tt +20 -0
- data/lib/generators/ruby_llm_agents/templates/application_speaker.rb.tt +49 -0
- data/lib/generators/ruby_llm_agents/templates/application_transcriber.rb.tt +53 -0
- data/lib/generators/ruby_llm_agents/templates/background_remover.rb.tt +44 -0
- data/lib/generators/ruby_llm_agents/templates/embedder.rb.tt +41 -0
- data/lib/generators/ruby_llm_agents/templates/image_analyzer.rb.tt +45 -0
- data/lib/generators/ruby_llm_agents/templates/image_editor.rb.tt +35 -0
- data/lib/generators/ruby_llm_agents/templates/image_generator.rb.tt +47 -0
- data/lib/generators/ruby_llm_agents/templates/image_pipeline.rb.tt +50 -0
- data/lib/generators/ruby_llm_agents/templates/image_transformer.rb.tt +44 -0
- data/lib/generators/ruby_llm_agents/templates/image_upscaler.rb.tt +38 -0
- data/lib/generators/ruby_llm_agents/templates/image_variator.rb.tt +33 -0
- data/lib/generators/ruby_llm_agents/templates/skills/AGENTS.md.tt +228 -0
- data/lib/generators/ruby_llm_agents/templates/skills/BACKGROUND_REMOVERS.md.tt +131 -0
- data/lib/generators/ruby_llm_agents/templates/skills/EMBEDDERS.md.tt +255 -0
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_ANALYZERS.md.tt +120 -0
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_EDITORS.md.tt +102 -0
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_GENERATORS.md.tt +282 -0
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_PIPELINES.md.tt +228 -0
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_TRANSFORMERS.md.tt +120 -0
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_UPSCALERS.md.tt +110 -0
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_VARIATORS.md.tt +120 -0
- data/lib/generators/ruby_llm_agents/templates/skills/SPEAKERS.md.tt +212 -0
- data/lib/generators/ruby_llm_agents/templates/skills/TOOLS.md.tt +227 -0
- data/lib/generators/ruby_llm_agents/templates/skills/TRANSCRIBERS.md.tt +251 -0
- data/lib/generators/ruby_llm_agents/templates/skills/WORKFLOWS.md.tt +300 -0
- data/lib/generators/ruby_llm_agents/templates/speaker.rb.tt +56 -0
- data/lib/generators/ruby_llm_agents/templates/transcriber.rb.tt +51 -0
- data/lib/generators/ruby_llm_agents/transcriber_generator.rb +107 -0
- data/lib/generators/ruby_llm_agents/upgrade_generator.rb +152 -1
- data/lib/ruby_llm/agents/audio/speaker.rb +553 -0
- data/lib/ruby_llm/agents/audio/transcriber.rb +669 -0
- data/lib/ruby_llm/agents/base_agent.rb +675 -0
- data/lib/ruby_llm/agents/core/base/moderation_dsl.rb +181 -0
- data/lib/ruby_llm/agents/core/base/moderation_execution.rb +274 -0
- data/lib/ruby_llm/agents/core/base.rb +135 -0
- data/lib/ruby_llm/agents/core/configuration.rb +981 -0
- data/lib/ruby_llm/agents/core/errors.rb +150 -0
- data/lib/ruby_llm/agents/{instrumentation.rb → core/instrumentation.rb} +22 -1
- data/lib/ruby_llm/agents/core/llm_tenant.rb +358 -0
- data/lib/ruby_llm/agents/{version.rb → core/version.rb} +1 -1
- data/lib/ruby_llm/agents/dsl/base.rb +110 -0
- data/lib/ruby_llm/agents/dsl/caching.rb +142 -0
- data/lib/ruby_llm/agents/dsl/reliability.rb +307 -0
- data/lib/ruby_llm/agents/dsl.rb +41 -0
- data/lib/ruby_llm/agents/image/analyzer/dsl.rb +130 -0
- data/lib/ruby_llm/agents/image/analyzer/execution.rb +402 -0
- data/lib/ruby_llm/agents/image/analyzer.rb +90 -0
- data/lib/ruby_llm/agents/image/background_remover/dsl.rb +154 -0
- data/lib/ruby_llm/agents/image/background_remover/execution.rb +240 -0
- data/lib/ruby_llm/agents/image/background_remover.rb +89 -0
- data/lib/ruby_llm/agents/image/concerns/image_operation_dsl.rb +91 -0
- data/lib/ruby_llm/agents/image/concerns/image_operation_execution.rb +165 -0
- data/lib/ruby_llm/agents/image/editor/dsl.rb +56 -0
- data/lib/ruby_llm/agents/image/editor/execution.rb +207 -0
- data/lib/ruby_llm/agents/image/editor.rb +92 -0
- data/lib/ruby_llm/agents/image/generator/active_storage_support.rb +127 -0
- data/lib/ruby_llm/agents/image/generator/content_policy.rb +95 -0
- data/lib/ruby_llm/agents/image/generator/pricing.rb +353 -0
- data/lib/ruby_llm/agents/image/generator/templates.rb +124 -0
- data/lib/ruby_llm/agents/image/generator.rb +455 -0
- data/lib/ruby_llm/agents/image/pipeline/dsl.rb +213 -0
- data/lib/ruby_llm/agents/image/pipeline/execution.rb +382 -0
- data/lib/ruby_llm/agents/image/pipeline.rb +97 -0
- data/lib/ruby_llm/agents/image/transformer/dsl.rb +148 -0
- data/lib/ruby_llm/agents/image/transformer/execution.rb +223 -0
- data/lib/ruby_llm/agents/image/transformer.rb +95 -0
- data/lib/ruby_llm/agents/image/upscaler/dsl.rb +83 -0
- data/lib/ruby_llm/agents/image/upscaler/execution.rb +219 -0
- data/lib/ruby_llm/agents/image/upscaler.rb +81 -0
- data/lib/ruby_llm/agents/image/variator/dsl.rb +62 -0
- data/lib/ruby_llm/agents/image/variator/execution.rb +189 -0
- data/lib/ruby_llm/agents/image/variator.rb +80 -0
- data/lib/ruby_llm/agents/{alert_manager.rb → infrastructure/alert_manager.rb} +17 -22
- data/lib/ruby_llm/agents/infrastructure/budget/budget_query.rb +145 -0
- data/lib/ruby_llm/agents/infrastructure/budget/config_resolver.rb +149 -0
- data/lib/ruby_llm/agents/infrastructure/budget/forecaster.rb +68 -0
- data/lib/ruby_llm/agents/infrastructure/budget/spend_recorder.rb +279 -0
- data/lib/ruby_llm/agents/infrastructure/budget_tracker.rb +275 -0
- data/lib/ruby_llm/agents/{execution_logger_job.rb → infrastructure/execution_logger_job.rb} +17 -1
- data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/executor.rb +2 -1
- data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/retry_strategy.rb +9 -3
- data/lib/ruby_llm/agents/{reliability.rb → infrastructure/reliability.rb} +11 -21
- data/lib/ruby_llm/agents/pipeline/builder.rb +215 -0
- data/lib/ruby_llm/agents/pipeline/context.rb +255 -0
- data/lib/ruby_llm/agents/pipeline/executor.rb +86 -0
- data/lib/ruby_llm/agents/pipeline/middleware/base.rb +124 -0
- data/lib/ruby_llm/agents/pipeline/middleware/budget.rb +95 -0
- data/lib/ruby_llm/agents/pipeline/middleware/cache.rb +171 -0
- data/lib/ruby_llm/agents/pipeline/middleware/instrumentation.rb +415 -0
- data/lib/ruby_llm/agents/pipeline/middleware/reliability.rb +276 -0
- data/lib/ruby_llm/agents/pipeline/middleware/tenant.rb +196 -0
- data/lib/ruby_llm/agents/pipeline.rb +68 -0
- data/lib/ruby_llm/agents/{engine.rb → rails/engine.rb} +79 -11
- data/lib/ruby_llm/agents/results/background_removal_result.rb +286 -0
- data/lib/ruby_llm/agents/{result.rb → results/base.rb} +73 -1
- data/lib/ruby_llm/agents/results/embedding_result.rb +243 -0
- data/lib/ruby_llm/agents/results/image_analysis_result.rb +314 -0
- data/lib/ruby_llm/agents/results/image_edit_result.rb +250 -0
- data/lib/ruby_llm/agents/results/image_generation_result.rb +346 -0
- data/lib/ruby_llm/agents/results/image_pipeline_result.rb +399 -0
- data/lib/ruby_llm/agents/results/image_transform_result.rb +251 -0
- data/lib/ruby_llm/agents/results/image_upscale_result.rb +255 -0
- data/lib/ruby_llm/agents/results/image_variation_result.rb +237 -0
- data/lib/ruby_llm/agents/results/moderation_result.rb +158 -0
- data/lib/ruby_llm/agents/results/speech_result.rb +338 -0
- data/lib/ruby_llm/agents/results/transcription_result.rb +408 -0
- data/lib/ruby_llm/agents/text/embedder.rb +444 -0
- data/lib/ruby_llm/agents/text/moderator.rb +237 -0
- data/lib/ruby_llm/agents/workflow/async.rb +220 -0
- data/lib/ruby_llm/agents/workflow/async_executor.rb +156 -0
- data/lib/ruby_llm/agents/{workflow.rb → workflow/orchestrator.rb} +6 -5
- data/lib/ruby_llm/agents/workflow/parallel.rb +34 -17
- data/lib/ruby_llm/agents/workflow/thread_pool.rb +185 -0
- data/lib/ruby_llm/agents.rb +86 -20
- metadata +172 -34
- data/lib/ruby_llm/agents/base/caching.rb +0 -40
- data/lib/ruby_llm/agents/base/cost_calculation.rb +0 -105
- data/lib/ruby_llm/agents/base/dsl.rb +0 -324
- data/lib/ruby_llm/agents/base/execution.rb +0 -366
- data/lib/ruby_llm/agents/base/reliability_dsl.rb +0 -82
- data/lib/ruby_llm/agents/base/reliability_execution.rb +0 -136
- data/lib/ruby_llm/agents/base/response_building.rb +0 -86
- data/lib/ruby_llm/agents/base/tool_tracking.rb +0 -57
- data/lib/ruby_llm/agents/base.rb +0 -210
- data/lib/ruby_llm/agents/budget_tracker.rb +0 -733
- data/lib/ruby_llm/agents/configuration.rb +0 -394
- /data/lib/ruby_llm/agents/{deprecations.rb → core/deprecations.rb} +0 -0
- /data/lib/ruby_llm/agents/{inflections.rb → core/inflections.rb} +0 -0
- /data/lib/ruby_llm/agents/{resolved_config.rb → core/resolved_config.rb} +0 -0
- /data/lib/ruby_llm/agents/{attempt_tracker.rb → infrastructure/attempt_tracker.rb} +0 -0
- /data/lib/ruby_llm/agents/{cache_helper.rb → infrastructure/cache_helper.rb} +0 -0
- /data/lib/ruby_llm/agents/{circuit_breaker.rb → infrastructure/circuit_breaker.rb} +0 -0
- /data/lib/ruby_llm/agents/{redactor.rb → infrastructure/redactor.rb} +0 -0
- /data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/breaker_manager.rb +0 -0
- /data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/execution_constraints.rb +0 -0
- /data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/fallback_routing.rb +0 -0
|
@@ -93,6 +93,24 @@ module RubyLLM
|
|
|
93
93
|
# @return [Integer] Number of tool calls made
|
|
94
94
|
attr_reader :tool_calls, :tool_calls_count
|
|
95
95
|
|
|
96
|
+
# @!group Thinking
|
|
97
|
+
# @!attribute [r] thinking_text
|
|
98
|
+
# @return [String, nil] The reasoning/thinking content from the model
|
|
99
|
+
# @!attribute [r] thinking_signature
|
|
100
|
+
# @return [String, nil] Signature for multi-turn thinking continuity (Claude)
|
|
101
|
+
# @!attribute [r] thinking_tokens
|
|
102
|
+
# @return [Integer, nil] Number of tokens used for thinking
|
|
103
|
+
attr_reader :thinking_text, :thinking_signature, :thinking_tokens
|
|
104
|
+
|
|
105
|
+
# @!group Moderation
|
|
106
|
+
# @!attribute [r] status
|
|
107
|
+
# @return [Symbol, nil] Result status (:success, :input_moderation_blocked, :output_moderation_blocked)
|
|
108
|
+
# @!attribute [r] moderation_result
|
|
109
|
+
# @return [Object, nil] The raw moderation result from RubyLLM
|
|
110
|
+
# @!attribute [r] moderation_phase
|
|
111
|
+
# @return [Symbol, nil] The phase where moderation blocked (:input or :output)
|
|
112
|
+
attr_reader :status, :moderation_result, :moderation_phase
|
|
113
|
+
|
|
96
114
|
# Creates a new Result instance
|
|
97
115
|
#
|
|
98
116
|
# @param content [Hash, String] The processed response content
|
|
@@ -137,6 +155,17 @@ module RubyLLM
|
|
|
137
155
|
# Tool calls
|
|
138
156
|
@tool_calls = options[:tool_calls] || []
|
|
139
157
|
@tool_calls_count = options[:tool_calls_count] || 0
|
|
158
|
+
|
|
159
|
+
# Thinking
|
|
160
|
+
@thinking_text = options[:thinking_text]
|
|
161
|
+
@thinking_signature = options[:thinking_signature]
|
|
162
|
+
@thinking_tokens = options[:thinking_tokens]
|
|
163
|
+
|
|
164
|
+
# Moderation
|
|
165
|
+
@status = options[:status] || :success
|
|
166
|
+
@moderation_flagged = options[:moderation_flagged] || false
|
|
167
|
+
@moderation_result = options[:moderation_result]
|
|
168
|
+
@moderation_phase = options[:moderation_phase]
|
|
140
169
|
end
|
|
141
170
|
|
|
142
171
|
# Returns total tokens (input + output)
|
|
@@ -188,6 +217,41 @@ module RubyLLM
|
|
|
188
217
|
tool_calls_count.to_i > 0
|
|
189
218
|
end
|
|
190
219
|
|
|
220
|
+
# Returns whether thinking data is present in the result
|
|
221
|
+
#
|
|
222
|
+
# @return [Boolean] true if thinking_text is present
|
|
223
|
+
def has_thinking?
|
|
224
|
+
thinking_text.present?
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Returns whether content was flagged by moderation
|
|
228
|
+
#
|
|
229
|
+
# @return [Boolean] true if moderation flagged the content
|
|
230
|
+
def moderation_flagged?
|
|
231
|
+
@moderation_flagged == true
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# Returns whether content passed moderation
|
|
235
|
+
#
|
|
236
|
+
# @return [Boolean] true if content was not flagged
|
|
237
|
+
def moderation_passed?
|
|
238
|
+
!moderation_flagged?
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Returns the categories flagged by moderation
|
|
242
|
+
#
|
|
243
|
+
# @return [Array<String, Symbol>] Flagged category names
|
|
244
|
+
def moderation_categories
|
|
245
|
+
@moderation_result&.flagged_categories || []
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# Returns the moderation category scores
|
|
249
|
+
#
|
|
250
|
+
# @return [Hash{String, Symbol => Float}] Category to score mapping
|
|
251
|
+
def moderation_scores
|
|
252
|
+
@moderation_result&.category_scores || {}
|
|
253
|
+
end
|
|
254
|
+
|
|
191
255
|
# Converts the result to a hash
|
|
192
256
|
#
|
|
193
257
|
# @return [Hash] All result data as a hash
|
|
@@ -216,7 +280,15 @@ module RubyLLM
|
|
|
216
280
|
attempts_count: attempts_count,
|
|
217
281
|
attempts: attempts,
|
|
218
282
|
tool_calls: tool_calls,
|
|
219
|
-
tool_calls_count: tool_calls_count
|
|
283
|
+
tool_calls_count: tool_calls_count,
|
|
284
|
+
thinking_text: thinking_text,
|
|
285
|
+
thinking_signature: thinking_signature,
|
|
286
|
+
thinking_tokens: thinking_tokens,
|
|
287
|
+
status: status,
|
|
288
|
+
moderation_flagged: moderation_flagged?,
|
|
289
|
+
moderation_phase: moderation_phase,
|
|
290
|
+
moderation_categories: moderation_categories,
|
|
291
|
+
moderation_scores: moderation_scores
|
|
220
292
|
}
|
|
221
293
|
end
|
|
222
294
|
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Agents
|
|
5
|
+
# Result object for embedding operations
|
|
6
|
+
#
|
|
7
|
+
# Wraps embedding vectors with metadata about the operation including
|
|
8
|
+
# token usage, cost, timing, and utility methods for similarity calculations.
|
|
9
|
+
#
|
|
10
|
+
# @example Single text embedding
|
|
11
|
+
# result = MyEmbedder.call(text: "Hello world")
|
|
12
|
+
# result.vector # => [0.123, -0.456, ...]
|
|
13
|
+
# result.dimensions # => 1536
|
|
14
|
+
# result.input_tokens # => 2
|
|
15
|
+
#
|
|
16
|
+
# @example Batch embedding
|
|
17
|
+
# result = MyEmbedder.call(texts: ["Hello", "World"])
|
|
18
|
+
# result.vectors # => [[...], [...]]
|
|
19
|
+
# result.count # => 2
|
|
20
|
+
#
|
|
21
|
+
# @example Similarity comparison
|
|
22
|
+
# result1 = MyEmbedder.call(text: "Ruby programming")
|
|
23
|
+
# result2 = MyEmbedder.call(text: "Python programming")
|
|
24
|
+
# result1.similarity(result2) # => 0.85
|
|
25
|
+
#
|
|
26
|
+
# @api public
|
|
27
|
+
class EmbeddingResult
|
|
28
|
+
# @!attribute [r] vectors
|
|
29
|
+
# @return [Array<Array<Float>>] The embedding vectors
|
|
30
|
+
attr_reader :vectors
|
|
31
|
+
|
|
32
|
+
# @!attribute [r] model_id
|
|
33
|
+
# @return [String, nil] The embedding model used
|
|
34
|
+
attr_reader :model_id
|
|
35
|
+
|
|
36
|
+
# @!attribute [r] dimensions
|
|
37
|
+
# @return [Integer, nil] The dimensionality of the vectors
|
|
38
|
+
attr_reader :dimensions
|
|
39
|
+
|
|
40
|
+
# @!attribute [r] input_tokens
|
|
41
|
+
# @return [Integer, nil] Number of input tokens consumed
|
|
42
|
+
attr_reader :input_tokens
|
|
43
|
+
|
|
44
|
+
# @!attribute [r] total_cost
|
|
45
|
+
# @return [Float, nil] Total cost in USD
|
|
46
|
+
attr_reader :total_cost
|
|
47
|
+
|
|
48
|
+
# @!attribute [r] duration_ms
|
|
49
|
+
# @return [Integer, nil] Execution duration in milliseconds
|
|
50
|
+
attr_reader :duration_ms
|
|
51
|
+
|
|
52
|
+
# @!attribute [r] count
|
|
53
|
+
# @return [Integer] Number of texts embedded
|
|
54
|
+
attr_reader :count
|
|
55
|
+
|
|
56
|
+
# @!attribute [r] started_at
|
|
57
|
+
# @return [Time, nil] When execution started
|
|
58
|
+
attr_reader :started_at
|
|
59
|
+
|
|
60
|
+
# @!attribute [r] completed_at
|
|
61
|
+
# @return [Time, nil] When execution completed
|
|
62
|
+
attr_reader :completed_at
|
|
63
|
+
|
|
64
|
+
# @!attribute [r] tenant_id
|
|
65
|
+
# @return [String, nil] Tenant identifier if multi-tenancy enabled
|
|
66
|
+
attr_reader :tenant_id
|
|
67
|
+
|
|
68
|
+
# @!attribute [r] error_class
|
|
69
|
+
# @return [String, nil] Exception class name if failed
|
|
70
|
+
attr_reader :error_class
|
|
71
|
+
|
|
72
|
+
# @!attribute [r] error_message
|
|
73
|
+
# @return [String, nil] Exception message if failed
|
|
74
|
+
attr_reader :error_message
|
|
75
|
+
|
|
76
|
+
# Creates a new EmbeddingResult instance
|
|
77
|
+
#
|
|
78
|
+
# @param attributes [Hash] Result attributes
|
|
79
|
+
# @option attributes [Array<Array<Float>>] :vectors The embedding vectors
|
|
80
|
+
# @option attributes [String] :model_id The model used
|
|
81
|
+
# @option attributes [Integer] :dimensions Vector dimensionality
|
|
82
|
+
# @option attributes [Integer] :input_tokens Tokens consumed
|
|
83
|
+
# @option attributes [Float] :total_cost Cost in USD
|
|
84
|
+
# @option attributes [Integer] :duration_ms Duration in milliseconds
|
|
85
|
+
# @option attributes [Integer] :count Number of texts
|
|
86
|
+
# @option attributes [Time] :started_at Start time
|
|
87
|
+
# @option attributes [Time] :completed_at Completion time
|
|
88
|
+
# @option attributes [String] :tenant_id Tenant identifier
|
|
89
|
+
# @option attributes [String] :error_class Error class name
|
|
90
|
+
# @option attributes [String] :error_message Error message
|
|
91
|
+
def initialize(attributes = {})
|
|
92
|
+
@vectors = attributes[:vectors] || []
|
|
93
|
+
@model_id = attributes[:model_id]
|
|
94
|
+
@dimensions = attributes[:dimensions]
|
|
95
|
+
@input_tokens = attributes[:input_tokens]
|
|
96
|
+
@total_cost = attributes[:total_cost]
|
|
97
|
+
@duration_ms = attributes[:duration_ms]
|
|
98
|
+
@count = attributes[:count] || @vectors.size
|
|
99
|
+
@started_at = attributes[:started_at]
|
|
100
|
+
@completed_at = attributes[:completed_at]
|
|
101
|
+
@tenant_id = attributes[:tenant_id]
|
|
102
|
+
@error_class = attributes[:error_class]
|
|
103
|
+
@error_message = attributes[:error_message]
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Returns whether this result contains a single embedding
|
|
107
|
+
#
|
|
108
|
+
# @return [Boolean] true if count is 1
|
|
109
|
+
def single?
|
|
110
|
+
count == 1
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Returns whether this result contains multiple embeddings
|
|
114
|
+
#
|
|
115
|
+
# @return [Boolean] true if count > 1
|
|
116
|
+
def batch?
|
|
117
|
+
count > 1
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Returns the first (or only) embedding vector
|
|
121
|
+
#
|
|
122
|
+
# Convenience method for single-text embeddings.
|
|
123
|
+
#
|
|
124
|
+
# @return [Array<Float>, nil] The embedding vector or nil if batch
|
|
125
|
+
def vector
|
|
126
|
+
vectors.first
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Returns whether the execution succeeded
|
|
130
|
+
#
|
|
131
|
+
# @return [Boolean] true if no error occurred
|
|
132
|
+
def success?
|
|
133
|
+
error_class.nil?
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Returns whether the execution failed
|
|
137
|
+
#
|
|
138
|
+
# @return [Boolean] true if an error occurred
|
|
139
|
+
def error?
|
|
140
|
+
!success?
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Calculates cosine similarity between this embedding and another
|
|
144
|
+
#
|
|
145
|
+
# @param other [EmbeddingResult, Array<Float>] Another embedding or vector
|
|
146
|
+
# @param index [Integer] Index of the vector to compare (for batch results)
|
|
147
|
+
# @return [Float] Cosine similarity score (-1.0 to 1.0)
|
|
148
|
+
# @example Compare two results
|
|
149
|
+
# result1.similarity(result2) # => 0.85
|
|
150
|
+
# @example Compare with raw vector
|
|
151
|
+
# result.similarity([0.1, 0.2, 0.3])
|
|
152
|
+
# @example Compare specific vector from batch
|
|
153
|
+
# batch_result.similarity(other, index: 2)
|
|
154
|
+
def similarity(other, index: 0)
|
|
155
|
+
v1 = vectors[index]
|
|
156
|
+
return nil if v1.nil?
|
|
157
|
+
|
|
158
|
+
v2 = case other
|
|
159
|
+
when EmbeddingResult
|
|
160
|
+
other.vector
|
|
161
|
+
when Array
|
|
162
|
+
other
|
|
163
|
+
else
|
|
164
|
+
raise ArgumentError, "other must be EmbeddingResult or Array, got #{other.class}"
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
return nil if v2.nil?
|
|
168
|
+
|
|
169
|
+
cosine_similarity(v1, v2)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Finds the most similar vectors from a collection
|
|
173
|
+
#
|
|
174
|
+
# @param others [Array<EmbeddingResult, Array<Float>>] Collection to search
|
|
175
|
+
# @param limit [Integer] Maximum results to return
|
|
176
|
+
# @param index [Integer] Index of the source vector (for batch results)
|
|
177
|
+
# @return [Array<Hash>] Sorted results with :index and :similarity keys
|
|
178
|
+
# @example Find top 5 similar
|
|
179
|
+
# result.most_similar(document_embeddings, limit: 5)
|
|
180
|
+
def most_similar(others, limit: 10, index: 0)
|
|
181
|
+
v1 = vectors[index]
|
|
182
|
+
return [] if v1.nil?
|
|
183
|
+
|
|
184
|
+
similarities = others.each_with_index.map do |other, idx|
|
|
185
|
+
v2 = case other
|
|
186
|
+
when EmbeddingResult
|
|
187
|
+
other.vector
|
|
188
|
+
when Array
|
|
189
|
+
other
|
|
190
|
+
else
|
|
191
|
+
next nil
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
next nil if v2.nil?
|
|
195
|
+
|
|
196
|
+
{ index: idx, similarity: cosine_similarity(v1, v2) }
|
|
197
|
+
end.compact
|
|
198
|
+
|
|
199
|
+
similarities.sort_by { |s| -s[:similarity] }.first(limit)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Converts the result to a hash
|
|
203
|
+
#
|
|
204
|
+
# @return [Hash] All result data as a hash
|
|
205
|
+
def to_h
|
|
206
|
+
{
|
|
207
|
+
vectors: vectors,
|
|
208
|
+
model_id: model_id,
|
|
209
|
+
dimensions: dimensions,
|
|
210
|
+
input_tokens: input_tokens,
|
|
211
|
+
total_cost: total_cost,
|
|
212
|
+
duration_ms: duration_ms,
|
|
213
|
+
count: count,
|
|
214
|
+
started_at: started_at,
|
|
215
|
+
completed_at: completed_at,
|
|
216
|
+
tenant_id: tenant_id,
|
|
217
|
+
error_class: error_class,
|
|
218
|
+
error_message: error_message
|
|
219
|
+
}
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
private
|
|
223
|
+
|
|
224
|
+
# Calculates cosine similarity between two vectors
|
|
225
|
+
#
|
|
226
|
+
# @param a [Array<Float>] First vector
|
|
227
|
+
# @param b [Array<Float>] Second vector
|
|
228
|
+
# @return [Float] Cosine similarity (-1.0 to 1.0)
|
|
229
|
+
def cosine_similarity(a, b)
|
|
230
|
+
return 0.0 if a.empty? || b.empty?
|
|
231
|
+
return 0.0 if a.size != b.size
|
|
232
|
+
|
|
233
|
+
dot_product = a.zip(b).sum { |x, y| x * y }
|
|
234
|
+
magnitude_a = Math.sqrt(a.sum { |x| x * x })
|
|
235
|
+
magnitude_b = Math.sqrt(b.sum { |x| x * x })
|
|
236
|
+
|
|
237
|
+
return 0.0 if magnitude_a.zero? || magnitude_b.zero?
|
|
238
|
+
|
|
239
|
+
dot_product / (magnitude_a * magnitude_b)
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
end
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Agents
|
|
5
|
+
# Result wrapper for image analysis operations
|
|
6
|
+
#
|
|
7
|
+
# Provides a consistent interface for accessing analysis data
|
|
8
|
+
# including captions, tags, objects, colors, and extracted text.
|
|
9
|
+
#
|
|
10
|
+
# @example Accessing analysis data
|
|
11
|
+
# result = ImageAnalyzer.call(image: "photo.jpg")
|
|
12
|
+
# result.caption # => "A sunset over mountains"
|
|
13
|
+
# result.tags # => ["sunset", "mountains", "nature"]
|
|
14
|
+
# result.objects # => [{name: "mountain", location: "center", confidence: "high"}]
|
|
15
|
+
# result.colors # => [{hex: "#FF6B35", name: "orange", percentage: 30}]
|
|
16
|
+
# result.description # => "A detailed description..."
|
|
17
|
+
# result.success? # => true
|
|
18
|
+
#
|
|
19
|
+
class ImageAnalysisResult
|
|
20
|
+
attr_reader :image, :model_id, :analysis_type,
|
|
21
|
+
:caption, :description, :tags, :objects, :colors, :text,
|
|
22
|
+
:raw_response, :started_at, :completed_at, :tenant_id, :analyzer_class,
|
|
23
|
+
:error_class, :error_message
|
|
24
|
+
|
|
25
|
+
# Initialize a new result
|
|
26
|
+
#
|
|
27
|
+
# @param image [String] The analyzed image path or URL
|
|
28
|
+
# @param model_id [String] Model used for analysis
|
|
29
|
+
# @param analysis_type [Symbol] Type of analysis performed
|
|
30
|
+
# @param caption [String, nil] Brief caption of the image
|
|
31
|
+
# @param description [String, nil] Detailed description
|
|
32
|
+
# @param tags [Array<String>] Tags/keywords for the image
|
|
33
|
+
# @param objects [Array<Hash>] Detected objects with metadata
|
|
34
|
+
# @param colors [Array<Hash>] Dominant colors with hex, name, percentage
|
|
35
|
+
# @param text [String, nil] Extracted text (OCR)
|
|
36
|
+
# @param raw_response [Hash, String, nil] Raw response from the model
|
|
37
|
+
# @param started_at [Time] When analysis started
|
|
38
|
+
# @param completed_at [Time] When analysis completed
|
|
39
|
+
# @param tenant_id [String, nil] Tenant identifier
|
|
40
|
+
# @param analyzer_class [String] Name of the analyzer class
|
|
41
|
+
# @param error_class [String, nil] Error class name if failed
|
|
42
|
+
# @param error_message [String, nil] Error message if failed
|
|
43
|
+
def initialize(image:, model_id:, analysis_type:, caption:, description:, tags:,
|
|
44
|
+
objects:, colors:, text:, raw_response:, started_at:, completed_at:,
|
|
45
|
+
tenant_id:, analyzer_class:, error_class: nil, error_message: nil)
|
|
46
|
+
@image = image
|
|
47
|
+
@model_id = model_id
|
|
48
|
+
@analysis_type = analysis_type
|
|
49
|
+
@caption = caption
|
|
50
|
+
@description = description
|
|
51
|
+
@tags = tags || []
|
|
52
|
+
@objects = objects || []
|
|
53
|
+
@colors = colors || []
|
|
54
|
+
@text = text
|
|
55
|
+
@raw_response = raw_response
|
|
56
|
+
@started_at = started_at
|
|
57
|
+
@completed_at = completed_at
|
|
58
|
+
@tenant_id = tenant_id
|
|
59
|
+
@analyzer_class = analyzer_class
|
|
60
|
+
@error_class = error_class
|
|
61
|
+
@error_message = error_message
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Status helpers
|
|
65
|
+
|
|
66
|
+
def success?
|
|
67
|
+
error_class.nil? && (caption.present? || description.present? || tags.any?)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def error?
|
|
71
|
+
!success?
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Analysis is always single
|
|
75
|
+
def single?
|
|
76
|
+
true
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def batch?
|
|
80
|
+
false
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Count (always 1 for analysis)
|
|
84
|
+
def count
|
|
85
|
+
success? ? 1 : 0
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Data access helpers
|
|
89
|
+
|
|
90
|
+
# Check if the result has a caption
|
|
91
|
+
def caption?
|
|
92
|
+
caption.present?
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Check if the result has a description
|
|
96
|
+
def description?
|
|
97
|
+
description.present?
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Check if the result has tags
|
|
101
|
+
def tags?
|
|
102
|
+
tags.any?
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Check if the result has detected objects
|
|
106
|
+
def objects?
|
|
107
|
+
objects.any?
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Check if the result has color information
|
|
111
|
+
def colors?
|
|
112
|
+
colors.any?
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Check if text was extracted
|
|
116
|
+
def text?
|
|
117
|
+
text.present?
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Get tags as symbols
|
|
121
|
+
#
|
|
122
|
+
# @return [Array<Symbol>] Tags as symbols
|
|
123
|
+
def tag_symbols
|
|
124
|
+
tags.map { |t| t.to_s.downcase.gsub(/\s+/, "_").to_sym }
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Get the dominant color (highest percentage)
|
|
128
|
+
#
|
|
129
|
+
# @return [Hash, nil] The dominant color or nil
|
|
130
|
+
def dominant_color
|
|
131
|
+
return nil unless colors?
|
|
132
|
+
|
|
133
|
+
colors.max_by { |c| c[:percentage] || 0 }
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Get objects by confidence level
|
|
137
|
+
#
|
|
138
|
+
# @param confidence [String] Confidence level ("high", "medium", "low")
|
|
139
|
+
# @return [Array<Hash>] Objects with matching confidence
|
|
140
|
+
def objects_with_confidence(confidence)
|
|
141
|
+
objects.select { |obj| obj[:confidence]&.downcase == confidence.to_s.downcase }
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Get high-confidence objects
|
|
145
|
+
#
|
|
146
|
+
# @return [Array<Hash>] High-confidence objects
|
|
147
|
+
def high_confidence_objects
|
|
148
|
+
objects_with_confidence("high")
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Check if a specific object was detected
|
|
152
|
+
#
|
|
153
|
+
# @param name [String] Object name to search for
|
|
154
|
+
# @return [Boolean] Whether the object was detected
|
|
155
|
+
def has_object?(name)
|
|
156
|
+
objects.any? { |obj| obj[:name]&.downcase&.include?(name.to_s.downcase) }
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Check if a specific tag is present
|
|
160
|
+
#
|
|
161
|
+
# @param tag [String] Tag to search for
|
|
162
|
+
# @return [Boolean] Whether the tag is present
|
|
163
|
+
def has_tag?(tag)
|
|
164
|
+
tags.any? { |t| t.downcase == tag.to_s.downcase }
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Timing
|
|
168
|
+
|
|
169
|
+
def duration_ms
|
|
170
|
+
return 0 unless started_at && completed_at
|
|
171
|
+
((completed_at - started_at) * 1000).round
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Cost estimation
|
|
175
|
+
|
|
176
|
+
def total_cost
|
|
177
|
+
return 0 if error?
|
|
178
|
+
|
|
179
|
+
# Analysis typically uses vision model pricing
|
|
180
|
+
# Estimate based on model and image size
|
|
181
|
+
ImageGenerator::Pricing.calculate_cost(
|
|
182
|
+
model_id: model_id,
|
|
183
|
+
count: 1
|
|
184
|
+
)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Serialization
|
|
188
|
+
|
|
189
|
+
def to_h
|
|
190
|
+
{
|
|
191
|
+
success: success?,
|
|
192
|
+
image: image,
|
|
193
|
+
model_id: model_id,
|
|
194
|
+
analysis_type: analysis_type,
|
|
195
|
+
caption: caption,
|
|
196
|
+
description: description,
|
|
197
|
+
tags: tags,
|
|
198
|
+
objects: objects,
|
|
199
|
+
colors: colors,
|
|
200
|
+
text: text,
|
|
201
|
+
total_cost: total_cost,
|
|
202
|
+
duration_ms: duration_ms,
|
|
203
|
+
started_at: started_at&.iso8601,
|
|
204
|
+
completed_at: completed_at&.iso8601,
|
|
205
|
+
tenant_id: tenant_id,
|
|
206
|
+
analyzer_class: analyzer_class,
|
|
207
|
+
error_class: error_class,
|
|
208
|
+
error_message: error_message
|
|
209
|
+
}
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Caching
|
|
213
|
+
|
|
214
|
+
def to_cache
|
|
215
|
+
{
|
|
216
|
+
image: image,
|
|
217
|
+
model_id: model_id,
|
|
218
|
+
analysis_type: analysis_type,
|
|
219
|
+
caption: caption,
|
|
220
|
+
description: description,
|
|
221
|
+
tags: tags,
|
|
222
|
+
objects: objects,
|
|
223
|
+
colors: colors,
|
|
224
|
+
text: text,
|
|
225
|
+
total_cost: total_cost,
|
|
226
|
+
cached_at: Time.current.iso8601
|
|
227
|
+
}
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def self.from_cache(data)
|
|
231
|
+
CachedImageAnalysisResult.new(data)
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Lightweight result for cached analyses
|
|
236
|
+
class CachedImageAnalysisResult
|
|
237
|
+
attr_reader :image, :model_id, :analysis_type,
|
|
238
|
+
:caption, :description, :tags, :objects, :colors, :text,
|
|
239
|
+
:total_cost, :cached_at
|
|
240
|
+
|
|
241
|
+
def initialize(data)
|
|
242
|
+
@image = data[:image]
|
|
243
|
+
@model_id = data[:model_id]
|
|
244
|
+
@analysis_type = data[:analysis_type]
|
|
245
|
+
@caption = data[:caption]
|
|
246
|
+
@description = data[:description]
|
|
247
|
+
@tags = data[:tags] || []
|
|
248
|
+
@objects = data[:objects] || []
|
|
249
|
+
@colors = data[:colors] || []
|
|
250
|
+
@text = data[:text]
|
|
251
|
+
@total_cost = data[:total_cost]
|
|
252
|
+
@cached_at = data[:cached_at]
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def success?
|
|
256
|
+
caption.present? || description.present? || tags.any?
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def error?
|
|
260
|
+
!success?
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def cached?
|
|
264
|
+
true
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def count
|
|
268
|
+
success? ? 1 : 0
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def single?
|
|
272
|
+
true
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def batch?
|
|
276
|
+
false
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def caption?
|
|
280
|
+
caption.present?
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def description?
|
|
284
|
+
description.present?
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def tags?
|
|
288
|
+
tags.any?
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def objects?
|
|
292
|
+
objects.any?
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def colors?
|
|
296
|
+
colors.any?
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def text?
|
|
300
|
+
text.present?
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def tag_symbols
|
|
304
|
+
tags.map { |t| t.to_s.downcase.gsub(/\s+/, "_").to_sym }
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def dominant_color
|
|
308
|
+
return nil unless colors?
|
|
309
|
+
|
|
310
|
+
colors.max_by { |c| c[:percentage] || 0 }
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
end
|