hindsight-ruby 0.1.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.txt +21 -0
- data/README.md +325 -0
- data/hindsight-ruby.gemspec +37 -0
- data/lib/hindsight/bank.rb +204 -0
- data/lib/hindsight/client.rb +296 -0
- data/lib/hindsight/errors.rb +36 -0
- data/lib/hindsight/option_validation.rb +255 -0
- data/lib/hindsight/resources/banks.rb +129 -0
- data/lib/hindsight/resources/base.rb +118 -0
- data/lib/hindsight/resources/chunks.rb +14 -0
- data/lib/hindsight/resources/config.rb +21 -0
- data/lib/hindsight/resources/directives.rb +58 -0
- data/lib/hindsight/resources/documents.rb +30 -0
- data/lib/hindsight/resources/entities.rb +27 -0
- data/lib/hindsight/resources/graph.rb +21 -0
- data/lib/hindsight/resources/memories.rb +30 -0
- data/lib/hindsight/resources/mental_models.rb +65 -0
- data/lib/hindsight/resources/observations.rb +16 -0
- data/lib/hindsight/resources/operations.rb +92 -0
- data/lib/hindsight/resources/tags.rb +18 -0
- data/lib/hindsight/types/fact.rb +76 -0
- data/lib/hindsight/types/operation_receipt.rb +63 -0
- data/lib/hindsight/types/operation_status.rb +57 -0
- data/lib/hindsight/types/payload.rb +33 -0
- data/lib/hindsight/types/recall_result.rb +63 -0
- data/lib/hindsight/types/reflection.rb +49 -0
- data/lib/hindsight/upload_normalizer.rb +142 -0
- data/lib/hindsight/version.rb +5 -0
- data/lib/hindsight-ruby.rb +3 -0
- data/lib/hindsight.rb +30 -0
- metadata +141 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 532ef9d4f663621fe910acdf0935a53b1983df34d9cb8300dd47cdcda8eee7da
|
|
4
|
+
data.tar.gz: 2aaa1601db930922f22ddd43494385e5cb92b8a78d2bff40f9a521ad8add8ea8
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 1c6d5db1e4d4965e64e1cdb5e0effbcb27f825730b72d1bc3bde64be3a816d4d37d7d549d68ea5c00d74a4cc4169e9967b64b1cf21e7e4ec9653df791f905e91
|
|
7
|
+
data.tar.gz: 4062a35309f63312689cff818437d28281e741729730271d28006239f45b97a4188c6048c146ad9aaccf3183d5f727cb3973b75dea328c89c6e357ba2e35d380
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Andrey Samsonov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
# hindsight-ruby
|
|
2
|
+
|
|
3
|
+
`hindsight-ruby` is a standalone, framework-agnostic Ruby client for the [Vectorize Hindsight](https://github.com/vectorize-io/hindsight) API.
|
|
4
|
+
|
|
5
|
+
It provides a small Ruby-idiomatic API for:
|
|
6
|
+
|
|
7
|
+
- retaining text memories
|
|
8
|
+
- retaining files for async processing
|
|
9
|
+
- polling async operation status
|
|
10
|
+
- recalling facts
|
|
11
|
+
- reflecting over stored memory
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
Add to your Gemfile:
|
|
16
|
+
|
|
17
|
+
```ruby
|
|
18
|
+
gem 'hindsight-ruby'
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Then run:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
bundle install
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Quick Start
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
require 'hindsight-ruby'
|
|
31
|
+
|
|
32
|
+
client = Hindsight::Client.new('http://localhost:8888')
|
|
33
|
+
bank = client.bank('quick-start')
|
|
34
|
+
|
|
35
|
+
bank.retain('The team is shipping v2.0 by end of March')
|
|
36
|
+
bank.retain('Maria is on vacation March 20-31')
|
|
37
|
+
bank.retain('Maria is the only one certified for production deploys')
|
|
38
|
+
|
|
39
|
+
# Recall: parallel retrieval (semantic, keyword, graph, temporal) — no LLM
|
|
40
|
+
result = bank.recall('shipping schedule')
|
|
41
|
+
result.facts.each { |f| puts "- #{f.text}" }
|
|
42
|
+
|
|
43
|
+
# Reflect: agentic multi-step search across topics, then synthesizes — uses LLM
|
|
44
|
+
reflection = bank.reflect('What are the risks to the v2.0 launch?')
|
|
45
|
+
puts reflection.text
|
|
46
|
+
|
|
47
|
+
client.banks.delete('quick-start')
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## API
|
|
51
|
+
|
|
52
|
+
### `Hindsight::Client`
|
|
53
|
+
|
|
54
|
+
```ruby
|
|
55
|
+
client = Hindsight::Client.new(
|
|
56
|
+
'https://hindsight.example.com',
|
|
57
|
+
api_key: ENV['HINDSIGHT_API_KEY'], # optional
|
|
58
|
+
headers: { 'X-Request-Source' => 'my-app' }, # optional
|
|
59
|
+
logger: Logger.new($stdout), # optional Faraday response logger
|
|
60
|
+
# connection: my_faraday, # optional; inject a pre-configured Faraday connection
|
|
61
|
+
# allow_insecure: true, # optional, only if you intentionally send credentials over non-localhost http://
|
|
62
|
+
open_timeout: 2,
|
|
63
|
+
timeout: 10
|
|
64
|
+
)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
If `api_key` is provided, the client sends:
|
|
68
|
+
|
|
69
|
+
```http
|
|
70
|
+
Authorization: Bearer <api_key>
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
`connection:` is mainly useful for tests or advanced Faraday customization. If provided, the client uses it directly instead of building an internal connection.
|
|
74
|
+
|
|
75
|
+
When credentials are configured (`api_key` or `Authorization` header), non-localhost `http://` base URLs are rejected unless `allow_insecure: true` is explicitly set.
|
|
76
|
+
|
|
77
|
+
Methods:
|
|
78
|
+
|
|
79
|
+
- `client.bank(bank_id)`
|
|
80
|
+
- `client.banks` (resource API for bank containers)
|
|
81
|
+
- `client.chunks` (resource API for chunk retrieval)
|
|
82
|
+
- `client.version`
|
|
83
|
+
- `client.health`
|
|
84
|
+
- `client.features`
|
|
85
|
+
- `client.file_upload_api_supported?`
|
|
86
|
+
|
|
87
|
+
### `Hindsight::Bank`
|
|
88
|
+
|
|
89
|
+
#### Retain text
|
|
90
|
+
|
|
91
|
+
```ruby
|
|
92
|
+
bank.retain(
|
|
93
|
+
'User prefers vegetarian restaurants',
|
|
94
|
+
context: 'chat',
|
|
95
|
+
tags: ['diet'],
|
|
96
|
+
document_id: 'msg-123',
|
|
97
|
+
timestamp: '2026-03-01T10:00:00Z',
|
|
98
|
+
metadata: { 'source' => 'slack' },
|
|
99
|
+
entities: [{ text: 'Alice', type: 'person' }],
|
|
100
|
+
document_tags: ['onboarding'],
|
|
101
|
+
async: false
|
|
102
|
+
)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
The client sends retain payloads to `POST /memories`.
|
|
106
|
+
|
|
107
|
+
Return value:
|
|
108
|
+
|
|
109
|
+
- when `async: true`, returns `Hindsight::Types::OperationReceipt` (poll with `#fetch` / `#wait`)
|
|
110
|
+
- when `async: false` (or omitted), returns the raw API response hash
|
|
111
|
+
|
|
112
|
+
#### Retain batch
|
|
113
|
+
|
|
114
|
+
```ruby
|
|
115
|
+
receipt = bank.retain_batch(
|
|
116
|
+
[
|
|
117
|
+
{ content: 'Alice likes coffee', context: 'chat', tags: ['prefs'] },
|
|
118
|
+
{ content: 'Bob likes tea', metadata: { 'source' => 'email' } }
|
|
119
|
+
],
|
|
120
|
+
document_tags: ['import'],
|
|
121
|
+
async: true
|
|
122
|
+
)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
`retain_batch` follows the same return contract as `retain`: `async: true` returns `OperationReceipt`; `async: false` returns the raw API response hash.
|
|
126
|
+
|
|
127
|
+
#### Retain files
|
|
128
|
+
|
|
129
|
+
```ruby
|
|
130
|
+
receipt = bank.retain_files(
|
|
131
|
+
['/tmp/profile.pdf'],
|
|
132
|
+
context: 'chat',
|
|
133
|
+
tags: ['onboarding'],
|
|
134
|
+
files_metadata: [{ context: 'report', document_id: 'doc-1' }],
|
|
135
|
+
allowed_paths: ['/tmp']
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
puts receipt.operation_ids.inspect
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
`retain_files` returns `Hindsight::Types::OperationReceipt`.
|
|
142
|
+
|
|
143
|
+
`retain_files` raises `Hindsight::FeatureNotSupported` when the server reports `file_upload_api: false`.
|
|
144
|
+
|
|
145
|
+
**Security:** For path entries, `retain_files` requires `allowed_paths:` and resolves paths with `File.realpath` (resolving symlinks and requiring the target to exist). Do not pass user-controlled paths directly. If you need custom unrestricted file handles, pass upload hashes (`{ io:, filename:, content_type: }`) instead of paths.
|
|
146
|
+
|
|
147
|
+
#### Poll operation status
|
|
148
|
+
|
|
149
|
+
```ruby
|
|
150
|
+
# Poll each operation id once
|
|
151
|
+
statuses = receipt.fetch
|
|
152
|
+
|
|
153
|
+
# Wait until all operations become terminal (completed/failed/cancelled/etc)
|
|
154
|
+
final_statuses = receipt.wait(interval: 1.0, timeout: 120.0)
|
|
155
|
+
|
|
156
|
+
final_statuses.each do |status|
|
|
157
|
+
puts "#{status.id} status=#{status.status} done=#{status.terminal?} ok=#{status.successful?}"
|
|
158
|
+
end
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
`receipt.fetch(bank: other_bank)` and `receipt.wait(bank: other_bank, ...)` are also supported if you want to override the bank context explicitly.
|
|
162
|
+
|
|
163
|
+
You can also call polling APIs directly:
|
|
164
|
+
|
|
165
|
+
```ruby
|
|
166
|
+
status = bank.operations.get('op-123')
|
|
167
|
+
done = bank.operations.wait(%w[op-123 op-456], interval: 0.5, timeout: 60, backoff: :exponential, max_interval: 30)
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
#### Recall
|
|
171
|
+
|
|
172
|
+
```ruby
|
|
173
|
+
result = bank.recall(
|
|
174
|
+
'What are dietary preferences?',
|
|
175
|
+
budget: :mid,
|
|
176
|
+
max_tokens: 2048,
|
|
177
|
+
types: [:world] # optional: :world, :experience, :observation
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
result.facts.each do |fact|
|
|
181
|
+
puts [fact.text, fact.type, fact.entities].inspect
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
puts result.token_count
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Returns `Hindsight::Types::RecallResult` (Enumerable).
|
|
188
|
+
|
|
189
|
+
#### Reflect
|
|
190
|
+
|
|
191
|
+
```ruby
|
|
192
|
+
reflection = bank.reflect(
|
|
193
|
+
'Summarize this user',
|
|
194
|
+
budget: :high
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
puts reflection.text
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Returns `Hindsight::Types::Reflection`.
|
|
201
|
+
|
|
202
|
+
Optional advanced controls:
|
|
203
|
+
|
|
204
|
+
```ruby
|
|
205
|
+
reflection = bank.reflect(
|
|
206
|
+
'Summarize this user',
|
|
207
|
+
include: { facts: { max_count: 8 }, tool_calls: { input: false, output: false } },
|
|
208
|
+
response_schema: {
|
|
209
|
+
type: 'object',
|
|
210
|
+
properties: { summary: { type: 'string' } },
|
|
211
|
+
required: ['summary']
|
|
212
|
+
}
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
puts reflection.json.inspect
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
#### Additional endpoints
|
|
219
|
+
|
|
220
|
+
The gem exposes client-level and bank-level endpoints through resource objects:
|
|
221
|
+
|
|
222
|
+
```ruby
|
|
223
|
+
# bank containers (client-level)
|
|
224
|
+
client.banks.list
|
|
225
|
+
client.banks.create(bank_id: 'user-42', name: 'Helpful support assistant')
|
|
226
|
+
client.banks.update(bank_id: 'user-42', mission: 'Support users clearly')
|
|
227
|
+
client.banks.get('user-42')
|
|
228
|
+
client.banks.stats('user-42')
|
|
229
|
+
client.banks.delete('user-42')
|
|
230
|
+
|
|
231
|
+
# chunk retrieval (client-level)
|
|
232
|
+
client.chunks.get('chunk-1')
|
|
233
|
+
|
|
234
|
+
# bank resources
|
|
235
|
+
bank.stats
|
|
236
|
+
bank.consolidate
|
|
237
|
+
|
|
238
|
+
bank.memories.list(type: :world, q: 'diet', limit: 50, offset: 0)
|
|
239
|
+
bank.memories.get('mem-1')
|
|
240
|
+
bank.memories.delete(type: :experience)
|
|
241
|
+
|
|
242
|
+
bank.mental_models.list(tags: ['team'], tags_match: :all)
|
|
243
|
+
bank.mental_models.create(name: 'Team model', source_query: 'How does team communicate?')
|
|
244
|
+
bank.mental_models.get('team-model')
|
|
245
|
+
bank.mental_models.update(mental_model_id: 'team-model', max_tokens: 4096)
|
|
246
|
+
bank.mental_models.refresh('team-model')
|
|
247
|
+
bank.mental_models.delete('team-model')
|
|
248
|
+
|
|
249
|
+
bank.directives.create(name: 'No competitors', content: 'Never recommend competitors.')
|
|
250
|
+
bank.directives.list(tags: ['policy'], tags_match: :exact, active_only: true)
|
|
251
|
+
bank.directives.get('dir-1')
|
|
252
|
+
bank.directives.update(directive_id: 'dir-1', is_active: false)
|
|
253
|
+
bank.directives.delete('dir-1')
|
|
254
|
+
|
|
255
|
+
bank.documents.list(q: 'proposal', limit: 20, offset: 0)
|
|
256
|
+
bank.documents.get('doc-1')
|
|
257
|
+
bank.documents.delete('doc-1')
|
|
258
|
+
|
|
259
|
+
bank.entities.list(limit: 20, offset: 0)
|
|
260
|
+
bank.entities.get('ent-1')
|
|
261
|
+
|
|
262
|
+
bank.operations.list(status: 'pending', limit: 20, offset: 0)
|
|
263
|
+
bank.operations.get('op-123')
|
|
264
|
+
bank.operations.wait(%w[op-123 op-456], interval: 0.5, timeout: 60, backoff: :exponential, max_interval: 30)
|
|
265
|
+
bank.operations.cancel('op-123')
|
|
266
|
+
|
|
267
|
+
bank.observations.delete
|
|
268
|
+
bank.observations.delete_for_memory('mem-1')
|
|
269
|
+
|
|
270
|
+
bank.tags.list(q: 'user:*', limit: 100, offset: 0)
|
|
271
|
+
|
|
272
|
+
bank.config.get
|
|
273
|
+
bank.config.patch(updates: { llm_model: 'gpt-4.1' })
|
|
274
|
+
bank.config.reset
|
|
275
|
+
|
|
276
|
+
bank.graph.get(type: 'world', q: 'alice', tags: ['team'], tags_match: 'all_strict', limit: 100)
|
|
277
|
+
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
## Error Handling
|
|
281
|
+
|
|
282
|
+
All gem errors inherit from `Hindsight::Error`.
|
|
283
|
+
|
|
284
|
+
- `Hindsight::ValidationError`
|
|
285
|
+
- `Hindsight::ConnectionError`
|
|
286
|
+
- `Hindsight::TimeoutError`
|
|
287
|
+
- `Hindsight::APIError` (`status`, `body`, `retriable?`)
|
|
288
|
+
- `Hindsight::FeatureNotSupported`
|
|
289
|
+
|
|
290
|
+
`APIError#message` includes status context; use `e.body` for full server payload.
|
|
291
|
+
|
|
292
|
+
Example:
|
|
293
|
+
|
|
294
|
+
```ruby
|
|
295
|
+
begin
|
|
296
|
+
bank.retain('hello')
|
|
297
|
+
rescue Hindsight::APIError => e
|
|
298
|
+
warn "status=#{e.status} retriable=#{e.retriable?} body=#{e.body.inspect}"
|
|
299
|
+
end
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
## Type Objects
|
|
303
|
+
|
|
304
|
+
- `Hindsight::Types::Fact`
|
|
305
|
+
- `Hindsight::Types::RecallResult`
|
|
306
|
+
- `Hindsight::Types::Reflection`
|
|
307
|
+
- `Hindsight::Types::OperationReceipt`
|
|
308
|
+
- `Hindsight::Types::OperationStatus`
|
|
309
|
+
|
|
310
|
+
Notes:
|
|
311
|
+
|
|
312
|
+
- `Fact#type` is optional; when the API payload omits type, it is `nil`.
|
|
313
|
+
- Type parsers normalize hash keys internally and read API fields using string-key conventions.
|
|
314
|
+
|
|
315
|
+
## Compatibility Notes
|
|
316
|
+
|
|
317
|
+
- Requires Ruby `>= 3.1`
|
|
318
|
+
- Uses Faraday 2.x and `faraday-multipart`
|
|
319
|
+
|
|
320
|
+
## Development
|
|
321
|
+
|
|
322
|
+
```bash
|
|
323
|
+
bundle install
|
|
324
|
+
bundle exec rspec
|
|
325
|
+
```
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lib/hindsight/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = "hindsight-ruby"
|
|
7
|
+
spec.version = Hindsight::VERSION
|
|
8
|
+
spec.authors = ["Andrey Samsonov"]
|
|
9
|
+
|
|
10
|
+
spec.summary = "Standalone Hindsight API client for Ruby"
|
|
11
|
+
spec.description = "Framework-agnostic ruby client for Hindsight APIs."
|
|
12
|
+
spec.homepage = "https://github.com/kryzhovnik/hindsight-ruby"
|
|
13
|
+
spec.license = "MIT"
|
|
14
|
+
spec.required_ruby_version = ">= 3.1"
|
|
15
|
+
|
|
16
|
+
spec.metadata["source_code_uri"] = spec.homepage
|
|
17
|
+
spec.metadata["bug_tracker_uri"] = "#{spec.homepage}/issues"
|
|
18
|
+
spec.metadata["changelog_uri"] = "#{spec.homepage}/releases"
|
|
19
|
+
|
|
20
|
+
spec.files = Dir.chdir(__dir__) do
|
|
21
|
+
Dir[
|
|
22
|
+
"lib/**/*",
|
|
23
|
+
"README.md",
|
|
24
|
+
"LICENSE.txt",
|
|
25
|
+
"hindsight-ruby.gemspec"
|
|
26
|
+
]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
spec.require_paths = ["lib"]
|
|
30
|
+
|
|
31
|
+
spec.add_dependency "faraday", "~> 2.0"
|
|
32
|
+
spec.add_dependency "faraday-multipart", "~> 1.0"
|
|
33
|
+
|
|
34
|
+
spec.add_development_dependency "rake", ">= 13.0"
|
|
35
|
+
spec.add_development_dependency "rspec", ">= 3.13"
|
|
36
|
+
spec.add_development_dependency "webmock", ">= 3.0"
|
|
37
|
+
end
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require_relative 'errors'
|
|
5
|
+
require_relative 'option_validation'
|
|
6
|
+
require_relative 'upload_normalizer'
|
|
7
|
+
require_relative 'types/payload'
|
|
8
|
+
require_relative 'types/operation_receipt'
|
|
9
|
+
require_relative 'types/recall_result'
|
|
10
|
+
require_relative 'types/reflection'
|
|
11
|
+
|
|
12
|
+
module Hindsight
|
|
13
|
+
class Bank
|
|
14
|
+
attr_reader :client, :bank_id
|
|
15
|
+
|
|
16
|
+
def initialize(client:, bank_id:)
|
|
17
|
+
@client = client
|
|
18
|
+
@bank_id = OptionValidation.normalize_non_empty_string(bank_id, key: :bank_id)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def inspect
|
|
22
|
+
"#<#{self.class} bank_id=#{bank_id.inspect}>"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def retain(text, context: nil, tags: nil, document_id: nil, timestamp: nil,
|
|
26
|
+
metadata: nil, entities: nil, document_tags: nil, async: nil)
|
|
27
|
+
items = [
|
|
28
|
+
normalize_memory_item(
|
|
29
|
+
content: text, context: context, tags: tags,
|
|
30
|
+
document_id: document_id, timestamp: timestamp,
|
|
31
|
+
metadata: metadata, entities: entities
|
|
32
|
+
)
|
|
33
|
+
]
|
|
34
|
+
submit_retain(items: items, document_tags: document_tags, async: async)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def retain_batch(items, document_tags: nil, async: nil)
|
|
38
|
+
raise ValidationError, 'items must be an Array' unless items.is_a?(Array)
|
|
39
|
+
raise ValidationError, 'items must not be empty' if items.empty?
|
|
40
|
+
|
|
41
|
+
submit_retain(
|
|
42
|
+
items: items.map.with_index { |item, index| normalize_batch_memory_item(item, index: index) },
|
|
43
|
+
document_tags: document_tags,
|
|
44
|
+
async: async
|
|
45
|
+
)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Upload files for retention. Accepts file paths (String/Pathname) or upload
|
|
49
|
+
# hashes ({io:, filename:, content_type:}).
|
|
50
|
+
#
|
|
51
|
+
# SECURITY: When +files+ contains user-controlled paths, always set
|
|
52
|
+
# +allowed_paths+ to restrict which directories the gem may read from.
|
|
53
|
+
# Without it, any file readable by the process can be uploaded.
|
|
54
|
+
def retain_files(files, context: nil, tags: nil, metadata: nil, files_metadata: nil, allowed_paths: nil)
|
|
55
|
+
if client.file_upload_api_supported? == false
|
|
56
|
+
raise FeatureNotSupported, 'files/retain requires file_upload_api support'
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
uploads, closers = UploadNormalizer.normalize_uploads(files, allowed_paths: allowed_paths)
|
|
60
|
+
request = {
|
|
61
|
+
context: context,
|
|
62
|
+
tags: OptionValidation.normalize_tags(tags),
|
|
63
|
+
metadata: OptionValidation.normalize_hash(metadata, key: :metadata),
|
|
64
|
+
files_metadata: OptionValidation.normalize_files_metadata(files_metadata)
|
|
65
|
+
}.compact
|
|
66
|
+
|
|
67
|
+
body = client.request_multipart_files(
|
|
68
|
+
:post, "#{bank_path}/files/retain", uploads,
|
|
69
|
+
extra_fields: { request: JSON.generate(request) }
|
|
70
|
+
)
|
|
71
|
+
Types::OperationReceipt.from_api(body, bank: self)
|
|
72
|
+
ensure
|
|
73
|
+
Array(closers).each { |io| io.close unless io.closed? }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def recall(query, budget: :mid, max_tokens: 4096, types: nil, tags: nil, tags_match: nil,
|
|
77
|
+
query_timestamp: nil, include: nil, trace: nil)
|
|
78
|
+
payload = {
|
|
79
|
+
query: OptionValidation.normalize_query(query),
|
|
80
|
+
budget: OptionValidation.normalize_budget(budget).to_s,
|
|
81
|
+
max_tokens: OptionValidation.normalize_positive_integer(max_tokens, key: :max_tokens),
|
|
82
|
+
types: OptionValidation.normalize_fact_types(types),
|
|
83
|
+
tags: OptionValidation.normalize_tags(tags),
|
|
84
|
+
tags_match: OptionValidation.normalize_tags_match(tags_match)&.to_s,
|
|
85
|
+
query_timestamp: OptionValidation.normalize_optional_non_empty_string(query_timestamp, key: :query_timestamp),
|
|
86
|
+
include: OptionValidation.normalize_include_options(include, key: :include),
|
|
87
|
+
trace: OptionValidation.normalize_optional_boolean(trace, key: :trace)
|
|
88
|
+
}.compact
|
|
89
|
+
|
|
90
|
+
body = client.request_json(:post, "#{bank_path}/memories/recall", payload)
|
|
91
|
+
Types::RecallResult.from_api(body, query: payload[:query], budget: payload[:budget])
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def reflect(query, budget: :mid, max_tokens: nil, include: nil, response_schema: nil, tags: nil, tags_match: nil)
|
|
95
|
+
payload = {
|
|
96
|
+
query: OptionValidation.normalize_query(query),
|
|
97
|
+
budget: OptionValidation.normalize_budget(budget).to_s,
|
|
98
|
+
max_tokens: max_tokens ? OptionValidation.normalize_positive_integer(max_tokens, key: :max_tokens) : nil,
|
|
99
|
+
include: OptionValidation.normalize_include_options(include, key: :include),
|
|
100
|
+
response_schema: OptionValidation.normalize_response_schema(response_schema),
|
|
101
|
+
tags: OptionValidation.normalize_tags(tags),
|
|
102
|
+
tags_match: OptionValidation.normalize_tags_match(tags_match)&.to_s
|
|
103
|
+
}.compact
|
|
104
|
+
|
|
105
|
+
body = client.request_json(:post, "#{bank_path}/reflect", payload)
|
|
106
|
+
Types::Reflection.from_api(body)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def stats
|
|
110
|
+
Types::Payload.stringify_keys(client.request_json(:get, "#{bank_path}/stats"))
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def consolidate
|
|
114
|
+
Types::Payload.stringify_keys(client.request_json(:post, "#{bank_path}/consolidate"))
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def memories
|
|
118
|
+
@memories ||= Resources::Memories.new(client: client, base_path: bank_path)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def mental_models
|
|
122
|
+
@mental_models ||= Resources::MentalModels.new(client: client, base_path: bank_path)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def directives
|
|
126
|
+
@directives ||= Resources::Directives.new(client: client, base_path: bank_path)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def documents
|
|
130
|
+
@documents ||= Resources::Documents.new(client: client, base_path: bank_path)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def entities
|
|
134
|
+
@entities ||= Resources::Entities.new(client: client, base_path: bank_path)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def operations
|
|
138
|
+
@operations ||= Resources::Operations.new(client: client, base_path: bank_path)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def observations
|
|
142
|
+
@observations ||= Resources::Observations.new(client: client, base_path: bank_path)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def tags
|
|
146
|
+
@tags ||= Resources::Tags.new(client: client, base_path: bank_path)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def config
|
|
150
|
+
@config ||= Resources::Config.new(client: client, base_path: bank_path)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def graph
|
|
154
|
+
@graph ||= Resources::Graph.new(client: client, base_path: bank_path)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
private
|
|
158
|
+
|
|
159
|
+
def bank_path
|
|
160
|
+
@bank_path ||= "/v1/default/banks/#{client.escape(bank_id)}"
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def submit_retain(items:, document_tags:, async:)
|
|
164
|
+
async_value = OptionValidation.normalize_optional_boolean(async, key: :async)
|
|
165
|
+
payload = {
|
|
166
|
+
async: async_value,
|
|
167
|
+
document_tags: OptionValidation.normalize_tags(document_tags),
|
|
168
|
+
items: items
|
|
169
|
+
}
|
|
170
|
+
body = client.retain_json(bank_path, payload.compact)
|
|
171
|
+
async_value ? Types::OperationReceipt.from_api(body, bank: self) : body
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def normalize_memory_item(content:, context: nil, tags: nil, document_id: nil,
|
|
175
|
+
timestamp: nil, metadata: nil, entities: nil)
|
|
176
|
+
{
|
|
177
|
+
content: OptionValidation.normalize_query(content, key: :content),
|
|
178
|
+
context: context,
|
|
179
|
+
tags: OptionValidation.normalize_tags(tags),
|
|
180
|
+
document_id: document_id,
|
|
181
|
+
timestamp: OptionValidation.normalize_optional_non_empty_string(timestamp, key: :timestamp),
|
|
182
|
+
metadata: OptionValidation.normalize_hash(metadata, key: :metadata),
|
|
183
|
+
entities: OptionValidation.normalize_entities(entities)
|
|
184
|
+
}.compact
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def normalize_batch_memory_item(item, index:)
|
|
188
|
+
raise ValidationError, "items[#{index}] must be a Hash" unless item.is_a?(Hash)
|
|
189
|
+
|
|
190
|
+
value = Types::Payload.stringify_keys(item)
|
|
191
|
+
raise ValidationError, "items[#{index}] must include :content" unless value.key?('content')
|
|
192
|
+
|
|
193
|
+
normalize_memory_item(
|
|
194
|
+
content: value['content'],
|
|
195
|
+
context: value['context'],
|
|
196
|
+
tags: value['tags'],
|
|
197
|
+
document_id: value['document_id'],
|
|
198
|
+
timestamp: value['timestamp'],
|
|
199
|
+
metadata: value['metadata'],
|
|
200
|
+
entities: value['entities']
|
|
201
|
+
)
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|