rcrewai 0.6.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 +12 -1
- data/README.md +4 -3
- data/lib/rcrewai/agent.rb +2 -1
- data/lib/rcrewai/knowledge/embedder.rb +78 -14
- data/lib/rcrewai/memory/entity_memory.rb +17 -2
- data/lib/rcrewai/memory/llm_entity_extractor.rb +34 -0
- data/lib/rcrewai/memory/sqlite_store.rb +17 -2
- data/lib/rcrewai/memory.rb +2 -2
- data/lib/rcrewai/version.rb +1 -1
- data/lib/rcrewai.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 32edcb190b0d85db67d404b260fbb86d9b2821d6a89adf350d792b60a0e21a06
|
|
4
|
+
data.tar.gz: 46d061ecebca5a67714a6a8139d41bf9d20ce60d4eb5f0a1505d965defffaeb8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a77a41b7fa6dad5de896c7b635777ee5f2429eb826e65516b2d3a5b2d9e9446ca8de0bf6fd0fd94f9efd4159c5080a94d534aa6c65447abd4d781b8d05a63ff4
|
|
7
|
+
data.tar.gz: a65c248d53e9f346cd88c4f9f8e04280e89d67d5bf9695ff252ca14ee270864f23ee1f64395831ce7f2be95d682fc54ecb655d64a8c2b09b99800dcd561d3524
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.6.1] - 2026-07-06
|
|
11
|
+
|
|
12
|
+
Follow-ups to the 0.6.0 Cognitive Memory release, deepening the areas that
|
|
13
|
+
shipped as focused first cuts. All additive and backward compatible.
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
- Multi-provider embeddings: `Knowledge::Embedder.new(provider:)` now supports `:openai` (default), `:azure`, `:google`, and `:ollama` embedding endpoints, removing the hard OpenAI dependency for RAG and memory. `:anthropic` raises a clear error (no first-party embeddings API). Per-provider default models via `DEFAULT_MODELS`; existing OpenAI usage is unchanged.
|
|
17
|
+
- LLM-backed entity extraction: `EntityMemory` accepts a pluggable `extractor:` (anything responding to `call(text) -> [names]`); `RCrewAI::Memory::LlmEntityExtractor` prompts an LLM for entities (handling multi-word names the capitalized-token heuristic misses). Falls back to the heuristic when the extractor is absent, returns nothing, or raises. Wire via `Agent.new(memory: { entity_extractor: ... })`.
|
|
18
|
+
- Bounded SQLite recall: `Memory::SqliteStore.new(max_candidates:)` (default 1000) caps how many most-recent rows a search cosines, keeping recall cost constant as total memory grows instead of brute-forcing every row. `nil` considers all rows.
|
|
19
|
+
|
|
10
20
|
## [0.6.0] - 2026-07-06
|
|
11
21
|
|
|
12
22
|
Replaces the placeholder agent memory with a **cognitive memory** system —
|
|
@@ -198,7 +208,8 @@ output, guardrails, planning, and training/testing. See `ROADMAP.md`.
|
|
|
198
208
|
- CLI usage documentation
|
|
199
209
|
- Real-world use cases and examples
|
|
200
210
|
|
|
201
|
-
[Unreleased]: https://github.com/gkosmo/rcrewAI/compare/v0.6.
|
|
211
|
+
[Unreleased]: https://github.com/gkosmo/rcrewAI/compare/v0.6.1...HEAD
|
|
212
|
+
[0.6.1]: https://github.com/gkosmo/rcrewAI/compare/v0.6.0...v0.6.1
|
|
202
213
|
[0.6.0]: https://github.com/gkosmo/rcrewAI/compare/v0.5.0...v0.6.0
|
|
203
214
|
[0.5.0]: https://github.com/gkosmo/rcrewAI/compare/v0.4.0...v0.5.0
|
|
204
215
|
[0.4.0]: https://github.com/gkosmo/rcrewAI/compare/v0.3.0...v0.4.0
|
data/README.md
CHANGED
|
@@ -370,9 +370,10 @@ support = RCrewAI::Agent.new(name: 'support', role: '...', goal: '...',
|
|
|
370
370
|
crew = RCrewAI::Crew.new('support_crew', knowledge: kb)
|
|
371
371
|
```
|
|
372
372
|
|
|
373
|
-
Embeddings default to OpenAI's `text-embedding-3-small
|
|
374
|
-
`
|
|
375
|
-
|
|
373
|
+
Embeddings default to OpenAI's `text-embedding-3-small`. Use another provider
|
|
374
|
+
with `RCrewAI::Knowledge::Embedder.new(provider: :ollama)` (also `:azure`,
|
|
375
|
+
`:google`; `:anthropic` has no embeddings API), or pass any custom `embedder:`
|
|
376
|
+
(anything responding to `embed(texts)`) / vector store to swap the backend.
|
|
376
377
|
|
|
377
378
|
## 🧠 Cognitive Memory
|
|
378
379
|
|
data/lib/rcrewai/agent.rb
CHANGED
|
@@ -228,7 +228,8 @@ module RCrewAI
|
|
|
228
228
|
return memory if memory.is_a?(Memory)
|
|
229
229
|
|
|
230
230
|
opts = memory.is_a?(Hash) ? memory : {}
|
|
231
|
-
Memory.new(scope: opts.fetch(:scope, name),
|
|
231
|
+
Memory.new(scope: opts.fetch(:scope, name),
|
|
232
|
+
**opts.slice(:embedder, :store, :short_term_limit, :entity_extractor))
|
|
232
233
|
end
|
|
233
234
|
|
|
234
235
|
# Accepts a pre-built Knowledge::Base via +knowledge:+ or an array of
|
|
@@ -5,36 +5,100 @@ require 'faraday'
|
|
|
5
5
|
|
|
6
6
|
module RCrewAI
|
|
7
7
|
module Knowledge
|
|
8
|
-
# Turns text into embedding vectors.
|
|
9
|
-
#
|
|
10
|
-
#
|
|
8
|
+
# Turns text into embedding vectors. Supports multiple providers
|
|
9
|
+
# (OpenAI [default], Azure OpenAI, Google Gemini, Ollama); Anthropic has no
|
|
10
|
+
# first-party embeddings API and raises a clear error. `#embed` takes an
|
|
11
|
+
# array of strings and returns an array of vectors. Any object responding to
|
|
12
|
+
# `#embed` can be substituted.
|
|
11
13
|
class Embedder
|
|
12
|
-
|
|
14
|
+
DEFAULT_MODELS = {
|
|
15
|
+
openai: 'text-embedding-3-small',
|
|
16
|
+
azure: 'text-embedding-3-small',
|
|
17
|
+
google: 'text-embedding-004',
|
|
18
|
+
ollama: 'nomic-embed-text'
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
13
21
|
OPENAI_URL = 'https://api.openai.com/v1/embeddings'
|
|
22
|
+
GOOGLE_BASE = 'https://generativelanguage.googleapis.com/v1beta'
|
|
23
|
+
OLLAMA_DEFAULT_URL = 'http://localhost:11434'
|
|
24
|
+
|
|
25
|
+
attr_reader :provider, :model
|
|
26
|
+
|
|
27
|
+
def initialize(provider: :openai, model: nil, api_key: nil, config: RCrewAI.configuration)
|
|
28
|
+
@provider = provider.to_sym
|
|
29
|
+
if @provider == :anthropic
|
|
30
|
+
raise EmbeddingError,
|
|
31
|
+
'anthropic does not provide an embeddings API; use :openai, :azure, :google, or :ollama'
|
|
32
|
+
end
|
|
14
33
|
|
|
15
|
-
|
|
16
|
-
@model = model
|
|
17
|
-
@api_key = api_key
|
|
34
|
+
@config = config
|
|
35
|
+
@model = model || DEFAULT_MODELS[@provider] || DEFAULT_MODELS[:openai]
|
|
36
|
+
@api_key = api_key
|
|
18
37
|
end
|
|
19
38
|
|
|
20
39
|
def embed(texts)
|
|
21
40
|
texts = Array(texts)
|
|
22
41
|
return [] if texts.empty?
|
|
23
42
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
43
|
+
send("embed_#{@provider}", texts)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def embed_openai(texts)
|
|
49
|
+
body = post_json(OPENAI_URL, { model: @model, input: texts },
|
|
50
|
+
'Authorization' => "Bearer #{api_key_for(:openai)}")
|
|
51
|
+
body['data'].map { |d| d['embedding'] }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def embed_azure(texts)
|
|
55
|
+
base = @config.base_url
|
|
56
|
+
version = @config.api_version || '2024-02-01'
|
|
57
|
+
deployment = @config.deployment_name || @model
|
|
58
|
+
url = "#{base}/openai/deployments/#{deployment}/embeddings?api-version=#{version}"
|
|
59
|
+
body = post_json(url, { input: texts }, 'api-key' => api_key_for(:azure))
|
|
60
|
+
body['data'].map { |d| d['embedding'] }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def embed_google(texts)
|
|
64
|
+
key = api_key_for(:google)
|
|
65
|
+
texts.map do |text|
|
|
66
|
+
url = "#{GOOGLE_BASE}/models/#{@model}:embedContent?key=#{key}"
|
|
67
|
+
payload = { model: "models/#{@model}", content: { parts: [{ text: text }] } }
|
|
68
|
+
body = post_json(url, payload)
|
|
69
|
+
body.dig('embedding', 'values')
|
|
28
70
|
end
|
|
71
|
+
end
|
|
29
72
|
|
|
73
|
+
def embed_ollama(texts)
|
|
74
|
+
base = @config.base_url || OLLAMA_DEFAULT_URL
|
|
75
|
+
texts.map do |text|
|
|
76
|
+
body = post_json("#{base}/api/embeddings", { model: @model, prompt: text })
|
|
77
|
+
body['embedding']
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def post_json(url, payload, headers = {})
|
|
82
|
+
response = connection.post(url) do |req|
|
|
83
|
+
req.headers['Content-Type'] = 'application/json'
|
|
84
|
+
headers.each { |k, v| req.headers[k] = v }
|
|
85
|
+
req.body = JSON.generate(payload)
|
|
86
|
+
end
|
|
30
87
|
raise EmbeddingError, "embedding request failed: #{response.status}" unless response.success?
|
|
31
88
|
|
|
32
89
|
body = response.body
|
|
33
|
-
body
|
|
34
|
-
body['data'].map { |d| d['embedding'] }
|
|
90
|
+
body.is_a?(String) ? JSON.parse(body) : body
|
|
35
91
|
end
|
|
36
92
|
|
|
37
|
-
|
|
93
|
+
def api_key_for(provider)
|
|
94
|
+
return @api_key if @api_key
|
|
95
|
+
|
|
96
|
+
case provider
|
|
97
|
+
when :openai then @config.openai_api_key || @config.api_key
|
|
98
|
+
when :azure then @config.azure_api_key || @config.api_key
|
|
99
|
+
when :google then @config.google_api_key || @config.api_key
|
|
100
|
+
end
|
|
101
|
+
end
|
|
38
102
|
|
|
39
103
|
def connection
|
|
40
104
|
@connection ||= Faraday.new do |f|
|
|
@@ -11,8 +11,9 @@ module RCrewAI
|
|
|
11
11
|
# Skip sentence-initial common words that happen to be capitalized.
|
|
12
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
13
|
|
|
14
|
-
def initialize(scope:, embedder: nil, store: nil, limit: nil)
|
|
15
|
-
super
|
|
14
|
+
def initialize(scope:, embedder: nil, store: nil, limit: nil, extractor: nil)
|
|
15
|
+
super(scope: scope, embedder: embedder, store: store, limit: limit)
|
|
16
|
+
@extractor = extractor
|
|
16
17
|
@entities = []
|
|
17
18
|
end
|
|
18
19
|
|
|
@@ -39,7 +40,21 @@ module RCrewAI
|
|
|
39
40
|
|
|
40
41
|
private
|
|
41
42
|
|
|
43
|
+
# Uses the custom extractor when provided, falling back to the heuristic
|
|
44
|
+
# if it's absent, returns nothing, or raises.
|
|
42
45
|
def extract_entities(text)
|
|
46
|
+
if @extractor
|
|
47
|
+
begin
|
|
48
|
+
result = Array(@extractor.call(text))
|
|
49
|
+
return result unless result.empty?
|
|
50
|
+
rescue StandardError
|
|
51
|
+
# fall through to heuristic
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
heuristic_entities(text)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def heuristic_entities(text)
|
|
43
58
|
text.to_s.scan(/\b([A-Z][a-zA-Z0-9]{1,})\b/).flatten.reject { |w| COMMON.include?(w) }.uniq
|
|
44
59
|
end
|
|
45
60
|
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../output_schema'
|
|
4
|
+
|
|
5
|
+
module RCrewAI
|
|
6
|
+
class Memory
|
|
7
|
+
# Extracts named entities from text with an LLM. Pass an instance as the
|
|
8
|
+
# `extractor:` for EntityMemory. Responds to `call(text) -> [names]`.
|
|
9
|
+
# Returns [] on any parse/LLM failure so EntityMemory falls back to the
|
|
10
|
+
# heuristic extractor — extraction is best-effort and never fatal.
|
|
11
|
+
class LlmEntityExtractor
|
|
12
|
+
PROMPT = <<~PROMPT
|
|
13
|
+
Extract the named entities (people, organizations, systems, products,
|
|
14
|
+
places, and key concepts) from the text below. Respond ONLY with a JSON
|
|
15
|
+
array of strings, e.g. ["Alice", "Payments", "Redis"]. No prose.
|
|
16
|
+
|
|
17
|
+
Text:
|
|
18
|
+
PROMPT
|
|
19
|
+
|
|
20
|
+
def initialize(llm)
|
|
21
|
+
@llm = llm
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def call(text)
|
|
25
|
+
response = @llm.chat(messages: [{ role: 'user', content: "#{PROMPT}#{text}" }])
|
|
26
|
+
content = response.is_a?(Hash) ? response[:content] : response
|
|
27
|
+
parsed = OutputSchema.parse(content.to_s)
|
|
28
|
+
parsed.is_a?(Array) ? parsed.map(&:to_s) : []
|
|
29
|
+
rescue StandardError
|
|
30
|
+
[]
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -15,11 +15,14 @@ module RCrewAI
|
|
|
15
15
|
class SqliteStore
|
|
16
16
|
DEFAULT_PATH = File.join(Dir.home, '.rcrewai', 'memory.db')
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
# max_candidates bounds how many (most-recent) rows a search cosines, so
|
|
19
|
+
# recall cost stays constant as total memory grows. nil = consider all.
|
|
20
|
+
def initialize(path: DEFAULT_PATH, max_candidates: 1000)
|
|
19
21
|
require 'sqlite3'
|
|
20
22
|
ensure_parent_dir(path) unless path == ':memory:'
|
|
21
23
|
@db = SQLite3::Database.new(path)
|
|
22
24
|
@db.results_as_hash = true
|
|
25
|
+
@max_candidates = max_candidates
|
|
23
26
|
create_schema
|
|
24
27
|
end
|
|
25
28
|
|
|
@@ -37,7 +40,7 @@ module RCrewAI
|
|
|
37
40
|
end
|
|
38
41
|
|
|
39
42
|
def search(vector, k:, scope:)
|
|
40
|
-
|
|
43
|
+
candidates(scope)
|
|
41
44
|
.reject { |r| r[:vector].nil? }
|
|
42
45
|
.map { |r| [r, Similarity.cosine(vector, r[:vector])] }
|
|
43
46
|
.sort_by { |(_r, score)| -score }
|
|
@@ -55,6 +58,18 @@ module RCrewAI
|
|
|
55
58
|
|
|
56
59
|
private
|
|
57
60
|
|
|
61
|
+
# The rows a search will consider: the most-recent @max_candidates within
|
|
62
|
+
# the scope (rowid tracks insertion order). Bounds cosine cost at scale.
|
|
63
|
+
def candidates(scope)
|
|
64
|
+
sql = 'SELECT * FROM memories WHERE scope = ? ORDER BY rowid DESC'
|
|
65
|
+
params = [scope]
|
|
66
|
+
if @max_candidates
|
|
67
|
+
sql += ' LIMIT ?'
|
|
68
|
+
params << @max_candidates
|
|
69
|
+
end
|
|
70
|
+
@db.execute(sql, params).map { |row| to_record(row) }
|
|
71
|
+
end
|
|
72
|
+
|
|
58
73
|
def create_schema
|
|
59
74
|
@db.execute(<<~SQL)
|
|
60
75
|
CREATE TABLE IF NOT EXISTS memories (
|
data/lib/rcrewai/memory.rb
CHANGED
|
@@ -12,10 +12,10 @@ module RCrewAI
|
|
|
12
12
|
# available, falls back to lexical similarity — so existing code behaves as
|
|
13
13
|
# before, just with better recall once an embedder is configured.
|
|
14
14
|
class Memory
|
|
15
|
-
def initialize(scope: 'default', embedder: nil, store: nil, short_term_limit: 100)
|
|
15
|
+
def initialize(scope: 'default', embedder: nil, store: nil, short_term_limit: 100, entity_extractor: nil)
|
|
16
16
|
@short_term = ShortTermMemory.new(scope: scope, embedder: embedder, store: store, limit: short_term_limit)
|
|
17
17
|
@long_term = LongTermMemory.new(scope: scope, embedder: embedder, store: store)
|
|
18
|
-
@entity = EntityMemory.new(scope: scope, embedder: embedder, store: store)
|
|
18
|
+
@entity = EntityMemory.new(scope: scope, embedder: embedder, store: store, extractor: entity_extractor)
|
|
19
19
|
@tool = ToolMemory.new(scope: scope, embedder: embedder, store: store)
|
|
20
20
|
end
|
|
21
21
|
|
data/lib/rcrewai/version.rb
CHANGED
data/lib/rcrewai.rb
CHANGED
|
@@ -30,6 +30,7 @@ require_relative 'rcrewai/memory/base_memory'
|
|
|
30
30
|
require_relative 'rcrewai/memory/short_term_memory'
|
|
31
31
|
require_relative 'rcrewai/memory/long_term_memory'
|
|
32
32
|
require_relative 'rcrewai/memory/entity_memory'
|
|
33
|
+
require_relative 'rcrewai/memory/llm_entity_extractor'
|
|
33
34
|
require_relative 'rcrewai/memory/tool_memory'
|
|
34
35
|
require_relative 'rcrewai/rate_limiter'
|
|
35
36
|
require_relative 'rcrewai/context_window'
|
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.6.
|
|
4
|
+
version: 0.6.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- gkosmo
|
|
@@ -413,6 +413,7 @@ files:
|
|
|
413
413
|
- lib/rcrewai/memory/base_memory.rb
|
|
414
414
|
- lib/rcrewai/memory/entity_memory.rb
|
|
415
415
|
- lib/rcrewai/memory/in_memory_store.rb
|
|
416
|
+
- lib/rcrewai/memory/llm_entity_extractor.rb
|
|
416
417
|
- lib/rcrewai/memory/long_term_memory.rb
|
|
417
418
|
- lib/rcrewai/memory/short_term_memory.rb
|
|
418
419
|
- lib/rcrewai/memory/sqlite_store.rb
|