Class: Vangrail::Tools

Inherits:
Object
  • Object
show all
Defined in:
lib/vangrail/tools.rb

Overview

Named callables a Conversation may invoke, and only after Admission says so. The handler never sees a request that was not granted.

tools = Tools.new
tools.register(:cite) { |args, convo| ... }
convo = Conversation.new(engine, allow: { cite: %i[data] }, tools: tools)
convo.ask('Cite the partition table.')
convo.invoke(:cite, arguments: page)

Defined Under Namespace

Classes: Entry, Transaction

Instance Method Summary collapse

Constructor Details

#initialize(handlers = {}) ⇒ Tools

Returns a new instance of Tools.



46
47
48
49
# File 'lib/vangrail/tools.rb', line 46

def initialize(handlers = {})
  @handlers = {}
  handlers.each { |name, fn| register(name, fn) }
end

Instance Method Details

#call(_name, _arguments, _conversation) ⇒ Object

Raises:



93
94
95
# File 'lib/vangrail/tools.rb', line 93

def call(_name, _arguments, _conversation)
  raise PrivilegeError, 'use Conversation#invoke'
end

#dispatch(name, arguments, conversation, authorization:, transaction: nil) ⇒ Object



119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'lib/vangrail/tools.rb', line 119

def dispatch(name, arguments, conversation, authorization:, transaction: nil)
  monitor = conversation&.monitor
  unless authorization&.call&.tool == name.to_sym && monitor&.claim(authorization)
    raise PrivilegeError, 'tool dispatch requires a live authorization'
  end

  value = if transaction
            commit_transaction(name, transaction, conversation)
          else
            fire(name, arguments, conversation)
          end
  label_output(name, value, authorization.call)
rescue StandardError
  rollback_transaction(transaction, conversation) if transaction&.prepared?
  raise
end

#dupObject



136
137
138
139
140
141
142
143
144
145
146
# File 'lib/vangrail/tools.rb', line 136

def dup
  copy = self.class.new
  @handlers.each do |name, entry|
    if entry.transaction
      copy.register_transactional(name, output: entry.output, **entry.transaction.to_h)
    else
      copy.register(name, entry.handler, readonly: entry.readonly, output: entry.output)
    end
  end
  copy
end

#key?(name) ⇒ Boolean

Returns:

  • (Boolean)


77
78
79
# File 'lib/vangrail/tools.rb', line 77

def key?(name)
  @handlers.key?(name.to_sym)
end

#namesObject



85
86
87
# File 'lib/vangrail/tools.rb', line 85

def names
  @handlers.keys
end

#prepare_transaction(name, arguments, conversation, call:) ⇒ Object



97
98
99
100
101
102
103
104
105
106
107
108
# File 'lib/vangrail/tools.rb', line 97

def prepare_transaction(name, arguments, conversation, call:)
  entry = @handlers[name.to_sym]
  return nil unless call.transaction? && entry&.transaction

  payload = entry.transaction.prepare.call(arguments, conversation)
  PreparedTransaction.new(call, payload).tap do |prepared|
    conversation.monitor.prepare_transaction(call, prepared)
  end
rescue StandardError
  entry.transaction.rollback.call(payload, conversation) if defined?(payload) && entry&.transaction
  raise
end

#readonly?(name) ⇒ Boolean

Returns:

  • (Boolean)


81
82
83
# File 'lib/vangrail/tools.rb', line 81

def readonly?(name)
  !!@handlers[name.to_sym]&.readonly
end

#register(name, callable = nil, readonly: false, output: Origin.tool, &block) ⇒ Object

Raises:

  • (ArgumentError)


51
52
53
54
55
56
57
58
# File 'lib/vangrail/tools.rb', line 51

def register(name, callable = nil, readonly: false, output: Origin.tool, &block)
  fn = callable || block
  raise ArgumentError, "tool #{name} needs a callable" unless fn.respond_to?(:call)

  output_label = output.is_a?(Label) ? output : Label.new(provenance: output)
  @handlers[name.to_sym] = Entry.new(handler: fn, readonly: readonly, output: output_label)
  self
end

#register_transactional(name, prepare:, commit:, rollback:, output: Origin.tool) ⇒ Object



60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# File 'lib/vangrail/tools.rb', line 60

def register_transactional(name, prepare:, commit:, rollback:, output: Origin.tool)
  callbacks = { prepare: prepare, commit: commit, rollback: rollback }
  callbacks.each do |phase, callback|
    raise ArgumentError, "transaction #{phase} needs a callable" unless callback.respond_to?(:call)
  end

  output_label = output.is_a?(Label) ? output : Label.new(provenance: output)
  contract = Transaction.new(**callbacks).freeze
  @handlers[name.to_sym] = Entry.new(
    handler: nil,
    readonly: false,
    output: output_label,
    transaction: contract,
  )
  self
end

#rollback_transaction(prepared, conversation) ⇒ Object



110
111
112
113
114
115
116
117
# File 'lib/vangrail/tools.rb', line 110

def rollback_transaction(prepared, conversation)
  return unless prepared&.prepared?

  entry = @handlers.fetch(prepared.tool)
  entry.transaction.rollback.call(prepared.payload, conversation)
  prepared.rollback!
  conversation.monitor.finish_transaction(prepared, committed: false)
end

#to_hObject



148
149
150
# File 'lib/vangrail/tools.rb', line 148

def to_h
  @handlers.transform_values(&:handler)
end

#transactional?(name) ⇒ Boolean

Returns:

  • (Boolean)


89
90
91
# File 'lib/vangrail/tools.rb', line 89

def transactional?(name)
  !@handlers[name.to_sym]&.transaction.nil?
end