hotkv 1.0.0
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 +202 -0
- data/NOTICE +6 -0
- data/README.md +459 -0
- data/SECURITY.md +30 -0
- data/lib/hotkv/ai/call_result.rb +25 -0
- data/lib/hotkv/ai/prompt_cache.rb +135 -0
- data/lib/hotkv/ai/semantic_cache.rb +66 -0
- data/lib/hotkv/client.rb +201 -0
- data/lib/hotkv/cluster/crc16.rb +43 -0
- data/lib/hotkv/cluster.rb +184 -0
- data/lib/hotkv/commands/admin.rb +36 -0
- data/lib/hotkv/commands/all.rb +49 -0
- data/lib/hotkv/commands/bitmaps.rb +52 -0
- data/lib/hotkv/commands/geo.rb +146 -0
- data/lib/hotkv/commands/hashes.rb +169 -0
- data/lib/hotkv/commands/hyperloglog.rb +21 -0
- data/lib/hotkv/commands/keys.rb +170 -0
- data/lib/hotkv/commands/lists.rb +113 -0
- data/lib/hotkv/commands/probabilistic.rb +23 -0
- data/lib/hotkv/commands/publishing.rb +46 -0
- data/lib/hotkv/commands/scan.rb +82 -0
- data/lib/hotkv/commands/scripting.rb +128 -0
- data/lib/hotkv/commands/server.rb +175 -0
- data/lib/hotkv/commands/sets.rb +75 -0
- data/lib/hotkv/commands/sorted_sets.rb +258 -0
- data/lib/hotkv/commands/streams.rb +207 -0
- data/lib/hotkv/commands/strings.rb +178 -0
- data/lib/hotkv/commands/support.rb +69 -0
- data/lib/hotkv/connection.rb +221 -0
- data/lib/hotkv/errors.rb +145 -0
- data/lib/hotkv/extensions.rb +118 -0
- data/lib/hotkv/future.rb +55 -0
- data/lib/hotkv/llm_price.rb +53 -0
- data/lib/hotkv/namespaces/admin.rb +261 -0
- data/lib/hotkv/namespaces/amem.rb +172 -0
- data/lib/hotkv/namespaces/caching.rb +397 -0
- data/lib/hotkv/namespaces/core.rb +190 -0
- data/lib/hotkv/namespaces/ops.rb +166 -0
- data/lib/hotkv/namespaces/probabilistic.rb +180 -0
- data/lib/hotkv/namespaces/rag.rb +63 -0
- data/lib/hotkv/pipeline.rb +57 -0
- data/lib/hotkv/pool.rb +123 -0
- data/lib/hotkv/pubsub.rb +60 -0
- data/lib/hotkv/resp.rb +176 -0
- data/lib/hotkv/transaction.rb +115 -0
- data/lib/hotkv/url.rb +100 -0
- data/lib/hotkv/usage.rb +42 -0
- data/lib/hotkv/version.rb +7 -0
- data/lib/hotkv/wire.rb +145 -0
- data/lib/hotkv.rb +28 -0
- metadata +137 -0
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
# Copyright (c) 2026 HotKV Ltd
|
|
2
|
+
# Licensed under the Apache License, Version 2.0. See LICENSE in the project root.
|
|
3
|
+
|
|
4
|
+
module HotKV
|
|
5
|
+
module Namespaces
|
|
6
|
+
# ACL: users and their permissions (core, not licensed).
|
|
7
|
+
class ACL
|
|
8
|
+
def initialize(client)
|
|
9
|
+
@client = client
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Creates or modifies a user. +rules+ are the ACL rule strings
|
|
13
|
+
# ("on", ">password", "~pattern", "+get", ...); every rule applies or
|
|
14
|
+
# none does.
|
|
15
|
+
def setuser(username, *rules)
|
|
16
|
+
@client.invoke("ACL", "SETUSER", username, *rules.flatten) { |r| r == "OK" }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Hash of the user's flags, passwords (SHA-256 hex), commands, keys,
|
|
20
|
+
# channels, databases and selectors; nil when the user does not exist.
|
|
21
|
+
def getuser(username)
|
|
22
|
+
@client.invoke("ACL", "GETUSER", username) { |r| r.nil? ? nil : hash_of(r) }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Deletes users and disconnects their clients. Returns how many existed.
|
|
26
|
+
def deluser(*usernames)
|
|
27
|
+
@client.invoke("ACL", "DELUSER", *usernames.flatten)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Every user as an ACL file line.
|
|
31
|
+
def list
|
|
32
|
+
@client.invoke("ACL", "LIST")
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def users
|
|
36
|
+
@client.invoke("ACL", "USERS")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# The user of the connection that runs it.
|
|
40
|
+
def whoami
|
|
41
|
+
@client.invoke("ACL", "WHOAMI")
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# The categories, or the commands in +category+.
|
|
45
|
+
def cat(category = nil)
|
|
46
|
+
category ? @client.invoke("ACL", "CAT", category) : @client.invoke("ACL", "CAT")
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# A random password of +bits+ bits (default 256) as hex.
|
|
50
|
+
def genpass(bits = nil)
|
|
51
|
+
bits ? @client.invoke("ACL", "GENPASS", bits) : @client.invoke("ACL", "GENPASS")
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Denied commands and failed logins (the newest +count+ entries).
|
|
55
|
+
def log(count = nil)
|
|
56
|
+
count ? @client.invoke("ACL", "LOG", count) : @client.invoke("ACL", "LOG")
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def log_reset
|
|
60
|
+
@client.invoke("ACL", "LOG", "RESET") { |r| r == "OK" }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# "OK" when +username+ may run the command, else the reason.
|
|
64
|
+
def dryrun(username, *command)
|
|
65
|
+
@client.invoke("ACL", "DRYRUN", username, *command.flatten)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
def hash_of(reply)
|
|
71
|
+
reply.is_a?(Hash) ? reply : reply.each_slice(2).to_h
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Multi-tenancy (TENANT.*, licensed).
|
|
76
|
+
class Tenant
|
|
77
|
+
def initialize(client)
|
|
78
|
+
@client = client
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Creates tenant +id+ with display +name+ and optional limits.
|
|
82
|
+
def create(id, name, max_memory: nil, max_keys: nil, max_connections: nil, max_commands_sec: nil,
|
|
83
|
+
max_databases: nil, max_egress: nil)
|
|
84
|
+
args = ["TENANT.CREATE", id, name]
|
|
85
|
+
args.push("MAX_MEMORY", max_memory) if max_memory
|
|
86
|
+
args.push("MAX_KEYS", max_keys) if max_keys
|
|
87
|
+
args.push("MAX_CONNECTIONS", max_connections) if max_connections
|
|
88
|
+
args.push("MAX_COMMANDS_SEC", max_commands_sec) if max_commands_sec
|
|
89
|
+
args.push("MAX_DATABASES", max_databases) if max_databases
|
|
90
|
+
args.push("MAX_EGRESS", max_egress) if max_egress
|
|
91
|
+
@client.invoke(*args) { |r| r == "OK" }
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def drop(id)
|
|
95
|
+
@client.invoke("TENANT.DROP", id) { |r| r == 1 || r == "OK" }
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Tenant ids.
|
|
99
|
+
def list
|
|
100
|
+
@client.invoke("TENANT.LIST")
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Hash with :id, :name, :state and the usage counters.
|
|
104
|
+
def info(id)
|
|
105
|
+
@client.invoke("TENANT.INFO", id) { |r| Commands::Support.pairs_to_h(r) }
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Hash of usage counters and limits (:memory_bytes, :memory_limit, ...).
|
|
109
|
+
def stats(id)
|
|
110
|
+
@client.invoke("TENANT.STATS", id) { |r| Commands::Support.pairs_to_h(r) }
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def suspend(id)
|
|
114
|
+
@client.invoke("TENANT.SUSPEND", id) { |r| r == "OK" }
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def resume(id)
|
|
118
|
+
@client.invoke("TENANT.RESUME", id) { |r| r == "OK" }
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Binds ACL user +username+ to tenant +tenant_id+.
|
|
122
|
+
def bind(tenant_id, username)
|
|
123
|
+
@client.invoke("TENANT.BIND", tenant_id, username) { |r| r == "OK" }
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def unbind(tenant_id, username)
|
|
127
|
+
@client.invoke("TENANT.UNBIND", tenant_id, username) { |r| r == 1 || r == "OK" }
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Enterprise IAM roles and sessions (IAM.*, licensed).
|
|
132
|
+
class IAM
|
|
133
|
+
def initialize(client)
|
|
134
|
+
@client = client
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# +permissions+ is a comma separated String or an Array of command
|
|
138
|
+
# patterns; :key_prefixes restricts the role to keys with those prefixes.
|
|
139
|
+
def role_create(name, permissions, key_prefixes: nil, rate_limit: nil, memory_quota: nil)
|
|
140
|
+
args = ["IAM.ROLE.CREATE", name, Array(permissions).join(",")]
|
|
141
|
+
args.push("KEY_PREFIXES", Array(key_prefixes).join(",")) if key_prefixes
|
|
142
|
+
args.push("RATE_LIMIT", rate_limit) if rate_limit
|
|
143
|
+
args.push("MEMORY_QUOTA", memory_quota) if memory_quota
|
|
144
|
+
@client.invoke(*args) { |r| r == "OK" }
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def role_delete(name)
|
|
148
|
+
@client.invoke("IAM.ROLE.DELETE", name) { |r| r == "OK" }
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def role_list
|
|
152
|
+
@client.invoke("IAM.ROLE.LIST")
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Hash with :name, :permissions (comma joined String), :rate_limit and
|
|
156
|
+
# :memory_quota.
|
|
157
|
+
def role_info(name)
|
|
158
|
+
@client.invoke("IAM.ROLE.INFO", name) { |r| Commands::Support.pairs_to_h(r) }
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def user_assign(username, rolename)
|
|
162
|
+
@client.invoke("IAM.USER.ASSIGN", username, rolename) { |r| r == "OK" }
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Active sessions as an Array of Hashes (:id, :username, :commands).
|
|
166
|
+
def session_list
|
|
167
|
+
@client.invoke("IAM.SESSION.LIST") { |r| r.map { |s| Commands::Support.pairs_to_h(s) } }
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def session_kill(session_id)
|
|
171
|
+
@client.invoke("IAM.SESSION.KILL", session_id) { |r| r == "OK" }
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Authenticates +username+ against +source+ ("ldap", "oidc", "saml", or
|
|
175
|
+
# anything else for local auth). Returns the session id.
|
|
176
|
+
def auth(source, username, credential)
|
|
177
|
+
@client.invoke("IAM.AUTH", source, username, credential)
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Tamper evident audit log (AUDIT.*, licensed).
|
|
182
|
+
class Audit
|
|
183
|
+
def initialize(client)
|
|
184
|
+
@client = client
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Hash with :total_entries, :file_size_bytes, :chain_valid (Integer
|
|
188
|
+
# 0/1) and :last_sequence.
|
|
189
|
+
def info
|
|
190
|
+
@client.invoke("AUDIT.INFO") { |r| Commands::Support.pairs_to_h(r) }
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Verifies the whole hash chain. Returns the server's status line
|
|
194
|
+
# ("OK chain verified"); raises ServerError when tampering is detected.
|
|
195
|
+
def verify
|
|
196
|
+
@client.invoke("AUDIT.VERIFY")
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Writes the log to +file+ on the server (+format+ "jsonl", "csv" or "cef").
|
|
200
|
+
def export(file, format: nil)
|
|
201
|
+
args = ["AUDIT.EXPORT", file]
|
|
202
|
+
args.push("FORMAT", format) if format
|
|
203
|
+
@client.invoke(*args) { |r| r == "OK" }
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Renames the current log with a timestamp suffix and starts a new one.
|
|
207
|
+
def rotate
|
|
208
|
+
@client.invoke("AUDIT.ROTATE") { |r| r == "OK" }
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Encryption at rest (ENCRYPTION.*, licensed).
|
|
213
|
+
class Encryption
|
|
214
|
+
def initialize(client)
|
|
215
|
+
@client = client
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Hash with :enabled (Integer 0/1), :algorithm and :provider.
|
|
219
|
+
def info
|
|
220
|
+
@client.invoke("ENCRYPTION.INFO") { |r| Commands::Support.pairs_to_h(r) }
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Switches snapshots to the 32 byte key in +new_key_file+ (a path on
|
|
224
|
+
# the server).
|
|
225
|
+
def rotate(new_key_file)
|
|
226
|
+
@client.invoke("ENCRYPTION.ROTATE", new_key_file) { |r| r == "OK" }
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# True when the encrypted file at +file+ (a server path) is intact.
|
|
230
|
+
def verify(file)
|
|
231
|
+
@client.invoke("ENCRYPTION.VERIFY", file) { |r| r == 1 }
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Replication control (REPLICATION.*, licensed).
|
|
236
|
+
class Replication
|
|
237
|
+
def initialize(client)
|
|
238
|
+
@client = client
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Hash with :role, :state, :repl_offset, :connected_replicas, ...
|
|
242
|
+
def info
|
|
243
|
+
@client.invoke("REPLICATION.INFO") { |r| Commands::Support.pairs_to_h(r) }
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Stops sending (primary) or applying (replica) writes.
|
|
247
|
+
def pause
|
|
248
|
+
@client.invoke("REPLICATION.PAUSE") { |r| r == "OK" }
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def resume
|
|
252
|
+
@client.invoke("REPLICATION.RESUME") { |r| r == "OK" }
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# Promotes this replica to primary (without cluster mode).
|
|
256
|
+
def failover
|
|
257
|
+
@client.invoke("REPLICATION.FAILOVER") { |r| r == "OK" }
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
end
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# Copyright (c) 2026 HotKV Ltd
|
|
2
|
+
# Licensed under the Apache License, Version 2.0. See LICENSE in the project root.
|
|
3
|
+
|
|
4
|
+
module HotKV
|
|
5
|
+
module Namespaces
|
|
6
|
+
# AMEM.* (AI agent memory; needs a license that includes the
|
|
7
|
+
# AgentMemory feature): conversation, semantic, working and episodic
|
|
8
|
+
# memory, plus cross-memory search. AMEM.SUMMARY.* (the AMemSummary
|
|
9
|
+
# feature) lives under the nested #summary accessor.
|
|
10
|
+
class AMem
|
|
11
|
+
attr_reader :summary
|
|
12
|
+
|
|
13
|
+
def initialize(client)
|
|
14
|
+
@client = client
|
|
15
|
+
@summary = Summary.new(client)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# -- conversation memory --
|
|
19
|
+
|
|
20
|
+
def conv_create(session_id, window: nil, ttl: nil)
|
|
21
|
+
args = ["AMEM.CONV.CREATE", session_id]
|
|
22
|
+
args.push("WINDOW", window) if window
|
|
23
|
+
args.push("TTL", ttl) if ttl
|
|
24
|
+
@client.invoke(*args) { |r| r == "OK" }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def conv_append(session_id, role, content)
|
|
28
|
+
@client.invoke("AMEM.CONV.APPEND", session_id, role, content) { |r| r == "OK" }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def conv_get(session_id)
|
|
32
|
+
@client.invoke("AMEM.CONV.GET", session_id) { |r| r.map { |row| labeled(row, %i[role content]) } }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def conv_window(session_id, n)
|
|
36
|
+
@client.invoke("AMEM.CONV.WINDOW", session_id, n) { |r| r.map { |row| labeled(row, %i[role content]) } }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def conv_clear(session_id)
|
|
40
|
+
@client.invoke("AMEM.CONV.CLEAR", session_id) { |r| r == 1 }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def conv_drop(session_id)
|
|
44
|
+
@client.invoke("AMEM.CONV.DROP", session_id) { |r| r == 1 }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# -- semantic memory (per-agent vector store) --
|
|
48
|
+
|
|
49
|
+
def sem_create(agent_id, dimensions)
|
|
50
|
+
@client.invoke("AMEM.SEM.CREATE", agent_id, dimensions) { |r| r == "OK" }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def sem_store(agent_id, key, content, vector, ttl: nil)
|
|
54
|
+
args = ["AMEM.SEM.STORE", agent_id, key, content, *Wire.finite_vector(vector)]
|
|
55
|
+
args.push("TTL", ttl) if ttl
|
|
56
|
+
@client.invoke(*args) { |r| r == "OK" }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def sem_recall(agent_id, k, vector)
|
|
60
|
+
@client.invoke("AMEM.SEM.RECALL", agent_id, k, *Wire.finite_vector(vector)) do |r|
|
|
61
|
+
r.map { |row| labeled(row, %i[key content similarity], float_fields: %i[similarity]) }
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def sem_forget(agent_id, key)
|
|
66
|
+
@client.invoke("AMEM.SEM.FORGET", agent_id, key) { |r| r == 1 }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def sem_list(agent_id)
|
|
70
|
+
@client.invoke("AMEM.SEM.LIST", agent_id)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# -- working memory (per-session scratch key/value) --
|
|
74
|
+
|
|
75
|
+
def work_set(session_id, key, value, ttl: nil)
|
|
76
|
+
args = ["AMEM.WORK.SET", session_id, key, value]
|
|
77
|
+
args.push("TTL", ttl) if ttl
|
|
78
|
+
@client.invoke(*args) { |r| r == "OK" }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def work_get(session_id, key)
|
|
82
|
+
@client.invoke("AMEM.WORK.GET", session_id, key)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def work_del(session_id, key)
|
|
86
|
+
@client.invoke("AMEM.WORK.DEL", session_id, key) { |r| r == 1 }
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def work_clear(session_id)
|
|
90
|
+
@client.invoke("AMEM.WORK.CLEAR", session_id) { |r| r == 1 }
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def work_list(session_id)
|
|
94
|
+
@client.invoke("AMEM.WORK.LIST", session_id)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# -- episodic memory (append only event log) --
|
|
98
|
+
|
|
99
|
+
def epis_log(agent_id, event_type, content, metadata: nil)
|
|
100
|
+
args = ["AMEM.EPIS.LOG", agent_id, event_type, content]
|
|
101
|
+
args.push("METADATA", metadata) if metadata
|
|
102
|
+
@client.invoke(*args) { |r| r == "OK" }
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def epis_query(agent_id, type: nil, from: nil, to: nil, limit: nil)
|
|
106
|
+
args = ["AMEM.EPIS.QUERY", agent_id]
|
|
107
|
+
args.push("TYPE", type) if type
|
|
108
|
+
args.push("FROM", from) if from
|
|
109
|
+
args.push("TO", to) if to
|
|
110
|
+
args.push("LIMIT", limit) if limit
|
|
111
|
+
@client.invoke(*args) { |r| r.map { |row| labeled(row, %i[type content metadata timestamp]) } }
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def epis_clear(agent_id)
|
|
115
|
+
@client.invoke("AMEM.EPIS.CLEAR", agent_id) { |r| r == 1 }
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def epis_stats(agent_id)
|
|
119
|
+
@client.invoke("AMEM.EPIS.STATS", agent_id) { |r| Commands::Support.pairs_to_h(r) }
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# -- cross-memory search (semantic + episodic + working together) --
|
|
123
|
+
|
|
124
|
+
def search(agent_id, k, vector)
|
|
125
|
+
@client.invoke("AMEM.SEARCH", agent_id, k, *Wire.finite_vector(vector)) do |r|
|
|
126
|
+
r.map { |row| labeled(row, %i[source key content similarity], float_fields: %i[similarity]) }
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
private
|
|
131
|
+
|
|
132
|
+
# A "labeled flat array" reply looks like ["role", <role>, "content",
|
|
133
|
+
# <content>]: fixed label tokens paired with their values. Unwraps
|
|
134
|
+
# it into a Hash keyed by +fields+, in the order the server documents.
|
|
135
|
+
def labeled(row, fields, float_fields: [])
|
|
136
|
+
h = {}
|
|
137
|
+
fields.each_with_index { |f, i| h[f] = row[(i * 2) + 1] }
|
|
138
|
+
float_fields.each { |f| h[f] = Wire.parse_float(h[f]) }
|
|
139
|
+
h
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# AMEM.SUMMARY.* (needs the AMemSummary license feature).
|
|
143
|
+
class Summary
|
|
144
|
+
def initialize(client)
|
|
145
|
+
@client = client
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def set(agent_id, summary_text)
|
|
149
|
+
@client.invoke("AMEM.SUMMARY.SET", agent_id, summary_text)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def append(agent_id, extra)
|
|
153
|
+
@client.invoke("AMEM.SUMMARY.APPEND", agent_id, extra)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def get(agent_id)
|
|
157
|
+
@client.invoke("AMEM.SUMMARY.GET", agent_id) do |r|
|
|
158
|
+
r.nil? ? nil : { summary: r[0], version: r[1], updated_unix: r[2] }
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def drop(agent_id)
|
|
163
|
+
@client.invoke("AMEM.SUMMARY.DROP", agent_id) { |r| r == 1 }
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def list
|
|
167
|
+
@client.invoke("AMEM.SUMMARY.LIST")
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|