llmemory 0.2.0 → 0.2.2
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/README.md +54 -3
- data/app/controllers/llmemory/dashboard/application_controller.rb +15 -1
- data/app/controllers/llmemory/dashboard/episodic_controller.rb +22 -0
- data/app/controllers/llmemory/dashboard/forget_log_controller.rb +12 -0
- data/app/controllers/llmemory/dashboard/maintenance_controller.rb +92 -0
- data/app/controllers/llmemory/dashboard/procedural_controller.rb +22 -0
- data/app/controllers/llmemory/dashboard/reflection_controller.rb +37 -0
- data/app/controllers/llmemory/dashboard/working_controller.rb +14 -0
- data/app/views/llmemory/dashboard/episodic/index.html.erb +37 -0
- data/app/views/llmemory/dashboard/forget_log/show.html.erb +23 -0
- data/app/views/llmemory/dashboard/maintenance/show.html.erb +65 -0
- data/app/views/llmemory/dashboard/procedural/index.html.erb +38 -0
- data/app/views/llmemory/dashboard/reflection/show.html.erb +29 -0
- data/app/views/llmemory/dashboard/users/show.html.erb +16 -0
- data/app/views/llmemory/dashboard/working/show.html.erb +20 -0
- data/config/routes.rb +14 -0
- data/lib/generators/llmemory/install/templates/create_llmemory_tables.rb +22 -0
- data/lib/llmemory/cli/commands/base.rb +8 -0
- data/lib/llmemory/cli/commands/episodic.rb +42 -0
- data/lib/llmemory/cli/commands/forget_log.rb +36 -0
- data/lib/llmemory/cli/commands/maintain.rb +62 -0
- data/lib/llmemory/cli/commands/mine_skills.rb +50 -0
- data/lib/llmemory/cli/commands/procedural.rb +44 -0
- data/lib/llmemory/cli/commands/working.rb +31 -0
- data/lib/llmemory/cli.rb +18 -0
- data/lib/llmemory/configuration.rb +11 -1
- data/lib/llmemory/instrumentation.rb +33 -0
- data/lib/llmemory/llm/anthropic.rb +19 -15
- data/lib/llmemory/llm/openai.rb +16 -12
- data/lib/llmemory/long_term/episodic/memory.rb +94 -26
- data/lib/llmemory/long_term/episodic/storage.rb +7 -5
- data/lib/llmemory/long_term/episodic/storages/active_record_models.rb +17 -0
- data/lib/llmemory/long_term/episodic/storages/active_record_storage.rb +103 -0
- data/lib/llmemory/long_term/episodic/storages/base.rb +15 -2
- data/lib/llmemory/long_term/episodic/storages/database_storage.rb +156 -0
- data/lib/llmemory/long_term/episodic/storages/file_storage.rb +28 -8
- data/lib/llmemory/long_term/episodic/storages/memory_storage.rb +36 -6
- data/lib/llmemory/long_term/file_based/memory.rb +12 -4
- data/lib/llmemory/long_term/file_based/storages/active_record_storage.rb +15 -6
- data/lib/llmemory/long_term/file_based/storages/base.rb +2 -2
- data/lib/llmemory/long_term/file_based/storages/database_storage.rb +20 -8
- data/lib/llmemory/long_term/file_based/storages/file_storage.rb +6 -6
- data/lib/llmemory/long_term/file_based/storages/memory_storage.rb +6 -6
- data/lib/llmemory/long_term/graph_based/memory.rb +89 -64
- data/lib/llmemory/long_term/graph_based/storages/active_record_storage.rb +4 -2
- data/lib/llmemory/long_term/graph_based/storages/base.rb +2 -2
- data/lib/llmemory/long_term/graph_based/storages/memory_storage.rb +4 -2
- data/lib/llmemory/long_term/procedural/memory.rb +97 -30
- data/lib/llmemory/long_term/procedural/skill.rb +6 -2
- data/lib/llmemory/long_term/procedural/storage.rb +7 -5
- data/lib/llmemory/long_term/procedural/storages/active_record_models.rb +17 -0
- data/lib/llmemory/long_term/procedural/storages/active_record_storage.rb +114 -0
- data/lib/llmemory/long_term/procedural/storages/base.rb +14 -1
- data/lib/llmemory/long_term/procedural/storages/database_storage.rb +169 -0
- data/lib/llmemory/long_term/procedural/storages/file_storage.rb +29 -9
- data/lib/llmemory/long_term/procedural/storages/memory_storage.rb +37 -7
- data/lib/llmemory/maintenance/cognitive_pass.rb +109 -0
- data/lib/llmemory/maintenance/ttl_expiry.rb +50 -0
- data/lib/llmemory/maintenance.rb +2 -0
- data/lib/llmemory/mcp/server.rb +17 -1
- data/lib/llmemory/mcp/tools/memory_episode_record.rb +48 -0
- data/lib/llmemory/mcp/tools/memory_episodes.rb +43 -0
- data/lib/llmemory/mcp/tools/memory_forget.rb +53 -0
- data/lib/llmemory/mcp/tools/memory_maintain.rb +53 -0
- data/lib/llmemory/mcp/tools/memory_mine_skills.rb +53 -0
- data/lib/llmemory/mcp/tools/memory_retrieve.rb +10 -2
- data/lib/llmemory/mcp/tools/memory_skill_register.rb +35 -0
- data/lib/llmemory/mcp/tools/memory_skill_report.rb +35 -0
- data/lib/llmemory/mcp/tools/memory_skills.rb +43 -0
- data/lib/llmemory/memory.rb +48 -3
- data/lib/llmemory/memory_module.rb +13 -6
- data/lib/llmemory/reflection/reflector.rb +24 -20
- data/lib/llmemory/retrieval/bm25_scorer.rb +1 -1
- data/lib/llmemory/retrieval/engine.rb +25 -16
- data/lib/llmemory/retrieval/mmr_reranker.rb +1 -1
- data/lib/llmemory/short_term/session_lifecycle.rb +19 -3
- data/lib/llmemory/skill_mining/miner.rb +163 -0
- data/lib/llmemory/skill_mining.rb +8 -0
- data/lib/llmemory/tokenizer.rb +27 -0
- data/lib/llmemory/vector_store/active_record_store.rb +4 -3
- data/lib/llmemory/vector_store/openai_embeddings.rb +11 -7
- data/lib/llmemory/vector_store.rb +14 -0
- data/lib/llmemory/version.rb +1 -1
- data/lib/llmemory.rb +3 -0
- metadata +39 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fdcf202249038554cae18d79da76c261a9c7a80687081126ce985562ef8607ae
|
|
4
|
+
data.tar.gz: 9b28b0ba29d4444712c2592a808f6b08bbf38f804c031e6b8b246914f7b86699
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4bddb0f7e9a4bfe6cfd488a341efce98ab4194c36fd6ebcb22b024db338224d0191e3bfb5bbc9638ad43092cac66bb29d159e87d81b97f6d17c8ee82b400c716
|
|
7
|
+
data.tar.gz: 8e5fb5edddabce0b1b57903282bb868a076d1595c4d187c973ccb7a7fa61f11847c9889e185459438423553aee0bf52f216e3bbdf0d9ac870f9f8196c230c1f1
|
data/README.md
CHANGED
|
@@ -74,6 +74,11 @@ Llmemory.configure do |config|
|
|
|
74
74
|
config.importance_weight = 1.0 # how strongly importance multiplies the score (0 = ignore)
|
|
75
75
|
config.retrieval_feedback_weight = 0.5 # how strongly useful/harmful feedback shifts ranking (0 = ignore)
|
|
76
76
|
|
|
77
|
+
# Semantic (embedding) retrieval for episodic/procedural memory (opt-in;
|
|
78
|
+
# default off keeps them deterministic and network-free)
|
|
79
|
+
config.episodic_vector_enabled = false
|
|
80
|
+
config.procedural_vector_enabled = false
|
|
81
|
+
|
|
77
82
|
# Pre-compaction memory flush (prevents knowledge loss when compacting)
|
|
78
83
|
config.memory_flush_enabled = true
|
|
79
84
|
config.memory_flush_threshold_tokens = 4000
|
|
@@ -129,7 +134,7 @@ rails g llmemory:install
|
|
|
129
134
|
rails db:migrate
|
|
130
135
|
```
|
|
131
136
|
|
|
132
|
-
La migración crea las tablas de long-term file-based (resources, items, categories), short-term (checkpoints) y, para graph-based, nodos, aristas y embeddings (`llmemory_nodes`, `llmemory_edges`, `llmemory_embeddings`). Para embeddings se usa pgvector; asegúrate de tener la extensión `vector` en PostgreSQL. Para usar ambas con ActiveRecord:
|
|
137
|
+
La migración crea las tablas de long-term file-based (resources, items, categories), short-term (checkpoints), episódica y procedural (`llmemory_episodes`, `llmemory_skills`; columna `data` JSONB) y, para graph-based, nodos, aristas y embeddings (`llmemory_nodes`, `llmemory_edges`, `llmemory_embeddings`). Para embeddings se usa pgvector; asegúrate de tener la extensión `vector` en PostgreSQL. Para usar ambas con ActiveRecord:
|
|
133
138
|
|
|
134
139
|
```ruby
|
|
135
140
|
# config/application.rb o config/initializers/llmemory.rb
|
|
@@ -211,10 +216,10 @@ llmemory implements the memory and internal-action concepts from [CoALA — Cogn
|
|
|
211
216
|
| Procedural memory | `Llmemory::LongTerm::Procedural::Memory` |
|
|
212
217
|
| Reasoning action | `Llmemory::Actions::Reason` |
|
|
213
218
|
| Retrieval action | `Retrieval::Engine` (+ feedback, iterative) |
|
|
214
|
-
| Learning action | `memorize` / `record_episode` / `register_skill` / reflection |
|
|
219
|
+
| Learning action | `memorize` / `record_episode` / `register_skill` / reflection / skill mining |
|
|
215
220
|
| Uniform interface | `Llmemory::MemoryModule` (`read`/`write`/`list`/`stats`/`forget`) |
|
|
216
221
|
|
|
217
|
-
All three long-term memories below are **additive** — episodic and procedural coexist with semantic memory rather than replacing it.
|
|
222
|
+
All three long-term memories below are **additive** — episodic and procedural coexist with semantic memory rather than replacing it. They support `:memory`, `:file`, `:postgres` and `:active_record` backends. Retrieval is keyword-based by default (tokenized, so multi-word queries work); semantic (embedding) retrieval is **opt-in** via `config.episodic_vector_enabled` / `config.procedural_vector_enabled` (or by injecting a `vector_store:`), which makes `search_candidates` hybrid (vector + keyword).
|
|
218
223
|
|
|
219
224
|
### Working memory (structured, persists across LLM calls)
|
|
220
225
|
|
|
@@ -305,6 +310,39 @@ skills.find_skill("revert deploy") # best match (a Skill)
|
|
|
305
310
|
skills.report_outcome(id, success: true) # feeds ranking + adaptive retrieval
|
|
306
311
|
```
|
|
307
312
|
|
|
313
|
+
### Skill mining (episodic → procedural)
|
|
314
|
+
|
|
315
|
+
Rather than writing every skill by hand, mine them from successful episode trajectories (Voyager's contribution: procedural memory grows from lived experience). Mining is **human-in-the-loop by default** — it returns proposals and writes nothing until you opt in.
|
|
316
|
+
|
|
317
|
+
```ruby
|
|
318
|
+
miner = Llmemory::SkillMining::Miner.new(episodic: episodic, procedural: skills)
|
|
319
|
+
|
|
320
|
+
proposals = miner.mine(window: 20) # => [{ name:, kind:, body:, description:, confidence: }, ...]
|
|
321
|
+
miner.mine(window: 20, outcomes: ["success"]) # deterministic pre-filter by outcome label
|
|
322
|
+
ids = miner.mine(window: 20, auto_register: true) # register proposals straight away
|
|
323
|
+
|
|
324
|
+
# Registered skills carry provenance { method: "skill_mining", sources: [{ type: "episode", id: ... }] }.
|
|
325
|
+
# From the unified API: memory.mine_skills!(auto_register: true)
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
### Cognitive maintenance pass (closing the loop)
|
|
329
|
+
|
|
330
|
+
One scheduled step that runs the whole learning loop — consolidate → reflect → mine skills → TTL expiry — for a user. Each step is isolated: a failure is captured in the report and never aborts the others.
|
|
331
|
+
|
|
332
|
+
```ruby
|
|
333
|
+
report = Llmemory::Maintenance::CognitivePass.run!(
|
|
334
|
+
"u1",
|
|
335
|
+
reflect: true, mine_skills: true, expire: true, # toggle steps
|
|
336
|
+
reflection_window: 10, mining_window: 20
|
|
337
|
+
)
|
|
338
|
+
# => { consolidated:, insights: [...], mined: [...], expired: { episodic:, procedural: }, errors: {} }
|
|
339
|
+
|
|
340
|
+
# From the unified API (wires in the live session, so consolidate! runs too):
|
|
341
|
+
memory.maintain!(mine_skills: true)
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
`mine_skills` defaults to `config.skill_mining_enabled` (default `false`). Run it from cron or a Rails job. Components (`episodic:`, `procedural:`, `semantic:`, `llm:`) are injectable; built per-config when omitted.
|
|
345
|
+
|
|
308
346
|
### Uniform interface (MemoryModule)
|
|
309
347
|
|
|
310
348
|
The queryable long-term memories (file, graph, episodic, procedural) share one agent-facing contract, so a framework can treat them polymorphically:
|
|
@@ -484,6 +522,14 @@ llmemory edges USER_ID [--subject NODE_ID] [--limit N]
|
|
|
484
522
|
llmemory graph USER_ID [--format dot|json]
|
|
485
523
|
llmemory search USER_ID "query" [--type short|long|all]
|
|
486
524
|
llmemory stats [USER_ID]
|
|
525
|
+
|
|
526
|
+
# Cognitive memory (CoALA)
|
|
527
|
+
llmemory episodes USER_ID [--limit N]
|
|
528
|
+
llmemory skills USER_ID [--limit N]
|
|
529
|
+
llmemory working USER_ID SESSION_ID
|
|
530
|
+
llmemory forget-log USER_ID
|
|
531
|
+
llmemory mine-skills USER_ID [--window N] [--outcomes success,recovered] [--register]
|
|
532
|
+
llmemory maintain USER_ID [--[no-]reflect] [--mine-skills] [--[no-]expire] [--window N]
|
|
487
533
|
```
|
|
488
534
|
|
|
489
535
|
Use `--store TYPE` where applicable to override the configured store (e.g. `memory`, `redis`, `postgres`, `active_record` for short-term; same or `file` for long-term file-based).
|
|
@@ -610,6 +656,11 @@ MCP_TOKEN=your-secret-token llmemory mcp serve --http --port 443 \
|
|
|
610
656
|
| `memory_consolidate` | Extract facts from conversation to long-term |
|
|
611
657
|
| `memory_stats` | Get memory statistics for a user |
|
|
612
658
|
| `memory_info` | Documentation on how to use the tools |
|
|
659
|
+
| `memory_episode_record` / `memory_episodes` | Record / list episodic trajectories |
|
|
660
|
+
| `memory_skill_register` / `memory_skill_report` / `memory_skills` | Register / outcome-track / list procedural skills |
|
|
661
|
+
| `memory_forget` | Forget entries by id (audited) across any memory type |
|
|
662
|
+
| `memory_mine_skills` | Mine reusable skills from episodes (proposals by default; `auto_register` to save) |
|
|
663
|
+
| `memory_maintain` | Run the cognitive maintenance pass (reflect → mine → expire) and return a report |
|
|
613
664
|
|
|
614
665
|
### Configuration for Claude Code
|
|
615
666
|
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
module Llmemory
|
|
4
4
|
module Dashboard
|
|
5
5
|
class ApplicationController < ActionController::Base
|
|
6
|
-
helper_method :short_term_store, :file_based_storage, :graph_based_storage,
|
|
6
|
+
helper_method :short_term_store, :file_based_storage, :graph_based_storage,
|
|
7
|
+
:episodic_storage, :procedural_storage, :forget_log,
|
|
8
|
+
:file_based?, :graph_based?
|
|
7
9
|
|
|
8
10
|
protected
|
|
9
11
|
|
|
@@ -19,6 +21,18 @@ module Llmemory
|
|
|
19
21
|
@graph_based_storage ||= build_graph_based_storage
|
|
20
22
|
end
|
|
21
23
|
|
|
24
|
+
def episodic_storage
|
|
25
|
+
@episodic_storage ||= Llmemory::LongTerm::Episodic::Storages.build
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def procedural_storage
|
|
29
|
+
@procedural_storage ||= Llmemory::LongTerm::Procedural::Storages.build
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def forget_log
|
|
33
|
+
@forget_log ||= Llmemory::ForgetLog.new
|
|
34
|
+
end
|
|
35
|
+
|
|
22
36
|
def long_term_type
|
|
23
37
|
Llmemory.configuration.long_term_type.to_s
|
|
24
38
|
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Llmemory
|
|
4
|
+
module Dashboard
|
|
5
|
+
class EpisodicController < ApplicationController
|
|
6
|
+
def index
|
|
7
|
+
@user_id = params[:user_id]
|
|
8
|
+
@limit = (params[:limit].presence || 50).to_i
|
|
9
|
+
@offset = (params[:offset].presence || 0).to_i
|
|
10
|
+
@episodes = episodic_storage.list_episodes(@user_id, limit: @limit, offset: @offset)
|
|
11
|
+
@total = episodic_storage.count_episodes(@user_id)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def forget
|
|
15
|
+
memory = Llmemory::LongTerm::Episodic::Memory.new(user_id: params[:user_id], storage: episodic_storage)
|
|
16
|
+
mode = params[:mode].to_s == "hard" ? :hard : :soft
|
|
17
|
+
memory.forget(ids: [params[:id]], reason: params[:reason], mode: mode)
|
|
18
|
+
redirect_to user_episodic_path(params[:user_id]), notice: "Forgot episode #{params[:id]} (#{mode})."
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
module Llmemory
|
|
2
|
+
module Dashboard
|
|
3
|
+
# Cognitive maintenance surface (SF20): trigger a maintenance pass and review
|
|
4
|
+
# mined-skill proposals before registering them (human-in-the-loop).
|
|
5
|
+
class MaintenanceController < ApplicationController
|
|
6
|
+
def show
|
|
7
|
+
@user_id = params[:user_id]
|
|
8
|
+
@window = (params[:window].presence || 10).to_i
|
|
9
|
+
@recent_episodes = episodic_storage.list_episodes(@user_id, limit: @window)
|
|
10
|
+
@proposals = session_proposals
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Runs the full cognitive pass (reflect -> mine -> expire) and reports.
|
|
14
|
+
def run
|
|
15
|
+
user_id = params[:user_id]
|
|
16
|
+
window = (params[:window].presence || 10).to_i
|
|
17
|
+
report = Llmemory::Maintenance::CognitivePass.run!(
|
|
18
|
+
user_id,
|
|
19
|
+
episodic: episodic_memory(user_id),
|
|
20
|
+
procedural: procedural_memory(user_id),
|
|
21
|
+
semantic: build_semantic_memory(user_id),
|
|
22
|
+
mine_skills: params[:mine_skills].present?,
|
|
23
|
+
reflection_window: window
|
|
24
|
+
)
|
|
25
|
+
redirect_to user_maintenance_path(user_id), notice: pass_notice(report)
|
|
26
|
+
rescue Llmemory::LLMError => e
|
|
27
|
+
redirect_to user_maintenance_path(user_id), alert: "Maintenance pass failed: #{e.message}"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Mines skills WITHOUT registering — returns proposals for review.
|
|
31
|
+
def mine
|
|
32
|
+
user_id = params[:user_id]
|
|
33
|
+
window = (params[:window].presence || 20).to_i
|
|
34
|
+
proposals = Llmemory::SkillMining::Miner.new(
|
|
35
|
+
episodic: episodic_memory(user_id), procedural: procedural_memory(user_id)
|
|
36
|
+
).mine(window: window, auto_register: false)
|
|
37
|
+
store_proposals(proposals)
|
|
38
|
+
redirect_to user_maintenance_path(user_id), notice: "Mined #{proposals.size} skill proposal(s) for review."
|
|
39
|
+
rescue Llmemory::LLMError => e
|
|
40
|
+
redirect_to user_maintenance_path(user_id), alert: "Skill mining failed: #{e.message}"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Registers a single reviewed proposal into procedural memory.
|
|
44
|
+
def register
|
|
45
|
+
user_id = params[:user_id]
|
|
46
|
+
procedural_memory(user_id).register_skill(
|
|
47
|
+
name: params[:name], body: params[:body], description: params[:description].presence,
|
|
48
|
+
kind: params[:kind].presence || Llmemory::LongTerm::Procedural::Skill::DEFAULT_KIND,
|
|
49
|
+
provenance: Llmemory::Provenance.build(method: "skill_mining")
|
|
50
|
+
)
|
|
51
|
+
redirect_to user_maintenance_path(user_id), notice: "Registered skill #{params[:name]}."
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def episodic_memory(user_id)
|
|
57
|
+
Llmemory::LongTerm::Episodic::Memory.new(user_id: user_id, storage: episodic_storage)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def procedural_memory(user_id)
|
|
61
|
+
Llmemory::LongTerm::Procedural::Memory.new(user_id: user_id, storage: procedural_storage)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def build_semantic_memory(user_id)
|
|
65
|
+
if graph_based?
|
|
66
|
+
Llmemory::LongTerm::GraphBased::Memory.new(user_id: user_id, storage: graph_based_storage)
|
|
67
|
+
else
|
|
68
|
+
Llmemory::LongTerm::FileBased::Memory.new(user_id: user_id, storage: file_based_storage)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def pass_notice(report)
|
|
73
|
+
expired = report[:expired] || {}
|
|
74
|
+
msg = "Pass complete: #{Array(report[:insights]).size} insight(s), " \
|
|
75
|
+
"#{Array(report[:mined]).size} skill(s) mined, " \
|
|
76
|
+
"expired episodic=#{expired[:episodic] || 0} procedural=#{expired[:procedural] || 0}."
|
|
77
|
+
errors = report[:errors] || {}
|
|
78
|
+
errors.empty? ? msg : "#{msg} Errors: #{errors.map { |k, v| "#{k}: #{v}" }.join('; ')}"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Proposals are stashed in the session so they survive the redirect to
|
|
82
|
+
# `show` without being persisted to a store before the user confirms.
|
|
83
|
+
def store_proposals(proposals)
|
|
84
|
+
session[:llmemory_skill_proposals] = proposals
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def session_proposals
|
|
88
|
+
Array(session.delete(:llmemory_skill_proposals)).map { |p| p.respond_to?(:symbolize_keys) ? p.symbolize_keys : p }
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Llmemory
|
|
4
|
+
module Dashboard
|
|
5
|
+
class ProceduralController < ApplicationController
|
|
6
|
+
def index
|
|
7
|
+
@user_id = params[:user_id]
|
|
8
|
+
@limit = (params[:limit].presence || 50).to_i
|
|
9
|
+
@offset = (params[:offset].presence || 0).to_i
|
|
10
|
+
@skills = procedural_storage.list_skills(@user_id, limit: @limit, offset: @offset)
|
|
11
|
+
@total = procedural_storage.count_skills(@user_id)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def forget
|
|
15
|
+
memory = Llmemory::LongTerm::Procedural::Memory.new(user_id: params[:user_id], storage: procedural_storage)
|
|
16
|
+
mode = params[:mode].to_s == "hard" ? :hard : :soft
|
|
17
|
+
memory.forget(ids: [params[:id]], reason: params[:reason], mode: mode)
|
|
18
|
+
redirect_to user_procedural_path(params[:user_id]), notice: "Forgot skill #{params[:id]} (#{mode})."
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Llmemory
|
|
4
|
+
module Dashboard
|
|
5
|
+
# Read-only landing for reflection. Triggers (POST #run) build a Reflector
|
|
6
|
+
# over the configured episodic + semantic memories and run a single pass
|
|
7
|
+
# over the last N episodes.
|
|
8
|
+
class ReflectionController < ApplicationController
|
|
9
|
+
def show
|
|
10
|
+
@user_id = params[:user_id]
|
|
11
|
+
@recent_window = (params[:window].presence || 10).to_i
|
|
12
|
+
@recent_episodes = episodic_storage.list_episodes(@user_id, limit: @recent_window)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def run
|
|
16
|
+
user_id = params[:user_id]
|
|
17
|
+
window = (params[:window].presence || 10).to_i
|
|
18
|
+
episodic = Llmemory::LongTerm::Episodic::Memory.new(user_id: user_id, storage: episodic_storage)
|
|
19
|
+
semantic = build_semantic_memory(user_id)
|
|
20
|
+
insight_ids = Llmemory::Reflection::Reflector.new(episodic: episodic, semantic: semantic).reflect(window: window)
|
|
21
|
+
redirect_to user_reflection_path(user_id), notice: "Reflection produced #{insight_ids.size} insight(s)."
|
|
22
|
+
rescue Llmemory::LLMError => e
|
|
23
|
+
redirect_to user_reflection_path(user_id), alert: "Reflection failed: #{e.message}"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def build_semantic_memory(user_id)
|
|
29
|
+
if graph_based?
|
|
30
|
+
Llmemory::LongTerm::GraphBased::Memory.new(user_id: user_id, storage: graph_based_storage)
|
|
31
|
+
else
|
|
32
|
+
Llmemory::LongTerm::FileBased::Memory.new(user_id: user_id, storage: file_based_storage)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Llmemory
|
|
4
|
+
module Dashboard
|
|
5
|
+
class WorkingController < ApplicationController
|
|
6
|
+
def show
|
|
7
|
+
@user_id = params[:user_id]
|
|
8
|
+
@session_id = params[:session_id]
|
|
9
|
+
@working = Llmemory::WorkingMemory.new(user_id: @user_id, session_id: @session_id, store: short_term_store)
|
|
10
|
+
@slots = @working.to_h
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<div class="card">
|
|
2
|
+
<h2>Episodic memory: <%= @user_id %></h2>
|
|
3
|
+
<p>Total active episodes: <strong><%= @total %></strong></p>
|
|
4
|
+
<% if @episodes.blank? %>
|
|
5
|
+
<p class="empty">No episodes.</p>
|
|
6
|
+
<% else %>
|
|
7
|
+
<table>
|
|
8
|
+
<thead>
|
|
9
|
+
<tr><th>ID</th><th>Summary</th><th>Outcome</th><th>Steps</th><th>Created</th><th>Forget</th></tr>
|
|
10
|
+
</thead>
|
|
11
|
+
<tbody>
|
|
12
|
+
<% @episodes.each do |e| %>
|
|
13
|
+
<% id = e[:id] || e["id"] %>
|
|
14
|
+
<tr>
|
|
15
|
+
<td><%= id %></td>
|
|
16
|
+
<td><%= truncate((e[:summary] || e["summary"]).to_s, length: 80) %></td>
|
|
17
|
+
<td><%= e[:outcome] || e["outcome"] %></td>
|
|
18
|
+
<td><%= Array(e[:steps] || e["steps"]).size %></td>
|
|
19
|
+
<td><%= e[:created_at] || e["created_at"] %></td>
|
|
20
|
+
<td>
|
|
21
|
+
<%= button_to "Soft", forget_user_episodic_path(@user_id), params: { id: id, mode: "soft" }, method: :post %>
|
|
22
|
+
<%= button_to "Hard", forget_user_episodic_path(@user_id), params: { id: id, mode: "hard" }, method: :post, data: { confirm: "Hard-delete is irreversible." } %>
|
|
23
|
+
</td>
|
|
24
|
+
</tr>
|
|
25
|
+
<% end %>
|
|
26
|
+
</tbody>
|
|
27
|
+
</table>
|
|
28
|
+
<p>
|
|
29
|
+
<% if @offset.positive? %>
|
|
30
|
+
<a href="?offset=<%= [@offset - @limit, 0].max %>&limit=<%= @limit %>">« Newer</a>
|
|
31
|
+
<% end %>
|
|
32
|
+
<% if (@offset + @limit) < @total %>
|
|
33
|
+
<a href="?offset=<%= @offset + @limit %>&limit=<%= @limit %>">Older »</a>
|
|
34
|
+
<% end %>
|
|
35
|
+
</p>
|
|
36
|
+
<% end %>
|
|
37
|
+
</div>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<div class="card">
|
|
2
|
+
<h2>Forget log: <%= @user_id %></h2>
|
|
3
|
+
<p>Audit trail of removals (soft-archive + hard-delete). Recorded by <code>MemoryModule#forget</code>.</p>
|
|
4
|
+
<% if @entries.blank? %>
|
|
5
|
+
<p class="empty">No forget events.</p>
|
|
6
|
+
<% else %>
|
|
7
|
+
<table>
|
|
8
|
+
<thead>
|
|
9
|
+
<tr><th>Time</th><th>Memory type</th><th>Ids</th><th>Reason</th></tr>
|
|
10
|
+
</thead>
|
|
11
|
+
<tbody>
|
|
12
|
+
<% @entries.each do |entry| %>
|
|
13
|
+
<tr>
|
|
14
|
+
<td><%= entry[:archived_at] || entry["archived_at"] %></td>
|
|
15
|
+
<td><%= entry[:memory_type] || entry["memory_type"] %></td>
|
|
16
|
+
<td><code><%= Array(entry[:ids] || entry["ids"]).join(", ") %></code></td>
|
|
17
|
+
<td><%= entry[:reason] || entry["reason"] %></td>
|
|
18
|
+
</tr>
|
|
19
|
+
<% end %>
|
|
20
|
+
</tbody>
|
|
21
|
+
</table>
|
|
22
|
+
<% end %>
|
|
23
|
+
</div>
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
<div class="card">
|
|
2
|
+
<h2>Cognitive maintenance: <%= @user_id %></h2>
|
|
3
|
+
<p>The maintenance pass closes the CoALA learning loop: reflect (episodes → insights), mine skills (episodes → procedural), and expire entries past their TTL. Each step is isolated — a failure in one does not abort the others.</p>
|
|
4
|
+
|
|
5
|
+
<%= form_with url: run_user_maintenance_path(@user_id), method: :post, local: true do %>
|
|
6
|
+
<label>Reflection window (episodes): <%= number_field_tag :window, @window, min: 1, max: 100 %></label>
|
|
7
|
+
<label><%= check_box_tag :mine_skills, "1", true %> Mine & register skills</label>
|
|
8
|
+
<%= submit_tag "Run maintenance pass" %>
|
|
9
|
+
<% end %>
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
<div class="card">
|
|
13
|
+
<h3>Review mined skills (human-in-the-loop)</h3>
|
|
14
|
+
<p>Mine proposals without registering them, then register the ones you want.</p>
|
|
15
|
+
|
|
16
|
+
<%= form_with url: mine_user_maintenance_path(@user_id), method: :post, local: true do %>
|
|
17
|
+
<label>Mining window (episodes): <%= number_field_tag :window, 20, min: 1, max: 100 %></label>
|
|
18
|
+
<%= submit_tag "Mine proposals" %>
|
|
19
|
+
<% end %>
|
|
20
|
+
|
|
21
|
+
<% if @proposals.present? %>
|
|
22
|
+
<table>
|
|
23
|
+
<thead>
|
|
24
|
+
<tr><th>Name</th><th>Kind</th><th>Confidence</th><th>Body</th><th>Register</th></tr>
|
|
25
|
+
</thead>
|
|
26
|
+
<tbody>
|
|
27
|
+
<% @proposals.each do |p| %>
|
|
28
|
+
<tr>
|
|
29
|
+
<td><%= p[:name] %></td>
|
|
30
|
+
<td><%= p[:kind] %></td>
|
|
31
|
+
<td><%= p[:confidence] %></td>
|
|
32
|
+
<td><%= truncate(p[:body].to_s, length: 120) %></td>
|
|
33
|
+
<td>
|
|
34
|
+
<%= button_to "Register", register_user_maintenance_path(@user_id), method: :post, params: {
|
|
35
|
+
name: p[:name], body: p[:body], kind: p[:kind], description: p[:description]
|
|
36
|
+
} %>
|
|
37
|
+
</td>
|
|
38
|
+
</tr>
|
|
39
|
+
<% end %>
|
|
40
|
+
</tbody>
|
|
41
|
+
</table>
|
|
42
|
+
<% end %>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<div class="card">
|
|
46
|
+
<h3>Last <%= @window %> episodes</h3>
|
|
47
|
+
<% if @recent_episodes.blank? %>
|
|
48
|
+
<p class="empty">No episodes to learn from.</p>
|
|
49
|
+
<% else %>
|
|
50
|
+
<table>
|
|
51
|
+
<thead>
|
|
52
|
+
<tr><th>ID</th><th>Summary</th><th>Outcome</th></tr>
|
|
53
|
+
</thead>
|
|
54
|
+
<tbody>
|
|
55
|
+
<% @recent_episodes.each do |e| %>
|
|
56
|
+
<tr>
|
|
57
|
+
<td><%= e[:id] || e["id"] %></td>
|
|
58
|
+
<td><%= truncate((e[:summary] || e["summary"]).to_s, length: 100) %></td>
|
|
59
|
+
<td><%= e[:outcome] || e["outcome"] %></td>
|
|
60
|
+
</tr>
|
|
61
|
+
<% end %>
|
|
62
|
+
</tbody>
|
|
63
|
+
</table>
|
|
64
|
+
<% end %>
|
|
65
|
+
</div>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<div class="card">
|
|
2
|
+
<h2>Procedural memory (skills): <%= @user_id %></h2>
|
|
3
|
+
<p>Total active skills: <strong><%= @total %></strong></p>
|
|
4
|
+
<% if @skills.blank? %>
|
|
5
|
+
<p class="empty">No skills.</p>
|
|
6
|
+
<% else %>
|
|
7
|
+
<table>
|
|
8
|
+
<thead>
|
|
9
|
+
<tr><th>ID</th><th>Name</th><th>Description</th><th>Success</th><th>Failure</th><th>Version</th><th>Forget</th></tr>
|
|
10
|
+
</thead>
|
|
11
|
+
<tbody>
|
|
12
|
+
<% @skills.each do |s| %>
|
|
13
|
+
<% id = s[:id] || s["id"] %>
|
|
14
|
+
<tr>
|
|
15
|
+
<td><%= id %></td>
|
|
16
|
+
<td><%= s[:name] || s["name"] %></td>
|
|
17
|
+
<td><%= truncate((s[:description] || s["description"]).to_s, length: 100) %></td>
|
|
18
|
+
<td><%= (s[:success_count] || s["success_count"]).to_i %></td>
|
|
19
|
+
<td><%= (s[:failure_count] || s["failure_count"]).to_i %></td>
|
|
20
|
+
<td><%= s[:version] || s["version"] %></td>
|
|
21
|
+
<td>
|
|
22
|
+
<%= button_to "Soft", forget_user_procedural_path(@user_id), params: { id: id, mode: "soft" }, method: :post %>
|
|
23
|
+
<%= button_to "Hard", forget_user_procedural_path(@user_id), params: { id: id, mode: "hard" }, method: :post, data: { confirm: "Hard-delete is irreversible." } %>
|
|
24
|
+
</td>
|
|
25
|
+
</tr>
|
|
26
|
+
<% end %>
|
|
27
|
+
</tbody>
|
|
28
|
+
</table>
|
|
29
|
+
<p>
|
|
30
|
+
<% if @offset.positive? %>
|
|
31
|
+
<a href="?offset=<%= [@offset - @limit, 0].max %>&limit=<%= @limit %>">« Newer</a>
|
|
32
|
+
<% end %>
|
|
33
|
+
<% if (@offset + @limit) < @total %>
|
|
34
|
+
<a href="?offset=<%= @offset + @limit %>&limit=<%= @limit %>">Older »</a>
|
|
35
|
+
<% end %>
|
|
36
|
+
</p>
|
|
37
|
+
<% end %>
|
|
38
|
+
</div>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<div class="card">
|
|
2
|
+
<h2>Reflection: <%= @user_id %></h2>
|
|
3
|
+
<p>The reflector distills the last N episodes into semantic insights (Reflexion / Generative Agents pattern), with provenance to the source episodes.</p>
|
|
4
|
+
|
|
5
|
+
<%= form_with url: run_user_reflection_path(@user_id), method: :post, local: true do %>
|
|
6
|
+
<label>Window (episodes): <%= number_field_tag :window, @recent_window, min: 1, max: 100 %></label>
|
|
7
|
+
<%= submit_tag "Run reflection" %>
|
|
8
|
+
<% end %>
|
|
9
|
+
|
|
10
|
+
<h3>Last <%= @recent_window %> episodes</h3>
|
|
11
|
+
<% if @recent_episodes.blank? %>
|
|
12
|
+
<p class="empty">No episodes to reflect over.</p>
|
|
13
|
+
<% else %>
|
|
14
|
+
<table>
|
|
15
|
+
<thead>
|
|
16
|
+
<tr><th>ID</th><th>Summary</th><th>Outcome</th></tr>
|
|
17
|
+
</thead>
|
|
18
|
+
<tbody>
|
|
19
|
+
<% @recent_episodes.each do |e| %>
|
|
20
|
+
<tr>
|
|
21
|
+
<td><%= e[:id] || e["id"] %></td>
|
|
22
|
+
<td><%= truncate((e[:summary] || e["summary"]).to_s, length: 100) %></td>
|
|
23
|
+
<td><%= e[:outcome] || e["outcome"] %></td>
|
|
24
|
+
</tr>
|
|
25
|
+
<% end %>
|
|
26
|
+
</tbody>
|
|
27
|
+
</table>
|
|
28
|
+
<% end %>
|
|
29
|
+
</div>
|
|
@@ -10,5 +10,21 @@
|
|
|
10
10
|
<% end %>
|
|
11
11
|
| <a href="<%= user_stats_path(@user_id) %>">Stats</a>
|
|
12
12
|
</p>
|
|
13
|
+
<p>
|
|
14
|
+
<strong>Cognitive memory:</strong>
|
|
15
|
+
<a href="<%= user_episodic_path(@user_id) %>">Episodes</a>
|
|
16
|
+
| <a href="<%= user_procedural_path(@user_id) %>">Skills</a>
|
|
17
|
+
| <a href="<%= user_reflection_path(@user_id) %>">Reflection</a>
|
|
18
|
+
| <a href="<%= user_maintenance_path(@user_id) %>">Maintenance</a>
|
|
19
|
+
| <a href="<%= user_forget_log_path(@user_id) %>">Forget log</a>
|
|
20
|
+
</p>
|
|
13
21
|
<p>Sessions: <%= @sessions.join(", ") %></p>
|
|
22
|
+
<% if @sessions.any? %>
|
|
23
|
+
<p>
|
|
24
|
+
<strong>Working memory:</strong>
|
|
25
|
+
<% @sessions.each do |s| %>
|
|
26
|
+
<a href="<%= user_working_path(@user_id, s) %>"><%= s %></a>
|
|
27
|
+
<% end %>
|
|
28
|
+
</p>
|
|
29
|
+
<% end %>
|
|
14
30
|
</div>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<div class="card">
|
|
2
|
+
<h2>Working memory: <%= @user_id %> / <%= @session_id %></h2>
|
|
3
|
+
<% if @slots.blank? %>
|
|
4
|
+
<p class="empty">No slots set.</p>
|
|
5
|
+
<% else %>
|
|
6
|
+
<table>
|
|
7
|
+
<thead>
|
|
8
|
+
<tr><th>Slot</th><th>Value</th></tr>
|
|
9
|
+
</thead>
|
|
10
|
+
<tbody>
|
|
11
|
+
<% @slots.each do |slot, value| %>
|
|
12
|
+
<tr>
|
|
13
|
+
<td><code><%= slot %></code></td>
|
|
14
|
+
<td><pre><%= value.is_a?(String) ? value : value.inspect %></pre></td>
|
|
15
|
+
</tr>
|
|
16
|
+
<% end %>
|
|
17
|
+
</tbody>
|
|
18
|
+
</table>
|
|
19
|
+
<% end %>
|
|
20
|
+
</div>
|
data/config/routes.rb
CHANGED
|
@@ -9,4 +9,18 @@ Llmemory::Dashboard::Engine.routes.draw do
|
|
|
9
9
|
get "u/:user_id/graph", to: "graph#index", as: :user_graph
|
|
10
10
|
get "u/:user_id/stats", to: "stats#index", as: :user_stats
|
|
11
11
|
get "search", to: "search#index", as: :search
|
|
12
|
+
|
|
13
|
+
# Cognitive memory (CoALA) surface
|
|
14
|
+
get "u/:user_id/episodic", to: "episodic#index", as: :user_episodic
|
|
15
|
+
post "u/:user_id/episodic/forget", to: "episodic#forget", as: :forget_user_episodic
|
|
16
|
+
get "u/:user_id/procedural", to: "procedural#index", as: :user_procedural
|
|
17
|
+
post "u/:user_id/procedural/forget", to: "procedural#forget", as: :forget_user_procedural
|
|
18
|
+
get "u/:user_id/working/:session_id", to: "working#show", as: :user_working
|
|
19
|
+
get "u/:user_id/reflection", to: "reflection#show", as: :user_reflection
|
|
20
|
+
post "u/:user_id/reflection/run", to: "reflection#run", as: :run_user_reflection
|
|
21
|
+
get "u/:user_id/forget_log", to: "forget_log#show", as: :user_forget_log
|
|
22
|
+
get "u/:user_id/maintenance", to: "maintenance#show", as: :user_maintenance
|
|
23
|
+
post "u/:user_id/maintenance/run", to: "maintenance#run", as: :run_user_maintenance
|
|
24
|
+
post "u/:user_id/maintenance/mine", to: "maintenance#mine", as: :mine_user_maintenance
|
|
25
|
+
post "u/:user_id/maintenance/register", to: "maintenance#register", as: :register_user_maintenance
|
|
12
26
|
end
|
|
@@ -30,6 +30,28 @@ class CreateLlmemoryTables < ActiveRecord::Migration[7.0]
|
|
|
30
30
|
end
|
|
31
31
|
add_index :llmemory_categories, [:user_id, :category_name], unique: true
|
|
32
32
|
|
|
33
|
+
# Episodic long-term memory (trajectories) — JSONB document per episode
|
|
34
|
+
create_table :llmemory_episodes, id: false do |t|
|
|
35
|
+
t.string :id, null: false, primary_key: true
|
|
36
|
+
t.string :user_id, null: false
|
|
37
|
+
t.jsonb :data, null: false, default: {}
|
|
38
|
+
t.text :search_text
|
|
39
|
+
t.datetime :archived_at
|
|
40
|
+
t.timestamps
|
|
41
|
+
end
|
|
42
|
+
add_index :llmemory_episodes, :user_id
|
|
43
|
+
|
|
44
|
+
# Procedural long-term memory (skill library) — JSONB document per skill
|
|
45
|
+
create_table :llmemory_skills, id: false do |t|
|
|
46
|
+
t.string :id, null: false, primary_key: true
|
|
47
|
+
t.string :user_id, null: false
|
|
48
|
+
t.jsonb :data, null: false, default: {}
|
|
49
|
+
t.text :search_text
|
|
50
|
+
t.datetime :archived_at
|
|
51
|
+
t.timestamps
|
|
52
|
+
end
|
|
53
|
+
add_index :llmemory_skills, :user_id
|
|
54
|
+
|
|
33
55
|
create_table :llmemory_checkpoints do |t|
|
|
34
56
|
t.string :user_id, null: false
|
|
35
57
|
t.string :session_id, null: false
|
|
@@ -70,6 +70,14 @@ module Llmemory
|
|
|
70
70
|
Llmemory::LongTerm::GraphBased::Storages::MemoryStorage.new
|
|
71
71
|
end
|
|
72
72
|
end
|
|
73
|
+
|
|
74
|
+
def episodic_storage(store_type = nil)
|
|
75
|
+
Llmemory::LongTerm::Episodic::Storages.build(store: (store_type || Llmemory.configuration.long_term_store))
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def procedural_storage(store_type = nil)
|
|
79
|
+
Llmemory::LongTerm::Procedural::Storages.build(store: (store_type || Llmemory.configuration.long_term_store))
|
|
80
|
+
end
|
|
73
81
|
end
|
|
74
82
|
end
|
|
75
83
|
end
|