rcrewai-rails 0.2.7 → 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 +4 -4
- data/app/jobs/rcrewai/rails/crew_execution_job.rb +39 -29
- data/app/jobs/rcrewai/rails/task_execution_job.rb +14 -6
- data/app/models/rcrewai/rails/agent.rb +2 -4
- data/app/models/rcrewai/rails/crew.rb +2 -7
- data/app/models/rcrewai/rails/task.rb +9 -4
- data/lib/rcrewai/rails/agent_builder.rb +5 -7
- data/lib/rcrewai/rails/engine.rb +4 -2
- data/lib/rcrewai/rails/tools/action_mailer_tool.rb +39 -26
- data/lib/rcrewai/rails/tools/active_record_tool.rb +40 -30
- data/lib/rcrewai/rails/tools/active_storage_tool.rb +35 -33
- data/lib/rcrewai/rails/tools/rails_cache_tool.rb +29 -30
- data/lib/rcrewai/rails/tools/rails_logger_tool.rb +24 -23
- data/lib/rcrewai/rails/version.rb +1 -4
- data/rcrewai-rails.gemspec +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2d2402d751d591a36e40134763d02f9c45d3b9fe3f3e38bfbcda988f08764269
|
|
4
|
+
data.tar.gz: d1e38e88b3effbba297ea931df56a7499912d8c2f7c7d4dd02b1b9c4e71fba1b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
|
|
20
|
+
result = rcrew.execute(stream: stream_sink_for(execution))
|
|
21
|
+
|
|
24
22
|
execution.complete!(result)
|
|
25
|
-
execution.log("info", "Crew execution completed
|
|
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
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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] =
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
|
@@ -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
|
-
|
|
84
|
+
tools: instantiate_tools,
|
|
85
85
|
verbose: verbose?,
|
|
86
86
|
allow_delegation: @attributes[:allow_delegation] || self.class.allow_delegation,
|
|
87
|
-
|
|
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
|
|
93
|
-
|
|
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
|
data/lib/rcrewai/rails/engine.rb
CHANGED
|
@@ -26,8 +26,10 @@ module RcrewAI
|
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
initializer "rcrewai_rails.assets" do |app|
|
|
29
|
-
app.config.
|
|
30
|
-
|
|
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
|
|
31
33
|
end
|
|
32
34
|
|
|
33
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
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
mail =
|
|
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:
|
|
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:
|
|
40
|
+
{ status: "queued", method: method_sym, job_id: job.job_id }
|
|
30
41
|
when :deliver_later_at
|
|
31
|
-
delivery_time =
|
|
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:
|
|
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.
|
|
45
|
-
|
|
46
|
-
|
|
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
|
|
50
|
-
return @mailer_class if
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
31
|
+
model_klass.find(conds[:id])
|
|
22
32
|
when :find_by
|
|
23
|
-
|
|
33
|
+
model_klass.find_by(conds)
|
|
24
34
|
when :where
|
|
25
|
-
|
|
35
|
+
model_klass.where(conds)
|
|
26
36
|
when :count
|
|
27
|
-
|
|
37
|
+
conds.empty? ? model_klass.count : model_klass.where(conds).count
|
|
28
38
|
when :pluck
|
|
29
|
-
field =
|
|
30
|
-
|
|
39
|
+
field = conds.delete(:field)
|
|
40
|
+
model_klass.where(conds).pluck(field)
|
|
31
41
|
when :exists?
|
|
32
|
-
|
|
42
|
+
model_klass.exists?(conds)
|
|
33
43
|
when :first
|
|
34
|
-
|
|
44
|
+
model_klass.where(conds).first
|
|
35
45
|
when :last
|
|
36
|
-
|
|
46
|
+
model_klass.where(conds).last
|
|
37
47
|
when :all
|
|
38
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
|
57
|
-
return @model_class if
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
when :
|
|
20
|
-
|
|
21
|
-
when :analyze
|
|
22
|
-
|
|
23
|
-
when :
|
|
24
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
when :
|
|
19
|
-
|
|
20
|
-
when :exist?
|
|
21
|
-
|
|
22
|
-
when :
|
|
23
|
-
|
|
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
|
|
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)
|
|
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
|
|
21
|
+
def execute(level:, message:, metadata: {})
|
|
14
22
|
logger = ::Rails.logger
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
23
|
+
meta = metadata || {}
|
|
24
|
+
formatted_message = format_message(message, meta)
|
|
25
|
+
|
|
19
26
|
case level.to_sym
|
|
20
|
-
when :debug
|
|
21
|
-
|
|
22
|
-
when :
|
|
23
|
-
|
|
24
|
-
when :
|
|
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:
|
|
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
|
data/rcrewai-rails.gemspec
CHANGED
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.
|
|
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.
|
|
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.
|
|
25
|
+
version: '0.3'
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: rails
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|