-
Notifications
You must be signed in to change notification settings - Fork 16
UserContextStorage
The idea and its implementation of "user's conversational context" is go-sarah's signature feature. While typical bot implementation is somewhat "stateless" and hence user-bot interaction does not consider previous state, Sarah let sarah.Command
tell what action to follow on next user input and stash current state temporary. On next user input, the message is passed to the function stated on previous command execution, and resume previous unfinished command. User may input special command, ".abort" by default, to forcefully get out of current state.
To use this feature, your bot must be initialized with sarah.UserContextStorage
implementation. This is where current user state is stored. Currently two kinds of storages are supported. One that stores state in process memory; another that stores in external storage. That will be covered in later section.
Below example shows how to setup sarah.Bot
with default storage.
// Setup slack adapter.
adapter, err := slack.NewAdapter(config.Slack)
if err != nil {
panic(fmt.Errorf("faileld to setup Slack Adapter: %s", err.Error()))
}
// Use default implementation of UserContextStorage, which stores state in process memory.
storage := sarah.NewUserContextStorage(config.UserContextStorage)
// Setup Bot with slack adapter and default storage.
bot, err := sarah.NewBot(adapter, sarah.BotWithStorage(storage))
if err != nil {
panic(fmt.Errorf("faileld to setup Slack Bot: %s", err.Error()))
}
This example command TodoCommand
let user supply required arguments step by step for better user experience.
Simplified example code is as below. In this example sarah.Command
is implemented by TodoCommand
struct. Complete runnable code is located at ./example/plugins/todo.
To see how to create one with sarah.CommandPropsBuilder
, see later example.
type Schedule struct {
time time.Time
title string
description string
userKey string
}
var matchPattern = regexp.MustCompile(`^\.todo`)
type TodoCommand struct {
db *dataBase
}
// TodoCommand implements sarah.Command
var _ sarah.Command = (*TodoCommand)(nil)
func (todo *TodoCommand) Identifier() string {
return "todo"
}
func (todo *TodoCommand) Execute(_ context.Context, input sarah.Input) (*sarah.CommandResponse, error) {
// This is called when user input ".todo blah blah"
task := sarah.StripMessage(matchPattern, input.Message())
schedule := &Schedule{
description: task,
}
// Instead of returning plain text with slack.NewStringResponse, use slack.NewStringResponseWithNext to indicate what function to execute on next user input
return slack.NewStringResponseWithNext("Input due date time. YYYY-MM-DD HH:MM", func(c context.Context, i sarah.Input) (*sarah.CommandResponse, error) {
fmt.Println(i.Message())
return todo.inputTime(c, i, schedule)
}), nil
}
func (todo *TodoCommand) InputExample() string {
return ".todo go to school"
}
func (todo *TodoCommand) Match(input sarah.Input) bool {
return matchPattern.Copy().MatchString(input.Message())
}
func (todo *TodoCommand) inputTime(_ context.Context, input sarah.Input, schedule *Schedule) (*sarah.CommandResponse, error) {
// See if given input includes valid time format
t, err := time.Parse("2006-01-02 15:04", strings.TrimSpace(input.Message()))
if err != nil {
// When invalid format is given, tell user and let input again. This very same method is to be called on next input.
return slack.NewStringResponseWithNext("Use YYYY-MM-DD HH:MM format.", func(c context.Context, i sarah.Input) (*sarah.CommandResponse, error) {
return todo.inputTime(c, i, schedule)
}), nil
}
// If due is successfully given, stash this input and let user go to next step.
schedule.time = t
return slack.NewStringResponseWithNext("Please input description.", func(c context.Context, i sarah.Input) (*sarah.CommandResponse, error) {
return todo.inputDescription(c, i, schedule)
}), nil
}
func (todo *TodoCommand) inputDescription(_ context.Context, input sarah.Input, schedule *Schedule) (*sarah.CommandResponse, error) {
// At this point all arguments are given, so store them into persistent storage.
schedule.description = input.Message()
todo.db.StoreSchedule(schedule)
// Then return plain string message for confirmation.
// This time, slack.NewStringResponse is used instead of slack.NewStringResponseWithNext.
msg := fmt.Sprintf(`Saved. Title: %s. Description: %s Due: %s`, schedule.title, schedule.description, schedule.time.String())
return slack.NewStringResponse(msg), nil
}
// dataBase stores data to persistent storage
type dataBase struct {
}
func (db *dataBase) StoreSchedule(schedule *Schedule) {
// Create record
}
This example game command generate a random value at initial input, and let user input numbers until given number is equal to the initially generated value.
Below example uses sarah.CommandPropsBuilder
to setup non contradicting set of arguments to build sarah.Command
on the fly. See CommandPropsBuilder for detailed usage.
var guessProps = sarah.NewCommandPropsBuilder().
BotType(slack.SLACK).
Identifier("guess").
InputExample(".guess").
MatchFunc(func(input sarah.Input) bool {
return strings.HasPrefix(strings.TrimSpace(input.Message()), ".guess")
}).
Func(func(ctx context.Context, input sarah.Input) (*sarah.CommandResponse, error) {
// Generate answer value at the very beginning.
rand.Seed(time.Now().UnixNano())
answer := rand.Intn(100)
// Let user guess the right answer.
return slack.NewStringResponseWithNext("Input number.", func(c context.Context, i sarah.Input) (*sarah.CommandResponse, error) {
return guessFunc(c, i, answer)
}), nil
}).
MustBuild()
func guessFunc(_ context.Context, input sarah.Input, answer int) (*sarah.CommandResponse, error) {
// For handiness, create a function that recursively calls guessFunc until user input right answer.
retry := func(c context.Context, i sarah.Input) (*sarah.CommandResponse, error) {
return guessFunc(c, i, answer)
}
// See if user inputs valid number.
guess, err := strconv.Atoi(strings.TrimSpace(input.Message()))
if err != nil {
return slack.NewStringResponseWithNext("Invalid input format.", retry), nil
}
// If guess is right, tell user and finish current user context.
// Otherwise let user input next guess with bit of a hint.
if guess == answer {
return slack.NewStringResponse("Correct!"), nil
} else if guess > answer {
return slack.NewStringResponseWithNext("Smaller!", retry), nil
} else {
return slack.NewStringResponseWithNext("Bigger!", retry), nil
}
}
To have a grasp of overall architecture, have a look at Components.