ruby_llm-agents 0.1.0

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 (51) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +898 -0
  4. data/app/channels/ruby_llm/agents/executions_channel.rb +23 -0
  5. data/app/controllers/ruby_llm/agents/agents_controller.rb +100 -0
  6. data/app/controllers/ruby_llm/agents/application_controller.rb +20 -0
  7. data/app/controllers/ruby_llm/agents/dashboard_controller.rb +34 -0
  8. data/app/controllers/ruby_llm/agents/executions_controller.rb +93 -0
  9. data/app/helpers/ruby_llm/agents/application_helper.rb +149 -0
  10. data/app/javascript/ruby_llm/agents/controllers/filter_controller.js +56 -0
  11. data/app/javascript/ruby_llm/agents/controllers/index.js +12 -0
  12. data/app/javascript/ruby_llm/agents/controllers/refresh_controller.js +83 -0
  13. data/app/models/ruby_llm/agents/execution/analytics.rb +166 -0
  14. data/app/models/ruby_llm/agents/execution/metrics.rb +89 -0
  15. data/app/models/ruby_llm/agents/execution/scopes.rb +81 -0
  16. data/app/models/ruby_llm/agents/execution.rb +81 -0
  17. data/app/services/ruby_llm/agents/agent_registry.rb +112 -0
  18. data/app/views/layouts/rubyllm/agents/application.html.erb +276 -0
  19. data/app/views/rubyllm/agents/agents/index.html.erb +89 -0
  20. data/app/views/rubyllm/agents/agents/show.html.erb +562 -0
  21. data/app/views/rubyllm/agents/dashboard/_execution_item.html.erb +48 -0
  22. data/app/views/rubyllm/agents/dashboard/index.html.erb +121 -0
  23. data/app/views/rubyllm/agents/executions/_execution.html.erb +64 -0
  24. data/app/views/rubyllm/agents/executions/_filters.html.erb +172 -0
  25. data/app/views/rubyllm/agents/executions/_list.html.erb +229 -0
  26. data/app/views/rubyllm/agents/executions/index.html.erb +83 -0
  27. data/app/views/rubyllm/agents/executions/index.turbo_stream.erb +4 -0
  28. data/app/views/rubyllm/agents/executions/show.html.erb +240 -0
  29. data/app/views/rubyllm/agents/shared/_executions_table.html.erb +193 -0
  30. data/app/views/rubyllm/agents/shared/_stat_card.html.erb +14 -0
  31. data/app/views/rubyllm/agents/shared/_status_badge.html.erb +65 -0
  32. data/app/views/rubyllm/agents/shared/_status_dot.html.erb +18 -0
  33. data/config/routes.rb +13 -0
  34. data/lib/generators/ruby_llm_agents/agent_generator.rb +79 -0
  35. data/lib/generators/ruby_llm_agents/install_generator.rb +89 -0
  36. data/lib/generators/ruby_llm_agents/templates/add_prompts_migration.rb.tt +12 -0
  37. data/lib/generators/ruby_llm_agents/templates/agent.rb.tt +46 -0
  38. data/lib/generators/ruby_llm_agents/templates/application_agent.rb.tt +22 -0
  39. data/lib/generators/ruby_llm_agents/templates/initializer.rb.tt +36 -0
  40. data/lib/generators/ruby_llm_agents/templates/migration.rb.tt +66 -0
  41. data/lib/generators/ruby_llm_agents/upgrade_generator.rb +59 -0
  42. data/lib/ruby_llm/agents/base.rb +271 -0
  43. data/lib/ruby_llm/agents/configuration.rb +36 -0
  44. data/lib/ruby_llm/agents/engine.rb +32 -0
  45. data/lib/ruby_llm/agents/execution_logger_job.rb +59 -0
  46. data/lib/ruby_llm/agents/inflections.rb +13 -0
  47. data/lib/ruby_llm/agents/instrumentation.rb +245 -0
  48. data/lib/ruby_llm/agents/version.rb +7 -0
  49. data/lib/ruby_llm/agents.rb +26 -0
  50. data/lib/ruby_llm-agents.rb +3 -0
  51. metadata +164 -0
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+
5
+ module RubyLlmAgents
6
+ # Agent generator for creating new agents
7
+ #
8
+ # Usage:
9
+ # rails generate ruby_llm_agents:agent SearchIntent query:required limit:10
10
+ #
11
+ # This will create:
12
+ # - app/agents/search_intent_agent.rb
13
+ #
14
+ # Parameter syntax:
15
+ # name - Optional parameter
16
+ # name:required - Required parameter
17
+ # name:default - Optional with default value (e.g., limit:10)
18
+ #
19
+ class AgentGenerator < ::Rails::Generators::NamedBase
20
+ source_root File.expand_path("templates", __dir__)
21
+
22
+ argument :params, type: :array, default: [], banner: "param[:required|:default] param[:required|:default]"
23
+
24
+ class_option :model, type: :string, default: "gemini-2.0-flash",
25
+ desc: "The LLM model to use"
26
+ class_option :temperature, type: :numeric, default: 0.0,
27
+ desc: "The temperature setting (0.0-1.0)"
28
+ class_option :cache, type: :string, default: nil,
29
+ desc: "Cache TTL (e.g., '1.hour', '30.minutes')"
30
+
31
+ def create_agent_file
32
+ template "agent.rb.tt", "app/agents/#{file_name}_agent.rb"
33
+ end
34
+
35
+ def show_usage
36
+ say ""
37
+ say "Agent #{class_name}Agent created!", :green
38
+ say ""
39
+ say "Usage:"
40
+ say " #{class_name}Agent.call(#{usage_params})"
41
+ say " #{class_name}Agent.call(#{usage_params}, dry_run: true)"
42
+ say ""
43
+ end
44
+
45
+ private
46
+
47
+ def parsed_params
48
+ @parsed_params ||= params.map do |param|
49
+ name, modifier = param.split(":")
50
+ ParsedParam.new(name, modifier)
51
+ end
52
+ end
53
+
54
+ def usage_params
55
+ parsed_params.map do |p|
56
+ if p.required?
57
+ "#{p.name}: value"
58
+ else
59
+ "#{p.name}: #{p.default || 'value'}"
60
+ end
61
+ end.join(", ")
62
+ end
63
+
64
+ # Helper class for parsing parameter definitions
65
+ class ParsedParam
66
+ attr_reader :name, :default
67
+
68
+ def initialize(name, modifier)
69
+ @name = name
70
+ @required = modifier == "required"
71
+ @default = @required ? nil : modifier
72
+ end
73
+
74
+ def required?
75
+ @required
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "rails/generators/active_record"
5
+
6
+ module RubyLlmAgents
7
+ # Install generator for ruby_llm-agents
8
+ #
9
+ # Usage:
10
+ # rails generate ruby_llm_agents:install
11
+ #
12
+ # This will:
13
+ # - Create the migration for ruby_llm_agents_executions table
14
+ # - Create the initializer at config/initializers/ruby_llm_agents.rb
15
+ # - Create app/agents/application_agent.rb base class
16
+ # - Optionally mount the dashboard engine in routes
17
+ #
18
+ class InstallGenerator < ::Rails::Generators::Base
19
+ include ::ActiveRecord::Generators::Migration
20
+
21
+ source_root File.expand_path("templates", __dir__)
22
+
23
+ class_option :skip_migration, type: :boolean, default: false,
24
+ desc: "Skip generating the migration file"
25
+ class_option :skip_initializer, type: :boolean, default: false,
26
+ desc: "Skip generating the initializer file"
27
+ class_option :mount_dashboard, type: :boolean, default: true,
28
+ desc: "Mount the dashboard engine in routes"
29
+
30
+ def create_migration_file
31
+ return if options[:skip_migration]
32
+
33
+ migration_template(
34
+ "migration.rb.tt",
35
+ File.join(db_migrate_path, "create_ruby_llm_agents_executions.rb")
36
+ )
37
+ end
38
+
39
+ def create_initializer
40
+ return if options[:skip_initializer]
41
+
42
+ template "initializer.rb.tt", "config/initializers/ruby_llm_agents.rb"
43
+ end
44
+
45
+ def create_agents_directory
46
+ empty_directory "app/agents"
47
+ end
48
+
49
+ def create_application_agent
50
+ template "application_agent.rb.tt", "app/agents/application_agent.rb"
51
+ end
52
+
53
+ def mount_dashboard_engine
54
+ return unless options[:mount_dashboard]
55
+
56
+ route_content = 'mount RubyLLM::Agents::Engine => "/agents"'
57
+
58
+ if File.exist?(File.join(destination_root, "config/routes.rb"))
59
+ inject_into_file(
60
+ "config/routes.rb",
61
+ " #{route_content}\n",
62
+ after: "Rails.application.routes.draw do\n"
63
+ )
64
+ say_status :route, route_content, :green
65
+ end
66
+ end
67
+
68
+ def show_post_install_message
69
+ say ""
70
+ say "RubyLLM::Agents has been installed!", :green
71
+ say ""
72
+ say "Next steps:"
73
+ say " 1. Run migrations: rails db:migrate"
74
+ say " 2. Generate an agent: rails generate ruby_llm_agents:agent MyAgent query:required"
75
+ say " 3. Access the dashboard at: /agents"
76
+ say ""
77
+ end
78
+
79
+ private
80
+
81
+ def migration_version
82
+ "[#{::ActiveRecord::VERSION::STRING.to_f}]"
83
+ end
84
+
85
+ def db_migrate_path
86
+ "db/migrate"
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddPromptsToRubyLLMAgentsExecutions < ActiveRecord::Migration<%= migration_version %>
4
+ def change
5
+ unless column_exists?(:ruby_llm_agents_executions, :system_prompt)
6
+ add_column :ruby_llm_agents_executions, :system_prompt, :text
7
+ end
8
+ unless column_exists?(:ruby_llm_agents_executions, :user_prompt)
9
+ add_column :ruby_llm_agents_executions, :user_prompt, :text
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ class <%= class_name %>Agent < ApplicationAgent
4
+ model "<%= options[:model] %>"
5
+ temperature <%= options[:temperature] %>
6
+ version "1.0"
7
+ <% if options[:cache] -%>
8
+ cache <%= options[:cache] %>
9
+ <% else -%>
10
+ # cache 1.hour # Uncomment to enable caching
11
+ <% end -%>
12
+
13
+ <% parsed_params.each do |param| -%>
14
+ param :<%= param.name %><%= ", required: true" if param.required? %><%= ", default: #{param.default.inspect}" if param.default && !param.required? %>
15
+ <% end -%>
16
+
17
+ private
18
+
19
+ def system_prompt
20
+ <<~PROMPT
21
+ You are a helpful assistant.
22
+ # Define your system instructions here
23
+ PROMPT
24
+ end
25
+
26
+ def user_prompt
27
+ # Build the prompt from parameters
28
+ <% if parsed_params.any? -%>
29
+ <%= parsed_params.first.name %>
30
+ <% else -%>
31
+ "Your prompt here"
32
+ <% end -%>
33
+ end
34
+
35
+ # Uncomment to use structured output with RubyLLM::Schema
36
+ # def schema
37
+ # @schema ||= RubyLLM::Schema.create do
38
+ # string :result, description: "The result"
39
+ # end
40
+ # end
41
+
42
+ # Uncomment to add custom metadata to execution logs
43
+ # def execution_metadata
44
+ # { custom_field: value }
45
+ # end
46
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ # ApplicationAgent - Base class for all agents in this application
4
+ #
5
+ # All agents should inherit from this class to share common configuration.
6
+ #
7
+ # Example:
8
+ # class MyAgent < ApplicationAgent
9
+ # param :query, required: true
10
+ #
11
+ # def user_prompt
12
+ # query
13
+ # end
14
+ # end
15
+ #
16
+ class ApplicationAgent < RubyLLM::Agents::Base
17
+ # Shared configuration for all agents
18
+ # Uncomment and modify as needed:
19
+ #
20
+ # model "gemini-2.0-flash"
21
+ # temperature 0.0
22
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Configuration for RubyLLM::Agents
4
+ #
5
+ # For more information, see: https://github.com/adham90/ruby_llm-agents
6
+
7
+ RubyLLM::Agents.configure do |config|
8
+ # Default model for all agents (can be overridden per agent)
9
+ # config.default_model = "gemini-2.0-flash"
10
+
11
+ # Default temperature (0.0 = deterministic, 1.0 = creative)
12
+ # config.default_temperature = 0.0
13
+
14
+ # Default timeout in seconds
15
+ # config.default_timeout = 60
16
+
17
+ # Cache store for agent response caching (defaults to Rails.cache)
18
+ # config.cache_store = Rails.cache
19
+
20
+ # Async logging (set to false to log synchronously, useful for debugging)
21
+ # config.async_logging = true
22
+
23
+ # Retention period for execution records (used by cleanup tasks)
24
+ # config.retention_period = 30.days
25
+
26
+ # Anomaly detection thresholds (executions exceeding these are logged as warnings)
27
+ # config.anomaly_cost_threshold = 5.00 # dollars
28
+ # config.anomaly_duration_threshold = 10_000 # milliseconds
29
+
30
+ # Dashboard authentication
31
+ # Return true to allow access, false to deny
32
+ # config.dashboard_auth = ->(controller) { controller.current_user&.admin? }
33
+
34
+ # Parent controller for dashboard (for authentication/layout inheritance)
35
+ # config.dashboard_parent_controller = "ApplicationController"
36
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateRubyLlmAgentsExecutions < ActiveRecord::Migration<%= migration_version %>
4
+ def change
5
+ create_table :ruby_llm_agents_executions do |t|
6
+ # Agent identification
7
+ t.string :agent_type, null: false
8
+ t.string :agent_version, default: "1.0"
9
+
10
+ # Model configuration
11
+ t.string :model_id, null: false
12
+ t.string :model_provider
13
+ t.decimal :temperature, precision: 3, scale: 2
14
+
15
+ # Timing
16
+ t.datetime :started_at, null: false
17
+ t.datetime :completed_at
18
+ t.integer :duration_ms
19
+
20
+ # Status
21
+ t.string :status, default: "success", null: false
22
+
23
+ # Token usage
24
+ t.integer :input_tokens
25
+ t.integer :output_tokens
26
+ t.integer :total_tokens
27
+ t.integer :cached_tokens, default: 0
28
+ t.integer :cache_creation_tokens, default: 0
29
+
30
+ # Costs (in dollars, 6 decimal precision)
31
+ t.decimal :input_cost, precision: 12, scale: 6
32
+ t.decimal :output_cost, precision: 12, scale: 6
33
+ t.decimal :total_cost, precision: 12, scale: 6
34
+
35
+ # Data (JSONB for PostgreSQL, JSON for others)
36
+ t.jsonb :parameters, null: false, default: {}
37
+ t.jsonb :response, default: {}
38
+ t.jsonb :metadata, null: false, default: {}
39
+
40
+ # Error tracking
41
+ t.string :error_class
42
+ t.text :error_message
43
+
44
+ # Prompts (for history/changelog)
45
+ t.text :system_prompt
46
+ t.text :user_prompt
47
+
48
+ t.timestamps
49
+ end
50
+
51
+ # Indexes for common queries
52
+ add_index :ruby_llm_agents_executions, :agent_type
53
+ add_index :ruby_llm_agents_executions, :status
54
+ add_index :ruby_llm_agents_executions, :created_at
55
+ add_index :ruby_llm_agents_executions, [:agent_type, :created_at]
56
+ add_index :ruby_llm_agents_executions, [:agent_type, :status]
57
+ add_index :ruby_llm_agents_executions, [:agent_type, :agent_version]
58
+ add_index :ruby_llm_agents_executions, :duration_ms
59
+ add_index :ruby_llm_agents_executions, :total_cost
60
+
61
+ # GIN indexes for JSONB columns (PostgreSQL only)
62
+ # Uncomment if using PostgreSQL:
63
+ # add_index :ruby_llm_agents_executions, :parameters, using: :gin
64
+ # add_index :ruby_llm_agents_executions, :metadata, using: :gin
65
+ end
66
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "rails/generators/active_record"
5
+
6
+ module RubyLlmAgents
7
+ # Upgrade generator for ruby_llm-agents
8
+ #
9
+ # Usage:
10
+ # rails generate ruby_llm_agents:upgrade
11
+ #
12
+ # This will create any missing migrations for upgrading from older versions.
13
+ #
14
+ class UpgradeGenerator < ::Rails::Generators::Base
15
+ include ::ActiveRecord::Generators::Migration
16
+
17
+ source_root File.expand_path("templates", __dir__)
18
+
19
+ def create_add_prompts_migration
20
+ # Check if columns already exist
21
+ if column_exists?(:ruby_llm_agents_executions, :system_prompt)
22
+ say_status :skip, "system_prompt column already exists", :yellow
23
+ return
24
+ end
25
+
26
+ migration_template(
27
+ "add_prompts_migration.rb.tt",
28
+ File.join(db_migrate_path, "add_prompts_to_ruby_llm_agents_executions.rb")
29
+ )
30
+ end
31
+
32
+ def show_post_upgrade_message
33
+ say ""
34
+ say "RubyLLM::Agents upgrade migration created!", :green
35
+ say ""
36
+ say "Next steps:"
37
+ say " 1. Run migrations: rails db:migrate"
38
+ say ""
39
+ end
40
+
41
+ private
42
+
43
+ def migration_version
44
+ "[#{::ActiveRecord::VERSION::STRING.to_f}]"
45
+ end
46
+
47
+ def db_migrate_path
48
+ "db/migrate"
49
+ end
50
+
51
+ def column_exists?(table, column)
52
+ return false unless ActiveRecord::Base.connection.table_exists?(table)
53
+
54
+ ActiveRecord::Base.connection.column_exists?(table, column)
55
+ rescue StandardError
56
+ false
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,271 @@
1
+ # frozen_string_literal: true
2
+
3
+ # RubyLLM::Agents::Base - Base class for LLM-powered agents
4
+ #
5
+ # == Creating an Agent
6
+ #
7
+ # class SearchAgent < ApplicationAgent
8
+ # model "gemini-2.0-flash"
9
+ # temperature 0.0
10
+ # version "1.0"
11
+ # timeout 30
12
+ # cache 1.hour
13
+ #
14
+ # param :query, required: true
15
+ # param :limit, default: 10
16
+ #
17
+ # def system_prompt
18
+ # "You are a search assistant..."
19
+ # end
20
+ #
21
+ # def user_prompt
22
+ # query
23
+ # end
24
+ #
25
+ # def schema
26
+ # @schema ||= RubyLLM::Schema.create do
27
+ # string :result
28
+ # end
29
+ # end
30
+ # end
31
+ #
32
+ # == Calling an Agent
33
+ #
34
+ # SearchAgent.call(query: "red dress")
35
+ # SearchAgent.call(query: "red dress", dry_run: true) # Debug prompts
36
+ # SearchAgent.call(query: "red dress", skip_cache: true) # Bypass cache
37
+ #
38
+ # == Configuration DSL
39
+ #
40
+ # model "gemini-2.0-flash" # LLM model (default from config)
41
+ # temperature 0.0 # Randomness 0.0-1.0 (default from config)
42
+ # version "1.0" # Version for cache invalidation
43
+ # timeout 30 # Seconds before timeout (default from config)
44
+ # cache 1.hour # Enable caching with TTL (default: disabled)
45
+ #
46
+ # == Parameter DSL
47
+ #
48
+ # param :name # Optional parameter
49
+ # param :query, required: true # Required - raises ArgumentError if missing
50
+ # param :limit, default: 10 # Optional with default value
51
+ #
52
+ # == Template Methods (override in subclasses)
53
+ #
54
+ # user_prompt - Required. The prompt sent to the LLM.
55
+ # system_prompt - Optional. System instructions for the LLM.
56
+ # schema - Optional. RubyLLM::Schema for structured output.
57
+ # process_response(response) - Optional. Post-process the LLM response.
58
+ # cache_key_data - Optional. Override to customize cache key generation.
59
+ #
60
+ module RubyLLM
61
+ module Agents
62
+ class Base
63
+ include Instrumentation
64
+
65
+ # Default constants (can be overridden by configuration)
66
+ VERSION = "1.0".freeze
67
+ CACHE_TTL = 1.hour
68
+
69
+ # ==========================================================================
70
+ # Class Methods (DSL)
71
+ # ==========================================================================
72
+
73
+ class << self
74
+ # Factory method - instantiates and calls the agent
75
+ def call(*args, **kwargs)
76
+ new(*args, **kwargs).call
77
+ end
78
+
79
+ # ------------------------------------------------------------------------
80
+ # Configuration DSL
81
+ # ------------------------------------------------------------------------
82
+
83
+ def model(value = nil)
84
+ @model = value if value
85
+ @model || inherited_or_default(:model, RubyLLM::Agents.configuration.default_model)
86
+ end
87
+
88
+ def temperature(value = nil)
89
+ @temperature = value if value
90
+ @temperature || inherited_or_default(:temperature, RubyLLM::Agents.configuration.default_temperature)
91
+ end
92
+
93
+ def version(value = nil)
94
+ @version = value if value
95
+ @version || inherited_or_default(:version, VERSION)
96
+ end
97
+
98
+ def timeout(value = nil)
99
+ @timeout = value if value
100
+ @timeout || inherited_or_default(:timeout, RubyLLM::Agents.configuration.default_timeout)
101
+ end
102
+
103
+ # ------------------------------------------------------------------------
104
+ # Parameter DSL
105
+ # ------------------------------------------------------------------------
106
+
107
+ def param(name, required: false, default: nil)
108
+ @params ||= {}
109
+ @params[name] = { required: required, default: default }
110
+ define_method(name) do
111
+ @options[name] || @options[name.to_s] || self.class.params.dig(name, :default)
112
+ end
113
+ end
114
+
115
+ def params
116
+ parent = superclass.respond_to?(:params) ? superclass.params : {}
117
+ parent.merge(@params || {})
118
+ end
119
+
120
+ # ------------------------------------------------------------------------
121
+ # Caching DSL
122
+ # ------------------------------------------------------------------------
123
+
124
+ def cache(ttl = CACHE_TTL)
125
+ @cache_enabled = true
126
+ @cache_ttl = ttl
127
+ end
128
+
129
+ def cache_enabled?
130
+ @cache_enabled || false
131
+ end
132
+
133
+ def cache_ttl
134
+ @cache_ttl || CACHE_TTL
135
+ end
136
+
137
+ private
138
+
139
+ def inherited_or_default(method, default)
140
+ superclass.respond_to?(method) ? superclass.send(method) : default
141
+ end
142
+ end
143
+
144
+ # ==========================================================================
145
+ # Instance Methods
146
+ # ==========================================================================
147
+
148
+ attr_reader :model, :temperature, :client
149
+
150
+ def initialize(model: self.class.model, temperature: self.class.temperature, **options)
151
+ @model = model
152
+ @temperature = temperature
153
+ @options = options
154
+ validate_required_params!
155
+ @client = build_client
156
+ end
157
+
158
+ # Main entry point
159
+ def call
160
+ return dry_run_response if @options[:dry_run]
161
+ return uncached_call if @options[:skip_cache] || !self.class.cache_enabled?
162
+
163
+ cache_store.fetch(cache_key, expires_in: self.class.cache_ttl) do
164
+ uncached_call
165
+ end
166
+ end
167
+
168
+ # --------------------------------------------------------------------------
169
+ # Template Methods (override in subclasses)
170
+ # --------------------------------------------------------------------------
171
+
172
+ def user_prompt
173
+ raise NotImplementedError, "#{self.class} must implement #user_prompt"
174
+ end
175
+
176
+ def system_prompt
177
+ nil
178
+ end
179
+
180
+ def schema
181
+ nil
182
+ end
183
+
184
+ def process_response(response)
185
+ content = response.content
186
+ return content unless content.is_a?(Hash)
187
+ content.transform_keys(&:to_sym)
188
+ end
189
+
190
+ # Debug mode - returns prompt info without API call
191
+ def dry_run_response
192
+ {
193
+ dry_run: true,
194
+ agent: self.class.name,
195
+ model: model,
196
+ temperature: temperature,
197
+ timeout: self.class.timeout,
198
+ system_prompt: system_prompt,
199
+ user_prompt: user_prompt,
200
+ schema: schema&.class&.name
201
+ }
202
+ end
203
+
204
+ # ==========================================================================
205
+ # Private Methods
206
+ # ==========================================================================
207
+
208
+ private
209
+
210
+ def uncached_call
211
+ instrument_execution do
212
+ Timeout.timeout(self.class.timeout) do
213
+ response = client.ask(user_prompt)
214
+ process_response(capture_response(response))
215
+ end
216
+ end
217
+ end
218
+
219
+ # --------------------------------------------------------------------------
220
+ # Caching
221
+ # --------------------------------------------------------------------------
222
+
223
+ def cache_store
224
+ RubyLLM::Agents.configuration.cache_store
225
+ end
226
+
227
+ def cache_key
228
+ ["ruby_llm_agent", self.class.name, self.class.version, cache_key_hash].join("/")
229
+ end
230
+
231
+ def cache_key_hash
232
+ Digest::SHA256.hexdigest(cache_key_data.to_json)
233
+ end
234
+
235
+ # Override to customize what's included in cache key
236
+ def cache_key_data
237
+ @options.except(:skip_cache, :dry_run)
238
+ end
239
+
240
+ # --------------------------------------------------------------------------
241
+ # Validation
242
+ # --------------------------------------------------------------------------
243
+
244
+ def validate_required_params!
245
+ required = self.class.params.select { |_, v| v[:required] }.keys
246
+ missing = required.reject { |p| @options.key?(p) || @options.key?(p.to_s) }
247
+ raise ArgumentError, "#{self.class} missing required params: #{missing.join(', ')}" if missing.any?
248
+ end
249
+
250
+ # --------------------------------------------------------------------------
251
+ # Client Building
252
+ # --------------------------------------------------------------------------
253
+
254
+ def build_client
255
+ client = RubyLLM.chat
256
+ .with_model(model)
257
+ .with_temperature(temperature)
258
+ client = client.with_instructions(system_prompt) if system_prompt
259
+ client = client.with_schema(schema) if schema
260
+ client
261
+ end
262
+
263
+ # Helper for subclasses that need conversation history
264
+ def build_client_with_messages(messages)
265
+ messages.reduce(build_client) do |client, message|
266
+ client.with_message(message[:role], message[:content])
267
+ end
268
+ end
269
+ end
270
+ end
271
+ end