rcrewai 0.3.0 → 0.4.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/.rubocop.yml +20 -0
- data/CHANGELOG.md +40 -1
- data/README.md +168 -0
- data/ROADMAP.md +84 -0
- data/lib/rcrewai/agent.rb +36 -2
- data/lib/rcrewai/configuration.rb +20 -0
- data/lib/rcrewai/crew.rb +79 -1
- data/lib/rcrewai/flow/state.rb +47 -0
- data/lib/rcrewai/flow/state_store.rb +50 -0
- data/lib/rcrewai/flow.rb +243 -0
- data/lib/rcrewai/knowledge/base.rb +52 -0
- data/lib/rcrewai/knowledge/chunker.rb +31 -0
- data/lib/rcrewai/knowledge/embedder.rb +48 -0
- data/lib/rcrewai/knowledge/sources.rb +83 -0
- data/lib/rcrewai/knowledge/store.rb +58 -0
- data/lib/rcrewai/knowledge.rb +13 -0
- data/lib/rcrewai/llm_client.rb +23 -0
- data/lib/rcrewai/output_schema.rb +79 -0
- data/lib/rcrewai/planning.rb +65 -0
- data/lib/rcrewai/task.rb +89 -2
- data/lib/rcrewai/version.rb +1 -1
- data/lib/rcrewai.rb +2 -0
- metadata +13 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f742fcf06518c80cbf29191d0eff275830ee618c9b896c3d443258b06aa120ff
|
|
4
|
+
data.tar.gz: 5cc9c03182d0bd80d73d99edb08f2267252684f64a23da1626d2d61e0abe1683
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0c32e4110a5bbabac9b120262e2ebe0e86b0c58c3b4213a11214efcff647024d9714fcee12934f4b734c9f64184e1f802d82b97ec50e5dc7ceab424c4cec38f1
|
|
7
|
+
data.tar.gz: 8a678ae53008c7c6a73cf365b460b80989da05f22eb646cf3a9126028a0d6dd220e6af75ef4dee80c02d73e69b536fb3d2b7c23f827e929078b76ed450d8f1ce
|
data/.rubocop.yml
CHANGED
|
@@ -1 +1,21 @@
|
|
|
1
1
|
inherit_from: .rubocop_todo.yml
|
|
2
|
+
|
|
3
|
+
Naming/MethodParameterName:
|
|
4
|
+
# `k` (top-k retrieval) and short math vars for vector similarity are
|
|
5
|
+
# conventional and clearer than forced longer names. The rest are RuboCop's
|
|
6
|
+
# defaults, restated because AllowedNames replaces rather than extends.
|
|
7
|
+
AllowedNames:
|
|
8
|
+
- k
|
|
9
|
+
- a
|
|
10
|
+
- b
|
|
11
|
+
- io
|
|
12
|
+
- id
|
|
13
|
+
- to
|
|
14
|
+
- by
|
|
15
|
+
- 'on'
|
|
16
|
+
- in
|
|
17
|
+
- at
|
|
18
|
+
- ip
|
|
19
|
+
- db
|
|
20
|
+
- os
|
|
21
|
+
- pp
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,43 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.4.0] - 2026-07-03
|
|
11
|
+
|
|
12
|
+
This release closes the feature-parity gap with the modern CrewAI framework,
|
|
13
|
+
adding its second pillar (**Flows**) alongside **Knowledge (RAG)**, structured
|
|
14
|
+
output, guardrails, planning, and training/testing. See `ROADMAP.md`.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
#### Flows (#11)
|
|
19
|
+
- `RCrewAI::Flow` — an event-driven workflow engine (CrewAI's second pillar). Subclass it and declare methods with a class-level DSL: `start`, `listen`, `router`, and the `and_` / `or_` trigger combinators. `kickoff` runs the graph to a fixed point; routers emit labels that listeners trigger on.
|
|
20
|
+
- Flow state (`Flow::State`) is a schemaless object with an automatic UUID, seedable via `kickoff(inputs:)`.
|
|
21
|
+
- Flow persistence: pluggable state stores (`Flow::MemoryStateStore`, `Flow::FileStateStore`, or any `#save`/`#load` object); `flow.restore(id)` resumes a persisted run.
|
|
22
|
+
- Flows can invoke a `Crew` as a step and pause for input via `#human_feedback`.
|
|
23
|
+
|
|
24
|
+
#### Knowledge / RAG (#9)
|
|
25
|
+
- `RCrewAI::Knowledge` module adds retrieval-augmented context. Sources (`StringSource`, `FileSource`, `PdfSource`, `CsvSource`, `UrlSource`) are chunked, embedded, and stored in an in-memory cosine-similarity vector store (no external DB required).
|
|
26
|
+
- Attach via `Agent.new(knowledge:)` / `knowledge_sources:` (role-specific) or `Crew.new(knowledge:)` / `knowledge_sources:` (shared with all agents); relevant chunks are injected into each task's prompt at execution.
|
|
27
|
+
- The embedder (`Knowledge::Embedder`, default OpenAI `text-embedding-3-small`) and vector store are pluggable.
|
|
28
|
+
|
|
29
|
+
#### Task output processing (#6, #7, #8)
|
|
30
|
+
- Structured output: `Task.new(output_schema:)` validates and coerces the agent's output against a JSON-schema subset, exposing the parsed object via `Task#structured_output` (and the raw string via `Task#raw_result`). JSON embedded in surrounding prose or a fenced code block is extracted automatically; output that doesn't conform re-runs the agent with the error fed back.
|
|
31
|
+
- Guardrails: `Task.new(guardrail:)` takes a callable returning `[ok, value_or_error]` to validate and transform output before it flows downstream, retrying up to `guardrail_max_retries` (default 3) with the rejection reason fed back to the agent.
|
|
32
|
+
- Output persistence & formatting: `Task.new(output_file:)` writes the result to disk (`create_directory:` controls parent-dir creation, default true), and `markdown: true` prepends a heading when the output isn't already a markdown document.
|
|
33
|
+
- `RCrewAI::OutputSchema` — a small JSON-schema-subset validator/coercer used by structured task output.
|
|
34
|
+
|
|
35
|
+
#### Per-agent LLM (#5)
|
|
36
|
+
- `Agent.new(llm:)` accepts a provider symbol (`:anthropic`), an options hash (`{ provider:, model:, api_key:, temperature: }`), or a pre-built client instance. Agents in the same crew can use different providers/models (e.g. a cheap worker model and a stronger manager model). Omitting `llm:` keeps the previous global-configuration behavior.
|
|
37
|
+
- `Configuration#with_overrides` returns a copy of the configuration with per-agent overrides applied, leaving global state untouched.
|
|
38
|
+
|
|
39
|
+
#### Planning (#10)
|
|
40
|
+
- `Crew.new(planning: true)` runs a single planner pass before execution that asks an LLM to draft a short plan for each task and folds it into the task's description. Optional `planning_llm:` selects the planner client (defaults to the global provider). Best-effort — a planner error or unparseable output leaves tasks unchanged and execution proceeds.
|
|
41
|
+
- `Task#enrich_description` appends supplementary guidance (used by the planner) without discarding the original instructions.
|
|
42
|
+
|
|
43
|
+
#### Training & testing (#12)
|
|
44
|
+
- `Crew#train(n_iterations:, filename:)` runs the crew repeatedly, collects feedback after each iteration (via a `feedback:` callable, defaulting to a human prompt), and persists it as JSON.
|
|
45
|
+
- `Crew#test(n_iterations:)` runs the crew repeatedly and reports per-run and average scores (via a `scorer:` callable, defaulting to the run's success rate).
|
|
46
|
+
|
|
10
47
|
## [0.3.0] - 2026-05-12
|
|
11
48
|
|
|
12
49
|
### Added
|
|
@@ -128,5 +165,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
128
165
|
- CLI usage documentation
|
|
129
166
|
- Real-world use cases and examples
|
|
130
167
|
|
|
131
|
-
[Unreleased]: https://github.com/gkosmo/rcrewAI/compare/v0.
|
|
168
|
+
[Unreleased]: https://github.com/gkosmo/rcrewAI/compare/v0.4.0...HEAD
|
|
169
|
+
[0.4.0]: https://github.com/gkosmo/rcrewAI/compare/v0.3.0...v0.4.0
|
|
170
|
+
[0.3.0]: https://github.com/gkosmo/rcrewAI/compare/v0.1.0...v0.3.0
|
|
132
171
|
[0.1.0]: https://github.com/gkosmo/rcrewAI/releases/tag/v0.1.0
|
data/README.md
CHANGED
|
@@ -19,6 +19,8 @@ RCrewAI is a Ruby implementation of the CrewAI framework, allowing you to create
|
|
|
19
19
|
- **🏗️ Hierarchical Teams**: Manager agents that coordinate and delegate tasks to specialist agents
|
|
20
20
|
- **🔒 Production Ready**: Security controls, error handling, logging, monitoring, and sandboxing
|
|
21
21
|
- **🎯 Flexible Orchestration**: Sequential, hierarchical, and concurrent execution modes
|
|
22
|
+
- **🌊 Flows**: Event-driven workflows with `start`/`listen`/`router`, branching, and persistent state
|
|
23
|
+
- **📚 Knowledge (RAG)**: Ground agents in your own documents with built-in retrieval
|
|
22
24
|
- **💎 Ruby-First Design**: Built specifically for Ruby developers with idiomatic patterns
|
|
23
25
|
|
|
24
26
|
## 📦 Installation
|
|
@@ -169,6 +171,172 @@ RCrewAI.configure do |config|
|
|
|
169
171
|
end
|
|
170
172
|
```
|
|
171
173
|
|
|
174
|
+
### Per-agent LLM
|
|
175
|
+
|
|
176
|
+
The `RCrewAI.configure` block sets the crew-wide default. Any agent can override
|
|
177
|
+
it with the `llm:` option, so a single crew can mix providers and models — for
|
|
178
|
+
example a cheap model for workers and a stronger one for the manager:
|
|
179
|
+
|
|
180
|
+
```ruby
|
|
181
|
+
# Provider only (uses that provider's configured model + key)
|
|
182
|
+
researcher = RCrewAI::Agent.new(name: 'researcher', role: '...', goal: '...',
|
|
183
|
+
llm: :anthropic)
|
|
184
|
+
|
|
185
|
+
# Provider + model (and optionally api_key / temperature)
|
|
186
|
+
manager = RCrewAI::Agent.new(name: 'manager', role: '...', goal: '...',
|
|
187
|
+
llm: { provider: :anthropic, model: 'claude-3-opus-20240229' })
|
|
188
|
+
|
|
189
|
+
worker = RCrewAI::Agent.new(name: 'worker', role: '...', goal: '...',
|
|
190
|
+
llm: { provider: :openai, model: 'gpt-4o-mini' })
|
|
191
|
+
|
|
192
|
+
# Or pass a pre-built client instance
|
|
193
|
+
worker = RCrewAI::Agent.new(name: 'worker', role: '...', goal: '...',
|
|
194
|
+
llm: my_client)
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Omit `llm:` to use the global `RCrewAI.configure` settings. Overrides never
|
|
198
|
+
mutate the global configuration.
|
|
199
|
+
|
|
200
|
+
## 📤 Structured Output, Guardrails & File Output
|
|
201
|
+
|
|
202
|
+
Tasks can validate, transform, and persist their output:
|
|
203
|
+
|
|
204
|
+
```ruby
|
|
205
|
+
task = RCrewAI::Task.new(
|
|
206
|
+
name: 'extract',
|
|
207
|
+
description: 'Extract the article title and word count as JSON',
|
|
208
|
+
agent: analyst,
|
|
209
|
+
|
|
210
|
+
# Structured output — validated & coerced against a JSON schema.
|
|
211
|
+
# Non-conforming output re-runs the agent with the error fed back.
|
|
212
|
+
output_schema: {
|
|
213
|
+
type: 'object',
|
|
214
|
+
properties: { title: { type: 'string' }, words: { type: 'integer' } },
|
|
215
|
+
required: ['title']
|
|
216
|
+
},
|
|
217
|
+
|
|
218
|
+
# Guardrail — ->(output) { [ok, value_or_error] }. On rejection the agent
|
|
219
|
+
# re-runs (up to guardrail_max_retries) with the reason appended.
|
|
220
|
+
guardrail: ->(out) { [out.length < 5000, 'must be under 5000 chars'] },
|
|
221
|
+
guardrail_max_retries: 3,
|
|
222
|
+
|
|
223
|
+
# Persist the result. Parent dirs are created unless create_directory: false.
|
|
224
|
+
output_file: 'out/report.md',
|
|
225
|
+
markdown: true
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
task.execute
|
|
229
|
+
task.structured_output # => { "title" => "...", "words" => 1234 }
|
|
230
|
+
task.raw_result # => the unprocessed string the agent produced
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
## 🗺️ Planning
|
|
234
|
+
|
|
235
|
+
Enable `planning:` on a crew to run a planner pass before execution. The planner
|
|
236
|
+
drafts a short plan for each task and folds it into the task description, giving
|
|
237
|
+
the executing agent a head start:
|
|
238
|
+
|
|
239
|
+
```ruby
|
|
240
|
+
crew = RCrewAI::Crew.new('research_crew', planning: true)
|
|
241
|
+
# Optionally use a dedicated (e.g. stronger) planner model:
|
|
242
|
+
crew = RCrewAI::Crew.new('research_crew', planning: true,
|
|
243
|
+
planning_llm: { provider: :anthropic, model: 'claude-3-opus-20240229' })
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Planning is best-effort: if the planner errors or returns unparseable output,
|
|
247
|
+
the crew runs with the original tasks unchanged.
|
|
248
|
+
|
|
249
|
+
## 🏋️ Training & Testing
|
|
250
|
+
|
|
251
|
+
Iterate on a crew by training it with feedback or scoring repeated runs:
|
|
252
|
+
|
|
253
|
+
```ruby
|
|
254
|
+
# Train: run N times, collect feedback after each run, persist to JSON.
|
|
255
|
+
crew.train(n_iterations: 3, filename: 'training.json')
|
|
256
|
+
|
|
257
|
+
# Provide feedback programmatically instead of prompting a human:
|
|
258
|
+
crew.train(n_iterations: 3, filename: 'training.json',
|
|
259
|
+
feedback: ->(iteration, result) { "run #{iteration}: #{result[:success_rate]}%" })
|
|
260
|
+
|
|
261
|
+
# Test: run N times and score each run (defaults to success_rate).
|
|
262
|
+
crew.test(n_iterations: 5)
|
|
263
|
+
# => { iterations: 5, scores: [...], average_score: 92.0 }
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
## 📚 Knowledge (RAG)
|
|
267
|
+
|
|
268
|
+
Ground agents in your own documents. Sources are chunked, embedded, and stored
|
|
269
|
+
in an in-memory vector store; the most relevant chunks are injected into each
|
|
270
|
+
task's prompt automatically.
|
|
271
|
+
|
|
272
|
+
```ruby
|
|
273
|
+
kb = RCrewAI::Knowledge::Base.new(sources: [
|
|
274
|
+
RCrewAI::Knowledge::StringSource.new('Our refund window is 30 days.'),
|
|
275
|
+
RCrewAI::Knowledge::FileSource.new('docs/policy.txt'),
|
|
276
|
+
RCrewAI::Knowledge::PdfSource.new('handbook.pdf'),
|
|
277
|
+
RCrewAI::Knowledge::UrlSource.new('https://example.com/faq')
|
|
278
|
+
])
|
|
279
|
+
|
|
280
|
+
# Agent-level (role-specific) knowledge:
|
|
281
|
+
support = RCrewAI::Agent.new(name: 'support', role: '...', goal: '...', knowledge: kb)
|
|
282
|
+
|
|
283
|
+
# Or pass raw sources and let the agent build the base:
|
|
284
|
+
support = RCrewAI::Agent.new(name: 'support', role: '...', goal: '...',
|
|
285
|
+
knowledge_sources: [RCrewAI::Knowledge::StringSource.new('...')])
|
|
286
|
+
|
|
287
|
+
# Crew-level knowledge is shared with every agent:
|
|
288
|
+
crew = RCrewAI::Crew.new('support_crew', knowledge: kb)
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
Embeddings default to OpenAI's `text-embedding-3-small`; pass a custom
|
|
292
|
+
`embedder:` (anything responding to `embed(texts)`) or vector store to swap the
|
|
293
|
+
backend.
|
|
294
|
+
|
|
295
|
+
## 🌊 Flows
|
|
296
|
+
|
|
297
|
+
Beyond crews, RCrewAI has **Flows** — an event-driven workflow engine for
|
|
298
|
+
orchestrating steps (and whole crews) with explicit branching and state:
|
|
299
|
+
|
|
300
|
+
```ruby
|
|
301
|
+
class ArticleFlow < RCrewAI::Flow
|
|
302
|
+
start :outline
|
|
303
|
+
def outline
|
|
304
|
+
state.sections = %w[intro body conclusion]
|
|
305
|
+
state.sections.length
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
listen :outline
|
|
309
|
+
def draft(section_count)
|
|
310
|
+
state.words = section_count * 100
|
|
311
|
+
state.words
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
router :draft
|
|
315
|
+
def review(words)
|
|
316
|
+
words >= 250 ? :publish : :expand
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
listen :publish
|
|
320
|
+
def publish = state.status = 'published'
|
|
321
|
+
|
|
322
|
+
listen :expand
|
|
323
|
+
def expand = state.status = 'needs more work'
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
flow = ArticleFlow.new
|
|
327
|
+
flow.kickoff(inputs: { author: 'me' })
|
|
328
|
+
flow.state.status # => "published"
|
|
329
|
+
flow.state.id # => automatic UUID
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
- `start` / `listen` / `router` wire methods into a graph; a listener receives
|
|
333
|
+
its trigger's return value.
|
|
334
|
+
- Combine triggers with `and_(:a, :b)` (all) and `or_(:a, :b)` (any).
|
|
335
|
+
- **State** is a schemaless object with a UUID, seedable via `kickoff(inputs:)`.
|
|
336
|
+
- **Persistence**: pass `state_store:` (`RCrewAI::Flow::FileStateStore.new(dir)`
|
|
337
|
+
or your own `#save`/`#load`) and call `flow.restore(id)` to resume.
|
|
338
|
+
- Invoke a `Crew` inside any step, or pause with `human_feedback('Approve?')`.
|
|
339
|
+
|
|
172
340
|
## 💡 Examples
|
|
173
341
|
|
|
174
342
|
### Hierarchical Team with Human Oversight
|
data/ROADMAP.md
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# RCrewAI Roadmap
|
|
2
|
+
|
|
3
|
+
This roadmap tracks feature parity between **RCrewAI** (Ruby) and the upstream
|
|
4
|
+
[**crewai**](https://pypi.org/project/crewai/) Python framework.
|
|
5
|
+
|
|
6
|
+
## Current status
|
|
7
|
+
|
|
8
|
+
- **RCrewAI:** `0.3.0` (2026-05-12)
|
|
9
|
+
- **Upstream crewai:** `1.15.x` (mid-2026)
|
|
10
|
+
|
|
11
|
+
RCrewAI is a faithful port of CrewAI's **"Crews"** mental model (Agents / Tasks /
|
|
12
|
+
Crew, sequential + hierarchical processes, tools, memory, human-in-the-loop). As
|
|
13
|
+
of `0.3.0` the LLM plumbing is modern: native function calling across all five
|
|
14
|
+
providers, a tool-schema DSL, typed streaming events, MCP client, and per-model
|
|
15
|
+
pricing.
|
|
16
|
+
|
|
17
|
+
Since CrewAI's `1.0`, the framework grew a second pillar (**Flows**) plus
|
|
18
|
+
**Knowledge (RAG)**, **Guardrails**, **structured output**, **Planning**, and
|
|
19
|
+
**Training/Testing**. As of the `[Unreleased]` changes, RCrewAI now implements
|
|
20
|
+
all of these — see the matrix below. Only backlog polish items remain.
|
|
21
|
+
|
|
22
|
+
**Status: all milestone issues (#5–#12) are complete.** The remaining backlog
|
|
23
|
+
covers smaller polish items (reasoning, rate-limiting, batch kickoff, kickoff
|
|
24
|
+
hooks, multimodal).
|
|
25
|
+
|
|
26
|
+
## Parity matrix
|
|
27
|
+
|
|
28
|
+
| Concept | crewai | RCrewAI 0.3.0 | Target |
|
|
29
|
+
|---|---|---|---|
|
|
30
|
+
| Agents / Tasks / Crew | ✅ | ✅ | — |
|
|
31
|
+
| Sequential / hierarchical process | ✅ | ✅ | — |
|
|
32
|
+
| Native function calling + tool DSL | ✅ | ✅ (0.3.0) | — |
|
|
33
|
+
| Streaming events | ✅ | ✅ (0.3.0) | — |
|
|
34
|
+
| MCP client | ✅ | ✅ (0.3.0) | — |
|
|
35
|
+
| Per-model pricing / cost | ✅ | ✅ (0.3.0) | — |
|
|
36
|
+
| Per-agent LLM override | ✅ | ✅ (#5) | ✅ done |
|
|
37
|
+
| Structured output (schema) | ✅ | ✅ (#6) | ✅ done |
|
|
38
|
+
| Task guardrails | ✅ | ✅ (#7) | ✅ done |
|
|
39
|
+
| `output_file` / markdown | ✅ | ✅ (#8) | ✅ done |
|
|
40
|
+
| Knowledge / RAG | ✅ | ✅ (#9) | ✅ done |
|
|
41
|
+
| Planning | ✅ | ✅ (#10) | ✅ done |
|
|
42
|
+
| Flows (`start`/`listen`/`router`) | ✅ | ✅ (#11) | ✅ done |
|
|
43
|
+
| Flow state + persistence | ✅ | ✅ (#11) | ✅ done |
|
|
44
|
+
| Training / Testing | ✅ | ✅ (#12) | ✅ done |
|
|
45
|
+
| Reasoning, rate-limiting, batch kickoff | ✅ | ❌ | backlog |
|
|
46
|
+
|
|
47
|
+
## Milestones (highest leverage first)
|
|
48
|
+
|
|
49
|
+
### 0.3.1 — Per-agent LLM override
|
|
50
|
+
Let `Agent.new(llm:)` accept a provider/model, instead of only the global
|
|
51
|
+
`RCrewAI.configure`. Unblocks mixed-model crews (cheap model for workers, strong
|
|
52
|
+
model for the manager).
|
|
53
|
+
|
|
54
|
+
### 0.4.0 — Structured output & guardrails
|
|
55
|
+
Builds directly on the 0.3.0 tool-schema/JSON-schema plumbing.
|
|
56
|
+
- `Task.new(output_schema:)` → validated, coerced structured result.
|
|
57
|
+
- `Task.new(guardrail:)` → proc/object that validates & transforms output, with
|
|
58
|
+
bounded retries (`guardrail_max_retries`).
|
|
59
|
+
- `output_file:` + `markdown:` output formatting.
|
|
60
|
+
|
|
61
|
+
### 0.5.0 — Knowledge (RAG) & Planning
|
|
62
|
+
- Knowledge sources: string, `.txt`, PDF (have `pdf-reader`), CSV, JSON, URL
|
|
63
|
+
(have `nokogiri`). Embeddings client + a pluggable vector store (start with an
|
|
64
|
+
in-memory / SQLite cosine store; no hard Chroma dependency).
|
|
65
|
+
- Attach at agent **and** crew level.
|
|
66
|
+
- `Crew.new(planning: true)` → a planner pass that drafts a step plan before
|
|
67
|
+
execution.
|
|
68
|
+
|
|
69
|
+
### 0.6.0 — Flows
|
|
70
|
+
The flagship. A Ruby DSL mirroring CrewAI Flows:
|
|
71
|
+
- `start`, `listen`, `router` decorators/class-methods.
|
|
72
|
+
- `and_` / `or_` trigger combinators.
|
|
73
|
+
- Structured flow **state** (a plain struct/`Data` or dry-struct) with a UUID.
|
|
74
|
+
- `@persist`-equivalent state persistence across restarts.
|
|
75
|
+
- `human_feedback` pause/resume point.
|
|
76
|
+
|
|
77
|
+
### 0.7.0 — Training & Testing
|
|
78
|
+
- `crew.train(n_iterations:, filename:)` capturing human feedback.
|
|
79
|
+
- `crew.test(n_iterations:, model:)` scoring runs.
|
|
80
|
+
|
|
81
|
+
### Backlog
|
|
82
|
+
Per-agent reasoning (`reasoning:`, `max_reasoning_attempts:`), `max_rpm`
|
|
83
|
+
rate-limiting, `respect_context_window`, `kickoff_for_each` batch execution,
|
|
84
|
+
`before_kickoff` / `after_kickoff` hooks, multimodal agents.
|
data/lib/rcrewai/agent.rb
CHANGED
|
@@ -11,8 +11,10 @@ require_relative 'human_input'
|
|
|
11
11
|
module RCrewAI
|
|
12
12
|
class Agent
|
|
13
13
|
include HumanInteractionExtensions
|
|
14
|
-
attr_reader :name, :role, :goal, :backstory, :tools, :memory, :llm_client
|
|
14
|
+
attr_reader :name, :role, :goal, :backstory, :tools, :memory, :llm_client, :knowledge
|
|
15
15
|
attr_accessor :verbose, :allow_delegation, :max_iterations, :max_execution_time, :manager
|
|
16
|
+
# Set by the crew so agents see shared knowledge in addition to their own.
|
|
17
|
+
attr_writer :crew_knowledge
|
|
16
18
|
|
|
17
19
|
def initialize(name:, role:, goal:, backstory: nil, tools: [], **options)
|
|
18
20
|
@name = name
|
|
@@ -31,7 +33,8 @@ module RCrewAI
|
|
|
31
33
|
@logger = Logger.new($stdout)
|
|
32
34
|
@logger.level = verbose ? Logger::DEBUG : Logger::INFO
|
|
33
35
|
@memory = Memory.new
|
|
34
|
-
@llm_client =
|
|
36
|
+
@llm_client = build_llm_client(options[:llm])
|
|
37
|
+
@knowledge = build_knowledge(options[:knowledge], options[:knowledge_sources])
|
|
35
38
|
@subordinates = [] # For manager agents
|
|
36
39
|
end
|
|
37
40
|
|
|
@@ -194,6 +197,21 @@ module RCrewAI
|
|
|
194
197
|
|
|
195
198
|
private
|
|
196
199
|
|
|
200
|
+
# Resolves the +llm:+ option into an LLM client. See LLMClient.resolve.
|
|
201
|
+
def build_llm_client(llm)
|
|
202
|
+
LLMClient.resolve(llm)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Accepts a pre-built Knowledge::Base via +knowledge:+ or an array of
|
|
206
|
+
# sources via +knowledge_sources:+ (wrapped in a Base). Returns nil if
|
|
207
|
+
# neither is given.
|
|
208
|
+
def build_knowledge(knowledge, sources)
|
|
209
|
+
return knowledge if knowledge
|
|
210
|
+
return nil if sources.nil? || sources.empty?
|
|
211
|
+
|
|
212
|
+
Knowledge::Base.new(sources: sources)
|
|
213
|
+
end
|
|
214
|
+
|
|
197
215
|
def build_context(task)
|
|
198
216
|
context = {
|
|
199
217
|
agent_role: role,
|
|
@@ -226,12 +244,28 @@ module RCrewAI
|
|
|
226
244
|
user << "\nExpected Output: #{task.expected_output}" if task.expected_output
|
|
227
245
|
user << "\nAdditional Context:\n#{ctx[:context_data]}" if ctx[:context_data] && !ctx[:context_data].to_s.empty?
|
|
228
246
|
|
|
247
|
+
knowledge = retrieve_knowledge(task)
|
|
248
|
+
user << "\n\nRelevant Knowledge:\n#{knowledge}" unless knowledge.empty?
|
|
249
|
+
|
|
229
250
|
[
|
|
230
251
|
{ role: 'system', content: system },
|
|
231
252
|
{ role: 'user', content: user }
|
|
232
253
|
]
|
|
233
254
|
end
|
|
234
255
|
|
|
256
|
+
# Retrieves knowledge chunks relevant to the task from the agent's own
|
|
257
|
+
# knowledge base and/or the crew-level base injected via #knowledge=.
|
|
258
|
+
def retrieve_knowledge(task)
|
|
259
|
+
bases = [@knowledge, @crew_knowledge].compact
|
|
260
|
+
return '' if bases.empty?
|
|
261
|
+
|
|
262
|
+
chunks = bases.flat_map { |kb| kb.search(task.description, k: 3) }
|
|
263
|
+
chunks.uniq.join("\n---\n")
|
|
264
|
+
rescue StandardError => e
|
|
265
|
+
@logger.warn("Knowledge retrieval failed: #{e.message}")
|
|
266
|
+
''
|
|
267
|
+
end
|
|
268
|
+
|
|
235
269
|
def build_task_result(task, runner_result)
|
|
236
270
|
{
|
|
237
271
|
task: task.name,
|
|
@@ -59,6 +59,26 @@ module RCrewAI
|
|
|
59
59
|
end
|
|
60
60
|
end
|
|
61
61
|
|
|
62
|
+
# Returns a copy of this configuration with the given per-agent overrides
|
|
63
|
+
# applied. The original configuration is left untouched, so agents can each
|
|
64
|
+
# target a different provider/model without mutating global state.
|
|
65
|
+
#
|
|
66
|
+
# config.with_overrides(provider: :anthropic, model: 'claude-3-opus-20240229')
|
|
67
|
+
def with_overrides(provider: nil, model: nil, api_key: nil, temperature: nil)
|
|
68
|
+
copy = dup
|
|
69
|
+
copy.llm_provider = provider.to_sym if provider
|
|
70
|
+
target = copy.llm_provider
|
|
71
|
+
|
|
72
|
+
copy.public_send("#{target}_model=", model) if model && copy.respond_to?("#{target}_model=")
|
|
73
|
+
copy.model = model if model
|
|
74
|
+
|
|
75
|
+
copy.public_send("#{target}_api_key=", api_key) if api_key && copy.respond_to?("#{target}_api_key=")
|
|
76
|
+
copy.api_key = api_key if api_key
|
|
77
|
+
|
|
78
|
+
copy.temperature = temperature unless temperature.nil?
|
|
79
|
+
copy
|
|
80
|
+
end
|
|
81
|
+
|
|
62
82
|
def validate!
|
|
63
83
|
raise ConfigurationError, 'LLM provider must be set' if @llm_provider.nil?
|
|
64
84
|
raise ConfigurationError, "API key must be set for #{@llm_provider}" if api_key.nil? || api_key.empty?
|
data/lib/rcrewai/crew.rb
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'logger'
|
|
3
4
|
require_relative 'process'
|
|
4
5
|
require_relative 'async_executor'
|
|
5
6
|
require_relative 'events'
|
|
7
|
+
require_relative 'planning'
|
|
6
8
|
|
|
7
9
|
module RCrewAI
|
|
8
10
|
class Crew
|
|
@@ -17,10 +19,20 @@ module RCrewAI
|
|
|
17
19
|
@process_type = options.fetch(:process, :sequential)
|
|
18
20
|
@verbose = options.fetch(:verbose, false)
|
|
19
21
|
@max_iterations = options.fetch(:max_iterations, 10)
|
|
22
|
+
@planning = options.fetch(:planning, false)
|
|
23
|
+
@planning_llm = options[:planning_llm]
|
|
24
|
+
@planned = false
|
|
25
|
+
@knowledge = build_knowledge(options[:knowledge], options[:knowledge_sources])
|
|
20
26
|
@process_instance = nil
|
|
21
27
|
validate_process_type!
|
|
22
28
|
end
|
|
23
29
|
|
|
30
|
+
attr_reader :knowledge, :stream_sink
|
|
31
|
+
|
|
32
|
+
def planning?
|
|
33
|
+
@planning
|
|
34
|
+
end
|
|
35
|
+
|
|
24
36
|
def add_agent(agent)
|
|
25
37
|
@agents << agent
|
|
26
38
|
end
|
|
@@ -35,6 +47,9 @@ module RCrewAI
|
|
|
35
47
|
Array(stream).each { |s| sinks << s } if stream
|
|
36
48
|
@stream_sink = sinks.empty? ? nil : RCrewAI::Events.fan_out(sinks)
|
|
37
49
|
|
|
50
|
+
distribute_knowledge if @knowledge
|
|
51
|
+
run_planning_pass if planning?
|
|
52
|
+
|
|
38
53
|
if async
|
|
39
54
|
execute_async(**async_options)
|
|
40
55
|
else
|
|
@@ -42,7 +57,34 @@ module RCrewAI
|
|
|
42
57
|
end
|
|
43
58
|
end
|
|
44
59
|
|
|
45
|
-
|
|
60
|
+
# Runs the crew repeatedly, collecting feedback after each iteration and
|
|
61
|
+
# persisting it to +filename+ as JSON. +feedback+ is a callable
|
|
62
|
+
# ->(iteration, result) { "..." }; it defaults to prompting a human.
|
|
63
|
+
# Mirrors CrewAI's crew.train.
|
|
64
|
+
def train(n_iterations:, filename:, feedback: nil)
|
|
65
|
+
feedback ||= method(:default_training_feedback)
|
|
66
|
+
entries = []
|
|
67
|
+
|
|
68
|
+
(1..n_iterations).each do |iteration|
|
|
69
|
+
result = execute
|
|
70
|
+
note = feedback.call(iteration, result)
|
|
71
|
+
entries << { iteration: iteration, feedback: note }
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
write_training_file(filename, entries)
|
|
75
|
+
{ iterations: n_iterations, filename: filename, entries: entries }
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Runs the crew repeatedly and scores each run. +scorer+ is a callable
|
|
79
|
+
# ->(result) { Float }; it defaults to the run's success_rate.
|
|
80
|
+
# Mirrors CrewAI's crew.test.
|
|
81
|
+
def test(n_iterations:, scorer: nil, model: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
82
|
+
scorer ||= ->(result) { result[:success_rate].to_f }
|
|
83
|
+
scores = (1..n_iterations).map { scorer.call(execute) }
|
|
84
|
+
average = scores.empty? ? 0.0 : (scores.sum / scores.length).round(2)
|
|
85
|
+
|
|
86
|
+
{ iterations: n_iterations, scores: scores, average_score: average }
|
|
87
|
+
end
|
|
46
88
|
|
|
47
89
|
def execute_async(**options)
|
|
48
90
|
puts "Executing crew: #{name} (async #{process_type} process)"
|
|
@@ -102,6 +144,42 @@ module RCrewAI
|
|
|
102
144
|
|
|
103
145
|
private
|
|
104
146
|
|
|
147
|
+
def build_knowledge(knowledge, sources)
|
|
148
|
+
return knowledge if knowledge
|
|
149
|
+
return nil if sources.nil? || sources.empty?
|
|
150
|
+
|
|
151
|
+
Knowledge::Base.new(sources: sources)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def distribute_knowledge
|
|
155
|
+
@knowledge.build!
|
|
156
|
+
agents.each { |agent| agent.crew_knowledge = @knowledge if agent.respond_to?(:crew_knowledge=) }
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def default_training_feedback(iteration, _result)
|
|
160
|
+
require_relative 'human_input'
|
|
161
|
+
response = HumanInput.new.request_input(
|
|
162
|
+
"Feedback for training iteration #{iteration} (press enter to skip):"
|
|
163
|
+
)
|
|
164
|
+
response.is_a?(Hash) ? response[:input].to_s : response.to_s
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def write_training_file(filename, entries)
|
|
168
|
+
require 'json'
|
|
169
|
+
require 'fileutils'
|
|
170
|
+
FileUtils.mkdir_p(File.dirname(filename))
|
|
171
|
+
File.write(filename, JSON.pretty_generate(entries))
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def run_planning_pass
|
|
175
|
+
return if @planned
|
|
176
|
+
|
|
177
|
+
logger = Logger.new($stdout)
|
|
178
|
+
logger.level = verbose ? Logger::DEBUG : Logger::INFO
|
|
179
|
+
Planning.new(self, llm: LLMClient.resolve(@planning_llm), logger: logger).plan!
|
|
180
|
+
@planned = true
|
|
181
|
+
end
|
|
182
|
+
|
|
105
183
|
def validate_process_type!
|
|
106
184
|
valid_processes = %i[sequential hierarchical consensual]
|
|
107
185
|
return if valid_processes.include?(process_type)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
|
|
5
|
+
module RCrewAI
|
|
6
|
+
class Flow
|
|
7
|
+
# Mutable, schemaless flow state with a stable unique id. Access attributes
|
|
8
|
+
# as methods (state.foo, state.foo = 1) or via [] / to_h. Mirrors CrewAI's
|
|
9
|
+
# unstructured (dict-based) flow state, with an automatic UUID.
|
|
10
|
+
class State
|
|
11
|
+
def initialize(attributes = {})
|
|
12
|
+
@attributes = {}
|
|
13
|
+
attributes.each { |k, v| @attributes[k.to_sym] = v }
|
|
14
|
+
@attributes[:id] ||= SecureRandom.uuid
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def id
|
|
18
|
+
@attributes[:id]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def [](key)
|
|
22
|
+
@attributes[key.to_sym]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def []=(key, value)
|
|
26
|
+
@attributes[key.to_sym] = value
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def to_h
|
|
30
|
+
@attributes.dup
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def respond_to_missing?(_name, _include_private = false)
|
|
34
|
+
true
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def method_missing(name, *args)
|
|
38
|
+
key = name.to_s
|
|
39
|
+
if key.end_with?('=')
|
|
40
|
+
@attributes[key[0..-2].to_sym] = args.first
|
|
41
|
+
else
|
|
42
|
+
@attributes[name]
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
|
|
6
|
+
module RCrewAI
|
|
7
|
+
class Flow
|
|
8
|
+
# Persists flow state keyed by state id, so a flow can be resumed across
|
|
9
|
+
# restarts. Two built-ins: in-memory (tests / single process) and file-based
|
|
10
|
+
# (JSON on disk). Any object with #save(id, hash) and #load(id) works.
|
|
11
|
+
class MemoryStateStore
|
|
12
|
+
def initialize
|
|
13
|
+
@data = {}
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def save(id, hash)
|
|
17
|
+
@data[id] = hash.dup
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def load(id)
|
|
21
|
+
@data[id]
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Stores each state as a JSON file named <id>.json under a directory.
|
|
26
|
+
class FileStateStore
|
|
27
|
+
def initialize(dir)
|
|
28
|
+
@dir = dir
|
|
29
|
+
FileUtils.mkdir_p(@dir)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def save(id, hash)
|
|
33
|
+
File.write(path_for(id), JSON.pretty_generate(hash))
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def load(id)
|
|
37
|
+
path = path_for(id)
|
|
38
|
+
return nil unless File.exist?(path)
|
|
39
|
+
|
|
40
|
+
JSON.parse(File.read(path))
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def path_for(id)
|
|
46
|
+
File.join(@dir, "#{id}.json")
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|