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 +4 -4
- data/CHANGELOG.md +22 -1
- data/README.md +24 -3
- data/docs/superpowers/specs/2026-07-07-consensual-process-design.md +82 -0
- data/docs/upgrading-to-0.7.md +36 -0
- data/examples/consensual_process_example.rb +63 -0
- data/lib/rcrewai/agent.rb +2 -1
- data/lib/rcrewai/crew.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/process.rb +86 -18
- data/lib/rcrewai/version.rb +1 -1
- data/lib/rcrewai.rb +1 -0
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '0024080cd05b151b653bcedb4b29afea563e149f9d68dce1662d636a919020a9'
|
|
4
|
+
data.tar.gz: 53bb6b1376f88d1379cb0f877eeaa35aed0822f08df5f76d482e28d55101efe6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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
|
|
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
|
|
|
@@ -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),
|
|
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.
|
|
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/process.rb
CHANGED
|
@@ -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
|
|
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
|
-
|
|
401
|
-
|
|
393
|
+
@logger.info "Consensus for task: #{task.name}"
|
|
394
|
+
participants = select_participants(task)
|
|
395
|
+
candidates = gather_proposals(task, participants)
|
|
402
396
|
|
|
403
|
-
|
|
404
|
-
|
|
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
|
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.
|
|
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
|