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.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +898 -0
- data/app/channels/ruby_llm/agents/executions_channel.rb +23 -0
- data/app/controllers/ruby_llm/agents/agents_controller.rb +100 -0
- data/app/controllers/ruby_llm/agents/application_controller.rb +20 -0
- data/app/controllers/ruby_llm/agents/dashboard_controller.rb +34 -0
- data/app/controllers/ruby_llm/agents/executions_controller.rb +93 -0
- data/app/helpers/ruby_llm/agents/application_helper.rb +149 -0
- data/app/javascript/ruby_llm/agents/controllers/filter_controller.js +56 -0
- data/app/javascript/ruby_llm/agents/controllers/index.js +12 -0
- data/app/javascript/ruby_llm/agents/controllers/refresh_controller.js +83 -0
- data/app/models/ruby_llm/agents/execution/analytics.rb +166 -0
- data/app/models/ruby_llm/agents/execution/metrics.rb +89 -0
- data/app/models/ruby_llm/agents/execution/scopes.rb +81 -0
- data/app/models/ruby_llm/agents/execution.rb +81 -0
- data/app/services/ruby_llm/agents/agent_registry.rb +112 -0
- data/app/views/layouts/rubyllm/agents/application.html.erb +276 -0
- data/app/views/rubyllm/agents/agents/index.html.erb +89 -0
- data/app/views/rubyllm/agents/agents/show.html.erb +562 -0
- data/app/views/rubyllm/agents/dashboard/_execution_item.html.erb +48 -0
- data/app/views/rubyllm/agents/dashboard/index.html.erb +121 -0
- data/app/views/rubyllm/agents/executions/_execution.html.erb +64 -0
- data/app/views/rubyllm/agents/executions/_filters.html.erb +172 -0
- data/app/views/rubyllm/agents/executions/_list.html.erb +229 -0
- data/app/views/rubyllm/agents/executions/index.html.erb +83 -0
- data/app/views/rubyllm/agents/executions/index.turbo_stream.erb +4 -0
- data/app/views/rubyllm/agents/executions/show.html.erb +240 -0
- data/app/views/rubyllm/agents/shared/_executions_table.html.erb +193 -0
- data/app/views/rubyllm/agents/shared/_stat_card.html.erb +14 -0
- data/app/views/rubyllm/agents/shared/_status_badge.html.erb +65 -0
- data/app/views/rubyllm/agents/shared/_status_dot.html.erb +18 -0
- data/config/routes.rb +13 -0
- data/lib/generators/ruby_llm_agents/agent_generator.rb +79 -0
- data/lib/generators/ruby_llm_agents/install_generator.rb +89 -0
- data/lib/generators/ruby_llm_agents/templates/add_prompts_migration.rb.tt +12 -0
- data/lib/generators/ruby_llm_agents/templates/agent.rb.tt +46 -0
- data/lib/generators/ruby_llm_agents/templates/application_agent.rb.tt +22 -0
- data/lib/generators/ruby_llm_agents/templates/initializer.rb.tt +36 -0
- data/lib/generators/ruby_llm_agents/templates/migration.rb.tt +66 -0
- data/lib/generators/ruby_llm_agents/upgrade_generator.rb +59 -0
- data/lib/ruby_llm/agents/base.rb +271 -0
- data/lib/ruby_llm/agents/configuration.rb +36 -0
- data/lib/ruby_llm/agents/engine.rb +32 -0
- data/lib/ruby_llm/agents/execution_logger_job.rb +59 -0
- data/lib/ruby_llm/agents/inflections.rb +13 -0
- data/lib/ruby_llm/agents/instrumentation.rb +245 -0
- data/lib/ruby_llm/agents/version.rb +7 -0
- data/lib/ruby_llm/agents.rb +26 -0
- data/lib/ruby_llm-agents.rb +3 -0
- 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
|