stealth 1.0.0.rc1 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/VERSION +1 -1
- data/docs/00-introduction.md +37 -0
- data/docs/01-getting-started.md +21 -0
- data/docs/02-local-development.md +40 -0
- data/docs/03-basics.md +155 -0
- data/docs/04-sessions.md +17 -0
- data/docs/05-controllers.md +179 -0
- data/docs/06-models.md +39 -0
- data/docs/07-replies.md +73 -0
- data/docs/08-catchalls.md +49 -0
- data/docs/09-messaging-integrations.md +79 -0
- data/docs/10-nlp-integrations.md +13 -0
- data/docs/11-analytics.md +13 -0
- data/docs/12-commands.md +62 -0
- data/docs/13-deployment.md +50 -0
- data/lib/stealth/cli.rb +2 -2
- data/lib/stealth/generators/generate/flow/controllers/controller.tt +1 -1
- data/lib/stealth/generators/generate/flow/helpers/helper.tt +1 -1
- data/lib/stealth/generators/generate/flow/models/model.tt +2 -0
- data/lib/stealth/generators/generate.rb +7 -1
- metadata +19 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7bcfc4f02dd83726d4bdcc3d9a2912ddcf1e767bdc17f3e8088fd46d811ff4d7
|
4
|
+
data.tar.gz: 434eab3e04d9282047da014e220afcd0075079c5fa5cbff4fa381aa29d43621d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1b383ef1b76db5d6ed33d2eb44ea3667ebe776115e7318f24a2142ed43cae57afce7449ed140a1e38b4ece60b9f4c74ec96483a20af5d9bef11a0d0936d41852
|
7
|
+
data.tar.gz: fb39ccdc11bf480af62b0611f5a12760b76335265302352cebe3da56a33a3bca921f1a271a2df6eda572cd511aa348a026917717bb3b91bb45e96bb59993b92c
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
1.0.0
|
1
|
+
1.0.0
|
@@ -0,0 +1,37 @@
|
|
1
|
+
---
|
2
|
+
title: Introduction
|
3
|
+
---
|
4
|
+
Stealth includes everything you need to build amazing chatbots with tools you know and love.
|
5
|
+
|
6
|
+
## Assumptions
|
7
|
+
|
8
|
+
These docs are designed for intermediate Ruby developers who want to get started with the Stealth framework.
|
9
|
+
|
10
|
+
If you do not yet have experience with Ruby, we would recommend checking out these guides first:
|
11
|
+
|
12
|
+
- [Official Ruby website](https://www.ruby-lang.org/en/documentation/)
|
13
|
+
- [List of Free Programming Books](https://github.com/EbookFoundation/free-programming-books/blob/master/free-programming-books.md#ruby)
|
14
|
+
|
15
|
+
## What is Stealth?
|
16
|
+
|
17
|
+
Stealth is an open source Ruby framework for conversational voice and text chatbots.
|
18
|
+
|
19
|
+
Stealth is inspired by the Model-View-Controller (MVC) pattern. However, instead of calling them *Views* Stealth refers to them as *Replies* to better match the chatbot domain.
|
20
|
+
|
21
|
+
- The [Model](#models) layer represents your data model (such as Account, User, Quote, etc.) and encapsulates the business logic that is specific to your bot. By default, Stealth uses [ActiveRecord](#models.active_record).
|
22
|
+
|
23
|
+
- The [Controller](#controllers) layer is responsible for handling incoming requests from messaging platforms and providing and transmitting the response (reply).
|
24
|
+
|
25
|
+
- The [Reply](#replies) layer is composed of "templates" that are responsible for constructing the respective response.
|
26
|
+
|
27
|
+
In addition to being inspired by Model-View-Controller (MVC) pattern, Stealth as a few other awesome things built in for you.
|
28
|
+
|
29
|
+
- **Plug and play services.** Every service integration in Stealth is a Ruby gem. One bot can support [multiple chat services](#messaging_integrations) (i.e. Facebook Messenger, SMS, Alexa, and more) and multiple NLP/NLU services.
|
30
|
+
|
31
|
+
- **Advanced tooling.** From web servers to continuous integration testing, Stealth is built to take advantage of all the great work done by the web development community.
|
32
|
+
|
33
|
+
- **Hosting you know.** Stealth is a Rack application. That means your bots can be [deployed](#deployment) using familiar services like Docker and Heroku.
|
34
|
+
|
35
|
+
- **Ready for production.** Stealth already powers bots for large, well-known brands including: Humana, TradeStation, Haven Life, and BarkBox.
|
36
|
+
|
37
|
+
- **Open source.** Stealth is MIT licensed to ensure you own your bot. More importantly, we welcome contributors to help make Stealth even better.
|
@@ -0,0 +1,21 @@
|
|
1
|
+
---
|
2
|
+
title: Getting Started
|
3
|
+
---
|
4
|
+
|
5
|
+
## Ruby
|
6
|
+
|
7
|
+
Stealth has been tested on Ruby (MRI) `2.4.x` and `2.5.x`. While we don't require any C-based Ruby gems, we haven't yet certified Stealth on other VMs (such as JRuby).
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
|
11
|
+
You can install Stealth via RubyGems:
|
12
|
+
|
13
|
+
```
|
14
|
+
gem install stealth
|
15
|
+
```
|
16
|
+
|
17
|
+
Next, you can create a new Stealth bot:
|
18
|
+
|
19
|
+
```
|
20
|
+
stealth new <bot_name>
|
21
|
+
```
|
@@ -0,0 +1,40 @@
|
|
1
|
+
---
|
2
|
+
title: Local Development
|
3
|
+
---
|
4
|
+
|
5
|
+
## Prerequisites
|
6
|
+
|
7
|
+
Stealth bundles [Sidekiq](https://github.com/mperham/sidekiq) in order to process background jobs. Therefore, it is required to run Redis in order to boot up a Stealth server.
|
8
|
+
|
9
|
+
## Starting the Server
|
10
|
+
|
11
|
+
Once you have made your current working directory your Stealth bot, you can install gems:
|
12
|
+
|
13
|
+
```
|
14
|
+
bundle install
|
15
|
+
```
|
16
|
+
|
17
|
+
To boot your bot:
|
18
|
+
|
19
|
+
```
|
20
|
+
stealth server
|
21
|
+
```
|
22
|
+
|
23
|
+
You can also use `stealth s`. This will use the [foreman](https://github.com/ddollar/foreman) gem to start the web server and Sidekiq processes together. Redis will have to be running for the server to start.
|
24
|
+
|
25
|
+
That's it! You are now running Stealth.
|
26
|
+
|
27
|
+
## Introspectable Tunnels to localhost
|
28
|
+
|
29
|
+
When developing locally, messaging services require access to your server in order to transmit user messages. We recommend downloading and using [ngrok](https://ngrok.com/download) to create a local tunnel to your development machine.
|
30
|
+
|
31
|
+
1. Download [ngrok](https://ngrok.com/download)
|
32
|
+
2. Start your Stealth server as detailed above.
|
33
|
+
3. Open up an ngrok tunnel to your Stealth server and port (default 5000) like this: `ngrok http 5000`. ngrok will output a unique ngrok local tunnel URL to your machine.
|
34
|
+
|
35
|
+
When you provide your local ngrok URL to a messaging service, you will have to add `/incoming/<service>`. For example:
|
36
|
+
|
37
|
+
* `https://abc1234.ngrok.io/incoming/facebook`
|
38
|
+
* `https://abc1234.ngrok.io/incoming/twilio`
|
39
|
+
|
40
|
+
More details on service specific settings can be found on the GitHub page for each service gem.
|
data/docs/03-basics.md
ADDED
@@ -0,0 +1,155 @@
|
|
1
|
+
---
|
2
|
+
title: The Basics
|
3
|
+
---
|
4
|
+
|
5
|
+
## Directory Structure
|
6
|
+
|
7
|
+
When you open up your Stealth bot you will see the following file structure:
|
8
|
+
|
9
|
+
```
|
10
|
+
├── Gemfile
|
11
|
+
├── Procfile.dev
|
12
|
+
├── README.md
|
13
|
+
├── Rakefile
|
14
|
+
├── bot
|
15
|
+
│ ├── controllers
|
16
|
+
│ │ ├── bot_controller.rb
|
17
|
+
│ │ ├── catch_alls_controller.rb
|
18
|
+
│ │ ├── concerns
|
19
|
+
│ │ ├── goodbyes_controller.rb
|
20
|
+
│ │ └── hellos_controller.rb
|
21
|
+
│ ├── helpers
|
22
|
+
│ │ └── bot_helper.rb
|
23
|
+
│ ├── models
|
24
|
+
│ │ ├── bot_record.rb
|
25
|
+
│ │ └── concerns
|
26
|
+
│ └── replies
|
27
|
+
│ ├── catch_alls
|
28
|
+
│ │ └── level1.yml
|
29
|
+
│ ├── goodbyes
|
30
|
+
│ │ └── say_goodbye.yml
|
31
|
+
│ └── hellos
|
32
|
+
│ └── say_hello.yml
|
33
|
+
├── config
|
34
|
+
│ ├── boot.rb
|
35
|
+
│ ├── database.yml
|
36
|
+
│ ├── environment.rb
|
37
|
+
│ ├── flow_map.rb
|
38
|
+
│ ├── initializers
|
39
|
+
│ ├── puma.rb
|
40
|
+
│ ├── services.yml
|
41
|
+
│ └── sidekiq.yml
|
42
|
+
├── config.ru
|
43
|
+
└── db
|
44
|
+
└── seeds.rb
|
45
|
+
```
|
46
|
+
|
47
|
+
## Flows
|
48
|
+
|
49
|
+
A `Flow` is a general term to describe a complete interaction between a user and the bot. Flows are comprised of `states`, like a finite state machine.
|
50
|
+
|
51
|
+
For example, if a user was using your bot to receive an insurance quote, the flow might be named `quote`. Note: Stealth requires that flows be named in the singular form, like Rails.
|
52
|
+
|
53
|
+
A flow consists of the following components:
|
54
|
+
|
55
|
+
1. A controller file, named in the plural form. For example, a `quote` flow would have a corresponding `QuotesController`.
|
56
|
+
2. Replies. Each flow will have a directory in the `replies` directory in plural form. Again using the `quote` flow example, the directory would named `quotes`.
|
57
|
+
3. An entry in `config/flow_map.rb`. The `FlowMap` file is where each flow and it's respective states are defined for your bot.
|
58
|
+
|
59
|
+
Flows can be generated using a generator:
|
60
|
+
|
61
|
+
```
|
62
|
+
stealth generate flow <NAME>
|
63
|
+
```
|
64
|
+
|
65
|
+
## FlowMap
|
66
|
+
|
67
|
+
The `FlowMap` file is where each flow and it's respective states are defined for your bot. Here is an example `flow_map.rb`:
|
68
|
+
|
69
|
+
```ruby
|
70
|
+
class FlowMap
|
71
|
+
|
72
|
+
include Stealth::Flow
|
73
|
+
|
74
|
+
flow :hello do
|
75
|
+
state :say_hello
|
76
|
+
state :ask_name
|
77
|
+
state :get_name, fails_to: :ask_name
|
78
|
+
end
|
79
|
+
|
80
|
+
flow :goodbye do
|
81
|
+
state :say_goodbye
|
82
|
+
end
|
83
|
+
|
84
|
+
flow :catch_all do
|
85
|
+
state :level1
|
86
|
+
state :level2
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
90
|
+
```
|
91
|
+
|
92
|
+
Here we have defined three flows: `hello`, `goodbye`, and `catch_all`. For the most part, these are default flows that are generated automatically when you create a new bot but we have made a few changes to highlight some functionality.
|
93
|
+
|
94
|
+
In the `hello` flow, the second state asks a user for their name. In the third state of `hello`, you see another option: `fails_to`. This is used to tell Stealth to return the user to the specified state if the `get_name` state raises an error or fails in another way. There are more details in the `CatchAll` section below.
|
95
|
+
|
96
|
+
## Default Flows
|
97
|
+
|
98
|
+
When you generate a new Stealth bot, it comes packaged with three default flows. While you will likely add many flows of your own, we recommend keeping these three flows as they encourage good bot building practices.
|
99
|
+
|
100
|
+
## Hello & Goodbye
|
101
|
+
|
102
|
+
These two flows make up the entrance and exit of your bot. We include blank examples on how to greet (say hello) and sendoff (say goodbye) your users. You can customize these flows to work with the design of your bot.
|
103
|
+
|
104
|
+
## CatchAll
|
105
|
+
|
106
|
+
Stealth also comes packaged with a `catch_all` flow. Stealth CatchAlls are designed to handle scenarios in which the user says something the bot is not expecting or the bot encounters an error.
|
107
|
+
|
108
|
+
Error handling is one of the most important parts of building great bots. We recommend that bot designers and developers spend sufficient time building the CatchAll states.
|
109
|
+
|
110
|
+
See the Catch All (#catchalls) section for more information on how Stealth handles `catch_all` flows.
|
111
|
+
|
112
|
+
## Say, Ask, Get
|
113
|
+
|
114
|
+
Stealth recommends you use the `say`, `ask`, and `get` prefix for your flow state names. It's not required, but it is a convention we have found helpful to keep state names under control. It also helps other developers on your team follow along more easily.
|
115
|
+
|
116
|
+
### SAY
|
117
|
+
|
118
|
+
*SAY* Stealth actions are for _saying_ something to the user.
|
119
|
+
|
120
|
+
For example:
|
121
|
+
|
122
|
+
```ruby
|
123
|
+
def say_hello
|
124
|
+
send_replies
|
125
|
+
end
|
126
|
+
```
|
127
|
+
|
128
|
+
### ASK
|
129
|
+
|
130
|
+
*ASK* Stealth actions are for _asking_ something from the user.
|
131
|
+
|
132
|
+
For example:
|
133
|
+
|
134
|
+
```ruby
|
135
|
+
def ask_weather
|
136
|
+
send_replies
|
137
|
+
update_session_to state: 'get_weather_reponse'
|
138
|
+
end
|
139
|
+
```
|
140
|
+
|
141
|
+
### GET
|
142
|
+
|
143
|
+
*GET* Stealth actions are for _getting_ and parsing a response from the user.
|
144
|
+
|
145
|
+
For example:
|
146
|
+
|
147
|
+
```ruby
|
148
|
+
def get_weather_reponse
|
149
|
+
if current_message.message == 'Sunny'
|
150
|
+
step_to state: "say_wear_sunglasses"
|
151
|
+
elsif current_message.message == 'Raining'
|
152
|
+
step_to state: "say_dont_forget_umbrella"
|
153
|
+
end
|
154
|
+
end
|
155
|
+
```
|
data/docs/04-sessions.md
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
---
|
2
|
+
title: Sessions
|
3
|
+
---
|
4
|
+
|
5
|
+
A user of your bot can be in any single flow and state at any given moment. They can never be in more than one flow or state. For this reason, Stealth sessions are modeled using [finite state machines](https://en.m.wikipedia.org/wiki/Finite-state_machine).
|
6
|
+
|
7
|
+
## Finite State Machines
|
8
|
+
|
9
|
+
Technically, each flow is its own state machine with its own states. Stealth, however, does not restrict the movement between states as rigidly. So while we find the state machine model helpful to learn sessions, don't spend too much time on Wikipedia!
|
10
|
+
|
11
|
+
## Redis Backed Sessions
|
12
|
+
|
13
|
+
User sessions are stored in Redis. Each session is a lightweight key-value pair. The key is the user's ID from the service -- so for Facebook it may be a long integer value: `100023838288224423`. The value is the flow and state for the user separated by a "stabby" operator (`->`).
|
14
|
+
|
15
|
+
So if for example a user with ID `100023838288224423` is currently at the `hello` flow and `ask_name` state, the value for the key would be: `hello->ask_name`.
|
16
|
+
|
17
|
+
You likely won't be interacting with sessions directly since Stealth manages it automatically for you. We just present it here for clarity into how sessions work.
|
@@ -0,0 +1,179 @@
|
|
1
|
+
---
|
2
|
+
title: Controllers
|
3
|
+
---
|
4
|
+
|
5
|
+
Controllers are responsible for handling incoming requests and getting a response back to the user (replies).
|
6
|
+
|
7
|
+
## Naming Conventions
|
8
|
+
|
9
|
+
The controller's methods, also referred to as actions, must be named after the flow's states. So for example, given the flow:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
flow :onboard do
|
13
|
+
state :say_welcome
|
14
|
+
state :ask_for_phone
|
15
|
+
state :get_phone, fails_to: :ask_for_phone
|
16
|
+
end
|
17
|
+
```
|
18
|
+
|
19
|
+
The corresponding controller would be:
|
20
|
+
|
21
|
+
```ruby
|
22
|
+
class OnboardsController < BotController
|
23
|
+
def say_welcome
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
def ask_for_phone
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
def get_phone
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
35
|
+
```
|
36
|
+
|
37
|
+
## `bot_controller.rb`
|
38
|
+
|
39
|
+
Every Stealth project comes with a default `bot_controller.rb`:
|
40
|
+
|
41
|
+
```ruby
|
42
|
+
class BotController < Stealth::Controller
|
43
|
+
|
44
|
+
before_action :current_user
|
45
|
+
|
46
|
+
def route
|
47
|
+
if current_session.present?
|
48
|
+
step_to session: current_session
|
49
|
+
else
|
50
|
+
step_to flow: 'hello', state: 'say_hello'
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
```
|
56
|
+
|
57
|
+
All of your controllers will inherit from `BotController`:
|
58
|
+
|
59
|
+
```ruby
|
60
|
+
class QuotesController < BotController
|
61
|
+
|
62
|
+
end
|
63
|
+
```
|
64
|
+
|
65
|
+
## Route Method
|
66
|
+
|
67
|
+
You can implement any method in `BotController`. Typically you will implement methods like `current_user` and methods for handling message payloads. The one method that `BotController` **must** implement is the `route` method.
|
68
|
+
|
69
|
+
The `route` method is called for every incoming message. In its default implementation, the `route` method checks whether the user already has a session, and if so routes them to that controller and action. If the user does not yet have a session, it will route them to the `hello` flow and `say_hello` action.
|
70
|
+
|
71
|
+
## Stepping and Updating Sessions
|
72
|
+
|
73
|
+
Stealth provides a few built-in methods to help you route a user through your bot.
|
74
|
+
|
75
|
+
## `step_to`
|
76
|
+
|
77
|
+
The `step_to` method is used to update the session and immediately move the user to the specified flow and/or state. `step_to` can accept a *flow*, a *state*, or both. `step_to` is often used after a `say` action where the next action typically doesn't require user input.
|
78
|
+
|
79
|
+
Some examples of the different parameters:
|
80
|
+
|
81
|
+
`step_to flow: 'hello'` - Sets the session's flow to `hello` and the state will be set to the *first* state in that flow (as defined by the `flow_map.rb` file). The corresponding controller action in the `HellosController` would also be called.
|
82
|
+
|
83
|
+
`step_to state: 'say_hello'` - Sets the session's state to `say_hello` and keeps the flow the same. The `say_hello` controller action would also be called.
|
84
|
+
|
85
|
+
`step_to flow: 'hello', state: 'say_hello'` - Sets the session's flow to `hello` and the state to `say_hello`. The `say_hello` controller action of the `HellosController` controller would also be called.
|
86
|
+
|
87
|
+
## `update_session_to`
|
88
|
+
|
89
|
+
Similar to `step_to`, `update_session_to` is used to update the user's session to a flow and/or state. It accepts the same parameters. However, `update_session_to` does not immediately call the respective controller action. `update_session_to` is typically used after an `ask` action where the next action is waiting for user input. So by asking a user for input, then updating the session, it ensures the response the user sends back can be handled by the `get` action.
|
90
|
+
|
91
|
+
Some examples of the different parameters:
|
92
|
+
|
93
|
+
`update_session_to flow: 'quote'` - Sets the session's flow to `quote` and the state will be set to the *first* state in that flow (as defined by the `flow_map.rb` file).
|
94
|
+
|
95
|
+
`update_session_to state: 'ask_zip_code'` - Sets the session's state to `ask_zip_code` and keeps the flow the same.
|
96
|
+
|
97
|
+
`step_to flow: 'quote', state: 'ask_zip_code'` - Sets the session's flow to `quote` and the state to `ask_zip_code`.
|
98
|
+
|
99
|
+
## `send_replies`
|
100
|
+
|
101
|
+
`send_replies` will instruct the `Reply` to construct the reply and transmit them. Not all of your controller actions will send replies. Typically in `get` action, you'll get a user's response, perform some action, and then send a user to a new state without replying.
|
102
|
+
|
103
|
+
The `send_replies` method does not take any parameters:
|
104
|
+
|
105
|
+
```ruby
|
106
|
+
class ContactsController < BotController
|
107
|
+
def say_contact_us
|
108
|
+
send_replies
|
109
|
+
end
|
110
|
+
end
|
111
|
+
```
|
112
|
+
|
113
|
+
This would render the reply contained in `replies/contacts/say_contact_us.yml`.
|
114
|
+
|
115
|
+
## `step_to_in`
|
116
|
+
|
117
|
+
The `step_to_in` method is similar to `step_to`. The only difference is that instead of calling the respective controller action immediately, it calls it after a specified duration. It can also take a flow, state, or both.
|
118
|
+
|
119
|
+
For example:
|
120
|
+
|
121
|
+
```ruby
|
122
|
+
step_to_in 8.hours, flow: 'hello', state: 'say_hello'
|
123
|
+
```
|
124
|
+
|
125
|
+
This will set the user's session to the `hello` flow and `say_hello` state in 8 hours after being called. It will then immediately call that responsible controller action.
|
126
|
+
|
127
|
+
## `step_to_at`
|
128
|
+
|
129
|
+
The `step_to_at` method is similar to `step_to`. The only difference is that instead of calling the respective controller action immediately, it calls it at a specific date and time. It can also take a flow, state, or both.
|
130
|
+
|
131
|
+
For example:
|
132
|
+
|
133
|
+
```ruby
|
134
|
+
step_to_at DateTime.strptime("01/23/23 20:23", "%m/%d/%y %H:%M"), flow: 'hello', state: 'say_hello'
|
135
|
+
```
|
136
|
+
|
137
|
+
This will set the user's session to the `hello` flow and `say_hello` state on `Jan 23, 2023 @ 20:23`. It will then immediately call that responsible controller action.
|
138
|
+
|
139
|
+
## Available Data
|
140
|
+
|
141
|
+
Within each controller action, you have access to a few objects containing information about the session and the message the being processed.
|
142
|
+
|
143
|
+
### current_session
|
144
|
+
|
145
|
+
The user's session is available to you at anytime using `current_session`. This is a `Stealth::Session` object. It has a few notable methods:
|
146
|
+
|
147
|
+
`flow_string`: Returns the name of the flow.
|
148
|
+
`state_string`: Returns the name of the state.
|
149
|
+
`current_session + 2.states`: Returns a new session object 2 states after the current state. If we've passed the last state, the last state is returned.
|
150
|
+
`current_session - 2.states`: Returns a new session object 2 states before the current state. If we've passed the first state, the first state is returned.
|
151
|
+
|
152
|
+
### current_message
|
153
|
+
|
154
|
+
The current message being processed is available to you at anytime using `current_message`. This is a `Stealth::ServiceMessage` object. It has a few notable methods:
|
155
|
+
|
156
|
+
`sender_id`: The ID of the user sending the message. This will vary based on the service integration.
|
157
|
+
`timestamp`: Ruby `DateTime` object of when the message was transmitted.
|
158
|
+
`service`: String indicating the service from where the message originated (i.e., 'facebook').
|
159
|
+
`messsage`: String of the message contents.
|
160
|
+
`payload`: This will vary by service integration.
|
161
|
+
`location`: This will vary by service integration.
|
162
|
+
`attachments`: This will vary by service integration.
|
163
|
+
`referral`: This will vary by service integration.
|
164
|
+
|
165
|
+
### current_service
|
166
|
+
|
167
|
+
This will be a string indicating the service from where the message originated (i.e., 'facebook' or 'twilio')
|
168
|
+
|
169
|
+
### has_location?
|
170
|
+
|
171
|
+
Returns `true` or `false` depending on whether or not the `current_message` contains location data.
|
172
|
+
|
173
|
+
### has_attachments?
|
174
|
+
|
175
|
+
Returns `true` or `false` depending on whether or not the `current_message` contains attachments.
|
176
|
+
|
177
|
+
### current_user_id
|
178
|
+
|
179
|
+
A convenience method for accessing the user's ID.
|
data/docs/06-models.md
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
---
|
2
|
+
title: Models
|
3
|
+
---
|
4
|
+
|
5
|
+
Models in Stealth are powered by [ActiveRecord](http://guides.rubyonrails.org/active_record_basics.html). Your bot may not need to persist data, but if it does, ActiveRecord comes built in. We've tried to keep things identical to Ruby on Rails.
|
6
|
+
|
7
|
+
## ActiveRecord Models
|
8
|
+
|
9
|
+
An ActiveRecord model in Stealth inherits all of the functionality from [ActiveRecord](http://guides.rubyonrails.org/active_record_basics.html). An empty model looks like this in Stealth:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
class Quote < BotRecord
|
13
|
+
|
14
|
+
end
|
15
|
+
```
|
16
|
+
|
17
|
+
With the exception of inheriting from `BotRecord` instead of `ApplicationRecord`, everything else matches what is in the [ActiveRecord](http://guides.rubyonrails.org/active_record_basics.html) documentation.
|
18
|
+
|
19
|
+
## Configuration
|
20
|
+
|
21
|
+
Configuring a database is done via `config/database.yml`. A sample `database.yml` file is included when you generate your bot. It is configured to use SQLite3. For more options please refer to the [Ruby on Rails documentation](http://guides.rubyonrails.org/configuring.html#configuring-a-database).
|
22
|
+
|
23
|
+
## Migrations
|
24
|
+
|
25
|
+
In order to use your models, you'll need to generate migrations to create your database schema:
|
26
|
+
|
27
|
+
```
|
28
|
+
stealth g migration create_users
|
29
|
+
```
|
30
|
+
|
31
|
+
This will create a migration named `CreateUsers`. To migrate your database:
|
32
|
+
|
33
|
+
```
|
34
|
+
stealth db:migrate
|
35
|
+
```
|
36
|
+
|
37
|
+
For more information about migrations, seed data, creating databases, or dropping databases please refer to the [Ruby on Rails documentation](http://guides.rubyonrails.org/active_record_migrations.html).
|
38
|
+
|
39
|
+
Just remember to prefix your commands with `stealth` rather than `rails`.
|
data/docs/07-replies.md
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
---
|
2
|
+
title: Replies
|
3
|
+
---
|
4
|
+
|
5
|
+
Stealth replies can send one or more replies to a user. Reply types are dependent on the specific messaging service you're using. Each service integration will detail it's supported reply types in it's respective docs.
|
6
|
+
|
7
|
+
However, here is a generic reply using text, delays, and suggestions.
|
8
|
+
|
9
|
+
```yml
|
10
|
+
- reply_type: text
|
11
|
+
text: "Hello. Welcome to our Bot."
|
12
|
+
- reply_type: delay
|
13
|
+
duration: 2
|
14
|
+
- reply_type: text
|
15
|
+
text: "We're here to help you learn more about something or another."
|
16
|
+
- reply_type: delay
|
17
|
+
duration: 2
|
18
|
+
- reply_type: text
|
19
|
+
text: 'By using the "Yes" and "No" buttons below, are you interested in do you want to continue?'
|
20
|
+
suggestions:
|
21
|
+
- text: "Yes"
|
22
|
+
- text: "No"
|
23
|
+
```
|
24
|
+
|
25
|
+
## Format
|
26
|
+
|
27
|
+
Stealth reply templates are written in YAML. Stealth doesn't use advanced YAML features, but we do recommend you familiarize yourself with the syntax. In the above reply example, you should be able to see there are 5 replies included in the reply file.
|
28
|
+
|
29
|
+
**Caveat:** YAML interprets "yes", "no", "true", "false", "y", "n", etc (without quotes) as boolean values. So make sure you wrap them in quotes as we did above.
|
30
|
+
|
31
|
+
## ERB
|
32
|
+
|
33
|
+
Reply templates currently support ERB:
|
34
|
+
|
35
|
+
```erb
|
36
|
+
- reply_type: text
|
37
|
+
text: "Hello, <%= current_user.first_name %>. Welcome to our Bot."
|
38
|
+
- reply_type: delay
|
39
|
+
duration: 2
|
40
|
+
- reply_type: text
|
41
|
+
text: "We're here to help you learn more about something or another."
|
42
|
+
- reply_type: delay
|
43
|
+
duration: 2
|
44
|
+
<% if current_user.valid? %>
|
45
|
+
- reply_type: text
|
46
|
+
text: 'By using the "Yes" and "No" buttons below, are you interested in do you want to continue?'
|
47
|
+
suggestions:
|
48
|
+
- text: "Yes"
|
49
|
+
- text: "No"
|
50
|
+
<% end %>
|
51
|
+
```
|
52
|
+
|
53
|
+
With ERB in your reply templates, you can access controller instance variables and helper methods in your replies.
|
54
|
+
|
55
|
+
## Delays
|
56
|
+
|
57
|
+
Delays are a common pattern of chatbot design. After a block of text, it's recommended to pause for a bit to give the user a chance to read the message. The duration of the delay depends on the length of the message sent.
|
58
|
+
|
59
|
+
Stealth will pause for the duration specified. For service integrations that support it (like Facebook), Stealth will send a typing indicator while it is paused.
|
60
|
+
|
61
|
+
## Naming Conventions
|
62
|
+
|
63
|
+
Replies are named after a flow's state (which is also the controller's action). So for a given controller:
|
64
|
+
|
65
|
+
```ruby
|
66
|
+
class QuotesController < BotController
|
67
|
+
def say_price
|
68
|
+
|
69
|
+
end
|
70
|
+
end
|
71
|
+
```
|
72
|
+
|
73
|
+
You would need to place your reply template in `replies/quotes/say_price.yml`. If your template contains ERB, you must add the `.erb` suffix to the template filename: `replies/quotes/say_price.yml.erb`.
|
@@ -0,0 +1,49 @@
|
|
1
|
+
---
|
2
|
+
title: CatchAll
|
3
|
+
---
|
4
|
+
|
5
|
+
Stealth CatchAlls are designed to handle a very common scenario within chatbots. What happens when the user says something the bot doesn't understand? The majority of bots will simply respond back with a generic "I don't understand" and hope the user to figures out the next step. While this experience might be ok for some bots, we built a more robust way of handling these experiences right into Stealth. The better your CatchAlls, the better your bot.
|
6
|
+
|
7
|
+
## Triggering
|
8
|
+
|
9
|
+
A CatchAll flow is automatically triggered when a controller action fails to do **at least one** of the following:
|
10
|
+
|
11
|
+
1. Update a session (via `step_to`, `update_session_to`, or any other of the step methods)
|
12
|
+
2. Send a reply via `send_replies`
|
13
|
+
|
14
|
+
In addition to the above two conditions, if your controller action raises an Exception, the CatchAll flow will automatically be triggered.
|
15
|
+
|
16
|
+
## Multi-Level
|
17
|
+
|
18
|
+
Stealth keeps track of how many times a CatchAll is triggered for a given session. This allows you to build experiences in which the user is provided different responses for subsequent failures. Once a session progresses past a failing state, the CatchAll counter resets.
|
19
|
+
|
20
|
+
## Retrying
|
21
|
+
|
22
|
+
By default, a Stealth bot comes with the first level CatchAll already defined. Here is the `CatchAllsController` action and associated reply:
|
23
|
+
|
24
|
+
```ruby
|
25
|
+
def level1
|
26
|
+
send_replies
|
27
|
+
|
28
|
+
if previous_session_specifies_fails_to?
|
29
|
+
step_to flow: previous_session.flow_string, state: previous_state.to_s
|
30
|
+
else
|
31
|
+
step_to session: previous_session - 2.states
|
32
|
+
end
|
33
|
+
end
|
34
|
+
```
|
35
|
+
|
36
|
+
```yml
|
37
|
+
- reply_type: text
|
38
|
+
text: Oops. It looks like something went wrong. Let's try that again
|
39
|
+
```
|
40
|
+
|
41
|
+
In the controller action, we check if the `previous_session` (the one that failed) specified a `fails_to` state. If so, we send the user there. Otherwise, we send the user back 2 states.
|
42
|
+
|
43
|
+
Sending a user back two states is a pretty good generic action. Going back 1 state takes us back to the action that failed. Since the actions most likely to fail are `get` actions, or actions that deal with user responses, going back 2 states usually takes us back to the original "question".
|
44
|
+
|
45
|
+
## Adding More Levels
|
46
|
+
|
47
|
+
If you would like to expand the experience, simply add a `level2` controller action and associated reply (and update the `FlowMap`). You can go as far as you want. CatchAlls have no limit, just make sure you increment using the standardized method names of `level1`, `level2`, `level3`, `level4`, etc.
|
48
|
+
|
49
|
+
If a user has encountered the maximum number of CatchAll levels as you have defined, the user's session will remain at the last CatchAll state.
|
@@ -0,0 +1,79 @@
|
|
1
|
+
---
|
2
|
+
title: Messaging Integrations
|
3
|
+
---
|
4
|
+
|
5
|
+
Stealth is designed for your bot to support one or more messaging integrations. For example, this could be just SMS or both SMS and Facebook Messenger. Messaging integrations can be attached to your Stealth bot by adding the messaging integration gem to your `Gemfile`.
|
6
|
+
|
7
|
+
## `Gemfile`
|
8
|
+
|
9
|
+
```
|
10
|
+
source 'https://rubygems.org'
|
11
|
+
|
12
|
+
ruby '2.5.1'
|
13
|
+
|
14
|
+
gem 'stealth', '~> 0.10.0'
|
15
|
+
|
16
|
+
# Uncomment to enable the Stealth Facebook Driver
|
17
|
+
# gem 'stealth-facebook'
|
18
|
+
|
19
|
+
# Uncomment to enable the Stealth Twilio SMS Driver
|
20
|
+
# gem 'stealth-twilio'
|
21
|
+
```
|
22
|
+
|
23
|
+
|
24
|
+
## `services.yml`
|
25
|
+
|
26
|
+
```yml
|
27
|
+
default: &default
|
28
|
+
# ==========================================
|
29
|
+
# ===== Example Facebook Service Setup =====
|
30
|
+
# ==========================================
|
31
|
+
# facebook:
|
32
|
+
# verify_token: XXXFACEBOOK_VERIFY_TOKENXXX
|
33
|
+
# page_access_token: XXXFACEBOOK_ACCESS_TOKENXXX
|
34
|
+
# setup:
|
35
|
+
# greeting: # Greetings are broken up by locale
|
36
|
+
# - locale: default
|
37
|
+
# text: "Welcome to the Stealth bot 🤖"
|
38
|
+
# persistent_menu:
|
39
|
+
# - type: payload
|
40
|
+
# text: Main Menu
|
41
|
+
# payload: main_menu
|
42
|
+
# - type: url
|
43
|
+
# text: Visit our website
|
44
|
+
# url: https://example.com
|
45
|
+
# - type: call
|
46
|
+
# text: Call us
|
47
|
+
# payload: "+4155330000"
|
48
|
+
#
|
49
|
+
# ===========================================
|
50
|
+
# ======== Example SMS Service Setup ========
|
51
|
+
# ===========================================
|
52
|
+
# twilio:
|
53
|
+
# account_sid: XXXTWILIO_ACCOUNT_SIDXXX
|
54
|
+
# auth_token: XXXTWILIO_AUTH_TOKENXXX
|
55
|
+
# from_phone: +14155330000
|
56
|
+
|
57
|
+
production:
|
58
|
+
<<: *default
|
59
|
+
development:
|
60
|
+
<<: *default
|
61
|
+
test:
|
62
|
+
<<: *default
|
63
|
+
|
64
|
+
```
|
65
|
+
|
66
|
+
## `stealth setup`
|
67
|
+
|
68
|
+
Most messaging integrations require an initial setup. For example, Facebook requires you to send a payload to define the default greeting and persistent menu. You can accomplish this by running the `stealth setup` followed by the integration. For example:
|
69
|
+
|
70
|
+
`stealth setup facebook`
|
71
|
+
|
72
|
+
Make sure to reference the respective messaging integration documentation for more specifics.
|
73
|
+
|
74
|
+
## Officially Supported
|
75
|
+
|
76
|
+
* [Facebook Messenger](https://github.com/hellostealth/stealth-facebook)
|
77
|
+
* [SMS (Twillio)](https://github.com/hellostealth/stealth-twilio)
|
78
|
+
|
79
|
+
While we plan to add more integrations in the future, please feel free to add your own and let us know so we can keep this list updated. 😎
|
@@ -0,0 +1,13 @@
|
|
1
|
+
---
|
2
|
+
title: NLP Integrations
|
3
|
+
---
|
4
|
+
|
5
|
+
Stealth can be extended with NLP/NLU integrations. While these are not needed for the majority of interactions within chatbot, you may find they are helpful for certain types of interactions.
|
6
|
+
|
7
|
+
## Officially Supported
|
8
|
+
|
9
|
+
Stealth currently supports:
|
10
|
+
|
11
|
+
* [AWS Comprehend](https://github.com/hellostealth/stealth-aws-comprehend)
|
12
|
+
|
13
|
+
While we plan to add more integrations in the future, please feel free to add your own and let us know so we can keep this list updated. 😎
|
@@ -0,0 +1,13 @@
|
|
1
|
+
---
|
2
|
+
title: Analytics Integrations
|
3
|
+
---
|
4
|
+
|
5
|
+
Stealth can be extended with metric/analytic integrations.
|
6
|
+
|
7
|
+
## Officially Supported
|
8
|
+
|
9
|
+
Stealth currently supports:
|
10
|
+
|
11
|
+
* [Mixpanel](https://github.com/hellostealth/stealth-mixpanel)
|
12
|
+
|
13
|
+
While we plan to add more integrations in the future, please feel free to add your own and let us know so we can keep this list updated. 😎
|
data/docs/12-commands.md
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
---
|
2
|
+
title: Commands
|
3
|
+
---
|
4
|
+
|
5
|
+
Stealth provides the `stealth` command-line program. It is used to generate new bots, generate flows, start a console, run integration setup tasks, run the server, and more.
|
6
|
+
|
7
|
+
To view details for a command at any time use `stealth help`.
|
8
|
+
|
9
|
+
```
|
10
|
+
Usage:
|
11
|
+
|
12
|
+
stealth [<flags>] <command> [<args> ...]
|
13
|
+
|
14
|
+
Flags:
|
15
|
+
|
16
|
+
-h, --help Output usage information.
|
17
|
+
-C, --chdir="." Change working directory.
|
18
|
+
-v, --verbose Enable verbose log output.
|
19
|
+
--format="text" Output formatter.
|
20
|
+
--version Show application version.
|
21
|
+
|
22
|
+
Commands:
|
23
|
+
stealth console # Starts a stealth console
|
24
|
+
stealth db:create # Creates the database from DATABASE_URL or config/database.yml for the current STEALTH_ENV
|
25
|
+
stealth db:create:all # Creates all databases from DATABASE_URL or config/database.yml
|
26
|
+
stealth db:drop # Drops the database from DATABASE_URL or config/database.yml for the current STEALTH_ENV
|
27
|
+
stealth db:drop:all # Drops all databases from DATABASE_URL or config/database.yml
|
28
|
+
stealth db:environment:set # Set the environment value for the database
|
29
|
+
stealth db:migrate # Migrate the database
|
30
|
+
stealth db:rollback # Rolls the schema back to the previous version
|
31
|
+
stealth db:schema:dump # Creates a db/schema.rb file that is portable against any DB supported by Active Record
|
32
|
+
stealth db:schema:load # Loads a schema.rb file into the database
|
33
|
+
stealth db:seed # Seeds the database with data from db/seeds.rb
|
34
|
+
stealth db:setup # Creates the database, loads the schema, and initializes with the seed data (use db:reset to also drop the database first)
|
35
|
+
stealth db:structure:dump # Dumps the database structure to db/structure.sql. Specify another file with SCHEMA=db/my_structure.sql
|
36
|
+
stealth db:structure:load # Recreates the databases from the structure.sql file
|
37
|
+
stealth db:version # Retrieves the current schema version number
|
38
|
+
stealth generate # Generates scaffold Stealth files
|
39
|
+
stealth help [COMMAND] # Describe available commands or one specific command
|
40
|
+
stealth new # Creates a new Stealth bot
|
41
|
+
stealth server # Starts a stealth server
|
42
|
+
stealth sessions:clear # Clears all sessions in development
|
43
|
+
stealth setup # Runs setup tasks for a specified service
|
44
|
+
stealth version # Prints stealth version
|
45
|
+
|
46
|
+
Examples:
|
47
|
+
|
48
|
+
Start a new Stealth project.
|
49
|
+
$ steath new [BOT NAME]
|
50
|
+
|
51
|
+
Generate a new flow inside your Stealth project.
|
52
|
+
$ stealth generate flow [FLOW NAME]
|
53
|
+
|
54
|
+
Run setup tasks for a specific driver.
|
55
|
+
$ stealth setup [INTEGRATION NAME]
|
56
|
+
|
57
|
+
Start a Stealth console.
|
58
|
+
$ stealth c
|
59
|
+
|
60
|
+
Start the Stealth server.
|
61
|
+
$ stealth s
|
62
|
+
```
|
@@ -0,0 +1,50 @@
|
|
1
|
+
---
|
2
|
+
title: Deployment
|
3
|
+
---
|
4
|
+
|
5
|
+
Stealth is a rack based application. That means it can be hosted on most platforms as well as taking advantage of existing tools such as Docker.
|
6
|
+
|
7
|
+
## Deploying on Heroku
|
8
|
+
|
9
|
+
Stealth supports [Heroku](http://herokuapp.com) out of the box. In fact, running a `stealth s` command locally boots `foreman` using a `Procfile.dev` file similar to what Heroku does. Here is a quick guide to get you started.
|
10
|
+
|
11
|
+
If you haven't, make sure to track your bot in Git
|
12
|
+
|
13
|
+
```
|
14
|
+
$ git init
|
15
|
+
Initialized empty Git repository in .git/
|
16
|
+
$ git add .
|
17
|
+
$ git commit -m "My first commit"
|
18
|
+
Created initial commit 5df2d09: My first commit
|
19
|
+
42 files changed, 470 insertions(+)
|
20
|
+
create mode 100644 Gemfile
|
21
|
+
create mode 100644 Gemfile.lock
|
22
|
+
create mode 100644 Procfile
|
23
|
+
...
|
24
|
+
```
|
25
|
+
|
26
|
+
After you have your bot tracked with Git, you're ready to deploy to Heroku. Next, we'll add our bot to Heroku using:
|
27
|
+
|
28
|
+
```
|
29
|
+
$ heroku apps:create <BOT NAME>
|
30
|
+
```
|
31
|
+
|
32
|
+
You will want a production `Procfile` separate from your development `Procfile.dev`. We recommend adding:
|
33
|
+
|
34
|
+
```
|
35
|
+
web: bundle exec puma -C config/puma.rb
|
36
|
+
sidekiq: bundle exec sidekiq -C config/sidekiq.yml -q webhooks -q default -r ./config/boot.rb
|
37
|
+
release: bundle exec rake db:migrate
|
38
|
+
```
|
39
|
+
|
40
|
+
Then deploy your bot to Heroku.
|
41
|
+
|
42
|
+
```
|
43
|
+
$ git push heroku master
|
44
|
+
```
|
45
|
+
|
46
|
+
Once deployed:
|
47
|
+
|
48
|
+
1. Make sure to enable both the `Heroku Postgres` (if you use a database) and `Heroku Redis` addons
|
49
|
+
2. Make sure the `web` and `sidekiq` dynos are spun up
|
50
|
+
3. Make sure you run any `stealth setup` commands to configure your messaging service
|
data/lib/stealth/cli.rb
CHANGED
@@ -217,9 +217,9 @@ module Stealth
|
|
217
217
|
end
|
218
218
|
|
219
219
|
|
220
|
-
desc 'db:seed', '
|
220
|
+
desc 'db:seed', 'Seeds the database with data from db/seeds.rb'
|
221
221
|
long_desc <<-EOS
|
222
|
-
`stealth db:seed`
|
222
|
+
`stealth db:seed` Seeds the database with data from db/seeds.rb
|
223
223
|
|
224
224
|
$ > stealth db:seed
|
225
225
|
EOS
|
@@ -1,2 +1,2 @@
|
|
1
|
-
module <%= name.underscore.camelize %>Helper < BotHelper
|
1
|
+
module <%= name.capitalize.underscore.camelize %>Helper < BotHelper
|
2
2
|
end
|
@@ -31,8 +31,14 @@ module Stealth
|
|
31
31
|
template('helpers/helper.tt', "bot/helpers/#{name}_helper.rb")
|
32
32
|
end
|
33
33
|
|
34
|
+
def create_model
|
35
|
+
template('models/model.tt', "bot/models/#{name}.rb")
|
36
|
+
end
|
37
|
+
|
34
38
|
def edit_flow_map
|
35
|
-
|
39
|
+
inject_into_file "config/flow_map.rb", after: "include Stealth::Flow\n" do
|
40
|
+
"\n\tflow :#{name} do\n\t\tstate :ask_example\n\t\tstate :get_example\n\t\tstate :say_yes_example\n\t\tstate :say_no_example\n\tend\n\n"
|
41
|
+
end
|
36
42
|
end
|
37
43
|
|
38
44
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: stealth
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.0
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Mauricio Gomes
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2018-05-
|
12
|
+
date: 2018-05-18 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: sinatra
|
@@ -194,6 +194,20 @@ files:
|
|
194
194
|
- README.md
|
195
195
|
- VERSION
|
196
196
|
- bin/stealth
|
197
|
+
- docs/00-introduction.md
|
198
|
+
- docs/01-getting-started.md
|
199
|
+
- docs/02-local-development.md
|
200
|
+
- docs/03-basics.md
|
201
|
+
- docs/04-sessions.md
|
202
|
+
- docs/05-controllers.md
|
203
|
+
- docs/06-models.md
|
204
|
+
- docs/07-replies.md
|
205
|
+
- docs/08-catchalls.md
|
206
|
+
- docs/09-messaging-integrations.md
|
207
|
+
- docs/10-nlp-integrations.md
|
208
|
+
- docs/11-analytics.md
|
209
|
+
- docs/12-commands.md
|
210
|
+
- docs/13-deployment.md
|
197
211
|
- lib/stealth.rb
|
198
212
|
- lib/stealth/base.rb
|
199
213
|
- lib/stealth/cli.rb
|
@@ -243,6 +257,7 @@ files:
|
|
243
257
|
- lib/stealth/generators/generate.rb
|
244
258
|
- lib/stealth/generators/generate/flow/controllers/controller.tt
|
245
259
|
- lib/stealth/generators/generate/flow/helpers/helper.tt
|
260
|
+
- lib/stealth/generators/generate/flow/models/model.tt
|
246
261
|
- lib/stealth/generators/generate/flow/replies/ask_reply.tt
|
247
262
|
- lib/stealth/generators/generate/flow/replies/say_no_reply.tt
|
248
263
|
- lib/stealth/generators/generate/flow/replies/say_yes_reply.tt
|
@@ -302,9 +317,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
302
317
|
version: '0'
|
303
318
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
304
319
|
requirements:
|
305
|
-
- - "
|
320
|
+
- - ">="
|
306
321
|
- !ruby/object:Gem::Version
|
307
|
-
version:
|
322
|
+
version: '0'
|
308
323
|
requirements: []
|
309
324
|
rubyforge_project:
|
310
325
|
rubygems_version: 2.7.6
|