rcrewai 0.3.0 → 0.5.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.
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RCrewAI
4
+ module Knowledge
5
+ # A knowledge source yields plain text via #read. Concrete sources load from
6
+ # strings, files, PDFs, CSVs, or URLs.
7
+ class Source
8
+ def read
9
+ raise NotImplementedError, 'Subclasses must implement #read'
10
+ end
11
+ end
12
+
13
+ class StringSource < Source
14
+ def initialize(text)
15
+ super()
16
+ @text = text.to_s
17
+ end
18
+
19
+ def read
20
+ @text
21
+ end
22
+ end
23
+
24
+ class FileSource < Source
25
+ def initialize(path)
26
+ super()
27
+ @path = path
28
+ end
29
+
30
+ def read
31
+ File.read(@path)
32
+ end
33
+ end
34
+
35
+ class PdfSource < Source
36
+ def initialize(path)
37
+ super()
38
+ @path = path
39
+ end
40
+
41
+ def read
42
+ require 'pdf-reader'
43
+ reader = PDF::Reader.new(@path)
44
+ reader.pages.map(&:text).join("\n")
45
+ end
46
+ end
47
+
48
+ class CsvSource < Source
49
+ def initialize(path)
50
+ super()
51
+ @path = path
52
+ end
53
+
54
+ def read
55
+ require 'csv'
56
+ CSV.read(@path).map { |row| row.join(', ') }.join("\n")
57
+ end
58
+ end
59
+
60
+ class UrlSource < Source
61
+ def initialize(url, fetcher: nil)
62
+ super()
63
+ @url = url
64
+ @fetcher = fetcher
65
+ end
66
+
67
+ def read
68
+ html = @fetcher ? @fetcher.call(@url) : fetch(@url)
69
+ require 'nokogiri'
70
+ doc = Nokogiri::HTML(html)
71
+ doc.search('script, style').remove
72
+ doc.text.gsub(/\s+/, ' ').strip
73
+ end
74
+
75
+ private
76
+
77
+ def fetch(url)
78
+ require 'faraday'
79
+ Faraday.get(url).body
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RCrewAI
4
+ module Knowledge
5
+ # In-memory vector store with cosine-similarity search. The default backing
6
+ # store for Knowledge — no external service required. The interface
7
+ # (#add, #search) is intentionally small so a Chroma/Qdrant-backed store can
8
+ # be swapped in later.
9
+ class Store
10
+ Entry = Struct.new(:text, :vector)
11
+
12
+ def initialize
13
+ @entries = []
14
+ end
15
+
16
+ def add(text, vector)
17
+ @entries << Entry.new(text, vector)
18
+ end
19
+
20
+ # Returns the texts of the top-k entries most similar to +query_vector+.
21
+ def search(query_vector, k: 3)
22
+ return [] if @entries.empty?
23
+
24
+ @entries
25
+ .map { |e| [e.text, cosine_similarity(query_vector, e.vector)] }
26
+ .sort_by { |(_text, score)| -score }
27
+ .first(k)
28
+ .map(&:first)
29
+ end
30
+
31
+ def size
32
+ @entries.length
33
+ end
34
+
35
+ def empty?
36
+ @entries.empty?
37
+ end
38
+
39
+ private
40
+
41
+ def cosine_similarity(a, b)
42
+ dot = 0.0
43
+ norm_a = 0.0
44
+ norm_b = 0.0
45
+ a.each_index do |i|
46
+ ai = a[i].to_f
47
+ bi = (b[i] || 0).to_f
48
+ dot += ai * bi
49
+ norm_a += ai * ai
50
+ norm_b += bi * bi
51
+ end
52
+ return 0.0 if norm_a.zero? || norm_b.zero?
53
+
54
+ dot / (Math.sqrt(norm_a) * Math.sqrt(norm_b))
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'knowledge/chunker'
4
+ require_relative 'knowledge/store'
5
+ require_relative 'knowledge/sources'
6
+ require_relative 'knowledge/embedder'
7
+ require_relative 'knowledge/base'
8
+
9
+ module RCrewAI
10
+ # Retrieval-augmented knowledge for agents and crews. See Knowledge::Base.
11
+ module Knowledge
12
+ end
13
+ end
@@ -30,7 +30,7 @@ module RCrewAI
30
30
  iter += 1
31
31
  emit(Events::IterationStart, iteration: iter, iteration_index: iter)
32
32
 
33
- response = @llm.chat(messages: msgs)
33
+ response = @llm.chat(messages: fit_context(msgs))
34
34
  accumulate_usage(total_usage, response[:usage])
35
35
  reasoning = response[:content] || ''
36
36
  last_reasoning = reasoning
@@ -60,6 +60,12 @@ module RCrewAI
60
60
 
61
61
  private
62
62
 
63
+ # Trims the message list to the model's context window when the agent
64
+ # supports it; a no-op otherwise.
65
+ def fit_context(messages)
66
+ @agent.respond_to?(:fit_context) ? @agent.fit_context(messages) : messages
67
+ end
68
+
63
69
  def parse_and_execute_actions(reasoning, iter)
64
70
  results = []
65
71
  iteration_history = []
@@ -28,6 +28,29 @@ module RCrewAI
28
28
  end
29
29
  end
30
30
 
31
+ # Resolves a per-agent / per-pass LLM spec into a client.
32
+ # nil -> global provider
33
+ # Symbol/String -> that provider, global model
34
+ # Hash -> { provider:, model:, api_key:, temperature: } overrides
35
+ # client object -> returned as-is (anything responding to #chat)
36
+ def self.resolve(spec, config = RCrewAI.configuration)
37
+ case spec
38
+ when nil
39
+ for_provider(nil, config)
40
+ when Symbol, String
41
+ overridden = config.with_overrides(provider: spec)
42
+ for_provider(overridden.llm_provider, overridden)
43
+ when Hash
44
+ overridden = config.with_overrides(**spec)
45
+ for_provider(overridden.llm_provider, overridden)
46
+ else
47
+ return spec if spec.respond_to?(:chat)
48
+
49
+ raise ConfigurationError,
50
+ "Invalid llm: expected a provider symbol, an options hash, or a client responding to #chat, got #{spec.class}"
51
+ end
52
+ end
53
+
31
54
  def self.chat(messages:, **options)
32
55
  client = for_provider
33
56
  client.chat(messages: messages, **options)
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'base64'
4
+
5
+ module RCrewAI
6
+ # Builds multimodal message content (text + images) in the OpenAI
7
+ # chat-completions format:
8
+ # [{ type: 'text', text: '...' },
9
+ # { type: 'image_url', image_url: { url: '...' } }]
10
+ #
11
+ # Local image paths are base64-encoded into data URLs; URLs pass through.
12
+ # Only OpenAI-style multimodal is supported today; other providers raise.
13
+ module Multimodal
14
+ SUPPORTED_PROVIDERS = %i[openai azure].freeze
15
+
16
+ MIME_TYPES = {
17
+ '.png' => 'image/png',
18
+ '.jpg' => 'image/jpeg',
19
+ '.jpeg' => 'image/jpeg',
20
+ '.gif' => 'image/gif',
21
+ '.webp' => 'image/webp'
22
+ }.freeze
23
+
24
+ module_function
25
+
26
+ # Returns an OpenAI-style content-parts array for the given text and
27
+ # attachments. With no attachments this is a single text part.
28
+ def content_parts(text, attachments)
29
+ parts = [{ type: 'text', text: text.to_s }]
30
+ Array(attachments).each { |att| parts << image_part(att) }
31
+ parts
32
+ end
33
+
34
+ def supported_provider?(provider)
35
+ SUPPORTED_PROVIDERS.include?(provider.to_sym)
36
+ end
37
+
38
+ def ensure_supported_provider!(provider)
39
+ return if supported_provider?(provider)
40
+
41
+ raise UnsupportedProviderError,
42
+ "multimodal attachments are not supported for provider #{provider}"
43
+ end
44
+
45
+ def image_part(attachment)
46
+ type = attachment[:type] || attachment['type']
47
+ raise UnsupportedAttachmentError, "unsupported attachment type: #{type.inspect}" unless type.to_sym == :image
48
+
49
+ url = attachment[:url] || attachment['url']
50
+ path = attachment[:path] || attachment['path']
51
+ resolved = url || data_url_for(path)
52
+
53
+ { type: 'image_url', image_url: { url: resolved } }
54
+ end
55
+
56
+ def data_url_for(path)
57
+ raise UnsupportedAttachmentError, 'image attachment needs a :url or :path' unless path
58
+
59
+ mime = MIME_TYPES[File.extname(path).downcase] || 'application/octet-stream'
60
+ encoded = Base64.strict_encode64(File.binread(path))
61
+ "data:#{mime};base64,#{encoded}"
62
+ end
63
+
64
+ class UnsupportedAttachmentError < RCrewAI::Error; end
65
+ class UnsupportedProviderError < RCrewAI::Error; end
66
+ end
67
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module RCrewAI
6
+ # Validates and coerces a task's raw string output against a JSON-Schema
7
+ # subset (object / type / required / property types). Used by Task for the
8
+ # `output_schema:` option. Kept intentionally small: it covers the shapes an
9
+ # LLM is realistically asked to emit, not the whole JSON Schema spec.
10
+ module OutputSchema
11
+ module_function
12
+
13
+ # Returns the validated/coerced object.
14
+ # Raises OutputSchemaError if the string can't be parsed or doesn't conform.
15
+ def coerce(raw, schema)
16
+ data = parse(raw)
17
+ validate!(data, schema)
18
+ data
19
+ end
20
+
21
+ # Extracts a JSON document from a string that may contain surrounding prose,
22
+ # then parses it. Prefers a fenced ```json block, then the first balanced
23
+ # object/array, then the whole string.
24
+ def parse(raw)
25
+ candidate = extract_json(raw.to_s)
26
+ JSON.parse(candidate)
27
+ rescue JSON::ParserError => e
28
+ raise OutputSchemaError, "output is not valid JSON: #{e.message}"
29
+ end
30
+
31
+ def extract_json(text)
32
+ if (fenced = text[/```(?:json)?\s*(\{.*?\}|\[.*?\])\s*```/m, 1])
33
+ return fenced
34
+ end
35
+
36
+ first = text.index(/[{\[]/)
37
+ last = text.rindex(/[}\]]/)
38
+ return text if first.nil? || last.nil? || last < first
39
+
40
+ text[first..last]
41
+ end
42
+
43
+ def validate!(data, schema)
44
+ type = (schema[:type] || schema['type'])&.to_s
45
+ case type
46
+ when 'object' then validate_object!(data, schema)
47
+ when 'array' then raise_unless(data.is_a?(Array), 'expected an array')
48
+ when 'string' then raise_unless(data.is_a?(String), 'expected a string')
49
+ when 'integer' then raise_unless(data.is_a?(Integer), 'expected an integer')
50
+ when 'number' then raise_unless(data.is_a?(Numeric), 'expected a number')
51
+ when 'boolean' then raise_unless([true, false].include?(data), 'expected a boolean')
52
+ end
53
+ data
54
+ end
55
+
56
+ def validate_object!(data, schema)
57
+ raise_unless(data.is_a?(Hash), 'expected a JSON object')
58
+
59
+ required = schema[:required] || schema['required'] || []
60
+ required.each do |key|
61
+ raise_unless(data.key?(key.to_s), "missing required property '#{key}'")
62
+ end
63
+
64
+ props = schema[:properties] || schema['properties'] || {}
65
+ props.each do |name, subschema|
66
+ value = data[name.to_s]
67
+ next if value.nil?
68
+
69
+ validate!(value, subschema)
70
+ end
71
+ end
72
+
73
+ def raise_unless(condition, message)
74
+ raise OutputSchemaError, message unless condition
75
+ end
76
+ end
77
+
78
+ class OutputSchemaError < Error; end
79
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require_relative 'output_schema'
5
+
6
+ module RCrewAI
7
+ # Runs a single planning pass over a crew's tasks before execution. Asks an
8
+ # LLM to draft a short, concrete plan for each task and folds that plan into
9
+ # the task's description, so the executing agent starts with a game plan.
10
+ #
11
+ # Mirrors CrewAI's `planning=True`. Best-effort: if the planner errors or
12
+ # returns unparseable output, execution proceeds with the original tasks.
13
+ class Planning
14
+ def initialize(crew, llm: nil, logger: nil)
15
+ @crew = crew
16
+ @llm = llm || LLMClient.for_provider
17
+ @logger = logger
18
+ end
19
+
20
+ def plan!
21
+ return if @crew.tasks.empty?
22
+
23
+ plans = request_plans
24
+ return if plans.nil? || plans.empty?
25
+
26
+ @crew.tasks.each do |task|
27
+ step = plans[task.name] || plans[task.name.to_s]
28
+ task.enrich_description("Plan: #{step}") if step
29
+ end
30
+ rescue StandardError => e
31
+ @logger&.warn("Planning pass failed, continuing without a plan: #{e.message}")
32
+ nil
33
+ end
34
+
35
+ private
36
+
37
+ def request_plans
38
+ response = @llm.chat(messages: [
39
+ { role: 'system', content: system_prompt },
40
+ { role: 'user', content: user_prompt }
41
+ ])
42
+ content = response.is_a?(Hash) ? response[:content].to_s : response.to_s
43
+ parse_plans(content)
44
+ end
45
+
46
+ def parse_plans(content)
47
+ OutputSchema.parse(content)
48
+ rescue OutputSchemaError
49
+ nil
50
+ end
51
+
52
+ def system_prompt
53
+ 'You are a planning assistant. Given a list of tasks, produce a short, ' \
54
+ 'concrete plan for each. Respond ONLY with a JSON object mapping each ' \
55
+ 'task name to a one-sentence plan string.'
56
+ end
57
+
58
+ def user_prompt
59
+ lines = @crew.tasks.map do |t|
60
+ "- #{t.name}: #{t.description} (expected: #{t.expected_output || 'n/a'})"
61
+ end
62
+ "Tasks:\n#{lines.join("\n")}\n\nReturn JSON: { \"<task name>\": \"<plan>\", ... }"
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RCrewAI
4
+ # Thread-safe requests-per-minute throttle. Records call timestamps in a
5
+ # rolling 60-second window; `acquire` blocks (sleeps) until a slot is free.
6
+ #
7
+ # The clock and sleeper are injectable so tests can drive time deterministically
8
+ # without touching the wall clock. `max_rpm` of nil or 0 means unlimited.
9
+ class RateLimiter
10
+ WINDOW = 60.0
11
+
12
+ def initialize(max_rpm:, clock: nil, sleeper: nil)
13
+ @max_rpm = max_rpm
14
+ @clock = clock || -> { current_time }
15
+ @sleeper = sleeper || ->(seconds) { sleep(seconds) }
16
+ @calls = []
17
+ @mutex = Mutex.new
18
+ end
19
+
20
+ # Blocks until making a call would keep us within max_rpm, then records it.
21
+ def acquire
22
+ return record_unlimited if unlimited?
23
+
24
+ loop do
25
+ wait = @mutex.synchronize do
26
+ prune(@clock.call)
27
+ if @calls.length < @max_rpm
28
+ @calls << @clock.call
29
+ return
30
+ end
31
+ # Time until the oldest in-window call ages out.
32
+ (@calls.first + WINDOW) - @clock.call
33
+ end
34
+
35
+ @sleeper.call(wait) if wait.positive?
36
+ end
37
+ end
38
+
39
+ # Number of calls currently inside the window (useful for tests/metrics).
40
+ def recent_count
41
+ @mutex.synchronize do
42
+ prune(@clock.call)
43
+ @calls.length
44
+ end
45
+ end
46
+
47
+ private
48
+
49
+ def unlimited?
50
+ @max_rpm.nil? || @max_rpm.zero?
51
+ end
52
+
53
+ def record_unlimited
54
+ @mutex.synchronize { @calls << @clock.call }
55
+ nil
56
+ end
57
+
58
+ def prune(now)
59
+ cutoff = now - WINDOW
60
+ @calls.reject! { |t| t <= cutoff }
61
+ end
62
+
63
+ def current_time
64
+ # Fully-qualified: bare `Process` would resolve to RCrewAI::Process here.
65
+ ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
66
+ end
67
+
68
+ # Wraps an LLM client so every #chat acquires a rate-limiter slot first.
69
+ # All other messages delegate to the wrapped client unchanged.
70
+ class ThrottledClient
71
+ def initialize(client, limiter)
72
+ @client = client
73
+ @limiter = limiter
74
+ end
75
+
76
+ def chat(**kwargs, &block)
77
+ @limiter.acquire
78
+ @client.chat(**kwargs, &block)
79
+ end
80
+
81
+ def respond_to_missing?(name, include_private = false)
82
+ @client.respond_to?(name, include_private)
83
+ end
84
+
85
+ def method_missing(name, *args, **kwargs, &block)
86
+ if @client.respond_to?(name)
87
+ @client.public_send(name, *args, **kwargs, &block)
88
+ else
89
+ super
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
data/lib/rcrewai/task.rb CHANGED
@@ -2,12 +2,14 @@
2
2
 
3
3
  require_relative 'async_executor'
4
4
  require_relative 'human_input'
5
+ require_relative 'output_schema'
5
6
 
6
7
  module RCrewAI
7
8
  class Task
8
9
  include AsyncExtensions
9
10
  include HumanInteractionExtensions
10
- attr_reader :name, :description, :agent, :context, :expected_output, :tools, :async
11
+ attr_reader :name, :description, :agent, :context, :expected_output, :tools, :async,
12
+ :raw_result, :structured_output, :attachments
11
13
  attr_accessor :result, :status, :start_time, :end_time, :execution_time
12
14
 
13
15
  def initialize(name:, description:, agent: nil, **options)
@@ -19,6 +21,17 @@ module RCrewAI
19
21
  @tools = options[:tools] || [] # Additional tools for this specific task
20
22
  @async = options[:async] || false # Whether task can run asynchronously
21
23
  @callback = options[:callback] # Callback function after completion
24
+ @attachments = options[:attachments] || [] # Multimodal inputs (images)
25
+
26
+ # Output processing (0.4.0)
27
+ @output_schema = options[:output_schema] # JSON-schema for structured output
28
+ @guardrail = options[:guardrail] # ->(output) { [ok, value_or_error] }
29
+ @guardrail_max_retries = options.fetch(:guardrail_max_retries, 3)
30
+ @output_file = options[:output_file] # Path to write result to
31
+ @create_directory = options.fetch(:create_directory, true)
32
+ @markdown = options.fetch(:markdown, false)
33
+ @raw_result = nil # Unprocessed string content
34
+ @structured_output = nil # Parsed object when output_schema set
22
35
 
23
36
  # Human interaction options
24
37
  @human_input_enabled = options[:human_input] || false
@@ -62,7 +75,7 @@ module RCrewAI
62
75
  )
63
76
  end
64
77
 
65
- @result = agent.execute_task(self)
78
+ @result = run_agent_with_output_processing
66
79
 
67
80
  # Post-execution human review if configured
68
81
  if @human_input_enabled && @human_review_points.include?(:completion)
@@ -166,6 +179,12 @@ module RCrewAI
166
179
  @context << task unless @context.include?(task)
167
180
  end
168
181
 
182
+ # Appends supplementary guidance (e.g. a planning step) to the task's
183
+ # description without discarding the original instructions.
184
+ def enrich_description(text)
185
+ @description = "#{@description}\n\n#{text}"
186
+ end
187
+
169
188
  def add_tool(tool)
170
189
  @tools << tool unless @tools.include?(tool)
171
190
  end
@@ -190,6 +209,74 @@ module RCrewAI
190
209
 
191
210
  private
192
211
 
212
+ # Runs the agent, then applies guardrail validation and schema coercion.
213
+ # Guardrail/schema failures re-run the agent (up to @guardrail_max_retries)
214
+ # with the failure fed back in, rather than raising immediately.
215
+ def run_agent_with_output_processing
216
+ attempts = 0
217
+ feedback = nil
218
+
219
+ loop do
220
+ attempts += 1
221
+ raw = extract_content(agent.execute_task(self))
222
+ @raw_result = raw
223
+
224
+ begin
225
+ apply_guardrail!(raw)
226
+ @structured_output = OutputSchema.coerce(raw, @output_schema) if @output_schema
227
+ rescue OutputProcessingError, OutputSchemaError => e
228
+ if attempts <= @guardrail_max_retries
229
+ feedback = e.message
230
+ append_feedback_to_description(feedback)
231
+ next
232
+ end
233
+ raise
234
+ end
235
+
236
+ write_output_file(guardrail_value_or(raw)) if @output_file
237
+ return guardrail_value_or(raw)
238
+ end
239
+ end
240
+
241
+ # Accepts either the legacy plain-string return or the 0.3.0 result hash.
242
+ def extract_content(agent_result)
243
+ return agent_result[:content].to_s if agent_result.is_a?(Hash)
244
+
245
+ agent_result.to_s
246
+ end
247
+
248
+ def apply_guardrail!(raw)
249
+ return unless @guardrail
250
+
251
+ ok, value = @guardrail.call(raw)
252
+ raise OutputProcessingError, "guardrail rejected output: #{value}" unless ok
253
+
254
+ @guardrail_value = value
255
+ end
256
+
257
+ def guardrail_value_or(raw)
258
+ @guardrail ? @guardrail_value : raw
259
+ end
260
+
261
+ def append_feedback_to_description(feedback)
262
+ @description = "#{@description}\n\n[Retry] Previous attempt was rejected: #{feedback}. " \
263
+ 'Please correct the output.'
264
+ end
265
+
266
+ def write_output_file(content)
267
+ dir = File.dirname(@output_file)
268
+ if @create_directory
269
+ require 'fileutils'
270
+ FileUtils.mkdir_p(dir)
271
+ elsif !Dir.exist?(dir)
272
+ raise OutputProcessingError, "output directory does not exist: #{dir}"
273
+ end
274
+
275
+ body = content.to_s
276
+ body = "# #{name}\n\n#{body}" if @markdown && !body.lstrip.start_with?('#')
277
+ File.write(@output_file, body)
278
+ end
279
+
193
280
  def confirm_task_execution
194
281
  message = "Confirm execution of task: #{name}"
195
282
  context = "Description: #{description}\nExpected Output: #{expected_output || 'Not specified'}\nAssigned Agent: #{agent&.name || 'No agent'}"
@@ -365,4 +452,5 @@ module RCrewAI
365
452
 
366
453
  class TaskExecutionError < Error; end
367
454
  class TaskDependencyError < TaskExecutionError; end
455
+ class OutputProcessingError < TaskExecutionError; end
368
456
  end
@@ -27,7 +27,7 @@ module RCrewAI
27
27
  emit(Events::IterationStart, iteration: iter, iteration_index: iter)
28
28
 
29
29
  response = @llm.chat(
30
- messages: msgs,
30
+ messages: fit_context(msgs),
31
31
  tools: @tools.map(&:json_schema),
32
32
  stream: ->(e) { @sink.call(retag(e, iter)) }
33
33
  )
@@ -80,6 +80,12 @@ module RCrewAI
80
80
 
81
81
  private
82
82
 
83
+ # Trims the message list to the model's context window when the agent
84
+ # supports it; a no-op otherwise.
85
+ def fit_context(messages)
86
+ @agent.respond_to?(:fit_context) ? @agent.fit_context(messages) : messages
87
+ end
88
+
83
89
  def tool_result_message(call_id, content)
84
90
  { role: 'tool', tool_call_id: call_id, content: content }
85
91
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RCrewAI
4
- VERSION = '0.3.0'
4
+ VERSION = '0.5.0'
5
5
  end
data/lib/rcrewai.rb CHANGED
@@ -23,6 +23,10 @@ require_relative 'rcrewai/sse_parser'
23
23
  require_relative 'rcrewai/pricing'
24
24
  require_relative 'rcrewai/llm_client'
25
25
  require_relative 'rcrewai/memory'
26
+ require_relative 'rcrewai/rate_limiter'
27
+ require_relative 'rcrewai/context_window'
28
+ require_relative 'rcrewai/multimodal'
29
+ require_relative 'rcrewai/knowledge'
26
30
  require_relative 'rcrewai/human_input'
27
31
  require_relative 'rcrewai/tool_schema'
28
32
  require_relative 'rcrewai/provider_schema'
@@ -41,4 +45,5 @@ require_relative 'rcrewai/async_executor'
41
45
  require_relative 'rcrewai/agent'
42
46
  require_relative 'rcrewai/task'
43
47
  require_relative 'rcrewai/crew'
48
+ require_relative 'rcrewai/flow'
44
49
  require_relative 'rcrewai/mcp'