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/crew.rb
CHANGED
|
@@ -23,16 +23,33 @@ module RCrewAI
|
|
|
23
23
|
@planning_llm = options[:planning_llm]
|
|
24
24
|
@planned = false
|
|
25
25
|
@knowledge = build_knowledge(options[:knowledge], options[:knowledge_sources])
|
|
26
|
+
@before_kickoff_hooks = []
|
|
27
|
+
@after_kickoff_hooks = []
|
|
28
|
+
@last_inputs = {}
|
|
26
29
|
@process_instance = nil
|
|
27
30
|
validate_process_type!
|
|
28
31
|
end
|
|
29
32
|
|
|
30
|
-
attr_reader :knowledge, :stream_sink
|
|
33
|
+
attr_reader :knowledge, :stream_sink, :last_inputs
|
|
31
34
|
|
|
32
35
|
def planning?
|
|
33
36
|
@planning
|
|
34
37
|
end
|
|
35
38
|
|
|
39
|
+
# Register a callback run before execution. Receives the inputs hash and may
|
|
40
|
+
# return a transformed hash. Multiple hooks run in registration order.
|
|
41
|
+
def before_kickoff(&block)
|
|
42
|
+
@before_kickoff_hooks << block
|
|
43
|
+
self
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Register a callback run after execution. Receives the result and may
|
|
47
|
+
# return a transformed result. Multiple hooks run in registration order.
|
|
48
|
+
def after_kickoff(&block)
|
|
49
|
+
@after_kickoff_hooks << block
|
|
50
|
+
self
|
|
51
|
+
end
|
|
52
|
+
|
|
36
53
|
def add_agent(agent)
|
|
37
54
|
@agents << agent
|
|
38
55
|
end
|
|
@@ -41,20 +58,25 @@ module RCrewAI
|
|
|
41
58
|
@tasks << task
|
|
42
59
|
end
|
|
43
60
|
|
|
44
|
-
def execute(async: false, stream: nil, **async_options, &block)
|
|
61
|
+
def execute(async: false, stream: nil, inputs: {}, **async_options, &block)
|
|
45
62
|
sinks = []
|
|
46
63
|
sinks << block if block_given?
|
|
47
64
|
Array(stream).each { |s| sinks << s } if stream
|
|
48
65
|
@stream_sink = sinks.empty? ? nil : RCrewAI::Events.fan_out(sinks)
|
|
49
66
|
|
|
67
|
+
run_before_hooks(inputs)
|
|
68
|
+
|
|
50
69
|
distribute_knowledge if @knowledge
|
|
51
70
|
run_planning_pass if planning?
|
|
52
71
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
72
|
+
result = async ? execute_async(**async_options) : execute_sync
|
|
73
|
+
run_after_hooks(result)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Runs the crew once per input set, returning one result per input in order.
|
|
77
|
+
# Runs are isolated: each execution starts from only its own inputs.
|
|
78
|
+
def kickoff_for_each(inputs:)
|
|
79
|
+
Array(inputs).map { |input| execute(inputs: input) }
|
|
58
80
|
end
|
|
59
81
|
|
|
60
82
|
# Runs the crew repeatedly, collecting feedback after each iteration and
|
|
@@ -144,6 +166,22 @@ module RCrewAI
|
|
|
144
166
|
|
|
145
167
|
private
|
|
146
168
|
|
|
169
|
+
def run_before_hooks(inputs)
|
|
170
|
+
# Assign before running hooks so a hook that reads #last_inputs sees this
|
|
171
|
+
# run's own inputs; update it as each hook transforms them.
|
|
172
|
+
@last_inputs = inputs || {}
|
|
173
|
+
@before_kickoff_hooks.each do |hook|
|
|
174
|
+
@last_inputs = hook.call(@last_inputs) || @last_inputs
|
|
175
|
+
end
|
|
176
|
+
@last_inputs
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def run_after_hooks(result)
|
|
180
|
+
@after_kickoff_hooks.reduce(result) do |acc, hook|
|
|
181
|
+
hook.call(acc) || acc
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
147
185
|
def build_knowledge(knowledge, sources)
|
|
148
186
|
return knowledge if knowledge
|
|
149
187
|
return nil if sources.nil? || sources.empty?
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative '../similarity'
|
|
4
|
+
|
|
3
5
|
module RCrewAI
|
|
4
6
|
module Knowledge
|
|
5
7
|
# In-memory vector store with cosine-similarity search. The default backing
|
|
@@ -38,20 +40,8 @@ module RCrewAI
|
|
|
38
40
|
|
|
39
41
|
private
|
|
40
42
|
|
|
41
|
-
def cosine_similarity(
|
|
42
|
-
|
|
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))
|
|
43
|
+
def cosine_similarity(vec_a, vec_b)
|
|
44
|
+
Similarity.cosine(vec_a, vec_b)
|
|
55
45
|
end
|
|
56
46
|
end
|
|
57
47
|
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 = []
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'digest'
|
|
4
|
+
require_relative '../similarity'
|
|
5
|
+
require_relative 'in_memory_store'
|
|
6
|
+
|
|
7
|
+
module RCrewAI
|
|
8
|
+
class Memory
|
|
9
|
+
# Shared behavior for the memory types: embed-on-write (when an embedder is
|
|
10
|
+
# present) and semantic recall with a lexical fallback when it isn't.
|
|
11
|
+
# Records are namespaced per (agent) scope + a type suffix so different
|
|
12
|
+
# memory types don't collide in a shared store.
|
|
13
|
+
class BaseMemory
|
|
14
|
+
def initialize(scope:, embedder: nil, store: nil, limit: nil)
|
|
15
|
+
@scope = "#{scope}:#{type_suffix}"
|
|
16
|
+
@embedder = embedder
|
|
17
|
+
@store = store || InMemoryStore.new
|
|
18
|
+
@limit = limit
|
|
19
|
+
@seq = 0
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def record(text, metadata = {})
|
|
23
|
+
add(text, metadata)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def recall(query, limit: 3)
|
|
27
|
+
records = search_records(query, limit)
|
|
28
|
+
records.map { |r| format(r) }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def count
|
|
32
|
+
@store.all(scope: @scope).length
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def clear!
|
|
36
|
+
@store.delete(scope: @scope)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
protected
|
|
40
|
+
|
|
41
|
+
# Subclasses override to namespace their records.
|
|
42
|
+
def type_suffix
|
|
43
|
+
'base'
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def format(record)
|
|
47
|
+
{ text: record[:text], metadata: record[:metadata] }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def add(text, metadata)
|
|
51
|
+
vector = embed(text)
|
|
52
|
+
id = next_id(text)
|
|
53
|
+
@store.add(id: id, text: text, vector: vector, scope: @scope, metadata: stringify(metadata))
|
|
54
|
+
evict_if_needed
|
|
55
|
+
id
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def search_records(query, limit)
|
|
59
|
+
all = @store.all(scope: @scope)
|
|
60
|
+
return [] if all.empty?
|
|
61
|
+
|
|
62
|
+
query_vector = embed(query)
|
|
63
|
+
if query_vector && all.any? { |r| r[:vector] }
|
|
64
|
+
@store.search(query_vector, k: limit, scope: @scope)
|
|
65
|
+
else
|
|
66
|
+
lexical_search(query, all, limit)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def lexical_search(query, records, limit)
|
|
71
|
+
records
|
|
72
|
+
.map { |r| [r, Similarity.lexical(query, r[:text])] }
|
|
73
|
+
.sort_by { |(_r, score)| -score }
|
|
74
|
+
.first(limit)
|
|
75
|
+
.map(&:first)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def embed(text)
|
|
79
|
+
return nil unless @embedder
|
|
80
|
+
|
|
81
|
+
@embedder.embed([text]).first
|
|
82
|
+
rescue StandardError
|
|
83
|
+
nil # embedding is best-effort; fall back to lexical
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def evict_if_needed
|
|
87
|
+
return unless @limit
|
|
88
|
+
|
|
89
|
+
records = @store.all(scope: @scope)
|
|
90
|
+
return if records.length <= @limit
|
|
91
|
+
|
|
92
|
+
# records carry a monotonic :seq in metadata; drop the oldest.
|
|
93
|
+
oldest = records.min_by { |r| r[:metadata]['seq'].to_i }
|
|
94
|
+
@store.delete_record(id: oldest[:id], scope: @scope) if @store.respond_to?(:delete_record)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def next_id(text)
|
|
98
|
+
@seq += 1
|
|
99
|
+
Digest::SHA256.hexdigest("#{@scope}:#{@seq}:#{text}")[0, 24]
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def stringify(metadata)
|
|
103
|
+
(metadata || {}).transform_keys(&:to_s).merge('seq' => @seq)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base_memory'
|
|
4
|
+
|
|
5
|
+
module RCrewAI
|
|
6
|
+
class Memory
|
|
7
|
+
# Facts about entities (people, systems, concepts) accumulated from work.
|
|
8
|
+
# Entities are extracted heuristically (capitalized tokens / acronyms); an
|
|
9
|
+
# LLM-backed extractor can be swapped in later.
|
|
10
|
+
class EntityMemory < BaseMemory
|
|
11
|
+
# Skip sentence-initial common words that happen to be capitalized.
|
|
12
|
+
COMMON = %w[The A An I In On At To For Of With By And Or But It This That Who Where When].freeze
|
|
13
|
+
|
|
14
|
+
def initialize(scope:, embedder: nil, store: nil, limit: nil)
|
|
15
|
+
super
|
|
16
|
+
@entities = []
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Records a full observation and indexes the entities it mentions.
|
|
20
|
+
def observe(text)
|
|
21
|
+
found = extract_entities(text)
|
|
22
|
+
@entities.concat(found)
|
|
23
|
+
add(text, { 'entities' => found })
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def entities
|
|
27
|
+
@entities.uniq
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
protected
|
|
31
|
+
|
|
32
|
+
def type_suffix
|
|
33
|
+
'entity'
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def format(record)
|
|
37
|
+
record[:text]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def extract_entities(text)
|
|
43
|
+
text.to_s.scan(/\b([A-Z][a-zA-Z0-9]{1,})\b/).flatten.reject { |w| COMMON.include?(w) }.uniq
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../similarity'
|
|
4
|
+
|
|
5
|
+
module RCrewAI
|
|
6
|
+
class Memory
|
|
7
|
+
# Default, volatile vector store: records live in a Hash keyed by scope.
|
|
8
|
+
# Records: { id:, text:, vector:, metadata: }. Cosine search in Ruby.
|
|
9
|
+
class InMemoryStore
|
|
10
|
+
def initialize
|
|
11
|
+
@scopes = Hash.new { |h, k| h[k] = {} }
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def add(id:, text:, vector:, scope:, metadata: {})
|
|
15
|
+
@scopes[scope][id] = { id: id, text: text, vector: vector, metadata: metadata || {} }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def all(scope:)
|
|
19
|
+
@scopes[scope].values
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def search(vector, k:, scope:)
|
|
23
|
+
@scopes[scope].values
|
|
24
|
+
.reject { |r| r[:vector].nil? }
|
|
25
|
+
.map { |r| [r, Similarity.cosine(vector, r[:vector])] }
|
|
26
|
+
.sort_by { |(_r, score)| -score }
|
|
27
|
+
.first(k)
|
|
28
|
+
.map(&:first)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def delete(scope:)
|
|
32
|
+
@scopes.delete(scope)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def delete_record(id:, scope:)
|
|
36
|
+
@scopes[scope].delete(id)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base_memory'
|
|
4
|
+
|
|
5
|
+
module RCrewAI
|
|
6
|
+
class Memory
|
|
7
|
+
# Durable insights promoted from successful executions. Dedupes
|
|
8
|
+
# near-identical insights so the store doesn't fill with paraphrases.
|
|
9
|
+
class LongTermMemory < BaseMemory
|
|
10
|
+
DEDUPE_THRESHOLD = 0.92
|
|
11
|
+
|
|
12
|
+
def record(text, metadata = {})
|
|
13
|
+
return nil if duplicate?(text)
|
|
14
|
+
|
|
15
|
+
add(text, metadata)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
protected
|
|
19
|
+
|
|
20
|
+
def type_suffix
|
|
21
|
+
'long_term'
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def duplicate?(text)
|
|
27
|
+
existing = @store.all(scope: @scope)
|
|
28
|
+
return false if existing.empty?
|
|
29
|
+
|
|
30
|
+
query_vector = embed(text)
|
|
31
|
+
if query_vector && existing.any? { |r| r[:vector] }
|
|
32
|
+
existing.any? { |r| r[:vector] && Similarity.cosine(query_vector, r[:vector]) >= DEDUPE_THRESHOLD }
|
|
33
|
+
else
|
|
34
|
+
existing.any? { |r| Similarity.lexical(text, r[:text]) >= DEDUPE_THRESHOLD }
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base_memory'
|
|
4
|
+
|
|
5
|
+
module RCrewAI
|
|
6
|
+
class Memory
|
|
7
|
+
# Recent executions with semantic recall. Capped and volatile-by-default.
|
|
8
|
+
class ShortTermMemory < BaseMemory
|
|
9
|
+
def initialize(scope:, embedder: nil, store: nil, limit: 100)
|
|
10
|
+
super
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
protected
|
|
14
|
+
|
|
15
|
+
def type_suffix
|
|
16
|
+
'short_term'
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require_relative '../similarity'
|
|
5
|
+
|
|
6
|
+
module RCrewAI
|
|
7
|
+
class Memory
|
|
8
|
+
# Persistent vector store backed by SQLite. Vectors are packed as
|
|
9
|
+
# little-endian floats; metadata is JSON. Cosine is computed in Ruby over
|
|
10
|
+
# rows filtered by scope — adequate for the thousands-of-memories scale an
|
|
11
|
+
# agent produces; an ANN index is a later optimization.
|
|
12
|
+
#
|
|
13
|
+
# The sqlite3 gem is required lazily so the rest of the library (and the
|
|
14
|
+
# in-memory store) works even if it isn't installed.
|
|
15
|
+
class SqliteStore
|
|
16
|
+
DEFAULT_PATH = File.join(Dir.home, '.rcrewai', 'memory.db')
|
|
17
|
+
|
|
18
|
+
def initialize(path: DEFAULT_PATH)
|
|
19
|
+
require 'sqlite3'
|
|
20
|
+
ensure_parent_dir(path) unless path == ':memory:'
|
|
21
|
+
@db = SQLite3::Database.new(path)
|
|
22
|
+
@db.results_as_hash = true
|
|
23
|
+
create_schema
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def add(id:, text:, vector:, scope:, metadata: {})
|
|
27
|
+
@db.execute(
|
|
28
|
+
'INSERT INTO memories (id, scope, text, vector, metadata) VALUES (?, ?, ?, ?, ?) ' \
|
|
29
|
+
'ON CONFLICT(id) DO UPDATE SET scope=excluded.scope, text=excluded.text, ' \
|
|
30
|
+
'vector=excluded.vector, metadata=excluded.metadata',
|
|
31
|
+
[id, scope, text, pack_vector(vector), JSON.generate(metadata || {})]
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def all(scope:)
|
|
36
|
+
@db.execute('SELECT * FROM memories WHERE scope = ?', [scope]).map { |row| to_record(row) }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def search(vector, k:, scope:)
|
|
40
|
+
all(scope: scope)
|
|
41
|
+
.reject { |r| r[:vector].nil? }
|
|
42
|
+
.map { |r| [r, Similarity.cosine(vector, r[:vector])] }
|
|
43
|
+
.sort_by { |(_r, score)| -score }
|
|
44
|
+
.first(k)
|
|
45
|
+
.map(&:first)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def delete(scope:)
|
|
49
|
+
@db.execute('DELETE FROM memories WHERE scope = ?', [scope])
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def delete_record(id:, scope:)
|
|
53
|
+
@db.execute('DELETE FROM memories WHERE id = ? AND scope = ?', [id, scope])
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def create_schema
|
|
59
|
+
@db.execute(<<~SQL)
|
|
60
|
+
CREATE TABLE IF NOT EXISTS memories (
|
|
61
|
+
id TEXT PRIMARY KEY,
|
|
62
|
+
scope TEXT NOT NULL,
|
|
63
|
+
text TEXT,
|
|
64
|
+
vector BLOB,
|
|
65
|
+
metadata TEXT,
|
|
66
|
+
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
67
|
+
)
|
|
68
|
+
SQL
|
|
69
|
+
@db.execute('CREATE INDEX IF NOT EXISTS idx_memories_scope ON memories (scope)')
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def ensure_parent_dir(path)
|
|
73
|
+
require 'fileutils'
|
|
74
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def pack_vector(vector)
|
|
78
|
+
return nil if vector.nil?
|
|
79
|
+
|
|
80
|
+
vector.map(&:to_f).pack('e*')
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def unpack_vector(blob)
|
|
84
|
+
return nil if blob.nil?
|
|
85
|
+
|
|
86
|
+
blob.unpack('e*')
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def to_record(row)
|
|
90
|
+
{
|
|
91
|
+
id: row['id'],
|
|
92
|
+
text: row['text'],
|
|
93
|
+
vector: unpack_vector(row['vector']),
|
|
94
|
+
metadata: JSON.parse(row['metadata'] || '{}')
|
|
95
|
+
}
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base_memory'
|
|
4
|
+
|
|
5
|
+
module RCrewAI
|
|
6
|
+
class Memory
|
|
7
|
+
# Tool-call history and outcomes. Replaces the old @tool_usage array;
|
|
8
|
+
# persistable and searchable like the other memory types.
|
|
9
|
+
class ToolMemory < BaseMemory
|
|
10
|
+
def record_call(tool_name, params, result)
|
|
11
|
+
success = !result.to_s.downcase.include?('error')
|
|
12
|
+
text = "#{tool_name}(#{format_params(params)}) -> #{result}"
|
|
13
|
+
add(text, { 'tool' => tool_name, 'success' => success, 'result' => result.to_s })
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Most-recent-first usage records for a given tool.
|
|
17
|
+
def usage_for(tool_name, limit: 5)
|
|
18
|
+
@store.all(scope: @scope)
|
|
19
|
+
.select { |r| r[:metadata]['tool'] == tool_name }
|
|
20
|
+
.sort_by { |r| -r[:metadata]['seq'].to_i }
|
|
21
|
+
.first(limit)
|
|
22
|
+
.map { |r| r[:text] }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
protected
|
|
26
|
+
|
|
27
|
+
def type_suffix
|
|
28
|
+
'tool'
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def format_params(params)
|
|
34
|
+
(params || {}).map { |k, v| "#{k}=#{v}" }.join(', ')
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|