llmemory 0.1.1 → 0.1.7
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 +48 -1
- data/exe/llmemory +8 -0
- data/lib/llmemory/cli/commands/base.rb +76 -0
- data/lib/llmemory/cli/commands/long_term/categories.rb +34 -0
- data/lib/llmemory/cli/commands/long_term/edges.rb +43 -0
- data/lib/llmemory/cli/commands/long_term/facts.rb +42 -0
- data/lib/llmemory/cli/commands/long_term/graph.rb +76 -0
- data/lib/llmemory/cli/commands/long_term/nodes.rb +42 -0
- data/lib/llmemory/cli/commands/long_term/resources.rb +42 -0
- data/lib/llmemory/cli/commands/long_term.rb +19 -0
- data/lib/llmemory/cli/commands/search.rb +86 -0
- data/lib/llmemory/cli/commands/short_term.rb +59 -0
- data/lib/llmemory/cli/commands/stats.rb +70 -0
- data/lib/llmemory/cli/commands/users.rb +27 -0
- data/lib/llmemory/cli.rb +76 -0
- data/lib/llmemory/dashboard/app/controllers/llmemory/dashboard/application_controller.rb +83 -0
- data/lib/llmemory/dashboard/app/controllers/llmemory/dashboard/graph_controller.rb +18 -0
- data/lib/llmemory/dashboard/app/controllers/llmemory/dashboard/long_term_controller.rb +31 -0
- data/lib/llmemory/dashboard/app/controllers/llmemory/dashboard/search_controller.rb +58 -0
- data/lib/llmemory/dashboard/app/controllers/llmemory/dashboard/short_term_controller.rb +15 -0
- data/lib/llmemory/dashboard/app/controllers/llmemory/dashboard/stats_controller.rb +21 -0
- data/lib/llmemory/dashboard/app/controllers/llmemory/dashboard/users_controller.rb +27 -0
- data/lib/llmemory/dashboard/app/views/layouts/application.html.erb +53 -0
- data/lib/llmemory/dashboard/app/views/llmemory/dashboard/graph/index.html.erb +41 -0
- data/lib/llmemory/dashboard/app/views/llmemory/dashboard/long_term/categories.html.erb +12 -0
- data/lib/llmemory/dashboard/app/views/llmemory/dashboard/long_term/index.html.erb +42 -0
- data/lib/llmemory/dashboard/app/views/llmemory/dashboard/search/index.html.erb +43 -0
- data/lib/llmemory/dashboard/app/views/llmemory/dashboard/short_term/show.html.erb +25 -0
- data/lib/llmemory/dashboard/app/views/llmemory/dashboard/stats/index.html.erb +32 -0
- data/lib/llmemory/dashboard/app/views/llmemory/dashboard/users/index.html.erb +30 -0
- data/lib/llmemory/dashboard/app/views/llmemory/dashboard/users/show.html.erb +14 -0
- data/lib/llmemory/dashboard/config/routes.rb +12 -0
- data/lib/llmemory/dashboard/engine.rb +9 -0
- data/lib/llmemory/dashboard.rb +8 -0
- data/lib/llmemory/llm.rb +4 -3
- data/lib/llmemory/long_term/file_based/storages/active_record_storage.rb +24 -0
- data/lib/llmemory/long_term/file_based/storages/base.rb +16 -0
- data/lib/llmemory/long_term/file_based/storages/database_storage.rb +35 -0
- data/lib/llmemory/long_term/file_based/storages/file_storage.rb +21 -0
- data/lib/llmemory/long_term/file_based/storages/memory_storage.rb +23 -0
- data/lib/llmemory/long_term/graph_based/storages/active_record_storage.rb +25 -2
- data/lib/llmemory/long_term/graph_based/storages/base.rb +17 -1
- data/lib/llmemory/long_term/graph_based/storages/memory_storage.rb +21 -2
- data/lib/llmemory/memory.rb +7 -4
- data/lib/llmemory/short_term/stores/active_record_store.rb +8 -0
- data/lib/llmemory/short_term/stores/base.rb +8 -0
- data/lib/llmemory/short_term/stores/memory_store.rb +9 -0
- data/lib/llmemory/short_term/stores/postgres_store.rb +15 -0
- data/lib/llmemory/short_term/stores/redis_store.rb +10 -0
- data/lib/llmemory/version.rb +1 -1
- metadata +39 -8
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 252d1921772537eaeba7f1f2b689643202fda6db8f5ddc7aa71c7b2f45636331
|
|
4
|
+
data.tar.gz: c2fc2f9152db916e4448f8a2aa55d8079c68ad40fec1eb0eeaa5564834b1789e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 92256d0f2a37bd2047e78c3651eaa60e8c46dc5bfa176e5e99800a28eeb90cf8b0f791248b5bdd85ff33828e4599acd80d2443698449cdf54017fde23b0a1685
|
|
7
|
+
data.tar.gz: 3aa07c74614b8975165ed6a073210d0c58c194969043c87080cf516668667d04b71b306212b09e2994d9769e746e2e9d1743ddf6b79ed37b39c54968cbfc06ff
|
data/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# llmemory
|
|
2
2
|
|
|
3
|
-
Persistent memory system for LLM agents. Implements short-term checkpointing, long-term memory (file-based or **graph-based**), retrieval with time decay, and maintenance jobs.
|
|
3
|
+
Persistent memory system for LLM agents. Implements short-term checkpointing, long-term memory (file-based or **graph-based**), retrieval with time decay, and maintenance jobs. You can inspect memory from the **CLI** or, in Rails apps, from an optional **dashboard**.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -188,6 +188,53 @@ Llmemory::Maintenance::Runner.run_weekly(user_id, storage: memory.storage)
|
|
|
188
188
|
Llmemory::Maintenance::Runner.run_monthly(user_id, storage: memory.storage)
|
|
189
189
|
```
|
|
190
190
|
|
|
191
|
+
## Inspecting memory
|
|
192
|
+
|
|
193
|
+
### CLI
|
|
194
|
+
|
|
195
|
+
The gem ships an executable to inspect memory from the terminal (no extra dependencies; uses Ruby’s OptParse):
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
llmemory --help
|
|
199
|
+
llmemory users
|
|
200
|
+
llmemory short-term USER_ID [--session SESSION_ID] [--list-sessions]
|
|
201
|
+
llmemory facts USER_ID [--category CATEGORY] [--limit N]
|
|
202
|
+
llmemory categories USER_ID
|
|
203
|
+
llmemory resources USER_ID [--limit N]
|
|
204
|
+
llmemory nodes USER_ID [--type TYPE] [--limit N] # graph-based
|
|
205
|
+
llmemory edges USER_ID [--subject NODE_ID] [--limit N]
|
|
206
|
+
llmemory graph USER_ID [--format dot|json]
|
|
207
|
+
llmemory search USER_ID "query" [--type short|long|all]
|
|
208
|
+
llmemory stats [USER_ID]
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
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).
|
|
212
|
+
|
|
213
|
+
### Dashboard (Rails, optional)
|
|
214
|
+
|
|
215
|
+
If you use Rails and want a web UI to browse memory, load the dashboard and mount the engine. **Rails is not a dependency of the gem**; the dashboard is only loaded when you require it.
|
|
216
|
+
|
|
217
|
+
1. In an initializer or early in boot (e.g. `config/initializers/llmemory.rb`):
|
|
218
|
+
|
|
219
|
+
```ruby
|
|
220
|
+
require "llmemory/dashboard"
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
2. In `config/routes.rb`:
|
|
224
|
+
|
|
225
|
+
```ruby
|
|
226
|
+
mount Llmemory::Dashboard::Engine, at: "/llmemory"
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
3. Visit `/llmemory`. You get:
|
|
230
|
+
- List of users with memory
|
|
231
|
+
- Short-term: conversation messages per session
|
|
232
|
+
- Long-term (file-based): resources, items by category, category summaries
|
|
233
|
+
- Long-term (graph-based): nodes and edges
|
|
234
|
+
- Search and stats
|
|
235
|
+
|
|
236
|
+
The dashboard uses your existing `Llmemory.configuration` (short-term store, long-term store/type, etc.) and does not add any gem dependency; it only runs when Rails is present and you require `llmemory/dashboard`.
|
|
237
|
+
|
|
191
238
|
## License
|
|
192
239
|
|
|
193
240
|
MIT. See [LICENSE.txt](LICENSE.txt).
|
data/exe/llmemory
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Llmemory
|
|
4
|
+
module Cli
|
|
5
|
+
module Commands
|
|
6
|
+
class Base
|
|
7
|
+
def run(argv)
|
|
8
|
+
opts = parse_options(argv)
|
|
9
|
+
execute(argv, opts)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def parse_options(argv)
|
|
13
|
+
OptionParser.new do |opts|
|
|
14
|
+
option_parser(opts)
|
|
15
|
+
end.parse!(argv)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def option_parser(parser)
|
|
19
|
+
# Override in subclasses to add options
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def execute(_argv, _opts)
|
|
23
|
+
raise NotImplementedError, "#{self.class}#execute must be implemented"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
protected
|
|
27
|
+
|
|
28
|
+
def short_term_store(store_type = nil)
|
|
29
|
+
type = (store_type || Llmemory.configuration.short_term_store).to_s.to_sym
|
|
30
|
+
case type
|
|
31
|
+
when :memory then Llmemory::ShortTerm::Stores::MemoryStore.new
|
|
32
|
+
when :redis then Llmemory::ShortTerm::Stores::RedisStore.new
|
|
33
|
+
when :postgres then Llmemory::ShortTerm::Stores::PostgresStore.new
|
|
34
|
+
when :active_record, :activerecord
|
|
35
|
+
require_relative "../../short_term/stores/active_record_store"
|
|
36
|
+
Llmemory::ShortTerm::Stores::ActiveRecordStore.new
|
|
37
|
+
else
|
|
38
|
+
Llmemory::ShortTerm::Stores::MemoryStore.new
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def file_based_storage(store_type = nil)
|
|
43
|
+
type = (store_type || Llmemory.configuration.long_term_store).to_s.to_sym
|
|
44
|
+
case type
|
|
45
|
+
when :memory then Llmemory::LongTerm::FileBased::Storages::MemoryStorage.new
|
|
46
|
+
when :file
|
|
47
|
+
Llmemory::LongTerm::FileBased::Storages::FileStorage.new(
|
|
48
|
+
base_path: Llmemory.configuration.long_term_storage_path
|
|
49
|
+
)
|
|
50
|
+
when :postgres, :database
|
|
51
|
+
Llmemory::LongTerm::FileBased::Storages::DatabaseStorage.new(
|
|
52
|
+
database_url: Llmemory.configuration.database_url
|
|
53
|
+
)
|
|
54
|
+
when :active_record, :activerecord
|
|
55
|
+
require_relative "../../long_term/file_based/storages/active_record_storage"
|
|
56
|
+
Llmemory::LongTerm::FileBased::Storages::ActiveRecordStorage.new
|
|
57
|
+
else
|
|
58
|
+
Llmemory::LongTerm::FileBased::Storages::MemoryStorage.new
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def graph_based_storage(store_type = nil)
|
|
63
|
+
type = (store_type || :memory).to_s.to_sym
|
|
64
|
+
case type
|
|
65
|
+
when :memory then Llmemory::LongTerm::GraphBased::Storages::MemoryStorage.new
|
|
66
|
+
when :active_record, :activerecord
|
|
67
|
+
require_relative "../../long_term/graph_based/storages/active_record_storage"
|
|
68
|
+
Llmemory::LongTerm::GraphBased::Storages::ActiveRecordStorage.new
|
|
69
|
+
else
|
|
70
|
+
Llmemory::LongTerm::GraphBased::Storages::MemoryStorage.new
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../base"
|
|
4
|
+
|
|
5
|
+
module Llmemory
|
|
6
|
+
module Cli
|
|
7
|
+
module Commands
|
|
8
|
+
module LongTerm
|
|
9
|
+
class Categories < Commands::Base
|
|
10
|
+
def option_parser(parser)
|
|
11
|
+
parser.on("--store TYPE", "Storage type") { |v| @store_type = v }
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def execute(argv, _opts)
|
|
15
|
+
user_id = argv.first
|
|
16
|
+
unless user_id
|
|
17
|
+
$stderr.puts "Usage: llmemory categories USER_ID"
|
|
18
|
+
exit 1
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
storage = file_based_storage(@store_type)
|
|
22
|
+
categories = storage.list_categories(user_id)
|
|
23
|
+
|
|
24
|
+
if categories.empty?
|
|
25
|
+
puts "No categories found for user #{user_id}."
|
|
26
|
+
else
|
|
27
|
+
categories.each { |c| puts c }
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../base"
|
|
4
|
+
|
|
5
|
+
module Llmemory
|
|
6
|
+
module Cli
|
|
7
|
+
module Commands
|
|
8
|
+
module LongTerm
|
|
9
|
+
class Edges < Commands::Base
|
|
10
|
+
def option_parser(parser)
|
|
11
|
+
parser.on("--subject NODE_ID", "Filter by subject node") { |v| @subject_id = v }
|
|
12
|
+
parser.on("--limit N", Integer, "Max number of edges") { |v| @limit = v }
|
|
13
|
+
parser.on("--store TYPE", "Storage type") { |v| @store_type = v }
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def execute(argv, _opts)
|
|
17
|
+
user_id = argv.first
|
|
18
|
+
unless user_id
|
|
19
|
+
$stderr.puts "Usage: llmemory edges USER_ID [--subject NODE_ID] [--limit N]"
|
|
20
|
+
exit 1
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
storage = graph_based_storage(@store_type)
|
|
24
|
+
edges = storage.list_edges(user_id, subject_id: @subject_id, limit: @limit)
|
|
25
|
+
|
|
26
|
+
if edges.empty?
|
|
27
|
+
puts "No edges found for user #{user_id}."
|
|
28
|
+
return
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
edges.each do |e|
|
|
32
|
+
id = e.respond_to?(:id) ? e.id : e[:id]
|
|
33
|
+
subj = e.respond_to?(:subject_id) ? e.subject_id : e[:subject_id]
|
|
34
|
+
pred = e.respond_to?(:predicate) ? e.predicate : e[:predicate]
|
|
35
|
+
obj = e.respond_to?(:object_id) ? e.object_id : e[:object_id]
|
|
36
|
+
puts "#{id}: #{subj} --#{pred}--> #{obj}"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../base"
|
|
4
|
+
|
|
5
|
+
module Llmemory
|
|
6
|
+
module Cli
|
|
7
|
+
module Commands
|
|
8
|
+
module LongTerm
|
|
9
|
+
class Facts < Commands::Base
|
|
10
|
+
def option_parser(parser)
|
|
11
|
+
parser.on("--category CATEGORY", "Filter by category") { |v| @category = v }
|
|
12
|
+
parser.on("--limit N", Integer, "Max number of items") { |v| @limit = v }
|
|
13
|
+
parser.on("--store TYPE", "Storage type") { |v| @store_type = v }
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def execute(argv, _opts)
|
|
17
|
+
user_id = argv.first
|
|
18
|
+
unless user_id
|
|
19
|
+
$stderr.puts "Usage: llmemory facts USER_ID [--category CATEGORY] [--limit N]"
|
|
20
|
+
exit 1
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
storage = file_based_storage(@store_type)
|
|
24
|
+
items = storage.list_items(user_id: user_id, category: @category, limit: @limit)
|
|
25
|
+
|
|
26
|
+
if items.empty?
|
|
27
|
+
puts "No facts found for user #{user_id}."
|
|
28
|
+
return
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
items.each do |i|
|
|
32
|
+
cat = i[:category] || i["category"]
|
|
33
|
+
content = (i[:content] || i["content"]).to_s
|
|
34
|
+
created = i[:created_at] || i["created_at"]
|
|
35
|
+
puts "[#{cat}] #{content} (#{created})"
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require_relative "../base"
|
|
5
|
+
|
|
6
|
+
module Llmemory
|
|
7
|
+
module Cli
|
|
8
|
+
module Commands
|
|
9
|
+
module LongTerm
|
|
10
|
+
class Graph < Commands::Base
|
|
11
|
+
def option_parser(parser)
|
|
12
|
+
parser.on("--format FORMAT", "Output format: dot, json") { |v| @format = (v || "dot").downcase }
|
|
13
|
+
parser.on("--store TYPE", "Storage type") { |v| @store_type = v }
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def execute(argv, _opts)
|
|
17
|
+
user_id = argv.first
|
|
18
|
+
unless user_id
|
|
19
|
+
$stderr.puts "Usage: llmemory graph USER_ID [--format dot|json]"
|
|
20
|
+
exit 1
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
storage = graph_based_storage(@store_type)
|
|
24
|
+
nodes = storage.list_nodes(user_id)
|
|
25
|
+
edges = storage.list_edges(user_id)
|
|
26
|
+
|
|
27
|
+
case @format
|
|
28
|
+
when "json"
|
|
29
|
+
puts JSON.pretty_generate(
|
|
30
|
+
nodes: nodes.map { |n| node_to_h(n) },
|
|
31
|
+
edges: edges.map { |e| edge_to_h(e) }
|
|
32
|
+
)
|
|
33
|
+
else
|
|
34
|
+
puts to_dot(nodes, edges)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def node_to_h(n)
|
|
41
|
+
if n.respond_to?(:to_h)
|
|
42
|
+
n.to_h
|
|
43
|
+
else
|
|
44
|
+
{ id: n[:id], entity_type: n[:entity_type], name: n[:name] }
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def edge_to_h(e)
|
|
49
|
+
if e.respond_to?(:to_h)
|
|
50
|
+
e.to_h
|
|
51
|
+
else
|
|
52
|
+
{ id: e[:id], subject_id: e[:subject_id], predicate: e[:predicate], object_id: e[:object_id] }
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def to_dot(nodes, edges)
|
|
57
|
+
lines = ["digraph llmemory {"]
|
|
58
|
+
nodes.each do |n|
|
|
59
|
+
id = n.respond_to?(:id) ? n.id : n[:id]
|
|
60
|
+
name = (n.respond_to?(:name) ? n.name : n[:name]).to_s.gsub('"', '\\"')
|
|
61
|
+
lines << " \"#{id}\" [label=\"#{name}\"];"
|
|
62
|
+
end
|
|
63
|
+
edges.each do |e|
|
|
64
|
+
subj = e.respond_to?(:subject_id) ? e.subject_id : e[:subject_id]
|
|
65
|
+
obj = e.respond_to?(:object_id) ? e.object_id : e[:object_id]
|
|
66
|
+
pred = e.respond_to?(:predicate) ? e.predicate : e[:predicate]
|
|
67
|
+
lines << " \"#{subj}\" -> \"#{obj}\" [label=\"#{pred}\"];"
|
|
68
|
+
end
|
|
69
|
+
lines << "}"
|
|
70
|
+
lines.join("\n")
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../base"
|
|
4
|
+
|
|
5
|
+
module Llmemory
|
|
6
|
+
module Cli
|
|
7
|
+
module Commands
|
|
8
|
+
module LongTerm
|
|
9
|
+
class Nodes < Commands::Base
|
|
10
|
+
def option_parser(parser)
|
|
11
|
+
parser.on("--type TYPE", "Filter by entity type") { |v| @entity_type = v }
|
|
12
|
+
parser.on("--limit N", Integer, "Max number of nodes") { |v| @limit = v }
|
|
13
|
+
parser.on("--store TYPE", "Storage type (memory, active_record)") { |v| @store_type = v }
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def execute(argv, _opts)
|
|
17
|
+
user_id = argv.first
|
|
18
|
+
unless user_id
|
|
19
|
+
$stderr.puts "Usage: llmemory nodes USER_ID [--type TYPE] [--limit N]"
|
|
20
|
+
exit 1
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
storage = graph_based_storage(@store_type)
|
|
24
|
+
nodes = storage.list_nodes(user_id, entity_type: @entity_type, limit: @limit)
|
|
25
|
+
|
|
26
|
+
if nodes.empty?
|
|
27
|
+
puts "No nodes found for user #{user_id}."
|
|
28
|
+
return
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
nodes.each do |n|
|
|
32
|
+
id = n.respond_to?(:id) ? n.id : n[:id]
|
|
33
|
+
type = n.respond_to?(:entity_type) ? n.entity_type : n[:entity_type]
|
|
34
|
+
name = n.respond_to?(:name) ? n.name : n[:name]
|
|
35
|
+
puts "#{id} [#{type}] #{name}"
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../base"
|
|
4
|
+
|
|
5
|
+
module Llmemory
|
|
6
|
+
module Cli
|
|
7
|
+
module Commands
|
|
8
|
+
module LongTerm
|
|
9
|
+
class Resources < Commands::Base
|
|
10
|
+
def option_parser(parser)
|
|
11
|
+
parser.on("--limit N", Integer, "Max number of resources") { |v| @limit = v }
|
|
12
|
+
parser.on("--store TYPE", "Storage type") { |v| @store_type = v }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def execute(argv, _opts)
|
|
16
|
+
user_id = argv.first
|
|
17
|
+
unless user_id
|
|
18
|
+
$stderr.puts "Usage: llmemory resources USER_ID [--limit N]"
|
|
19
|
+
exit 1
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
storage = file_based_storage(@store_type)
|
|
23
|
+
resources = storage.list_resources(user_id: user_id, limit: @limit)
|
|
24
|
+
|
|
25
|
+
if resources.empty?
|
|
26
|
+
puts "No resources found for user #{user_id}."
|
|
27
|
+
return
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
resources.each do |r|
|
|
31
|
+
id = r[:id] || r["id"]
|
|
32
|
+
text = (r[:text] || r["text"]).to_s
|
|
33
|
+
text = text[0, 150] + "..." if text.length > 150
|
|
34
|
+
created = r[:created_at] || r["created_at"]
|
|
35
|
+
puts "#{id}: #{text} (#{created})"
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
require_relative "long_term/facts"
|
|
5
|
+
require_relative "long_term/categories"
|
|
6
|
+
require_relative "long_term/resources"
|
|
7
|
+
require_relative "long_term/nodes"
|
|
8
|
+
require_relative "long_term/edges"
|
|
9
|
+
require_relative "long_term/graph"
|
|
10
|
+
|
|
11
|
+
module Llmemory
|
|
12
|
+
module Cli
|
|
13
|
+
module Commands
|
|
14
|
+
module LongTerm
|
|
15
|
+
# Namespace for long-term inspection commands
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
module Llmemory
|
|
6
|
+
module Cli
|
|
7
|
+
module Commands
|
|
8
|
+
class Search < Base
|
|
9
|
+
def option_parser(parser)
|
|
10
|
+
parser.on("--type TYPE", "Search in: short, long, all (default: all)") { |v| @search_type = (v || "all").downcase }
|
|
11
|
+
parser.on("--store TYPE", "Store type") { |v| @store_type = v }
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def execute(argv, _opts)
|
|
15
|
+
user_id = argv.shift
|
|
16
|
+
query = argv.join(" ").strip
|
|
17
|
+
unless user_id && !query.empty?
|
|
18
|
+
$stderr.puts "Usage: llmemory search USER_ID \"query\" [--type short|long|all]"
|
|
19
|
+
exit 1
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
type = @search_type || "all"
|
|
23
|
+
|
|
24
|
+
if type == "short" || type == "all"
|
|
25
|
+
search_short_term(user_id, query)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
if type == "long" || type == "all"
|
|
29
|
+
if Llmemory.configuration.long_term_type.to_s == "graph_based"
|
|
30
|
+
search_graph_based(user_id, query)
|
|
31
|
+
else
|
|
32
|
+
search_file_based(user_id, query)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def search_short_term(user_id, query)
|
|
40
|
+
store = short_term_store(@store_type)
|
|
41
|
+
sessions = store.list_sessions(user_id: user_id)
|
|
42
|
+
puts "=== Short-term ==="
|
|
43
|
+
sessions.each do |session_id|
|
|
44
|
+
state = store.load(user_id, session_id)
|
|
45
|
+
next unless state
|
|
46
|
+
messages = state[:messages] || state["messages"] || []
|
|
47
|
+
messages.each do |m|
|
|
48
|
+
content = (m[:content] || m["content"]).to_s
|
|
49
|
+
next unless content.downcase.include?(query.downcase)
|
|
50
|
+
role = m[:role] || m["role"]
|
|
51
|
+
puts "[#{session_id}] [#{role}] #{content[0, 150]}..."
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def search_file_based(user_id, query)
|
|
57
|
+
storage = file_based_storage(@store_type)
|
|
58
|
+
puts "=== Long-term (file-based) ==="
|
|
59
|
+
items = storage.search_items(user_id, query)
|
|
60
|
+
items.each do |i|
|
|
61
|
+
content = (i[:content] || i["content"]).to_s
|
|
62
|
+
puts "[#{i[:category]}] #{content}"
|
|
63
|
+
end
|
|
64
|
+
resources = storage.search_resources(user_id, query)
|
|
65
|
+
resources.each do |r|
|
|
66
|
+
text = (r[:text] || r["text"]).to_s
|
|
67
|
+
puts "[resource] #{text[0, 150]}..."
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def search_graph_based(user_id, query)
|
|
72
|
+
storage = graph_based_storage(@store_type)
|
|
73
|
+
puts "=== Long-term (graph-based) ==="
|
|
74
|
+
nodes = storage.list_nodes(user_id)
|
|
75
|
+
query_lower = query.downcase
|
|
76
|
+
nodes.each do |n|
|
|
77
|
+
name = (n.respond_to?(:name) ? n.name : n[:name]).to_s
|
|
78
|
+
next unless name.downcase.include?(query_lower)
|
|
79
|
+
type = n.respond_to?(:entity_type) ? n.entity_type : n[:entity_type]
|
|
80
|
+
puts "[node] #{type}: #{name}"
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
module Llmemory
|
|
6
|
+
module Cli
|
|
7
|
+
module Commands
|
|
8
|
+
class ShortTerm < Base
|
|
9
|
+
DEFAULT_SESSION = "default"
|
|
10
|
+
|
|
11
|
+
def option_parser(parser)
|
|
12
|
+
parser.on("--session SESSION_ID", "Session ID (default: default)") { |v| @session_id = v }
|
|
13
|
+
parser.on("--list-sessions", "List sessions for the user") { @list_sessions = true }
|
|
14
|
+
parser.on("--store TYPE", "Store type") { |v| @store_type = v }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def execute(argv, _opts)
|
|
18
|
+
user_id = argv.first
|
|
19
|
+
unless user_id
|
|
20
|
+
$stderr.puts "Usage: llmemory short-term USER_ID [--session SESSION_ID] [--list-sessions]"
|
|
21
|
+
exit 1
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
store = short_term_store(@store_type)
|
|
25
|
+
|
|
26
|
+
if @list_sessions
|
|
27
|
+
sessions = store.list_sessions(user_id: user_id)
|
|
28
|
+
if sessions.empty?
|
|
29
|
+
puts "No sessions found for user #{user_id}."
|
|
30
|
+
else
|
|
31
|
+
sessions.each { |s| puts s }
|
|
32
|
+
end
|
|
33
|
+
return
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
session_id = @session_id || DEFAULT_SESSION
|
|
37
|
+
state = store.load(user_id, session_id)
|
|
38
|
+
if state.nil?
|
|
39
|
+
puts "No state found for user #{user_id}, session #{session_id}."
|
|
40
|
+
return
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
messages = state[:messages] || state["messages"] || []
|
|
44
|
+
if messages.empty?
|
|
45
|
+
puts "No messages in session #{session_id}."
|
|
46
|
+
return
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
messages.each do |m|
|
|
50
|
+
role = m[:role] || m["role"]
|
|
51
|
+
content = (m[:content] || m["content"]).to_s
|
|
52
|
+
content = content[0, 200] + "..." if content.length > 200
|
|
53
|
+
puts "[#{role}] #{content}"
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
module Llmemory
|
|
6
|
+
module Cli
|
|
7
|
+
module Commands
|
|
8
|
+
class Stats < Base
|
|
9
|
+
def option_parser(parser)
|
|
10
|
+
parser.on("--store TYPE", "Store type") { |v| @store_type = v }
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def execute(argv, _opts)
|
|
14
|
+
user_id = argv.first
|
|
15
|
+
short_store = short_term_store(@store_type)
|
|
16
|
+
long_type = Llmemory.configuration.long_term_type.to_s
|
|
17
|
+
|
|
18
|
+
if user_id
|
|
19
|
+
print_user_stats(user_id, short_store, long_type)
|
|
20
|
+
else
|
|
21
|
+
print_global_stats(short_store, long_type)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def print_user_stats(user_id, short_store, long_type)
|
|
28
|
+
puts "Stats for user: #{user_id}"
|
|
29
|
+
puts "---"
|
|
30
|
+
|
|
31
|
+
sessions = short_store.list_sessions(user_id: user_id)
|
|
32
|
+
puts "Short-term sessions: #{sessions.size}"
|
|
33
|
+
|
|
34
|
+
if long_type == "graph_based"
|
|
35
|
+
storage = graph_based_storage(@store_type)
|
|
36
|
+
puts "Long-term (graph) nodes: #{storage.count_nodes(user_id)}"
|
|
37
|
+
puts "Long-term (graph) edges: #{storage.count_edges(user_id)}"
|
|
38
|
+
else
|
|
39
|
+
storage = file_based_storage(@store_type)
|
|
40
|
+
puts "Long-term (file) items: #{storage.count_items(user_id: user_id)}"
|
|
41
|
+
puts "Long-term (file) categories: #{storage.list_categories(user_id).size}"
|
|
42
|
+
puts "Long-term (file) resources: #{storage.list_resources(user_id: user_id).size}"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def print_global_stats(short_store, long_type)
|
|
47
|
+
users = short_store.list_users
|
|
48
|
+
puts "Total users (short-term): #{users.size}"
|
|
49
|
+
puts "---"
|
|
50
|
+
|
|
51
|
+
if long_type == "graph_based"
|
|
52
|
+
storage = graph_based_storage(@store_type)
|
|
53
|
+
long_users = storage.list_users
|
|
54
|
+
puts "Total users (long-term graph): #{long_users.size}"
|
|
55
|
+
total_nodes = long_users.sum { |u| storage.count_nodes(u) }
|
|
56
|
+
total_edges = long_users.sum { |u| storage.count_edges(u) }
|
|
57
|
+
puts "Total nodes: #{total_nodes}"
|
|
58
|
+
puts "Total edges: #{total_edges}"
|
|
59
|
+
else
|
|
60
|
+
storage = file_based_storage(@store_type)
|
|
61
|
+
long_users = storage.list_users
|
|
62
|
+
puts "Total users (long-term file): #{long_users.size}"
|
|
63
|
+
total_items = long_users.sum { |u| storage.count_items(user_id: u) }
|
|
64
|
+
puts "Total items: #{total_items}"
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|