app/models/agents/key_value_store_agent.rb
# frozen_string_literal: true
module Agents
class KeyValueStoreAgent < Agent
can_control_other_agents!
cannot_be_scheduled!
cannot_create_events!
description <<~MD
The Key-Value Store Agent is a data storage that keeps an associative array in its memory. It receives events to store values and provides the data to other agents as an object via Liquid Templating.
Liquid templates specified in the `key` and `value` options are evaluated for each received event to be stored in the memory.
The `variable` option specifies the name by which agents that use the storage refers to the data in Liquid templating.
The `max_keys` option specifies up to how many keys to keep in the storage. When the number of keys goes beyond this, the oldest key-value pair gets removed. (default: 100)
### Storing data
For example, say your agent receives these incoming events:
{
"city": "Tokyo",
"weather": "cloudy"
}
{
"city": "Osaka",
"weather": "sunny"
}
Then you could configure the agent with `{ "key": "{{ city }}", "value": "{{ weather }}" }` to get the following data stored:
{
"Tokyo": "cloudy",
"Osaka": "sunny"
}
Here are some specifications:
- Keys are always stringified as mandated by the JSON format.
- Values are stringified by default. Use the `as_object` filter to store non-string values.
- If the key is evaluated to an empty string, the event is ignored.
- If the value is evaluated to either `null` or empty (`""`, `[]`, `{}`) the key gets deleted.
- In the `value` template, the existing value (if any) can be accessed via the variable `_value_`.
- In the `key` and `value` templates, the whole event payload can be accessed via the variable `_event_`.
### Extracting data
To allow other agents to use the data of a Key-Value Store Agent, designate the agent as a controller.
You can do that by adding those agents to the "Controller targets" of the agent.
The target agents can refer to the storage via the variable specified by the `variable` option value. So, if the store agent in the above example had an option `"variable": "weather"`, they can say something like `{{ weather[city] | default: "unknown" }}` in their templates to get the weather of a city stored in the variable `city`.
MD
def validate_options
options[:key].is_a?(String) or
errors.add(:base, "key is required and must be a string.")
options[:value] or
errors.add(:base, "value is required.")
/\A(?!\d)\w+\z/ === options[:variable] or
errors.add(:base, "variable is required and must be valid as a variable name.")
max_keys > 0 or
errors.add(:base, "max_keys must be a positive number.")
end
def default_options
{
'key' => '{{ id }}',
'value' => '{{ _event_ | as_object }}',
'variable' => 'var',
}
end
def working?
!recent_error_logs?
end
def control_action
'provide'
end
def max_keys
if value = options[:max_keys].presence
value.to_i
else
100
end
end
def receive(incoming_events)
max_keys = max_keys()
incoming_events.each do |event|
interpolate_with(event) do
interpolation_context.stack do
interpolation_context['_event_'] = event.payload
key = interpolate_options(options)['key'].to_s
next if key.empty?
storage = memory
interpolation_context['_value_'] = storage.delete(key)
value = interpolate_options(options)['value']
if value.nil? || value.try(:empty?)
storage.delete(key)
else
storage[key] = value
storage.shift while storage.size > max_keys
end
update!(memory: storage)
end
end
end
end
end
end