rcrewai 0.5.0 → 0.6.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 +21 -1
- data/README.md +26 -1
- data/ROADMAP.md +1 -2
- data/docs/superpowers/specs/2026-07-06-cognitive-memory-design.md +118 -0
- data/docs/upgrading-to-0.6.md +101 -0
- data/examples/cognitive_memory_example.rb +66 -0
- data/lib/rcrewai/agent.rb +12 -1
- data/lib/rcrewai/knowledge/store.rb +4 -14
- data/lib/rcrewai/memory/base_memory.rb +107 -0
- data/lib/rcrewai/memory/entity_memory.rb +47 -0
- data/lib/rcrewai/memory/in_memory_store.rb +40 -0
- data/lib/rcrewai/memory/long_term_memory.rb +39 -0
- data/lib/rcrewai/memory/short_term_memory.rb +20 -0
- data/lib/rcrewai/memory/sqlite_store.rb +99 -0
- data/lib/rcrewai/memory/tool_memory.rb +38 -0
- data/lib/rcrewai/memory.rb +57 -160
- data/lib/rcrewai/similarity.rb +43 -0
- data/lib/rcrewai/version.rb +1 -1
- data/lib/rcrewai.rb +8 -0
- data/rcrewai.gemspec +1 -0
- metadata +26 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fb90e26000ddf4a2907b06150fdc87b1707fc701c6c61ba35db33339ed6f38d4
|
|
4
|
+
data.tar.gz: ba3435eb1918be328ce65faf2c3000553833b09778eb3b06f2a6191cae31aab4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 992ac3d7368cb7690f11294b78fe876fcea29d48cffd001a355471a351c22c321563fbd390a0688951be6bcbd0f177322509e59cdd19cf8073e228784d96b503
|
|
7
|
+
data.tar.gz: 92e7db7609d4503693396f85735fd48633cb5204a34d2431e57ffcb7c758e693bff5b1282ed716899f4242bd9a26b0483f278c991dedb13b21d05894a59b7105
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.6.0] - 2026-07-06
|
|
11
|
+
|
|
12
|
+
Replaces the placeholder agent memory with a **cognitive memory** system —
|
|
13
|
+
semantic recall (embeddings + cosine), optional SQLite persistence, and four
|
|
14
|
+
memory types. This is the one area where CrewAI advanced past what the gem
|
|
15
|
+
originally ported. Backward compatible: the `Memory` public API is unchanged and
|
|
16
|
+
the zero-config default behaves as before.
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
- Cognitive memory: `RCrewAI::Memory` is now a semantic, optionally-persistent, multi-type memory system, replacing the previous word-overlap placeholder. It composes four memory types — `ShortTermMemory` (recent executions, capped), `LongTermMemory` (durable, deduped insights), `EntityMemory` (facts about entities seen in work), and `ToolMemory` (tool-call history) — behind the original `Memory` public API.
|
|
20
|
+
- Semantic recall: pass `Agent.new(memory: { embedder: RCrewAI::Knowledge::Embedder.new })` to recall conceptually related past work via embeddings + cosine similarity. Without an embedder, recall falls back to lexical (word-overlap) similarity — and embedding failures fall back gracefully, so memory never breaks execution.
|
|
21
|
+
- Persistent memory: `RCrewAI::Memory::SqliteStore` persists memories to SQLite (vectors packed as floats, metadata as JSON) so recall survives restarts. Default store is `InMemoryStore` (volatile). The store interface is pluggable.
|
|
22
|
+
- Memory scoping: each agent's memory is scoped to its name by default, so agents sharing a persistent store don't cross-read; override via `memory: { scope: ... }`.
|
|
23
|
+
- `RCrewAI::Similarity` — shared cosine + lexical similarity helpers (extracted from `Knowledge::Store`, now used by both Knowledge and Memory).
|
|
24
|
+
- `sqlite3` added as a runtime dependency (required lazily; only needed for `SqliteStore`).
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
- `Agent.new(memory:)` accepts a pre-built `Memory`, an options hash (`{ embedder:, store:, scope:, short_term_limit: }`), or nothing (zero-config default, unchanged behavior).
|
|
28
|
+
|
|
10
29
|
## [0.5.0] - 2026-07-03
|
|
11
30
|
|
|
12
31
|
Polish release completing the roadmap backlog: crew lifecycle hooks, batch
|
|
@@ -179,7 +198,8 @@ output, guardrails, planning, and training/testing. See `ROADMAP.md`.
|
|
|
179
198
|
- CLI usage documentation
|
|
180
199
|
- Real-world use cases and examples
|
|
181
200
|
|
|
182
|
-
[Unreleased]: https://github.com/gkosmo/rcrewAI/compare/v0.
|
|
201
|
+
[Unreleased]: https://github.com/gkosmo/rcrewAI/compare/v0.6.0...HEAD
|
|
202
|
+
[0.6.0]: https://github.com/gkosmo/rcrewAI/compare/v0.5.0...v0.6.0
|
|
183
203
|
[0.5.0]: https://github.com/gkosmo/rcrewAI/compare/v0.4.0...v0.5.0
|
|
184
204
|
[0.4.0]: https://github.com/gkosmo/rcrewAI/compare/v0.3.0...v0.4.0
|
|
185
205
|
[0.3.0]: https://github.com/gkosmo/rcrewAI/compare/v0.1.0...v0.3.0
|
data/README.md
CHANGED
|
@@ -13,7 +13,7 @@ RCrewAI is a Ruby implementation of the CrewAI framework, allowing you to create
|
|
|
13
13
|
- **🤖 Intelligent Agents**: AI agents with reasoning loops, memory, and tool usage capabilities
|
|
14
14
|
- **🔗 Multi-LLM Support**: OpenAI, Anthropic (Claude), Google (Gemini), Azure OpenAI, and Ollama
|
|
15
15
|
- **🛠️ Rich Tool Ecosystem**: Web search, file operations, SQL, email, code execution, PDF processing, and custom tools
|
|
16
|
-
- **🧠
|
|
16
|
+
- **🧠 Cognitive Memory**: Semantic recall (embeddings + cosine) with optional SQLite persistence and short-term/long-term/entity/tool memory types
|
|
17
17
|
- **🤝 Human-in-the-Loop**: Interactive approval workflows, human guidance, and collaborative decision making
|
|
18
18
|
- **⚡ Advanced Task System**: Dependencies, retries, async/concurrent execution, and context sharing
|
|
19
19
|
- **🏗️ Hierarchical Teams**: Manager agents that coordinate and delegate tasks to specialist agents
|
|
@@ -374,6 +374,31 @@ Embeddings default to OpenAI's `text-embedding-3-small`; pass a custom
|
|
|
374
374
|
`embedder:` (anything responding to `embed(texts)`) or vector store to swap the
|
|
375
375
|
backend.
|
|
376
376
|
|
|
377
|
+
## 🧠 Cognitive Memory
|
|
378
|
+
|
|
379
|
+
Agents remember what they've done and recall it semantically on future tasks.
|
|
380
|
+
Memory is zero-config by default (in-memory, lexical recall); add an embedder
|
|
381
|
+
for semantic recall and a SQLite store for persistence:
|
|
382
|
+
|
|
383
|
+
```ruby
|
|
384
|
+
embedder = RCrewAI::Knowledge::Embedder.new
|
|
385
|
+
store = RCrewAI::Memory::SqliteStore.new(path: '~/.rcrewai/memory.db')
|
|
386
|
+
|
|
387
|
+
agent = RCrewAI::Agent.new(
|
|
388
|
+
name: 'engineer', role: '...', goal: '...',
|
|
389
|
+
memory: { embedder: embedder, store: store } # both optional
|
|
390
|
+
)
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
- **Semantic recall** — with an embedder, an agent recalls conceptually related
|
|
394
|
+
past work even when the wording differs (falls back to word-overlap without one).
|
|
395
|
+
- **Persistence** — a `SqliteStore` makes memory survive restarts.
|
|
396
|
+
- **Memory types** — `agent.memory.short_term` / `long_term` / `entity` / `tool`.
|
|
397
|
+
- **Scoping** — memory is scoped per agent so a shared store doesn't cross-read.
|
|
398
|
+
|
|
399
|
+
Memory is best-effort: embedding failures fall back to lexical similarity, so it
|
|
400
|
+
never breaks agent execution.
|
|
401
|
+
|
|
377
402
|
## 🌊 Flows
|
|
378
403
|
|
|
379
404
|
Beyond crews, RCrewAI has **Flows** — an event-driven workflow engine for
|
data/ROADMAP.md
CHANGED
|
@@ -19,8 +19,7 @@ Since CrewAI's `1.0`, the framework grew a second pillar (**Flows**) plus
|
|
|
19
19
|
**Training/Testing**. RCrewAI now implements all of these — see the matrix below.
|
|
20
20
|
|
|
21
21
|
**Status: complete.** All milestone issues (#5–#12) shipped in v0.4.0, and all
|
|
22
|
-
backlog items (#15–#20)
|
|
23
|
-
release). There is no outstanding roadmap work.
|
|
22
|
+
backlog items (#15–#20) shipped in v0.5.0. There is no outstanding roadmap work.
|
|
24
23
|
|
|
25
24
|
## Parity matrix
|
|
26
25
|
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# Cognitive Memory: Semantic, Persistent, Multi-Type Agent Memory
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-07-06
|
|
4
|
+
**Status:** Approved design (pending implementation)
|
|
5
|
+
**Target version:** `rcrewai` 0.6.0 (current is 0.5.0)
|
|
6
|
+
**Scope:** Replace the placeholder `RCrewAI::Memory` with a cognitive memory system modeled on CrewAI's memory taxonomy — short-term (semantic recent recall), long-term (persistent insights), entity memory, and tool-usage memory — backed by embeddings and a persistent SQLite vector store. No new external dependencies; reuses the in-repo `Knowledge::Embedder` and cosine-search infrastructure.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Motivation
|
|
11
|
+
|
|
12
|
+
The current `RCrewAI::Memory` is a placeholder. It matches "relevant" past executions with word-overlap similarity (`common_words / total_words`) plus a hardcoded keyword/stopword list and a `classify_task_type` that greps the description for words like "research" or "write". Nothing is embedded; nothing is persisted; everything is lost when the process exits. It is the weakest module in the codebase and it undercuts the framework's core promise — agents that learn from what they've done.
|
|
13
|
+
|
|
14
|
+
Meanwhile CrewAI has advanced *past* the version this project ported: its 2026 "cognitive memory" work adds semantic recall, distinct memory types (short-term, long-term, entity), and persistence. This is genuine parity work, not gold-plating — and the infrastructure to do it well already exists in-repo (`Knowledge::Embedder`, cosine `Knowledge::Store`), so we compose rather than build from scratch.
|
|
15
|
+
|
|
16
|
+
## Goals
|
|
17
|
+
|
|
18
|
+
1. **Semantic recall.** Replace word-overlap with embedding-based similarity so an agent recalls conceptually related past work, not just keyword-overlapping work.
|
|
19
|
+
2. **Persistence.** Memory survives process restarts via a SQLite-backed vector store (no external service).
|
|
20
|
+
3. **Memory types.** Model CrewAI's taxonomy: short-term, long-term, entity, and tool-usage memory, each with clear write/read semantics.
|
|
21
|
+
4. **Backward compatibility.** The existing `Memory` public API (`add_execution`, `add_tool_usage`, `relevant_executions`, `tool_usage_for`, `clear_*!`, `stats`) keeps working, so `Agent` and the runners need no changes to function.
|
|
22
|
+
5. **No new hard dependencies.** SQLite via Ruby's stdlib-adjacent `sqlite3` gem (already common in Ruby projects) — but the store is pluggable, and the default remains in-memory so the gem works with zero setup.
|
|
23
|
+
|
|
24
|
+
## Non-goals
|
|
25
|
+
|
|
26
|
+
- External vector databases (Chroma/Qdrant/pgvector). The pluggable store interface leaves room for them later; we ship SQLite + in-memory.
|
|
27
|
+
- Reranking, query rewriting, or summarization of memories (CrewAI has some of this; it's a follow-up).
|
|
28
|
+
- Multi-tenant / shared cross-agent memory servers.
|
|
29
|
+
|
|
30
|
+
## Decisions captured during brainstorming
|
|
31
|
+
|
|
32
|
+
| # | Decision | Choice |
|
|
33
|
+
|---|----------|--------|
|
|
34
|
+
| 1 | Similarity mechanism | Embeddings + cosine, reusing `Knowledge::Embedder` and the cosine math already in `Knowledge::Store`. |
|
|
35
|
+
| 2 | Persistence | Pluggable vector store; ship `InMemoryStore` (default) and `SqliteStore`. |
|
|
36
|
+
| 3 | Memory taxonomy | Four types: `ShortTermMemory`, `LongTermMemory`, `EntityMemory`, `ToolMemory`. |
|
|
37
|
+
| 4 | Backward compat | Keep the `Memory` facade and its current method signatures; delegate internally to the new types. |
|
|
38
|
+
| 5 | Default behavior | Zero-config: in-memory store, embeddings only if an embedder is available/keyed; graceful fallback to the current lexical similarity when no embedder. |
|
|
39
|
+
| 6 | Embedding failures | Never fatal. If embedding fails (no key, network), fall back to lexical similarity so agents still run. |
|
|
40
|
+
| 7 | Namespacing | Memories are scoped by agent (role/name) so one agent's memory doesn't leak into another, matching CrewAI's per-role collections. |
|
|
41
|
+
|
|
42
|
+
## Architecture
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
RCrewAI::Memory (facade — preserves today's public API)
|
|
46
|
+
├── ShortTermMemory recent executions, semantic recall, capped, volatile-by-default
|
|
47
|
+
├── LongTermMemory durable insights from successful executions, persistent
|
|
48
|
+
├── EntityMemory facts about entities (people, systems, concepts) extracted from work
|
|
49
|
+
└── ToolMemory tool-call history + outcomes (replaces @tool_usage)
|
|
50
|
+
|
|
51
|
+
Each memory type holds:
|
|
52
|
+
- a VectorStore (InMemoryStore | SqliteStore) ← persistence + search
|
|
53
|
+
- an Embedder (Knowledge::Embedder | nil) ← semantic vectors
|
|
54
|
+
- a lexical fallback (current word-overlap) ← when no embedder
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Storage: `Memory::VectorStore`
|
|
58
|
+
|
|
59
|
+
A small interface (mirrors `Knowledge::Store` so they can converge later):
|
|
60
|
+
|
|
61
|
+
```ruby
|
|
62
|
+
store.add(id:, text:, vector:, metadata:) # upsert a record
|
|
63
|
+
store.search(vector, k:, scope:) # cosine top-k, filtered by scope
|
|
64
|
+
store.all(scope:) # enumerate (for stats / lexical fallback)
|
|
65
|
+
store.delete(scope:) # clear a scope
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
- **`InMemoryStore`** — array of records; cosine in Ruby (lift the existing private cosine out of `Knowledge::Store` into a shared `Similarity` helper). Default.
|
|
69
|
+
- **`SqliteStore`** — one row per memory: `id, scope, type, text, vector (blob), metadata (json), created_at`. Vectors stored as packed floats (`Array#pack('e*')`). Cosine computed in Ruby over candidate rows filtered by `scope`/`type` (fine for the thousands-of-memories scale agents produce; a real ANN index is a later optimization). Opened at a configurable path (default `~/.rcrewai/memory.db` or `:memory:`).
|
|
70
|
+
|
|
71
|
+
### Embeddings
|
|
72
|
+
|
|
73
|
+
Reuse `Knowledge::Embedder`. The memory system takes an optional `embedder:`; when present, `add_*` embeds the text and stores the vector, and recall embeds the query. When absent (no key, or explicitly disabled), the store keeps `vector: nil` and recall falls back to the current lexical similarity over `store.all`. This keeps the zero-config path working.
|
|
74
|
+
|
|
75
|
+
### The four memory types
|
|
76
|
+
|
|
77
|
+
- **ShortTermMemory** — every execution is written here; recall returns the top-k semantically similar recent items. Capped (default 100) and, by default, uses the in-memory store (volatile). This is the direct upgrade of today's `@short_term` + `relevant_executions`.
|
|
78
|
+
- **LongTermMemory** — successful executions promote a distilled "insight" record (task type, what worked, result summary) into a persistent store, deduped by similarity so we don't store near-identical insights repeatedly. Upgrade of today's `@long_term`.
|
|
79
|
+
- **EntityMemory** — optional lightweight entity extraction (heuristic first: capitalized noun phrases / quoted terms; pluggable LLM extractor later) so agents accumulate facts about recurring entities. New capability.
|
|
80
|
+
- **ToolMemory** — replaces `@tool_usage`; same API (`add_tool_usage`, `tool_usage_for`) but persistable and searchable.
|
|
81
|
+
|
|
82
|
+
### Facade: `RCrewAI::Memory`
|
|
83
|
+
|
|
84
|
+
Keeps today's signatures, delegating to the types:
|
|
85
|
+
|
|
86
|
+
```ruby
|
|
87
|
+
Memory.new(embedder: nil, store: nil, scope: nil, short_term_limit: 100)
|
|
88
|
+
|
|
89
|
+
add_execution(task, result, execution_time) # -> ShortTerm + (if success) LongTerm + Entity
|
|
90
|
+
add_tool_usage(tool_name, params, result) # -> ToolMemory
|
|
91
|
+
relevant_executions(task, limit = 3) # -> semantic recall across Short+Long, formatted string|nil
|
|
92
|
+
tool_usage_for(tool_name, limit = 5) # -> ToolMemory
|
|
93
|
+
clear_short_term! / clear_all! / stats # -> delegate
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
`Agent#initialize` gains optional `memory:` config (embedder/store/persistence) but defaults to the zero-config in-memory, lexical-fallback behavior — so existing code is unchanged.
|
|
97
|
+
|
|
98
|
+
## Backward compatibility & migration
|
|
99
|
+
|
|
100
|
+
- The `Memory` public API is preserved exactly; `agent.rb` and the runners are untouched functionally.
|
|
101
|
+
- Default construction (`Memory.new` with no args) behaves like today from the caller's perspective — just with better recall when an embedder is configured, and identical lexical recall when not.
|
|
102
|
+
- `relevant_executions` still returns the same formatted-string-or-nil shape consumed at `agent.rb:241`.
|
|
103
|
+
|
|
104
|
+
## Testing strategy
|
|
105
|
+
|
|
106
|
+
- **Unit**: each memory type with a fake deterministic embedder (as `knowledge_spec.rb` already does) — assert semantic recall ranks the conceptually-closest item first, not the keyword-overlapping one.
|
|
107
|
+
- **Persistence**: `SqliteStore` round-trip — write memories, reopen the DB, recall survives.
|
|
108
|
+
- **Fallback**: with no embedder, recall still returns results via lexical similarity (proves the graceful path).
|
|
109
|
+
- **Facade compat**: the existing `memory_spec.rb` expectations continue to pass (or are updated only where the placeholder behavior was clearly a bug).
|
|
110
|
+
- **Agent integration**: an agent with `reasoning`/knowledge off recalls a semantically-related prior execution injected into its context.
|
|
111
|
+
|
|
112
|
+
## Dependencies
|
|
113
|
+
|
|
114
|
+
- `sqlite3` gem — added as a runtime dependency, but only required lazily inside `SqliteStore` so the gem loads and the in-memory path works even if it's absent. (If we'd rather avoid the dependency entirely, the fallback is a JSON-file-backed store; SQLite is preferred for query/scale.)
|
|
115
|
+
|
|
116
|
+
## Rollout
|
|
117
|
+
|
|
118
|
+
Ship in `0.6.0`. `docs/upgrading-to-0.6.md` documents opt-in persistence and embeddings; the default path is unchanged, so it's a no-action upgrade for existing users.
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# Upgrading to RCrewAI 0.6
|
|
2
|
+
|
|
3
|
+
RCrewAI 0.6 replaces the placeholder memory with a **cognitive memory** system:
|
|
4
|
+
semantic recall (embeddings + cosine), optional SQLite persistence, and four
|
|
5
|
+
memory types (short-term, long-term, entity, tool).
|
|
6
|
+
|
|
7
|
+
**0.6 is backward compatible.** The `RCrewAI::Memory` public API is unchanged,
|
|
8
|
+
and the default (`Memory.new` with no config) behaves like before from the
|
|
9
|
+
caller's perspective — just with better recall once an embedder is configured.
|
|
10
|
+
Everything below is opt-in.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## 1. What you must do
|
|
15
|
+
|
|
16
|
+
Nothing. Upgrade the gem; existing code keeps working:
|
|
17
|
+
|
|
18
|
+
```ruby
|
|
19
|
+
gem 'rcrewai', '~> 0.6'
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Note: `0.6` adds `sqlite3` as a runtime dependency (used only if you opt into
|
|
23
|
+
persistence). It is required lazily, so the in-memory default works even if the
|
|
24
|
+
native extension isn't present.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## 2. What you can do (new capabilities)
|
|
29
|
+
|
|
30
|
+
### 2a. Semantic recall via embeddings
|
|
31
|
+
|
|
32
|
+
By default, memory recall uses lexical (word-overlap) similarity — same as
|
|
33
|
+
before. Pass an embedder to get semantic recall, so an agent remembers
|
|
34
|
+
conceptually related work even when the wording differs:
|
|
35
|
+
|
|
36
|
+
```ruby
|
|
37
|
+
embedder = RCrewAI::Knowledge::Embedder.new # OpenAI text-embedding-3-small
|
|
38
|
+
agent = RCrewAI::Agent.new(
|
|
39
|
+
name: 'engineer', role: '...', goal: '...',
|
|
40
|
+
memory: { embedder: embedder }
|
|
41
|
+
)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
If embedding fails (no API key, network error), recall automatically falls back
|
|
45
|
+
to lexical similarity — memory never breaks agent execution.
|
|
46
|
+
|
|
47
|
+
### 2b. Persistent memory (SQLite)
|
|
48
|
+
|
|
49
|
+
Give memory a SQLite store and it survives restarts:
|
|
50
|
+
|
|
51
|
+
```ruby
|
|
52
|
+
store = RCrewAI::Memory::SqliteStore.new(path: '~/.rcrewai/memory.db')
|
|
53
|
+
agent = RCrewAI::Agent.new(
|
|
54
|
+
name: 'engineer', role: '...', goal: '...',
|
|
55
|
+
memory: { embedder: embedder, store: store }
|
|
56
|
+
)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The default store is in-memory (volatile). Any object implementing the store
|
|
60
|
+
interface (`add` / `search` / `all` / `delete` / `delete_record`) can be used —
|
|
61
|
+
e.g. a future Chroma/pgvector adapter.
|
|
62
|
+
|
|
63
|
+
### 2c. Memory scoping
|
|
64
|
+
|
|
65
|
+
Each agent's memory is scoped to its name by default, so agents sharing a
|
|
66
|
+
persistent store don't read each other's memories. Override with
|
|
67
|
+
`memory: { scope: 'custom' }` if you want deliberate sharing.
|
|
68
|
+
|
|
69
|
+
### 2d. Memory types (direct access)
|
|
70
|
+
|
|
71
|
+
The `Memory` facade exposes the underlying types for advanced use:
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
agent.memory.short_term # recent executions, semantic recall, capped
|
|
75
|
+
agent.memory.long_term # durable insights from successful executions, deduped
|
|
76
|
+
agent.memory.entity # facts about entities (people, systems) seen in work
|
|
77
|
+
agent.memory.tool # tool-call history + outcomes
|
|
78
|
+
|
|
79
|
+
agent.memory.entity.entities # => ["Alice", "AWS", ...]
|
|
80
|
+
agent.memory.long_term.recall('...', limit: 3)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### 2e. Pre-built Memory
|
|
84
|
+
|
|
85
|
+
You can also construct and pass a `Memory` directly:
|
|
86
|
+
|
|
87
|
+
```ruby
|
|
88
|
+
memory = RCrewAI::Memory.new(scope: 'shared', embedder: embedder, store: store)
|
|
89
|
+
agent = RCrewAI::Agent.new(name: 'a', role: '...', goal: '...', memory: memory)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Behavior notes
|
|
95
|
+
|
|
96
|
+
- `add_execution` writes to short-term always, and (on success) promotes a
|
|
97
|
+
deduped insight to long-term and extracts entities.
|
|
98
|
+
- `relevant_executions(task, limit)` recalls across short- and long-term and
|
|
99
|
+
returns the same formatted-string-or-nil shape as before.
|
|
100
|
+
- `clear_short_term!` leaves long-term insights intact; `clear_all!` wipes
|
|
101
|
+
everything.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Cognitive Memory — semantic recall + persistence.
|
|
5
|
+
#
|
|
6
|
+
# Agents remember past executions and recall them by *meaning*, not just shared
|
|
7
|
+
# words. With a SQLite store, memory survives restarts.
|
|
8
|
+
#
|
|
9
|
+
# This example uses a fake, deterministic embedder so it runs WITHOUT an API
|
|
10
|
+
# key. In real use you'd pass `RCrewAI::Knowledge::Embedder.new` (OpenAI).
|
|
11
|
+
#
|
|
12
|
+
# Run:
|
|
13
|
+
# ruby examples/cognitive_memory_example.rb
|
|
14
|
+
|
|
15
|
+
require_relative '../lib/rcrewai'
|
|
16
|
+
require 'tmpdir'
|
|
17
|
+
|
|
18
|
+
RCrewAI.configure(validate: false) do |c|
|
|
19
|
+
c.llm_provider = :openai
|
|
20
|
+
c.api_key = 'demo-key'
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Fake embedder: maps text to a concept vector by keyword. Any object
|
|
24
|
+
# responding to embed(texts) -> [[float, ...], ...] works.
|
|
25
|
+
class ConceptEmbedder
|
|
26
|
+
def embed(texts)
|
|
27
|
+
texts.map do |t|
|
|
28
|
+
l = t.downcase
|
|
29
|
+
[
|
|
30
|
+
l.match?(/payment|billing|invoice|charge/) ? 1.0 : 0.0,
|
|
31
|
+
l.match?(/auth|login|session|token/) ? 1.0 : 0.0,
|
|
32
|
+
l.match?(/deploy|release|ci|pipeline/) ? 1.0 : 0.0
|
|
33
|
+
]
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
Task = Struct.new(:name, :description)
|
|
39
|
+
|
|
40
|
+
Dir.mktmpdir do |dir|
|
|
41
|
+
store = RCrewAI::Memory::SqliteStore.new(path: File.join(dir, 'memory.db'))
|
|
42
|
+
agent = RCrewAI::Agent.new(
|
|
43
|
+
name: 'engineer', role: 'Senior engineer', goal: 'Ship reliable software',
|
|
44
|
+
memory: { embedder: ConceptEmbedder.new, store: store }
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
# Record a few past executions.
|
|
48
|
+
agent.memory.add_execution(Task.new('t1', 'fixed the billing invoice bug'),
|
|
49
|
+
'adjusted the payment retry logic', 1.2)
|
|
50
|
+
agent.memory.add_execution(Task.new('t2', 'patched the login session flow'),
|
|
51
|
+
'rotated the auth tokens', 0.8)
|
|
52
|
+
agent.memory.add_execution(Task.new('t3', 'sped up the release pipeline'),
|
|
53
|
+
'parallelized the CI stages', 2.0)
|
|
54
|
+
|
|
55
|
+
puts '== Semantic recall =='
|
|
56
|
+
# Query shares NO words with the billing execution, but is conceptually close.
|
|
57
|
+
query = Task.new('q', 'a customer got double-charged on checkout')
|
|
58
|
+
puts agent.memory.relevant_executions(query, 1)
|
|
59
|
+
|
|
60
|
+
puts "\n== Persistence (reopen the DB in a fresh agent) =="
|
|
61
|
+
store2 = RCrewAI::Memory::SqliteStore.new(path: File.join(dir, 'memory.db'))
|
|
62
|
+
agent2 = RCrewAI::Agent.new(name: 'engineer', role: 'r', goal: 'g',
|
|
63
|
+
memory: { embedder: ConceptEmbedder.new, store: store2 })
|
|
64
|
+
puts agent2.memory.relevant_executions(query, 1)
|
|
65
|
+
puts "stats: #{agent2.memory.stats.inspect}"
|
|
66
|
+
end
|
data/lib/rcrewai/agent.rb
CHANGED
|
@@ -39,7 +39,7 @@ module RCrewAI
|
|
|
39
39
|
@reasoning = options.fetch(:reasoning, false)
|
|
40
40
|
@max_reasoning_attempts = options.fetch(:max_reasoning_attempts, 3)
|
|
41
41
|
@respect_context_window = options.fetch(:respect_context_window, false)
|
|
42
|
-
@memory =
|
|
42
|
+
@memory = build_memory(options[:memory])
|
|
43
43
|
@rate_limiter = options[:max_rpm] ? RateLimiter.new(max_rpm: options[:max_rpm]) : nil
|
|
44
44
|
@llm_client = wrap_with_rate_limiter(build_llm_client(options[:llm]))
|
|
45
45
|
@knowledge = build_knowledge(options[:knowledge], options[:knowledge_sources])
|
|
@@ -220,6 +220,17 @@ module RCrewAI
|
|
|
220
220
|
RateLimiter::ThrottledClient.new(client, @rate_limiter)
|
|
221
221
|
end
|
|
222
222
|
|
|
223
|
+
# Builds the agent's memory. Accepts a pre-built Memory, an options hash
|
|
224
|
+
# (`{ embedder:, store:, scope:, short_term_limit: }`), or nil for the
|
|
225
|
+
# zero-config default. Memory is scoped to the agent's name so agents don't
|
|
226
|
+
# share recall (matters when a persistent store is shared).
|
|
227
|
+
def build_memory(memory)
|
|
228
|
+
return memory if memory.is_a?(Memory)
|
|
229
|
+
|
|
230
|
+
opts = memory.is_a?(Hash) ? memory : {}
|
|
231
|
+
Memory.new(scope: opts.fetch(:scope, name), **opts.slice(:embedder, :store, :short_term_limit))
|
|
232
|
+
end
|
|
233
|
+
|
|
223
234
|
# Accepts a pre-built Knowledge::Base via +knowledge:+ or an array of
|
|
224
235
|
# sources via +knowledge_sources:+ (wrapped in a Base). Returns nil if
|
|
225
236
|
# neither is given.
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative '../similarity'
|
|
4
|
+
|
|
3
5
|
module RCrewAI
|
|
4
6
|
module Knowledge
|
|
5
7
|
# In-memory vector store with cosine-similarity search. The default backing
|
|
@@ -38,20 +40,8 @@ module RCrewAI
|
|
|
38
40
|
|
|
39
41
|
private
|
|
40
42
|
|
|
41
|
-
def cosine_similarity(
|
|
42
|
-
|
|
43
|
-
norm_a = 0.0
|
|
44
|
-
norm_b = 0.0
|
|
45
|
-
a.each_index do |i|
|
|
46
|
-
ai = a[i].to_f
|
|
47
|
-
bi = (b[i] || 0).to_f
|
|
48
|
-
dot += ai * bi
|
|
49
|
-
norm_a += ai * ai
|
|
50
|
-
norm_b += bi * bi
|
|
51
|
-
end
|
|
52
|
-
return 0.0 if norm_a.zero? || norm_b.zero?
|
|
53
|
-
|
|
54
|
-
dot / (Math.sqrt(norm_a) * Math.sqrt(norm_b))
|
|
43
|
+
def cosine_similarity(vec_a, vec_b)
|
|
44
|
+
Similarity.cosine(vec_a, vec_b)
|
|
55
45
|
end
|
|
56
46
|
end
|
|
57
47
|
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'digest'
|
|
4
|
+
require_relative '../similarity'
|
|
5
|
+
require_relative 'in_memory_store'
|
|
6
|
+
|
|
7
|
+
module RCrewAI
|
|
8
|
+
class Memory
|
|
9
|
+
# Shared behavior for the memory types: embed-on-write (when an embedder is
|
|
10
|
+
# present) and semantic recall with a lexical fallback when it isn't.
|
|
11
|
+
# Records are namespaced per (agent) scope + a type suffix so different
|
|
12
|
+
# memory types don't collide in a shared store.
|
|
13
|
+
class BaseMemory
|
|
14
|
+
def initialize(scope:, embedder: nil, store: nil, limit: nil)
|
|
15
|
+
@scope = "#{scope}:#{type_suffix}"
|
|
16
|
+
@embedder = embedder
|
|
17
|
+
@store = store || InMemoryStore.new
|
|
18
|
+
@limit = limit
|
|
19
|
+
@seq = 0
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def record(text, metadata = {})
|
|
23
|
+
add(text, metadata)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def recall(query, limit: 3)
|
|
27
|
+
records = search_records(query, limit)
|
|
28
|
+
records.map { |r| format(r) }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def count
|
|
32
|
+
@store.all(scope: @scope).length
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def clear!
|
|
36
|
+
@store.delete(scope: @scope)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
protected
|
|
40
|
+
|
|
41
|
+
# Subclasses override to namespace their records.
|
|
42
|
+
def type_suffix
|
|
43
|
+
'base'
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def format(record)
|
|
47
|
+
{ text: record[:text], metadata: record[:metadata] }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def add(text, metadata)
|
|
51
|
+
vector = embed(text)
|
|
52
|
+
id = next_id(text)
|
|
53
|
+
@store.add(id: id, text: text, vector: vector, scope: @scope, metadata: stringify(metadata))
|
|
54
|
+
evict_if_needed
|
|
55
|
+
id
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def search_records(query, limit)
|
|
59
|
+
all = @store.all(scope: @scope)
|
|
60
|
+
return [] if all.empty?
|
|
61
|
+
|
|
62
|
+
query_vector = embed(query)
|
|
63
|
+
if query_vector && all.any? { |r| r[:vector] }
|
|
64
|
+
@store.search(query_vector, k: limit, scope: @scope)
|
|
65
|
+
else
|
|
66
|
+
lexical_search(query, all, limit)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def lexical_search(query, records, limit)
|
|
71
|
+
records
|
|
72
|
+
.map { |r| [r, Similarity.lexical(query, r[:text])] }
|
|
73
|
+
.sort_by { |(_r, score)| -score }
|
|
74
|
+
.first(limit)
|
|
75
|
+
.map(&:first)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def embed(text)
|
|
79
|
+
return nil unless @embedder
|
|
80
|
+
|
|
81
|
+
@embedder.embed([text]).first
|
|
82
|
+
rescue StandardError
|
|
83
|
+
nil # embedding is best-effort; fall back to lexical
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def evict_if_needed
|
|
87
|
+
return unless @limit
|
|
88
|
+
|
|
89
|
+
records = @store.all(scope: @scope)
|
|
90
|
+
return if records.length <= @limit
|
|
91
|
+
|
|
92
|
+
# records carry a monotonic :seq in metadata; drop the oldest.
|
|
93
|
+
oldest = records.min_by { |r| r[:metadata]['seq'].to_i }
|
|
94
|
+
@store.delete_record(id: oldest[:id], scope: @scope) if @store.respond_to?(:delete_record)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def next_id(text)
|
|
98
|
+
@seq += 1
|
|
99
|
+
Digest::SHA256.hexdigest("#{@scope}:#{@seq}:#{text}")[0, 24]
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def stringify(metadata)
|
|
103
|
+
(metadata || {}).transform_keys(&:to_s).merge('seq' => @seq)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base_memory'
|
|
4
|
+
|
|
5
|
+
module RCrewAI
|
|
6
|
+
class Memory
|
|
7
|
+
# Facts about entities (people, systems, concepts) accumulated from work.
|
|
8
|
+
# Entities are extracted heuristically (capitalized tokens / acronyms); an
|
|
9
|
+
# LLM-backed extractor can be swapped in later.
|
|
10
|
+
class EntityMemory < BaseMemory
|
|
11
|
+
# Skip sentence-initial common words that happen to be capitalized.
|
|
12
|
+
COMMON = %w[The A An I In On At To For Of With By And Or But It This That Who Where When].freeze
|
|
13
|
+
|
|
14
|
+
def initialize(scope:, embedder: nil, store: nil, limit: nil)
|
|
15
|
+
super
|
|
16
|
+
@entities = []
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Records a full observation and indexes the entities it mentions.
|
|
20
|
+
def observe(text)
|
|
21
|
+
found = extract_entities(text)
|
|
22
|
+
@entities.concat(found)
|
|
23
|
+
add(text, { 'entities' => found })
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def entities
|
|
27
|
+
@entities.uniq
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
protected
|
|
31
|
+
|
|
32
|
+
def type_suffix
|
|
33
|
+
'entity'
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def format(record)
|
|
37
|
+
record[:text]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def extract_entities(text)
|
|
43
|
+
text.to_s.scan(/\b([A-Z][a-zA-Z0-9]{1,})\b/).flatten.reject { |w| COMMON.include?(w) }.uniq
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../similarity'
|
|
4
|
+
|
|
5
|
+
module RCrewAI
|
|
6
|
+
class Memory
|
|
7
|
+
# Default, volatile vector store: records live in a Hash keyed by scope.
|
|
8
|
+
# Records: { id:, text:, vector:, metadata: }. Cosine search in Ruby.
|
|
9
|
+
class InMemoryStore
|
|
10
|
+
def initialize
|
|
11
|
+
@scopes = Hash.new { |h, k| h[k] = {} }
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def add(id:, text:, vector:, scope:, metadata: {})
|
|
15
|
+
@scopes[scope][id] = { id: id, text: text, vector: vector, metadata: metadata || {} }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def all(scope:)
|
|
19
|
+
@scopes[scope].values
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def search(vector, k:, scope:)
|
|
23
|
+
@scopes[scope].values
|
|
24
|
+
.reject { |r| r[:vector].nil? }
|
|
25
|
+
.map { |r| [r, Similarity.cosine(vector, r[:vector])] }
|
|
26
|
+
.sort_by { |(_r, score)| -score }
|
|
27
|
+
.first(k)
|
|
28
|
+
.map(&:first)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def delete(scope:)
|
|
32
|
+
@scopes.delete(scope)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def delete_record(id:, scope:)
|
|
36
|
+
@scopes[scope].delete(id)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base_memory'
|
|
4
|
+
|
|
5
|
+
module RCrewAI
|
|
6
|
+
class Memory
|
|
7
|
+
# Durable insights promoted from successful executions. Dedupes
|
|
8
|
+
# near-identical insights so the store doesn't fill with paraphrases.
|
|
9
|
+
class LongTermMemory < BaseMemory
|
|
10
|
+
DEDUPE_THRESHOLD = 0.92
|
|
11
|
+
|
|
12
|
+
def record(text, metadata = {})
|
|
13
|
+
return nil if duplicate?(text)
|
|
14
|
+
|
|
15
|
+
add(text, metadata)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
protected
|
|
19
|
+
|
|
20
|
+
def type_suffix
|
|
21
|
+
'long_term'
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def duplicate?(text)
|
|
27
|
+
existing = @store.all(scope: @scope)
|
|
28
|
+
return false if existing.empty?
|
|
29
|
+
|
|
30
|
+
query_vector = embed(text)
|
|
31
|
+
if query_vector && existing.any? { |r| r[:vector] }
|
|
32
|
+
existing.any? { |r| r[:vector] && Similarity.cosine(query_vector, r[:vector]) >= DEDUPE_THRESHOLD }
|
|
33
|
+
else
|
|
34
|
+
existing.any? { |r| Similarity.lexical(text, r[:text]) >= DEDUPE_THRESHOLD }
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base_memory'
|
|
4
|
+
|
|
5
|
+
module RCrewAI
|
|
6
|
+
class Memory
|
|
7
|
+
# Recent executions with semantic recall. Capped and volatile-by-default.
|
|
8
|
+
class ShortTermMemory < BaseMemory
|
|
9
|
+
def initialize(scope:, embedder: nil, store: nil, limit: 100)
|
|
10
|
+
super
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
protected
|
|
14
|
+
|
|
15
|
+
def type_suffix
|
|
16
|
+
'short_term'
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require_relative '../similarity'
|
|
5
|
+
|
|
6
|
+
module RCrewAI
|
|
7
|
+
class Memory
|
|
8
|
+
# Persistent vector store backed by SQLite. Vectors are packed as
|
|
9
|
+
# little-endian floats; metadata is JSON. Cosine is computed in Ruby over
|
|
10
|
+
# rows filtered by scope — adequate for the thousands-of-memories scale an
|
|
11
|
+
# agent produces; an ANN index is a later optimization.
|
|
12
|
+
#
|
|
13
|
+
# The sqlite3 gem is required lazily so the rest of the library (and the
|
|
14
|
+
# in-memory store) works even if it isn't installed.
|
|
15
|
+
class SqliteStore
|
|
16
|
+
DEFAULT_PATH = File.join(Dir.home, '.rcrewai', 'memory.db')
|
|
17
|
+
|
|
18
|
+
def initialize(path: DEFAULT_PATH)
|
|
19
|
+
require 'sqlite3'
|
|
20
|
+
ensure_parent_dir(path) unless path == ':memory:'
|
|
21
|
+
@db = SQLite3::Database.new(path)
|
|
22
|
+
@db.results_as_hash = true
|
|
23
|
+
create_schema
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def add(id:, text:, vector:, scope:, metadata: {})
|
|
27
|
+
@db.execute(
|
|
28
|
+
'INSERT INTO memories (id, scope, text, vector, metadata) VALUES (?, ?, ?, ?, ?) ' \
|
|
29
|
+
'ON CONFLICT(id) DO UPDATE SET scope=excluded.scope, text=excluded.text, ' \
|
|
30
|
+
'vector=excluded.vector, metadata=excluded.metadata',
|
|
31
|
+
[id, scope, text, pack_vector(vector), JSON.generate(metadata || {})]
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def all(scope:)
|
|
36
|
+
@db.execute('SELECT * FROM memories WHERE scope = ?', [scope]).map { |row| to_record(row) }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def search(vector, k:, scope:)
|
|
40
|
+
all(scope: scope)
|
|
41
|
+
.reject { |r| r[:vector].nil? }
|
|
42
|
+
.map { |r| [r, Similarity.cosine(vector, r[:vector])] }
|
|
43
|
+
.sort_by { |(_r, score)| -score }
|
|
44
|
+
.first(k)
|
|
45
|
+
.map(&:first)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def delete(scope:)
|
|
49
|
+
@db.execute('DELETE FROM memories WHERE scope = ?', [scope])
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def delete_record(id:, scope:)
|
|
53
|
+
@db.execute('DELETE FROM memories WHERE id = ? AND scope = ?', [id, scope])
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def create_schema
|
|
59
|
+
@db.execute(<<~SQL)
|
|
60
|
+
CREATE TABLE IF NOT EXISTS memories (
|
|
61
|
+
id TEXT PRIMARY KEY,
|
|
62
|
+
scope TEXT NOT NULL,
|
|
63
|
+
text TEXT,
|
|
64
|
+
vector BLOB,
|
|
65
|
+
metadata TEXT,
|
|
66
|
+
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
67
|
+
)
|
|
68
|
+
SQL
|
|
69
|
+
@db.execute('CREATE INDEX IF NOT EXISTS idx_memories_scope ON memories (scope)')
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def ensure_parent_dir(path)
|
|
73
|
+
require 'fileutils'
|
|
74
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def pack_vector(vector)
|
|
78
|
+
return nil if vector.nil?
|
|
79
|
+
|
|
80
|
+
vector.map(&:to_f).pack('e*')
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def unpack_vector(blob)
|
|
84
|
+
return nil if blob.nil?
|
|
85
|
+
|
|
86
|
+
blob.unpack('e*')
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def to_record(row)
|
|
90
|
+
{
|
|
91
|
+
id: row['id'],
|
|
92
|
+
text: row['text'],
|
|
93
|
+
vector: unpack_vector(row['vector']),
|
|
94
|
+
metadata: JSON.parse(row['metadata'] || '{}')
|
|
95
|
+
}
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base_memory'
|
|
4
|
+
|
|
5
|
+
module RCrewAI
|
|
6
|
+
class Memory
|
|
7
|
+
# Tool-call history and outcomes. Replaces the old @tool_usage array;
|
|
8
|
+
# persistable and searchable like the other memory types.
|
|
9
|
+
class ToolMemory < BaseMemory
|
|
10
|
+
def record_call(tool_name, params, result)
|
|
11
|
+
success = !result.to_s.downcase.include?('error')
|
|
12
|
+
text = "#{tool_name}(#{format_params(params)}) -> #{result}"
|
|
13
|
+
add(text, { 'tool' => tool_name, 'success' => success, 'result' => result.to_s })
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Most-recent-first usage records for a given tool.
|
|
17
|
+
def usage_for(tool_name, limit: 5)
|
|
18
|
+
@store.all(scope: @scope)
|
|
19
|
+
.select { |r| r[:metadata]['tool'] == tool_name }
|
|
20
|
+
.sort_by { |r| -r[:metadata]['seq'].to_i }
|
|
21
|
+
.first(limit)
|
|
22
|
+
.map { |r| r[:text] }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
protected
|
|
26
|
+
|
|
27
|
+
def type_suffix
|
|
28
|
+
'tool'
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def format_params(params)
|
|
34
|
+
(params || {}).map { |k, v| "#{k}=#{v}" }.join(', ')
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
data/lib/rcrewai/memory.rb
CHANGED
|
@@ -1,202 +1,99 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'json'
|
|
4
|
-
require 'digest'
|
|
5
4
|
|
|
6
5
|
module RCrewAI
|
|
6
|
+
# Cognitive memory facade. Preserves the original public API
|
|
7
|
+
# (add_execution / add_tool_usage / relevant_executions / tool_usage_for /
|
|
8
|
+
# clear_*! / stats) while delegating to semantic, optionally-persistent
|
|
9
|
+
# memory types (short-term, long-term, entity, tool).
|
|
10
|
+
#
|
|
11
|
+
# Zero-config: `Memory.new` uses an in-memory store and, when no embedder is
|
|
12
|
+
# available, falls back to lexical similarity — so existing code behaves as
|
|
13
|
+
# before, just with better recall once an embedder is configured.
|
|
7
14
|
class Memory
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
@
|
|
12
|
-
@
|
|
13
|
-
@tool_usage = [] # Tool usage history
|
|
14
|
-
@max_short_term = 100
|
|
15
|
-
@similarity_threshold = 0.7
|
|
15
|
+
def initialize(scope: 'default', embedder: nil, store: nil, short_term_limit: 100)
|
|
16
|
+
@short_term = ShortTermMemory.new(scope: scope, embedder: embedder, store: store, limit: short_term_limit)
|
|
17
|
+
@long_term = LongTermMemory.new(scope: scope, embedder: embedder, store: store)
|
|
18
|
+
@entity = EntityMemory.new(scope: scope, embedder: embedder, store: store)
|
|
19
|
+
@tool = ToolMemory.new(scope: scope, embedder: embedder, store: store)
|
|
16
20
|
end
|
|
17
21
|
|
|
22
|
+
# --- original API --------------------------------------------------------
|
|
23
|
+
|
|
18
24
|
def add_execution(task, result, execution_time)
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
execution_time
|
|
25
|
-
|
|
26
|
-
success: !result.to_s.downcase.include?('failed'),
|
|
27
|
-
hash: generate_task_hash(task)
|
|
25
|
+
success = !result.to_s.downcase.include?('failed')
|
|
26
|
+
text = "Task: #{task.name}\nDescription: #{task.description}\nResult: #{truncate(result, 300)}"
|
|
27
|
+
metadata = {
|
|
28
|
+
'task' => task.name,
|
|
29
|
+
'success' => success,
|
|
30
|
+
'execution_time' => execution_time,
|
|
31
|
+
'result' => result.to_s
|
|
28
32
|
}
|
|
29
33
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
task_type = execution_data[:task_type]
|
|
38
|
-
@long_term[task_type] ||= []
|
|
39
|
-
@long_term[task_type] << execution_data
|
|
40
|
-
|
|
41
|
-
# Keep only best executions for each type
|
|
42
|
-
@long_term[task_type] = @long_term[task_type]
|
|
43
|
-
.sort_by { |e| [e[:success] ? 0 : 1, -e[:execution_time]] }
|
|
44
|
-
.first(10)
|
|
34
|
+
@short_term.record(text, metadata)
|
|
35
|
+
if success
|
|
36
|
+
@long_term.record(text, metadata)
|
|
37
|
+
@entity.observe("#{task.description} #{result}")
|
|
38
|
+
end
|
|
39
|
+
nil
|
|
45
40
|
end
|
|
46
41
|
|
|
47
42
|
def add_tool_usage(tool_name, params, result)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
params: params,
|
|
51
|
-
result: result,
|
|
52
|
-
timestamp: Time.now,
|
|
53
|
-
success: !result.to_s.downcase.include?('error')
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
@tool_usage.unshift(usage_data)
|
|
57
|
-
@tool_usage = @tool_usage.first(50) # Keep last 50 tool usages
|
|
43
|
+
@tool.record_call(tool_name, params, result)
|
|
44
|
+
nil
|
|
58
45
|
end
|
|
59
46
|
|
|
47
|
+
# Returns a formatted string of the most relevant past executions, or nil.
|
|
60
48
|
def relevant_executions(task, limit = 3)
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
# Check short-term for exact or similar matches
|
|
68
|
-
@short_term.each do |execution|
|
|
69
|
-
if execution[:hash] == task_hash
|
|
70
|
-
candidates << { execution: execution, similarity: 1.0 }
|
|
71
|
-
elsif execution[:task_type] == task_type
|
|
72
|
-
similarity = calculate_similarity(task, execution)
|
|
73
|
-
candidates << { execution: execution, similarity: similarity } if similarity > @similarity_threshold
|
|
74
|
-
end
|
|
75
|
-
end
|
|
49
|
+
query = "#{task.name} #{task.description}"
|
|
50
|
+
recalled = (@short_term.recall(query, limit: limit) + @long_term.recall(query, limit: limit))
|
|
51
|
+
seen = {}
|
|
52
|
+
unique = recalled.reject { |r| seen[r[:text]].tap { seen[r[:text]] = true } }
|
|
53
|
+
return nil if unique.empty?
|
|
76
54
|
|
|
77
|
-
|
|
78
|
-
@long_term[task_type]&.each do |execution|
|
|
79
|
-
similarity = calculate_similarity(task, execution)
|
|
80
|
-
candidates << { execution: execution, similarity: similarity } if similarity > @similarity_threshold
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
# Sort by similarity and success, return top results
|
|
84
|
-
relevant = candidates
|
|
85
|
-
.sort_by { |c| [-c[:similarity], c[:execution][:success] ? 0 : 1] }
|
|
86
|
-
.first(limit)
|
|
87
|
-
.map { |c| format_execution_for_context(c[:execution]) }
|
|
88
|
-
|
|
89
|
-
relevant.empty? ? nil : relevant.join("\n---\n")
|
|
55
|
+
unique.first(limit).map { |r| format_execution(r) }.join("\n---\n")
|
|
90
56
|
end
|
|
91
57
|
|
|
92
58
|
def tool_usage_for(tool_name, limit = 5)
|
|
93
|
-
@
|
|
94
|
-
.select { |usage| usage[:tool_name] == tool_name }
|
|
95
|
-
.first(limit)
|
|
96
|
-
.map { |usage| format_tool_usage_for_context(usage) }
|
|
97
|
-
.join("\n")
|
|
59
|
+
@tool.usage_for(tool_name, limit: limit).join("\n")
|
|
98
60
|
end
|
|
99
61
|
|
|
100
62
|
def clear_short_term!
|
|
101
|
-
@short_term.clear
|
|
63
|
+
@short_term.clear!
|
|
102
64
|
end
|
|
103
65
|
|
|
104
66
|
def clear_all!
|
|
105
|
-
@short_term.clear
|
|
106
|
-
@long_term.clear
|
|
107
|
-
@
|
|
67
|
+
@short_term.clear!
|
|
68
|
+
@long_term.clear!
|
|
69
|
+
@entity.clear!
|
|
70
|
+
@tool.clear!
|
|
108
71
|
end
|
|
109
72
|
|
|
110
73
|
def stats
|
|
111
74
|
{
|
|
112
|
-
short_term_count: @short_term.
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
tool_usage_count: @
|
|
116
|
-
success_rate: calculate_success_rate
|
|
75
|
+
short_term_count: @short_term.count,
|
|
76
|
+
long_term_total: @long_term.count,
|
|
77
|
+
entity_count: @entity.entities.length,
|
|
78
|
+
tool_usage_count: @tool.count
|
|
117
79
|
}
|
|
118
80
|
end
|
|
119
81
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
def classify_task_type(task)
|
|
123
|
-
description = task.description.downcase
|
|
124
|
-
|
|
125
|
-
if description.include?('research') || description.include?('find') || description.include?('search')
|
|
126
|
-
return :research
|
|
127
|
-
end
|
|
128
|
-
if description.include?('analyze') || description.include?('examine') || description.include?('study')
|
|
129
|
-
return :analysis
|
|
130
|
-
end
|
|
131
|
-
if description.include?('write') || description.include?('create') || description.include?('compose')
|
|
132
|
-
return :writing
|
|
133
|
-
end
|
|
134
|
-
if description.include?('code') || description.include?('program') || description.include?('develop')
|
|
135
|
-
return :coding
|
|
136
|
-
end
|
|
137
|
-
if description.include?('plan') || description.include?('strategy') || description.include?('organize')
|
|
138
|
-
return :planning
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
:general
|
|
142
|
-
end
|
|
143
|
-
|
|
144
|
-
def generate_task_hash(task)
|
|
145
|
-
content = "#{task.name}:#{task.description}"
|
|
146
|
-
Digest::SHA256.hexdigest(content)[0..16]
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
def calculate_similarity(task, execution)
|
|
150
|
-
# Simple similarity based on common words and task type
|
|
151
|
-
task_words = extract_keywords(task.description)
|
|
152
|
-
execution_words = extract_keywords(execution[:task_description])
|
|
153
|
-
|
|
154
|
-
common_words = (task_words & execution_words).length
|
|
155
|
-
total_words = (task_words | execution_words).length
|
|
156
|
-
|
|
157
|
-
return 0.0 if total_words.zero?
|
|
82
|
+
# --- new surface (optional direct access) --------------------------------
|
|
158
83
|
|
|
159
|
-
|
|
84
|
+
attr_reader :short_term, :long_term, :entity, :tool
|
|
160
85
|
|
|
161
|
-
|
|
162
|
-
type_bonus = classify_task_type(task) == execution[:task_type] ? 0.2 : 0.0
|
|
163
|
-
|
|
164
|
-
[word_similarity + type_bonus, 1.0].min
|
|
165
|
-
end
|
|
166
|
-
|
|
167
|
-
def extract_keywords(text)
|
|
168
|
-
# Simple keyword extraction - remove common words
|
|
169
|
-
stopwords = %w[the a an and or but in on at to for of with by]
|
|
170
|
-
text.downcase.split(/\W+/).reject { |w| w.length < 3 || stopwords.include?(w) }
|
|
171
|
-
end
|
|
172
|
-
|
|
173
|
-
def format_execution_for_context(execution)
|
|
174
|
-
success_indicator = execution[:success] ? '✓' : '✗'
|
|
175
|
-
<<~CONTEXT
|
|
176
|
-
#{success_indicator} Task: #{execution[:task_name]}
|
|
177
|
-
Description: #{execution[:task_description]}
|
|
178
|
-
Result: #{execution[:result][0..200]}#{'...' if execution[:result].length > 200}
|
|
179
|
-
Time: #{execution[:execution_time].round(2)}s
|
|
180
|
-
Date: #{execution[:timestamp].strftime('%Y-%m-%d %H:%M')}
|
|
181
|
-
CONTEXT
|
|
182
|
-
end
|
|
86
|
+
private
|
|
183
87
|
|
|
184
|
-
def
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
#{success_indicator} Tool: #{usage[:tool_name]}
|
|
189
|
-
Params: #{params_str}
|
|
190
|
-
Result: #{usage[:result][0..100]}#{'...' if usage[:result].to_s.length > 100}
|
|
191
|
-
Date: #{usage[:timestamp].strftime('%Y-%m-%d %H:%M')}
|
|
192
|
-
CONTEXT
|
|
88
|
+
def format_execution(record)
|
|
89
|
+
meta = record[:metadata] || {}
|
|
90
|
+
indicator = meta['success'] == false ? '✗' : '✓'
|
|
91
|
+
"#{indicator} #{record[:text]}"
|
|
193
92
|
end
|
|
194
93
|
|
|
195
|
-
def
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
successful = @short_term.count { |e| e[:success] }
|
|
199
|
-
(successful.to_f / @short_term.length * 100).round(1)
|
|
94
|
+
def truncate(text, limit)
|
|
95
|
+
str = text.to_s
|
|
96
|
+
str.length > limit ? "#{str[0, limit]}..." : str
|
|
200
97
|
end
|
|
201
98
|
end
|
|
202
99
|
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RCrewAI
|
|
4
|
+
# Similarity measures shared across Knowledge and Memory. `cosine` compares
|
|
5
|
+
# embedding vectors; `lexical` is the word-overlap fallback used when no
|
|
6
|
+
# embedder is available.
|
|
7
|
+
module Similarity
|
|
8
|
+
STOPWORDS = %w[the a an and or but in on at to for of with by is are was were be].freeze
|
|
9
|
+
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def cosine(vec_a, vec_b)
|
|
13
|
+
dot = 0.0
|
|
14
|
+
norm_a = 0.0
|
|
15
|
+
norm_b = 0.0
|
|
16
|
+
length = [vec_a.length, vec_b.length].max
|
|
17
|
+
length.times do |i|
|
|
18
|
+
ai = (vec_a[i] || 0).to_f
|
|
19
|
+
bi = (vec_b[i] || 0).to_f
|
|
20
|
+
dot += ai * bi
|
|
21
|
+
norm_a += ai * ai
|
|
22
|
+
norm_b += bi * bi
|
|
23
|
+
end
|
|
24
|
+
return 0.0 if norm_a.zero? || norm_b.zero?
|
|
25
|
+
|
|
26
|
+
dot / (Math.sqrt(norm_a) * Math.sqrt(norm_b))
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Jaccard-style overlap of content words. Cheap, no embeddings.
|
|
30
|
+
def lexical(text_a, text_b)
|
|
31
|
+
words_a = keywords(text_a)
|
|
32
|
+
words_b = keywords(text_b)
|
|
33
|
+
union = (words_a | words_b).length
|
|
34
|
+
return 0.0 if union.zero?
|
|
35
|
+
|
|
36
|
+
(words_a & words_b).length.to_f / union
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def keywords(text)
|
|
40
|
+
text.to_s.downcase.split(/\W+/).reject { |w| w.length < 3 || STOPWORDS.include?(w) }
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
data/lib/rcrewai/version.rb
CHANGED
data/lib/rcrewai.rb
CHANGED
|
@@ -22,7 +22,15 @@ require_relative 'rcrewai/events'
|
|
|
22
22
|
require_relative 'rcrewai/sse_parser'
|
|
23
23
|
require_relative 'rcrewai/pricing'
|
|
24
24
|
require_relative 'rcrewai/llm_client'
|
|
25
|
+
require_relative 'rcrewai/similarity'
|
|
25
26
|
require_relative 'rcrewai/memory'
|
|
27
|
+
require_relative 'rcrewai/memory/in_memory_store'
|
|
28
|
+
require_relative 'rcrewai/memory/sqlite_store'
|
|
29
|
+
require_relative 'rcrewai/memory/base_memory'
|
|
30
|
+
require_relative 'rcrewai/memory/short_term_memory'
|
|
31
|
+
require_relative 'rcrewai/memory/long_term_memory'
|
|
32
|
+
require_relative 'rcrewai/memory/entity_memory'
|
|
33
|
+
require_relative 'rcrewai/memory/tool_memory'
|
|
26
34
|
require_relative 'rcrewai/rate_limiter'
|
|
27
35
|
require_relative 'rcrewai/context_window'
|
|
28
36
|
require_relative 'rcrewai/multimodal'
|
data/rcrewai.gemspec
CHANGED
|
@@ -59,6 +59,7 @@ Gem::Specification.new do |spec|
|
|
|
59
59
|
spec.add_dependency 'nokogiri', '~> 1.15'
|
|
60
60
|
spec.add_dependency 'pdf-reader', '~> 2.11'
|
|
61
61
|
spec.add_dependency 'ruby-openai', '~> 6.3'
|
|
62
|
+
spec.add_dependency 'sqlite3', '~> 2.0'
|
|
62
63
|
spec.add_dependency 'thor', '~> 1.3'
|
|
63
64
|
|
|
64
65
|
# Development dependencies
|
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.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- gkosmo
|
|
@@ -149,6 +149,20 @@ dependencies:
|
|
|
149
149
|
- - "~>"
|
|
150
150
|
- !ruby/object:Gem::Version
|
|
151
151
|
version: '6.3'
|
|
152
|
+
- !ruby/object:Gem::Dependency
|
|
153
|
+
name: sqlite3
|
|
154
|
+
requirement: !ruby/object:Gem::Requirement
|
|
155
|
+
requirements:
|
|
156
|
+
- - "~>"
|
|
157
|
+
- !ruby/object:Gem::Version
|
|
158
|
+
version: '2.0'
|
|
159
|
+
type: :runtime
|
|
160
|
+
prerelease: false
|
|
161
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
162
|
+
requirements:
|
|
163
|
+
- - "~>"
|
|
164
|
+
- !ruby/object:Gem::Version
|
|
165
|
+
version: '2.0'
|
|
152
166
|
- !ruby/object:Gem::Dependency
|
|
153
167
|
name: thor
|
|
154
168
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -342,6 +356,7 @@ files:
|
|
|
342
356
|
- docs/mcp.md
|
|
343
357
|
- docs/superpowers/plans/2026-05-11-llm-modernization.md
|
|
344
358
|
- docs/superpowers/specs/2026-05-11-llm-modernization-design.md
|
|
359
|
+
- docs/superpowers/specs/2026-07-06-cognitive-memory-design.md
|
|
345
360
|
- docs/tutorials/advanced-agents.md
|
|
346
361
|
- docs/tutorials/custom-tools.md
|
|
347
362
|
- docs/tutorials/deployment.md
|
|
@@ -350,7 +365,9 @@ files:
|
|
|
350
365
|
- docs/tutorials/multiple-crews.md
|
|
351
366
|
- docs/upgrading-to-0.3.md
|
|
352
367
|
- docs/upgrading-to-0.4.md
|
|
368
|
+
- docs/upgrading-to-0.6.md
|
|
353
369
|
- examples/async_execution_example.rb
|
|
370
|
+
- examples/cognitive_memory_example.rb
|
|
354
371
|
- examples/flow_example.rb
|
|
355
372
|
- examples/hierarchical_crew_example.rb
|
|
356
373
|
- examples/human_in_the_loop_example.rb
|
|
@@ -393,6 +410,13 @@ files:
|
|
|
393
410
|
- lib/rcrewai/mcp/transport/http.rb
|
|
394
411
|
- lib/rcrewai/mcp/transport/stdio.rb
|
|
395
412
|
- lib/rcrewai/memory.rb
|
|
413
|
+
- lib/rcrewai/memory/base_memory.rb
|
|
414
|
+
- lib/rcrewai/memory/entity_memory.rb
|
|
415
|
+
- lib/rcrewai/memory/in_memory_store.rb
|
|
416
|
+
- lib/rcrewai/memory/long_term_memory.rb
|
|
417
|
+
- lib/rcrewai/memory/short_term_memory.rb
|
|
418
|
+
- lib/rcrewai/memory/sqlite_store.rb
|
|
419
|
+
- lib/rcrewai/memory/tool_memory.rb
|
|
396
420
|
- lib/rcrewai/multimodal.rb
|
|
397
421
|
- lib/rcrewai/output_schema.rb
|
|
398
422
|
- lib/rcrewai/planning.rb
|
|
@@ -400,6 +424,7 @@ files:
|
|
|
400
424
|
- lib/rcrewai/process.rb
|
|
401
425
|
- lib/rcrewai/provider_schema.rb
|
|
402
426
|
- lib/rcrewai/rate_limiter.rb
|
|
427
|
+
- lib/rcrewai/similarity.rb
|
|
403
428
|
- lib/rcrewai/sse_parser.rb
|
|
404
429
|
- lib/rcrewai/task.rb
|
|
405
430
|
- lib/rcrewai/tool_runner.rb
|