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.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +48 -1
  3. data/exe/llmemory +8 -0
  4. data/lib/llmemory/cli/commands/base.rb +76 -0
  5. data/lib/llmemory/cli/commands/long_term/categories.rb +34 -0
  6. data/lib/llmemory/cli/commands/long_term/edges.rb +43 -0
  7. data/lib/llmemory/cli/commands/long_term/facts.rb +42 -0
  8. data/lib/llmemory/cli/commands/long_term/graph.rb +76 -0
  9. data/lib/llmemory/cli/commands/long_term/nodes.rb +42 -0
  10. data/lib/llmemory/cli/commands/long_term/resources.rb +42 -0
  11. data/lib/llmemory/cli/commands/long_term.rb +19 -0
  12. data/lib/llmemory/cli/commands/search.rb +86 -0
  13. data/lib/llmemory/cli/commands/short_term.rb +59 -0
  14. data/lib/llmemory/cli/commands/stats.rb +70 -0
  15. data/lib/llmemory/cli/commands/users.rb +27 -0
  16. data/lib/llmemory/cli.rb +76 -0
  17. data/lib/llmemory/dashboard/app/controllers/llmemory/dashboard/application_controller.rb +83 -0
  18. data/lib/llmemory/dashboard/app/controllers/llmemory/dashboard/graph_controller.rb +18 -0
  19. data/lib/llmemory/dashboard/app/controllers/llmemory/dashboard/long_term_controller.rb +31 -0
  20. data/lib/llmemory/dashboard/app/controllers/llmemory/dashboard/search_controller.rb +58 -0
  21. data/lib/llmemory/dashboard/app/controllers/llmemory/dashboard/short_term_controller.rb +15 -0
  22. data/lib/llmemory/dashboard/app/controllers/llmemory/dashboard/stats_controller.rb +21 -0
  23. data/lib/llmemory/dashboard/app/controllers/llmemory/dashboard/users_controller.rb +27 -0
  24. data/lib/llmemory/dashboard/app/views/layouts/application.html.erb +53 -0
  25. data/lib/llmemory/dashboard/app/views/llmemory/dashboard/graph/index.html.erb +41 -0
  26. data/lib/llmemory/dashboard/app/views/llmemory/dashboard/long_term/categories.html.erb +12 -0
  27. data/lib/llmemory/dashboard/app/views/llmemory/dashboard/long_term/index.html.erb +42 -0
  28. data/lib/llmemory/dashboard/app/views/llmemory/dashboard/search/index.html.erb +43 -0
  29. data/lib/llmemory/dashboard/app/views/llmemory/dashboard/short_term/show.html.erb +25 -0
  30. data/lib/llmemory/dashboard/app/views/llmemory/dashboard/stats/index.html.erb +32 -0
  31. data/lib/llmemory/dashboard/app/views/llmemory/dashboard/users/index.html.erb +30 -0
  32. data/lib/llmemory/dashboard/app/views/llmemory/dashboard/users/show.html.erb +14 -0
  33. data/lib/llmemory/dashboard/config/routes.rb +12 -0
  34. data/lib/llmemory/dashboard/engine.rb +9 -0
  35. data/lib/llmemory/dashboard.rb +8 -0
  36. data/lib/llmemory/llm.rb +4 -3
  37. data/lib/llmemory/long_term/file_based/storages/active_record_storage.rb +24 -0
  38. data/lib/llmemory/long_term/file_based/storages/base.rb +16 -0
  39. data/lib/llmemory/long_term/file_based/storages/database_storage.rb +35 -0
  40. data/lib/llmemory/long_term/file_based/storages/file_storage.rb +21 -0
  41. data/lib/llmemory/long_term/file_based/storages/memory_storage.rb +23 -0
  42. data/lib/llmemory/long_term/graph_based/storages/active_record_storage.rb +25 -2
  43. data/lib/llmemory/long_term/graph_based/storages/base.rb +17 -1
  44. data/lib/llmemory/long_term/graph_based/storages/memory_storage.rb +21 -2
  45. data/lib/llmemory/memory.rb +7 -4
  46. data/lib/llmemory/short_term/stores/active_record_store.rb +8 -0
  47. data/lib/llmemory/short_term/stores/base.rb +8 -0
  48. data/lib/llmemory/short_term/stores/memory_store.rb +9 -0
  49. data/lib/llmemory/short_term/stores/postgres_store.rb +15 -0
  50. data/lib/llmemory/short_term/stores/redis_store.rb +10 -0
  51. data/lib/llmemory/version.rb +1 -1
  52. metadata +39 -8
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 84ebe29d1042b2efa07d02ded2a5374bbdcc1b0bad195a90d6cc5a15c1a2f73a
4
- data.tar.gz: 6b6c0157acc13d89a07b9491c206a5a132d4c1e9c3c63a08f1810dd39681a067
3
+ metadata.gz: 252d1921772537eaeba7f1f2b689643202fda6db8f5ddc7aa71c7b2f45636331
4
+ data.tar.gz: c2fc2f9152db916e4448f8a2aa55d8079c68ad40fec1eb0eeaa5564834b1789e
5
5
  SHA512:
6
- metadata.gz: cfd74b6f30e3d049de41596344a15c7b7734794a146a22e9dc2ad61153eb8fa584982efe1a4969bbe24fdbe2a079f46f8f42aeabd1e90e6f78f65d511fbc10df
7
- data.tar.gz: 644dd2c11dbcdb40486d4a1b3cc33e5c13aa0ae0f9616358b4aff069d7b0ac7a04a263bb09a98657e58bcbefd11def65c7c5c711bbf6be3d36645512367439e3
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,8 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ $LOAD_PATH.unshift(File.expand_path("../lib", __dir__)) unless $LOAD_PATH.include?(File.expand_path("../lib", __dir__))
5
+ require "llmemory"
6
+ require "llmemory/cli"
7
+
8
+ Llmemory::CLI.run(ARGV)
@@ -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