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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c7682076eeeb0d3c1bdc0de2185c83ad8c925ce19165786daa0910c1925fdf64
4
- data.tar.gz: 1db4ba5508d8aef52b6645be9cae3dcb7328564f8335a24fe844655196d2f53f
3
+ metadata.gz: f742fcf06518c80cbf29191d0eff275830ee618c9b896c3d443258b06aa120ff
4
+ data.tar.gz: 5cc9c03182d0bd80d73d99edb08f2267252684f64a23da1626d2d61e0abe1683
5
5
  SHA512:
6
- metadata.gz: dfaf95b581c86cd36573400729a02e72e307721f51267fefafdb3245aae28bffc89201855dcd294f38ecfc54a6a1dbf2062d92fbd55310e4f2f9d968b49a1a6e
7
- data.tar.gz: 4c8ed1132d754e92ab154c192f28d8add3d80fdbedbaaa15fa6fd876002f18674d30a91f7ef9fb5dcba65a4240ca9f464d26fa15b64a8cdcdee4ad806cefe3bd
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.1.0...HEAD
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 = LLMClient.for_provider
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
- attr_reader :stream_sink
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