socrates 0.1.1 → 0.1.2
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 +4 -4
- data/lib/socrates/adapters/console_adapter.rb +43 -24
- data/lib/socrates/adapters/memory_adapter.rb +37 -20
- data/lib/socrates/adapters/slack_adapter.rb +36 -22
- data/lib/socrates/adapters/stubs.rb +7 -0
- data/lib/socrates/bots/cli_bot.rb +2 -5
- data/lib/socrates/bots/slack_bot.rb +1 -1
- data/lib/socrates/config.rb +1 -0
- data/lib/socrates/core/dispatcher.rb +29 -14
- data/lib/socrates/core/state_data.rb +10 -1
- data/lib/socrates/sample_states.rb +16 -2
- data/lib/socrates/version.rb +1 -1
- data/lib/socrates.rb +1 -0
- data/socrates.gemspec +1 -0
- metadata +17 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 18f251a1edba1096bdb4e8e1e93a2f9662100da0
|
4
|
+
data.tar.gz: e24c7497b5ac98e6d9b0c53830ede79a9dbcbee6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ddf938c670c619986a693c7f52e9a0d35304a6757f25a7031f013d55ecdb1cbcc13c75144ba4ea286664e478a127605e6b208fd210d2416e06a6062f27e84442
|
7
|
+
data.tar.gz: 24d70a3a10cc50f47f829f15a7c51ee96f2699ad4c1c4551800a67f66e1513f86b7d1b1749acc7a019671180be2607e6da21914a710ba1e104ab0bb6378f3ef7
|
@@ -1,34 +1,53 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
module Socrates
|
2
|
+
module Adapters
|
3
|
+
class ConsoleAdapter
|
4
|
+
CLIENT_ID = "CONSOLE"
|
3
5
|
|
4
|
-
|
5
|
-
@name = name
|
6
|
-
end
|
6
|
+
attr_accessor :email, :users
|
7
7
|
|
8
|
-
|
9
|
-
|
10
|
-
|
8
|
+
def initialize(name: "@socrates")
|
9
|
+
@name = name
|
10
|
+
@users = []
|
11
|
+
end
|
11
12
|
|
12
|
-
|
13
|
-
|
14
|
-
|
13
|
+
def client_id_from_context(_context)
|
14
|
+
CLIENT_ID
|
15
|
+
end
|
15
16
|
|
16
|
-
|
17
|
-
|
18
|
-
if user.respond_to?(:name)
|
19
|
-
user.name
|
20
|
-
elsif user.respond_to?(:id)
|
21
|
-
user.id
|
22
|
-
else
|
23
|
-
user
|
17
|
+
def send_message(message, *)
|
18
|
+
puts "\n#{colorize(@name, "32;1")}: #{message}"
|
24
19
|
end
|
25
20
|
|
26
|
-
|
27
|
-
|
21
|
+
def send_direct_message(message, user, *)
|
22
|
+
name =
|
23
|
+
if user.respond_to?(:name)
|
24
|
+
user.name
|
25
|
+
elsif user.respond_to?(:id)
|
26
|
+
user.id
|
27
|
+
else
|
28
|
+
user
|
29
|
+
end
|
28
30
|
|
29
|
-
|
31
|
+
puts "\n[DM] #{colorize(name, "34;1")}: #{message}"
|
32
|
+
end
|
30
33
|
|
31
|
-
|
32
|
-
|
34
|
+
def add_user(id: nil, name: nil, first: nil, last: nil, email: nil)
|
35
|
+
users << User.new(id, name, Profile.new(first, last, email))
|
36
|
+
end
|
37
|
+
|
38
|
+
def users_list
|
39
|
+
Response.new(users)
|
40
|
+
end
|
41
|
+
|
42
|
+
def lookup_email(*)
|
43
|
+
email
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def colorize(str, color_code)
|
49
|
+
"\e[#{color_code}m#{str}\e[0m"
|
50
|
+
end
|
51
|
+
end
|
33
52
|
end
|
34
53
|
end
|
@@ -1,29 +1,46 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
module Socrates
|
2
|
+
module Adapters
|
3
|
+
class MemoryAdapter
|
4
|
+
CLIENT_ID = "MEMORY"
|
3
5
|
|
4
|
-
|
6
|
+
attr_reader :history, :dms
|
7
|
+
attr_accessor :email, :users
|
5
8
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
9
|
+
def initialize
|
10
|
+
@history = []
|
11
|
+
@dms = Hash.new { |hash, key| hash[key] = [] }
|
12
|
+
@users = []
|
13
|
+
end
|
10
14
|
|
11
|
-
|
12
|
-
|
13
|
-
|
15
|
+
def client_id_from_context(_context)
|
16
|
+
CLIENT_ID
|
17
|
+
end
|
14
18
|
|
15
|
-
|
16
|
-
|
17
|
-
|
19
|
+
def send_message(message, *)
|
20
|
+
@history << message
|
21
|
+
end
|
18
22
|
|
19
|
-
|
20
|
-
|
23
|
+
def send_direct_message(message, user, *)
|
24
|
+
user = user.id if user.respond_to?(:id)
|
21
25
|
|
22
|
-
|
23
|
-
|
24
|
-
|
26
|
+
@dms[user] << message
|
27
|
+
end
|
28
|
+
|
29
|
+
def last_message
|
30
|
+
@history.last
|
31
|
+
end
|
32
|
+
|
33
|
+
def add_user(id: nil, name: nil, first: nil, last: nil, email: nil)
|
34
|
+
users << User.new(id, name, Profile.new(first, last, email))
|
35
|
+
end
|
36
|
+
|
37
|
+
def users_list
|
38
|
+
Response.new(users)
|
39
|
+
end
|
25
40
|
|
26
|
-
|
27
|
-
|
41
|
+
def lookup_email(*)
|
42
|
+
email
|
43
|
+
end
|
44
|
+
end
|
28
45
|
end
|
29
46
|
end
|
@@ -1,33 +1,47 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
1
|
+
module Socrates
|
2
|
+
module Adapters
|
3
|
+
class SlackAdapter
|
4
|
+
def initialize(slack_real_time_client)
|
5
|
+
@slack_real_time_client = slack_real_time_client
|
6
|
+
end
|
5
7
|
|
6
|
-
|
7
|
-
|
8
|
-
|
8
|
+
def client_id_from_context(context)
|
9
|
+
context&.user
|
10
|
+
end
|
9
11
|
|
10
|
-
|
11
|
-
|
12
|
-
|
12
|
+
def send_message(message, context:)
|
13
|
+
@slack_real_time_client.message(text: message, channel: context.channel)
|
14
|
+
end
|
13
15
|
|
14
|
-
|
15
|
-
|
16
|
+
def send_direct_message(message, user, *)
|
17
|
+
user = user.id if user.respond_to?(:id)
|
16
18
|
|
17
|
-
|
19
|
+
im_channel = lookup_im_channel(user)
|
18
20
|
|
19
|
-
|
20
|
-
|
21
|
+
@slack_real_time_client.message(text: message, channel: im_channel)
|
22
|
+
end
|
23
|
+
|
24
|
+
def users_list
|
25
|
+
client = @slack_real_time_client.web_client
|
26
|
+
client.users_list
|
27
|
+
end
|
28
|
+
|
29
|
+
def lookup_email(context:)
|
30
|
+
client = @slack_real_time_client.web_client
|
31
|
+
client.users_info(user: context.user)
|
32
|
+
end
|
21
33
|
|
22
|
-
|
34
|
+
private
|
23
35
|
|
24
|
-
|
25
|
-
|
36
|
+
def lookup_im_channel(user)
|
37
|
+
im = @slack_real_time_client.ims.values.find { |i| i.user == user }
|
26
38
|
|
27
|
-
|
39
|
+
return im if im.present?
|
28
40
|
|
29
|
-
|
30
|
-
|
31
|
-
|
41
|
+
# Start a new conversation with this user.
|
42
|
+
response = @slack_real_time_client.web_client.im_open(user: user.id)
|
43
|
+
response.channel.id
|
44
|
+
end
|
45
|
+
end
|
32
46
|
end
|
33
47
|
end
|
@@ -1,15 +1,12 @@
|
|
1
1
|
module Socrates
|
2
2
|
module Bots
|
3
3
|
class CLIBot
|
4
|
-
def initialize(state_factory:)
|
5
|
-
@adapter = ConsoleAdapter.new
|
4
|
+
def initialize(adapter:, state_factory:)
|
5
|
+
@adapter = adapter || Adapters::ConsoleAdapter.new
|
6
6
|
@dispatcher = Core::Dispatcher.new(adapter: @adapter, state_factory: state_factory)
|
7
7
|
end
|
8
8
|
|
9
9
|
def start
|
10
|
-
# Clear out any remnants from previous runs.
|
11
|
-
@storage.clear(ConsoleAdapter::CLIENT_ID)
|
12
|
-
|
13
10
|
while (input = gets.chomp)
|
14
11
|
@dispatcher.dispatch(message: input)
|
15
12
|
end
|
@@ -13,7 +13,7 @@ module Socrates
|
|
13
13
|
end
|
14
14
|
|
15
15
|
@slack_client = Slack::RealTime::Client.new
|
16
|
-
@adapter = SlackAdapter.new(@slack_client)
|
16
|
+
@adapter = Adapters::SlackAdapter.new(@slack_client)
|
17
17
|
@dispatcher = Core::Dispatcher.new(adapter: @adapter, state_factory: state_factory)
|
18
18
|
end
|
19
19
|
|
data/lib/socrates/config.rb
CHANGED
@@ -63,34 +63,51 @@ module Socrates
|
|
63
63
|
|
64
64
|
DEFAULT_ERROR_MESSAGE = "Sorry, an error occurred. We'll have to start over..."
|
65
65
|
|
66
|
+
# rubocop:disable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
|
66
67
|
def fetch_snapshot(client_id)
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
@logger.warn e
|
75
|
-
end
|
68
|
+
if @storage.has_key?(client_id)
|
69
|
+
begin
|
70
|
+
snapshot = @storage.get(client_id)
|
71
|
+
state_data = StateData.deserialize(snapshot)
|
72
|
+
rescue => e
|
73
|
+
@logger.warn "Error while fetching snapshot for client id '#{client_id}', resetting state: #{e.message}"
|
74
|
+
@logger.warn e
|
76
75
|
end
|
76
|
+
end
|
77
77
|
|
78
78
|
state_data ||= StateData.new
|
79
79
|
|
80
80
|
# If the current state is nil or END_OF_CONVERSATION, set it to the default state, which is typically a state
|
81
81
|
# that waits for an initial command or input from the user (e.g. help, start, etc).
|
82
82
|
if state_data.state_id.nil? || state_data.state_id == State::END_OF_CONVERSATION
|
83
|
-
|
84
|
-
|
83
|
+
default_state, default_action = @state_factory.default
|
84
|
+
|
85
|
+
state_data.state_id = default_state
|
86
|
+
state_data.state_action = default_action || :listen
|
87
|
+
|
88
|
+
# Check to see if the last interation was too long ago.
|
89
|
+
elsif state_data_expired?(state_data) && @state_factory.expired(state_data).present?
|
90
|
+
expired_state, expired_action = @state_factory.expired(state_data)
|
91
|
+
|
92
|
+
state_data.state_id = expired_state
|
93
|
+
state_data.state_action = expired_action || :ask
|
85
94
|
end
|
86
95
|
|
87
96
|
state_data
|
88
97
|
end
|
98
|
+
# rubocop:enable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
|
89
99
|
|
90
100
|
def persist_snapshot(client_id, state_data)
|
101
|
+
state_data.reset_elapsed_time
|
91
102
|
@storage.put(client_id, state_data.serialize)
|
92
103
|
end
|
93
104
|
|
105
|
+
def state_data_expired?(state_data)
|
106
|
+
return unless state_data.timestamp.present?
|
107
|
+
|
108
|
+
state_data.elapsed_time > (Config.expired_timeout || 30.minutes)
|
109
|
+
end
|
110
|
+
|
94
111
|
def instantiate_state(state_data, context)
|
95
112
|
@state_factory.build(state_data: state_data, adapter: @adapter, context: context)
|
96
113
|
end
|
@@ -100,9 +117,7 @@ module Socrates
|
|
100
117
|
return true if state.data.state_action == :listen
|
101
118
|
|
102
119
|
# Stop transitioning if there's no state to transition to, or the conversation has ended.
|
103
|
-
|
104
|
-
|
105
|
-
false
|
120
|
+
state.data.state_id.nil? || state.data.state_id == State::END_OF_CONVERSATION
|
106
121
|
end
|
107
122
|
|
108
123
|
def handle_action_error(e, client_id, state, context)
|
@@ -6,15 +6,24 @@ require "yaml"
|
|
6
6
|
module Socrates
|
7
7
|
module Core
|
8
8
|
class StateData
|
9
|
-
attr_accessor :state_id, :state_action
|
9
|
+
attr_accessor :state_id, :state_action, :timestamp
|
10
10
|
|
11
11
|
def initialize(state_id: nil, state_action: nil, data: {}, temporary_keys: [])
|
12
12
|
@state_id = state_id
|
13
13
|
@state_action = state_action
|
14
|
+
@timestamp = Time.current
|
14
15
|
@data = data
|
15
16
|
@temporary_keys = Set.new(temporary_keys)
|
16
17
|
end
|
17
18
|
|
19
|
+
def elapsed_time
|
20
|
+
Time.current - @timestamp
|
21
|
+
end
|
22
|
+
|
23
|
+
def reset_elapsed_time
|
24
|
+
@timestamp = Time.current
|
25
|
+
end
|
26
|
+
|
18
27
|
def keys
|
19
28
|
@data.keys
|
20
29
|
end
|
@@ -5,10 +5,14 @@ require "socrates/core/state"
|
|
5
5
|
module Socrates
|
6
6
|
module SampleStates
|
7
7
|
class StateFactory
|
8
|
-
def
|
8
|
+
def default
|
9
9
|
:get_started
|
10
10
|
end
|
11
11
|
|
12
|
+
def expired(*)
|
13
|
+
:expired
|
14
|
+
end
|
15
|
+
|
12
16
|
def build(state_data:, adapter:, context: nil)
|
13
17
|
classname = StringHelpers.underscore_to_classname(state_data.state_id)
|
14
18
|
|
@@ -21,7 +25,7 @@ module Socrates
|
|
21
25
|
include Socrates::Core::State
|
22
26
|
|
23
27
|
def listen(message)
|
24
|
-
case message.
|
28
|
+
case message.downcase
|
25
29
|
when "help"
|
26
30
|
transition_to :help
|
27
31
|
when "age"
|
@@ -62,6 +66,16 @@ module Socrates
|
|
62
66
|
end
|
63
67
|
end
|
64
68
|
|
69
|
+
class Expired
|
70
|
+
include Socrates::Core::State
|
71
|
+
|
72
|
+
def ask
|
73
|
+
respond message: "I've forgotten what we're talking about, let's start over."
|
74
|
+
|
75
|
+
transition_to :help
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
65
79
|
class AskForName
|
66
80
|
include Socrates::Core::State
|
67
81
|
|
data/lib/socrates/version.rb
CHANGED
data/lib/socrates.rb
CHANGED
@@ -6,6 +6,7 @@ require "socrates/string_helpers"
|
|
6
6
|
require "socrates/adapters/console_adapter"
|
7
7
|
require "socrates/adapters/memory_adapter"
|
8
8
|
require "socrates/adapters/slack_adapter"
|
9
|
+
require "socrates/adapters/stubs"
|
9
10
|
require "socrates/storage/storage"
|
10
11
|
require "socrates/core/state_data"
|
11
12
|
require "socrates/core/state"
|
data/socrates.gemspec
CHANGED
@@ -29,6 +29,7 @@ Gem::Specification.new do |spec|
|
|
29
29
|
spec.add_development_dependency "rake", "~> 10.0"
|
30
30
|
spec.add_development_dependency "rspec", "~> 3.5"
|
31
31
|
spec.add_development_dependency "rubocop", "~> 0.48.1"
|
32
|
+
spec.add_development_dependency "timecop", "~> 0.8.1"
|
32
33
|
|
33
34
|
spec.add_dependency "activesupport", ">= 5.0.2"
|
34
35
|
spec.add_dependency "celluloid-io", ">= 0.17.3"
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: socrates
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Christian Nelson
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-04-
|
11
|
+
date: 2017-04-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -66,6 +66,20 @@ dependencies:
|
|
66
66
|
- - "~>"
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: 0.48.1
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: timecop
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 0.8.1
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 0.8.1
|
69
83
|
- !ruby/object:Gem::Dependency
|
70
84
|
name: activesupport
|
71
85
|
requirement: !ruby/object:Gem::Requirement
|
@@ -159,6 +173,7 @@ files:
|
|
159
173
|
- lib/socrates/adapters/console_adapter.rb
|
160
174
|
- lib/socrates/adapters/memory_adapter.rb
|
161
175
|
- lib/socrates/adapters/slack_adapter.rb
|
176
|
+
- lib/socrates/adapters/stubs.rb
|
162
177
|
- lib/socrates/bots/cli_bot.rb
|
163
178
|
- lib/socrates/bots/slack_bot.rb
|
164
179
|
- lib/socrates/config.rb
|