Yoctol / bottender

⚡️ A framework for building conversational user interfaces.
https://bottender.js.org
MIT License
4.21k stars 333 forks source link

1.0 Roadmap #435

Open chentsulin opened 5 years ago

chentsulin commented 5 years ago

planned

create-bottender-app

nice to have (planned in 1.x)

create-bottender-app
elithe1 commented 4 years ago

Hi! I found out about your framework a couple of days ago. I played a bit with it and I love it. I have some telegram bots written with telegraf. I use scenes as first class cityzens for registration flows and flows which require some questions and answers. I couldn't find anything matching on Bottender but I think it can be pretty easily implemented with your nice session and custom routing mechanisms. I saw the multiple round convo proposal but it seems to me that having something similar to telegraf scenes is more robust and flexible. I am thinking of implementing something like this myslef and use it with Bottender. What are your thoghts? Is it something you would be interested in adding?

chentsulin commented 4 years ago

Hi @elithe1, we don't have enough understanding of scenes in telegraf, so we need a few time to catch it up first. Anyway, we are open to discuss the possibilities on how to handle multiple round conversations in Bottender. If you have any implemented prototype or imagined code snippet, feel free to share with us.

cc @etrex

elithe1 commented 4 years ago

@chentsulin, @etrex I made a small MVP of what I had in mind: It is a fairly small example and it needs a lot more love ofc but I think it demonstrates pretty good how it might be looking like. For real, I would write it much differently, but still thats something to begin discussion from. Would love to hear what you think.

scene.js:

class Scene {
  constructor(id, steps, leaveSceneHandler) {
    this.id = id;
    this.steps = steps;
    this.leaveSceneHandler = leaveSceneHandler;
  }
  startScene = async (context) => {
    await context.setState({
      scene: this.id,
      step: 0,
    });
  };

  leaveScene = async (context) => {
    await this.leaveSceneHandler(context);
    await context.setState({
      scene: null,
      step: 0,
    });
  };

  invokeNextHandler = async (context) => {
    if (context.event.payload && context.event.payload === "CANCEL_SCENE") {
      await this.leaveScene(context);
      return;
    }

    await this.steps[context.state.step](context);
    await context.setState({
      step: context.state.step + 1,
    });

    if (context.state.step === this.steps.length) {
      console.log("all steps used, kinda time to leave scene");
      await this.leaveScene(context);
    }
  };
}

module.exports = Scene;

index.js:

const { route, router, text, messenger } = require("bottender/router");
const Scene = require("./scene");
async function SayHi(context) {
  await context.sendText("Hi!");
}

async function UNKNOWN(context) {
  context.log("unknown text: " + context.event.text);
  await context.sendText(
    "Hi! I'm sorry, but i'm just a bot and i don't know how to answer that.\nPlease let me call the human and he will shortly respond to your question."
  );
}

let cancelScene = {
  contentType: "text",
  title: "Cancel",
  payload: "CANCEL_SCENE",
};

// headlers for transfer scene
const handlers = [
  //handler number 0
  async (context) => {
    context.log("step 0");
    context.state.sceneData.card = context.event.text;
    await context.sendText("gimme sum", {
      quickReplies: [cancelScene],
    });
  },
  //handler number 1
  async (context) => {
    context.log("step 1");
    let sum = context.event.text;
    context.setState({
      sceneData: { ...context.state.sceneData, sum },
    });
    await context.sendText("ok transferring, sure?", {
      quickReplies: [
        {
          contentType: "text",
          title: "Yes",
          payload: "YES_SCENE",
        },
        cancelScene,
      ],
    });
  },
  //handler number 2
  async (context) => {
    context.log("step 2");
    if (context.event.payload && context.event.payload === "YES_SCENE") {
      let sum = context.state.sceneData.sum;

      await context.sendText("On it.... Might take a couple of sec... ");
      setTimeout(() => {
        context.sendText("You have been successfully charged with " + sum);
      }, 3000);
    }
  },
];

const leaveSceneHandler = async (context) => {
  await context.sendText("Leaving scene");
};

// generating scenes
const transferScene = new Scene("TransferScene", handlers, leaveSceneHandler);
// will be inside bottender.
let Scenes = {};
// scenes can be registered.
Scenes[transferScene.id] = transferScene;

async function HandlePostback(context) {
  let payload = context.event.postback.payload;
  // this starts the scene!
  if (payload === "TRANSFER") {
    await Scenes["TransferScene"].startScene(context);

    await context.sendText("Please provide me with me your member number:", {
      quickReplies: [cancelScene],
    });
  }
}

module.exports = async function App(context) {
  context.log = (msg) => console.log(msg);
  context.log("Got a msg");

  // can use a predicate in routing.
  if (context.state.scene) {
    return Scenes[context.state.scene].invokeNextHandler(context);
  }
  return router([
    // return the `SayHi` action when receiving "hi" text messages
    text("hi", SayHi),
    // return the `SayHello` action when receiving "hello" text messages
    text("*", UNKNOWN),

    messenger.postback(HandlePostback),
  ]);
};
etrex commented 4 years ago

Hi @elithe1 ,

Thank you for your sample code for Telegraf scenes.

We discussed about multiple round conversation for long time to figure out the best practice.

Our conclusion is the experiential library bottender-proposal-conversation. We continue to improve the library by users feedback.

In bottender-proposal-conversation, the basic idea is that we allow developers to control the entry point for next coming message to specified action(handler).

And then you can receive the next coming message in props.

For now, we provide a function prompt for developers to lock user in the current action for next coming message.

The benefit of prompt is that you ask a question to user and receive the user response in the same action, that is make sense when you have multiple path to the same question.

I think bottender-proposal-conversation is more flexible because of the composite pattern of bottender action.

I tried your sample code and try to mapping the idea to bottender-proposal-conversation, here is the sample code: https://gist.github.com/etrex/c52b450c04bcb6d07fac9b044428bd0f

In this demo, the user input of the conversational form is collected by bottender-proposal-conversation in the props.

There is another sample code to split transfer scene to multiple actions: https://gist.github.com/etrex/e520c8059828456a92661b3614a5dd8d

It's possible to provide more syntax sugar to reduce the repetitive code.