yetibot / core

:expressionless: Core yetibot utilities, extracted for shared use among Yetibot and its various plugins
https://yetibot.com
Eclipse Public License 1.0
27 stars 17 forks source link

create framework to allow for bot "dialogs" #149

Open gkspranger opened 4 years ago

gkspranger commented 4 years ago

as a YB dev, i would like the ability to code multi-step dialogs, so that my users can interact with YB in a "deeper" manner ..

right now, my user interactions with YB are very "shallow" -- meaning :: a command is issued (with or without args), YB does the thing, task complete .. this is great for 80% of the commands i create ..

that said, i would like for my users to have the ability to have "deeper", multi-step dialogs with YB, such that ::

  1. a YB command is invoked which triggers a response, that asks for additional data (i.e. question)
  2. additional data can be pulled from the user via a YB-question/USER-response format -- a USER-response may invoke additional YB-questions
  3. YB can alter the YB-question flow based on previous USER-responses -- IF response =~ YES; THEN do X; ELSE do Y ENDIF;
  4. the final outcome will have access to all USER-responses during the dialog
  5. at any point in time, the user should be able to STOP the dialog
  6. all dialog YB-questions should have a timeout setting, that will invoke some action -- that said, maybe it is possible to "save" a dialog that a YB user can restart ??

i don't expect this to be easy, and am willing to help however i can .. if i have any say on the impl details, it is that i have used both the HUBOT version of this (https://www.npmjs.com/package/hubot-conversation) and the ERRBOT version of this (https://github.com/errbotio/err-guess-a-number) -- and i much prefer the HUBOT approach ..

please forgive my pseudo code, but it would be great if the flow of the dialog could follow something like you see below .. ONE GLARING omission is being able to have nested questions based on responses .. for example ::

;; there needs to be a clean way to nest questions based on responses
{:question
 {:text "are you older than 60"
  :response
  [{:test "yes"
    :question
    {:text "are you older than 120"}
    :response
    [{:test "yes"
      :return "you are really old"}
     {:test "no"
      :return "you are old"}]}
   {:test "no"
    :return "you are young"}]}}
;; dopey attempt at what some code might look like
(defn greeting-dialog
  "greeting-dialog # converse with the bot so you can properly be greeted"
  [_]
  (dialog
   (question "what is your name?" :name
             (response #".+"))

   (question "are you older than 40? (yes or no)" :older-than-40
             (response-if #"y(i|u)p|yes|y" true)
             (response-if #".+" false))

   (final
    (str "Hello " ((:name responses)) ". I hear you are "
         (when (not (:older-than-40 responses)) "not ")
         "older than 40."))))

(cmd-hook #"greeting-dialog"
          _ greeting-dialog)

anyway -- just wanted to open this and get the ball rolling .. thanks !!

gkspranger commented 4 years ago

so a couple of ideas i have around the functionality and the implementation (<< gulp) ..

functionality :: wouldn't it be cool if i, a normal user, could create a "dialog" using commands ?? so this would be a runtime dialog vs code-base .. obviously the end result would have to be useful, and i think a multi-stepped collection of user input that is then sent off to some "external" API could be valuable .. the final "send" action might be code-based -- but the collection of that user input could be defined by users .. THE PROBLEM i see with this is would i, the user, be able to assemble a meaningful payload that i can then send off to the "external" API .. if i can't then all of this work might not be of value ..

so just guessing at a user flow ::

user: dialog --create user-info
yb: ok, let's create a dialog .. what is your 1st question ??
user: what is your name?
yb: you said "what is your name?" .. is this correct ??
user: yes
yb: ok -- saving question .. what is your next question ??
user: how old are you?
yb: you said "how old are you?" .. is this correct ??
user: no
yb: ok -- deleting question .. what is your next question ??
user: what is your age?
yb: you said "what is your age?" .. is this correct ??
user: yes
yb: ok -- saving question .. what is your next question ??
user: stop
yb: ok -- stopping the creation of questions .. the question payload will look like:
{:question1 {:text "what is your name?"
                     :value <tbd>}
 :question2 {:text "what is your age?"
                     :value <tbd>}}
yb: what is your final action?
user: http post "https://example.com/api/v1/update-user-info" -d ~{payload}

ugh -- even typing that tho seems it might limit the usefulness of this tool .. i wonder if there was a well defined EDN or YAML schema, a user could show up with said schema and just post that to YB and YB would know what to do .. let's guess what that might look like in YAML ::

dialog:
  questions:
    - text: what is your name?
       var: name
    - text: what is your age?
       var: age
  actions:
    - command: http-post
       args:
         url: https://example.com/api/v1/update-user-info
         # the dialog_payload in this example would/could look like {"name":"Greg","age": 45}
         data: {{ dialog_payload }}
         headers:
           token: <secret>

^^ that makes me cringe a little because of my token reference, but typically you need some kind of auth to be able to post somewhere .. maybe that could be something that could be an "admin only" capability, and the YB user would have to ask said admin to set said secret in some var that can be referenced by these actions ..

anyhoo -- onto implementation .. i was thinking of that nasty nested pseudo code example i posted before, and it made me cringe a little .. so i was thinking that any "dialog API" that would allow me to easily implement multi-methods i think would be great .. that way i could easily loop on said multi-method and my code remains nice and flat and easy to reason about ..

(defmulti collect-user-info
...
(defmethod collect-user-info "name" [dialog-data]
...
(defmethod collect-user-info "age" [dialog-data]
...
(defmethod collect-user-info "update-user-info" [dialog-data]
...

(loop [dialog-data {:command "name"}]
  (if (empty? (dialog-data :command))
    dialog-data
    (recur (collect-user-info dialog-data))))

ok -- i promise this is my last cup of ☕ -- will stop now ..

gkspranger commented 4 years ago

EMAIL could be a final action for a user defined dialog ..

# do question/answer loop here
yb: Thanks for providing information related to the USER INFO questionnaire .. We appreciate your feedback ..
# email action sends to target with questionnaire data as well as Slack/adapter user data

ok -- i promise now -- no more ☕ -- last comment of the day

devth commented 4 years ago

Thanks for kicking this off @gkspranger!

wouldn't it be cool if i, a normal user, could create a "dialog" using commands

Would be awesome. I want to explore this direction.

I'm wondering if we could utilize yb's pipeline capabilities to compose a dialog flow out of primitives.

!ask --param name .{3,} what's your name? | ask --param age \d+ hi {{name}}, how old are you? | ask --param location .+ where do you live? | render thanks! your info is: name: {{name}}, age: {{age}}, location: {{location}}

Between every ask yb waits for user input from the user that initiated, using threads (Slack, Mattermost) where possible.

Now obviously this is linear without branching or logic. But I'm wondering if yb's pipelines should have generic logic capabilities (currently we do have an or command but it is very simple) that would allow the construction of a tree.

Your YAML/EDN idea is interesting. Kind of reminds me of StackStorm.

typically you need some kind of auth to be able to post somewhere

if posting via a configured command, e.g. to GitHub or JIRA, the auth would already be configured on the YB. But it does bring up a good question of how to do generic secrets in YB (have thought about this a bit before). I could see using a parameterized template that consumes configured secrets without exposing them.. kind of a separate topic though.

thinking that any "dialog API" that would allow me to easily implement multi-methods

great idea if we end up wanting to bake dialogs in via code instead of configuring them at runtime. I want to push on runtime config a bit to see how far we could take it, or determine whether it's just too unwieldy.

EMAIL could be a final action for a user defined dialog

YB does have a mail command... I haven't used it in awhile but might be fun to dust it off.

no more ☕

🤣 😂 😆 ☕

Good stuff!! 💯 Hope you refill your ☕ stores for the next round 😂

gkspranger commented 4 years ago

i really like the idea of using pipelines .. i can see where i would create an alias using ask, pipelines, and the final "action" to create runtime dialogs .. also, this is a major differentiator compared to the other bots that exist out there ..

THAT SAID -- it would be sweet if we can somehow create a cond associated with a previous ask response -- and then somehow land on a final action that is appropriate for dialog when considering all io .. not exactly what that would look like right now -- but can stew on it ..

this would be really neat if i could do this (simplistic, yes, but oh-so-powerful) ::

!alias deploy-app =
ask --param app .{3,} what app would you like to deploy? |
ask --param env (dev|qa|prod) what environment do you want to deploy to? |
jenkins --job deploy_app --params "app={{app}}&env={{env}}"
gkspranger commented 4 years ago

a hacky example at how we might add cond to user input

# || states a cond is coming
# | remains a normal pipeline "forward" action, meaning it will skip any "remaining" conds
# <| means go back to previous command - in this case they are all ask commands
# |> means go forward to declared "action" ::
#     problem is will YB be smart enough to know if this is a command or just some text,
#.    because ideally I would like the option of both

!alias deploy-app =
ask app What app do you want to deploy? ||
  cond ^stop$ |> Ending "deploy-app" dialog.
  cond ^..?$ An app name typically has 3 or more characters - please try again. <|
  cond ^.{3,}$ |
ask env what environment do you want to deploy to? ||
  cond ^stop$ |> Ending "deploy-app" dialog.
  cond ^dev|qa|prod$ |
  cond  .+ I'm sorry, I do not recognize the {{env}} environment - please try again. <|
jenkins --job deploy_app --params "app={{app}}&env={{env}}"