0.0
No release in over 3 years
Framework-agnostic ruby client for Hindsight APIs.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Development

>= 13.0
>= 3.13
>= 3.0

Runtime

 Project Readme

hindsight-ruby

hindsight-ruby is a standalone, framework-agnostic Ruby client for the Vectorize Hindsight API.

It provides a small Ruby-idiomatic API for:

  • retaining text memories
  • retaining files for async processing
  • polling async operation status
  • recalling facts
  • reflecting over stored memory

Installation

Add to your Gemfile:

gem 'hindsight-ruby'

Then run:

bundle install

Quick Start

require 'hindsight-ruby'

client = Hindsight::Client.new('http://localhost:8888')
bank = client.bank('quick-start')

bank.retain('The team is shipping v2.0 by end of March')
bank.retain('Maria is on vacation March 20-31')
bank.retain('Maria is the only one certified for production deploys')

# Recall: parallel retrieval (semantic, keyword, graph, temporal) — no LLM
result = bank.recall('shipping schedule')
result.facts.each { |f| puts "- #{f.text}" }

# Reflect: agentic multi-step search across topics, then synthesizes — uses LLM
reflection = bank.reflect('What are the risks to the v2.0 launch?')
puts reflection.text

client.banks.delete('quick-start')

API

Hindsight::Client

client = Hindsight::Client.new(
  'https://hindsight.example.com',
  api_key: ENV['HINDSIGHT_API_KEY'], # optional
  headers: { 'X-Request-Source' => 'my-app' }, # optional
  logger: Logger.new($stdout), # optional Faraday response logger
  # connection: my_faraday, # optional; inject a pre-configured Faraday connection
  # allow_insecure: true, # optional, only if you intentionally send credentials over non-localhost http://
  open_timeout: 2,
  timeout: 10
)

If api_key is provided, the client sends:

Authorization: Bearer <api_key>

connection: is mainly useful for tests or advanced Faraday customization. If provided, the client uses it directly instead of building an internal connection.

When credentials are configured (api_key or Authorization header), non-localhost http:// base URLs are rejected unless allow_insecure: true is explicitly set.

Methods:

  • client.bank(bank_id)
  • client.banks (resource API for bank containers)
  • client.chunks (resource API for chunk retrieval)
  • client.version
  • client.health
  • client.features
  • client.file_upload_api_supported?

Hindsight::Bank

Retain text

bank.retain(
  'User prefers vegetarian restaurants',
  context: 'chat',
  tags: ['diet'],
  document_id: 'msg-123',
  timestamp: '2026-03-01T10:00:00Z',
  metadata: { 'source' => 'slack' },
  entities: [{ text: 'Alice', type: 'person' }],
  document_tags: ['onboarding'],
  async: false
)

The client sends retain payloads to POST /memories.

Return value:

  • when async: true, returns Hindsight::Types::OperationReceipt (poll with #fetch / #wait)
  • when async: false (or omitted), returns the raw API response hash

Retain batch

receipt = bank.retain_batch(
  [
    { content: 'Alice likes coffee', context: 'chat', tags: ['prefs'] },
    { content: 'Bob likes tea', metadata: { 'source' => 'email' } }
  ],
  document_tags: ['import'],
  async: true
)

retain_batch follows the same return contract as retain: async: true returns OperationReceipt; async: false returns the raw API response hash.

Retain files

receipt = bank.retain_files(
  ['/tmp/profile.pdf'],
  context: 'chat',
  tags: ['onboarding'],
  files_metadata: [{ context: 'report', document_id: 'doc-1' }],
  allowed_paths: ['/tmp']
)

puts receipt.operation_ids.inspect

retain_files returns Hindsight::Types::OperationReceipt.

retain_files raises Hindsight::FeatureNotSupported when the server reports file_upload_api: false.

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.

Poll operation status

# Poll each operation id once
statuses = receipt.fetch

# Wait until all operations become terminal (completed/failed/cancelled/etc)
final_statuses = receipt.wait(interval: 1.0, timeout: 120.0)

final_statuses.each do |status|
  puts "#{status.id} status=#{status.status} done=#{status.terminal?} ok=#{status.successful?}"
end

receipt.fetch(bank: other_bank) and receipt.wait(bank: other_bank, ...) are also supported if you want to override the bank context explicitly.

You can also call polling APIs directly:

status = bank.operations.get('op-123')
done = bank.operations.wait(%w[op-123 op-456], interval: 0.5, timeout: 60, backoff: :exponential, max_interval: 30)

Recall

result = bank.recall(
  'What are dietary preferences?',
  budget: :mid,
  max_tokens: 2048,
  types: [:world] # optional: :world, :experience, :observation
)

result.facts.each do |fact|
  puts [fact.text, fact.type, fact.entities].inspect
end

puts result.token_count

Returns Hindsight::Types::RecallResult (Enumerable).

Reflect

reflection = bank.reflect(
  'Summarize this user',
  budget: :high
)

puts reflection.text

Returns Hindsight::Types::Reflection.

Optional advanced controls:

reflection = bank.reflect(
  'Summarize this user',
  include: { facts: { max_count: 8 }, tool_calls: { input: false, output: false } },
  response_schema: {
    type: 'object',
    properties: { summary: { type: 'string' } },
    required: ['summary']
  }
)

puts reflection.json.inspect

Additional endpoints

The gem exposes client-level and bank-level endpoints through resource objects:

# bank containers (client-level)
client.banks.list
client.banks.create(bank_id: 'user-42', name: 'Helpful support assistant')
client.banks.update(bank_id: 'user-42', mission: 'Support users clearly')
client.banks.get('user-42')
client.banks.stats('user-42')
client.banks.delete('user-42')

# chunk retrieval (client-level)
client.chunks.get('chunk-1')

# bank resources
bank.stats
bank.consolidate

bank.memories.list(type: :world, q: 'diet', limit: 50, offset: 0)
bank.memories.get('mem-1')
bank.memories.delete(type: :experience)

bank.mental_models.list(tags: ['team'], tags_match: :all)
bank.mental_models.create(name: 'Team model', source_query: 'How does team communicate?')
bank.mental_models.get('team-model')
bank.mental_models.update(mental_model_id: 'team-model', max_tokens: 4096)
bank.mental_models.refresh('team-model')
bank.mental_models.delete('team-model')

bank.directives.create(name: 'No competitors', content: 'Never recommend competitors.')
bank.directives.list(tags: ['policy'], tags_match: :exact, active_only: true)
bank.directives.get('dir-1')
bank.directives.update(directive_id: 'dir-1', is_active: false)
bank.directives.delete('dir-1')

bank.documents.list(q: 'proposal', limit: 20, offset: 0)
bank.documents.get('doc-1')
bank.documents.delete('doc-1')

bank.entities.list(limit: 20, offset: 0)
bank.entities.get('ent-1')

bank.operations.list(status: 'pending', limit: 20, offset: 0)
bank.operations.get('op-123')
bank.operations.wait(%w[op-123 op-456], interval: 0.5, timeout: 60, backoff: :exponential, max_interval: 30)
bank.operations.cancel('op-123')

bank.observations.delete
bank.observations.delete_for_memory('mem-1')

bank.tags.list(q: 'user:*', limit: 100, offset: 0)

bank.config.get
bank.config.patch(updates: { llm_model: 'gpt-4.1' })
bank.config.reset

bank.graph.get(type: 'world', q: 'alice', tags: ['team'], tags_match: 'all_strict', limit: 100)

Error Handling

All gem errors inherit from Hindsight::Error.

  • Hindsight::ValidationError
  • Hindsight::ConnectionError
  • Hindsight::TimeoutError
  • Hindsight::APIError (status, body, retriable?)
  • Hindsight::FeatureNotSupported

APIError#message includes status context; use e.body for full server payload.

Example:

begin
  bank.retain('hello')
rescue Hindsight::APIError => e
  warn "status=#{e.status} retriable=#{e.retriable?} body=#{e.body.inspect}"
end

Type Objects

  • Hindsight::Types::Fact
  • Hindsight::Types::RecallResult
  • Hindsight::Types::Reflection
  • Hindsight::Types::OperationReceipt
  • Hindsight::Types::OperationStatus

Notes:

  • Fact#type is optional; when the API payload omits type, it is nil.
  • Type parsers normalize hash keys internally and read API fields using string-key conventions.

Compatibility Notes

  • Requires Ruby >= 3.1
  • Uses Faraday 2.x and faraday-multipart

Development

bundle install
bundle exec rspec