socrates 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/.rspec +1 -0
- data/.rubocop.yml +5 -0
- data/.ruby-version +1 -0
- data/Gemfile +4 -0
- data/README.md +160 -0
- data/Rakefile +8 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/circle.yml +3 -0
- data/exe/socrates +12 -0
- data/lib/socrates/adapters/console_adapter.rb +34 -0
- data/lib/socrates/adapters/memory_adapter.rb +29 -0
- data/lib/socrates/adapters/slack_adapter.rb +33 -0
- data/lib/socrates/bots/cli_bot.rb +20 -0
- data/lib/socrates/bots/slack_bot.rb +35 -0
- data/lib/socrates/config.rb +19 -0
- data/lib/socrates/core/dispatcher.rb +121 -0
- data/lib/socrates/core/state.rb +111 -0
- data/lib/socrates/core/state_data.rb +78 -0
- data/lib/socrates/logger.rb +13 -0
- data/lib/socrates/sample_states.rb +161 -0
- data/lib/socrates/storage/storage.rb +50 -0
- data/lib/socrates/string_helpers.rb +23 -0
- data/lib/socrates/version.rb +3 -0
- data/lib/socrates.rb +19 -0
- data/sample-conversation-console.gif +0 -0
- data/socrates.gemspec +38 -0
- metadata +199 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: da36c4903bca86d379dd6bbf2163f325b71fb852
|
4
|
+
data.tar.gz: ebd41affe29ae7b51ae5cfae2f348779b675d968
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: de3be228b5f26e6c4c2f33d10125f852f8fee4b2a943c5a7eebcfdba8ce18fd0d66498138c86222d438707f0f752f19b3d4a23da063da97b61ca705e3af20dc2
|
7
|
+
data.tar.gz: 9c5dc64b695f7a767f98461ee984c6bec32cc9bb3ebf8e8e25eeac95f7c70c12e67d8bbae59953e6c1eb1c3cec76b9e19b04f82873a29498651d5f7cc04e9679
|
data/.gitignore
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
/.bundle/
|
2
|
+
/.yardoc
|
3
|
+
/Gemfile.lock
|
4
|
+
/_yardoc/
|
5
|
+
/coverage/
|
6
|
+
/doc/
|
7
|
+
/pkg/
|
8
|
+
/spec/reports/
|
9
|
+
/tmp/
|
10
|
+
/.rubocop-https---raw-githubusercontent-com-carbonfive-c5-conventions-master-rubocop-rubocop-yml
|
11
|
+
/socrates-*.gem
|
12
|
+
|
13
|
+
# rspec failure tracking
|
14
|
+
.rspec_status
|
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/.rubocop.yml
ADDED
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.4.1
|
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,160 @@
|
|
1
|
+
# Socrates
|
2
|
+
|
3
|
+
Socrates is a micro-framework for building conversational interfaces. It provides straight-forward state management, a clear pattern for modeling the states and conversational flow (transitions), and some helpers.
|
4
|
+
|
5
|
+
It's designed for building conversational Slack bots, but is designed in such a way that other adapters could be written. It ships with a Console adapter for testing locally in the terminal as well as a Memory adapter for use in automated tests.
|
6
|
+
|
7
|
+
*Disclaimer: This framework is currently experimental and will change.*
|
8
|
+
|
9
|
+
Conceptually, a conversation is a sequence of asking and listening actions. As a conversation progresses, information is gathered and at some point, acted upon by the system.
|
10
|
+
|
11
|
+
Conversational state is captured either in memory (development and testing) or in Redis. This too is pluggable so that other storage backends could be used.
|
12
|
+
|
13
|
+
Here is a simple example, asking for one's name, birth date, and then responding with the current age.
|
14
|
+
|
15
|
+
![Calculate Age](./sample-conversation-console.gif)
|
16
|
+
|
17
|
+
```ruby
|
18
|
+
class GetStarted
|
19
|
+
include Socrates::Core::State
|
20
|
+
|
21
|
+
def listen(message)
|
22
|
+
case message.strip
|
23
|
+
when "help"
|
24
|
+
transition_to :help
|
25
|
+
when "age"
|
26
|
+
transition_to :ask_for_name
|
27
|
+
else
|
28
|
+
transition_to :no_comprende
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class Help
|
34
|
+
include Socrates::Core::State
|
35
|
+
|
36
|
+
def ask
|
37
|
+
respond message: <<~MSG
|
38
|
+
Thanks for asking! I can do these things for you...
|
39
|
+
|
40
|
+
• `age` - Calculate your age from your birth date.
|
41
|
+
• `help` - Tell you what I can do for you.
|
42
|
+
|
43
|
+
So, what shall it be?
|
44
|
+
MSG
|
45
|
+
transition_to :get_started, action: :listen
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
class NoComprende
|
50
|
+
include Socrates::Core::State
|
51
|
+
|
52
|
+
def ask
|
53
|
+
respond message: "Whoops, I don't know what you mean by that. Try `help` to see my commands."
|
54
|
+
transition_to :get_started
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
class AskForName
|
59
|
+
include Socrates::Core::State
|
60
|
+
|
61
|
+
def ask
|
62
|
+
respond message: "First things first, what's your name?"
|
63
|
+
end
|
64
|
+
|
65
|
+
def listen(message)
|
66
|
+
# Transition to the next step while persisting the name for future retrieval.
|
67
|
+
transition_to :ask_for_birth_date, data: { name: message }
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
class AskForBirthDate
|
72
|
+
include Socrates::Core::State
|
73
|
+
|
74
|
+
def ask
|
75
|
+
respond message: "Hi #{first_name}! What's your birth date (e.g. MM/DD/YYYY)?"
|
76
|
+
end
|
77
|
+
|
78
|
+
def listen(message)
|
79
|
+
begin
|
80
|
+
birth_date = Date.strptime(message, "%m/%d/%Y")
|
81
|
+
rescue ArgumentError
|
82
|
+
respond message: "Whoops, I didn't understand that. What's your birth date (e.g. MM/DD/YYYY)?"
|
83
|
+
repeat_action
|
84
|
+
return
|
85
|
+
end
|
86
|
+
# Transition to the next step while persisting the birth date for future retrieval.
|
87
|
+
transition_to :calculate_age, data: { birth_date: birth_date }
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
def first_name
|
93
|
+
@data.get(:name).split.first
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
class CalculateAge
|
98
|
+
include Socrates::Core::State
|
99
|
+
|
100
|
+
def ask
|
101
|
+
respond message: "Got it #{first_name}! So that makes you #{calculate_age} years old."
|
102
|
+
end_conversation
|
103
|
+
end
|
104
|
+
|
105
|
+
private
|
106
|
+
|
107
|
+
def first_name
|
108
|
+
@data.get(:name).split.first
|
109
|
+
end
|
110
|
+
|
111
|
+
def birth_date
|
112
|
+
@data.get(:birth_date)
|
113
|
+
end
|
114
|
+
|
115
|
+
def calculate_age
|
116
|
+
((Date.today.to_time - birth_date.to_time) / 31_536_000).floor
|
117
|
+
end
|
118
|
+
end
|
119
|
+
```
|
120
|
+
|
121
|
+
## Installation
|
122
|
+
|
123
|
+
Add this line to your application's Gemfile:
|
124
|
+
|
125
|
+
```ruby
|
126
|
+
gem 'socrates'
|
127
|
+
```
|
128
|
+
|
129
|
+
And then execute:
|
130
|
+
|
131
|
+
$ bundle
|
132
|
+
|
133
|
+
Or install it yourself as:
|
134
|
+
|
135
|
+
$ gem install socrates
|
136
|
+
|
137
|
+
## Usage
|
138
|
+
|
139
|
+
TODO: Write usage instructions here.
|
140
|
+
|
141
|
+
## Core Concepts
|
142
|
+
|
143
|
+
* Dispatcher
|
144
|
+
* Adapter (Slack, Console, Memory)
|
145
|
+
* Storage (Memory, Redis)
|
146
|
+
* State
|
147
|
+
* Helpers
|
148
|
+
|
149
|
+
TODO: Expand descriptions. Include a diagram.
|
150
|
+
|
151
|
+
## Development
|
152
|
+
|
153
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake` to run the specs and rubocop. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
154
|
+
|
155
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
156
|
+
|
157
|
+
## Contributing
|
158
|
+
|
159
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/carbonfive/socrates.
|
160
|
+
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "socrates"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
data/circle.yml
ADDED
data/exe/socrates
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
File.expand_path("../../lib", __FILE__).tap do |lib|
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
end
|
6
|
+
|
7
|
+
require "socrates"
|
8
|
+
require "socrates/sample_states"
|
9
|
+
|
10
|
+
puts "Type `help` to see what I can do..."
|
11
|
+
|
12
|
+
Socrates::Bots::CLIBot.new(state_factory: Socrates::SampleStates::StateFactory.new).start
|
@@ -0,0 +1,34 @@
|
|
1
|
+
class ConsoleAdapter
|
2
|
+
CLIENT_ID = "CONSOLE"
|
3
|
+
|
4
|
+
def initialize(name: "@socrates")
|
5
|
+
@name = name
|
6
|
+
end
|
7
|
+
|
8
|
+
def client_id_from_context(_context)
|
9
|
+
CLIENT_ID
|
10
|
+
end
|
11
|
+
|
12
|
+
def send_message(message, *)
|
13
|
+
puts "\n#{colorize(@name, "32;1")}: #{message}"
|
14
|
+
end
|
15
|
+
|
16
|
+
def send_direct_message(message, user, *)
|
17
|
+
name =
|
18
|
+
if user.respond_to?(:name)
|
19
|
+
user.name
|
20
|
+
elsif user.respond_to?(:id)
|
21
|
+
user.id
|
22
|
+
else
|
23
|
+
user
|
24
|
+
end
|
25
|
+
|
26
|
+
puts "\n[DM] #{colorize(name, "34;1")}: #{message}"
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def colorize(str, color_code)
|
32
|
+
"\e[#{color_code}m#{str}\e[0m"
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
class MemoryAdapter
|
2
|
+
CLIENT_ID = "MEMORY"
|
3
|
+
|
4
|
+
attr_reader :history, :dms
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@history = []
|
8
|
+
@dms = {}
|
9
|
+
end
|
10
|
+
|
11
|
+
def client_id_from_context(_context)
|
12
|
+
CLIENT_ID
|
13
|
+
end
|
14
|
+
|
15
|
+
def send_message(message, *)
|
16
|
+
@history << message
|
17
|
+
end
|
18
|
+
|
19
|
+
def send_direct_message(message, user, *)
|
20
|
+
user = user.id if user.respond_to?(:id)
|
21
|
+
|
22
|
+
@dms[user] = [] unless @dms.key?(user)
|
23
|
+
@dms[user] << message
|
24
|
+
end
|
25
|
+
|
26
|
+
def last_message
|
27
|
+
@history.last
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
class SlackAdapter
|
2
|
+
def initialize(slack_real_time_client)
|
3
|
+
@slack_real_time_client = slack_real_time_client
|
4
|
+
end
|
5
|
+
|
6
|
+
def client_id_from_context(context)
|
7
|
+
context&.user
|
8
|
+
end
|
9
|
+
|
10
|
+
def send_message(message, context:)
|
11
|
+
@slack_real_time_client.message(text: message, channel: context.channel)
|
12
|
+
end
|
13
|
+
|
14
|
+
def send_direct_message(message, user, *)
|
15
|
+
user = user.id if user.respond_to?(:id)
|
16
|
+
|
17
|
+
im_channel = lookup_im_channel(user)
|
18
|
+
|
19
|
+
@slack_real_time_client.message(text: message, channel: im_channel)
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def lookup_im_channel(user)
|
25
|
+
im = @slack_real_time_client.ims.values.find { |i| i.user == user }
|
26
|
+
|
27
|
+
return im if im.present?
|
28
|
+
|
29
|
+
# Start a new conversation with this user.
|
30
|
+
response = @slack_real_time_client.web_client.im_open(user: user.id)
|
31
|
+
response.channel.id
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Socrates
|
2
|
+
module Bots
|
3
|
+
class CLIBot
|
4
|
+
def initialize(state_factory:)
|
5
|
+
@adapter = ConsoleAdapter.new
|
6
|
+
@storage = Storage::MemoryStorage.new
|
7
|
+
@dispatcher = Core::Dispatcher.new(storage: @storage, adapter: @adapter, state_factory: state_factory)
|
8
|
+
end
|
9
|
+
|
10
|
+
def start
|
11
|
+
# Clear out any remnants from previous runs.
|
12
|
+
@storage.clear(ConsoleAdapter::CLIENT_ID)
|
13
|
+
|
14
|
+
while (input = gets.chomp)
|
15
|
+
@dispatcher.dispatch(message: input)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require "slack-ruby-client"
|
2
|
+
|
3
|
+
module Socrates
|
4
|
+
module Bots
|
5
|
+
class SlackBot
|
6
|
+
def initialize(state_factory:)
|
7
|
+
Slack.configure do |config|
|
8
|
+
config.token = ENV["SLACK_API_TOKEN"]
|
9
|
+
config.logger = Logger.new(STDOUT)
|
10
|
+
config.logger.level = Logger::INFO
|
11
|
+
|
12
|
+
raise "Missing ENV[SLACK_API_TOKEN]!" unless config.token
|
13
|
+
end
|
14
|
+
|
15
|
+
@slack_client = Slack::RealTime::Client.new
|
16
|
+
@adapter = SlackAdapter.new(@slack_client)
|
17
|
+
@storage = Storage::RedisStorage.new
|
18
|
+
@dispatcher = Core::Dispatcher.new(storage: @storage, adapter: @adapter, state_factory: state_factory)
|
19
|
+
end
|
20
|
+
|
21
|
+
def start
|
22
|
+
@slack_client.on :message do |data|
|
23
|
+
# puts "> #{data}"
|
24
|
+
|
25
|
+
# When first connecting, Slack may resend the last message. Ignore it...
|
26
|
+
next if data.reply_to.present?
|
27
|
+
|
28
|
+
@dispatcher.dispatch(message: data.text, context: data)
|
29
|
+
end
|
30
|
+
|
31
|
+
@slack_client.start!
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Socrates
|
2
|
+
module Config
|
3
|
+
extend self
|
4
|
+
|
5
|
+
attr_accessor :view_path
|
6
|
+
attr_accessor :error_message
|
7
|
+
attr_accessor :logger
|
8
|
+
end
|
9
|
+
|
10
|
+
class << self
|
11
|
+
def configure
|
12
|
+
block_given? ? yield(Config) : Config
|
13
|
+
end
|
14
|
+
|
15
|
+
def config
|
16
|
+
Config
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
require "hashie"
|
2
|
+
|
3
|
+
require "socrates/logger"
|
4
|
+
require "socrates/string_helpers"
|
5
|
+
require "socrates/storage/storage"
|
6
|
+
require "socrates/core/state_data"
|
7
|
+
|
8
|
+
module Socrates
|
9
|
+
module Core
|
10
|
+
class Dispatcher
|
11
|
+
def initialize(adapter:, state_factory:, storage: nil)
|
12
|
+
@adapter = adapter
|
13
|
+
@state_factory = state_factory
|
14
|
+
@storage = storage || Storage::MemoryStorage.new
|
15
|
+
|
16
|
+
@logger = Socrates::Config.logger || Socrates::Logger.default
|
17
|
+
@error_message = Socrates::Config.error_message || DEFAULT_ERROR_MESSAGE
|
18
|
+
end
|
19
|
+
|
20
|
+
# rubocop:disable Metrics/AbcSize
|
21
|
+
def dispatch(message:, context: {})
|
22
|
+
message = message.strip
|
23
|
+
|
24
|
+
client_id = @adapter.client_id_from_context(context)
|
25
|
+
|
26
|
+
@logger.info %(#{client_id} recv: "#{message}")
|
27
|
+
|
28
|
+
# In many cases, a two actions will run in this loop: :listen => :ask, but it's possible that a chain of 2 or
|
29
|
+
# more :ask actions could run, before stopping at a :listen (and waiting for the next input).
|
30
|
+
loop do
|
31
|
+
state_data = fetch_snapshot(client_id)
|
32
|
+
state = instantiate_state(state_data, context)
|
33
|
+
|
34
|
+
args = [state.data.state_action]
|
35
|
+
args << message if state.data.state_action == :listen
|
36
|
+
|
37
|
+
msg = %(#{client_id} processing :#{state.data.state_id} / :#{args.first})
|
38
|
+
msg += %( / message: "#{args.second}") if args.count > 1
|
39
|
+
@logger.debug msg
|
40
|
+
|
41
|
+
begin
|
42
|
+
state.send(*args)
|
43
|
+
rescue => e
|
44
|
+
handle_action_error(e, client_id, state, context)
|
45
|
+
return
|
46
|
+
end
|
47
|
+
|
48
|
+
# Update the persisted state data so we know what to run next time.
|
49
|
+
state.data.state_id = state.next_state_id
|
50
|
+
state.data.state_action = state.next_state_action
|
51
|
+
|
52
|
+
@logger.debug %(#{client_id} transition to :#{state.data.state_id} / :#{state.data.state_action})
|
53
|
+
|
54
|
+
persist_snapshot(client_id, state.data)
|
55
|
+
|
56
|
+
# Break from the loop if there's nothing left to do, i.e. no more state transitions.
|
57
|
+
break if done_transitioning?(state)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
# rubocop:enable Metrics/AbcSize
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
DEFAULT_ERROR_MESSAGE = "Sorry, an error occurred. We'll have to start over..."
|
65
|
+
|
66
|
+
def fetch_snapshot(client_id)
|
67
|
+
state_data =
|
68
|
+
if @storage.has_key?(client_id)
|
69
|
+
begin
|
70
|
+
snapshot = @storage.get(client_id)
|
71
|
+
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
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
state_data ||= StateData.new
|
79
|
+
|
80
|
+
# If the current state is nil or END_OF_CONVERSATION, set it to the default state, which is typically a state
|
81
|
+
# that waits for an initial command or input from the user (e.g. help, start, etc).
|
82
|
+
if state_data.state_id.nil? || state_data.state_id == State::END_OF_CONVERSATION
|
83
|
+
state_data.state_id = @state_factory.default_state
|
84
|
+
state_data.state_action = :listen
|
85
|
+
end
|
86
|
+
|
87
|
+
state_data
|
88
|
+
end
|
89
|
+
|
90
|
+
def persist_snapshot(client_id, state_data)
|
91
|
+
@storage.put(client_id, state_data.serialize)
|
92
|
+
end
|
93
|
+
|
94
|
+
def instantiate_state(state_data, context)
|
95
|
+
@state_factory.build(state_data: state_data, adapter: @adapter, context: context)
|
96
|
+
end
|
97
|
+
|
98
|
+
def done_transitioning?(state)
|
99
|
+
# Stop transitioning if we're waiting for the user to respond (i.e. we're listening).
|
100
|
+
return true if state.data.state_action == :listen
|
101
|
+
|
102
|
+
# Stop transitioning if there's no state to transition to, or the conversation has ended.
|
103
|
+
return true if state.data.state_id.nil? || state.data.state_id == State::END_OF_CONVERSATION
|
104
|
+
|
105
|
+
false
|
106
|
+
end
|
107
|
+
|
108
|
+
def handle_action_error(e, client_id, state, context)
|
109
|
+
@logger.warn "Error while processing action #{state.data.state_id}/#{state.data.state_action}: #{e.message}"
|
110
|
+
@logger.warn e
|
111
|
+
|
112
|
+
@adapter.send_message(@error_message, context: context)
|
113
|
+
state.data.clear
|
114
|
+
state.data.state_id = nil
|
115
|
+
state.data.state_action = nil
|
116
|
+
|
117
|
+
persist_snapshot(client_id, state.data)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
require "hashie"
|
2
|
+
require "erb"
|
3
|
+
|
4
|
+
module Socrates
|
5
|
+
module Core
|
6
|
+
module State
|
7
|
+
attr_reader :data, :context
|
8
|
+
|
9
|
+
def initialize(data: StateData.new, adapter:, context: nil)
|
10
|
+
@data = data
|
11
|
+
@adapter = adapter
|
12
|
+
@context = context
|
13
|
+
@next_state_id = nil
|
14
|
+
@next_state_action = nil
|
15
|
+
@logger = Socrates::Config.logger || Socrates::Logger.default
|
16
|
+
end
|
17
|
+
|
18
|
+
def next_state_id
|
19
|
+
if @next_state_id.nil?
|
20
|
+
state_id_from_classname
|
21
|
+
else
|
22
|
+
@next_state_id
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def next_state_action
|
27
|
+
if @next_state_action.nil?
|
28
|
+
next_action(@data.state_action)
|
29
|
+
else
|
30
|
+
@next_state_action
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def respond(message: nil, template: nil)
|
35
|
+
if template
|
36
|
+
# TODO: Partials?
|
37
|
+
filename = File.join(Socrates.config.view_path, template)
|
38
|
+
source = File.read(filename)
|
39
|
+
message = ERB.new(source, 0, "<>").result(binding)
|
40
|
+
end
|
41
|
+
|
42
|
+
return if message.empty?
|
43
|
+
|
44
|
+
@logger.info %(#{client_id} send: "#{format_for_logging(message)}")
|
45
|
+
@adapter.send_message(message, context: @context)
|
46
|
+
end
|
47
|
+
|
48
|
+
def send_message(to:, message:)
|
49
|
+
@logger.info %(#{client_id} send direct to #{to}: "#{format_for_logging(message)}")
|
50
|
+
@adapter.send_direct_message(message, to, context: @context)
|
51
|
+
end
|
52
|
+
|
53
|
+
def transition_to(state_id, action: nil, data: {})
|
54
|
+
if action.nil?
|
55
|
+
action =
|
56
|
+
if state_id.nil?
|
57
|
+
nil
|
58
|
+
elsif state_id == state_id_from_classname
|
59
|
+
next_action(@data.state_action)
|
60
|
+
else
|
61
|
+
:ask
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
@next_state_id = state_id
|
66
|
+
@next_state_action = action
|
67
|
+
|
68
|
+
@data.merge(data)
|
69
|
+
end
|
70
|
+
|
71
|
+
def repeat_action
|
72
|
+
@next_state_id = @data.state_id
|
73
|
+
@next_state_action = @data.state_action
|
74
|
+
end
|
75
|
+
|
76
|
+
def end_conversation
|
77
|
+
@data.clear
|
78
|
+
|
79
|
+
transition_to END_OF_CONVERSATION, action: END_OF_CONVERSATION
|
80
|
+
end
|
81
|
+
|
82
|
+
def ask
|
83
|
+
# stub implementation, to be overwritten.
|
84
|
+
end
|
85
|
+
|
86
|
+
def listen(_message)
|
87
|
+
# stub implementation, to be overwritten.
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
END_OF_CONVERSATION = :__end__
|
93
|
+
|
94
|
+
def next_action(current_action)
|
95
|
+
(%i[ask listen] - [current_action]).first
|
96
|
+
end
|
97
|
+
|
98
|
+
def client_id
|
99
|
+
@adapter.client_id_from_context(@context)
|
100
|
+
end
|
101
|
+
|
102
|
+
def format_for_logging(message)
|
103
|
+
message.gsub("\n", "\\n")
|
104
|
+
end
|
105
|
+
|
106
|
+
def state_id_from_classname
|
107
|
+
StringHelpers.classname_to_underscore(self.class.to_s.split("::").last).to_sym
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
require "hashie"
|
2
|
+
require "json"
|
3
|
+
require "set"
|
4
|
+
require "yaml"
|
5
|
+
|
6
|
+
module Socrates
|
7
|
+
module Core
|
8
|
+
class StateData
|
9
|
+
attr_accessor :state_id, :state_action
|
10
|
+
|
11
|
+
def initialize(state_id: nil, state_action: nil, data: {}, temporary_keys: [])
|
12
|
+
@state_id = state_id
|
13
|
+
@state_action = state_action
|
14
|
+
@data = data
|
15
|
+
@temporary_keys = Set.new(temporary_keys)
|
16
|
+
end
|
17
|
+
|
18
|
+
def keys
|
19
|
+
@data.keys
|
20
|
+
end
|
21
|
+
|
22
|
+
def has_key?(key)
|
23
|
+
@data.has_key?(key)
|
24
|
+
end
|
25
|
+
|
26
|
+
def has_temporary_key?(key)
|
27
|
+
# The !! turns nils into false, which shouldn"t be necessary, but seems to be after the set is loaded from yaml.
|
28
|
+
@temporary_keys.include?(key) == true
|
29
|
+
end
|
30
|
+
|
31
|
+
def get(key, clear: false)
|
32
|
+
value = @data[key]
|
33
|
+
|
34
|
+
if @temporary_keys.include?(key) || clear
|
35
|
+
@temporary_keys.delete(key)
|
36
|
+
@data.delete(key)
|
37
|
+
end
|
38
|
+
|
39
|
+
value
|
40
|
+
end
|
41
|
+
|
42
|
+
def set(key, value)
|
43
|
+
@data[key] = value
|
44
|
+
end
|
45
|
+
|
46
|
+
def set_temporary(key, value)
|
47
|
+
if @data.has_key?(key) && !@temporary_keys.include?(key)
|
48
|
+
raise ArgumentError, "Cannot overrite key '#{key}' with a temporary value."
|
49
|
+
end
|
50
|
+
|
51
|
+
@data[key] = value
|
52
|
+
@temporary_keys << key
|
53
|
+
end
|
54
|
+
|
55
|
+
def merge(other)
|
56
|
+
@data.merge!(other)
|
57
|
+
end
|
58
|
+
|
59
|
+
def clear(key = nil)
|
60
|
+
if key
|
61
|
+
@data.delete(key)
|
62
|
+
@temporary_keys.delete(key)
|
63
|
+
else
|
64
|
+
@data.clear
|
65
|
+
@temporary_keys.clear
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def serialize
|
70
|
+
YAML.dump(self)
|
71
|
+
end
|
72
|
+
|
73
|
+
def self.deserialize(string)
|
74
|
+
YAML.load(string)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,161 @@
|
|
1
|
+
require "date"
|
2
|
+
|
3
|
+
require "socrates/core/state"
|
4
|
+
|
5
|
+
module Socrates
|
6
|
+
module SampleStates
|
7
|
+
class StateFactory
|
8
|
+
def default_state
|
9
|
+
:get_started
|
10
|
+
end
|
11
|
+
|
12
|
+
def build(state_data:, adapter:, context: nil)
|
13
|
+
classname = StringHelpers.underscore_to_classname(state_data.state_id)
|
14
|
+
|
15
|
+
Object.const_get("Socrates::SampleStates::#{classname}")
|
16
|
+
.new(data: state_data, adapter: adapter, context: context)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class GetStarted
|
21
|
+
include Socrates::Core::State
|
22
|
+
|
23
|
+
def listen(message)
|
24
|
+
case message.strip
|
25
|
+
when "help"
|
26
|
+
transition_to :help
|
27
|
+
when "age"
|
28
|
+
transition_to :ask_for_name
|
29
|
+
when "error"
|
30
|
+
transition_to :raise_error
|
31
|
+
else
|
32
|
+
transition_to :no_comprende
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
class Help
|
38
|
+
include Socrates::Core::State
|
39
|
+
|
40
|
+
def ask
|
41
|
+
respond message: <<~MSG
|
42
|
+
Thanks for asking! I can do these things for you...
|
43
|
+
|
44
|
+
• `age` - Calculate your age from your birth date.
|
45
|
+
• `error` - Start a short error path that raises an error.
|
46
|
+
• `help` - Tell you what I can do for you.
|
47
|
+
|
48
|
+
So, what shall it be?
|
49
|
+
MSG
|
50
|
+
|
51
|
+
transition_to :get_started, action: :listen
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
class NoComprende
|
56
|
+
include Socrates::Core::State
|
57
|
+
|
58
|
+
def ask
|
59
|
+
respond message: "Whoops, I don't know what you mean by that. Try `help` to see my commands."
|
60
|
+
|
61
|
+
transition_to :get_started
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
class AskForName
|
66
|
+
include Socrates::Core::State
|
67
|
+
|
68
|
+
def ask
|
69
|
+
respond message: "First things first, what's your name?"
|
70
|
+
end
|
71
|
+
|
72
|
+
def listen(message)
|
73
|
+
transition_to :ask_for_birth_date, data: { name: message }
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
class AskForBirthDate
|
78
|
+
include Socrates::Core::State
|
79
|
+
|
80
|
+
def ask
|
81
|
+
respond message: "Hi #{first_name}! What's your birth date (e.g. MM/DD/YYYY)?"
|
82
|
+
end
|
83
|
+
|
84
|
+
def listen(message)
|
85
|
+
begin
|
86
|
+
birth_date = Date.strptime(message, "%m/%d/%Y")
|
87
|
+
rescue ArgumentError
|
88
|
+
respond message: "Whoops, I didn't understand that. What's your birth date (e.g. MM/DD/YYYY)?"
|
89
|
+
repeat_action
|
90
|
+
return
|
91
|
+
end
|
92
|
+
|
93
|
+
transition_to :calculate_age, data: { birth_date: birth_date }
|
94
|
+
end
|
95
|
+
|
96
|
+
private
|
97
|
+
|
98
|
+
def first_name
|
99
|
+
@data.get(:name).split.first
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
class CalculateAge
|
104
|
+
include Socrates::Core::State
|
105
|
+
|
106
|
+
def ask
|
107
|
+
respond message: "Got it #{first_name}! So that makes you #{calculate_age} years old."
|
108
|
+
|
109
|
+
# Example of a :ask => :ask transition.
|
110
|
+
transition_to :end_conversation_1
|
111
|
+
end
|
112
|
+
|
113
|
+
private
|
114
|
+
|
115
|
+
def first_name
|
116
|
+
@data.get(:name).split.first
|
117
|
+
end
|
118
|
+
|
119
|
+
def birth_date
|
120
|
+
@data.get(:birth_date)
|
121
|
+
end
|
122
|
+
|
123
|
+
def calculate_age
|
124
|
+
((Date.today.to_time - birth_date.to_time) / 31_536_000).floor
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
class EndConversation1
|
129
|
+
include Socrates::Core::State
|
130
|
+
|
131
|
+
def ask
|
132
|
+
respond message: "That's all for now..."
|
133
|
+
|
134
|
+
# Example of another :ask => :ask transition.
|
135
|
+
transition_to :end_conversation_2
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
class EndConversation2
|
140
|
+
include Socrates::Core::State
|
141
|
+
|
142
|
+
def ask
|
143
|
+
respond message: "Type `help` to see what else I can do."
|
144
|
+
|
145
|
+
end_conversation
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
class RaiseError
|
150
|
+
include Socrates::Core::State
|
151
|
+
|
152
|
+
def ask
|
153
|
+
respond message: "I will raise an error regardless of what you enter next..."
|
154
|
+
end
|
155
|
+
|
156
|
+
def listen(_message)
|
157
|
+
raise ArgumentError, "Boom!"
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require "redis"
|
2
|
+
require "json"
|
3
|
+
|
4
|
+
module Socrates
|
5
|
+
module Storage
|
6
|
+
class MemoryStorage
|
7
|
+
def initialize
|
8
|
+
@storage = {}
|
9
|
+
end
|
10
|
+
|
11
|
+
def has_key?(key)
|
12
|
+
@storage.has_key? key
|
13
|
+
end
|
14
|
+
|
15
|
+
def clear(key)
|
16
|
+
@storage.delete key
|
17
|
+
end
|
18
|
+
|
19
|
+
def get(key)
|
20
|
+
@storage[key]
|
21
|
+
end
|
22
|
+
|
23
|
+
def put(key, value)
|
24
|
+
@storage[key] = value
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class RedisStorage
|
29
|
+
def initialize(url: "redis://localhost")
|
30
|
+
@redis = Redis.new(url: url)
|
31
|
+
end
|
32
|
+
|
33
|
+
def has_key?(key)
|
34
|
+
@redis.exists(key)
|
35
|
+
end
|
36
|
+
|
37
|
+
def clear(key)
|
38
|
+
@redis.del key
|
39
|
+
end
|
40
|
+
|
41
|
+
def get(key)
|
42
|
+
@redis.get(key)
|
43
|
+
end
|
44
|
+
|
45
|
+
def put(key, value)
|
46
|
+
@redis.set(key, value)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require "active_support/inflector"
|
2
|
+
|
3
|
+
module StringHelpers
|
4
|
+
def self.underscore_to_classname(underscored_symbol)
|
5
|
+
underscored_symbol.to_s.camelize
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.classname_to_underscore(classname)
|
9
|
+
classname.underscore
|
10
|
+
end
|
11
|
+
|
12
|
+
# Lifted from Rails' text helpers.
|
13
|
+
def self.pluralize(count, singular, plural_arg = nil, plural: plural_arg)
|
14
|
+
word =
|
15
|
+
if count == 1 || count =~ /^1(\.0+)?$/
|
16
|
+
singular
|
17
|
+
else
|
18
|
+
plural || singular.pluralize
|
19
|
+
end
|
20
|
+
|
21
|
+
"#{count || 0} #{word}"
|
22
|
+
end
|
23
|
+
end
|
data/lib/socrates.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
# Socrates
|
2
|
+
require "socrates/version"
|
3
|
+
require "socrates/config"
|
4
|
+
require "socrates/logger"
|
5
|
+
require "socrates/string_helpers"
|
6
|
+
require "socrates/adapters/console_adapter"
|
7
|
+
require "socrates/adapters/memory_adapter"
|
8
|
+
require "socrates/adapters/slack_adapter"
|
9
|
+
require "socrates/storage/storage"
|
10
|
+
require "socrates/core/state_data"
|
11
|
+
require "socrates/core/state"
|
12
|
+
require "socrates/core/dispatcher"
|
13
|
+
|
14
|
+
# Bot implementations
|
15
|
+
require "socrates/bots/cli_bot"
|
16
|
+
require "socrates/bots/slack_bot"
|
17
|
+
|
18
|
+
module Socrates
|
19
|
+
end
|
Binary file
|
data/socrates.gemspec
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
lib = File.expand_path("../lib", __FILE__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
|
6
|
+
require "socrates/version"
|
7
|
+
|
8
|
+
Gem::Specification.new do |spec|
|
9
|
+
spec.name = "socrates"
|
10
|
+
spec.version = Socrates::VERSION
|
11
|
+
spec.license = "MIT"
|
12
|
+
spec.authors = ["Christian Nelson"]
|
13
|
+
spec.email = ["christian@carbonfive.com"]
|
14
|
+
|
15
|
+
spec.summary = "A microframework for building stateful conversational bots."
|
16
|
+
spec.homepage = "https://github.com/carbonfive/socrates"
|
17
|
+
|
18
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
19
|
+
f.match(%r{^(test|spec|features)/})
|
20
|
+
end
|
21
|
+
|
22
|
+
spec.bindir = "exe"
|
23
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
24
|
+
spec.require_paths = ["lib"]
|
25
|
+
|
26
|
+
spec.required_ruby_version = ">= 2.3.0"
|
27
|
+
|
28
|
+
spec.add_development_dependency "bundler", "~> 1.14"
|
29
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
30
|
+
spec.add_development_dependency "rspec", "~> 3.5"
|
31
|
+
spec.add_development_dependency "rubocop", "~> 0.48.1"
|
32
|
+
|
33
|
+
spec.add_dependency "activesupport", ">= 5.0.2"
|
34
|
+
spec.add_dependency "celluloid-io", ">= 0.17.3"
|
35
|
+
spec.add_dependency "hashie", ">= 3.5.5"
|
36
|
+
spec.add_dependency "redis", ">= 3.3.3"
|
37
|
+
spec.add_dependency "slack-ruby-client", ">= 0.8.0"
|
38
|
+
end
|
metadata
ADDED
@@ -0,0 +1,199 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: socrates
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Christian Nelson
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-04-20 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.14'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.14'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '3.5'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '3.5'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rubocop
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 0.48.1
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 0.48.1
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: activesupport
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 5.0.2
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 5.0.2
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: celluloid-io
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: 0.17.3
|
90
|
+
type: :runtime
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: 0.17.3
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: hashie
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: 3.5.5
|
104
|
+
type: :runtime
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: 3.5.5
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: redis
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: 3.3.3
|
118
|
+
type: :runtime
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: 3.3.3
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: slack-ruby-client
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: 0.8.0
|
132
|
+
type: :runtime
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: 0.8.0
|
139
|
+
description:
|
140
|
+
email:
|
141
|
+
- christian@carbonfive.com
|
142
|
+
executables:
|
143
|
+
- socrates
|
144
|
+
extensions: []
|
145
|
+
extra_rdoc_files: []
|
146
|
+
files:
|
147
|
+
- ".gitignore"
|
148
|
+
- ".rspec"
|
149
|
+
- ".rubocop.yml"
|
150
|
+
- ".ruby-version"
|
151
|
+
- Gemfile
|
152
|
+
- README.md
|
153
|
+
- Rakefile
|
154
|
+
- bin/console
|
155
|
+
- bin/setup
|
156
|
+
- circle.yml
|
157
|
+
- exe/socrates
|
158
|
+
- lib/socrates.rb
|
159
|
+
- lib/socrates/adapters/console_adapter.rb
|
160
|
+
- lib/socrates/adapters/memory_adapter.rb
|
161
|
+
- lib/socrates/adapters/slack_adapter.rb
|
162
|
+
- lib/socrates/bots/cli_bot.rb
|
163
|
+
- lib/socrates/bots/slack_bot.rb
|
164
|
+
- lib/socrates/config.rb
|
165
|
+
- lib/socrates/core/dispatcher.rb
|
166
|
+
- lib/socrates/core/state.rb
|
167
|
+
- lib/socrates/core/state_data.rb
|
168
|
+
- lib/socrates/logger.rb
|
169
|
+
- lib/socrates/sample_states.rb
|
170
|
+
- lib/socrates/storage/storage.rb
|
171
|
+
- lib/socrates/string_helpers.rb
|
172
|
+
- lib/socrates/version.rb
|
173
|
+
- sample-conversation-console.gif
|
174
|
+
- socrates.gemspec
|
175
|
+
homepage: https://github.com/carbonfive/socrates
|
176
|
+
licenses:
|
177
|
+
- MIT
|
178
|
+
metadata: {}
|
179
|
+
post_install_message:
|
180
|
+
rdoc_options: []
|
181
|
+
require_paths:
|
182
|
+
- lib
|
183
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
184
|
+
requirements:
|
185
|
+
- - ">="
|
186
|
+
- !ruby/object:Gem::Version
|
187
|
+
version: 2.3.0
|
188
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
189
|
+
requirements:
|
190
|
+
- - ">="
|
191
|
+
- !ruby/object:Gem::Version
|
192
|
+
version: '0'
|
193
|
+
requirements: []
|
194
|
+
rubyforge_project:
|
195
|
+
rubygems_version: 2.6.11
|
196
|
+
signing_key:
|
197
|
+
specification_version: 4
|
198
|
+
summary: A microframework for building stateful conversational bots.
|
199
|
+
test_files: []
|