rcrewai 0.6.0 → 0.7.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fb90e26000ddf4a2907b06150fdc87b1707fc701c6c61ba35db33339ed6f38d4
4
- data.tar.gz: ba3435eb1918be328ce65faf2c3000553833b09778eb3b06f2a6191cae31aab4
3
+ metadata.gz: '0024080cd05b151b653bcedb4b29afea563e149f9d68dce1662d636a919020a9'
4
+ data.tar.gz: 53bb6b1376f88d1379cb0f877eeaa35aed0822f08df5f76d482e28d55101efe6
5
5
  SHA512:
6
- metadata.gz: 992ac3d7368cb7690f11294b78fe876fcea29d48cffd001a355471a351c22c321563fbd390a0688951be6bcbd0f177322509e59cdd19cf8073e228784d96b503
7
- data.tar.gz: 92e7db7609d4503693396f85735fd48633cb5204a34d2431e57ffcb7c758e693bff5b1282ed716899f4242bd9a26b0483f278c991dedb13b21d05894a59b7105
6
+ metadata.gz: b7923adbf161caa5361568268306513c92e5ffbddc3736cd91cd0fbccedd8cdd4230854f8310cf358964c7ccea09b1e43040e944f4c5472b0f5e58983bf37aaf
7
+ data.tar.gz: e4c9426c4e948fca50c3708a8a0e57588b4b18c51a5ac8df0bd2dd902aa2440a20d8d1f6a464333bb9c3244d5ff6901070645bb932fd5fb6e77f3d3f82ff9f83
data/CHANGELOG.md CHANGED
@@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.7.0] - 2026-07-07
11
+
12
+ Turns the `:consensual` crew process from a stub into a real multi-agent
13
+ consensus (propose → vote → pick). This is a behavior change to an existing
14
+ process type — see `docs/upgrading-to-0.7.md`.
15
+
16
+ ### Changed
17
+ - The `:consensual` crew process now performs real multi-agent consensus instead of silently running sequentially. For each task, up to `consensus_agents` agents (default 3) propose candidate answers, all participants score each candidate 0–10, and the highest-scored candidate wins (ties break toward the task's assigned agent). Configure via `Crew.new(process: :consensual, consensus_agents: N)`. A proposer that errors is dropped; if all proposals fail the task is marked failed. **Behavior change:** code relying on `:consensual`'s previous (accidental) sequential behavior should switch to `process: :sequential`. See `docs/upgrading-to-0.7.md`.
18
+
19
+ ## [0.6.1] - 2026-07-06
20
+
21
+ Follow-ups to the 0.6.0 Cognitive Memory release, deepening the areas that
22
+ shipped as focused first cuts. All additive and backward compatible.
23
+
24
+ ### Added
25
+ - 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.
26
+ - 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: ... })`.
27
+ - 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.
28
+
10
29
  ## [0.6.0] - 2026-07-06
11
30
 
12
31
  Replaces the placeholder agent memory with a **cognitive memory** system —
@@ -198,7 +217,9 @@ output, guardrails, planning, and training/testing. See `ROADMAP.md`.
198
217
  - CLI usage documentation
199
218
  - Real-world use cases and examples
200
219
 
201
- [Unreleased]: https://github.com/gkosmo/rcrewAI/compare/v0.6.0...HEAD
220
+ [Unreleased]: https://github.com/gkosmo/rcrewAI/compare/v0.7.0...HEAD
221
+ [0.7.0]: https://github.com/gkosmo/rcrewAI/compare/v0.6.1...v0.7.0
222
+ [0.6.1]: https://github.com/gkosmo/rcrewAI/compare/v0.6.0...v0.6.1
202
223
  [0.6.0]: https://github.com/gkosmo/rcrewAI/compare/v0.5.0...v0.6.0
203
224
  [0.5.0]: https://github.com/gkosmo/rcrewAI/compare/v0.4.0...v0.5.0
204
225
  [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
 
@@ -444,6 +445,26 @@ flow.state.id # => automatic UUID
444
445
  or your own `#save`/`#load`) and call `flow.restore(id)` to resume.
445
446
  - Invoke a `Crew` inside any step, or pause with `human_feedback('Approve?')`.
446
447
 
448
+ ## 🗳️ Consensual Process
449
+
450
+ For decisions where multiple perspectives matter, the `:consensual` process has
451
+ several agents propose competing answers and vote to pick the best:
452
+
453
+ ```ruby
454
+ crew = RCrewAI::Crew.new('panel', process: :consensual, consensus_agents: 3)
455
+ crew.add_agent(junior)
456
+ crew.add_agent(senior)
457
+ crew.add_task(task)
458
+
459
+ crew.execute # each task: agents propose → all score 0–10 → highest wins
460
+ ```
461
+
462
+ For every task, up to `consensus_agents` agents (default 3) propose a candidate
463
+ answer, all participants score each candidate, and the highest-scored candidate
464
+ becomes the result (ties break toward the task's assigned agent). A proposer that
465
+ errors is dropped; if all proposals fail the task is marked failed. Consensus
466
+ multiplies LLM calls, so `consensus_agents` bounds the cost on larger crews.
467
+
447
468
  ## 💡 Examples
448
469
 
449
470
  ### Hierarchical Team with Human Oversight
@@ -0,0 +1,82 @@
1
+ # Consensual Process: propose → vote → pick
2
+
3
+ **Date:** 2026-07-07
4
+ **Status:** Approved design (pending implementation)
5
+ **Target version:** `rcrewai` 0.7.0 (current is 0.6.1)
6
+ **Scope:** Replace the stubbed `Process::Consensual` (which silently runs tasks sequentially) with a real multi-agent consensus: agents propose candidate answers, score each other's candidates, and the highest-scored candidate wins.
7
+
8
+ ---
9
+
10
+ ## Motivation
11
+
12
+ `Crew.new('c', process: :consensual)` is a public, validated, documented process type. But `Process::Consensual#execute` currently just runs each task once (`execute_with_consensus` calls `task.execute` with a "for now, just execute normally" comment). A user selecting `:consensual` silently gets sequential behavior — the feature is advertised but not implemented. This is a latent honesty gap: fix the code to do what its name and comments promise ("agent voting/discussion").
13
+
14
+ ## Behavior
15
+
16
+ For each task in the crew, instead of a single execution:
17
+
18
+ 1. **Propose** — up to `consensus_agents` agents (default 3, capped from the crew) each independently produce a candidate answer for the task.
19
+ 2. **Vote** — each participating agent scores every candidate 0–10 for how well it satisfies the task's description and expected output.
20
+ 3. **Pick** — the candidate with the highest total score wins. Ties break toward the task's assigned agent's candidate (or the first proposer if the assigned agent didn't propose).
21
+
22
+ ## Components
23
+
24
+ All within `Process::Consensual`, replacing the stub. Result shape is unchanged
25
+ (`{ task:, result:, status: }`) so `Crew#format_execution_results` needs no changes.
26
+
27
+ - `execute` — iterate tasks, call `execute_with_consensus(task)`, log, return results.
28
+ - `select_participants(task)` — first N agents (`consensus_agents`), always including the task's assigned agent if present and not already in the first N.
29
+ - `gather_proposals(task, participants)` — each agent runs the task; returns `[{ agent:, content: }]`. A proposer that raises is dropped (logged), not fatal.
30
+ - `score_candidates(task, candidates, participants)` — each participant scores each candidate via an LLM call; returns per-candidate total. Non-numeric / failed scores count as 0.
31
+ - `pick_winner(task, scored)` — highest total; deterministic tie-break toward the task's assigned agent, else first proposer.
32
+
33
+ ## Configuration
34
+
35
+ ```ruby
36
+ Crew.new('c', process: :consensual, consensus_agents: 3)
37
+ ```
38
+
39
+ `consensus_agents` (default 3) caps how many agents propose and vote, bounding
40
+ cost regardless of crew size. Read by the process from the crew.
41
+
42
+ ## Cost
43
+
44
+ Bounded per task: N proposals + N×N scoring calls, N defaulting to 3
45
+ (≈ 3 + 9 = 12 LLM calls/task worst case), independent of crew size.
46
+
47
+ ## Edge cases
48
+
49
+ - **1 participant** → a single proposal, no meaningful vote; returns a valid result.
50
+ - **A proposer errors** → that candidate is dropped; consensus continues with the rest.
51
+ - **All proposals fail** → task marked `:failed` with the error, matching Sequential's rescue behavior.
52
+ - **No agents in crew** → task `:failed` (nothing can propose).
53
+
54
+ ## Scoring mechanism
55
+
56
+ Each participant is prompted: given the task (description + expected output) and a
57
+ candidate answer, return a single integer 0–10. The response is parsed with a
58
+ tolerant integer extraction (first integer in the text; clamp to 0–10; default 0
59
+ on failure). Scores are summed across participants per candidate.
60
+
61
+ ## Backward compatibility
62
+
63
+ `:consensual` already exists and validates. This changes only runtime behavior
64
+ (from "silently sequential" to "actual consensus"). The async consensual path
65
+ (`Crew#execute_consensual_async`, parallel aggregation) is intentionally left
66
+ unchanged for this pass and noted in docs; expanding it is a possible follow-up.
67
+
68
+ ## Testing (TDD)
69
+
70
+ Fake agents returning canned proposals and scores (no live LLM) prove:
71
+ - highest-scored candidate wins
72
+ - deterministic tie-break toward the task's assigned agent
73
+ - `consensus_agents` cap respected on a larger crew
74
+ - single-agent degradation returns a valid result
75
+ - a proposer raising is dropped, consensus still completes
76
+ - all-proposals-fail marks the task `:failed`
77
+
78
+ ## Rollout
79
+
80
+ Ship in `0.7.0` (behavior change to an existing feature warrants a minor bump).
81
+ `docs/upgrading-to-0.7.md` notes the behavior change; README documents the
82
+ consensus flow and `consensus_agents`.
@@ -0,0 +1,36 @@
1
+ # Upgrading to RCrewAI 0.7
2
+
3
+ RCrewAI 0.7 makes the `:consensual` crew process do what its name promises.
4
+
5
+ ## Behavior change: `:consensual` process
6
+
7
+ Previously, `Crew.new('c', process: :consensual)` was a stub — it silently ran
8
+ tasks **sequentially**. As of 0.7 it performs real multi-agent consensus:
9
+
10
+ 1. **Propose** — up to `consensus_agents` agents (default 3) each produce a
11
+ candidate answer for the task.
12
+ 2. **Vote** — every participant scores each candidate 0–10.
13
+ 3. **Pick** — the highest-scored candidate wins (ties break toward the task's
14
+ assigned agent).
15
+
16
+ ### What you must do
17
+
18
+ Nothing to keep your code running — the API is unchanged. But be aware:
19
+
20
+ - If you were using `process: :consensual` and relying on its (accidental)
21
+ sequential behavior, switch to `process: :sequential` explicitly.
22
+ - Consensus multiplies LLM calls (≈ N proposals + N×N scoring per task, N=3 by
23
+ default). Tune or bound it with `consensus_agents:`.
24
+
25
+ ```ruby
26
+ crew = RCrewAI::Crew.new('panel', process: :consensual, consensus_agents: 3)
27
+ ```
28
+
29
+ ### Edge cases
30
+
31
+ - One agent → a single proposal (no real vote), still a valid result.
32
+ - A proposer that errors is dropped; consensus continues with the rest.
33
+ - If every proposal fails, the task is marked failed.
34
+
35
+ The async consensual path (`crew.execute(async: true)` with `:consensual`) is
36
+ unchanged in this release (parallel aggregation).
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Consensual process — agents propose competing answers and vote to pick one.
5
+ #
6
+ # For each task: up to `consensus_agents` agents each produce a candidate
7
+ # answer, every participant scores each candidate 0-10, and the highest-scored
8
+ # candidate wins (ties break toward the task's assigned agent).
9
+ #
10
+ # This example stubs the agents so it runs WITHOUT an API key. In real use each
11
+ # agent calls your configured LLM to propose and to score.
12
+ #
13
+ # Run:
14
+ # ruby examples/consensual_process_example.rb
15
+
16
+ require_relative '../lib/rcrewai'
17
+
18
+ RCrewAI.configure(validate: false) do |c|
19
+ c.llm_provider = :openai
20
+ c.api_key = 'demo-key'
21
+ end
22
+
23
+ # A stand-in agent: proposes a fixed answer, and scores candidates by a simple
24
+ # rubric (here: answers mentioning "trade-offs" are judged higher). A real
25
+ # Agent proposes via execute_task and scores via its llm_client.
26
+ class PanelAgent
27
+ attr_reader :name
28
+
29
+ def initialize(name, answer)
30
+ @name = name
31
+ @answer = answer
32
+ end
33
+
34
+ def execute_task(_task)
35
+ { content: @answer }
36
+ end
37
+
38
+ def llm_client
39
+ self
40
+ end
41
+
42
+ def chat(messages:, **)
43
+ candidate = messages.first[:content]
44
+ score = candidate.include?('trade-offs') ? 9 : 5
45
+ { content: score.to_s }
46
+ end
47
+ end
48
+
49
+ junior = PanelAgent.new('junior', 'Use PostgreSQL.')
50
+ senior = PanelAgent.new('senior', 'Use PostgreSQL, but weigh the trade-offs vs. DynamoDB for scale.')
51
+
52
+ crew = RCrewAI::Crew.new('architecture_panel', process: :consensual, consensus_agents: 3)
53
+ crew.add_agent(junior)
54
+ crew.add_agent(senior)
55
+
56
+ task = RCrewAI::Task.new(name: 'db_choice', description: 'Recommend a database', agent: junior)
57
+ crew.add_task(task)
58
+
59
+ result = crew.execute
60
+
61
+ puts "process: #{result[:process]}"
62
+ puts "consensus winner: #{result[:results].first[:result].inspect}"
63
+ puts '(the answer weighing trade-offs scored higher and won)'
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
data/lib/rcrewai/crew.rb CHANGED
@@ -22,6 +22,7 @@ module RCrewAI
22
22
  @planning = options.fetch(:planning, false)
23
23
  @planning_llm = options[:planning_llm]
24
24
  @planned = false
25
+ @consensus_agents = options.fetch(:consensus_agents, 3)
25
26
  @knowledge = build_knowledge(options[:knowledge], options[:knowledge_sources])
26
27
  @before_kickoff_hooks = []
27
28
  @after_kickoff_hooks = []
@@ -30,7 +31,7 @@ module RCrewAI
30
31
  validate_process_type!
31
32
  end
32
33
 
33
- attr_reader :knowledge, :stream_sink, :last_inputs
34
+ attr_reader :knowledge, :stream_sink, :last_inputs, :consensus_agents
34
35
 
35
36
  def planning?
36
37
  @planning
@@ -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
 
@@ -373,23 +373,16 @@ module RCrewAI
373
373
  end
374
374
  end
375
375
 
376
+ # Multi-agent consensus: for each task, several agents propose candidate
377
+ # answers, all participants score each candidate, and the highest-scored
378
+ # candidate wins (ties break toward the task's assigned agent).
376
379
  class Consensual < Base
380
+ DEFAULT_CONSENSUS_AGENTS = 3
381
+
377
382
  def execute
378
383
  log_execution_start
379
- @logger.info 'Consensual execution - agents collaborate on decisions'
380
-
381
- # For now, implement as enhanced sequential with collaboration
382
- # Full consensual process would involve agent voting/discussion
383
- results = []
384
-
385
- crew.tasks.each do |task|
386
- @logger.info "Collaborative execution of task: #{task.name}"
387
-
388
- # Simple consensus: let multiple agents provide input
389
- consensus_result = execute_with_consensus(task)
390
- results << consensus_result
391
- end
392
-
384
+ @logger.info 'Consensual execution - agents propose, vote, and pick'
385
+ results = crew.tasks.map { |task| execute_with_consensus(task) }
393
386
  log_execution_end(results)
394
387
  results
395
388
  end
@@ -397,14 +390,89 @@ module RCrewAI
397
390
  private
398
391
 
399
392
  def execute_with_consensus(task)
400
- # For now, just execute normally
401
- # Future: implement actual consensus mechanisms
393
+ @logger.info "Consensus for task: #{task.name}"
394
+ participants = select_participants(task)
395
+ candidates = gather_proposals(task, participants)
402
396
 
403
- result = task.execute
404
- { task: task, result: result, status: :completed }
397
+ if candidates.empty?
398
+ return { task: task, result: 'All agents failed to produce a proposal', status: :failed }
399
+ end
400
+
401
+ scored = score_candidates(task, candidates, participants)
402
+ winner = pick_winner(task, scored)
403
+ task.result = winner[:content] if task.respond_to?(:result=)
404
+
405
+ { task: task, result: winner[:content], status: :completed }
405
406
  rescue StandardError => e
407
+ @logger.error "Consensus failed for #{task.name}: #{e.message}"
406
408
  { task: task, result: e.message, status: :failed }
407
409
  end
410
+
411
+ # First N agents, always including the task's assigned agent if present.
412
+ def select_participants(task)
413
+ cap = consensus_agent_cap
414
+ chosen = crew.agents.first(cap)
415
+ assigned = task.respond_to?(:agent) ? task.agent : nil
416
+ if assigned && crew.agents.include?(assigned) && !chosen.include?(assigned)
417
+ chosen = ([assigned] + chosen).first(cap)
418
+ end
419
+ chosen
420
+ end
421
+
422
+ def gather_proposals(task, participants)
423
+ participants.filter_map do |agent|
424
+ content = extract_content(agent.execute_task(task))
425
+ { agent: agent, content: content }
426
+ rescue StandardError => e
427
+ @logger.warn "Agent #{agent.name} failed to propose: #{e.message}"
428
+ nil
429
+ end
430
+ end
431
+
432
+ def score_candidates(task, candidates, participants)
433
+ candidates.map do |candidate|
434
+ total = participants.sum { |voter| score(voter, task, candidate[:content]) }
435
+ candidate.merge(score: total)
436
+ end
437
+ end
438
+
439
+ def score(voter, task, candidate_content)
440
+ prompt = <<~PROMPT
441
+ Score how well the following answer satisfies the task, from 0 to 10.
442
+ Reply with just the integer.
443
+
444
+ Task: #{task.description}
445
+ Expected output: #{task.respond_to?(:expected_output) ? task.expected_output : 'n/a'}
446
+
447
+ Answer:
448
+ #{candidate_content}
449
+ PROMPT
450
+ response = voter.llm_client.chat(messages: [{ role: 'user', content: prompt }])
451
+ parse_score(response.is_a?(Hash) ? response[:content] : response)
452
+ rescue StandardError
453
+ 0
454
+ end
455
+
456
+ def pick_winner(task, scored)
457
+ assigned = task.respond_to?(:agent) ? task.agent : nil
458
+ scored.max_by { |c| [c[:score], c[:agent] == assigned ? 1 : 0] }
459
+ end
460
+
461
+ def parse_score(text)
462
+ match = text.to_s[/-?\d+/]
463
+ return 0 unless match
464
+
465
+ match.to_i.clamp(0, 10)
466
+ end
467
+
468
+ def extract_content(agent_result)
469
+ agent_result.is_a?(Hash) ? agent_result[:content].to_s : agent_result.to_s
470
+ end
471
+
472
+ def consensus_agent_cap
473
+ cap = crew.respond_to?(:consensus_agents) ? crew.consensus_agents : nil
474
+ [(cap || DEFAULT_CONSENSUS_AGENTS).to_i, 1].max
475
+ end
408
476
  end
409
477
 
410
478
  class ProcessError < RCrewAI::Error; end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RCrewAI
4
- VERSION = '0.6.0'
4
+ VERSION = '0.7.0'
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.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - gkosmo
@@ -357,6 +357,7 @@ files:
357
357
  - docs/superpowers/plans/2026-05-11-llm-modernization.md
358
358
  - docs/superpowers/specs/2026-05-11-llm-modernization-design.md
359
359
  - docs/superpowers/specs/2026-07-06-cognitive-memory-design.md
360
+ - docs/superpowers/specs/2026-07-07-consensual-process-design.md
360
361
  - docs/tutorials/advanced-agents.md
361
362
  - docs/tutorials/custom-tools.md
362
363
  - docs/tutorials/deployment.md
@@ -366,8 +367,10 @@ files:
366
367
  - docs/upgrading-to-0.3.md
367
368
  - docs/upgrading-to-0.4.md
368
369
  - docs/upgrading-to-0.6.md
370
+ - docs/upgrading-to-0.7.md
369
371
  - examples/async_execution_example.rb
370
372
  - examples/cognitive_memory_example.rb
373
+ - examples/consensual_process_example.rb
371
374
  - examples/flow_example.rb
372
375
  - examples/hierarchical_crew_example.rb
373
376
  - examples/human_in_the_loop_example.rb
@@ -413,6 +416,7 @@ files:
413
416
  - lib/rcrewai/memory/base_memory.rb
414
417
  - lib/rcrewai/memory/entity_memory.rb
415
418
  - lib/rcrewai/memory/in_memory_store.rb
419
+ - lib/rcrewai/memory/llm_entity_extractor.rb
416
420
  - lib/rcrewai/memory/long_term_memory.rb
417
421
  - lib/rcrewai/memory/short_term_memory.rb
418
422
  - lib/rcrewai/memory/sqlite_store.rb