llmemory 0.1.1
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 +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +193 -0
- data/lib/generators/llmemory/install/install_generator.rb +24 -0
- data/lib/generators/llmemory/install/templates/create_llmemory_tables.rb +73 -0
- data/lib/llmemory/configuration.rb +51 -0
- data/lib/llmemory/extractors/entity_relation_extractor.rb +74 -0
- data/lib/llmemory/extractors/fact_extractor.rb +74 -0
- data/lib/llmemory/extractors.rb +9 -0
- data/lib/llmemory/llm/anthropic.rb +48 -0
- data/lib/llmemory/llm/base.rb +17 -0
- data/lib/llmemory/llm/openai.rb +46 -0
- data/lib/llmemory/llm.rb +18 -0
- data/lib/llmemory/long_term/file_based/category.rb +22 -0
- data/lib/llmemory/long_term/file_based/item.rb +31 -0
- data/lib/llmemory/long_term/file_based/memory.rb +83 -0
- data/lib/llmemory/long_term/file_based/resource.rb +22 -0
- data/lib/llmemory/long_term/file_based/retrieval.rb +90 -0
- data/lib/llmemory/long_term/file_based/storage.rb +35 -0
- data/lib/llmemory/long_term/file_based/storages/active_record_models.rb +26 -0
- data/lib/llmemory/long_term/file_based/storages/active_record_storage.rb +144 -0
- data/lib/llmemory/long_term/file_based/storages/base.rb +71 -0
- data/lib/llmemory/long_term/file_based/storages/database_storage.rb +231 -0
- data/lib/llmemory/long_term/file_based/storages/file_storage.rb +180 -0
- data/lib/llmemory/long_term/file_based/storages/memory_storage.rb +100 -0
- data/lib/llmemory/long_term/file_based.rb +15 -0
- data/lib/llmemory/long_term/graph_based/conflict_resolver.rb +33 -0
- data/lib/llmemory/long_term/graph_based/edge.rb +49 -0
- data/lib/llmemory/long_term/graph_based/knowledge_graph.rb +114 -0
- data/lib/llmemory/long_term/graph_based/memory.rb +143 -0
- data/lib/llmemory/long_term/graph_based/node.rb +42 -0
- data/lib/llmemory/long_term/graph_based/storage.rb +24 -0
- data/lib/llmemory/long_term/graph_based/storages/active_record_models.rb +23 -0
- data/lib/llmemory/long_term/graph_based/storages/active_record_storage.rb +132 -0
- data/lib/llmemory/long_term/graph_based/storages/base.rb +39 -0
- data/lib/llmemory/long_term/graph_based/storages/memory_storage.rb +106 -0
- data/lib/llmemory/long_term/graph_based.rb +15 -0
- data/lib/llmemory/long_term.rb +9 -0
- data/lib/llmemory/maintenance/consolidator.rb +55 -0
- data/lib/llmemory/maintenance/reindexer.rb +27 -0
- data/lib/llmemory/maintenance/runner.rb +34 -0
- data/lib/llmemory/maintenance/summarizer.rb +57 -0
- data/lib/llmemory/maintenance.rb +8 -0
- data/lib/llmemory/memory.rb +96 -0
- data/lib/llmemory/retrieval/context_assembler.rb +53 -0
- data/lib/llmemory/retrieval/engine.rb +74 -0
- data/lib/llmemory/retrieval/temporal_ranker.rb +23 -0
- data/lib/llmemory/retrieval.rb +10 -0
- data/lib/llmemory/short_term/checkpoint.rb +47 -0
- data/lib/llmemory/short_term/stores/active_record_checkpoint.rb +14 -0
- data/lib/llmemory/short_term/stores/active_record_store.rb +58 -0
- data/lib/llmemory/short_term/stores/base.rb +21 -0
- data/lib/llmemory/short_term/stores/memory_store.rb +37 -0
- data/lib/llmemory/short_term/stores/postgres_store.rb +80 -0
- data/lib/llmemory/short_term/stores/redis_store.rb +54 -0
- data/lib/llmemory/short_term.rb +8 -0
- data/lib/llmemory/vector_store/base.rb +19 -0
- data/lib/llmemory/vector_store/memory_store.rb +53 -0
- data/lib/llmemory/vector_store/openai_embeddings.rb +49 -0
- data/lib/llmemory/vector_store.rb +10 -0
- data/lib/llmemory/version.rb +5 -0
- data/lib/llmemory.rb +19 -0
- metadata +163 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Llmemory
|
|
4
|
+
module LongTerm
|
|
5
|
+
module FileBased
|
|
6
|
+
class Category
|
|
7
|
+
attr_reader :user_id, :name, :content, :updated_at
|
|
8
|
+
|
|
9
|
+
def initialize(user_id:, name:, content: "", updated_at: nil)
|
|
10
|
+
@user_id = user_id
|
|
11
|
+
@name = name
|
|
12
|
+
@content = content
|
|
13
|
+
@updated_at = updated_at || Time.now
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def to_h
|
|
17
|
+
{ user_id: user_id, name: name, content: content, updated_at: updated_at.iso8601 }
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Llmemory
|
|
4
|
+
module LongTerm
|
|
5
|
+
module FileBased
|
|
6
|
+
class Item
|
|
7
|
+
attr_reader :id, :user_id, :category, :content, :source_resource_id, :created_at
|
|
8
|
+
|
|
9
|
+
def initialize(id:, user_id:, category:, content:, source_resource_id: nil, created_at: nil)
|
|
10
|
+
@id = id
|
|
11
|
+
@user_id = user_id
|
|
12
|
+
@category = category
|
|
13
|
+
@content = content
|
|
14
|
+
@source_resource_id = source_resource_id
|
|
15
|
+
@created_at = created_at || Time.now
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def to_h
|
|
19
|
+
{
|
|
20
|
+
id: id,
|
|
21
|
+
user_id: user_id,
|
|
22
|
+
category: category,
|
|
23
|
+
content: content,
|
|
24
|
+
source_resource_id: source_resource_id,
|
|
25
|
+
created_at: created_at.iso8601
|
|
26
|
+
}
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "resource"
|
|
4
|
+
require_relative "item"
|
|
5
|
+
require_relative "category"
|
|
6
|
+
require_relative "storage"
|
|
7
|
+
|
|
8
|
+
module Llmemory
|
|
9
|
+
module LongTerm
|
|
10
|
+
module FileBased
|
|
11
|
+
class Memory
|
|
12
|
+
def initialize(user_id:, storage: nil, llm: nil, extractor: nil)
|
|
13
|
+
@user_id = user_id
|
|
14
|
+
@storage = storage || Storages.build
|
|
15
|
+
@llm = llm || Llmemory::LLM.client
|
|
16
|
+
@extractor = extractor || Llmemory::Extractors::FactExtractor.new(llm: @llm)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def memorize(conversation_text)
|
|
20
|
+
resource_id = save_resource(conversation_text)
|
|
21
|
+
items = @extractor.extract_items(conversation_text)
|
|
22
|
+
updates_by_category = {}
|
|
23
|
+
|
|
24
|
+
items.each do |item|
|
|
25
|
+
content = item.is_a?(Hash) ? (item["content"] || item[:content]) : item.to_s
|
|
26
|
+
cat = @extractor.classify_item(content)
|
|
27
|
+
updates_by_category[cat] ||= []
|
|
28
|
+
updates_by_category[cat] << content.to_s
|
|
29
|
+
save_item(category: cat, item: item, source_resource_id: resource_id)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
updates_by_category.each do |category, new_memories|
|
|
33
|
+
existing_summary = @storage.load_category(@user_id, category)
|
|
34
|
+
updated_summary = @extractor.evolve_summary(existing: existing_summary, new_memories: new_memories)
|
|
35
|
+
@storage.save_category(@user_id, category, updated_summary)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
true
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def retrieve(query)
|
|
42
|
+
retrieval = Retrieval.new(user_id: @user_id, storage: @storage, llm: @llm)
|
|
43
|
+
retrieval.retrieve(query)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def search_candidates(query, user_id: nil, top_k: 20)
|
|
47
|
+
uid = user_id || @user_id
|
|
48
|
+
items = @storage.search_items(uid, query)
|
|
49
|
+
resources = @storage.search_resources(uid, query)
|
|
50
|
+
out = []
|
|
51
|
+
items.first(top_k).each do |i|
|
|
52
|
+
out << {
|
|
53
|
+
text: i[:content] || i["content"],
|
|
54
|
+
timestamp: i[:created_at] || i["created_at"],
|
|
55
|
+
score: 1.0
|
|
56
|
+
}
|
|
57
|
+
end
|
|
58
|
+
resources.first([top_k - out.size, 0].max).each do |r|
|
|
59
|
+
out << {
|
|
60
|
+
text: r[:text] || r["text"],
|
|
61
|
+
timestamp: r[:created_at] || r["created_at"],
|
|
62
|
+
score: 0.9
|
|
63
|
+
}
|
|
64
|
+
end
|
|
65
|
+
out
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
attr_reader :storage, :user_id
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def save_resource(text)
|
|
73
|
+
@storage.save_resource(@user_id, text)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def save_item(category:, item:, source_resource_id:)
|
|
77
|
+
content = item.is_a?(Hash) ? item["content"] || item[:content] : item.to_s
|
|
78
|
+
@storage.save_item(@user_id, category: category, content: content, source_resource_id: source_resource_id)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Llmemory
|
|
4
|
+
module LongTerm
|
|
5
|
+
module FileBased
|
|
6
|
+
class Resource
|
|
7
|
+
attr_reader :id, :user_id, :text, :created_at
|
|
8
|
+
|
|
9
|
+
def initialize(id:, user_id:, text:, created_at: nil)
|
|
10
|
+
@id = id
|
|
11
|
+
@user_id = user_id
|
|
12
|
+
@text = text
|
|
13
|
+
@created_at = created_at || Time.now
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def to_h
|
|
17
|
+
{ id: id, user_id: user_id, text: text, created_at: created_at.iso8601 }
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Llmemory
|
|
6
|
+
module LongTerm
|
|
7
|
+
module FileBased
|
|
8
|
+
class Retrieval
|
|
9
|
+
def initialize(user_id:, storage:, llm: nil)
|
|
10
|
+
@user_id = user_id
|
|
11
|
+
@storage = storage
|
|
12
|
+
@llm = llm || Llmemory::LLM.client
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def retrieve(query)
|
|
16
|
+
all_categories = @storage.list_categories(@user_id)
|
|
17
|
+
return {} if all_categories.empty?
|
|
18
|
+
|
|
19
|
+
relevant_categories = select_relevant_categories(query, all_categories)
|
|
20
|
+
summaries = {}
|
|
21
|
+
relevant_categories.each { |cat| summaries[cat] = @storage.load_category(@user_id, cat) }
|
|
22
|
+
|
|
23
|
+
return summaries if is_sufficient?(query, summaries)
|
|
24
|
+
|
|
25
|
+
search_query = generate_search_query(query, summaries)
|
|
26
|
+
items = @storage.search_items(@user_id, search_query)
|
|
27
|
+
return format_items(items) if items.any?
|
|
28
|
+
|
|
29
|
+
resources = @storage.search_resources(@user_id, search_query)
|
|
30
|
+
format_resources(resources)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def select_relevant_categories(query, categories)
|
|
36
|
+
return categories if categories.size <= 3
|
|
37
|
+
prompt = <<~PROMPT
|
|
38
|
+
Query: #{query}
|
|
39
|
+
Available Categories: #{categories.join(', ')}
|
|
40
|
+
|
|
41
|
+
Return a JSON array of the categories most relevant to this query. Example: ["work_life", "preferences"]
|
|
42
|
+
PROMPT
|
|
43
|
+
response = @llm.invoke(prompt.strip)
|
|
44
|
+
parsed = parse_categories_response(response)
|
|
45
|
+
parsed.any? ? parsed : categories.first(3)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def parse_categories_response(response)
|
|
49
|
+
json = response.to_s.strip
|
|
50
|
+
start_idx = json.index("[")
|
|
51
|
+
return [] unless start_idx
|
|
52
|
+
end_idx = json.rindex("]")
|
|
53
|
+
return [] unless end_idx
|
|
54
|
+
arr = JSON.parse(json[start_idx..end_idx])
|
|
55
|
+
arr.is_a?(Array) ? arr.map(&:to_s) : []
|
|
56
|
+
rescue JSON::ParserError
|
|
57
|
+
[]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def is_sufficient?(query, summaries)
|
|
61
|
+
return false if summaries.values.all?(&:empty?)
|
|
62
|
+
prompt = <<~PROMPT
|
|
63
|
+
Query: #{query}
|
|
64
|
+
Summaries: #{summaries.to_json}
|
|
65
|
+
Can you answer the query comprehensively with just these summaries? Reply with exactly YES or NO.
|
|
66
|
+
PROMPT
|
|
67
|
+
response = @llm.invoke(prompt.strip).upcase
|
|
68
|
+
response.include?("YES")
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def generate_search_query(query, summaries)
|
|
72
|
+
prompt = <<~PROMPT
|
|
73
|
+
Query: #{query}
|
|
74
|
+
Existing summary context: #{summaries.values.join("\n")[0..500]}
|
|
75
|
+
Generate a short search phrase (3-6 words) to find specific facts. Return only the phrase.
|
|
76
|
+
PROMPT
|
|
77
|
+
@llm.invoke(prompt.strip).to_s.strip
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def format_items(items)
|
|
81
|
+
items.map { |i| i[:content] || i["content"] }.compact
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def format_resources(resources)
|
|
85
|
+
resources.map { |r| r[:text] || r["text"] }.compact
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "storages/base"
|
|
4
|
+
require_relative "storages/memory_storage"
|
|
5
|
+
require_relative "storages/file_storage"
|
|
6
|
+
require_relative "storages/database_storage"
|
|
7
|
+
|
|
8
|
+
module Llmemory
|
|
9
|
+
module LongTerm
|
|
10
|
+
module FileBased
|
|
11
|
+
# Backward compatibility: Storage points to in-memory implementation.
|
|
12
|
+
# Use Storages::MemoryStorage, Storages::FileStorage, or Storages::DatabaseStorage explicitly,
|
|
13
|
+
# or build from config via Storages.build.
|
|
14
|
+
Storage = Storages::MemoryStorage
|
|
15
|
+
|
|
16
|
+
module Storages
|
|
17
|
+
def self.build(store: nil, base_path: nil, database_url: nil)
|
|
18
|
+
case (store || Llmemory.configuration.long_term_store).to_s.to_sym
|
|
19
|
+
when :memory
|
|
20
|
+
MemoryStorage.new
|
|
21
|
+
when :file
|
|
22
|
+
FileStorage.new(base_path: base_path || Llmemory.configuration.long_term_storage_path)
|
|
23
|
+
when :postgres, :database
|
|
24
|
+
DatabaseStorage.new(database_url: database_url || Llmemory.configuration.database_url)
|
|
25
|
+
when :active_record, :activerecord
|
|
26
|
+
require_relative "storages/active_record_storage"
|
|
27
|
+
ActiveRecordStorage.new
|
|
28
|
+
else
|
|
29
|
+
MemoryStorage.new
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Models for ActiveRecordStorage. Loaded only when using store: :active_record.
|
|
4
|
+
# In Rails, run: rails g llmemory:install (or create the migration manually).
|
|
5
|
+
|
|
6
|
+
module Llmemory
|
|
7
|
+
module LongTerm
|
|
8
|
+
module FileBased
|
|
9
|
+
module Storages
|
|
10
|
+
class LlmemoryResource < ::ActiveRecord::Base
|
|
11
|
+
self.table_name = "llmemory_resources"
|
|
12
|
+
self.primary_key = "id"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
class LlmemoryItem < ::ActiveRecord::Base
|
|
16
|
+
self.table_name = "llmemory_items"
|
|
17
|
+
self.primary_key = "id"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
class LlmemoryCategory < ::ActiveRecord::Base
|
|
21
|
+
self.table_name = "llmemory_categories"
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
require_relative "base"
|
|
5
|
+
|
|
6
|
+
module Llmemory
|
|
7
|
+
module LongTerm
|
|
8
|
+
module FileBased
|
|
9
|
+
module Storages
|
|
10
|
+
class ActiveRecordStorage < Base
|
|
11
|
+
def initialize
|
|
12
|
+
self.class.load_models!
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.load_models!
|
|
16
|
+
return if @models_loaded
|
|
17
|
+
require "active_record"
|
|
18
|
+
require_relative "active_record_models"
|
|
19
|
+
@models_loaded = true
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def save_resource(user_id, text)
|
|
23
|
+
id = "res_#{SecureRandom.hex(8)}"
|
|
24
|
+
LlmemoryResource.create!(
|
|
25
|
+
id: id,
|
|
26
|
+
user_id: user_id,
|
|
27
|
+
text: text,
|
|
28
|
+
created_at: Time.current
|
|
29
|
+
)
|
|
30
|
+
id
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def save_item(user_id, category:, content:, source_resource_id:)
|
|
34
|
+
id = "item_#{SecureRandom.hex(8)}"
|
|
35
|
+
LlmemoryItem.create!(
|
|
36
|
+
id: id,
|
|
37
|
+
user_id: user_id,
|
|
38
|
+
category: category,
|
|
39
|
+
content: content,
|
|
40
|
+
source_resource_id: source_resource_id,
|
|
41
|
+
created_at: Time.current
|
|
42
|
+
)
|
|
43
|
+
id
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def load_category(user_id, category_name)
|
|
47
|
+
rec = LlmemoryCategory.find_by(user_id: user_id, category_name: category_name)
|
|
48
|
+
rec ? rec.content.to_s : ""
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def save_category(user_id, category_name, content)
|
|
52
|
+
rec = LlmemoryCategory.find_or_initialize_by(user_id: user_id, category_name: category_name)
|
|
53
|
+
rec.content = content
|
|
54
|
+
rec.updated_at = Time.current
|
|
55
|
+
rec.save!
|
|
56
|
+
true
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def list_categories(user_id)
|
|
60
|
+
LlmemoryCategory.where(user_id: user_id).pluck(:category_name)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def search_items(user_id, query)
|
|
64
|
+
q = "%#{sanitize_like(query)}%"
|
|
65
|
+
LlmemoryItem.where(user_id: user_id).where("LOWER(content) LIKE LOWER(?)", q).map { |r| row_to_item(r) }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def search_resources(user_id, query)
|
|
69
|
+
q = "%#{sanitize_like(query)}%"
|
|
70
|
+
LlmemoryResource.where(user_id: user_id).where("LOWER(text) LIKE LOWER(?)", q).map { |r| row_to_resource(r) }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def get_resources_since(user_id, hours:)
|
|
74
|
+
cutoff = hours.hours.ago
|
|
75
|
+
LlmemoryResource.where(user_id: user_id).where("created_at >= ?", cutoff).order(:created_at).map { |r| row_to_resource(r) }
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def get_items_older_than(user_id, days:)
|
|
79
|
+
cutoff = days.days.ago
|
|
80
|
+
LlmemoryItem.where(user_id: user_id).where("created_at < ?", cutoff).order(:created_at).map { |r| row_to_item(r) }
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def get_all_items(user_id)
|
|
84
|
+
LlmemoryItem.where(user_id: user_id).order(:created_at).map { |r| row_to_item(r) }
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def get_all_resources(user_id)
|
|
88
|
+
LlmemoryResource.where(user_id: user_id).order(:created_at).map { |r| row_to_resource(r) }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def get_items_since(user_id, hours:)
|
|
92
|
+
cutoff = hours.hours.ago
|
|
93
|
+
LlmemoryItem.where(user_id: user_id).where("created_at >= ?", cutoff).order(:created_at).map { |r| row_to_item(r) }
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def replace_items(user_id, ids_to_remove, merged_item)
|
|
97
|
+
LlmemoryItem.where(user_id: user_id, id: ids_to_remove).destroy_all
|
|
98
|
+
created_at = merged_item[:created_at] || Time.current
|
|
99
|
+
LlmemoryItem.create!(
|
|
100
|
+
id: "item_#{SecureRandom.hex(8)}",
|
|
101
|
+
user_id: user_id,
|
|
102
|
+
category: merged_item[:category],
|
|
103
|
+
content: merged_item[:content],
|
|
104
|
+
source_resource_id: merged_item[:source_resource_id],
|
|
105
|
+
created_at: created_at
|
|
106
|
+
)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def archive_items(user_id, item_ids)
|
|
110
|
+
LlmemoryItem.where(user_id: user_id, id: item_ids).destroy_all
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def archive_resources(user_id, resource_ids)
|
|
114
|
+
LlmemoryResource.where(user_id: user_id, id: resource_ids).destroy_all
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
private
|
|
118
|
+
|
|
119
|
+
def sanitize_like(str)
|
|
120
|
+
(str || "").to_s.gsub(/[%_\\]/) { |c| "\\#{c}" }
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def row_to_item(r)
|
|
124
|
+
{
|
|
125
|
+
id: r.id,
|
|
126
|
+
category: r.category,
|
|
127
|
+
content: r.content,
|
|
128
|
+
source_resource_id: r.source_resource_id,
|
|
129
|
+
created_at: r.created_at
|
|
130
|
+
}
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def row_to_resource(r)
|
|
134
|
+
{
|
|
135
|
+
id: r.id,
|
|
136
|
+
text: r.text,
|
|
137
|
+
created_at: r.created_at
|
|
138
|
+
}
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Llmemory
|
|
4
|
+
module LongTerm
|
|
5
|
+
module FileBased
|
|
6
|
+
module Storages
|
|
7
|
+
class Base
|
|
8
|
+
def save_resource(user_id, text)
|
|
9
|
+
raise NotImplementedError, "#{self.class}#save_resource must be implemented"
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def save_item(user_id, category:, content:, source_resource_id:)
|
|
13
|
+
raise NotImplementedError, "#{self.class}#save_item must be implemented"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def load_category(user_id, category_name)
|
|
17
|
+
raise NotImplementedError, "#{self.class}#load_category must be implemented"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def save_category(user_id, category_name, content)
|
|
21
|
+
raise NotImplementedError, "#{self.class}#save_category must be implemented"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def list_categories(user_id)
|
|
25
|
+
raise NotImplementedError, "#{self.class}#list_categories must be implemented"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def search_items(user_id, query)
|
|
29
|
+
raise NotImplementedError, "#{self.class}#search_items must be implemented"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def search_resources(user_id, query)
|
|
33
|
+
raise NotImplementedError, "#{self.class}#search_resources must be implemented"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def get_resources_since(user_id, hours:)
|
|
37
|
+
raise NotImplementedError, "#{self.class}#get_resources_since must be implemented"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def get_items_older_than(user_id, days:)
|
|
41
|
+
raise NotImplementedError, "#{self.class}#get_items_older_than must be implemented"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def get_all_items(user_id)
|
|
45
|
+
raise NotImplementedError, "#{self.class}#get_all_items must be implemented"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def get_all_resources(user_id)
|
|
49
|
+
raise NotImplementedError, "#{self.class}#get_all_resources must be implemented"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def get_items_since(user_id, hours:)
|
|
53
|
+
raise NotImplementedError, "#{self.class}#get_items_since must be implemented"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def replace_items(user_id, ids_to_remove, merged_item)
|
|
57
|
+
raise NotImplementedError, "#{self.class}#replace_items must be implemented"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def archive_items(user_id, item_ids)
|
|
61
|
+
raise NotImplementedError, "#{self.class}#archive_items must be implemented"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def archive_resources(user_id, resource_ids)
|
|
65
|
+
raise NotImplementedError, "#{self.class}#archive_resources must be implemented"
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|