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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fb90e26000ddf4a2907b06150fdc87b1707fc701c6c61ba35db33339ed6f38d4
4
- data.tar.gz: ba3435eb1918be328ce65faf2c3000553833b09778eb3b06f2a6191cae31aab4
3
+ metadata.gz: 32edcb190b0d85db67d404b260fbb86d9b2821d6a89adf350d792b60a0e21a06
4
+ data.tar.gz: 46d061ecebca5a67714a6a8139d41bf9d20ce60d4eb5f0a1505d965defffaeb8
5
5
  SHA512:
6
- metadata.gz: 992ac3d7368cb7690f11294b78fe876fcea29d48cffd001a355471a351c22c321563fbd390a0688951be6bcbd0f177322509e59cdd19cf8073e228784d96b503
7
- data.tar.gz: 92e7db7609d4503693396f85735fd48633cb5204a34d2431e57ffcb7c758e693bff5b1282ed716899f4242bd9a26b0483f278c991dedb13b21d05894a59b7105
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.0...HEAD
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`; pass a custom
374
- `embedder:` (anything responding to `embed(texts)`) or vector store to swap the
375
- backend.
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), **opts.slice(:embedder, :store, :short_term_limit))
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. Defaults to OpenAI's embeddings API;
9
- # #embed takes an array of strings and returns an array of vectors. Any
10
- # object responding to #embed can be substituted (see specs).
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
- DEFAULT_MODEL = 'text-embedding-3-small'
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
- def initialize(model: DEFAULT_MODEL, api_key: nil, config: RCrewAI.configuration)
16
- @model = model
17
- @api_key = api_key || config.openai_api_key || config.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
- response = connection.post(OPENAI_URL) do |req|
25
- req.headers['Authorization'] = "Bearer #{@api_key}"
26
- req.headers['Content-Type'] = 'application/json'
27
- req.body = JSON.generate(model: @model, input: texts)
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 = JSON.parse(body) if body.is_a?(String)
34
- body['data'].map { |d| d['embedding'] }
90
+ body.is_a?(String) ? JSON.parse(body) : body
35
91
  end
36
92
 
37
- private
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
- def initialize(path: DEFAULT_PATH)
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
- all(scope: scope)
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 (
@@ -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
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RCrewAI
4
- VERSION = '0.6.0'
4
+ VERSION = '0.6.1'
5
5
  end
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.0
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