rcrewai-rails 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +41 -1
- data/README.md +149 -3
- data/app/controllers/rcrewai/rails/observations_controller.rb +27 -0
- data/app/jobs/rcrewai/rails/crew_execution_job.rb +18 -29
- data/app/models/rcrewai/rails/execution.rb +6 -0
- data/app/models/rcrewai/rails/span.rb +57 -0
- data/app/models/rcrewai/rails/span_event.rb +19 -0
- data/app/views/rcrewai/rails/observations/_span.html.erb +51 -0
- data/app/views/rcrewai/rails/observations/costs.html.erb +52 -0
- data/app/views/rcrewai/rails/observations/show.html.erb +47 -0
- data/config/routes.rb +6 -0
- data/db/migrate/010_create_rcrewai_spans.rb +42 -0
- data/db/migrate/011_add_observation_rollups_to_rcrewai_executions.rb +8 -0
- data/docs/api.html +223 -0
- data/docs/capabilities.html +268 -0
- data/docs/examples.html +257 -0
- data/docs/getting-started.html +275 -0
- data/docs/index.html +57 -207
- data/lib/generators/rcrewai/rails/install/install_generator.rb +28 -16
- data/lib/generators/rcrewai/rails/install/templates/create_rcrewai_tables.rb +60 -0
- data/lib/generators/rcrewai/rails/install/templates/rcrewai.rb +17 -0
- data/lib/rcrewai/rails/configuration.rb +10 -1
- data/lib/rcrewai/rails/observation/collector.rb +301 -0
- data/lib/rcrewai/rails/observation/pruner.rb +48 -0
- data/lib/rcrewai/rails/observation/rollup.rb +60 -0
- data/lib/rcrewai/rails/observation/span_stack.rb +76 -0
- data/lib/rcrewai/rails/observation/writer.rb +89 -0
- data/lib/rcrewai/rails/version.rb +1 -1
- data/lib/rcrewai/rails.rb +13 -0
- data/lib/tasks/rcrewai_observation.rake +10 -0
- data/rcrewai-rails.gemspec +1 -1
- metadata +21 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e21a3a1057f9221cca3d280f2307b219d15a636995284c781404c4ed3d283fa6
|
|
4
|
+
data.tar.gz: 9b715b0cc17839eb5fb23f578905d962c4b6450d0aae2b8b1f494d549497a8b3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1be59f83c847e48c36b0ac61527ca9dedc61b903763be6145d6dd26354575a74d2dee3fc807a804e6f847660347c8368a28d70f33df6cb57d75d2b952b98657f
|
|
7
|
+
data.tar.gz: 0f37c9c304ef0264afeb363f07ca1b23c72045d9686869fe9325e336a79f30bc1930d08f97470a6235ad7d4dc8d1e5ebab4c78eb3de9d9ae1129ea42cb48dca8
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,44 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.7.0] - 2026-08-13
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Observation engine: span-tree tracing of every crew execution with per-agent,
|
|
14
|
+
per-LLM-call and per-tool-call detail (timings, token counts, cost). Includes a
|
|
15
|
+
trace waterfall at `/rcrewai/executions/:id/observation`, a cost/performance
|
|
16
|
+
dashboard at `/rcrewai/observations/costs`, live monitoring over Turbo Streams,
|
|
17
|
+
and a `rcrewai:observation:prune` rake task with retention configuration
|
|
18
|
+
(`observation_retention_days`).
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
- The install generator only wrote an initializer: it never copied a migration
|
|
22
|
+
and never mounted the engine, so following the documented install produced no
|
|
23
|
+
database tables and no reachable UI. It now creates a timestamped
|
|
24
|
+
`create_rcrewai_tables` migration, mounts the engine at `/rcrewai`, and
|
|
25
|
+
installs the fully documented initializer.
|
|
26
|
+
- The install migration template was missing the `rcrewai_tools` table and the
|
|
27
|
+
tasks table's `agent` reference, so a fresh install shipped a `Tool` model with
|
|
28
|
+
no backing table. The template now matches the schema the test suite validates.
|
|
29
|
+
|
|
30
|
+
### Deprecated
|
|
31
|
+
- `Execution#log` and `ExecutionLog`, superseded by the observation engine. Both
|
|
32
|
+
still work and now emit a deprecation warning; scheduled for removal one minor
|
|
33
|
+
version after this release.
|
|
34
|
+
|
|
35
|
+
### Requires
|
|
36
|
+
- `rcrewai >= 0.7.1` for agent-level tracing. On earlier 0.7.x versions traces
|
|
37
|
+
contain only crew-level spans.
|
|
38
|
+
|
|
39
|
+
## [0.6.1] - 2026-07-07
|
|
40
|
+
|
|
41
|
+
### Fixed
|
|
42
|
+
- Refresh the README for rcrewai 0.7: corrected the install-generator namespace
|
|
43
|
+
(`rcrewai:rails:install`) and removed a broken crew-level `memory_enabled`
|
|
44
|
+
example (that DSL method no longer exists — memory is agent-level). Added a
|
|
45
|
+
"rcrewai 0.7 capabilities" section documenting the new agent/task/crew/
|
|
46
|
+
knowledge/flow configuration. Docs only.
|
|
47
|
+
|
|
10
48
|
## [0.6.0] - 2026-07-07
|
|
11
49
|
|
|
12
50
|
Tracks rcrewai 0.7.0: adds the `:consensual` crew process and agent-level
|
|
@@ -114,7 +152,9 @@ existing agents, tasks, and crews build unchanged.
|
|
|
114
152
|
### Changed
|
|
115
153
|
- Rename generators from `rcrew_a_i` to `rcrewai` namespacing.
|
|
116
154
|
|
|
117
|
-
[Unreleased]: https://github.com/gkosmo/rcrewai-rails/compare/v0.
|
|
155
|
+
[Unreleased]: https://github.com/gkosmo/rcrewai-rails/compare/v0.7.0...HEAD
|
|
156
|
+
[0.7.0]: https://github.com/gkosmo/rcrewai-rails/compare/v0.6.1...v0.7.0
|
|
157
|
+
[0.6.1]: https://github.com/gkosmo/rcrewai-rails/compare/v0.6.0...v0.6.1
|
|
118
158
|
[0.6.0]: https://github.com/gkosmo/rcrewai-rails/compare/v0.5.1...v0.6.0
|
|
119
159
|
[0.5.1]: https://github.com/gkosmo/rcrewai-rails/compare/v0.5.0...v0.5.1
|
|
120
160
|
[0.5.0]: https://github.com/gkosmo/rcrewai-rails/compare/v0.4.0...v0.5.0
|
data/README.md
CHANGED
|
@@ -10,6 +10,55 @@ Rails engine for integrating [RcrewAI](https://github.com/gkosmo/rcrewai-rails)
|
|
|
10
10
|
- **Web UI**: Monitor and manage crews through a built-in interface
|
|
11
11
|
- **Rails-Specific Tools**: Pre-built tools for ActiveRecord, ActionMailer, Rails cache, and more
|
|
12
12
|
- **Configuration**: Flexible configuration through Rails initializers
|
|
13
|
+
- **Full rcrewai 0.7 feature coverage** (see [rcrewai 0.7 capabilities](#rcrewai-07-capabilities)):
|
|
14
|
+
- Agent config: reasoning, per-agent LLM, rate limiting, context-window trimming, cognitive memory
|
|
15
|
+
- Task output: structured output schemas, guardrails, file output, multimodal attachments
|
|
16
|
+
- Crew: `before_kickoff`/`after_kickoff` hooks, planning, the `consensual` process, batch execution
|
|
17
|
+
- Knowledge (RAG) sources and Flow persistence
|
|
18
|
+
|
|
19
|
+
## Observation Engine
|
|
20
|
+
|
|
21
|
+
Every crew execution is traced as a tree of spans — crew, agent, task, LLM call, and
|
|
22
|
+
tool call — carrying timings, token counts, and cost.
|
|
23
|
+
|
|
24
|
+
- **Trace view** at `/rcrewai/executions/:id/observation`: a waterfall of the run, with
|
|
25
|
+
prompts, tool arguments, and errors on each span.
|
|
26
|
+
- **Cost and performance** at `/rcrewai/observations/costs`: spend and token totals
|
|
27
|
+
across recent executions.
|
|
28
|
+
- **Live monitoring**: the trace view updates over Turbo Streams while a run is in progress.
|
|
29
|
+
|
|
30
|
+
Configure it in `config/initializers/rcrewai.rb`:
|
|
31
|
+
|
|
32
|
+
```ruby
|
|
33
|
+
config.observation_enabled = true
|
|
34
|
+
config.observation_capture_prompts = :truncated # :none | :truncated | :full
|
|
35
|
+
config.observation_prompt_max_bytes = 4_096
|
|
36
|
+
config.observation_flush_mode = :batched # :batched | :immediate
|
|
37
|
+
config.observation_retention_days = 30
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Prompt text is truncated by default: full prompts can be large and may contain personal
|
|
41
|
+
data. Set `:full` only when you need lossless replay.
|
|
42
|
+
|
|
43
|
+
Prune old spans with the bundled rake task:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
rake rcrewai:observation:prune # uses observation_retention_days
|
|
47
|
+
rake rcrewai:observation:prune DAYS=7
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Limitations
|
|
51
|
+
|
|
52
|
+
Token and cost data depend on rcrewai's **streaming** execution path. `Usage` events are
|
|
53
|
+
only emitted when an agent runs via `ToolRunner`, which passes a `stream:` to the LLM
|
|
54
|
+
client — not via `LegacyReactRunner`, which does not. `ToolRunner` is selected when the
|
|
55
|
+
tools have JSON schemas **and** the LLM client reports `supports_native_tools?`. OpenAI,
|
|
56
|
+
Anthropic, and Google all report true, so cost capture works normally with them. With a
|
|
57
|
+
provider or configuration that falls back to `LegacyReactRunner` (for example Ollama
|
|
58
|
+
without native tools), **the cost dashboard will be empty rather than showing an error**.
|
|
59
|
+
|
|
60
|
+
Agent-level tracing requires **rcrewai >= 0.7.1**, the version that threads the event
|
|
61
|
+
stream down to agent execution. On earlier versions traces contain only crew-level spans.
|
|
13
62
|
|
|
14
63
|
## Installation
|
|
15
64
|
|
|
@@ -28,7 +77,7 @@ $ bundle install
|
|
|
28
77
|
Run the installation generator:
|
|
29
78
|
|
|
30
79
|
```bash
|
|
31
|
-
$ rails generate
|
|
80
|
+
$ rails generate rcrewai:rails:install
|
|
32
81
|
$ rails db:migrate
|
|
33
82
|
```
|
|
34
83
|
|
|
@@ -103,8 +152,7 @@ class ResearchCrew
|
|
|
103
152
|
|
|
104
153
|
crew_name "research_team"
|
|
105
154
|
crew_description "AI-powered research team"
|
|
106
|
-
process_type :sequential
|
|
107
|
-
memory_enabled true
|
|
155
|
+
process_type :sequential # :sequential, :hierarchical, or :consensual
|
|
108
156
|
|
|
109
157
|
def setup_agents
|
|
110
158
|
@researcher = create_agent("researcher",
|
|
@@ -187,6 +235,101 @@ crew.execute_sync(inputs)
|
|
|
187
235
|
CrewExecutionJob.set(wait: 5.minutes).perform_later(crew, inputs)
|
|
188
236
|
```
|
|
189
237
|
|
|
238
|
+
## rcrewai 0.7 capabilities
|
|
239
|
+
|
|
240
|
+
This engine tracks [rcrewai](https://github.com/gkosmo/rcrewAI) `~> 0.7`. The
|
|
241
|
+
following capabilities are configured through columns on the persisted models and
|
|
242
|
+
forwarded to the core objects at build time. All are **off/absent by default**, so
|
|
243
|
+
existing records are unaffected — set only what you need.
|
|
244
|
+
|
|
245
|
+
### Agent configuration (`RcrewAI::Rails::Agent`)
|
|
246
|
+
|
|
247
|
+
| Column | Effect |
|
|
248
|
+
|---|---|
|
|
249
|
+
| `max_rpm` | Rate-limit the agent's LLM calls (requests per minute) |
|
|
250
|
+
| `reasoning` / `max_reasoning_attempts` | Run a planning/reasoning pass before answering |
|
|
251
|
+
| `respect_context_window` | Trim history to fit the model's context window |
|
|
252
|
+
| `llm_config` (JSON) | Per-agent LLM override, e.g. `{ "provider": "anthropic", "model": "claude-sonnet-5" }` |
|
|
253
|
+
| `memory_enabled` + `memory_scope` + `memory_short_term_limit` | Enable cognitive memory (see below) |
|
|
254
|
+
|
|
255
|
+
```ruby
|
|
256
|
+
agent = crew.agents.create!(
|
|
257
|
+
name: "researcher", role: "Researcher", goal: "Find facts",
|
|
258
|
+
reasoning: true,
|
|
259
|
+
max_rpm: 30,
|
|
260
|
+
llm_config: { provider: "anthropic", model: "claude-sonnet-5" },
|
|
261
|
+
memory_enabled: true, memory_scope: "research", memory_short_term_limit: 20
|
|
262
|
+
)
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
**Agent memory** (rcrewai 0.6+): set `memory_enabled: true` to turn on cognitive
|
|
266
|
+
memory. `memory_scope` isolates an agent's memories; `memory_short_term_limit`
|
|
267
|
+
caps recent-execution recall. The embedder and store are objects, so configure
|
|
268
|
+
them once in the initializer:
|
|
269
|
+
|
|
270
|
+
```ruby
|
|
271
|
+
RcrewAI::Rails.configure do |config|
|
|
272
|
+
config.default_memory_embedder = RCrewAI::Knowledge::Embedder.new
|
|
273
|
+
config.default_memory_store = RCrewAI::Memory::SqliteStore.new(path: "db/rcrewai_memory.sqlite3")
|
|
274
|
+
end
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
### Task output processing (`RcrewAI::Rails::Task`)
|
|
278
|
+
|
|
279
|
+
| Column | Effect |
|
|
280
|
+
|---|---|
|
|
281
|
+
| `output_schema` (JSON) | Validate/coerce the result against a JSON schema (structured output) |
|
|
282
|
+
| `guardrail_class` + `guardrail_method_name` + `guardrail_max_retries` | Validate/transform output, retrying on failure |
|
|
283
|
+
| `output_file` + `create_directory` + `markdown` | Write the result to disk |
|
|
284
|
+
| `attachments` (JSON) | Multimodal image inputs, e.g. `[{ "type": "image", "url": "https://…" }]` |
|
|
285
|
+
|
|
286
|
+
A guardrail is resolved from a host class: `guardrail_class` names a class whose
|
|
287
|
+
`guardrail_method_name` accepts the output and returns `[ok, value_or_error]`.
|
|
288
|
+
|
|
289
|
+
### Crew orchestration (`RcrewAI::Rails::Crew`)
|
|
290
|
+
|
|
291
|
+
| Column | Effect |
|
|
292
|
+
|---|---|
|
|
293
|
+
| `process_type` | `"sequential"`, `"hierarchical"`, or `"consensual"` |
|
|
294
|
+
| `consensus_agents` | Number of proposers for the `consensual` process (default 3) |
|
|
295
|
+
| `planning` / `planning_llm` | Run a planner pass before execution |
|
|
296
|
+
| `before_kickoff_class`/`_method`, `after_kickoff_class`/`_method` | Lifecycle hooks resolved from host classes |
|
|
297
|
+
|
|
298
|
+
**Batch execution** (rcrewai `kickoff_for_each` parity) runs the crew once per
|
|
299
|
+
input set, one `Execution` per input grouped by a shared `batch_id`:
|
|
300
|
+
|
|
301
|
+
```ruby
|
|
302
|
+
result = crew.execute_batch_sync([{ topic: "a" }, { topic: "b" }])
|
|
303
|
+
crew.batch_executions(result[:batch_id]) # the runs, in order
|
|
304
|
+
crew.execute_batch_async(inputs_list) # enqueue N jobs, returns the batch_id
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
### Knowledge (RAG)
|
|
308
|
+
|
|
309
|
+
Attach sources to an agent (role-specific) or a crew (shared with all its agents):
|
|
310
|
+
|
|
311
|
+
```ruby
|
|
312
|
+
agent.knowledge_sources.create!(source_type: "url", value: "https://example.com/doc")
|
|
313
|
+
crew.knowledge_sources.create!(source_type: "string", value: "Reference text…")
|
|
314
|
+
# source_type: "string" | "file" | "pdf" | "csv" | "url"
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
Active sources are embedded lazily at execution. See the memory initializer above
|
|
318
|
+
for embedder configuration.
|
|
319
|
+
|
|
320
|
+
### Flows
|
|
321
|
+
|
|
322
|
+
Define Flow subclasses in your app (Ruby); the engine persists their state and
|
|
323
|
+
runs. Pass `RcrewAI::Rails::ActiveRecordStateStore` so flows resume from the DB,
|
|
324
|
+
and use `FlowRun.execute` to track a kickoff:
|
|
325
|
+
|
|
326
|
+
```ruby
|
|
327
|
+
run = RcrewAI::Rails::FlowRun.execute(MyFlow, inputs: { topic: "ruby" })
|
|
328
|
+
run.status # "completed" / "failed"
|
|
329
|
+
run.result # the final flow state
|
|
330
|
+
RcrewAI::Rails::FlowState.find_by(state_id: run.state_id) # the persisted state
|
|
331
|
+
```
|
|
332
|
+
|
|
190
333
|
## Database Models
|
|
191
334
|
|
|
192
335
|
The gem provides these ActiveRecord models:
|
|
@@ -196,6 +339,9 @@ The gem provides these ActiveRecord models:
|
|
|
196
339
|
- `RcrewAI::Rails::Task` - Task definitions
|
|
197
340
|
- `RcrewAI::Rails::Execution` - Execution history
|
|
198
341
|
- `RcrewAI::Rails::ExecutionLog` - Detailed execution logs
|
|
342
|
+
- `RcrewAI::Rails::KnowledgeSource` - Knowledge (RAG) sources, owned by an agent or a crew
|
|
343
|
+
- `RcrewAI::Rails::FlowState` - Persisted rcrewai Flow state (resume flows across restarts)
|
|
344
|
+
- `RcrewAI::Rails::FlowRun` - Flow-run tracking (status, inputs, result, timing)
|
|
199
345
|
|
|
200
346
|
## API Endpoints
|
|
201
347
|
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
module RcrewAI
|
|
2
|
+
module Rails
|
|
3
|
+
class ObservationsController < ApplicationController
|
|
4
|
+
# Hard ceiling on how deep the span partial will recurse. Real traces
|
|
5
|
+
# are a handful of levels deep; this only exists so a malformed tree
|
|
6
|
+
# cannot recurse until the request dies.
|
|
7
|
+
MAX_SPAN_DEPTH = 50
|
|
8
|
+
|
|
9
|
+
def show
|
|
10
|
+
@execution = Execution.find(params[:execution_id])
|
|
11
|
+
# Load the whole tree in one query and nest in memory — a recursive
|
|
12
|
+
# per-node query would be N+1 on deep traces.
|
|
13
|
+
@spans = @execution.spans.ordered.to_a
|
|
14
|
+
@children = @spans.group_by(&:parent_span_id)
|
|
15
|
+
@roots = @children[nil] || []
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def costs
|
|
19
|
+
@executions = Execution.where.not(total_cost_usd: nil)
|
|
20
|
+
.order(created_at: :desc)
|
|
21
|
+
.limit(100)
|
|
22
|
+
@total_cost = @executions.sum(&:total_cost_usd)
|
|
23
|
+
@total_tokens = @executions.sum { |e| e.total_tokens.to_i }
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -11,6 +11,7 @@ module RcrewAI
|
|
|
11
11
|
inputs: inputs,
|
|
12
12
|
batch_id: batch_id
|
|
13
13
|
)
|
|
14
|
+
collector = nil
|
|
14
15
|
|
|
15
16
|
begin
|
|
16
17
|
execution.start!
|
|
@@ -18,7 +19,16 @@ module RcrewAI
|
|
|
18
19
|
|
|
19
20
|
rcrew = crew.to_rcrew
|
|
20
21
|
|
|
21
|
-
|
|
22
|
+
collector = collector_for(execution)
|
|
23
|
+
collector&.start_crew_span(crew_name: crew.name)
|
|
24
|
+
|
|
25
|
+
result = rcrew.execute(stream: collector)
|
|
26
|
+
|
|
27
|
+
# Close agent spans as successful before finish!, which treats
|
|
28
|
+
# anything still open as an aborted run.
|
|
29
|
+
collector&.finish_open_agent_spans(status: "ok")
|
|
30
|
+
collector&.finish_crew_span(status: "ok")
|
|
31
|
+
collector&.finish!
|
|
22
32
|
|
|
23
33
|
execution.complete!(result)
|
|
24
34
|
execution.log("info", "Crew execution completed", { result: result })
|
|
@@ -27,6 +37,7 @@ module RcrewAI
|
|
|
27
37
|
|
|
28
38
|
result
|
|
29
39
|
rescue => e
|
|
40
|
+
collector&.finish!
|
|
30
41
|
execution.fail!(e)
|
|
31
42
|
execution.log("error", "Crew execution failed", {
|
|
32
43
|
error: e.message,
|
|
@@ -39,34 +50,12 @@ module RcrewAI
|
|
|
39
50
|
|
|
40
51
|
private
|
|
41
52
|
|
|
42
|
-
#
|
|
43
|
-
#
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
when RCrewAI::Events::IterationStart
|
|
49
|
-
execution.log("debug", "Iteration #{event.iteration_index} start", { agent: event.agent })
|
|
50
|
-
when RCrewAI::Events::IterationEnd
|
|
51
|
-
execution.log("debug", "Iteration end", { agent: event.agent, finish_reason: event.finish_reason })
|
|
52
|
-
when RCrewAI::Events::ToolCallStart
|
|
53
|
-
execution.log("info", "Tool call: #{event.tool}", { args: event.args, agent: event.agent })
|
|
54
|
-
when RCrewAI::Events::ToolCallResult
|
|
55
|
-
execution.log("info", "Tool result: #{event.tool}", { duration_ms: event.duration_ms, agent: event.agent })
|
|
56
|
-
when RCrewAI::Events::ToolCallError
|
|
57
|
-
execution.log("error", "Tool error: #{event.tool}", { error: event.error, agent: event.agent })
|
|
58
|
-
when RCrewAI::Events::Usage
|
|
59
|
-
execution.log("debug", "Usage", {
|
|
60
|
-
prompt_tokens: event.prompt_tokens,
|
|
61
|
-
completion_tokens: event.completion_tokens,
|
|
62
|
-
total_tokens: event.total_tokens,
|
|
63
|
-
cost_usd: event.cost_usd,
|
|
64
|
-
agent: event.agent
|
|
65
|
-
})
|
|
66
|
-
when RCrewAI::Events::Error
|
|
67
|
-
execution.log("error", "Crew error", { error: event.error, agent: event.agent })
|
|
68
|
-
end
|
|
69
|
-
end
|
|
53
|
+
# Translates rcrewai events into the span tree. Returns nil when
|
|
54
|
+
# observation is disabled so no sink is attached at all.
|
|
55
|
+
def collector_for(execution)
|
|
56
|
+
return nil unless RcrewAI::Rails.config.observation_enabled
|
|
57
|
+
|
|
58
|
+
RcrewAI::Rails::Observation::Collector.new(execution: execution)
|
|
70
59
|
end
|
|
71
60
|
|
|
72
61
|
def notify_completion(crew, execution, result)
|
|
@@ -5,6 +5,7 @@ module RcrewAI
|
|
|
5
5
|
|
|
6
6
|
belongs_to :crew
|
|
7
7
|
has_many :execution_logs, dependent: :destroy
|
|
8
|
+
has_many :spans, dependent: :destroy
|
|
8
9
|
|
|
9
10
|
validates :status, inclusion: { in: %w[pending running completed failed cancelled] }
|
|
10
11
|
|
|
@@ -74,7 +75,12 @@ module RcrewAI
|
|
|
74
75
|
%w[completed failed cancelled].include?(status)
|
|
75
76
|
end
|
|
76
77
|
|
|
78
|
+
# Deprecated: superseded by the observation engine's span tree.
|
|
79
|
+
# Scheduled for removal one minor version after the engine ships.
|
|
77
80
|
def log(level, message, details = {})
|
|
81
|
+
RcrewAI::Rails.deprecator_warn(
|
|
82
|
+
"Execution#log and ExecutionLog are deprecated; use the observation engine (Execution#spans)."
|
|
83
|
+
)
|
|
78
84
|
execution_logs.create!(
|
|
79
85
|
level: level,
|
|
80
86
|
message: message,
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
module RcrewAI
|
|
2
|
+
module Rails
|
|
3
|
+
class Span < ApplicationRecord
|
|
4
|
+
self.table_name = "rcrewai_spans"
|
|
5
|
+
|
|
6
|
+
KINDS = %w[crew agent task llm_call tool_call].freeze
|
|
7
|
+
STATUSES = %w[running ok error].freeze
|
|
8
|
+
|
|
9
|
+
belongs_to :execution
|
|
10
|
+
belongs_to :parent, class_name: "RcrewAI::Rails::Span",
|
|
11
|
+
foreign_key: :parent_span_id, optional: true
|
|
12
|
+
has_many :children, class_name: "RcrewAI::Rails::Span",
|
|
13
|
+
foreign_key: :parent_span_id, dependent: :destroy
|
|
14
|
+
has_many :span_events, dependent: :destroy
|
|
15
|
+
|
|
16
|
+
validates :kind, inclusion: { in: KINDS }
|
|
17
|
+
validates :status, inclusion: { in: STATUSES }
|
|
18
|
+
validates :name, :trace_id, :started_at, :sequence, presence: true
|
|
19
|
+
|
|
20
|
+
scope :roots, -> { where(parent_span_id: nil).order(:sequence) }
|
|
21
|
+
scope :ordered, -> { order(:sequence) }
|
|
22
|
+
scope :errored, -> { where(status: "error") }
|
|
23
|
+
scope :running, -> { where(status: "running") }
|
|
24
|
+
scope :llm_calls, -> { where(kind: "llm_call") }
|
|
25
|
+
scope :tool_calls, -> { where(kind: "tool_call") }
|
|
26
|
+
|
|
27
|
+
def attributes_hash
|
|
28
|
+
raw = self[:attributes_json]
|
|
29
|
+
return {} if raw.blank?
|
|
30
|
+
|
|
31
|
+
JSON.parse(raw)
|
|
32
|
+
rescue JSON::ParserError
|
|
33
|
+
{}
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def attributes_hash=(hash)
|
|
37
|
+
self[:attributes_json] = hash.nil? ? nil : JSON.generate(hash)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def finish!(status: "ok", ended_at: Time.current)
|
|
41
|
+
update!(
|
|
42
|
+
status: status,
|
|
43
|
+
ended_at: ended_at,
|
|
44
|
+
duration_ms: ((ended_at - started_at) * 1000).round
|
|
45
|
+
)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def running?
|
|
49
|
+
status == "running"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def errored?
|
|
53
|
+
status == "error"
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module RcrewAI
|
|
2
|
+
module Rails
|
|
3
|
+
class SpanEvent < ApplicationRecord
|
|
4
|
+
self.table_name = "rcrewai_span_events"
|
|
5
|
+
|
|
6
|
+
LEVELS = %w[debug info warn error].freeze
|
|
7
|
+
|
|
8
|
+
belongs_to :span
|
|
9
|
+
|
|
10
|
+
validates :level, inclusion: { in: LEVELS }
|
|
11
|
+
validates :name, presence: true
|
|
12
|
+
|
|
13
|
+
serialize :details, coder: JSON
|
|
14
|
+
|
|
15
|
+
scope :errors, -> { where(level: "error") }
|
|
16
|
+
scope :recent, -> { order(timestamp: :desc) }
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
<%#
|
|
2
|
+
Renders one span and, recursively, its children.
|
|
3
|
+
|
|
4
|
+
`children` is the parent_span_id => [spans] map built once by the
|
|
5
|
+
controller. Children are read from that map, never via `span.children`,
|
|
6
|
+
which would issue a query per node and turn a deep trace into an N+1.
|
|
7
|
+
|
|
8
|
+
A malformed tree (a parent/child cycle, e.g. a bad backfill) would
|
|
9
|
+
otherwise recurse until the request dies, so recursion is bounded by
|
|
10
|
+
MAX_SPAN_DEPTH and by `seen`, which drops any span already on the path.
|
|
11
|
+
%>
|
|
12
|
+
<% depth = local_assigns.fetch(:depth, 0) %>
|
|
13
|
+
<% children = local_assigns.fetch(:children, {}) %>
|
|
14
|
+
<% seen = local_assigns.fetch(:seen, []) %>
|
|
15
|
+
<% max_depth = RcrewAI::Rails::ObservationsController::MAX_SPAN_DEPTH %>
|
|
16
|
+
|
|
17
|
+
<div class="span-row span-<%= span.status %> span-kind-<%= span.kind %>" id="span-<%= span.id %>">
|
|
18
|
+
<div class="span-line" style="padding-left: <%= depth * 20 %>px">
|
|
19
|
+
<span class="span-kind"><%= span.kind %></span>
|
|
20
|
+
<span class="span-name"><%= span.name %></span>
|
|
21
|
+
<span class="span-status status-<%= span.status %>"><%= span.status %></span>
|
|
22
|
+
<% if span.duration_ms %>
|
|
23
|
+
<span class="span-duration"><%= span.duration_ms %>ms</span>
|
|
24
|
+
<% end %>
|
|
25
|
+
<% if span.total_tokens.to_i.positive? %>
|
|
26
|
+
<span class="span-tokens"><%= span.total_tokens %> tokens</span>
|
|
27
|
+
<% end %>
|
|
28
|
+
<% if span.cost_usd.present? %>
|
|
29
|
+
<span class="span-cost">$<%= span.cost_usd %></span>
|
|
30
|
+
<% end %>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<% attrs = span.attributes_hash %>
|
|
34
|
+
<% if attrs.present? %>
|
|
35
|
+
<details class="span-attributes" style="margin-left: <%= (depth * 20) + 20 %>px">
|
|
36
|
+
<summary>Details</summary>
|
|
37
|
+
<pre class="code-block"><%= JSON.pretty_generate(attrs) %></pre>
|
|
38
|
+
</details>
|
|
39
|
+
<% end %>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<% if depth < max_depth %>
|
|
43
|
+
<% (children[span.id] || []).each do |child| %>
|
|
44
|
+
<% next if seen.include?(child.id) %>
|
|
45
|
+
<%= render "rcrewai/rails/observations/span",
|
|
46
|
+
span: child,
|
|
47
|
+
children: children,
|
|
48
|
+
depth: depth + 1,
|
|
49
|
+
seen: seen + [span.id] %>
|
|
50
|
+
<% end %>
|
|
51
|
+
<% end %>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
<div class="page-header">
|
|
2
|
+
<h1>Cost & Performance</h1>
|
|
3
|
+
</div>
|
|
4
|
+
|
|
5
|
+
<div class="metadata-grid">
|
|
6
|
+
<div class="meta-item">
|
|
7
|
+
<strong>Total Cost:</strong> $<%= @total_cost %>
|
|
8
|
+
</div>
|
|
9
|
+
<div class="meta-item">
|
|
10
|
+
<strong>Total Tokens:</strong> <%= @total_tokens %>
|
|
11
|
+
</div>
|
|
12
|
+
<div class="meta-item">
|
|
13
|
+
<strong>Executions:</strong> <%= @executions.size %>
|
|
14
|
+
</div>
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
<div class="section">
|
|
18
|
+
<h2>Recent Executions</h2>
|
|
19
|
+
|
|
20
|
+
<% if @executions.empty? %>
|
|
21
|
+
<p class="empty-state">No executions with recorded cost yet.</p>
|
|
22
|
+
<% else %>
|
|
23
|
+
<table class="table">
|
|
24
|
+
<thead>
|
|
25
|
+
<tr>
|
|
26
|
+
<th>Execution</th>
|
|
27
|
+
<th>Crew</th>
|
|
28
|
+
<th>Status</th>
|
|
29
|
+
<th>Spans</th>
|
|
30
|
+
<th>Errors</th>
|
|
31
|
+
<th>Tokens</th>
|
|
32
|
+
<th>Cost</th>
|
|
33
|
+
<th></th>
|
|
34
|
+
</tr>
|
|
35
|
+
</thead>
|
|
36
|
+
<tbody>
|
|
37
|
+
<% @executions.each do |execution| %>
|
|
38
|
+
<tr>
|
|
39
|
+
<td><%= link_to "##{execution.id}", execution_path(execution) %></td>
|
|
40
|
+
<td><%= execution.crew&.name %></td>
|
|
41
|
+
<td><span class="status-<%= execution.status %>"><%= execution.status.humanize %></span></td>
|
|
42
|
+
<td><%= execution.span_count.to_i %></td>
|
|
43
|
+
<td><%= execution.error_count.to_i %></td>
|
|
44
|
+
<td><%= execution.total_tokens.to_i %></td>
|
|
45
|
+
<td>$<%= execution.total_cost_usd %></td>
|
|
46
|
+
<td><%= link_to "Trace", execution_observation_path(execution), class: "btn btn-sm" %></td>
|
|
47
|
+
</tr>
|
|
48
|
+
<% end %>
|
|
49
|
+
</tbody>
|
|
50
|
+
</table>
|
|
51
|
+
<% end %>
|
|
52
|
+
</div>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<div class="page-header">
|
|
2
|
+
<h1>Trace for Execution #<%= @execution.id %></h1>
|
|
3
|
+
<div class="actions">
|
|
4
|
+
<%= link_to "Back to Execution", execution_path(@execution), class: "btn" %>
|
|
5
|
+
<%= link_to "Costs", observation_costs_path, class: "btn" %>
|
|
6
|
+
</div>
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
<div class="execution-details">
|
|
10
|
+
<div class="status-banner status-<%= @execution.status %>">
|
|
11
|
+
<strong>Status:</strong> <%= @execution.status.humanize %>
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
<div class="metadata-grid">
|
|
15
|
+
<div class="meta-item">
|
|
16
|
+
<strong>Spans:</strong> <%= @execution.span_count.to_i %>
|
|
17
|
+
</div>
|
|
18
|
+
<div class="meta-item">
|
|
19
|
+
<strong>Errors:</strong> <%= @execution.error_count.to_i %>
|
|
20
|
+
</div>
|
|
21
|
+
<div class="meta-item">
|
|
22
|
+
<strong>Tokens:</strong> <%= @execution.total_tokens.to_i %>
|
|
23
|
+
</div>
|
|
24
|
+
<div class="meta-item">
|
|
25
|
+
<strong>Cost:</strong> <%= @execution.total_cost_usd.present? ? "$#{@execution.total_cost_usd}" : "N/A" %>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
<div class="section">
|
|
31
|
+
<h2>Waterfall</h2>
|
|
32
|
+
|
|
33
|
+
<div class="span-tree" id="execution-<%= @execution.id %>-spans">
|
|
34
|
+
<% if @roots.empty? %>
|
|
35
|
+
<p class="empty-state">No spans recorded for this execution.</p>
|
|
36
|
+
<% else %>
|
|
37
|
+
<% @roots.each do |span| %>
|
|
38
|
+
<%= render "rcrewai/rails/observations/span",
|
|
39
|
+
span: span, children: @children, depth: 0, seen: [] %>
|
|
40
|
+
<% end %>
|
|
41
|
+
<% end %>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<% if defined?(turbo_stream_from) && @execution.running? %>
|
|
46
|
+
<%= turbo_stream_from "rcrewai_execution_#{@execution.id}" %>
|
|
47
|
+
<% end %>
|
data/config/routes.rb
CHANGED
|
@@ -16,6 +16,12 @@ RcrewAI::Rails::Engine.routes.draw do
|
|
|
16
16
|
end
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
+
resources :executions, only: [] do
|
|
20
|
+
resource :observation, only: [:show], controller: "observations"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
get "observations/costs", to: "observations#costs", as: :observation_costs
|
|
24
|
+
|
|
19
25
|
resources :agents do
|
|
20
26
|
resources :tools
|
|
21
27
|
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
class CreateRcrewaiSpans < ActiveRecord::Migration[7.0]
|
|
2
|
+
def change
|
|
3
|
+
create_table :rcrewai_spans do |t|
|
|
4
|
+
t.references :execution, null: false, foreign_key: { to_table: :rcrewai_executions }
|
|
5
|
+
t.bigint :parent_span_id
|
|
6
|
+
t.string :trace_id, null: false
|
|
7
|
+
t.string :kind, null: false
|
|
8
|
+
t.string :name, null: false
|
|
9
|
+
t.string :status, null: false, default: "running"
|
|
10
|
+
t.datetime :started_at, null: false
|
|
11
|
+
t.datetime :ended_at
|
|
12
|
+
t.integer :duration_ms
|
|
13
|
+
t.integer :prompt_tokens
|
|
14
|
+
t.integer :completion_tokens
|
|
15
|
+
t.integer :total_tokens
|
|
16
|
+
t.decimal :cost_usd, precision: 12, scale: 6
|
|
17
|
+
t.text :attributes_json
|
|
18
|
+
t.integer :sequence, null: false
|
|
19
|
+
|
|
20
|
+
t.timestamps
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
add_index :rcrewai_spans, :parent_span_id
|
|
24
|
+
add_index :rcrewai_spans, :trace_id
|
|
25
|
+
add_index :rcrewai_spans, :kind
|
|
26
|
+
add_index :rcrewai_spans, :status
|
|
27
|
+
add_index :rcrewai_spans, %i[execution_id sequence]
|
|
28
|
+
|
|
29
|
+
create_table :rcrewai_span_events do |t|
|
|
30
|
+
t.references :span, null: false, foreign_key: { to_table: :rcrewai_spans }
|
|
31
|
+
t.string :level, null: false, default: "info"
|
|
32
|
+
t.string :name, null: false
|
|
33
|
+
t.text :details
|
|
34
|
+
t.datetime :timestamp, null: false
|
|
35
|
+
|
|
36
|
+
t.timestamps
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
add_index :rcrewai_span_events, :level
|
|
40
|
+
add_index :rcrewai_span_events, :timestamp
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
class AddObservationRollupsToRcrewaiExecutions < ActiveRecord::Migration[7.0]
|
|
2
|
+
def change
|
|
3
|
+
add_column :rcrewai_executions, :total_cost_usd, :decimal, precision: 12, scale: 6
|
|
4
|
+
add_column :rcrewai_executions, :total_tokens, :integer
|
|
5
|
+
add_column :rcrewai_executions, :span_count, :integer, default: 0, null: false
|
|
6
|
+
add_column :rcrewai_executions, :error_count, :integer, default: 0, null: false
|
|
7
|
+
end
|
|
8
|
+
end
|