rcrewai 0.4.0 → 0.5.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 +16 -1
- data/README.md +82 -0
- data/ROADMAP.md +16 -10
- data/docs/upgrading-to-0.4.md +191 -0
- data/examples/flow_example.rb +89 -0
- data/examples/knowledge_rag_example.rb +72 -0
- data/examples/planning_and_training_example.rb +72 -0
- data/examples/structured_output_example.rb +92 -0
- data/lib/rcrewai/agent.rb +38 -6
- data/lib/rcrewai/agent_augmentations.rb +75 -0
- data/lib/rcrewai/context_window.rb +75 -0
- data/lib/rcrewai/crew.rb +45 -7
- data/lib/rcrewai/legacy_react_runner.rb +7 -1
- data/lib/rcrewai/multimodal.rb +67 -0
- data/lib/rcrewai/rate_limiter.rb +94 -0
- data/lib/rcrewai/task.rb +2 -1
- data/lib/rcrewai/tool_runner.rb +7 -1
- data/lib/rcrewai/version.rb +1 -1
- data/lib/rcrewai.rb +3 -0
- metadata +10 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 68a381d21e047e37972fda09fd7e9faa93da77e6fe01a040a4c77b9ee0066aca
|
|
4
|
+
data.tar.gz: 3818acc6b9b6eb71d27d4099cb1c1fc574adbdd0a3a8128389803c051c364774
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 65d796bde314db55466f7f4f043db9eeb772dee3e72914d2bc27947eb6dfe84ce246b89a6181d473c3f7d175d353fbc9531914c8baa5b7b010e02229324bac27
|
|
7
|
+
data.tar.gz: b2fffaab35dc672b12d2a5aea5fd0b02e6519b2f4a5ca98daaf66bef740551e2e674f4cc39967f3d987ed9dfe1c13e7dec4c8a30e5b86e2140242e347dd96a11
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.5.0] - 2026-07-03
|
|
11
|
+
|
|
12
|
+
Polish release completing the roadmap backlog: crew lifecycle hooks, batch
|
|
13
|
+
execution, rate limiting, per-agent reasoning, context-window management, and
|
|
14
|
+
multimodal image input. All additive — existing code runs unchanged.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- Crew lifecycle hooks: `Crew#before_kickoff` and `Crew#after_kickoff` register callbacks that run before/after execution. A `before_kickoff` hook receives the inputs hash (passed via `crew.execute(inputs:)`) and may transform it; an `after_kickoff` hook receives the result and may transform it. Multiple hooks run in registration order. The (possibly transformed) inputs are exposed on `Crew#last_inputs`. (#15)
|
|
18
|
+
- `Crew#kickoff_for_each(inputs:)` runs the crew once per input set and returns one result per input, in order. Runs are isolated — each execution starts from only its own inputs. (#16)
|
|
19
|
+
- Rate limiting: `Agent.new(max_rpm:)` throttles the agent's LLM calls to the given requests-per-minute using a thread-safe rolling-window `RCrewAI::RateLimiter`. The agent's client is transparently wrapped (`RateLimiter::ThrottledClient`) so every `chat` acquires a slot first; `max_rpm` nil/0 means unlimited. (#17)
|
|
20
|
+
- Reasoning: `Agent.new(reasoning: true)` runs a reasoning/planning pass before answering — the LLM drafts a short plan for the task, which is injected into the answer prompt and surfaced on the result hash as `:reasoning` (without polluting `task.result`). Bounded by `max_reasoning_attempts` (default 3), retrying if the model returns empty output; if every attempt is empty, execution proceeds without a plan. Off by default. (#18)
|
|
21
|
+
- Context-window management: `Agent.new(respect_context_window: true)` trims the message history to fit the model's context window before each LLM call, dropping the oldest non-system messages while always keeping system messages and the latest message. The new `RCrewAI::ContextWindow` module provides the token estimate (chars/4 heuristic), a per-model window-size table, and the `fit` trimmer. Off by default. (#19)
|
|
22
|
+
- Multimodal input: `Task.new(attachments:)` accepts image attachments (`{ type: :image, url: '...' }` or `{ type: :image, path: '...' }`). When a task has attachments, the agent builds an OpenAI-style multimodal user message (text + `image_url` parts); local files are base64-encoded into data URLs with a mime type inferred from the extension. Supported on OpenAI/Azure; other providers raise a clear `Multimodal::UnsupportedProviderError`. The new `RCrewAI::Multimodal` module builds the content parts. (#20)
|
|
23
|
+
|
|
10
24
|
## [0.4.0] - 2026-07-03
|
|
11
25
|
|
|
12
26
|
This release closes the feature-parity gap with the modern CrewAI framework,
|
|
@@ -165,7 +179,8 @@ output, guardrails, planning, and training/testing. See `ROADMAP.md`.
|
|
|
165
179
|
- CLI usage documentation
|
|
166
180
|
- Real-world use cases and examples
|
|
167
181
|
|
|
168
|
-
[Unreleased]: https://github.com/gkosmo/rcrewAI/compare/v0.
|
|
182
|
+
[Unreleased]: https://github.com/gkosmo/rcrewAI/compare/v0.5.0...HEAD
|
|
183
|
+
[0.5.0]: https://github.com/gkosmo/rcrewAI/compare/v0.4.0...v0.5.0
|
|
169
184
|
[0.4.0]: https://github.com/gkosmo/rcrewAI/compare/v0.3.0...v0.4.0
|
|
170
185
|
[0.3.0]: https://github.com/gkosmo/rcrewAI/compare/v0.1.0...v0.3.0
|
|
171
186
|
[0.1.0]: https://github.com/gkosmo/rcrewAI/releases/tag/v0.1.0
|
data/README.md
CHANGED
|
@@ -263,6 +263,88 @@ crew.test(n_iterations: 5)
|
|
|
263
263
|
# => { iterations: 5, scores: [...], average_score: 92.0 }
|
|
264
264
|
```
|
|
265
265
|
|
|
266
|
+
## 🪝 Kickoff Hooks & Batch Runs
|
|
267
|
+
|
|
268
|
+
Run setup/teardown around a crew, and batch it over many inputs:
|
|
269
|
+
|
|
270
|
+
```ruby
|
|
271
|
+
crew.before_kickoff { |inputs| inputs.merge(started_at: Time.now) } # may transform inputs
|
|
272
|
+
crew.after_kickoff { |result| notify(result); result } # may transform result
|
|
273
|
+
|
|
274
|
+
crew.execute(inputs: { topic: 'ruby' })
|
|
275
|
+
crew.last_inputs # => the (possibly transformed) inputs the run used
|
|
276
|
+
|
|
277
|
+
# Batch: run the crew once per input set, results returned in order.
|
|
278
|
+
results = crew.kickoff_for_each(inputs: [
|
|
279
|
+
{ topic: 'ruby' },
|
|
280
|
+
{ topic: 'python' }
|
|
281
|
+
])
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
## ⏱️ Rate Limiting
|
|
285
|
+
|
|
286
|
+
Cap an agent's LLM calls to stay under provider limits. Calls beyond the cap
|
|
287
|
+
block until the rolling 60-second window frees up:
|
|
288
|
+
|
|
289
|
+
```ruby
|
|
290
|
+
agent = RCrewAI::Agent.new(name: 'a', role: '...', goal: '...', max_rpm: 20)
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
The limiter (`RCrewAI::RateLimiter`) is thread-safe, so it holds under async
|
|
294
|
+
execution. `max_rpm: nil` (the default) or `0` means unlimited.
|
|
295
|
+
|
|
296
|
+
## 🧠 Reasoning
|
|
297
|
+
|
|
298
|
+
Have an agent think through a plan before answering. The reasoning trace is
|
|
299
|
+
surfaced on the result and does not pollute `task.result`:
|
|
300
|
+
|
|
301
|
+
```ruby
|
|
302
|
+
agent = RCrewAI::Agent.new(name: 'a', role: '...', goal: '...',
|
|
303
|
+
reasoning: true, max_reasoning_attempts: 3)
|
|
304
|
+
|
|
305
|
+
result = agent.execute_task(task)
|
|
306
|
+
result[:reasoning] # => the plan the agent drafted before answering
|
|
307
|
+
result[:content] # => the final answer
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
Off by default. If the reasoning pass keeps returning empty output past
|
|
311
|
+
`max_reasoning_attempts`, the agent proceeds without a plan.
|
|
312
|
+
|
|
313
|
+
## 🪟 Context Window Management
|
|
314
|
+
|
|
315
|
+
Keep long tool-use loops or large injected context from overflowing the model's
|
|
316
|
+
context window. When enabled, the oldest non-system messages are dropped to fit
|
|
317
|
+
before each LLM call (system messages and the latest message are always kept):
|
|
318
|
+
|
|
319
|
+
```ruby
|
|
320
|
+
agent = RCrewAI::Agent.new(name: 'a', role: '...', goal: '...',
|
|
321
|
+
respect_context_window: true)
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
Window sizes come from `RCrewAI::ContextWindow` (with a conservative default for
|
|
325
|
+
unknown models); headroom for the response is reserved from `max_tokens`. Off by
|
|
326
|
+
default.
|
|
327
|
+
|
|
328
|
+
## 🖼️ Multimodal Input
|
|
329
|
+
|
|
330
|
+
Pass images to a vision-capable model via task attachments. Local files are
|
|
331
|
+
base64-encoded automatically; URLs pass through:
|
|
332
|
+
|
|
333
|
+
```ruby
|
|
334
|
+
RCrewAI.configure { |c| c.llm_provider = :openai; c.openai_model = 'gpt-4o' }
|
|
335
|
+
|
|
336
|
+
task = RCrewAI::Task.new(
|
|
337
|
+
name: 'describe', description: 'What is in this chart?', agent: agent,
|
|
338
|
+
attachments: [
|
|
339
|
+
{ type: :image, path: 'chart.png' },
|
|
340
|
+
{ type: :image, url: 'https://example.com/photo.jpg' }
|
|
341
|
+
]
|
|
342
|
+
)
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
Supported on OpenAI and Azure; other providers raise a clear error when
|
|
346
|
+
attachments are present.
|
|
347
|
+
|
|
266
348
|
## 📚 Knowledge (RAG)
|
|
267
349
|
|
|
268
350
|
Ground agents in your own documents. Sources are chunked, embedded, and stored
|
data/ROADMAP.md
CHANGED
|
@@ -16,12 +16,11 @@ pricing.
|
|
|
16
16
|
|
|
17
17
|
Since CrewAI's `1.0`, the framework grew a second pillar (**Flows**) plus
|
|
18
18
|
**Knowledge (RAG)**, **Guardrails**, **structured output**, **Planning**, and
|
|
19
|
-
**Training/Testing**.
|
|
20
|
-
all of these — see the matrix below. Only backlog polish items remain.
|
|
19
|
+
**Training/Testing**. RCrewAI now implements all of these — see the matrix below.
|
|
21
20
|
|
|
22
|
-
**Status:
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
**Status: complete.** All milestone issues (#5–#12) shipped in v0.4.0, and all
|
|
22
|
+
backlog items (#15–#20) are done and merged to `main` (awaiting the next
|
|
23
|
+
release). There is no outstanding roadmap work.
|
|
25
24
|
|
|
26
25
|
## Parity matrix
|
|
27
26
|
|
|
@@ -42,7 +41,7 @@ hooks, multimodal).
|
|
|
42
41
|
| Flows (`start`/`listen`/`router`) | ✅ | ✅ (#11) | ✅ done |
|
|
43
42
|
| Flow state + persistence | ✅ | ✅ (#11) | ✅ done |
|
|
44
43
|
| Training / Testing | ✅ | ✅ (#12) | ✅ done |
|
|
45
|
-
| Reasoning, rate-limiting, batch kickoff | ✅ |
|
|
44
|
+
| Reasoning, rate-limiting, batch kickoff, hooks, context window, multimodal | ✅ | ✅ (#15–#20) | ✅ done |
|
|
46
45
|
|
|
47
46
|
## Milestones (highest leverage first)
|
|
48
47
|
|
|
@@ -78,7 +77,14 @@ The flagship. A Ruby DSL mirroring CrewAI Flows:
|
|
|
78
77
|
- `crew.train(n_iterations:, filename:)` capturing human feedback.
|
|
79
78
|
- `crew.test(n_iterations:, model:)` scoring runs.
|
|
80
79
|
|
|
81
|
-
### Backlog
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
80
|
+
### Backlog — ✅ all complete
|
|
81
|
+
|
|
82
|
+
Formerly polish items with no set version; all shipped in the `[Unreleased]`
|
|
83
|
+
changes (see CHANGELOG):
|
|
84
|
+
|
|
85
|
+
- [#15](https://github.com/gkosmo/rcrewAI/issues/15) — `before_kickoff` / `after_kickoff` lifecycle hooks ✅
|
|
86
|
+
- [#16](https://github.com/gkosmo/rcrewAI/issues/16) — `kickoff_for_each` batch execution ✅
|
|
87
|
+
- [#17](https://github.com/gkosmo/rcrewAI/issues/17) — `max_rpm` rate limiting ✅
|
|
88
|
+
- [#18](https://github.com/gkosmo/rcrewAI/issues/18) — per-agent reasoning (`reasoning:`, `max_reasoning_attempts:`) ✅
|
|
89
|
+
- [#19](https://github.com/gkosmo/rcrewAI/issues/19) — `respect_context_window` history trimming ✅
|
|
90
|
+
- [#20](https://github.com/gkosmo/rcrewAI/issues/20) — multimodal agents (image/file inputs) ✅
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# Upgrading to RCrewAI 0.4
|
|
2
|
+
|
|
3
|
+
RCrewAI 0.4 closes the feature-parity gap with the modern CrewAI framework. It
|
|
4
|
+
adds CrewAI's second pillar — **Flows** — alongside **Knowledge (RAG)**,
|
|
5
|
+
structured task output, guardrails, planning, and training/testing.
|
|
6
|
+
|
|
7
|
+
**0.4 is fully backward compatible.** There are no breaking changes: existing
|
|
8
|
+
0.3 code runs unchanged. Everything below is opt-in.
|
|
9
|
+
|
|
10
|
+
Each new capability has a runnable example under `examples/` — see the links
|
|
11
|
+
throughout.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## 1. What you must do
|
|
16
|
+
|
|
17
|
+
Nothing. Upgrade the gem and your existing code keeps working:
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
gem 'rcrewai', '~> 0.4'
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## 2. What you can do (new capabilities)
|
|
26
|
+
|
|
27
|
+
### 2a. Per-agent LLM
|
|
28
|
+
|
|
29
|
+
Give each agent its own provider/model instead of only the global default.
|
|
30
|
+
Pass a provider symbol, an options hash, or a pre-built client:
|
|
31
|
+
|
|
32
|
+
```ruby
|
|
33
|
+
worker = RCrewAI::Agent.new(name: 'worker', role: '...', goal: '...',
|
|
34
|
+
llm: { provider: :openai, model: 'gpt-4o-mini' })
|
|
35
|
+
manager = RCrewAI::Agent.new(name: 'manager', role: '...', goal: '...',
|
|
36
|
+
llm: { provider: :anthropic, model: 'claude-3-opus-20240229' })
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Omitting `llm:` keeps the global `RCrewAI.configure` behavior. Overrides never
|
|
40
|
+
mutate the global configuration.
|
|
41
|
+
|
|
42
|
+
### 2b. Structured output, guardrails, and file output
|
|
43
|
+
|
|
44
|
+
Post-process a task's result after the agent produces it:
|
|
45
|
+
|
|
46
|
+
```ruby
|
|
47
|
+
task = RCrewAI::Task.new(
|
|
48
|
+
name: 'extract', description: '...', agent: agent,
|
|
49
|
+
|
|
50
|
+
# Validate & coerce against a JSON schema. Non-conforming output re-runs the
|
|
51
|
+
# agent with the error fed back. Parsed object on task.structured_output.
|
|
52
|
+
output_schema: { type: 'object', properties: { title: { type: 'string' } },
|
|
53
|
+
required: ['title'] },
|
|
54
|
+
|
|
55
|
+
# Validate & transform. ->(output) { [ok, value_or_error] }. Retries up to
|
|
56
|
+
# guardrail_max_retries (default 3) with the reason fed back to the agent.
|
|
57
|
+
guardrail: ->(out) { [out.length < 5000, 'too long'] },
|
|
58
|
+
|
|
59
|
+
# Persist the result (parent dirs created unless create_directory: false).
|
|
60
|
+
output_file: 'out/report.md', markdown: true
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
task.execute
|
|
64
|
+
task.structured_output # => { "title" => "..." }
|
|
65
|
+
task.raw_result # => the unprocessed string the agent produced
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
See `examples/structured_output_example.rb`.
|
|
69
|
+
|
|
70
|
+
### 2c. Knowledge (RAG)
|
|
71
|
+
|
|
72
|
+
Ground agents in your own documents. Sources are chunked, embedded, and stored
|
|
73
|
+
in an in-memory cosine vector store; relevant chunks are injected into each
|
|
74
|
+
task's prompt automatically.
|
|
75
|
+
|
|
76
|
+
```ruby
|
|
77
|
+
kb = RCrewAI::Knowledge::Base.new(sources: [
|
|
78
|
+
RCrewAI::Knowledge::StringSource.new('Refunds within 30 days.'),
|
|
79
|
+
RCrewAI::Knowledge::FileSource.new('docs/policy.txt'),
|
|
80
|
+
RCrewAI::Knowledge::PdfSource.new('handbook.pdf'),
|
|
81
|
+
RCrewAI::Knowledge::UrlSource.new('https://example.com/faq')
|
|
82
|
+
])
|
|
83
|
+
|
|
84
|
+
# Agent-level (role-specific):
|
|
85
|
+
agent = RCrewAI::Agent.new(name: 'support', role: '...', goal: '...', knowledge: kb)
|
|
86
|
+
|
|
87
|
+
# Or pass raw sources and let the agent build the base:
|
|
88
|
+
agent = RCrewAI::Agent.new(name: 'support', role: '...', goal: '...',
|
|
89
|
+
knowledge_sources: [RCrewAI::Knowledge::StringSource.new('...')])
|
|
90
|
+
|
|
91
|
+
# Crew-level knowledge is shared with every agent:
|
|
92
|
+
crew = RCrewAI::Crew.new('support', knowledge: kb)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Embeddings default to OpenAI's `text-embedding-3-small`; pass a custom
|
|
96
|
+
`embedder:` (anything responding to `embed(texts)`) to swap the backend.
|
|
97
|
+
See `examples/knowledge_rag_example.rb`.
|
|
98
|
+
|
|
99
|
+
### 2d. Planning
|
|
100
|
+
|
|
101
|
+
Have a planner pass draft a per-task plan before execution:
|
|
102
|
+
|
|
103
|
+
```ruby
|
|
104
|
+
crew = RCrewAI::Crew.new('research_crew', planning: true)
|
|
105
|
+
# Optionally use a dedicated (stronger) planner model:
|
|
106
|
+
crew = RCrewAI::Crew.new('research_crew', planning: true,
|
|
107
|
+
planning_llm: { provider: :anthropic, model: 'claude-3-opus-20240229' })
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
The plan is folded into each task's description. Planning is best-effort: a
|
|
111
|
+
planner error or unparseable output leaves tasks unchanged.
|
|
112
|
+
|
|
113
|
+
### 2e. Training & testing
|
|
114
|
+
|
|
115
|
+
Iterate on a crew with feedback, or score repeated runs:
|
|
116
|
+
|
|
117
|
+
```ruby
|
|
118
|
+
# Run N times, collect feedback after each run, persist to JSON.
|
|
119
|
+
crew.train(n_iterations: 3, filename: 'training.json')
|
|
120
|
+
|
|
121
|
+
# Provide feedback programmatically instead of prompting a human:
|
|
122
|
+
crew.train(n_iterations: 3, filename: 'training.json',
|
|
123
|
+
feedback: ->(iteration, result) { "run #{iteration}: #{result[:success_rate]}%" })
|
|
124
|
+
|
|
125
|
+
# Run N times and score each run (defaults to success_rate).
|
|
126
|
+
crew.test(n_iterations: 5)
|
|
127
|
+
# => { iterations: 5, scores: [...], average_score: 92.0 }
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
See `examples/planning_and_training_example.rb`.
|
|
131
|
+
|
|
132
|
+
### 2f. Flows
|
|
133
|
+
|
|
134
|
+
Flows are an event-driven workflow engine — the biggest addition in 0.4.
|
|
135
|
+
Subclass `RCrewAI::Flow` and wire methods with a class-level DSL:
|
|
136
|
+
|
|
137
|
+
```ruby
|
|
138
|
+
class ArticleFlow < RCrewAI::Flow
|
|
139
|
+
start :outline
|
|
140
|
+
def outline
|
|
141
|
+
state.sections = %w[intro body conclusion]
|
|
142
|
+
state.sections.length
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
listen :outline
|
|
146
|
+
def draft(section_count)
|
|
147
|
+
state.words = section_count * 100
|
|
148
|
+
state.words
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
router :draft
|
|
152
|
+
def review(words)
|
|
153
|
+
words >= 250 ? :publish : :expand
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
listen :publish
|
|
157
|
+
def publish = state.status = 'published'
|
|
158
|
+
|
|
159
|
+
listen :expand
|
|
160
|
+
def expand = state.status = 'needs more work'
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
flow = ArticleFlow.new
|
|
164
|
+
flow.kickoff(inputs: { author: 'me' })
|
|
165
|
+
flow.state.status # => "published"
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
- `start` / `listen` / `router` wire methods into a graph; a listener receives
|
|
169
|
+
its trigger's return value, and a router's return becomes a label listeners
|
|
170
|
+
fire on.
|
|
171
|
+
- Combine triggers with `and_(:a, :b)` (all) and `or_(:a, :b)` (any).
|
|
172
|
+
- **State** is a schemaless object with a UUID, seedable via `kickoff(inputs:)`.
|
|
173
|
+
- **Persistence**: pass `state_store:`
|
|
174
|
+
(`RCrewAI::Flow::FileStateStore.new(dir)` or your own `#save`/`#load`) and
|
|
175
|
+
call `flow.restore(id)` to resume.
|
|
176
|
+
- Invoke a `Crew` inside any step, or pause with `human_feedback('Approve?')`.
|
|
177
|
+
|
|
178
|
+
See `examples/flow_example.rb`.
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## When to use Flows vs. Crews
|
|
183
|
+
|
|
184
|
+
- **Crew** — a team of agents collaborating on a set of tasks (sequential,
|
|
185
|
+
hierarchical, or async). Reach for a crew when the work is "have these agents
|
|
186
|
+
produce these outputs."
|
|
187
|
+
- **Flow** — explicit, branching orchestration with state. Reach for a flow
|
|
188
|
+
when you need conditional paths, joins, persistence/resumption, or you want to
|
|
189
|
+
coordinate multiple crews and plain Ruby steps.
|
|
190
|
+
|
|
191
|
+
They compose: a Flow step can kick off a Crew.
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Flows — event-driven workflows (RCrewAI's second pillar).
|
|
5
|
+
#
|
|
6
|
+
# Subclass RCrewAI::Flow and wire methods together with the class-level DSL:
|
|
7
|
+
# `start` kicks things off, `listen` reacts to another method's output, and
|
|
8
|
+
# `router` branches by emitting a label that listeners can trigger on. State
|
|
9
|
+
# is a schemaless object with an automatic UUID, and can be persisted so a run
|
|
10
|
+
# can be resumed later.
|
|
11
|
+
#
|
|
12
|
+
# This example needs no API key — it demonstrates the engine itself.
|
|
13
|
+
#
|
|
14
|
+
# Run:
|
|
15
|
+
# ruby examples/flow_example.rb
|
|
16
|
+
|
|
17
|
+
require_relative '../lib/rcrewai'
|
|
18
|
+
|
|
19
|
+
# A tiny content pipeline: outline -> draft -> review (router) -> publish/expand.
|
|
20
|
+
class ArticleFlow < RCrewAI::Flow
|
|
21
|
+
start :outline
|
|
22
|
+
def outline
|
|
23
|
+
state.sections = %w[intro body conclusion]
|
|
24
|
+
state.sections.length # this return value is passed to listeners of :outline
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
listen :outline
|
|
28
|
+
def draft(section_count)
|
|
29
|
+
state.words = section_count * 100
|
|
30
|
+
state.words
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# A router's return value (:publish / :expand) becomes a label that the
|
|
34
|
+
# matching `listen` methods fire on.
|
|
35
|
+
router :draft
|
|
36
|
+
def review(words)
|
|
37
|
+
words >= 250 ? :publish : :expand
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
listen :publish
|
|
41
|
+
def publish
|
|
42
|
+
state.status = 'published'
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
listen :expand
|
|
46
|
+
def expand
|
|
47
|
+
state.status = 'needs more work'
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
puts '== Basic run =='
|
|
52
|
+
flow = ArticleFlow.new
|
|
53
|
+
flow.kickoff(inputs: { author: 'Ada' })
|
|
54
|
+
puts "id: #{flow.state.id}"
|
|
55
|
+
puts "author: #{flow.state.author} (seeded via kickoff inputs)"
|
|
56
|
+
puts "sections: #{flow.state.sections.inspect}"
|
|
57
|
+
puts "words: #{flow.state.words}"
|
|
58
|
+
puts "status: #{flow.state.status.inspect} (routed to :publish since words >= 250)"
|
|
59
|
+
|
|
60
|
+
puts "\n== and_/or_ combinators =="
|
|
61
|
+
class GateFlow < RCrewAI::Flow
|
|
62
|
+
start :fetch_a
|
|
63
|
+
def fetch_a = 'A'
|
|
64
|
+
|
|
65
|
+
start :fetch_b
|
|
66
|
+
def fetch_b = 'B'
|
|
67
|
+
|
|
68
|
+
# Fires only after BOTH starts complete.
|
|
69
|
+
listen and_(:fetch_a, :fetch_b)
|
|
70
|
+
def merge
|
|
71
|
+
state.merged = 'both done'
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
gate = GateFlow.new
|
|
76
|
+
gate.kickoff
|
|
77
|
+
puts "merged: #{gate.state.merged.inspect} (and_ waited for both starts)"
|
|
78
|
+
|
|
79
|
+
puts "\n== Persistence round-trip =="
|
|
80
|
+
require 'tmpdir'
|
|
81
|
+
store = RCrewAI::Flow::FileStateStore.new(File.join(Dir.tmpdir, 'rcrewai-flow-demo'))
|
|
82
|
+
|
|
83
|
+
original = ArticleFlow.new(state_store: store)
|
|
84
|
+
original.kickoff
|
|
85
|
+
id = original.state.id
|
|
86
|
+
|
|
87
|
+
resumed = ArticleFlow.new(state_store: store)
|
|
88
|
+
resumed.restore(id)
|
|
89
|
+
puts "restored status for #{id[0, 8]}...: #{resumed.state.status.inspect}"
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Knowledge (RAG) — ground agents in your own documents.
|
|
5
|
+
#
|
|
6
|
+
# Sources (strings, files, PDFs, CSVs, URLs) are chunked, embedded, and stored
|
|
7
|
+
# in an in-memory cosine-similarity vector store. At execution time the most
|
|
8
|
+
# relevant chunks are injected into the agent's task prompt.
|
|
9
|
+
#
|
|
10
|
+
# This example uses a fake, deterministic embedder so it runs WITHOUT an API
|
|
11
|
+
# key. In real use you'd omit `embedder:` and let it default to OpenAI's
|
|
12
|
+
# text-embedding-3-small (set OPENAI_API_KEY).
|
|
13
|
+
#
|
|
14
|
+
# Run:
|
|
15
|
+
# ruby examples/knowledge_rag_example.rb
|
|
16
|
+
|
|
17
|
+
require_relative '../lib/rcrewai'
|
|
18
|
+
|
|
19
|
+
# A toy embedder: maps text to a small vector by keyword presence. Any object
|
|
20
|
+
# responding to `embed(texts) -> [[float, ...], ...]` works here.
|
|
21
|
+
class KeywordEmbedder
|
|
22
|
+
KEYWORDS = %w[refund shipping warranty].freeze
|
|
23
|
+
|
|
24
|
+
def embed(texts)
|
|
25
|
+
texts.map do |t|
|
|
26
|
+
lower = t.downcase
|
|
27
|
+
KEYWORDS.map { |kw| lower.include?(kw) ? 1.0 : 0.0 }
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# 1. Build a knowledge base from a few policy snippets.
|
|
33
|
+
knowledge = RCrewAI::Knowledge::Base.new(
|
|
34
|
+
sources: [
|
|
35
|
+
RCrewAI::Knowledge::StringSource.new('Refunds are available within 30 days of purchase.'),
|
|
36
|
+
RCrewAI::Knowledge::StringSource.new('Standard shipping takes 5-7 business days.'),
|
|
37
|
+
RCrewAI::Knowledge::StringSource.new('The warranty covers manufacturing defects for one year.')
|
|
38
|
+
],
|
|
39
|
+
embedder: KeywordEmbedder.new
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
# 2. Retrieve directly (what the agent does under the hood).
|
|
43
|
+
puts '== Direct retrieval =='
|
|
44
|
+
%w[refund shipping warranty].each do |query|
|
|
45
|
+
top = knowledge.search(query, k: 1).first
|
|
46
|
+
puts "#{query.ljust(9)} -> #{top}"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# 3. Attach the knowledge to an agent and see it injected into the prompt.
|
|
50
|
+
puts "\n== Injected into the agent prompt =="
|
|
51
|
+
RCrewAI.configure(validate: false) do |c|
|
|
52
|
+
c.llm_provider = :openai
|
|
53
|
+
c.api_key = 'demo-key' # not used — we only build the prompt below
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
agent = RCrewAI::Agent.new(
|
|
57
|
+
name: 'support',
|
|
58
|
+
role: 'Customer support specialist',
|
|
59
|
+
goal: 'Answer customer questions using company policy',
|
|
60
|
+
knowledge: knowledge
|
|
61
|
+
)
|
|
62
|
+
task = RCrewAI::Task.new(
|
|
63
|
+
name: 'answer',
|
|
64
|
+
description: 'What is the refund policy?',
|
|
65
|
+
agent: agent
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
messages = agent.send(:build_initial_messages, task)
|
|
69
|
+
puts messages.find { |m| m[:role] == 'user' }[:content]
|
|
70
|
+
|
|
71
|
+
# Crew-level knowledge is shared with every agent, e.g.:
|
|
72
|
+
# crew = RCrewAI::Crew.new('support', knowledge: knowledge)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Crew planning, plus the train/test workflows.
|
|
5
|
+
#
|
|
6
|
+
# - planning: true -> a planner pass drafts a per-task plan and
|
|
7
|
+
# folds it into each task's description before
|
|
8
|
+
# execution.
|
|
9
|
+
# - crew.train(...) -> runs the crew repeatedly, collecting feedback
|
|
10
|
+
# after each run and persisting it as JSON.
|
|
11
|
+
# - crew.test(...) -> runs the crew repeatedly and scores each run.
|
|
12
|
+
#
|
|
13
|
+
# This example stubs the planner LLM and the process so it runs WITHOUT an API
|
|
14
|
+
# key, focusing on the wiring.
|
|
15
|
+
#
|
|
16
|
+
# Run:
|
|
17
|
+
# ruby examples/planning_and_training_example.rb
|
|
18
|
+
|
|
19
|
+
require_relative '../lib/rcrewai'
|
|
20
|
+
require 'tmpdir'
|
|
21
|
+
|
|
22
|
+
RCrewAI.configure(validate: false) do |c|
|
|
23
|
+
c.llm_provider = :openai
|
|
24
|
+
c.api_key = 'demo-key'
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# A fake planner client: returns a JSON map of task name -> plan.
|
|
28
|
+
class FakePlanner
|
|
29
|
+
def chat(**)
|
|
30
|
+
{ content: '{"research": "list 3 sources", "summarize": "write 5 bullets"}' }
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
agent = RCrewAI::Agent.new(name: 'analyst', role: 'Analyst', goal: 'Analyze')
|
|
35
|
+
research = RCrewAI::Task.new(name: 'research', description: 'Research the topic', agent: agent)
|
|
36
|
+
summarize = RCrewAI::Task.new(name: 'summarize', description: 'Summarize findings', agent: agent)
|
|
37
|
+
|
|
38
|
+
crew = RCrewAI::Crew.new('analysis', planning: true, planning_llm: FakePlanner.new)
|
|
39
|
+
crew.add_agent(agent)
|
|
40
|
+
crew.add_task(research)
|
|
41
|
+
crew.add_task(summarize)
|
|
42
|
+
|
|
43
|
+
# Stub the actual task execution so the demo needs no live LLM.
|
|
44
|
+
module RCrewAI
|
|
45
|
+
module Process
|
|
46
|
+
class Sequential
|
|
47
|
+
def execute
|
|
48
|
+
[{ status: :completed }]
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
puts '== Planning pass =='
|
|
55
|
+
crew.execute
|
|
56
|
+
puts "research.description:\n #{research.description.gsub("\n", "\n ")}"
|
|
57
|
+
puts "summarize.description:\n #{summarize.description.gsub("\n", "\n ")}"
|
|
58
|
+
|
|
59
|
+
puts "\n== Training (feedback persisted to JSON) =="
|
|
60
|
+
file = File.join(Dir.tmpdir, 'rcrewai-training-demo.json')
|
|
61
|
+
summary = crew.train(
|
|
62
|
+
n_iterations: 3,
|
|
63
|
+
filename: file,
|
|
64
|
+
feedback: ->(iteration, _result) { "run #{iteration}: looked good" }
|
|
65
|
+
)
|
|
66
|
+
puts "iterations: #{summary[:iterations]}, file: #{summary[:filename]}"
|
|
67
|
+
puts File.read(file)
|
|
68
|
+
File.delete(file)
|
|
69
|
+
|
|
70
|
+
puts "\n== Testing (per-run scores) =="
|
|
71
|
+
result = crew.test(n_iterations: 3, scorer: ->(_run) { 90.0 + rand(10) })
|
|
72
|
+
puts "scores: #{result[:scores].inspect}, average: #{result[:average_score]}"
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Structured output, guardrails, and file output on a Task.
|
|
5
|
+
#
|
|
6
|
+
# After the agent produces its answer, a Task can:
|
|
7
|
+
# - validate & coerce it against a JSON schema (output_schema:)
|
|
8
|
+
# - validate & transform it with a guardrail (guardrail:)
|
|
9
|
+
# - write it to disk, optionally as markdown (output_file:, markdown:)
|
|
10
|
+
#
|
|
11
|
+
# Schema/guardrail failures re-run the agent with the error fed back.
|
|
12
|
+
#
|
|
13
|
+
# This example stubs the agent so it runs WITHOUT an API key. In real use the
|
|
14
|
+
# agent calls your configured LLM.
|
|
15
|
+
#
|
|
16
|
+
# Run:
|
|
17
|
+
# ruby examples/structured_output_example.rb
|
|
18
|
+
|
|
19
|
+
require_relative '../lib/rcrewai'
|
|
20
|
+
require 'tmpdir'
|
|
21
|
+
|
|
22
|
+
# A stand-in agent: returns canned responses so we can demonstrate the
|
|
23
|
+
# post-processing pipeline deterministically. A real Agent behaves the same
|
|
24
|
+
# way from the Task's point of view (it returns { content: "..." }).
|
|
25
|
+
class ScriptedAgent
|
|
26
|
+
def initialize(responses)
|
|
27
|
+
@responses = responses
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def tools = []
|
|
31
|
+
|
|
32
|
+
def execute_task(_task)
|
|
33
|
+
{ content: @responses.shift }
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
puts '== Structured output (with a repair retry) =='
|
|
38
|
+
# First response is invalid JSON; the task feeds the error back and retries,
|
|
39
|
+
# and the second response conforms to the schema.
|
|
40
|
+
agent = ScriptedAgent.new(['sorry, not sure', '{"title": "Q3 Report", "words": 1200}'])
|
|
41
|
+
|
|
42
|
+
task = RCrewAI::Task.new(
|
|
43
|
+
name: 'extract',
|
|
44
|
+
description: 'Extract the article title and word count as JSON',
|
|
45
|
+
agent: agent,
|
|
46
|
+
output_schema: {
|
|
47
|
+
type: 'object',
|
|
48
|
+
properties: { title: { type: 'string' }, words: { type: 'integer' } },
|
|
49
|
+
required: ['title']
|
|
50
|
+
}
|
|
51
|
+
)
|
|
52
|
+
task.execute
|
|
53
|
+
puts "structured_output: #{task.structured_output.inspect}"
|
|
54
|
+
puts "raw_result: #{task.raw_result.inspect}"
|
|
55
|
+
|
|
56
|
+
puts "\n== Guardrail (transform + reject/retry) =="
|
|
57
|
+
# The guardrail requires the answer to mention a price; the first attempt does
|
|
58
|
+
# not, so the task re-runs, and the second attempt passes (and is stripped).
|
|
59
|
+
agent = ScriptedAgent.new(['no price yet', ' Final price: $49 '])
|
|
60
|
+
|
|
61
|
+
guardrail = lambda do |output|
|
|
62
|
+
if output.include?('$')
|
|
63
|
+
[true, output.strip] # accept + transform
|
|
64
|
+
else
|
|
65
|
+
[false, 'must include a price'] # reject with a reason (fed back to the agent)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
task = RCrewAI::Task.new(
|
|
70
|
+
name: 'quote',
|
|
71
|
+
description: 'Give the final price',
|
|
72
|
+
agent: agent,
|
|
73
|
+
guardrail: guardrail,
|
|
74
|
+
guardrail_max_retries: 2
|
|
75
|
+
)
|
|
76
|
+
puts "result: #{task.execute.inspect}"
|
|
77
|
+
|
|
78
|
+
puts "\n== File output (markdown) =="
|
|
79
|
+
agent = ScriptedAgent.new(['All systems nominal.'])
|
|
80
|
+
path = File.join(Dir.tmpdir, 'rcrewai-report-demo.md')
|
|
81
|
+
|
|
82
|
+
task = RCrewAI::Task.new(
|
|
83
|
+
name: 'report',
|
|
84
|
+
description: 'Write a status report',
|
|
85
|
+
agent: agent,
|
|
86
|
+
output_file: path,
|
|
87
|
+
markdown: true
|
|
88
|
+
)
|
|
89
|
+
task.execute
|
|
90
|
+
puts "wrote #{path}:"
|
|
91
|
+
puts File.read(path)
|
|
92
|
+
File.delete(path)
|
data/lib/rcrewai/agent.rb
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
require 'logger'
|
|
4
4
|
require_relative 'llm_client'
|
|
5
5
|
require_relative 'memory'
|
|
6
|
+
require_relative 'rate_limiter'
|
|
7
|
+
require_relative 'agent_augmentations'
|
|
8
|
+
require_relative 'multimodal'
|
|
6
9
|
require_relative 'tools/base'
|
|
7
10
|
require_relative 'tool_runner'
|
|
8
11
|
require_relative 'legacy_react_runner'
|
|
@@ -11,7 +14,8 @@ require_relative 'human_input'
|
|
|
11
14
|
module RCrewAI
|
|
12
15
|
class Agent
|
|
13
16
|
include HumanInteractionExtensions
|
|
14
|
-
|
|
17
|
+
include AgentAugmentations
|
|
18
|
+
attr_reader :name, :role, :goal, :backstory, :tools, :memory, :llm_client, :knowledge, :rate_limiter
|
|
15
19
|
attr_accessor :verbose, :allow_delegation, :max_iterations, :max_execution_time, :manager
|
|
16
20
|
# Set by the crew so agents see shared knowledge in addition to their own.
|
|
17
21
|
attr_writer :crew_knowledge
|
|
@@ -32,8 +36,12 @@ module RCrewAI
|
|
|
32
36
|
@require_approval_for_final_answer = options.fetch(:require_approval_for_final_answer, false)
|
|
33
37
|
@logger = Logger.new($stdout)
|
|
34
38
|
@logger.level = verbose ? Logger::DEBUG : Logger::INFO
|
|
39
|
+
@reasoning = options.fetch(:reasoning, false)
|
|
40
|
+
@max_reasoning_attempts = options.fetch(:max_reasoning_attempts, 3)
|
|
41
|
+
@respect_context_window = options.fetch(:respect_context_window, false)
|
|
35
42
|
@memory = Memory.new
|
|
36
|
-
@
|
|
43
|
+
@rate_limiter = options[:max_rpm] ? RateLimiter.new(max_rpm: options[:max_rpm]) : nil
|
|
44
|
+
@llm_client = wrap_with_rate_limiter(build_llm_client(options[:llm]))
|
|
37
45
|
@knowledge = build_knowledge(options[:knowledge], options[:knowledge_sources])
|
|
38
46
|
@subordinates = [] # For manager agents
|
|
39
47
|
end
|
|
@@ -46,6 +54,9 @@ module RCrewAI
|
|
|
46
54
|
initial_messages = build_initial_messages(task)
|
|
47
55
|
sink = stream || ->(_) {}
|
|
48
56
|
|
|
57
|
+
reasoning = reasoning? ? run_reasoning_pass(task) : nil
|
|
58
|
+
initial_messages = inject_reasoning(initial_messages, reasoning) if reasoning
|
|
59
|
+
|
|
49
60
|
runner_class = pick_runner_class
|
|
50
61
|
@logger.info "[rcrewai] agent=#{name} runner=#{runner_class.name.split('::').last}"
|
|
51
62
|
|
|
@@ -63,7 +74,7 @@ module RCrewAI
|
|
|
63
74
|
memory.add_execution(task, result_string, execution_time)
|
|
64
75
|
task.result = result_string
|
|
65
76
|
|
|
66
|
-
build_task_result(task, runner_result)
|
|
77
|
+
build_task_result(task, runner_result, reasoning: reasoning)
|
|
67
78
|
rescue StandardError => e
|
|
68
79
|
@logger.error "Task execution failed: #{e.message}"
|
|
69
80
|
task.result = "Task failed: #{e.message}"
|
|
@@ -202,6 +213,13 @@ module RCrewAI
|
|
|
202
213
|
LLMClient.resolve(llm)
|
|
203
214
|
end
|
|
204
215
|
|
|
216
|
+
# Wraps the client so every #chat is throttled, when a rate limiter is set.
|
|
217
|
+
def wrap_with_rate_limiter(client)
|
|
218
|
+
return client unless @rate_limiter
|
|
219
|
+
|
|
220
|
+
RateLimiter::ThrottledClient.new(client, @rate_limiter)
|
|
221
|
+
end
|
|
222
|
+
|
|
205
223
|
# Accepts a pre-built Knowledge::Base via +knowledge:+ or an array of
|
|
206
224
|
# sources via +knowledge_sources:+ (wrapped in a Base). Returns nil if
|
|
207
225
|
# neither is given.
|
|
@@ -249,10 +267,20 @@ module RCrewAI
|
|
|
249
267
|
|
|
250
268
|
[
|
|
251
269
|
{ role: 'system', content: system },
|
|
252
|
-
{ role: 'user', content: user }
|
|
270
|
+
{ role: 'user', content: build_user_content(user, task) }
|
|
253
271
|
]
|
|
254
272
|
end
|
|
255
273
|
|
|
274
|
+
# Returns a plain string, or an OpenAI-style multimodal parts array when the
|
|
275
|
+
# task carries attachments (guarded to providers that support it).
|
|
276
|
+
def build_user_content(text, task)
|
|
277
|
+
attachments = task.respond_to?(:attachments) ? task.attachments : nil
|
|
278
|
+
return text if attachments.nil? || attachments.empty?
|
|
279
|
+
|
|
280
|
+
Multimodal.ensure_supported_provider!(RCrewAI.configuration.llm_provider)
|
|
281
|
+
Multimodal.content_parts(text, attachments)
|
|
282
|
+
end
|
|
283
|
+
|
|
256
284
|
# Retrieves knowledge chunks relevant to the task from the agent's own
|
|
257
285
|
# knowledge base and/or the crew-level base injected via #knowledge=.
|
|
258
286
|
def retrieve_knowledge(task)
|
|
@@ -266,7 +294,7 @@ module RCrewAI
|
|
|
266
294
|
''
|
|
267
295
|
end
|
|
268
296
|
|
|
269
|
-
def build_task_result(task, runner_result)
|
|
297
|
+
def build_task_result(task, runner_result, reasoning: nil)
|
|
270
298
|
{
|
|
271
299
|
task: task.name,
|
|
272
300
|
agent: name,
|
|
@@ -274,10 +302,14 @@ module RCrewAI
|
|
|
274
302
|
tool_calls_history: runner_result[:tool_calls_history] || [],
|
|
275
303
|
usage: runner_result[:usage] || {},
|
|
276
304
|
iterations: runner_result[:iterations],
|
|
277
|
-
finish_reason: runner_result[:finish_reason]
|
|
305
|
+
finish_reason: runner_result[:finish_reason],
|
|
306
|
+
reasoning: reasoning
|
|
278
307
|
}
|
|
279
308
|
end
|
|
280
309
|
|
|
310
|
+
# Asks the LLM to think through an approach before answering. Retries up to
|
|
311
|
+
# @max_reasoning_attempts if the model returns empty output; returns nil if
|
|
312
|
+
# every attempt is empty (execution then proceeds without a plan).
|
|
281
313
|
def pick_runner_class
|
|
282
314
|
schemas_ok = @tools.empty? || @tools.all? { |t| t.respond_to?(:json_schema) && t.json_schema }
|
|
283
315
|
native = @llm_client.respond_to?(:supports_native_tools?) &&
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'context_window'
|
|
4
|
+
|
|
5
|
+
module RCrewAI
|
|
6
|
+
# Optional per-task augmentations mixed into Agent: a reasoning/planning pass
|
|
7
|
+
# before answering, and context-window trimming of the message history.
|
|
8
|
+
# Kept in a module so Agent's core stays focused.
|
|
9
|
+
module AgentAugmentations
|
|
10
|
+
def reasoning?
|
|
11
|
+
@reasoning
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def respect_context_window?
|
|
15
|
+
@respect_context_window
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Trims a message list to fit the model's context window when the agent has
|
|
19
|
+
# respect_context_window enabled; otherwise returns it unchanged. Called by
|
|
20
|
+
# the runners before each LLM call.
|
|
21
|
+
def fit_context(messages)
|
|
22
|
+
return messages unless @respect_context_window
|
|
23
|
+
|
|
24
|
+
limit = ContextWindow.window_for(llm_model_name)
|
|
25
|
+
reserve = [RCrewAI.configuration.max_tokens.to_i, 0].max
|
|
26
|
+
ContextWindow.fit(messages, limit: limit, reserve: reserve)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
# Asks the LLM to think through an approach before answering. Retries up to
|
|
32
|
+
# @max_reasoning_attempts if the model returns empty output; returns nil if
|
|
33
|
+
# every attempt is empty (execution then proceeds without a plan).
|
|
34
|
+
def run_reasoning_pass(task)
|
|
35
|
+
prompt = <<~PROMPT
|
|
36
|
+
You are #{role}. Before answering, think step by step about how to best
|
|
37
|
+
accomplish this task. Produce a short, concrete plan (do not answer yet).
|
|
38
|
+
|
|
39
|
+
Task: #{task.description}
|
|
40
|
+
Expected Output: #{task.expected_output || 'not specified'}
|
|
41
|
+
PROMPT
|
|
42
|
+
|
|
43
|
+
@max_reasoning_attempts.times do
|
|
44
|
+
response = @llm_client.chat(messages: [{ role: 'user', content: prompt }])
|
|
45
|
+
text = (response.is_a?(Hash) ? response[:content] : response).to_s.strip
|
|
46
|
+
return text unless text.empty?
|
|
47
|
+
end
|
|
48
|
+
nil
|
|
49
|
+
rescue StandardError => e
|
|
50
|
+
@logger.warn("Reasoning pass failed: #{e.message}")
|
|
51
|
+
nil
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Adds the reasoning trace to the user message so the answer pass can use it.
|
|
55
|
+
def inject_reasoning(messages, reasoning)
|
|
56
|
+
messages.map do |msg|
|
|
57
|
+
next msg unless msg[:role] == 'user'
|
|
58
|
+
|
|
59
|
+
{ role: 'user', content: "#{msg[:content]}\n\nYour plan:\n#{reasoning}" }
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Best-effort model name from the (possibly wrapped) client, for context
|
|
64
|
+
# window sizing. Falls back to the global configured model.
|
|
65
|
+
def llm_model_name
|
|
66
|
+
if @llm_client.respond_to?(:config) && @llm_client.config.respond_to?(:model)
|
|
67
|
+
@llm_client.config.model
|
|
68
|
+
else
|
|
69
|
+
RCrewAI.configuration.model
|
|
70
|
+
end
|
|
71
|
+
rescue StandardError
|
|
72
|
+
RCrewAI.configuration.model
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RCrewAI
|
|
4
|
+
# Keeps a conversation within a model's context window by dropping the oldest
|
|
5
|
+
# non-system messages when it would overflow. Token counts use a cheap
|
|
6
|
+
# chars/4 heuristic (no tokenizer dependency); the goal is to avoid hard
|
|
7
|
+
# context-length errors, not exact accounting.
|
|
8
|
+
module ContextWindow
|
|
9
|
+
CHARS_PER_TOKEN = 4
|
|
10
|
+
DEFAULT_WINDOW = 8_192
|
|
11
|
+
|
|
12
|
+
# Approximate context window sizes (in tokens) by model.
|
|
13
|
+
WINDOWS = {
|
|
14
|
+
'gpt-4o' => 128_000,
|
|
15
|
+
'gpt-4o-mini' => 128_000,
|
|
16
|
+
'gpt-4-turbo' => 128_000,
|
|
17
|
+
'gpt-4' => 8_192,
|
|
18
|
+
'gpt-3.5-turbo' => 16_385,
|
|
19
|
+
'claude-opus-4-7' => 200_000,
|
|
20
|
+
'claude-sonnet-4-6' => 200_000,
|
|
21
|
+
'claude-haiku-4-5' => 200_000,
|
|
22
|
+
'claude-3-5-sonnet-20241022' => 200_000,
|
|
23
|
+
'claude-3-haiku-20240307' => 200_000,
|
|
24
|
+
'gemini-1.5-pro' => 1_000_000,
|
|
25
|
+
'gemini-1.5-flash' => 1_000_000
|
|
26
|
+
}.freeze
|
|
27
|
+
|
|
28
|
+
module_function
|
|
29
|
+
|
|
30
|
+
def estimate_tokens(input)
|
|
31
|
+
text = input.is_a?(Array) ? input.map { |m| m[:content].to_s }.join : input.to_s
|
|
32
|
+
(text.length / CHARS_PER_TOKEN.to_f).ceil
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def window_for(model)
|
|
36
|
+
WINDOWS[model] || DEFAULT_WINDOW
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Returns a copy of +messages+ trimmed to fit within (limit - reserve)
|
|
40
|
+
# tokens. System messages are always kept, as is the final message. The
|
|
41
|
+
# oldest non-system, non-final messages are dropped first.
|
|
42
|
+
def fit(messages, limit:, reserve: 0)
|
|
43
|
+
budget = limit - reserve
|
|
44
|
+
return messages if estimate_tokens(messages) <= budget
|
|
45
|
+
|
|
46
|
+
system = messages.select { |m| m[:role] == 'system' }
|
|
47
|
+
last = messages.last
|
|
48
|
+
# Candidates for dropping: everything that isn't a system message or the
|
|
49
|
+
# final message, oldest first.
|
|
50
|
+
middle = messages.reject { |m| m[:role] == 'system' || m.equal?(last) }
|
|
51
|
+
|
|
52
|
+
kept_middle = middle.dup
|
|
53
|
+
until fits?(system, kept_middle, last, budget) || kept_middle.empty?
|
|
54
|
+
kept_middle.shift # drop the oldest
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
rebuild(messages, system, kept_middle, last)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# -- helpers --------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
def fits?(system, middle, last, budget)
|
|
63
|
+
parts = system + middle
|
|
64
|
+
parts << last unless system.include?(last) || middle.include?(last)
|
|
65
|
+
estimate_tokens(parts) <= budget
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def rebuild(original, system, middle, last)
|
|
69
|
+
keep = (system + middle)
|
|
70
|
+
keep << last unless keep.include?(last)
|
|
71
|
+
# Preserve original ordering.
|
|
72
|
+
original.select { |m| keep.include?(m) }
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
data/lib/rcrewai/crew.rb
CHANGED
|
@@ -23,16 +23,33 @@ module RCrewAI
|
|
|
23
23
|
@planning_llm = options[:planning_llm]
|
|
24
24
|
@planned = false
|
|
25
25
|
@knowledge = build_knowledge(options[:knowledge], options[:knowledge_sources])
|
|
26
|
+
@before_kickoff_hooks = []
|
|
27
|
+
@after_kickoff_hooks = []
|
|
28
|
+
@last_inputs = {}
|
|
26
29
|
@process_instance = nil
|
|
27
30
|
validate_process_type!
|
|
28
31
|
end
|
|
29
32
|
|
|
30
|
-
attr_reader :knowledge, :stream_sink
|
|
33
|
+
attr_reader :knowledge, :stream_sink, :last_inputs
|
|
31
34
|
|
|
32
35
|
def planning?
|
|
33
36
|
@planning
|
|
34
37
|
end
|
|
35
38
|
|
|
39
|
+
# Register a callback run before execution. Receives the inputs hash and may
|
|
40
|
+
# return a transformed hash. Multiple hooks run in registration order.
|
|
41
|
+
def before_kickoff(&block)
|
|
42
|
+
@before_kickoff_hooks << block
|
|
43
|
+
self
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Register a callback run after execution. Receives the result and may
|
|
47
|
+
# return a transformed result. Multiple hooks run in registration order.
|
|
48
|
+
def after_kickoff(&block)
|
|
49
|
+
@after_kickoff_hooks << block
|
|
50
|
+
self
|
|
51
|
+
end
|
|
52
|
+
|
|
36
53
|
def add_agent(agent)
|
|
37
54
|
@agents << agent
|
|
38
55
|
end
|
|
@@ -41,20 +58,25 @@ module RCrewAI
|
|
|
41
58
|
@tasks << task
|
|
42
59
|
end
|
|
43
60
|
|
|
44
|
-
def execute(async: false, stream: nil, **async_options, &block)
|
|
61
|
+
def execute(async: false, stream: nil, inputs: {}, **async_options, &block)
|
|
45
62
|
sinks = []
|
|
46
63
|
sinks << block if block_given?
|
|
47
64
|
Array(stream).each { |s| sinks << s } if stream
|
|
48
65
|
@stream_sink = sinks.empty? ? nil : RCrewAI::Events.fan_out(sinks)
|
|
49
66
|
|
|
67
|
+
run_before_hooks(inputs)
|
|
68
|
+
|
|
50
69
|
distribute_knowledge if @knowledge
|
|
51
70
|
run_planning_pass if planning?
|
|
52
71
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
72
|
+
result = async ? execute_async(**async_options) : execute_sync
|
|
73
|
+
run_after_hooks(result)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Runs the crew once per input set, returning one result per input in order.
|
|
77
|
+
# Runs are isolated: each execution starts from only its own inputs.
|
|
78
|
+
def kickoff_for_each(inputs:)
|
|
79
|
+
Array(inputs).map { |input| execute(inputs: input) }
|
|
58
80
|
end
|
|
59
81
|
|
|
60
82
|
# Runs the crew repeatedly, collecting feedback after each iteration and
|
|
@@ -144,6 +166,22 @@ module RCrewAI
|
|
|
144
166
|
|
|
145
167
|
private
|
|
146
168
|
|
|
169
|
+
def run_before_hooks(inputs)
|
|
170
|
+
# Assign before running hooks so a hook that reads #last_inputs sees this
|
|
171
|
+
# run's own inputs; update it as each hook transforms them.
|
|
172
|
+
@last_inputs = inputs || {}
|
|
173
|
+
@before_kickoff_hooks.each do |hook|
|
|
174
|
+
@last_inputs = hook.call(@last_inputs) || @last_inputs
|
|
175
|
+
end
|
|
176
|
+
@last_inputs
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def run_after_hooks(result)
|
|
180
|
+
@after_kickoff_hooks.reduce(result) do |acc, hook|
|
|
181
|
+
hook.call(acc) || acc
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
147
185
|
def build_knowledge(knowledge, sources)
|
|
148
186
|
return knowledge if knowledge
|
|
149
187
|
return nil if sources.nil? || sources.empty?
|
|
@@ -30,7 +30,7 @@ module RCrewAI
|
|
|
30
30
|
iter += 1
|
|
31
31
|
emit(Events::IterationStart, iteration: iter, iteration_index: iter)
|
|
32
32
|
|
|
33
|
-
response = @llm.chat(messages: msgs)
|
|
33
|
+
response = @llm.chat(messages: fit_context(msgs))
|
|
34
34
|
accumulate_usage(total_usage, response[:usage])
|
|
35
35
|
reasoning = response[:content] || ''
|
|
36
36
|
last_reasoning = reasoning
|
|
@@ -60,6 +60,12 @@ module RCrewAI
|
|
|
60
60
|
|
|
61
61
|
private
|
|
62
62
|
|
|
63
|
+
# Trims the message list to the model's context window when the agent
|
|
64
|
+
# supports it; a no-op otherwise.
|
|
65
|
+
def fit_context(messages)
|
|
66
|
+
@agent.respond_to?(:fit_context) ? @agent.fit_context(messages) : messages
|
|
67
|
+
end
|
|
68
|
+
|
|
63
69
|
def parse_and_execute_actions(reasoning, iter)
|
|
64
70
|
results = []
|
|
65
71
|
iteration_history = []
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'base64'
|
|
4
|
+
|
|
5
|
+
module RCrewAI
|
|
6
|
+
# Builds multimodal message content (text + images) in the OpenAI
|
|
7
|
+
# chat-completions format:
|
|
8
|
+
# [{ type: 'text', text: '...' },
|
|
9
|
+
# { type: 'image_url', image_url: { url: '...' } }]
|
|
10
|
+
#
|
|
11
|
+
# Local image paths are base64-encoded into data URLs; URLs pass through.
|
|
12
|
+
# Only OpenAI-style multimodal is supported today; other providers raise.
|
|
13
|
+
module Multimodal
|
|
14
|
+
SUPPORTED_PROVIDERS = %i[openai azure].freeze
|
|
15
|
+
|
|
16
|
+
MIME_TYPES = {
|
|
17
|
+
'.png' => 'image/png',
|
|
18
|
+
'.jpg' => 'image/jpeg',
|
|
19
|
+
'.jpeg' => 'image/jpeg',
|
|
20
|
+
'.gif' => 'image/gif',
|
|
21
|
+
'.webp' => 'image/webp'
|
|
22
|
+
}.freeze
|
|
23
|
+
|
|
24
|
+
module_function
|
|
25
|
+
|
|
26
|
+
# Returns an OpenAI-style content-parts array for the given text and
|
|
27
|
+
# attachments. With no attachments this is a single text part.
|
|
28
|
+
def content_parts(text, attachments)
|
|
29
|
+
parts = [{ type: 'text', text: text.to_s }]
|
|
30
|
+
Array(attachments).each { |att| parts << image_part(att) }
|
|
31
|
+
parts
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def supported_provider?(provider)
|
|
35
|
+
SUPPORTED_PROVIDERS.include?(provider.to_sym)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def ensure_supported_provider!(provider)
|
|
39
|
+
return if supported_provider?(provider)
|
|
40
|
+
|
|
41
|
+
raise UnsupportedProviderError,
|
|
42
|
+
"multimodal attachments are not supported for provider #{provider}"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def image_part(attachment)
|
|
46
|
+
type = attachment[:type] || attachment['type']
|
|
47
|
+
raise UnsupportedAttachmentError, "unsupported attachment type: #{type.inspect}" unless type.to_sym == :image
|
|
48
|
+
|
|
49
|
+
url = attachment[:url] || attachment['url']
|
|
50
|
+
path = attachment[:path] || attachment['path']
|
|
51
|
+
resolved = url || data_url_for(path)
|
|
52
|
+
|
|
53
|
+
{ type: 'image_url', image_url: { url: resolved } }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def data_url_for(path)
|
|
57
|
+
raise UnsupportedAttachmentError, 'image attachment needs a :url or :path' unless path
|
|
58
|
+
|
|
59
|
+
mime = MIME_TYPES[File.extname(path).downcase] || 'application/octet-stream'
|
|
60
|
+
encoded = Base64.strict_encode64(File.binread(path))
|
|
61
|
+
"data:#{mime};base64,#{encoded}"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
class UnsupportedAttachmentError < RCrewAI::Error; end
|
|
65
|
+
class UnsupportedProviderError < RCrewAI::Error; end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RCrewAI
|
|
4
|
+
# Thread-safe requests-per-minute throttle. Records call timestamps in a
|
|
5
|
+
# rolling 60-second window; `acquire` blocks (sleeps) until a slot is free.
|
|
6
|
+
#
|
|
7
|
+
# The clock and sleeper are injectable so tests can drive time deterministically
|
|
8
|
+
# without touching the wall clock. `max_rpm` of nil or 0 means unlimited.
|
|
9
|
+
class RateLimiter
|
|
10
|
+
WINDOW = 60.0
|
|
11
|
+
|
|
12
|
+
def initialize(max_rpm:, clock: nil, sleeper: nil)
|
|
13
|
+
@max_rpm = max_rpm
|
|
14
|
+
@clock = clock || -> { current_time }
|
|
15
|
+
@sleeper = sleeper || ->(seconds) { sleep(seconds) }
|
|
16
|
+
@calls = []
|
|
17
|
+
@mutex = Mutex.new
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Blocks until making a call would keep us within max_rpm, then records it.
|
|
21
|
+
def acquire
|
|
22
|
+
return record_unlimited if unlimited?
|
|
23
|
+
|
|
24
|
+
loop do
|
|
25
|
+
wait = @mutex.synchronize do
|
|
26
|
+
prune(@clock.call)
|
|
27
|
+
if @calls.length < @max_rpm
|
|
28
|
+
@calls << @clock.call
|
|
29
|
+
return
|
|
30
|
+
end
|
|
31
|
+
# Time until the oldest in-window call ages out.
|
|
32
|
+
(@calls.first + WINDOW) - @clock.call
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
@sleeper.call(wait) if wait.positive?
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Number of calls currently inside the window (useful for tests/metrics).
|
|
40
|
+
def recent_count
|
|
41
|
+
@mutex.synchronize do
|
|
42
|
+
prune(@clock.call)
|
|
43
|
+
@calls.length
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def unlimited?
|
|
50
|
+
@max_rpm.nil? || @max_rpm.zero?
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def record_unlimited
|
|
54
|
+
@mutex.synchronize { @calls << @clock.call }
|
|
55
|
+
nil
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def prune(now)
|
|
59
|
+
cutoff = now - WINDOW
|
|
60
|
+
@calls.reject! { |t| t <= cutoff }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def current_time
|
|
64
|
+
# Fully-qualified: bare `Process` would resolve to RCrewAI::Process here.
|
|
65
|
+
::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Wraps an LLM client so every #chat acquires a rate-limiter slot first.
|
|
69
|
+
# All other messages delegate to the wrapped client unchanged.
|
|
70
|
+
class ThrottledClient
|
|
71
|
+
def initialize(client, limiter)
|
|
72
|
+
@client = client
|
|
73
|
+
@limiter = limiter
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def chat(**kwargs, &block)
|
|
77
|
+
@limiter.acquire
|
|
78
|
+
@client.chat(**kwargs, &block)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def respond_to_missing?(name, include_private = false)
|
|
82
|
+
@client.respond_to?(name, include_private)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def method_missing(name, *args, **kwargs, &block)
|
|
86
|
+
if @client.respond_to?(name)
|
|
87
|
+
@client.public_send(name, *args, **kwargs, &block)
|
|
88
|
+
else
|
|
89
|
+
super
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
data/lib/rcrewai/task.rb
CHANGED
|
@@ -9,7 +9,7 @@ module RCrewAI
|
|
|
9
9
|
include AsyncExtensions
|
|
10
10
|
include HumanInteractionExtensions
|
|
11
11
|
attr_reader :name, :description, :agent, :context, :expected_output, :tools, :async,
|
|
12
|
-
:raw_result, :structured_output
|
|
12
|
+
:raw_result, :structured_output, :attachments
|
|
13
13
|
attr_accessor :result, :status, :start_time, :end_time, :execution_time
|
|
14
14
|
|
|
15
15
|
def initialize(name:, description:, agent: nil, **options)
|
|
@@ -21,6 +21,7 @@ module RCrewAI
|
|
|
21
21
|
@tools = options[:tools] || [] # Additional tools for this specific task
|
|
22
22
|
@async = options[:async] || false # Whether task can run asynchronously
|
|
23
23
|
@callback = options[:callback] # Callback function after completion
|
|
24
|
+
@attachments = options[:attachments] || [] # Multimodal inputs (images)
|
|
24
25
|
|
|
25
26
|
# Output processing (0.4.0)
|
|
26
27
|
@output_schema = options[:output_schema] # JSON-schema for structured output
|
data/lib/rcrewai/tool_runner.rb
CHANGED
|
@@ -27,7 +27,7 @@ module RCrewAI
|
|
|
27
27
|
emit(Events::IterationStart, iteration: iter, iteration_index: iter)
|
|
28
28
|
|
|
29
29
|
response = @llm.chat(
|
|
30
|
-
messages: msgs,
|
|
30
|
+
messages: fit_context(msgs),
|
|
31
31
|
tools: @tools.map(&:json_schema),
|
|
32
32
|
stream: ->(e) { @sink.call(retag(e, iter)) }
|
|
33
33
|
)
|
|
@@ -80,6 +80,12 @@ module RCrewAI
|
|
|
80
80
|
|
|
81
81
|
private
|
|
82
82
|
|
|
83
|
+
# Trims the message list to the model's context window when the agent
|
|
84
|
+
# supports it; a no-op otherwise.
|
|
85
|
+
def fit_context(messages)
|
|
86
|
+
@agent.respond_to?(:fit_context) ? @agent.fit_context(messages) : messages
|
|
87
|
+
end
|
|
88
|
+
|
|
83
89
|
def tool_result_message(call_id, content)
|
|
84
90
|
{ role: 'tool', tool_call_id: call_id, content: content }
|
|
85
91
|
end
|
data/lib/rcrewai/version.rb
CHANGED
data/lib/rcrewai.rb
CHANGED
|
@@ -23,6 +23,9 @@ require_relative 'rcrewai/sse_parser'
|
|
|
23
23
|
require_relative 'rcrewai/pricing'
|
|
24
24
|
require_relative 'rcrewai/llm_client'
|
|
25
25
|
require_relative 'rcrewai/memory'
|
|
26
|
+
require_relative 'rcrewai/rate_limiter'
|
|
27
|
+
require_relative 'rcrewai/context_window'
|
|
28
|
+
require_relative 'rcrewai/multimodal'
|
|
26
29
|
require_relative 'rcrewai/knowledge'
|
|
27
30
|
require_relative 'rcrewai/human_input'
|
|
28
31
|
require_relative 'rcrewai/tool_schema'
|
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.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- gkosmo
|
|
@@ -349,17 +349,24 @@ files:
|
|
|
349
349
|
- docs/tutorials/index.md
|
|
350
350
|
- docs/tutorials/multiple-crews.md
|
|
351
351
|
- docs/upgrading-to-0.3.md
|
|
352
|
+
- docs/upgrading-to-0.4.md
|
|
352
353
|
- examples/async_execution_example.rb
|
|
354
|
+
- examples/flow_example.rb
|
|
353
355
|
- examples/hierarchical_crew_example.rb
|
|
354
356
|
- examples/human_in_the_loop_example.rb
|
|
357
|
+
- examples/knowledge_rag_example.rb
|
|
355
358
|
- examples/mcp_example.rb
|
|
356
359
|
- examples/native_tools_example.rb
|
|
360
|
+
- examples/planning_and_training_example.rb
|
|
357
361
|
- examples/streaming_example.rb
|
|
362
|
+
- examples/structured_output_example.rb
|
|
358
363
|
- lib/rcrewai.rb
|
|
359
364
|
- lib/rcrewai/agent.rb
|
|
365
|
+
- lib/rcrewai/agent_augmentations.rb
|
|
360
366
|
- lib/rcrewai/async_executor.rb
|
|
361
367
|
- lib/rcrewai/cli.rb
|
|
362
368
|
- lib/rcrewai/configuration.rb
|
|
369
|
+
- lib/rcrewai/context_window.rb
|
|
363
370
|
- lib/rcrewai/crew.rb
|
|
364
371
|
- lib/rcrewai/events.rb
|
|
365
372
|
- lib/rcrewai/flow.rb
|
|
@@ -386,11 +393,13 @@ files:
|
|
|
386
393
|
- lib/rcrewai/mcp/transport/http.rb
|
|
387
394
|
- lib/rcrewai/mcp/transport/stdio.rb
|
|
388
395
|
- lib/rcrewai/memory.rb
|
|
396
|
+
- lib/rcrewai/multimodal.rb
|
|
389
397
|
- lib/rcrewai/output_schema.rb
|
|
390
398
|
- lib/rcrewai/planning.rb
|
|
391
399
|
- lib/rcrewai/pricing.rb
|
|
392
400
|
- lib/rcrewai/process.rb
|
|
393
401
|
- lib/rcrewai/provider_schema.rb
|
|
402
|
+
- lib/rcrewai/rate_limiter.rb
|
|
394
403
|
- lib/rcrewai/sse_parser.rb
|
|
395
404
|
- lib/rcrewai/task.rb
|
|
396
405
|
- lib/rcrewai/tool_runner.rb
|