llmemory 0.2.1 → 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 +47 -1
- 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 +2 -0
- data/lib/llmemory/cli/commands/maintain.rb +62 -0
- data/lib/llmemory/cli/commands/mine_skills.rb +50 -0
- data/lib/llmemory/cli.rb +6 -0
- data/lib/llmemory/configuration.rb +7 -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 +23 -10
- data/lib/llmemory/long_term/episodic/storages/active_record_storage.rb +14 -4
- data/lib/llmemory/long_term/episodic/storages/base.rb +15 -2
- data/lib/llmemory/long_term/episodic/storages/database_storage.rb +26 -5
- data/lib/llmemory/long_term/episodic/storages/file_storage.rb +27 -6
- data/lib/llmemory/long_term/episodic/storages/memory_storage.rb +35 -4
- data/lib/llmemory/long_term/file_based/memory.rb +12 -4
- data/lib/llmemory/long_term/file_based/storages/active_record_storage.rb +4 -2
- data/lib/llmemory/long_term/file_based/storages/base.rb +2 -2
- data/lib/llmemory/long_term/file_based/storages/database_storage.rb +4 -2
- data/lib/llmemory/long_term/file_based/storages/file_storage.rb +4 -2
- data/lib/llmemory/long_term/file_based/storages/memory_storage.rb +4 -2
- data/lib/llmemory/long_term/graph_based/memory.rb +12 -4
- 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 +26 -13
- data/lib/llmemory/long_term/procedural/skill.rb +6 -2
- data/lib/llmemory/long_term/procedural/storages/active_record_storage.rb +15 -5
- data/lib/llmemory/long_term/procedural/storages/base.rb +14 -1
- data/lib/llmemory/long_term/procedural/storages/database_storage.rb +27 -6
- data/lib/llmemory/long_term/procedural/storages/file_storage.rb +28 -7
- data/lib/llmemory/long_term/procedural/storages/memory_storage.rb +36 -5
- 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 +5 -1
- data/lib/llmemory/mcp/tools/memory_maintain.rb +53 -0
- data/lib/llmemory/mcp/tools/memory_mine_skills.rb +53 -0
- data/lib/llmemory/memory.rb +20 -0
- data/lib/llmemory/memory_module.rb +13 -6
- data/lib/llmemory/reflection/reflector.rb +24 -20
- data/lib/llmemory/retrieval/engine.rb +25 -16
- data/lib/llmemory/skill_mining/miner.rb +163 -0
- data/lib/llmemory/skill_mining.rb +8 -0
- data/lib/llmemory/vector_store/openai_embeddings.rb +11 -7
- data/lib/llmemory/version.rb +1 -1
- data/lib/llmemory.rb +2 -0
- metadata +22 -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
|
@@ -216,7 +216,7 @@ llmemory implements the memory and internal-action concepts from [CoALA — Cogn
|
|
|
216
216
|
| Procedural memory | `Llmemory::LongTerm::Procedural::Memory` |
|
|
217
217
|
| Reasoning action | `Llmemory::Actions::Reason` |
|
|
218
218
|
| Retrieval action | `Retrieval::Engine` (+ feedback, iterative) |
|
|
219
|
-
| Learning action | `memorize` / `record_episode` / `register_skill` / reflection |
|
|
219
|
+
| Learning action | `memorize` / `record_episode` / `register_skill` / reflection / skill mining |
|
|
220
220
|
| Uniform interface | `Llmemory::MemoryModule` (`read`/`write`/`list`/`stats`/`forget`) |
|
|
221
221
|
|
|
222
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).
|
|
@@ -310,6 +310,39 @@ skills.find_skill("revert deploy") # best match (a Skill)
|
|
|
310
310
|
skills.report_outcome(id, success: true) # feeds ranking + adaptive retrieval
|
|
311
311
|
```
|
|
312
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
|
+
|
|
313
346
|
### Uniform interface (MemoryModule)
|
|
314
347
|
|
|
315
348
|
The queryable long-term memories (file, graph, episodic, procedural) share one agent-facing contract, so a framework can treat them polymorphically:
|
|
@@ -489,6 +522,14 @@ llmemory edges USER_ID [--subject NODE_ID] [--limit N]
|
|
|
489
522
|
llmemory graph USER_ID [--format dot|json]
|
|
490
523
|
llmemory search USER_ID "query" [--type short|long|all]
|
|
491
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]
|
|
492
533
|
```
|
|
493
534
|
|
|
494
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).
|
|
@@ -615,6 +656,11 @@ MCP_TOKEN=your-secret-token llmemory mcp serve --http --port 443 \
|
|
|
615
656
|
| `memory_consolidate` | Extract facts from conversation to long-term |
|
|
616
657
|
| `memory_stats` | Get memory statistics for a user |
|
|
617
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 |
|
|
618
664
|
|
|
619
665
|
### Configuration for Claude Code
|
|
620
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
|
|
@@ -36,6 +36,7 @@ class CreateLlmemoryTables < ActiveRecord::Migration[7.0]
|
|
|
36
36
|
t.string :user_id, null: false
|
|
37
37
|
t.jsonb :data, null: false, default: {}
|
|
38
38
|
t.text :search_text
|
|
39
|
+
t.datetime :archived_at
|
|
39
40
|
t.timestamps
|
|
40
41
|
end
|
|
41
42
|
add_index :llmemory_episodes, :user_id
|
|
@@ -46,6 +47,7 @@ class CreateLlmemoryTables < ActiveRecord::Migration[7.0]
|
|
|
46
47
|
t.string :user_id, null: false
|
|
47
48
|
t.jsonb :data, null: false, default: {}
|
|
48
49
|
t.text :search_text
|
|
50
|
+
t.datetime :archived_at
|
|
49
51
|
t.timestamps
|
|
50
52
|
end
|
|
51
53
|
add_index :llmemory_skills, :user_id
|