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.
@@ -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
- attr_reader :short_term, :long_term, :tool_usage
9
-
10
- def initialize
11
- @short_term = [] # Recent executions, limited size
12
- @long_term = {} # Persistent memory, keyed by task type/similarity
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
- execution_data = {
20
- task_name: task.name,
21
- task_description: task.description,
22
- task_type: classify_task_type(task),
23
- result: result,
24
- execution_time: execution_time,
25
- timestamp: Time.now,
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
- # Add to short-term memory
31
- @short_term.unshift(execution_data)
32
- @short_term = @short_term.first(@max_short_term)
33
-
34
- # Add to long-term memory if successful
35
- return unless execution_data[:success]
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
- usage_data = {
49
- tool_name: tool_name,
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
- task_type = classify_task_type(task)
62
- task_hash = generate_task_hash(task)
63
-
64
- # Get similar executions from both short and long term memory
65
- candidates = []
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
- # Check long-term memory
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
- @tool_usage
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
- @tool_usage.clear
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.length,
113
- long_term_types: @long_term.keys.length,
114
- long_term_total: @long_term.values.flatten.length,
115
- tool_usage_count: @tool_usage.length,
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
- private
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
- word_similarity = common_words.to_f / total_words
84
+ attr_reader :short_term, :long_term, :entity, :tool
160
85
 
161
- # Boost similarity if task types match
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 format_tool_usage_for_context(usage)
185
- success_indicator = usage[:success] ? '✓' : '✗'
186
- params_str = usage[:params].map { |k, v| "#{k}=#{v}" }.join(', ')
187
- <<~CONTEXT
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 calculate_success_rate
196
- return 0.0 if @short_term.empty?
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RCrewAI
4
- VERSION = '0.5.0'
4
+ VERSION = '0.6.1'
5
5
  end
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.5.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