rcrewai 0.5.0 → 0.6.1
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 +32 -1
- data/README.md +30 -4
- data/ROADMAP.md +1 -2
- data/docs/superpowers/specs/2026-07-06-cognitive-memory-design.md +118 -0
- data/docs/upgrading-to-0.6.md +101 -0
- data/examples/cognitive_memory_example.rb +66 -0
- data/lib/rcrewai/agent.rb +13 -1
- data/lib/rcrewai/knowledge/embedder.rb +78 -14
- data/lib/rcrewai/knowledge/store.rb +4 -14
- data/lib/rcrewai/memory/base_memory.rb +107 -0
- data/lib/rcrewai/memory/entity_memory.rb +62 -0
- data/lib/rcrewai/memory/in_memory_store.rb +40 -0
- data/lib/rcrewai/memory/llm_entity_extractor.rb +34 -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 +114 -0
- data/lib/rcrewai/memory/tool_memory.rb +38 -0
- data/lib/rcrewai/memory.rb +57 -160
- data/lib/rcrewai/similarity.rb +43 -0
- data/lib/rcrewai/version.rb +1 -1
- data/lib/rcrewai.rb +9 -0
- data/rcrewai.gemspec +1 -0
- metadata +27 -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, entity_extractor: nil)
|
|
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, extractor: entity_extractor)
|
|
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,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/version.rb
CHANGED
data/lib/rcrewai.rb
CHANGED
|
@@ -22,7 +22,16 @@ 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/llm_entity_extractor'
|
|
34
|
+
require_relative 'rcrewai/memory/tool_memory'
|
|
26
35
|
require_relative 'rcrewai/rate_limiter'
|
|
27
36
|
require_relative 'rcrewai/context_window'
|
|
28
37
|
require_relative 'rcrewai/multimodal'
|
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.1
|
|
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
|
|
@@ -350,7 +365,9 @@ files:
|
|
|
350
365
|
- docs/tutorials/multiple-crews.md
|
|
351
366
|
- docs/upgrading-to-0.3.md
|
|
352
367
|
- docs/upgrading-to-0.4.md
|
|
368
|
+
- docs/upgrading-to-0.6.md
|
|
353
369
|
- examples/async_execution_example.rb
|
|
370
|
+
- examples/cognitive_memory_example.rb
|
|
354
371
|
- examples/flow_example.rb
|
|
355
372
|
- examples/hierarchical_crew_example.rb
|
|
356
373
|
- examples/human_in_the_loop_example.rb
|
|
@@ -393,6 +410,14 @@ files:
|
|
|
393
410
|
- lib/rcrewai/mcp/transport/http.rb
|
|
394
411
|
- lib/rcrewai/mcp/transport/stdio.rb
|
|
395
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/llm_entity_extractor.rb
|
|
417
|
+
- lib/rcrewai/memory/long_term_memory.rb
|
|
418
|
+
- lib/rcrewai/memory/short_term_memory.rb
|
|
419
|
+
- lib/rcrewai/memory/sqlite_store.rb
|
|
420
|
+
- lib/rcrewai/memory/tool_memory.rb
|
|
396
421
|
- lib/rcrewai/multimodal.rb
|
|
397
422
|
- lib/rcrewai/output_schema.rb
|
|
398
423
|
- lib/rcrewai/planning.rb
|
|
@@ -400,6 +425,7 @@ files:
|
|
|
400
425
|
- lib/rcrewai/process.rb
|
|
401
426
|
- lib/rcrewai/provider_schema.rb
|
|
402
427
|
- lib/rcrewai/rate_limiter.rb
|
|
428
|
+
- lib/rcrewai/similarity.rb
|
|
403
429
|
- lib/rcrewai/sse_parser.rb
|
|
404
430
|
- lib/rcrewai/task.rb
|
|
405
431
|
- lib/rcrewai/tool_runner.rb
|