{"id":17648828,"url":"https://github.com/wvteijlingen/conversationalist","last_synced_at":"2025-03-30T07:42:34.131Z","repository":{"id":201585318,"uuid":"198691428","full_name":"wvteijlingen/conversationalist","owner":"wvteijlingen","description":"A UI and platform agnostic framework for creating simple or advanced chat bots using reusable dialogues","archived":false,"fork":false,"pushed_at":"2020-01-22T12:16:18.000Z","size":473,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2025-02-05T09:53:19.988Z","etag":null,"topics":["bot","chat","chat-bot","chatbot","chatbot-framework","conversation","conversational-bots","typescript"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/wvteijlingen.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null}},"created_at":"2019-07-24T18:42:12.000Z","updated_at":"2023-10-19T17:16:42.000Z","dependencies_parsed_at":null,"dependency_job_id":"5a31f2f9-8a49-421a-966c-55f5ab5b9f8a","html_url":"https://github.com/wvteijlingen/conversationalist","commit_stats":null,"previous_names":["wvteijlingen/conversationalist"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wvteijlingen%2Fconversationalist","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wvteijlingen%2Fconversationalist/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wvteijlingen%2Fconversationalist/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wvteijlingen%2Fconversationalist/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/wvteijlingen","download_url":"https://codeload.github.com/wvteijlingen/conversationalist/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":246290583,"owners_count":20753724,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["bot","chat","chat-bot","chatbot","chatbot-framework","conversation","conversational-bots","typescript"],"created_at":"2024-10-23T11:21:03.539Z","updated_at":"2025-03-30T07:42:34.096Z","avatar_url":"https://github.com/wvteijlingen.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Conversationalist\n\nConversationalist is a TypeScript framework that allows you to easily create simple or advanced chat bots using reusable dialogues.\n\n## Notable features\n\n- Interface based approach allows great flexibility.\n- Built in dialogue classes to cover most common conversation patterns.\n- Fully UI and platform agnostic. You can run this locally on a device, your own server, or in \"the cloud\".\n- No dependencies!\n\n## Table of contents\n\n* [Terminology](#terminology)\n* [Building blocks of a conversation](#building-blocks-of-a-conversation)\n* [Example: Pasta-bot](#example-pasta-bot)\n* [Sequential dialogues](#sequential-dialogues)\n* [Dialogue state](#dialogue-state)\n* [Advanced](#advanced)\n  + [Simulating human behaviour](#simulating-human-behaviour)\n  + [Attachments](#attachments)\n  + [Persistence](#persistence)\n  + [Message body vs value](#message-body-vs-value)\n  + [Undoing user responses](#undoing-user-responses)\n  + [Message flow](#message-flow)\n  + [Creating custom dialogue subclasses](#creating-custom-dialogue-subclasses)\n    - [Sending output to the user](#sending-output-to-the-user)\n    - [Example: Translator-bot](#example-translator-bot)\n    - [Showing processing state](#showing-processing-state)\n\n## Terminology\n\n- **Conversation**: All the messages that are sent between the bot and the user. \"What the user sees in the chat window\".\n- **(Chat) Bot**: The main structure that manages an entire conversation with a single end user. A bot does not contain any conversational logic itself. Instead, it manages a stack of dialogues to which it delegates. The dialogue that is on top of the dialogue stack is called the \"active dialogue\". When a chat bot receives input from a user, it passes that input on to the active dialogue. This dialogue can then act on it.\n- **Dialogue**: A structure that contains the conversational logic (i.e. which messages to send, how to respond to them etc.).\n- **Middleware**: Custom logic that sits between the bot and the dialogues.\n\n## Building blocks of a conversation\n\nEach instance of a `Bot` handles a single conversation with a single end user. A conversation is \"What the user sees in the chat window\".\n\nA conversation itself is made up of separate `Dialogues`. Dialogues are structures in your bot that contain the conversational logic. They can act like functions in your bot's program. A dialogue can receive input from the user and act on it by emitting output back to the user.\n\nAt any time there is only 1 active dialogue. This does not mean your chat bot is limited to one dialogue, a dialogue can initiate a transition to another dialogue which allows you to string them together as reusable blocks to make up a conversation.\n\n## Example: Pasta-bot\n\nThe following example dialogue is a bot that takes orders for pasta. It shows most of the basic\nfunctionality that is provided by the framework.\n\n```typescript\nimport { Bot } from \"conversationalist\"\nimport SequentialDialogue, {\n  InvalidInputError,\n  StepContext,\n  StepOutput\n} from \"conversationalist/dialogues/SequentialDialogue\"\n\ninterface Order {\n  pastaType?: string\n  sauce?: string\n}\n\ninterface State {\n  orders: Order[]\n  currentOrder: Order\n}\n\nexport default class PastaOrderDialogue extends SequentialDialogue\u003cState\u003e {\n  identifier = \"pastaOrder\"\n\n  steps = {\n    // The start method gets called automatically once the dialogue becomes active.\n    // This is the entry point of your dialogue.\n    async start(context: StepContext\u003cState\u003e): Promise\u003cStepOutput\u003cState\u003e\u003e {\n      return {\n        // The messages to send to the user.\n        messages: \"What kind of pasta would you like?\",\n\n        // Include a prompt that allows the user to pick from a predefined set of pastas.\n        // The result of this prompt will be passed into the `handlePastaType` method, as indicated by\n        // the `nextStep` field.\n        prompt: {\n          type: \"picker\",\n          choices: [\n            { body: \"Spaghetti\", value: \"spaghetti\" },\n            { body: \"Tagliatelle\", value: \"tagliatelle\" },\n            { body: \"Fusilli\", value: \"fusilli\" }\n          ]\n        },\n\n        // We can update the dialogue state by including a merged state in the step return value.\n        // Here we store a new pasta order in the state so we can populate it in subsequent steps.\n        state: { ...context.state, currentOrder: {} },\n\n        // Specify that `handlePastaType` is the next step that should be called with the result of\n        // the prompt.\n        nextStep: this.handlePastaType\n      }\n    },\n\n    async handlePastaType(context: StepContext\u003cState\u003e): Promise\u003cStepOutput\u003cState\u003e\u003e {\n      const pastaType = context.input\n\n      // We validate the user input to see if it is a valid string.\n      // If not, throw an `InvalidInputError` which will automatically reprompt the user for input.\n      if(typeof pastaType !== \"string\" || pastaType.trim().length === 0) {\n        throw new InvalidInputError(\"We don't have that pasta. Please select a pasta from our menu.\")\n      }\n\n      return {\n        messages: [\n          \"Great!\",\n          \"What sauce would you like with that?\"\n        ],\n        prompt: {\n          type: \"picker\", choices: [\n            { body: \"Bolognaise\", value: \"bolognaise\" },\n            { body: \"Carbonara\", value: \"carbonara\" },\n            { body: \"Marinara\", value: \"marinara\" }\n          ]\n        },\n        state: { ...context.state, currentOrder: { ...context.state.currentOrder, pastaType } },\n        nextStep: this.handleSauce\n      }\n    },\n\n    async handleSauce(context: StepContext\u003cState\u003e): Promise\u003cStepOutput\u003cState\u003e\u003e {\n      const sauce = context.input\n\n      if(typeof sauce !== \"string\" || sauce.trim().length === 0) {\n        throw new InvalidInputError(\"We don't have that sauce. Please select a sauce from our menu.\")\n      }\n\n      return {\n        messages: [\n          `Got it! One ${context.state.currentOrder?.pastaType} ${sauce}.`,\n          \"Would you like to add another pasta to your order?\"\n        ],\n        prompt: {\n          type: \"picker\",\n          choices: [\n            { body: \"Yes\", value: true },\n            { body: \"No, I want to finish ordering\", value: false },\n          ]\n        },\n        state: { ...context.state, currentOrder: { ...context.state.currentOrder, sauce } },\n        nextStep: this.handleAnotherOrder\n      }\n    },\n\n    async handleAnotherOrder(context: StepContext\u003cState\u003e): Promise\u003cStepOutput\u003cState\u003e\u003e {\n      // If the user wants to add another pasta, we add the current order to\n      // the array of completed orders and go back to the start step.\n      if(context.input === true) {\n        return {\n          state: { ...context.state, orders: [...context.state.orders, context.state.currentOrder] },\n          nextStep: this.start\n        }\n      }\n\n      return {\n        messages: \"Great, I just need your address so I know where to send your delicious pasta.\",\n        prompt: { type: \"text\" },\n        nextStep: this.finishOrder\n      }\n    },\n\n    async finishOrder(context: StepContext\u003cState\u003e): Promise\u003cStepOutput\u003cState\u003e\u003e {\n      const address = context.input\n\n      if(typeof address !== \"string\" || address.trim().length === 0) {\n        throw new InvalidInputError(\"Hmm, I cannot find that address. Please enter a valid address.\")\n      }\n\n      // Initiate the pasta delivery in the back-end.\n      await DeliveryService.deliver({\n        orders: context.state.orders\n        address,\n      })\n\n      const pdfReceipt = await DeliveryService.generatePDFReceipt({\n        orders: context.state.orders\n        address,\n      })\n\n      return {\n        messages: [\n          \"Your pasta is on its way! Thank you for ordering with pasta-bot.\",\n\n          // You can also return messages that include an attachment.\n          // In this case, we attach a URL attachment with the link to a PDF receipt.\n          {\n            body: \"Here is a link to your receipt as a PDF.\"\n            attachment: {\n              type: \"url\",\n              href: pdfReceipt\n            }\n          }\n        ]\n      }\n    }\n  }\n}\n\n// Create a new bot with the dialogue and start it.\nconst dialogue = new PastaOrderDialogue({\n  state: { orders: [] }\n})\nconst bot = new Bot(dialogue)\nbot.start()\n```\n\n## Sequential dialogues\n\nTBD: Explain how the `SequentialDialogue` works.\n\n## Dialogue state\n\nEach dialogue contains internal state. This state can contain things such as saved user responses (e.g. the user's name), external dependencies, and more. What you put into the state is up to you. In it's most basic form, it is an empty object.\n\nThe state is also used when persisting a snapshot of the dialogue. See advanced usage \u003e persistence.\n\n## Advanced\n\n### Simulating human typing behaviour\n\nNo human can instantaneously respond to incoming messages. They require some time to read the message, think of a response, and type the response. Conversationalist comes with the tools to easily simulate this behaviour and make your bot feel much more human.\n\nYou can funnel messages through a `DelayedTypingEmitter` instance to simulate reading and typing delay. A `DelayedTypingEmitter` coalesces all bot events into a single callback, allowing you to update your UI in one place:\n\n```typescript\nimport { DelayedTypingEmitter } from \"conversationalist\"\nimport TranslatorDialogue from \"./TranslatorDialogue\"\n\nconst dialogue = new TranslatorDialogue()\nconst bot = new Bot(dialogue)\n\nconst emitter = new DelayedTypingEmitter(bot, {\n  readingDelay: 500 // Simulate the bot taking 0.5 seconds to \"read\" a message before starting to \"type\".\n  typingDelay: 1500 // Simulate the bot taking 1.5 seconds to \"type\" a message.\n})\n\nemitter.events.update.on(({ isTyping, allMessages, addedMessages, prompt } =\u003e {\n  // Update your UI here\n  ui.showTypingIndicator = isTyping\n  ui.chatMessages = allMessages\n  ui.userInputPrompt = prompt\n})\n```\n\n### Attachments\n\nA sent or received message is not restricted to text only. Both a BotMessage and a UserMessage can contain an attachment. The structure of an attachment is generic, it is up to you to define the types of attachments that make sense for your use case.\n\n### Persistence\n\nTBD: Explain snapshots.\n\n### Message body vs value\n\nTDB: Explain the difference between a message body and value.\n\n### Undoing user responses\n\nTBD: Explain undoing of user responses and rewinding.\n\n### Message flow\n\nWhen the user sends input to a chat bot, it is handled in the following way:\n\n1. The user sends input to the bot.\n2. The bot invokes each `before` middleware with the input, giving the middleware a change to inspect it and perform any desired side effect.\n3. The bot sends the user input to the currently active dialogue.\n4. The dialogue receives input as `DialogueInput`, acts on it, and emits `DialogueOutput` as as reponse.\n5. The output is passed back to the bot.\n6. The bot invokes each `after` middleware with the output, giving the middleware a change to inspect it and perform any desired side effect.\n7. The bot transforms the output to a series of chat messages and adds those to the message log. It also emits certain events to let the developer know that the message log has changed.\n\n### Creating custom dialogue subclasses\n\nThe easiest way to start with Conversationalist is to use the built-in `SequentialDialogue` class. This style of dialogue fits most use cases, and allows you to quickly get started.\n\nIf you need more control over the dialogue logic, you can also create your own dialogue classes by implementing the `Dialogue` interface. This allows you to fully customize the logic for your dialogue.\n\nCreating a custom dialogue is as \"simple\" as creating a class that implements the `Dialogue` protocol. You can handle user input via the `onReceiveInput` method, and emit outpout calling the `output` event. The way you structure the internal dialogue logic is completely up to you.\n\n#### Sending output to the user\n\nTo output one or more messages to the user, a dialogue must emit a `DialogueOutput` object by calling its `events.output` callback. Usually a dialogue will emit output in response to receiving input, but it also perfectly valid to emit output without receiving input from the user. See Dialogue.ts\n\n[API documentation for DialogueOutput](docs/interfaces/_dialogue_.dialogueoutput.html)\n\nOutput can contain data such as:\n\n- One or more messages or attachments to send to the user.\n- The input UI that is available for the user to respond.\n- Whether the dialogue is finished.\n- A next dialogue to transition to.\n\n#### Showing a typing state\n\nWhen your custom dialogue receives input that you plan on handling, you can call the `events.outputStart` callback which will indicate to the bot that the dialogue has received the input and is working on a response. Firing this callback will cause the bot to update its `isActive` flag and fire an `activeChanged` event, allowing you to show a typing indicator in your UI.\n\n**Note: The built in SequentialDialogue automatically calls `outputStart` when it receives a response.**\n\n#### Example: Translate-o-bot\n\nThe following example is a never-ending dialogue that translates user input using an async call to a third party:\n\n```typescript\nclass TranslatorDialogue implements Dialogue\u003c{}\u003e {\n  readonly identifier = \"translate\"\n  events: DialogueEvents = {}\n\n  get snapshot() {\n    return undefined\n  }\n\n  onStart() {\n    this.events.output?.({\n      body: [\n        \"Hi, I am translate-o-bot!\",\n        \"Say anything, and I will translate it for you.\"\n      ]\n    }, false)\n  }\n\n  async onReceiveInput(input: DialogueInput) {\n    this.events.outputStart?.()\n\n    const translation = await ThirdPartyTranslator.translate(input)\n\n    this.events.output?.({\n      body: translation\n    }, false)\n  }\n}\n\nconst dialogue = new TranslatorDialogue()\n\n// Create a new bot with the dialogue and start it.\nconst bot = new Bot(dialogue)\nbot.start()\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwvteijlingen%2Fconversationalist","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fwvteijlingen%2Fconversationalist","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwvteijlingen%2Fconversationalist/lists"}