rcrewai 0.4.0 → 0.6.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/CHANGELOG.md +36 -1
- data/README.md +108 -1
- data/ROADMAP.md +15 -10
- data/docs/superpowers/specs/2026-07-06-cognitive-memory-design.md +118 -0
- data/docs/upgrading-to-0.4.md +191 -0
- data/docs/upgrading-to-0.6.md +101 -0
- data/examples/cognitive_memory_example.rb +66 -0
- data/examples/flow_example.rb +89 -0
- data/examples/knowledge_rag_example.rb +72 -0
- data/examples/planning_and_training_example.rb +72 -0
- data/examples/structured_output_example.rb +92 -0
- data/lib/rcrewai/agent.rb +50 -7
- data/lib/rcrewai/agent_augmentations.rb +75 -0
- data/lib/rcrewai/context_window.rb +75 -0
- data/lib/rcrewai/crew.rb +45 -7
- data/lib/rcrewai/knowledge/store.rb +4 -14
- data/lib/rcrewai/legacy_react_runner.rb +7 -1
- data/lib/rcrewai/memory/base_memory.rb +107 -0
- data/lib/rcrewai/memory/entity_memory.rb +47 -0
- data/lib/rcrewai/memory/in_memory_store.rb +40 -0
- data/lib/rcrewai/memory/long_term_memory.rb +39 -0
- data/lib/rcrewai/memory/short_term_memory.rb +20 -0
- data/lib/rcrewai/memory/sqlite_store.rb +99 -0
- data/lib/rcrewai/memory/tool_memory.rb +38 -0
- data/lib/rcrewai/memory.rb +57 -160
- data/lib/rcrewai/multimodal.rb +67 -0
- data/lib/rcrewai/rate_limiter.rb +94 -0
- data/lib/rcrewai/similarity.rb +43 -0
- data/lib/rcrewai/task.rb +2 -1
- data/lib/rcrewai/tool_runner.rb +7 -1
- data/lib/rcrewai/version.rb +1 -1
- data/lib/rcrewai.rb +11 -0
- data/rcrewai.gemspec +1 -0
- metadata +35 -1
data/lib/rcrewai/memory.rb
CHANGED
|
@@ -1,202 +1,99 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'json'
|
|
4
|
-
require 'digest'
|
|
5
4
|
|
|
6
5
|
module RCrewAI
|
|
6
|
+
# Cognitive memory facade. Preserves the original public API
|
|
7
|
+
# (add_execution / add_tool_usage / relevant_executions / tool_usage_for /
|
|
8
|
+
# clear_*! / stats) while delegating to semantic, optionally-persistent
|
|
9
|
+
# memory types (short-term, long-term, entity, tool).
|
|
10
|
+
#
|
|
11
|
+
# Zero-config: `Memory.new` uses an in-memory store and, when no embedder is
|
|
12
|
+
# available, falls back to lexical similarity — so existing code behaves as
|
|
13
|
+
# before, just with better recall once an embedder is configured.
|
|
7
14
|
class Memory
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
@
|
|
12
|
-
@
|
|
13
|
-
@tool_usage = [] # Tool usage history
|
|
14
|
-
@max_short_term = 100
|
|
15
|
-
@similarity_threshold = 0.7
|
|
15
|
+
def initialize(scope: 'default', embedder: nil, store: nil, short_term_limit: 100)
|
|
16
|
+
@short_term = ShortTermMemory.new(scope: scope, embedder: embedder, store: store, limit: short_term_limit)
|
|
17
|
+
@long_term = LongTermMemory.new(scope: scope, embedder: embedder, store: store)
|
|
18
|
+
@entity = EntityMemory.new(scope: scope, embedder: embedder, store: store)
|
|
19
|
+
@tool = ToolMemory.new(scope: scope, embedder: embedder, store: store)
|
|
16
20
|
end
|
|
17
21
|
|
|
22
|
+
# --- original API --------------------------------------------------------
|
|
23
|
+
|
|
18
24
|
def add_execution(task, result, execution_time)
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
execution_time
|
|
25
|
-
|
|
26
|
-
success: !result.to_s.downcase.include?('failed'),
|
|
27
|
-
hash: generate_task_hash(task)
|
|
25
|
+
success = !result.to_s.downcase.include?('failed')
|
|
26
|
+
text = "Task: #{task.name}\nDescription: #{task.description}\nResult: #{truncate(result, 300)}"
|
|
27
|
+
metadata = {
|
|
28
|
+
'task' => task.name,
|
|
29
|
+
'success' => success,
|
|
30
|
+
'execution_time' => execution_time,
|
|
31
|
+
'result' => result.to_s
|
|
28
32
|
}
|
|
29
33
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
task_type = execution_data[:task_type]
|
|
38
|
-
@long_term[task_type] ||= []
|
|
39
|
-
@long_term[task_type] << execution_data
|
|
40
|
-
|
|
41
|
-
# Keep only best executions for each type
|
|
42
|
-
@long_term[task_type] = @long_term[task_type]
|
|
43
|
-
.sort_by { |e| [e[:success] ? 0 : 1, -e[:execution_time]] }
|
|
44
|
-
.first(10)
|
|
34
|
+
@short_term.record(text, metadata)
|
|
35
|
+
if success
|
|
36
|
+
@long_term.record(text, metadata)
|
|
37
|
+
@entity.observe("#{task.description} #{result}")
|
|
38
|
+
end
|
|
39
|
+
nil
|
|
45
40
|
end
|
|
46
41
|
|
|
47
42
|
def add_tool_usage(tool_name, params, result)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
params: params,
|
|
51
|
-
result: result,
|
|
52
|
-
timestamp: Time.now,
|
|
53
|
-
success: !result.to_s.downcase.include?('error')
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
@tool_usage.unshift(usage_data)
|
|
57
|
-
@tool_usage = @tool_usage.first(50) # Keep last 50 tool usages
|
|
43
|
+
@tool.record_call(tool_name, params, result)
|
|
44
|
+
nil
|
|
58
45
|
end
|
|
59
46
|
|
|
47
|
+
# Returns a formatted string of the most relevant past executions, or nil.
|
|
60
48
|
def relevant_executions(task, limit = 3)
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
# Check short-term for exact or similar matches
|
|
68
|
-
@short_term.each do |execution|
|
|
69
|
-
if execution[:hash] == task_hash
|
|
70
|
-
candidates << { execution: execution, similarity: 1.0 }
|
|
71
|
-
elsif execution[:task_type] == task_type
|
|
72
|
-
similarity = calculate_similarity(task, execution)
|
|
73
|
-
candidates << { execution: execution, similarity: similarity } if similarity > @similarity_threshold
|
|
74
|
-
end
|
|
75
|
-
end
|
|
49
|
+
query = "#{task.name} #{task.description}"
|
|
50
|
+
recalled = (@short_term.recall(query, limit: limit) + @long_term.recall(query, limit: limit))
|
|
51
|
+
seen = {}
|
|
52
|
+
unique = recalled.reject { |r| seen[r[:text]].tap { seen[r[:text]] = true } }
|
|
53
|
+
return nil if unique.empty?
|
|
76
54
|
|
|
77
|
-
|
|
78
|
-
@long_term[task_type]&.each do |execution|
|
|
79
|
-
similarity = calculate_similarity(task, execution)
|
|
80
|
-
candidates << { execution: execution, similarity: similarity } if similarity > @similarity_threshold
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
# Sort by similarity and success, return top results
|
|
84
|
-
relevant = candidates
|
|
85
|
-
.sort_by { |c| [-c[:similarity], c[:execution][:success] ? 0 : 1] }
|
|
86
|
-
.first(limit)
|
|
87
|
-
.map { |c| format_execution_for_context(c[:execution]) }
|
|
88
|
-
|
|
89
|
-
relevant.empty? ? nil : relevant.join("\n---\n")
|
|
55
|
+
unique.first(limit).map { |r| format_execution(r) }.join("\n---\n")
|
|
90
56
|
end
|
|
91
57
|
|
|
92
58
|
def tool_usage_for(tool_name, limit = 5)
|
|
93
|
-
@
|
|
94
|
-
.select { |usage| usage[:tool_name] == tool_name }
|
|
95
|
-
.first(limit)
|
|
96
|
-
.map { |usage| format_tool_usage_for_context(usage) }
|
|
97
|
-
.join("\n")
|
|
59
|
+
@tool.usage_for(tool_name, limit: limit).join("\n")
|
|
98
60
|
end
|
|
99
61
|
|
|
100
62
|
def clear_short_term!
|
|
101
|
-
@short_term.clear
|
|
63
|
+
@short_term.clear!
|
|
102
64
|
end
|
|
103
65
|
|
|
104
66
|
def clear_all!
|
|
105
|
-
@short_term.clear
|
|
106
|
-
@long_term.clear
|
|
107
|
-
@
|
|
67
|
+
@short_term.clear!
|
|
68
|
+
@long_term.clear!
|
|
69
|
+
@entity.clear!
|
|
70
|
+
@tool.clear!
|
|
108
71
|
end
|
|
109
72
|
|
|
110
73
|
def stats
|
|
111
74
|
{
|
|
112
|
-
short_term_count: @short_term.
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
tool_usage_count: @
|
|
116
|
-
success_rate: calculate_success_rate
|
|
75
|
+
short_term_count: @short_term.count,
|
|
76
|
+
long_term_total: @long_term.count,
|
|
77
|
+
entity_count: @entity.entities.length,
|
|
78
|
+
tool_usage_count: @tool.count
|
|
117
79
|
}
|
|
118
80
|
end
|
|
119
81
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
def classify_task_type(task)
|
|
123
|
-
description = task.description.downcase
|
|
124
|
-
|
|
125
|
-
if description.include?('research') || description.include?('find') || description.include?('search')
|
|
126
|
-
return :research
|
|
127
|
-
end
|
|
128
|
-
if description.include?('analyze') || description.include?('examine') || description.include?('study')
|
|
129
|
-
return :analysis
|
|
130
|
-
end
|
|
131
|
-
if description.include?('write') || description.include?('create') || description.include?('compose')
|
|
132
|
-
return :writing
|
|
133
|
-
end
|
|
134
|
-
if description.include?('code') || description.include?('program') || description.include?('develop')
|
|
135
|
-
return :coding
|
|
136
|
-
end
|
|
137
|
-
if description.include?('plan') || description.include?('strategy') || description.include?('organize')
|
|
138
|
-
return :planning
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
:general
|
|
142
|
-
end
|
|
143
|
-
|
|
144
|
-
def generate_task_hash(task)
|
|
145
|
-
content = "#{task.name}:#{task.description}"
|
|
146
|
-
Digest::SHA256.hexdigest(content)[0..16]
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
def calculate_similarity(task, execution)
|
|
150
|
-
# Simple similarity based on common words and task type
|
|
151
|
-
task_words = extract_keywords(task.description)
|
|
152
|
-
execution_words = extract_keywords(execution[:task_description])
|
|
153
|
-
|
|
154
|
-
common_words = (task_words & execution_words).length
|
|
155
|
-
total_words = (task_words | execution_words).length
|
|
156
|
-
|
|
157
|
-
return 0.0 if total_words.zero?
|
|
82
|
+
# --- new surface (optional direct access) --------------------------------
|
|
158
83
|
|
|
159
|
-
|
|
84
|
+
attr_reader :short_term, :long_term, :entity, :tool
|
|
160
85
|
|
|
161
|
-
|
|
162
|
-
type_bonus = classify_task_type(task) == execution[:task_type] ? 0.2 : 0.0
|
|
163
|
-
|
|
164
|
-
[word_similarity + type_bonus, 1.0].min
|
|
165
|
-
end
|
|
166
|
-
|
|
167
|
-
def extract_keywords(text)
|
|
168
|
-
# Simple keyword extraction - remove common words
|
|
169
|
-
stopwords = %w[the a an and or but in on at to for of with by]
|
|
170
|
-
text.downcase.split(/\W+/).reject { |w| w.length < 3 || stopwords.include?(w) }
|
|
171
|
-
end
|
|
172
|
-
|
|
173
|
-
def format_execution_for_context(execution)
|
|
174
|
-
success_indicator = execution[:success] ? '✓' : '✗'
|
|
175
|
-
<<~CONTEXT
|
|
176
|
-
#{success_indicator} Task: #{execution[:task_name]}
|
|
177
|
-
Description: #{execution[:task_description]}
|
|
178
|
-
Result: #{execution[:result][0..200]}#{'...' if execution[:result].length > 200}
|
|
179
|
-
Time: #{execution[:execution_time].round(2)}s
|
|
180
|
-
Date: #{execution[:timestamp].strftime('%Y-%m-%d %H:%M')}
|
|
181
|
-
CONTEXT
|
|
182
|
-
end
|
|
86
|
+
private
|
|
183
87
|
|
|
184
|
-
def
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
#{success_indicator} Tool: #{usage[:tool_name]}
|
|
189
|
-
Params: #{params_str}
|
|
190
|
-
Result: #{usage[:result][0..100]}#{'...' if usage[:result].to_s.length > 100}
|
|
191
|
-
Date: #{usage[:timestamp].strftime('%Y-%m-%d %H:%M')}
|
|
192
|
-
CONTEXT
|
|
88
|
+
def format_execution(record)
|
|
89
|
+
meta = record[:metadata] || {}
|
|
90
|
+
indicator = meta['success'] == false ? '✗' : '✓'
|
|
91
|
+
"#{indicator} #{record[:text]}"
|
|
193
92
|
end
|
|
194
93
|
|
|
195
|
-
def
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
successful = @short_term.count { |e| e[:success] }
|
|
199
|
-
(successful.to_f / @short_term.length * 100).round(1)
|
|
94
|
+
def truncate(text, limit)
|
|
95
|
+
str = text.to_s
|
|
96
|
+
str.length > limit ? "#{str[0, limit]}..." : str
|
|
200
97
|
end
|
|
201
98
|
end
|
|
202
99
|
end
|
|
@@ -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,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
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RCrewAI
|
|
4
|
+
# Similarity measures shared across Knowledge and Memory. `cosine` compares
|
|
5
|
+
# embedding vectors; `lexical` is the word-overlap fallback used when no
|
|
6
|
+
# embedder is available.
|
|
7
|
+
module Similarity
|
|
8
|
+
STOPWORDS = %w[the a an and or but in on at to for of with by is are was were be].freeze
|
|
9
|
+
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def cosine(vec_a, vec_b)
|
|
13
|
+
dot = 0.0
|
|
14
|
+
norm_a = 0.0
|
|
15
|
+
norm_b = 0.0
|
|
16
|
+
length = [vec_a.length, vec_b.length].max
|
|
17
|
+
length.times do |i|
|
|
18
|
+
ai = (vec_a[i] || 0).to_f
|
|
19
|
+
bi = (vec_b[i] || 0).to_f
|
|
20
|
+
dot += ai * bi
|
|
21
|
+
norm_a += ai * ai
|
|
22
|
+
norm_b += bi * bi
|
|
23
|
+
end
|
|
24
|
+
return 0.0 if norm_a.zero? || norm_b.zero?
|
|
25
|
+
|
|
26
|
+
dot / (Math.sqrt(norm_a) * Math.sqrt(norm_b))
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Jaccard-style overlap of content words. Cheap, no embeddings.
|
|
30
|
+
def lexical(text_a, text_b)
|
|
31
|
+
words_a = keywords(text_a)
|
|
32
|
+
words_b = keywords(text_b)
|
|
33
|
+
union = (words_a | words_b).length
|
|
34
|
+
return 0.0 if union.zero?
|
|
35
|
+
|
|
36
|
+
(words_a & words_b).length.to_f / union
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def keywords(text)
|
|
40
|
+
text.to_s.downcase.split(/\W+/).reject { |w| w.length < 3 || STOPWORDS.include?(w) }
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
data/lib/rcrewai/task.rb
CHANGED
|
@@ -9,7 +9,7 @@ module RCrewAI
|
|
|
9
9
|
include AsyncExtensions
|
|
10
10
|
include HumanInteractionExtensions
|
|
11
11
|
attr_reader :name, :description, :agent, :context, :expected_output, :tools, :async,
|
|
12
|
-
:raw_result, :structured_output
|
|
12
|
+
:raw_result, :structured_output, :attachments
|
|
13
13
|
attr_accessor :result, :status, :start_time, :end_time, :execution_time
|
|
14
14
|
|
|
15
15
|
def initialize(name:, description:, agent: nil, **options)
|
|
@@ -21,6 +21,7 @@ module RCrewAI
|
|
|
21
21
|
@tools = options[:tools] || [] # Additional tools for this specific task
|
|
22
22
|
@async = options[:async] || false # Whether task can run asynchronously
|
|
23
23
|
@callback = options[:callback] # Callback function after completion
|
|
24
|
+
@attachments = options[:attachments] || [] # Multimodal inputs (images)
|
|
24
25
|
|
|
25
26
|
# Output processing (0.4.0)
|
|
26
27
|
@output_schema = options[:output_schema] # JSON-schema for structured output
|
data/lib/rcrewai/tool_runner.rb
CHANGED
|
@@ -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
|
data/lib/rcrewai/version.rb
CHANGED
data/lib/rcrewai.rb
CHANGED
|
@@ -22,7 +22,18 @@ require_relative 'rcrewai/events'
|
|
|
22
22
|
require_relative 'rcrewai/sse_parser'
|
|
23
23
|
require_relative 'rcrewai/pricing'
|
|
24
24
|
require_relative 'rcrewai/llm_client'
|
|
25
|
+
require_relative 'rcrewai/similarity'
|
|
25
26
|
require_relative 'rcrewai/memory'
|
|
27
|
+
require_relative 'rcrewai/memory/in_memory_store'
|
|
28
|
+
require_relative 'rcrewai/memory/sqlite_store'
|
|
29
|
+
require_relative 'rcrewai/memory/base_memory'
|
|
30
|
+
require_relative 'rcrewai/memory/short_term_memory'
|
|
31
|
+
require_relative 'rcrewai/memory/long_term_memory'
|
|
32
|
+
require_relative 'rcrewai/memory/entity_memory'
|
|
33
|
+
require_relative 'rcrewai/memory/tool_memory'
|
|
34
|
+
require_relative 'rcrewai/rate_limiter'
|
|
35
|
+
require_relative 'rcrewai/context_window'
|
|
36
|
+
require_relative 'rcrewai/multimodal'
|
|
26
37
|
require_relative 'rcrewai/knowledge'
|
|
27
38
|
require_relative 'rcrewai/human_input'
|
|
28
39
|
require_relative 'rcrewai/tool_schema'
|
data/rcrewai.gemspec
CHANGED
|
@@ -59,6 +59,7 @@ Gem::Specification.new do |spec|
|
|
|
59
59
|
spec.add_dependency 'nokogiri', '~> 1.15'
|
|
60
60
|
spec.add_dependency 'pdf-reader', '~> 2.11'
|
|
61
61
|
spec.add_dependency 'ruby-openai', '~> 6.3'
|
|
62
|
+
spec.add_dependency 'sqlite3', '~> 2.0'
|
|
62
63
|
spec.add_dependency 'thor', '~> 1.3'
|
|
63
64
|
|
|
64
65
|
# Development dependencies
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rcrewai
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- gkosmo
|
|
@@ -149,6 +149,20 @@ dependencies:
|
|
|
149
149
|
- - "~>"
|
|
150
150
|
- !ruby/object:Gem::Version
|
|
151
151
|
version: '6.3'
|
|
152
|
+
- !ruby/object:Gem::Dependency
|
|
153
|
+
name: sqlite3
|
|
154
|
+
requirement: !ruby/object:Gem::Requirement
|
|
155
|
+
requirements:
|
|
156
|
+
- - "~>"
|
|
157
|
+
- !ruby/object:Gem::Version
|
|
158
|
+
version: '2.0'
|
|
159
|
+
type: :runtime
|
|
160
|
+
prerelease: false
|
|
161
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
162
|
+
requirements:
|
|
163
|
+
- - "~>"
|
|
164
|
+
- !ruby/object:Gem::Version
|
|
165
|
+
version: '2.0'
|
|
152
166
|
- !ruby/object:Gem::Dependency
|
|
153
167
|
name: thor
|
|
154
168
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -342,6 +356,7 @@ files:
|
|
|
342
356
|
- docs/mcp.md
|
|
343
357
|
- docs/superpowers/plans/2026-05-11-llm-modernization.md
|
|
344
358
|
- docs/superpowers/specs/2026-05-11-llm-modernization-design.md
|
|
359
|
+
- docs/superpowers/specs/2026-07-06-cognitive-memory-design.md
|
|
345
360
|
- docs/tutorials/advanced-agents.md
|
|
346
361
|
- docs/tutorials/custom-tools.md
|
|
347
362
|
- docs/tutorials/deployment.md
|
|
@@ -349,17 +364,26 @@ files:
|
|
|
349
364
|
- docs/tutorials/index.md
|
|
350
365
|
- docs/tutorials/multiple-crews.md
|
|
351
366
|
- docs/upgrading-to-0.3.md
|
|
367
|
+
- docs/upgrading-to-0.4.md
|
|
368
|
+
- docs/upgrading-to-0.6.md
|
|
352
369
|
- examples/async_execution_example.rb
|
|
370
|
+
- examples/cognitive_memory_example.rb
|
|
371
|
+
- examples/flow_example.rb
|
|
353
372
|
- examples/hierarchical_crew_example.rb
|
|
354
373
|
- examples/human_in_the_loop_example.rb
|
|
374
|
+
- examples/knowledge_rag_example.rb
|
|
355
375
|
- examples/mcp_example.rb
|
|
356
376
|
- examples/native_tools_example.rb
|
|
377
|
+
- examples/planning_and_training_example.rb
|
|
357
378
|
- examples/streaming_example.rb
|
|
379
|
+
- examples/structured_output_example.rb
|
|
358
380
|
- lib/rcrewai.rb
|
|
359
381
|
- lib/rcrewai/agent.rb
|
|
382
|
+
- lib/rcrewai/agent_augmentations.rb
|
|
360
383
|
- lib/rcrewai/async_executor.rb
|
|
361
384
|
- lib/rcrewai/cli.rb
|
|
362
385
|
- lib/rcrewai/configuration.rb
|
|
386
|
+
- lib/rcrewai/context_window.rb
|
|
363
387
|
- lib/rcrewai/crew.rb
|
|
364
388
|
- lib/rcrewai/events.rb
|
|
365
389
|
- lib/rcrewai/flow.rb
|
|
@@ -386,11 +410,21 @@ files:
|
|
|
386
410
|
- lib/rcrewai/mcp/transport/http.rb
|
|
387
411
|
- lib/rcrewai/mcp/transport/stdio.rb
|
|
388
412
|
- lib/rcrewai/memory.rb
|
|
413
|
+
- lib/rcrewai/memory/base_memory.rb
|
|
414
|
+
- lib/rcrewai/memory/entity_memory.rb
|
|
415
|
+
- lib/rcrewai/memory/in_memory_store.rb
|
|
416
|
+
- lib/rcrewai/memory/long_term_memory.rb
|
|
417
|
+
- lib/rcrewai/memory/short_term_memory.rb
|
|
418
|
+
- lib/rcrewai/memory/sqlite_store.rb
|
|
419
|
+
- lib/rcrewai/memory/tool_memory.rb
|
|
420
|
+
- lib/rcrewai/multimodal.rb
|
|
389
421
|
- lib/rcrewai/output_schema.rb
|
|
390
422
|
- lib/rcrewai/planning.rb
|
|
391
423
|
- lib/rcrewai/pricing.rb
|
|
392
424
|
- lib/rcrewai/process.rb
|
|
393
425
|
- lib/rcrewai/provider_schema.rb
|
|
426
|
+
- lib/rcrewai/rate_limiter.rb
|
|
427
|
+
- lib/rcrewai/similarity.rb
|
|
394
428
|
- lib/rcrewai/sse_parser.rb
|
|
395
429
|
- lib/rcrewai/task.rb
|
|
396
430
|
- lib/rcrewai/tool_runner.rb
|