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 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