-
-
Notifications
You must be signed in to change notification settings - Fork 3.7k
/
event_formatting_agent.rb
217 lines (172 loc) · 6.87 KB
/
event_formatting_agent.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
module Agents
class EventFormattingAgent < Agent
cannot_be_scheduled!
can_dry_run!
description <<~MD
The Event Formatting Agent allows you to format incoming Events, adding new fields as needed.
For example, here is a possible Event:
{
"high": {
"celsius": "18",
"fahreinheit": "64"
},
"date": {
"epoch": "1357959600",
"pretty": "10:00 PM EST on January 11, 2013"
},
"conditions": "Rain showers",
"data": "This is some data"
}
You may want to send this event to another Agent, for example a Twilio Agent, which expects a `message` key.
You can use an Event Formatting Agent's `instructions` setting to do this in the following way:
"instructions": {
"message": "Today's conditions look like {{conditions}} with a high temperature of {{high.celsius}} degrees Celsius.",
"subject": "{{data}}",
"created_at": "{{created_at}}"
}
Names here like `conditions`, `high` and `data` refer to the corresponding values in the Event hash.
The special key `created_at` refers to the timestamp of the Event, which can be reformatted by the `date` filter, like `{{created_at | date:"at %I:%M %p" }}`.
The upstream agent of each received event is accessible via the key `agent`, which has the following attributes: #{''.tap { |s| s << Agent::Drop.instance_methods(false).map { |m| "`#{m}`" }.join(', ') }}.
Have a look at the [Wiki](https://github.com/huginn/huginn/wiki/Formatting-Events-using-Liquid) to learn more about liquid templating.
Events generated by this possible Event Formatting Agent will look like:
{
"message": "Today's conditions look like Rain showers with a high temperature of 18 degrees Celsius.",
"subject": "This is some data"
}
In `matchers` setting you can perform regular expression matching against contents of events and expand the match data for use in `instructions` setting. Here is an example:
{
"matchers": [
{
"path": "{{date.pretty}}",
"regexp": "\\A(?<time>\\d\\d:\\d\\d [AP]M [A-Z]+)",
"to": "pretty_date"
}
]
}
This virtually merges the following hash into the original event hash:
"pretty_date": {
"time": "10:00 PM EST",
"0": "10:00 PM EST on January 11, 2013"
"1": "10:00 PM EST"
}
You could also use the `regex_extract` filter to achieve the same goal.
So you can use it in `instructions` like this:
"instructions": {
"message": "Today's conditions look like {{conditions}} with a high temperature of {{high.celsius}} degrees Celsius according to the forecast at {{pretty_date.time}}.",
"subject": "{{data}}"
}
If you want to retain original contents of events and only add new keys, then set `mode` to `merge`, otherwise set it to `clean`.
To CGI escape output (for example when creating a link), use the Liquid `uri_escape` filter, like so:
{
"message": "A peak was on Twitter in {{group_by}}. Search: https://twitter.com/search?q={{group_by | uri_escape}}"
}
MD
event_description do
"Events will have the following fields%s:\n\n %s" % [
case options['mode'].to_s
when 'merge'
', merged with the original contents'
when /\{/
', conditionally merged with the original contents'
end,
Utils.pretty_print(Hash[options['instructions'].keys.map { |key|
[key, "..."]
}])
]
end
def validate_options
errors.add(:base,
"instructions and mode need to be present.") unless options['instructions'].present? && options['mode'].present?
if options['mode'].present? && !options['mode'].to_s.include?('{{') && !%(clean merge).include?(options['mode'].to_s)
errors.add(:base, "mode must be 'clean' or 'merge'")
end
validate_matchers
end
def default_options
{
'instructions' => {
'message' => "You received a text {{text}} from {{fields.from}}",
'agent' => "{{agent.type}}",
'some_other_field' => "Looks like the weather is going to be {{fields.weather}}"
},
'mode' => "clean",
}
end
def working?
!recent_error_logs?
end
def receive(incoming_events)
matchers = compiled_matchers
incoming_events.each do |event|
interpolate_with(event) do
apply_compiled_matchers(matchers, event) do
formatted_event = interpolated['mode'].to_s == "merge" ? event.payload.dup : {}
formatted_event.merge! interpolated['instructions']
create_event payload: formatted_event
end
end
end
end
private
def validate_matchers
matchers = options['matchers'] or return
unless matchers.is_a?(Array)
errors.add(:base, "matchers must be an array if present")
return
end
matchers.each do |matcher|
unless matcher.is_a?(Hash)
errors.add(:base, "each matcher must be a hash")
next
end
regexp, path, to = matcher.values_at(*%w[regexp path to])
if regexp.present?
begin
Regexp.new(regexp)
rescue StandardError
errors.add(:base, "bad regexp found in matchers: #{regexp}")
end
else
errors.add(:base, "regexp is mandatory for a matcher and must be a string")
end
errors.add(:base, "path is mandatory for a matcher and must be a string") if !path.present?
errors.add(:base, "to must be a string if present in a matcher") if to.present? && !to.is_a?(String)
end
end
def compiled_matchers
if matchers = options['matchers']
matchers.map { |matcher|
regexp, path, to = matcher.values_at(*%w[regexp path to])
[Regexp.new(regexp), path, to]
}
end
end
def apply_compiled_matchers(matchers, event, &block)
return yield if matchers.nil?
# event.payload.dup does not work; HashWithIndifferentAccess is
# a source of trouble here.
hash = {}.update(event.payload)
matchers.each do |re, path, to|
m = re.match(interpolate_string(path, hash)) or next
mhash =
if to
case value = hash[to]
when Hash
value
else
hash[to] = {}
end
else
hash
end
m.size.times do |i|
mhash[i.to_s] = m[i]
end
m.names.each do |name|
mhash[name] = m[name]
end
end
interpolate_with(hash, &block)
end
end
end