rcrewai 0.2.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +21 -0
  3. data/.rubocop_todo.yml +99 -0
  4. data/CHANGELOG.md +64 -1
  5. data/README.md +170 -2
  6. data/ROADMAP.md +84 -0
  7. data/Rakefile +53 -53
  8. data/bin/rcrewai +3 -3
  9. data/docs/mcp.md +109 -0
  10. data/docs/superpowers/plans/2026-05-11-llm-modernization.md +2753 -0
  11. data/docs/superpowers/specs/2026-05-11-llm-modernization-design.md +479 -0
  12. data/docs/upgrading-to-0.3.md +163 -0
  13. data/examples/async_execution_example.rb +82 -81
  14. data/examples/hierarchical_crew_example.rb +68 -72
  15. data/examples/human_in_the_loop_example.rb +73 -74
  16. data/examples/mcp_example.rb +48 -0
  17. data/examples/native_tools_example.rb +64 -0
  18. data/examples/streaming_example.rb +56 -0
  19. data/lib/rcrewai/agent.rb +181 -286
  20. data/lib/rcrewai/async_executor.rb +43 -43
  21. data/lib/rcrewai/cli.rb +11 -11
  22. data/lib/rcrewai/configuration.rb +34 -9
  23. data/lib/rcrewai/crew.rb +134 -39
  24. data/lib/rcrewai/events.rb +30 -0
  25. data/lib/rcrewai/flow/state.rb +47 -0
  26. data/lib/rcrewai/flow/state_store.rb +50 -0
  27. data/lib/rcrewai/flow.rb +243 -0
  28. data/lib/rcrewai/human_input.rb +104 -114
  29. data/lib/rcrewai/knowledge/base.rb +52 -0
  30. data/lib/rcrewai/knowledge/chunker.rb +31 -0
  31. data/lib/rcrewai/knowledge/embedder.rb +48 -0
  32. data/lib/rcrewai/knowledge/sources.rb +83 -0
  33. data/lib/rcrewai/knowledge/store.rb +58 -0
  34. data/lib/rcrewai/knowledge.rb +13 -0
  35. data/lib/rcrewai/legacy_react_runner.rb +172 -0
  36. data/lib/rcrewai/llm_client.rb +24 -1
  37. data/lib/rcrewai/llm_clients/anthropic.rb +174 -54
  38. data/lib/rcrewai/llm_clients/azure.rb +23 -128
  39. data/lib/rcrewai/llm_clients/base.rb +11 -7
  40. data/lib/rcrewai/llm_clients/google.rb +159 -95
  41. data/lib/rcrewai/llm_clients/ollama.rb +150 -106
  42. data/lib/rcrewai/llm_clients/openai.rb +140 -63
  43. data/lib/rcrewai/mcp/client.rb +101 -0
  44. data/lib/rcrewai/mcp/tool_adapter.rb +59 -0
  45. data/lib/rcrewai/mcp/transport/http.rb +53 -0
  46. data/lib/rcrewai/mcp/transport/stdio.rb +55 -0
  47. data/lib/rcrewai/mcp.rb +8 -0
  48. data/lib/rcrewai/memory.rb +45 -37
  49. data/lib/rcrewai/output_schema.rb +79 -0
  50. data/lib/rcrewai/planning.rb +65 -0
  51. data/lib/rcrewai/pricing.rb +34 -0
  52. data/lib/rcrewai/process.rb +86 -95
  53. data/lib/rcrewai/provider_schema.rb +38 -0
  54. data/lib/rcrewai/sse_parser.rb +55 -0
  55. data/lib/rcrewai/task.rb +145 -66
  56. data/lib/rcrewai/tool_runner.rb +132 -0
  57. data/lib/rcrewai/tool_schema.rb +97 -0
  58. data/lib/rcrewai/tools/base.rb +98 -37
  59. data/lib/rcrewai/tools/code_executor.rb +71 -74
  60. data/lib/rcrewai/tools/email_sender.rb +70 -78
  61. data/lib/rcrewai/tools/file_reader.rb +38 -30
  62. data/lib/rcrewai/tools/file_writer.rb +40 -38
  63. data/lib/rcrewai/tools/pdf_processor.rb +115 -130
  64. data/lib/rcrewai/tools/sql_database.rb +58 -55
  65. data/lib/rcrewai/tools/web_search.rb +26 -25
  66. data/lib/rcrewai/version.rb +2 -2
  67. data/lib/rcrewai.rb +20 -10
  68. data/rcrewai.gemspec +39 -39
  69. metadata +77 -47
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RCrewAI
4
+ module Events
5
+ BASE_ATTRS = %i[type timestamp agent iteration].freeze
6
+
7
+ Event = Struct.new(*BASE_ATTRS, keyword_init: true)
8
+ TextDelta = Struct.new(*BASE_ATTRS, :text, keyword_init: true)
9
+ TextDone = Struct.new(*BASE_ATTRS, :text, keyword_init: true)
10
+ ToolCallStart = Struct.new(*BASE_ATTRS, :tool, :args, :call_id, keyword_init: true)
11
+ ToolCallResult = Struct.new(*BASE_ATTRS, :tool, :call_id, :result, :duration_ms, keyword_init: true)
12
+ ToolCallError = Struct.new(*BASE_ATTRS, :tool, :call_id, :error, keyword_init: true)
13
+ Thinking = Struct.new(*BASE_ATTRS, :text, keyword_init: true)
14
+ Usage = Struct.new(*BASE_ATTRS, :prompt_tokens, :completion_tokens, :total_tokens, :cost_usd, keyword_init: true)
15
+ IterationStart = Struct.new(*BASE_ATTRS, :iteration_index, keyword_init: true)
16
+ IterationEnd = Struct.new(*BASE_ATTRS, :finish_reason, keyword_init: true)
17
+ Error = Struct.new(*BASE_ATTRS, :error, keyword_init: true)
18
+
19
+ def self.fan_out(sinks)
20
+ sinks = Array(sinks).compact
21
+ lambda do |event|
22
+ sinks.each do |s|
23
+ s.call(event)
24
+ rescue StandardError => e
25
+ Kernel.warn "[rcrewai] event sink raised: #{e.class}: #{e.message}"
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module RCrewAI
6
+ class Flow
7
+ # Mutable, schemaless flow state with a stable unique id. Access attributes
8
+ # as methods (state.foo, state.foo = 1) or via [] / to_h. Mirrors CrewAI's
9
+ # unstructured (dict-based) flow state, with an automatic UUID.
10
+ class State
11
+ def initialize(attributes = {})
12
+ @attributes = {}
13
+ attributes.each { |k, v| @attributes[k.to_sym] = v }
14
+ @attributes[:id] ||= SecureRandom.uuid
15
+ end
16
+
17
+ def id
18
+ @attributes[:id]
19
+ end
20
+
21
+ def [](key)
22
+ @attributes[key.to_sym]
23
+ end
24
+
25
+ def []=(key, value)
26
+ @attributes[key.to_sym] = value
27
+ end
28
+
29
+ def to_h
30
+ @attributes.dup
31
+ end
32
+
33
+ def respond_to_missing?(_name, _include_private = false)
34
+ true
35
+ end
36
+
37
+ def method_missing(name, *args)
38
+ key = name.to_s
39
+ if key.end_with?('=')
40
+ @attributes[key[0..-2].to_sym] = args.first
41
+ else
42
+ @attributes[name]
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'fileutils'
5
+
6
+ module RCrewAI
7
+ class Flow
8
+ # Persists flow state keyed by state id, so a flow can be resumed across
9
+ # restarts. Two built-ins: in-memory (tests / single process) and file-based
10
+ # (JSON on disk). Any object with #save(id, hash) and #load(id) works.
11
+ class MemoryStateStore
12
+ def initialize
13
+ @data = {}
14
+ end
15
+
16
+ def save(id, hash)
17
+ @data[id] = hash.dup
18
+ end
19
+
20
+ def load(id)
21
+ @data[id]
22
+ end
23
+ end
24
+
25
+ # Stores each state as a JSON file named <id>.json under a directory.
26
+ class FileStateStore
27
+ def initialize(dir)
28
+ @dir = dir
29
+ FileUtils.mkdir_p(@dir)
30
+ end
31
+
32
+ def save(id, hash)
33
+ File.write(path_for(id), JSON.pretty_generate(hash))
34
+ end
35
+
36
+ def load(id)
37
+ path = path_for(id)
38
+ return nil unless File.exist?(path)
39
+
40
+ JSON.parse(File.read(path))
41
+ end
42
+
43
+ private
44
+
45
+ def path_for(id)
46
+ File.join(@dir, "#{id}.json")
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,243 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'flow/state'
4
+ require_relative 'flow/state_store'
5
+
6
+ module RCrewAI
7
+ # Event-driven workflow engine — CrewAI's second pillar, in Ruby.
8
+ #
9
+ # Subclass Flow and declare methods with the class-level DSL:
10
+ #
11
+ # class GuideFlow < RCrewAI::Flow
12
+ # start :pick_topic
13
+ # def pick_topic = state.topic = 'ruby'
14
+ #
15
+ # listen :pick_topic
16
+ # def research(prev) = "researched #{prev}"
17
+ #
18
+ # router :research
19
+ # def route(prev) = prev.include?('ruby') ? :publish : :revise
20
+ #
21
+ # listen :publish
22
+ # def publish = state.done = true
23
+ # end
24
+ #
25
+ # GuideFlow.new.kickoff
26
+ #
27
+ # Triggers combine with and_/or_. State is a schemaless object with a UUID and
28
+ # can be persisted/restored via a state store.
29
+ class Flow
30
+ # --- Trigger descriptors -------------------------------------------------
31
+ Trigger = Struct.new(:mode, :names) do
32
+ def satisfied_by?(completed)
33
+ case mode
34
+ when :single, :or then names.any? { |n| completed.include?(n) }
35
+ when :and then names.all? { |n| completed.include?(n) }
36
+ end
37
+ end
38
+ end
39
+
40
+ # --- Class-level DSL -----------------------------------------------------
41
+ class << self
42
+ def start_methods
43
+ @start_methods ||= []
44
+ end
45
+
46
+ # name => Trigger. Populated when a listen/router declaration is bound to
47
+ # the next defined method.
48
+ def listeners
49
+ @listeners ||= {}
50
+ end
51
+
52
+ def routers
53
+ @routers ||= {}
54
+ end
55
+
56
+ def start(method_name)
57
+ start_methods << method_name.to_sym
58
+ end
59
+
60
+ def listen(trigger)
61
+ @pending = [:listen, normalize_trigger(trigger)]
62
+ end
63
+
64
+ def router(trigger)
65
+ @pending = [:router, normalize_trigger(trigger)]
66
+ end
67
+
68
+ def or_(*names)
69
+ Trigger.new(:or, names.map(&:to_sym))
70
+ end
71
+
72
+ def and_(*names)
73
+ Trigger.new(:and, names.map(&:to_sym))
74
+ end
75
+
76
+ # Binds a pending listen/router declaration to the method just defined.
77
+ def method_added(method_name)
78
+ super
79
+ return unless @pending
80
+
81
+ kind, trigger = @pending
82
+ @pending = nil
83
+ case kind
84
+ when :listen then listeners[method_name.to_sym] = trigger
85
+ when :router then routers[method_name.to_sym] = trigger
86
+ end
87
+ end
88
+
89
+ # Merge inherited declarations so subclasses of a Flow subclass compose.
90
+ def inherited(subclass)
91
+ super
92
+ subclass.instance_variable_set(:@start_methods, start_methods.dup)
93
+ subclass.instance_variable_set(:@listeners, listeners.dup)
94
+ subclass.instance_variable_set(:@routers, routers.dup)
95
+ end
96
+
97
+ private
98
+
99
+ def normalize_trigger(trigger)
100
+ return trigger if trigger.is_a?(Trigger)
101
+
102
+ Trigger.new(:single, [trigger.to_sym])
103
+ end
104
+ end
105
+
106
+ # --- Instance ------------------------------------------------------------
107
+ attr_reader :state
108
+
109
+ def initialize(state_store: nil, feedback_handler: nil)
110
+ @state = State.new
111
+ @state_store = state_store
112
+ @feedback_handler = feedback_handler
113
+ end
114
+
115
+ # A pause point for human feedback. Calls the configured feedback_handler
116
+ # with the prompt and returns its response; without a handler, prompts on
117
+ # the console. Mirrors CrewAI's @human_feedback.
118
+ def human_feedback(prompt)
119
+ return @feedback_handler.call(prompt) if @feedback_handler
120
+
121
+ require_relative 'human_input'
122
+ response = HumanInput.new.request_input(prompt)
123
+ response.is_a?(Hash) ? response[:input] : response
124
+ end
125
+
126
+ # Runs the flow to completion. Optional inputs seed the state.
127
+ def kickoff(inputs: {})
128
+ inputs.each { |k, v| @state[k] = v }
129
+
130
+ @completed = [] # method names that have finished
131
+ @outputs = {} # method name => return value
132
+ @router_labels = [] # labels emitted by routers, act as pseudo-triggers
133
+
134
+ self.class.start_methods.each { |m| run_method(m) }
135
+ drain_listeners
136
+
137
+ persist
138
+ @state
139
+ end
140
+
141
+ # Restores state previously persisted under +id+.
142
+ def restore(id)
143
+ raise FlowError, 'no state store configured' unless @state_store
144
+
145
+ hash = @state_store.load(id)
146
+ raise FlowError, "no persisted state for id #{id}" unless hash
147
+
148
+ @state = State.new(symbolize(hash))
149
+ @state
150
+ end
151
+
152
+ private
153
+
154
+ def run_method(method_name)
155
+ trigger = self.class.listeners[method_name] || self.class.routers[method_name]
156
+ arg = trigger ? @outputs[last_trigger_name(trigger)] : nil
157
+
158
+ result = arity_for(method_name).zero? ? send(method_name) : send(method_name, arg)
159
+
160
+ @completed << method_name
161
+ @outputs[method_name] = result
162
+
163
+ # A router's return value becomes a label that listeners can trigger on.
164
+ @router_labels << result.to_sym if self.class.routers.key?(method_name) && result
165
+ end
166
+
167
+ # Repeatedly fire any listeners/routers whose triggers are now satisfied,
168
+ # until no new method runs (fixed point).
169
+ def drain_listeners
170
+ reactive = self.class.listeners.merge(self.class.routers)
171
+ loop do
172
+ ran = false
173
+ reactive.each do |method_name, trigger|
174
+ next if fired_enough?(method_name, trigger)
175
+
176
+ if trigger.satisfied_by?(satisfied_set)
177
+ run_listener(method_name, trigger)
178
+ ran = true
179
+ end
180
+ end
181
+ break unless ran
182
+ end
183
+ end
184
+
185
+ # For :single/:or triggers we fire once per completed trigger name; for :and
186
+ # we fire once. Track how many times each listener has fired.
187
+ def run_listener(method_name, trigger)
188
+ @fired ||= Hash.new { |h, k| h[k] = [] }
189
+
190
+ case trigger.mode
191
+ when :and
192
+ @fired[method_name] << :once
193
+ invoke_listener(method_name, @outputs[trigger.names.last])
194
+ else
195
+ pending = trigger.names.select { |n| satisfied_set.include?(n) } - @fired[method_name]
196
+ pending.each do |name|
197
+ @fired[method_name] << name
198
+ invoke_listener(method_name, @outputs[name])
199
+ end
200
+ end
201
+ end
202
+
203
+ def invoke_listener(method_name, arg)
204
+ result = arity_for(method_name).zero? ? send(method_name) : send(method_name, arg)
205
+ @completed << method_name
206
+ @outputs[method_name] = result
207
+ @router_labels << result.to_sym if self.class.routers.key?(method_name) && result
208
+ end
209
+
210
+ def fired_enough?(method_name, trigger)
211
+ @fired ||= Hash.new { |h, k| h[k] = [] }
212
+ case trigger.mode
213
+ when :and then @fired[method_name].any?
214
+ else (trigger.names & satisfied_set).all? { |n| @fired[method_name].include?(n) }
215
+ end
216
+ end
217
+
218
+ # Names available to satisfy triggers: completed methods + router labels.
219
+ def satisfied_set
220
+ @completed + @router_labels
221
+ end
222
+
223
+ def last_trigger_name(trigger)
224
+ (trigger.names & @completed).last || trigger.names.last
225
+ end
226
+
227
+ def arity_for(method_name)
228
+ method(method_name).arity
229
+ end
230
+
231
+ def persist
232
+ return unless @state_store
233
+
234
+ @state_store.save(@state.id, @state.to_h)
235
+ end
236
+
237
+ def symbolize(hash)
238
+ hash.transform_keys(&:to_sym)
239
+ end
240
+ end
241
+
242
+ class FlowError < Error; end
243
+ end