Class: Llmemory::LongTerm::Episodic::Storages::DatabaseStorage

Inherits:
Base
  • Object
show all
Includes:
Crypto::FieldHelpers
Defined in:
lib/llmemory/long_term/episodic/storages/database_storage.rb

Overview

PostgreSQL backend. Each episode is stored as a JSONB data document (plus id/user_id/created_at and a denormalized search_text for keyword search), mirroring the file-based DatabaseStorage pattern.

Instance Method Summary collapse

Constructor Details

#initialize(database_url: nil, cipher: nil) ⇒ DatabaseStorage

Returns a new instance of DatabaseStorage.



19
20
21
22
23
# File 'lib/llmemory/long_term/episodic/storages/database_storage.rb', line 19

def initialize(database_url: nil, cipher: nil)
  @database_url = database_url || Llmemory.configuration.database_url
  @connection = nil
  @cipher = cipher || Llmemory.build_cipher
end

Instance Method Details

#archive_episodes(user_id, ids) ⇒ Object



94
95
96
97
98
99
100
101
102
# File 'lib/llmemory/long_term/episodic/storages/database_storage.rb', line 94

def archive_episodes(user_id, ids)
  ensure_tables!
  Array(ids).sum do |id|
    conn.exec_params(
      "UPDATE llmemory_episodes SET archived_at = NOW() WHERE user_id = $1 AND id = $2 AND archived_at IS NULL",
      [user_id, id]
    ).cmd_tuples
  end
end

#count_episodes(user_id) ⇒ Object



82
83
84
85
# File 'lib/llmemory/long_term/episodic/storages/database_storage.rb', line 82

def count_episodes(user_id)
  ensure_tables!
  conn.exec_params("SELECT COUNT(*) AS c FROM llmemory_episodes WHERE user_id = $1 AND archived_at IS NULL", [user_id]).first["c"].to_i
end

#delete_episodes(user_id, ids) ⇒ Object



87
88
89
90
91
92
# File 'lib/llmemory/long_term/episodic/storages/database_storage.rb', line 87

def delete_episodes(user_id, ids)
  ensure_tables!
  Array(ids).sum do |id|
    conn.exec_params("DELETE FROM llmemory_episodes WHERE user_id = $1 AND id = $2", [user_id, id]).cmd_tuples
  end
end

#expired_episode_ids(user_id, cutoff:) ⇒ Object



104
105
106
107
108
109
110
# File 'lib/llmemory/long_term/episodic/storages/database_storage.rb', line 104

def expired_episode_ids(user_id, cutoff:)
  ensure_tables!
  conn.exec_params(
    "SELECT id FROM llmemory_episodes WHERE user_id = $1 AND archived_at IS NULL AND created_at < $2",
    [user_id, cutoff.iso8601]
  ).map { |r| r["id"] }
end

#get_episode(user_id, id) ⇒ Object



40
41
42
43
44
# File 'lib/llmemory/long_term/episodic/storages/database_storage.rb', line 40

def get_episode(user_id, id)
  ensure_tables!
  rows = conn.exec_params("SELECT data FROM llmemory_episodes WHERE user_id = $1 AND id = $2", [user_id, id])
  rows.any? ? parse_data(rows.first["data"]) : nil
end

#list_episodes(user_id, limit: nil, offset: nil) ⇒ Object



46
47
48
49
50
51
52
# File 'lib/llmemory/long_term/episodic/storages/database_storage.rb', line 46

def list_episodes(user_id, limit: nil, offset: nil)
  ensure_tables!
  sql = "SELECT data FROM llmemory_episodes WHERE user_id = $1 AND archived_at IS NULL ORDER BY created_at DESC"
  sql += " LIMIT #{limit.to_i}" if limit && limit.to_i.positive?
  sql += " OFFSET #{offset.to_i}" if offset && offset.to_i.positive?
  conn.exec_params(sql, [user_id]).map { |r| parse_data(r["data"]) }
end

#list_usersObject



112
113
114
115
# File 'lib/llmemory/long_term/episodic/storages/database_storage.rb', line 112

def list_users
  ensure_tables!
  conn.exec("SELECT DISTINCT user_id FROM llmemory_episodes").map { |r| r["user_id"] }
end

#save_episode(user_id, episode) ⇒ Object



25
26
27
28
29
30
31
32
33
34
35
36
37
38
# File 'lib/llmemory/long_term/episodic/storages/database_storage.rb', line 25

def save_episode(user_id, episode)
  ensure_tables!
  id = episode[:id] || episode["id"] || "ep_#{SecureRandom.hex(8)}"
  data = symbolize(episode).merge(id: id, user_id: user_id)
  data[:created_at] ||= Time.now.utc.iso8601
  search = searchable_text(data)
  conn.exec_params(
    "INSERT INTO llmemory_episodes (id, user_id, data, search_text, search_tokens, created_at) " \
    "VALUES ($1, $2, $3::jsonb, $4, $5, $6) " \
    "ON CONFLICT (id) DO UPDATE SET data = $3::jsonb, search_text = $4, search_tokens = $5",
    [id, user_id, store_data(data), enc(search), search_tokens_for(search), created_at_value(data)]
  )
  id
end

#search_episodes(user_id, query) ⇒ Object



54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
# File 'lib/llmemory/long_term/episodic/storages/database_storage.rb', line 54

def search_episodes(user_id, query)
  ensure_tables!
  tokens = Llmemory::Tokenizer.tokenize(query)
  return conn.exec_params(
    "SELECT data FROM llmemory_episodes WHERE user_id = $1 AND archived_at IS NULL ORDER BY created_at DESC",
    [user_id]
  ).map { |r| parse_data(r["data"]) } if tokens.empty?

  suffix, params = blind_token_filter("search_text", query, 2, search_tokens_column: cipher.enabled? ? "search_tokens" : nil)
  rows = conn.exec_params(
    "SELECT data FROM llmemory_episodes WHERE user_id = $1 AND archived_at IS NULL#{suffix} ORDER BY created_at DESC",
    [user_id, *params]
  )
  episodes = rows.map { |r| parse_data(r["data"]) }
  return episodes unless cipher.enabled?

  legacy_rows = conn.exec_params(
    "SELECT data FROM llmemory_episodes WHERE user_id = $1 AND archived_at IS NULL AND search_tokens IS NULL",
    [user_id]
  )
  legacy = legacy_rows.map { |r| parse_data(r["data"]) }.select do |ep|
    Llmemory::Tokenizer.matches?(searchable_text(ep), query)
  end
  by_id = {}
  (episodes + legacy).each { |ep| by_id[ep[:id] || ep["id"]] = ep }
  by_id.values
end