rcrewai-rails 0.2.8 → 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a983bc15d53694f94c8730ff76d92b4d2b51c5526d4ce72b3e8df78251328f16
4
- data.tar.gz: 30cf57ab32d034b63fd3ff3f94a26afe3db2d6b2db02261378a59a9dd0bb6ce5
3
+ metadata.gz: 2d2402d751d591a36e40134763d02f9c45d3b9fe3f3e38bfbcda988f08764269
4
+ data.tar.gz: d1e38e88b3effbba297ea931df56a7499912d8c2f7c7d4dd02b1b9c4e71fba1b
5
5
  SHA512:
6
- metadata.gz: e30e0d23b8dac34e84eb157cb5ac9478f3025ffb48805001e16548b0d238f93f2b631ed585484dd6aca17f126fce967ee1754edc07ab3a3c5c2655149d6ddc12
7
- data.tar.gz: dcda024dbcfec7f39e84c6e40aab2a3f8e9c1b53e9aa0a8d80f0220bd2a84be0b2a118a40296c5a96afff12b0dc9b0b829961e9643c7cd383f01603935643765
6
+ metadata.gz: bdaa9583e0b28908b9b44ae811dcb9c48e3fa5e25f929028c09e26911abc825f2867245b83a62112676c8b431cd83c7185320d43c65ee683b5e7d415bb9a6529
7
+ data.tar.gz: 425ebe18b1e13d9963887e5906f47574ade7c882347d4086ff1f9b32b9814424379f6e9193248e7febc433649ff63624008bcc9434473653e4d345be21f2729d
@@ -13,52 +13,63 @@ module RcrewAI
13
13
 
14
14
  begin
15
15
  execution.start!
16
- execution.log("info", "Starting crew execution", { crew_id: crew.id })
16
+ execution.log("info", "Starting crew execution", { crew_id: crew.id, inputs: inputs })
17
17
 
18
- # Convert Rails models to RcrewAI objects
19
18
  rcrew = crew.to_rcrew
20
-
21
- # Execute the crew with logging
22
- result = execute_with_logging(rcrew, inputs, execution)
23
-
19
+
20
+ result = rcrew.execute(stream: stream_sink_for(execution))
21
+
24
22
  execution.complete!(result)
25
- execution.log("info", "Crew execution completed successfully", { result: result })
26
-
27
- # Trigger callbacks if configured
23
+ execution.log("info", "Crew execution completed", { result: result })
24
+
28
25
  notify_completion(crew, execution, result)
29
-
26
+
30
27
  result
31
28
  rescue => e
32
29
  execution.fail!(e)
33
- execution.log("error", "Crew execution failed", {
30
+ execution.log("error", "Crew execution failed", {
34
31
  error: e.message,
35
- backtrace: e.backtrace.first(5)
32
+ backtrace: e.backtrace&.first(5)
36
33
  })
37
-
38
- # Re-raise for ActiveJob retry mechanism
34
+
39
35
  raise
40
36
  end
41
37
  end
42
38
 
43
39
  private
44
40
 
45
- def execute_with_logging(rcrew, inputs, execution)
46
- # Set up logging callbacks
47
- rcrew.before_task do |task|
48
- execution.log("info", "Starting task: #{task.description}")
49
- end
50
-
51
- rcrew.after_task do |task, output|
52
- execution.log("info", "Completed task: #{task.description}", { output: output })
41
+ # Build a stream sink that translates rcrewai events into ExecutionLog rows.
42
+ # Note: the gem currently builds the sink but does not yet thread it down
43
+ # to per-agent execution, so this is wired up for forward-compatibility.
44
+ def stream_sink_for(execution)
45
+ lambda do |event|
46
+ case event
47
+ when RCrewAI::Events::IterationStart
48
+ execution.log("debug", "Iteration #{event.iteration_index} start", { agent: event.agent })
49
+ when RCrewAI::Events::IterationEnd
50
+ execution.log("debug", "Iteration end", { agent: event.agent, finish_reason: event.finish_reason })
51
+ when RCrewAI::Events::ToolCallStart
52
+ execution.log("info", "Tool call: #{event.tool}", { args: event.args, agent: event.agent })
53
+ when RCrewAI::Events::ToolCallResult
54
+ execution.log("info", "Tool result: #{event.tool}", { duration_ms: event.duration_ms, agent: event.agent })
55
+ when RCrewAI::Events::ToolCallError
56
+ execution.log("error", "Tool error: #{event.tool}", { error: event.error, agent: event.agent })
57
+ when RCrewAI::Events::Usage
58
+ execution.log("debug", "Usage", {
59
+ prompt_tokens: event.prompt_tokens,
60
+ completion_tokens: event.completion_tokens,
61
+ total_tokens: event.total_tokens,
62
+ cost_usd: event.cost_usd,
63
+ agent: event.agent
64
+ })
65
+ when RCrewAI::Events::Error
66
+ execution.log("error", "Crew error", { error: event.error, agent: event.agent })
67
+ end
53
68
  end
54
-
55
- # Execute the crew
56
- rcrew.kickoff(inputs)
57
69
  end
58
70
 
59
71
  def notify_completion(crew, execution, result)
60
- # Send notifications if configured
61
- if crew.notification_webhook_url.present?
72
+ if crew.respond_to?(:notification_webhook_url) && crew.notification_webhook_url.present?
62
73
  NotificationJob.perform_later(
63
74
  crew.notification_webhook_url,
64
75
  {
@@ -70,7 +81,6 @@ module RcrewAI
70
81
  )
71
82
  end
72
83
 
73
- # Trigger Rails events
74
84
  ActiveSupport::Notifications.instrument("crew_execution.completed", {
75
85
  crew: crew,
76
86
  execution: execution,
@@ -79,4 +89,4 @@ module RcrewAI
79
89
  end
80
90
  end
81
91
  end
82
- end
92
+ end
@@ -17,17 +17,25 @@ module RcrewAI
17
17
  rcrew_task = task.to_rcrew_task
18
18
  rcrew_agent = agent.to_rcrew_agent
19
19
 
20
- # Execute the task
21
- result = rcrew_agent.execute_task(rcrew_task, context: inputs)
20
+ # Execute the task. Agent#execute_task returns a hash:
21
+ # { content:, tool_calls_history:, usage:, iterations:, finish_reason: }
22
+ # `inputs` is recorded by the caller and made available as task context
23
+ # via the Task#context column; the gem does not accept it as a kwarg.
24
+ result = rcrew_agent.execute_task(rcrew_task)
25
+ content = result.is_a?(Hash) ? result[:content].to_s : result.to_s
22
26
 
23
27
  execution_log[:completed_at] = Time.current
24
28
  execution_log[:status] = "completed"
25
- execution_log[:result] = result
29
+ execution_log[:result] = content
30
+ if result.is_a?(Hash)
31
+ execution_log[:usage] = result[:usage]
32
+ execution_log[:tool_calls] = result[:tool_calls_history]
33
+ execution_log[:iterations] = result[:iterations]
34
+ execution_log[:finish_reason] = result[:finish_reason]
35
+ end
26
36
 
27
37
  # Save result if configured
28
- if task.output_file.present?
29
- save_output_to_file(task.output_file, result)
30
- end
38
+ save_output_to_file(task.output_file, content) if task.output_file.present?
31
39
 
32
40
  # Log success
33
41
  ::Rails.logger.info "Task #{task.id} completed successfully by agent #{agent.id}"
@@ -17,16 +17,14 @@ module RcrewAI
17
17
 
18
18
  def to_rcrew_agent
19
19
  RCrewAI::Agent.new(
20
+ name: name,
20
21
  role: role,
21
22
  goal: goal,
22
23
  backstory: backstory,
23
- memory: memory_enabled,
24
24
  verbose: verbose,
25
25
  allow_delegation: allow_delegation,
26
26
  tools: instantiated_tools,
27
- max_iter: max_iterations,
28
- max_rpm: max_rpm,
29
- llm: llm_config
27
+ max_iterations: max_iterations
30
28
  )
31
29
  end
32
30
 
@@ -18,14 +18,9 @@ module RcrewAI
18
18
 
19
19
  def to_rcrew
20
20
  crew = RCrewAI::Crew.new(
21
- name: name,
22
- description: description,
21
+ name,
23
22
  process: process_type.to_sym,
24
- verbose: verbose,
25
- memory: memory_enabled,
26
- cache: cache_enabled,
27
- max_rpm: max_rpm,
28
- manager_llm: manager_llm
23
+ verbose: verbose
29
24
  )
30
25
 
31
26
  agents.each do |agent|
@@ -20,14 +20,12 @@ module RcrewAI
20
20
 
21
21
  def to_rcrew_task
22
22
  RCrewAI::Task.new(
23
+ name: rcrew_task_name,
23
24
  description: description,
24
25
  expected_output: expected_output,
25
26
  agent: agent&.to_rcrew_agent,
26
27
  context: context,
27
- async_execution: async_execution,
28
- output_json: output_json,
29
- output_pydantic: output_pydantic,
30
- output_file: output_file,
28
+ async: async_execution,
31
29
  tools: instantiated_tools,
32
30
  callback: callback_method
33
31
  )
@@ -54,6 +52,13 @@ module RcrewAI
54
52
 
55
53
  private
56
54
 
55
+ def rcrew_task_name
56
+ return "task_#{id}" if id
57
+ return description.to_s.parameterize.first(40).presence || "task" if description.present?
58
+
59
+ "task"
60
+ end
61
+
57
62
  def callback_method
58
63
  return nil unless callback_class.present? && callback_method_name.present?
59
64
 
@@ -0,0 +1,42 @@
1
+ require "rails/generators"
2
+
3
+ module RcrewAI
4
+ module Rails
5
+ module Generators
6
+ class CrewGenerator < ::Rails::Generators::NamedBase
7
+ source_root File.expand_path("templates", __dir__)
8
+
9
+ argument :process_type, type: :string, default: "sequential", banner: "sequential|hierarchical"
10
+
11
+ class_option :agents, type: :array, default: [], desc: "List of agents to create"
12
+ class_option :description, type: :string, desc: "Crew description"
13
+
14
+ def create_crew_file
15
+ template "crew.rb.erb", "app/crews/#{file_name}_crew.rb"
16
+ end
17
+
18
+ def create_agent_files
19
+ options[:agents].each do |agent_name|
20
+ @agent_name = agent_name
21
+ template "agent.rb.erb", "app/crews/agents/#{agent_name.underscore}_agent.rb"
22
+ end
23
+ end
24
+
25
+ def display_next_steps
26
+ say "\n✅ Created #{class_name}Crew!", :green
27
+ say "\nNext steps:", :yellow
28
+ say " 1. Configure your crew in app/crews/#{file_name}_crew.rb"
29
+ say " 2. Define tasks for your crew"
30
+ say " 3. Run your crew with: #{class_name}Crew.new.execute"
31
+ say "\n"
32
+ end
33
+
34
+ private
35
+
36
+ def crew_description
37
+ options[:description] || "#{class_name} crew for AI task orchestration"
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,45 @@
1
+ class <%= @agent_name.camelize %>Agent
2
+ include RcrewAI::Rails::AgentBuilder
3
+
4
+ def initialize
5
+ @agent = RcrewAI::Agent.new(
6
+ role: role,
7
+ goal: goal,
8
+ backstory: backstory,
9
+ memory: true,
10
+ verbose: true,
11
+ allow_delegation: false,
12
+ tools: tools,
13
+ max_iter: 25,
14
+ max_rpm: 10
15
+ )
16
+ end
17
+
18
+ def role
19
+ "<%= @agent_name.humanize %> Specialist"
20
+ end
21
+
22
+ def goal
23
+ "Perform <%= @agent_name.humanize.downcase %> tasks efficiently and accurately"
24
+ end
25
+
26
+ def backstory
27
+ <<~BACKSTORY
28
+ You are a highly skilled <%= @agent_name.humanize.downcase %> specialist with extensive experience.
29
+ Your expertise allows you to handle complex <%= @agent_name.humanize.downcase %> challenges effectively.
30
+ BACKSTORY
31
+ end
32
+
33
+ def tools
34
+ [
35
+ # Add your tools here
36
+ # RcrewAI::Tools::WebSearch.new,
37
+ # RcrewAI::Tools::FileReader.new,
38
+ # CustomTool.new
39
+ ]
40
+ end
41
+
42
+ def to_agent
43
+ @agent
44
+ end
45
+ end
@@ -0,0 +1,72 @@
1
+ class <%= class_name %>Crew
2
+ include RcrewAI::Rails::CrewBuilder
3
+
4
+ def initialize
5
+ @crew = RcrewAI::Rails::Crew.create!(
6
+ name: "<%= file_name %>",
7
+ description: "<%= crew_description %>",
8
+ process_type: "<%= process_type %>",
9
+ verbose: true,
10
+ memory_enabled: false,
11
+ cache_enabled: true
12
+ )
13
+
14
+ setup_agents
15
+ setup_tasks
16
+ end
17
+
18
+ def execute(inputs = {})
19
+ # Execute asynchronously via ActiveJob
20
+ @crew.execute_async(inputs)
21
+
22
+ # Or execute synchronously:
23
+ # @crew.execute_sync(inputs)
24
+ end
25
+
26
+ private
27
+
28
+ def setup_agents
29
+ # Define your agents here
30
+ # Example:
31
+ # @researcher = @crew.agents.create!(
32
+ # name: "researcher",
33
+ # role: "Senior Research Analyst",
34
+ # goal: "Uncover cutting-edge developments in AI and data science",
35
+ # backstory: "You are an expert researcher with years of experience",
36
+ # verbose: true,
37
+ # allow_delegation: false
38
+ # )
39
+ <% options[:agents].each do |agent_name| %>
40
+
41
+ @<%= agent_name.underscore %> = @crew.agents.create!(
42
+ name: "<%= agent_name.underscore %>",
43
+ role: "<%= agent_name.humanize %> Agent",
44
+ goal: "Perform <%= agent_name.humanize.downcase %> tasks",
45
+ backstory: "You are an expert <%= agent_name.humanize.downcase %> agent",
46
+ verbose: true,
47
+ allow_delegation: false
48
+ )
49
+ <% end %>
50
+ end
51
+
52
+ def setup_tasks
53
+ # Define your tasks here
54
+ # Example:
55
+ # @research_task = @crew.tasks.create!(
56
+ # description: "Research the latest AI trends",
57
+ # expected_output: "A comprehensive report on AI trends",
58
+ # position: 1
59
+ # )
60
+ # @research_task.agents << @researcher
61
+
62
+ # Task dependencies can be set like:
63
+ # @analysis_task.add_dependency(@research_task)
64
+ end
65
+
66
+ # Add any helper methods here
67
+
68
+ # Example callback for task completion:
69
+ # def task_completed(output)
70
+ # Rails.logger.info "Task completed with output: #{output}"
71
+ # end
72
+ end
@@ -0,0 +1,40 @@
1
+ require "rails/generators"
2
+ require "rails/generators/migration"
3
+
4
+ module RcrewAI
5
+ module Rails
6
+ module Generators
7
+ class InstallGenerator < ::Rails::Generators::Base
8
+ include ::Rails::Generators::Migration
9
+
10
+ source_root File.expand_path("templates", __dir__)
11
+
12
+ def self.next_migration_number(dirname)
13
+ Time.now.utc.strftime("%Y%m%d%H%M%S")
14
+ end
15
+
16
+ def create_migration_file
17
+ migration_template "create_rcrewai_tables.rb", "db/migrate/create_rcrewai_tables.rb", migration_class_name: "CreateRcrewaiTables"
18
+ end
19
+
20
+ def create_initializer
21
+ template "rcrewai.rb", "config/initializers/rcrewai.rb"
22
+ end
23
+
24
+ def add_routes
25
+ route "mount RcrewAI::Rails::Engine => '/rcrewai'"
26
+ end
27
+
28
+ def display_post_install_message
29
+ say "\n✅ RcrewAI Rails has been installed!", :green
30
+ say "\nNext steps:", :yellow
31
+ say " 1. Run migrations: rails db:migrate"
32
+ say " 2. Configure your settings in config/initializers/rcrewai.rb"
33
+ say " 3. Set your LLM API keys in environment variables"
34
+ say " 4. Visit /rcrewai for the web UI (if enabled)"
35
+ say "\n"
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,109 @@
1
+ class CreateRcrewaiTables < ActiveRecord::Migration[7.0]
2
+ def change
3
+ create_table :rcrewai_crews do |t|
4
+ t.string :name, null: false
5
+ t.text :description
6
+ t.string :process_type, default: "sequential"
7
+ t.boolean :verbose, default: false
8
+ t.boolean :memory_enabled, default: false
9
+ t.boolean :cache_enabled, default: false
10
+ t.integer :max_rpm
11
+ t.string :manager_llm
12
+ t.text :config
13
+ t.text :memory
14
+ t.boolean :active, default: true
15
+
16
+ t.timestamps
17
+ end
18
+
19
+ add_index :rcrewai_crews, :name
20
+ add_index :rcrewai_crews, :active
21
+
22
+ create_table :rcrewai_agents do |t|
23
+ t.references :crew, null: false, foreign_key: { to_table: :rcrewai_crews }
24
+ t.string :name, null: false
25
+ t.string :role, null: false
26
+ t.text :goal
27
+ t.text :backstory
28
+ t.boolean :memory_enabled, default: false
29
+ t.boolean :verbose, default: false
30
+ t.boolean :allow_delegation, default: false
31
+ t.text :tools
32
+ t.integer :max_iterations, default: 25
33
+ t.integer :max_rpm
34
+ t.text :llm_config
35
+ t.boolean :active, default: true
36
+
37
+ t.timestamps
38
+ end
39
+
40
+ add_index :rcrewai_agents, :name
41
+
42
+ create_table :rcrewai_tasks do |t|
43
+ t.references :crew, null: false, foreign_key: { to_table: :rcrewai_crews }
44
+ t.text :description, null: false
45
+ t.text :expected_output, null: false
46
+ t.boolean :async_execution, default: false
47
+ t.text :context
48
+ t.text :output_json
49
+ t.text :output_pydantic
50
+ t.string :output_file
51
+ t.text :tools
52
+ t.string :callback_class
53
+ t.string :callback_method_name
54
+ t.integer :position
55
+
56
+ t.timestamps
57
+ end
58
+
59
+ add_index :rcrewai_tasks, :position
60
+
61
+ create_table :rcrewai_task_assignments do |t|
62
+ t.references :task, null: false, foreign_key: { to_table: :rcrewai_tasks }
63
+ t.references :agent, null: false, foreign_key: { to_table: :rcrewai_agents }
64
+
65
+ t.timestamps
66
+ end
67
+
68
+ add_index :rcrewai_task_assignments, [:task_id, :agent_id], unique: true
69
+
70
+ create_table :rcrewai_task_dependencies do |t|
71
+ t.references :task, null: false, foreign_key: { to_table: :rcrewai_tasks }
72
+ t.references :dependency, null: false, foreign_key: { to_table: :rcrewai_tasks }
73
+
74
+ t.timestamps
75
+ end
76
+
77
+ add_index :rcrewai_task_dependencies, [:task_id, :dependency_id], unique: true
78
+
79
+ create_table :rcrewai_executions do |t|
80
+ t.references :crew, null: false, foreign_key: { to_table: :rcrewai_crews }
81
+ t.string :status, null: false
82
+ t.text :inputs
83
+ t.text :output
84
+ t.string :error_message
85
+ t.text :error_details
86
+ t.datetime :started_at
87
+ t.datetime :completed_at
88
+ t.integer :duration_seconds
89
+
90
+ t.timestamps
91
+ end
92
+
93
+ add_index :rcrewai_executions, :status
94
+ add_index :rcrewai_executions, :created_at
95
+
96
+ create_table :rcrewai_execution_logs do |t|
97
+ t.references :execution, null: false, foreign_key: { to_table: :rcrewai_executions }
98
+ t.string :level, null: false
99
+ t.text :message, null: false
100
+ t.text :details
101
+ t.datetime :timestamp, null: false
102
+
103
+ t.timestamps
104
+ end
105
+
106
+ add_index :rcrewai_execution_logs, :level
107
+ add_index :rcrewai_execution_logs, :timestamp
108
+ end
109
+ end
@@ -0,0 +1,53 @@
1
+ RcrewAI::Rails.configure do |config|
2
+ # ActiveJob queue name for background processing
3
+ # Default: "default"
4
+ config.job_queue_name = ENV.fetch("RCREWAI_QUEUE", "default")
5
+
6
+ # Enable web UI for monitoring crews
7
+ # Default: true
8
+ config.enable_web_ui = ENV.fetch("RCREWAI_WEB_UI", "true") == "true"
9
+
10
+ # Use ActiveJob for async execution
11
+ # Default: true
12
+ config.async_execution = ENV.fetch("RCREWAI_ASYNC", "true") == "true"
13
+
14
+ # Persistence backend (currently only :active_record supported)
15
+ # Default: :active_record
16
+ config.persistence_backend = :active_record
17
+
18
+ # Default LLM provider
19
+ # Options: "openai", "anthropic", "cohere", "groq", etc.
20
+ config.default_llm_provider = ENV.fetch("RCREWAI_LLM_PROVIDER", "openai")
21
+
22
+ # Default LLM model
23
+ config.default_llm_model = ENV.fetch("RCREWAI_LLM_MODEL", "gpt-4")
24
+
25
+ # Maximum retries for failed tasks
26
+ # Default: 3
27
+ config.max_retries = 3
28
+
29
+ # Timeout for crew execution (in seconds)
30
+ # Default: 300 (5 minutes)
31
+ config.timeout = 300
32
+
33
+ # Enable logging
34
+ # Default: true
35
+ config.enable_logging = true
36
+
37
+ # Log level
38
+ # Options: :debug, :info, :warn, :error
39
+ # Default: :info
40
+ config.log_level = :info
41
+ end
42
+
43
+ # Configure RcrewAI base gem if needed
44
+ RcrewAI.configure do |config|
45
+ # Set your API keys here or in environment variables
46
+ # config.openai_api_key = ENV["OPENAI_API_KEY"]
47
+ # config.anthropic_api_key = ENV["ANTHROPIC_API_KEY"]
48
+ # config.groq_api_key = ENV["GROQ_API_KEY"]
49
+
50
+ # Configure other RcrewAI settings
51
+ # config.default_model = "gpt-4"
52
+ # config.temperature = 0.7
53
+ end
@@ -1,32 +1,37 @@
1
1
  require "rails/generators"
2
+ require "rails/generators/migration"
2
3
 
3
4
  module RcrewAI
4
5
  module Rails
5
6
  module Generators
6
7
  class InstallGenerator < ::Rails::Generators::Base
8
+ include ::Rails::Generators::Migration
9
+
7
10
  source_root File.expand_path("templates", __dir__)
8
11
 
12
+ def self.next_migration_number(dirname)
13
+ Time.now.utc.strftime("%Y%m%d%H%M%S")
14
+ end
15
+
16
+ def create_migration_file
17
+ migration_template "create_rcrewai_tables.rb", "db/migrate/create_rcrewai_tables.rb", migration_class_name: "CreateRcrewaiTables"
18
+ end
19
+
9
20
  def create_initializer
10
- create_file "config/initializers/rcrewai.rb", <<~RUBY
11
- RcrewAI.configure do |config|
12
- # Configure your LLM settings
13
- # config.default_llm = :openai
14
- # config.openai_api_key = ENV['OPENAI_API_KEY']
15
-
16
- # Rails specific configuration
17
- config.job_queue_name = ENV.fetch("RCREWAI_QUEUE", "default")
18
- config.enable_web_ui = ENV.fetch("RCREWAI_WEB_UI", "true") == "true"
19
- config.async_execution = ENV.fetch("RCREWAI_ASYNC", "true") == "true"
20
- end
21
- RUBY
21
+ template "rcrewai.rb", "config/initializers/rcrewai.rb"
22
+ end
23
+
24
+ def add_routes
25
+ route "mount RcrewAI::Rails::Engine => '/rcrewai'"
22
26
  end
23
27
 
24
28
  def display_post_install_message
25
29
  say "\n✅ RcrewAI Rails has been installed!", :green
26
30
  say "\nNext steps:", :yellow
27
- say " 1. Configure your settings in config/initializers/rcrewai.rb"
28
- say " 2. Set your LLM API keys in environment variables"
29
- say " 3. Start building AI crews with RcrewAI!"
31
+ say " 1. Run migrations: rails db:migrate"
32
+ say " 2. Configure your settings in config/initializers/rcrewai.rb"
33
+ say " 3. Set your LLM API keys in environment variables"
34
+ say " 4. Visit /rcrewai for the web UI (if enabled)"
30
35
  say "\n"
31
36
  end
32
37
  end
@@ -62,7 +62,6 @@ module RcrewAI
62
62
  def initialize(attributes = {})
63
63
  @attributes = attributes
64
64
  @agent = build_agent
65
- setup_tools
66
65
  configure_agent
67
66
  end
68
67
 
@@ -78,20 +77,19 @@ module RcrewAI
78
77
 
79
78
  def build_agent
80
79
  RCrewAI::Agent.new(
80
+ name: @attributes[:name] || default_agent_name,
81
81
  role: @attributes[:role] || self.class.agent_role,
82
82
  goal: @attributes[:goal] || self.class.agent_goal,
83
83
  backstory: @attributes[:backstory] || self.class.agent_backstory,
84
- memory: @attributes[:memory] || self.class.memory_enabled,
84
+ tools: instantiate_tools,
85
85
  verbose: verbose?,
86
86
  allow_delegation: @attributes[:allow_delegation] || self.class.allow_delegation,
87
- max_iter: @attributes[:max_iterations] || self.class.max_iterations,
88
- llm: @attributes[:llm_config] || self.class.llm_config
87
+ max_iterations: @attributes[:max_iterations] || self.class.max_iterations
89
88
  )
90
89
  end
91
90
 
92
- def setup_tools
93
- tools = instantiate_tools
94
- @agent.tools = tools if tools.any?
91
+ def default_agent_name
92
+ self.class.name.to_s.split("::").last&.underscore.presence || "agent"
95
93
  end
96
94
 
97
95
  def instantiate_tools
@@ -19,11 +19,6 @@ module RcrewAI
19
19
  #{config.root}/lib
20
20
  ]
21
21
 
22
- # Tell Zeitwerk to ignore generator files - they aren't meant to be autoloaded
23
- initializer "rcrewai_rails.zeitwerk" do |app|
24
- ::Rails.autoloaders.main.ignore("#{root}/lib/generators")
25
- end
26
-
27
22
  config.generators do |g|
28
23
  g.test_framework :rspec
29
24
  g.fixture_replacement :factory_bot
@@ -31,8 +26,10 @@ module RcrewAI
31
26
  end
32
27
 
33
28
  initializer "rcrewai_rails.assets" do |app|
34
- app.config.assets.paths << root.join("app/assets/stylesheets")
35
- app.config.assets.paths << root.join("app/assets/javascripts")
29
+ if app.config.respond_to?(:assets)
30
+ app.config.assets.paths << root.join("app/assets/stylesheets")
31
+ app.config.assets.paths << root.join("app/assets/javascripts")
32
+ end
36
33
  end
37
34
 
38
35
 
@@ -2,35 +2,46 @@ module RcrewAI
2
2
  module Rails
3
3
  module Tools
4
4
  class ActionMailerTool < RCrewAI::Tools::Base
5
+ tool_name "action_mailer_send"
6
+ description "Send emails using a Rails Action Mailer. Choose the mailer class, the mailer method, and the delivery strategy."
7
+
8
+ param :mailer_method, type: :string, required: true,
9
+ description: "Name of the mailer method to invoke (e.g. 'welcome_email')."
10
+ param :mailer, type: :string, required: false,
11
+ description: "Mailer class name with or without the 'Mailer' suffix (e.g. 'User' or 'UserMailer'). Defaults to the mailer configured on the tool."
12
+ param :params, type: :object, required: false,
13
+ description: "Keyword arguments to pass to the mailer method."
14
+ param :deliver_method, type: :enum, required: false,
15
+ values: %w[deliver_now deliver_later deliver_later_at],
16
+ description: "How to send: deliver_now, deliver_later, or deliver_later_at."
17
+ param :at, type: :string, required: false,
18
+ description: "ISO8601 timestamp for deliver_later_at. Required when deliver_method is deliver_later_at."
19
+
5
20
  def initialize(mailer_class: nil, allowed_methods: [])
21
+ super()
6
22
  @mailer_class = mailer_class
7
- @allowed_methods = allowed_methods
8
- super(
9
- name: "Action Mailer Tool",
10
- description: "Send emails using Rails Action Mailer"
11
- )
23
+ @allowed_methods = allowed_methods.map(&:to_sym)
12
24
  end
13
25
 
14
- def execute(mailer_method, params = {}, deliver_method = :deliver_later)
15
- validate_mailer_method!(mailer_method)
16
-
17
- mailer = get_mailer_class(params.delete(:mailer) || @mailer_class)
18
-
19
- # Build the mail object
20
- mail = mailer.send(mailer_method, **params)
21
-
22
- # Deliver based on specified method
26
+ def execute(mailer_method:, mailer: nil, params: {}, deliver_method: "deliver_later", at: nil)
27
+ method_sym = mailer_method.to_sym
28
+ validate_mailer_method!(method_sym)
29
+
30
+ mailer_klass = resolve_mailer_class(mailer)
31
+ mailer_params = (params || {}).transform_keys(&:to_sym)
32
+ mail = mailer_klass.public_send(method_sym, **mailer_params)
33
+
23
34
  case deliver_method.to_sym
24
35
  when :deliver_now
25
36
  mail.deliver_now
26
- { status: "sent", method: mailer_method, delivered_at: Time.current }
37
+ { status: "sent", method: method_sym, delivered_at: Time.current }
27
38
  when :deliver_later
28
39
  job = mail.deliver_later
29
- { status: "queued", method: mailer_method, job_id: job.job_id }
40
+ { status: "queued", method: method_sym, job_id: job.job_id }
30
41
  when :deliver_later_at
31
- delivery_time = params.delete(:at) || 1.hour.from_now
42
+ delivery_time = at.present? ? Time.iso8601(at) : 1.hour.from_now
32
43
  job = mail.deliver_later(wait_until: delivery_time)
33
- { status: "scheduled", method: mailer_method, job_id: job.job_id, scheduled_for: delivery_time }
44
+ { status: "scheduled", method: method_sym, job_id: job.job_id, scheduled_for: delivery_time }
34
45
  else
35
46
  { error: "Unknown delivery method: #{deliver_method}" }
36
47
  end
@@ -41,15 +52,17 @@ module RcrewAI
41
52
  private
42
53
 
43
54
  def validate_mailer_method!(method)
44
- if @allowed_methods.any? && !@allowed_methods.include?(method.to_sym)
45
- raise ArgumentError, "Mailer method '#{method}' is not allowed"
46
- end
55
+ return if @allowed_methods.empty? || @allowed_methods.include?(method)
56
+
57
+ raise ArgumentError, "Mailer method '#{method}' is not allowed"
47
58
  end
48
59
 
49
- def get_mailer_class(mailer_name)
50
- return @mailer_class if @mailer_class && mailer_name.nil?
51
-
52
- mailer_name = "#{mailer_name.to_s.camelize}Mailer" unless mailer_name.to_s.end_with?("Mailer")
60
+ def resolve_mailer_class(mailer_name)
61
+ return @mailer_class if mailer_name.nil? && @mailer_class
62
+ raise ArgumentError, "No mailer specified" if mailer_name.nil?
63
+
64
+ mailer_name = mailer_name.to_s
65
+ mailer_name = "#{mailer_name.camelize}Mailer" unless mailer_name.end_with?("Mailer")
53
66
  mailer_name.constantize
54
67
  rescue NameError
55
68
  raise ArgumentError, "Mailer class '#{mailer_name}' not found"
@@ -57,4 +70,4 @@ module RcrewAI
57
70
  end
58
71
  end
59
72
  end
60
- end
73
+ end
@@ -2,40 +2,50 @@ module RcrewAI
2
2
  module Rails
3
3
  module Tools
4
4
  class ActiveRecordTool < RCrewAI::Tools::Base
5
- def initialize(model_class: nil, allowed_methods: [:find, :where, :count])
5
+ tool_name "active_record_query"
6
+ description "Query the Rails database using ActiveRecord. Supports find, find_by, where, count, pluck, exists?, first, last, and all."
7
+
8
+ param :query_type, type: :enum, required: true,
9
+ values: %w[find find_by where count pluck exists? first last all],
10
+ description: "ActiveRecord query method to invoke."
11
+ param :model, type: :string, required: false,
12
+ description: "Model class name (e.g. 'User'). Defaults to the model configured on the tool."
13
+ param :conditions, type: :object, required: false,
14
+ description: "Query conditions as a hash. For :find, include :id. For :pluck, include :field."
15
+
16
+ def initialize(model_class: nil, allowed_methods: %i[find where count])
17
+ super()
6
18
  @model_class = model_class
7
- @allowed_methods = allowed_methods
8
- super(
9
- name: "ActiveRecord Query Tool",
10
- description: "Query Rails database using ActiveRecord"
11
- )
19
+ @allowed_methods = allowed_methods.map(&:to_sym)
12
20
  end
13
21
 
14
- def execute(query_type, conditions = {})
22
+ def execute(query_type:, model: nil, conditions: {})
23
+ query_type = query_type.to_sym
15
24
  validate_query_type!(query_type)
16
-
17
- model = get_model_class(conditions.delete(:model) || @model_class)
18
-
19
- case query_type.to_sym
25
+
26
+ model_klass = resolve_model_class(model)
27
+ conds = (conditions || {}).transform_keys(&:to_sym)
28
+
29
+ case query_type
20
30
  when :find
21
- model.find(conditions[:id])
31
+ model_klass.find(conds[:id])
22
32
  when :find_by
23
- model.find_by(conditions)
33
+ model_klass.find_by(conds)
24
34
  when :where
25
- model.where(conditions)
35
+ model_klass.where(conds)
26
36
  when :count
27
- conditions.empty? ? model.count : model.where(conditions).count
37
+ conds.empty? ? model_klass.count : model_klass.where(conds).count
28
38
  when :pluck
29
- field = conditions.delete(:field)
30
- model.where(conditions).pluck(field)
39
+ field = conds.delete(:field)
40
+ model_klass.where(conds).pluck(field)
31
41
  when :exists?
32
- model.exists?(conditions)
42
+ model_klass.exists?(conds)
33
43
  when :first
34
- model.where(conditions).first
44
+ model_klass.where(conds).first
35
45
  when :last
36
- model.where(conditions).last
46
+ model_klass.where(conds).last
37
47
  when :all
38
- model.where(conditions).to_a
48
+ model_klass.where(conds).to_a
39
49
  else
40
50
  raise ArgumentError, "Unsupported query type: #{query_type}"
41
51
  end
@@ -48,20 +58,20 @@ module RcrewAI
48
58
  private
49
59
 
50
60
  def validate_query_type!(query_type)
51
- unless @allowed_methods.include?(query_type.to_sym)
52
- raise ArgumentError, "Query type '#{query_type}' is not allowed"
53
- end
61
+ return if @allowed_methods.include?(query_type)
62
+
63
+ raise ArgumentError, "Query type '#{query_type}' is not allowed"
54
64
  end
55
65
 
56
- def get_model_class(model_name)
57
- return @model_class if @model_class && model_name.nil?
58
-
59
- model_name = model_name.to_s.camelize
60
- model_name.constantize
66
+ def resolve_model_class(model_name)
67
+ return @model_class if model_name.nil? && @model_class
68
+ raise ArgumentError, "No model specified" if model_name.nil?
69
+
70
+ model_name.to_s.camelize.constantize
61
71
  rescue NameError
62
72
  raise ArgumentError, "Model class '#{model_name}' not found"
63
73
  end
64
74
  end
65
75
  end
66
76
  end
67
- end
77
+ end
@@ -2,30 +2,32 @@ module RcrewAI
2
2
  module Rails
3
3
  module Tools
4
4
  class ActiveStorageTool < RCrewAI::Tools::Base
5
- def initialize(allowed_operations: [:attach, :download, :analyze])
6
- @allowed_operations = allowed_operations
7
- super(
8
- name: "Active Storage Tool",
9
- description: "Manage file attachments using Rails Active Storage"
10
- )
5
+ tool_name "active_storage"
6
+ description "Manage Rails Active Storage attachments: attach, download, analyze, delete, or build a URL."
7
+
8
+ param :operation, type: :enum, required: true,
9
+ values: %w[attach download analyze delete url],
10
+ description: "Active Storage operation to perform."
11
+ param :params, type: :object, required: false,
12
+ description: "Operation parameters (model, id, attachment_name, blob_id, signed_id, key, file, filename, content_type, expires_in)."
13
+
14
+ def initialize(allowed_operations: %i[attach download analyze])
15
+ super()
16
+ @allowed_operations = allowed_operations.map(&:to_sym)
11
17
  end
12
18
 
13
- def execute(operation, params = {})
14
- validate_operation!(operation)
15
-
16
- case operation.to_sym
17
- when :attach
18
- attach_file(params)
19
- when :download
20
- download_file(params)
21
- when :analyze
22
- analyze_file(params)
23
- when :delete
24
- delete_file(params)
25
- when :url
26
- get_url(params)
27
- else
28
- { error: "Unknown operation: #{operation}" }
19
+ def execute(operation:, params: {})
20
+ op = operation.to_sym
21
+ validate_operation!(op)
22
+ opts = (params || {}).transform_keys(&:to_sym)
23
+
24
+ case op
25
+ when :attach then attach_file(opts)
26
+ when :download then download_file(opts)
27
+ when :analyze then analyze_file(opts)
28
+ when :delete then delete_file(opts)
29
+ when :url then get_url(opts)
30
+ else { error: "Unknown operation: #{operation}" }
29
31
  end
30
32
  rescue => e
31
33
  { error: "Active Storage operation failed", message: e.message }
@@ -34,31 +36,31 @@ module RcrewAI
34
36
  private
35
37
 
36
38
  def validate_operation!(operation)
37
- unless @allowed_operations.include?(operation.to_sym)
38
- raise ArgumentError, "Operation '#{operation}' is not allowed"
39
- end
39
+ return if @allowed_operations.include?(operation)
40
+
41
+ raise ArgumentError, "Operation '#{operation}' is not allowed"
40
42
  end
41
43
 
42
44
  def attach_file(params)
43
45
  record = find_record(params[:model], params[:id])
44
46
  attachment_name = params[:attachment_name] || :file
45
-
47
+
46
48
  file = params[:file] || params[:io]
47
49
  filename = params[:filename] || "attachment"
48
50
  content_type = params[:content_type] || "application/octet-stream"
49
-
51
+
50
52
  record.send(attachment_name).attach(
51
53
  io: file,
52
54
  filename: filename,
53
55
  content_type: content_type
54
56
  )
55
-
57
+
56
58
  { attached: true, filename: filename, record_id: record.id }
57
59
  end
58
60
 
59
61
  def download_file(params)
60
62
  blob = find_blob(params)
61
-
63
+
62
64
  {
63
65
  filename: blob.filename.to_s,
64
66
  content_type: blob.content_type,
@@ -70,7 +72,7 @@ module RcrewAI
70
72
  def analyze_file(params)
71
73
  blob = find_blob(params)
72
74
  blob.analyze unless blob.analyzed?
73
-
75
+
74
76
  {
75
77
  filename: blob.filename.to_s,
76
78
  content_type: blob.content_type,
@@ -84,14 +86,14 @@ module RcrewAI
84
86
  blob = find_blob(params)
85
87
  filename = blob.filename.to_s
86
88
  blob.purge
87
-
89
+
88
90
  { deleted: true, filename: filename }
89
91
  end
90
92
 
91
93
  def get_url(params)
92
94
  blob = find_blob(params)
93
95
  expires_in = params[:expires_in] || 5.minutes
94
-
96
+
95
97
  {
96
98
  url: ::Rails.application.routes.url_helpers.rails_blob_url(blob, expires_in: expires_in),
97
99
  expires_at: expires_in.from_now
@@ -119,4 +121,4 @@ module RcrewAI
119
121
  end
120
122
  end
121
123
  end
122
- end
124
+ end
@@ -2,29 +2,31 @@ module RcrewAI
2
2
  module Rails
3
3
  module Tools
4
4
  class RailsCacheTool < RCrewAI::Tools::Base
5
- def initialize
6
- super(
7
- name: "Rails Cache Tool",
8
- description: "Read and write to Rails cache"
9
- )
10
- end
5
+ tool_name "rails_cache"
6
+ description "Read from and write to the Rails cache. Supports read, write, delete, exist?, fetch, and clear."
7
+
8
+ param :action, type: :enum, required: true,
9
+ values: %w[read write delete exist? fetch clear],
10
+ description: "Cache action to perform."
11
+ param :key, type: :string, required: false,
12
+ description: "Cache key. Required for every action except clear."
13
+ param :value, type: :string, required: false,
14
+ description: "Value to store (used by write and fetch fallback)."
15
+ param :options, type: :object, required: false,
16
+ description: "Cache options hash (e.g. expires_in)."
11
17
 
12
- def execute(action, key, value = nil, options = {})
13
- case action.to_sym
14
- when :read
15
- read_cache(key)
16
- when :write
17
- write_cache(key, value, options)
18
- when :delete
19
- delete_cache(key)
20
- when :exist?
21
- cache_exists?(key)
22
- when :fetch
23
- fetch_cache(key, options) { value }
24
- when :clear
25
- clear_cache
26
- else
27
- { error: "Unknown action: #{action}" }
18
+ def execute(action:, key: nil, value: nil, options: {})
19
+ act = action.to_sym
20
+ opts = (options || {}).transform_keys(&:to_sym)
21
+
22
+ case act
23
+ when :read then read_cache(key)
24
+ when :write then write_cache(key, value, opts)
25
+ when :delete then delete_cache(key)
26
+ when :exist? then cache_exists?(key)
27
+ when :fetch then fetch_cache(key, value, opts)
28
+ when :clear then clear_cache
29
+ else { error: "Unknown action: #{action}" }
28
30
  end
29
31
  rescue => e
30
32
  { error: "Cache operation failed", message: e.message }
@@ -37,7 +39,7 @@ module RcrewAI
37
39
  { key: key, value: value, exists: !value.nil? }
38
40
  end
39
41
 
40
- def write_cache(key, value, options = {})
42
+ def write_cache(key, value, options)
41
43
  success = ::Rails.cache.write(key, value, options)
42
44
  { key: key, written: success }
43
45
  end
@@ -48,14 +50,11 @@ module RcrewAI
48
50
  end
49
51
 
50
52
  def cache_exists?(key)
51
- exists = ::Rails.cache.exist?(key)
52
- { key: key, exists: exists }
53
+ { key: key, exists: ::Rails.cache.exist?(key) }
53
54
  end
54
55
 
55
- def fetch_cache(key, options = {})
56
- value = ::Rails.cache.fetch(key, options) do
57
- yield if block_given?
58
- end
56
+ def fetch_cache(key, fallback, options)
57
+ value = ::Rails.cache.fetch(key, options) { fallback }
59
58
  { key: key, value: value }
60
59
  end
61
60
 
@@ -66,4 +65,4 @@ module RcrewAI
66
65
  end
67
66
  end
68
67
  end
69
- end
68
+ end
@@ -2,40 +2,41 @@ module RcrewAI
2
2
  module Rails
3
3
  module Tools
4
4
  class RailsLoggerTool < RCrewAI::Tools::Base
5
+ tool_name "rails_logger"
6
+ description "Log a message to the Rails logger at the given level and emit an ActiveSupport notification."
7
+
8
+ param :level, type: :enum, required: true,
9
+ values: %w[debug info warn error fatal],
10
+ description: "Log level."
11
+ param :message, type: :string, required: true,
12
+ description: "Message to log."
13
+ param :metadata, type: :object, required: false,
14
+ description: "Additional metadata appended as JSON to the log line."
15
+
5
16
  def initialize(tag: "RcrewAI")
17
+ super()
6
18
  @tag = tag
7
- super(
8
- name: "Rails Logger Tool",
9
- description: "Log messages to Rails logger"
10
- )
11
19
  end
12
20
 
13
- def execute(level, message, metadata = {})
21
+ def execute(level:, message:, metadata: {})
14
22
  logger = ::Rails.logger
15
-
16
- # Format the message with metadata
17
- formatted_message = format_message(message, metadata)
18
-
23
+ meta = metadata || {}
24
+ formatted_message = format_message(message, meta)
25
+
19
26
  case level.to_sym
20
- when :debug
21
- logger.debug(formatted_message)
22
- when :info
23
- logger.info(formatted_message)
24
- when :warn
25
- logger.warn(formatted_message)
26
- when :error
27
- logger.error(formatted_message)
28
- when :fatal
29
- logger.fatal(formatted_message)
27
+ when :debug then logger.debug(formatted_message)
28
+ when :info then logger.info(formatted_message)
29
+ when :warn then logger.warn(formatted_message)
30
+ when :error then logger.error(formatted_message)
31
+ when :fatal then logger.fatal(formatted_message)
30
32
  else
31
33
  return { error: "Unknown log level: #{level}" }
32
34
  end
33
35
 
34
- # Also instrument for monitoring
35
36
  ActiveSupport::Notifications.instrument("log.rcrewai", {
36
37
  level: level,
37
38
  message: message,
38
- metadata: metadata,
39
+ metadata: meta,
39
40
  tag: @tag
40
41
  })
41
42
 
@@ -48,10 +49,10 @@ module RcrewAI
48
49
 
49
50
  def format_message(message, metadata)
50
51
  return "[#{@tag}] #{message}" if metadata.empty?
51
-
52
+
52
53
  "[#{@tag}] #{message} | #{metadata.to_json}"
53
54
  end
54
55
  end
55
56
  end
56
57
  end
57
- end
58
+ end
@@ -1,8 +1,5 @@
1
1
  module RcrewAI
2
2
  module Rails
3
- VERSION = "0.2.8"
4
-
5
- # Also define Version for Zeitwerk compatibility
6
- Version = VERSION
3
+ VERSION = "0.3.0"
7
4
  end
8
5
  end
@@ -45,7 +45,7 @@ Gem::Specification.new do |spec|
45
45
  spec.require_paths = ["lib"]
46
46
 
47
47
  # Core dependency
48
- spec.add_dependency "rcrewai", "~> 0.2"
48
+ spec.add_dependency "rcrewai", "~> 0.3"
49
49
 
50
50
  # Rails dependencies
51
51
  spec.add_dependency "rails", ">= 7.0.0"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rcrewai-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.8
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - gkosmo
@@ -15,14 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - "~>"
17
17
  - !ruby/object:Gem::Version
18
- version: '0.2'
18
+ version: '0.3'
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
- version: '0.2'
25
+ version: '0.3'
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: rails
28
28
  requirement: !ruby/object:Gem::Requirement
@@ -238,6 +238,12 @@ files:
238
238
  - app/views/rcrewai/rails/tools/show.html.erb
239
239
  - config/routes.rb
240
240
  - db/migrate/001_add_agent_to_tasks.rb
241
+ - lib/generators/rcrew_a_i/rails/crew/crew_generator.rb
242
+ - lib/generators/rcrew_a_i/rails/crew/templates/agent.rb.erb
243
+ - lib/generators/rcrew_a_i/rails/crew/templates/crew.rb.erb
244
+ - lib/generators/rcrew_a_i/rails/install/install_generator.rb
245
+ - lib/generators/rcrew_a_i/rails/install/templates/create_rcrewai_tables.rb
246
+ - lib/generators/rcrew_a_i/rails/install/templates/rcrewai.rb
241
247
  - lib/generators/rcrewai/rails/crew/crew_generator.rb
242
248
  - lib/generators/rcrewai/rails/crew/templates/agent.rb.erb
243
249
  - lib/generators/rcrewai/rails/crew/templates/crew.rb.erb