microsoft / botframework-sdk

Bot Framework provides the most comprehensive experience for building conversation applications.
MIT License
7.5k stars 2.44k forks source link

Custom Adapter receives error: [onTurnError] unhandled error: TypeError: bot.reply is not a function #6373

Closed KittPhi closed 3 years ago

KittPhi commented 3 years ago

Hello, We've created a Custom Botbuilder Adapter to connect to the Vonage API called botbuilder-adapter-vonage-js. To test the Adapter's basic functionality, with a basic bot reply, we send an sms to the Vonage number and should get a sms reply back "Hello Back", but instead receive the error below.

[onTurnError] unhandled error: TypeError: bot.reply is not a function

Not sure how to actually debug the Custom Adapter to find where it is broken.

Would be great to find someone familiar with the either the Botkit Core library and Botkit Platform Adapters could help with this. I've attached the Express Server (webhook-server.js) below.

// webhook-server.js
require('dotenv').config();
const express = require('express');
const app = express();
const port = 3000;
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
const SendMessagesAPI = require('./Vonage-SEND-messages-api');
const VonageAdapter = require('botbuilder-adapter-vonage-js');
const Botkit = require('botkit');

const {
  BotFrameworkAdapter,
  InspectionMiddleware,
  MemoryStorage,
  InspectionState,
  UserState,
  ConversationState,
} = require('botbuilder');

const { MicrosoftAppCredentials } = require('botframework-connector');

// This bot's main dialog.
const { IntersectionBot } = require('./bot');
const { Message } = require('@vonage/server-sdk');

const creds = {
  apiKey: process.env.VONAGE_API_KEY,
  apiSecret: process.env.VONAGE_API_SECRET,
  applicationId: process.env.VONAGE_APPLICATION_ID,
  privateKey: process.env.VONAGE_APPLICATION_PRIVATE_KEY_PATH,
};
const config = {
  to_number: process.env.TO_NUMBER,
  from_number: process.env.FROM_NUMBER,
  // enable_incomplete: true
};

// Create Adapter
const adapter = new VonageAdapter(creds, config);

// Create the Storage provider and the various types of BotState.
const memoryStorage = new MemoryStorage();
const inspectionState = new InspectionState(memoryStorage);
const userState = new UserState(memoryStorage);
const conversationState = new ConversationState(memoryStorage);

// Create and add the InspectionMiddleware to the adapter.
adapter.use(
  new InspectionMiddleware(
    inspectionState,
    userState,
    conversationState,
    new MicrosoftAppCredentials(
      process.env.MicrosoftAppId,
      process.env.MicrosoftAppPassword
    )
  )
);

app.post('/webhooks/dlr', (req, res) => {
  res.status(200).end();
});

// Catch-all for errors.
adapter.onTurnError = async (, error) => {
  // This check writes out errors to console log .vs. app insights.
  // NOTE: In production environment, you should consider logging this to Azure
  //       application insights. See https://aka.ms/bottelemetry for telemetry
  //       configuration instructions.
  console.error(`\n [onTurnError] unhandled error: ${error}`);

  // Send a trace activity, which will be displayed in Bot Framework Emulator
  await .sendTraceActivity(
    'OnTurnError Trace',
    `${error}`,
    'https://www.botframework.com/schemas/error',
    'TurnError'
  );

  // Send a message to the user
  await .sendActivity('The bot encountered an error or bug.');
  await .sendActivity(
    'To continue to run this bot, please fix the bot source code.'
  );
  // Clear out state
  await conversationState.clear();
};

// Create the main dialog.
const bot = new IntersectionBot(conversationState, userState);

// Listen for incoming requests.
app.post('/webhooks/inbound', (req, res) => {
  console.log('/webhooks/inbound req.body', req.body);
  adapter.processActivity(req, res, async () => {

    console.log(context);
    // [onTurnError] unhandled error: TypeError: Cannot read property 'from' of undefined
    // await bot.run();

     //  [onTurnError] unhandled error: TypeError: .reply is not a function
    // await .reply('I heard a message!');

    // [onTurnError] unhandled error: TypeError: bot.reply is not a function
    await bot.reply('Hello Back!');

  });
  res.status(200).end();
});

app.post('/webhooks/status', (req, res) => {
  res.status(200).end();
});

app.listen(port, () => {
  console.log(`🌏 Server running at http://localhost:${port}`);
});

Response

🌏 Server running at http://localhost:3000
/webhooks/inbound req.body {
  message_uuid: 'e93a3007-f7a5-436a-8ba7-c46d64343d80',
  to: { type: 'sms', number: '12018994297' },
  from: { type: 'sms', number: '15754947000' },
  timestamp: '2021-08-27T21:14:51.228Z',
  usage: { price: '0.0057', currency: 'EUR' },
  message: {
    content: { type: 'text', text: 'Hello' },
    sms: { num_messages: '1' }
  },
  direction: 'inbound'
}
TurnContext {
  _respondedRef: { responded: false },
  _turnState: TurnContextStateCollection(2) [Map] {
    'httpStatus' => 200,
    Symbol(state) => { state: [Object], hash: '{}' }
  },
  _onSendActivities: [],
  _onUpdateActivity: [],
  _onDeleteActivity: [],
  _turn: 'turn',
  _locale: 'locale',
  bufferedReplyActivities: [],
  _adapter: VonageAdapter {
    middleware: MiddlewareSet { middleware: [Array] },
    BotIdentityKey: Symbol(BotIdentity),
    OAuthScopeKey: Symbol(OAuthScope),
    name: 'Vonage Adapter',
    middlewares: null,
    botkit_worker: [class VonageBotWorker extends BotWorker],
    credentials: {
      apiKey: '4f2ff535',
      apiSecret: 'jtYzPbh3MXr8M1Hr',
      applicationId: '978500cf-7ea8-4d7b-ac54-2b42f67b28a2',
      privateKey: './private.key'
    },
    options: {},
    to_number: '15754947000',
    from_number: '12018994297',
    enable_incomplete: undefined,
    turnError: [AsyncFunction (anonymous)]
  },
  _activity: {
    id: 'e93a3007-f7a5-436a-8ba7-c46d64343d80',
    timestamp: 2021-08-27T21:14:39.573Z,
    channelId: 'vonage-sms',
    conversation: { id: '15754947000' },
    from: { id: '15754947000' },
    recipient: { id: '12018994297' },
    text: 'Hello',
    channelData: {
      message_uuid: 'e93a3007-f7a5-436a-8ba7-c46d64343d80',
      to: [Object],
      from: [Object],
      timestamp: '2021-08-27T21:14:51.228Z',
      usage: [Object],
      message: [Object],
      direction: 'inbound'
    },
    type: 'message'
  }
}

 [onTurnError] unhandled error: TypeError: bot.reply is not a function
LeeParrishMSFT commented 3 years ago

Closed as duplicate of Custom Adapter receives error: [onTurnError] unhandled error: TypeError: bot.reply is not a function #3912

LeeParrishMSFT commented 3 years ago

Re-open this issue, I'll close the other duplicate and leave this one open as Steven is assigned to look into this one.

stevkan commented 3 years ago

It looks like your custom adapter is built using both Botkit and BotFramework similar to other Botkit adapters. However, your bot's implementation aligns more with a bot built only for BotFramework yet you are trying to call the reply() method that belongs to a Botkit bot.

For example, in Botkit's 'botbuilder-adapter-twilio-sms' adapter, you are presented with two ways to utilize the adapter. In the first, under Botkit Basics, an adapter is created which is then consumed by Botkit to form the controller. This then allows you to access the reply() method callable from a Botkit bot.

In the second, under BotBuilder Basics, an adapter is created which is then utilized within the Express server's /api/messages endpoint. Inbound messages are passed to the adapter's processActivity() method where the bot then responds using the sendActivity() method, callable from within a BotFramework adapter's context.

Narrowing down which implementation you intend to use I believe will alleviate the error you are receiving.

KittPhi commented 3 years ago

Hi @stevkan, thanks for pointing that out. I've updated some code and have been trying to get it to work with Botbuilder. The server and custom allows me to send an echo reply back, but how would I go about connecting it to a simple bot, the code below just allows an echo bot. Currently, I see a problem in my custom adapter. I believe that the processActivity function is not passing the incoming activity to the bot. Comparing it to the other adapters, I don't understand at exactly which part in the adapter where the incoming activity is passed to the Bot to process. Thank you

Updated to:

// Listen for incoming requests.
app.post('/webhooks/inbound', (req, res) => {

  vonageAdapter.processActivity(req, res, async () => {
   // Do something with this incoming activity!
      if (TurnContext.activity.type === 'message') {
        // Get the user's text
        const utterance = TurnContext.activity.text;

        await vonageAdapter.sendWAActivities(request);
      }
  });
  res.status(200).end();
});
// VonageAdapter.js
require('dotenv').config();
const { BotWorker } = require('botkit');
const {
  ConversationAccount,
  ChannelAccount,
  BotAdapter,
  Activity,
  ActivityTypes,
  TurnContext,
  ConversationReference,
  ResourceResponse,
} = require('botbuilder');
const Debug = require('debug');
const debug = Debug('botkit:vonage');
const Vonage = require('@vonage/server-sdk');
const CredentialsObject = require('@vonage/server-sdk');

/**
 * This is a specialized version of [Botkit's core BotWorker class](core.md#BotWorker) that includes additional methods for interacting with Vonage.
 * It includes all functionality from the base class, as well as the extension methods below.
 *
 * When using the VonageAdapter with Botkit, all `bot` objects passed to handler functions will include these extensions.
 */
class VonageBotWorker extends BotWorker {
  /**
   * A copy of the Vonage Message API client giving access to `let res = await bot.api.callAPI(path, method, parameters);`
   *
   * `getConfig()` is used to get the config, which contains the options passed to the constructor.
   */
  constructor(Vonage) {
    this.api = Vonage;
  }
  async startConversationWithUser(userId) {
    return this.changeContext({
      channelId: 'vonage',
      conversation: { id: userId },
      bot: { id: this.controller.getConfig('from_number'), name: 'bot' },
      user: { id: userId },
    });
  }
}
/**
 *
 */
class VonageCredentialsObject extends CredentialsObject {
  constructor(CredentialsObject) {
    this.credentials = CredentialsObject;
  }
}

module.exports = class VonageAdapter extends BotAdapter {
  constructor(VonageCredentialsObject, config) {
    super();
    this.name = 'Vonage Adapter';
    this.middlewares = null;
    this.botkit_worker = VonageBotWorker;
    this.credentials = VonageCredentialsObject;
    this.options = {};
    this.to_number = config.to_number;
    this.from_number = config.from_number;
    this.enable_incomplete = config.enable_incomplete;
  }
  validation() {
    if (!this.credentials.apiKey || !this.credentials.applicationId) {
      const err =
        'Either apiKey or applicationId is required part of the configuration';
      if (!this.enable_incomplete) {
        throw new Error(err);
      } else {
        console.error(err);
      }
    }

    if (!this.credentials.apiSecret || !this.credentials.privateKey) {
      const err =
        'Either apiKey or applicationId is required part of the configuration';
      if (!this.enable_incomplete) {
        throw new Error(err);
      } else {
        console.error(err);
      }
    }

    if (!this.to_number || !this.from_number) {
      const err =
        'Both to_number and from_number are required parts of the configuration';
      if (!this.enable_incomplete) {
        throw new Error(err);
      } else {
        console.error(err);
      }
    }

    // this.enable_incomplete
    if (true) {
      const warning = [
        '',
        '****************************************************************************************',
        '* WARNING: Your adapter may be running with an incomplete/unsafe configuration.        *',
        '* - Ensure all required configuration toFrom are present                              *',
        '* - Disable the "enable_incomplete" option!                                            *',
        '****************************************************************************************',
        '',
      ];
      console.warn(warning.join('\n'));
    }

    try {
      this.api = new Vonage(this.credentials, this.options);
    } catch (err) {
      if (err) {
        if (!this.enable_incomplete) {
          throw new Error(err);
        } else {
          console.error(err);
        }
      }
    }

    this.middlewares = {
      spawn: [
        async (bot, next) => {
          // make the Vonage API available to the bot instance.
          bot.api = this.api;
          next();
        },
      ],
    };
  }
  /**
   * Converts a BotBuilder Activity Object into an outgoing Vonage Messages API.
   * @param activity A BotBuilder Activity object
   * @returns a Vonage Messages API object
   */
  activityToVonage(activity) {
    // const message = {
    //   message: {
    //     content: {
    //       text: activity.channelData.message.text
    //     }
    //   },
    //   from: activity.channelData.from.number,
    //   to: activity.channelData.to.number,
    // };
    // const message = `{ type: 'sms', number: ${this.to_number} },{ type: 'sms', number: ${this.from_number} },{ content: { type: 'text', text: ${activity} } }`;
    // const message =
    //   ({ type: 'sms', number: activity.channelData.from.number }, // REVERSED NUMBERS
    //   { type: 'sms', number: activity.channelData.to.number }, // REVERSED NUMBERS
    //   { content: { type: 'text', text: activity.channelData.message.text } });
    // return message;
  } // END activityToVonage

  /** Message API <<< SendActiviy <<< Bot
   * Standard BotBuilder adapter method to send a message from the bot to the messaging API.
   * [BotBuilder reference docs](https://docs.microsoft.com/en-us/javascript/api/botbuilder-core/botadapter?view=botbuilder-ts-latest#sendactivities).
   * @param context A TurnContext representing the current incoming message and environment. (Not used)
   * @param activities An array of outgoing activities to be sent back to the messaging API.
   */
  async sendActivities(context, activities) {
    // const sendMessageOverChannel = promisify(this.api.channel.send); // HANGS AFTER processActivity > req.body: 15754947093
    console.log('sendActivities > context:', context);

    // console.log(this.api); // undefined

    const vonage = new Vonage({
      apiKey: process.env.VONAGE_API_KEY,
      apiSecret: process.env.VONAGE_API_SECRET,
      applicationId: process.env.VONAGE_APPLICATION_ID,
      privateKey: process.env.VONAGE_APPLICATION_PRIVATE_KEY_PATH,
    });

    vonage.channel.send(
      { type: 'sms', number: this.to_number },
      { type: 'sms', number: this.from_number },
      {
        content: {
          type: 'text',
          text: context,
        },
      },
      (err, data) => {
        if (err) {
          console.error(err);
        } else {
          console.log('message_uuid: ', data.message_uuid);
        }
      }
    );

    // DOESN'T WORK < < < < < <
    const responses = [];
    for (let a = 0; a < activities.length; a++) {
      const activity = activities[a];
      if (
        activity.type === ActivityTypes.Message ||
        activity.type === ActivityTypes.Event
      ) {
        const message = this.activityToVonage(activity);
        console.log('message:', message);
        debug('message: ', message);
        try {
          // const { message_uuid } = await sendMessageOverChannel(message);
          // // const { message_uuid } = await sendMessageOverChannel(
          // //   activity.channelData.to.number,
          // //   activity.channelData.from.number,
          // //   message
          // // );
          const res = await this.api.channel.send(message);
          if (res) {
            responses.push({ id: res.message_uuid });

            // const { message_uuid } = await this.api.channel.send(message);
            // responses.push({ id: message_uuid });
            // debug("message_uuid: ", message_uuid)
          } else {
            debug('RESPONSE FROM VONAGE > ', res);
          }
        } catch (err) {
          debug('Error sending activity to Vonage', err);
        }
      } else {
        debug(
          'Unknown message type encountered in sendActivities: ',
          activity.type
        );
      }
    }
    return responses;
  } // END sendActivities

  async updateActivity(context, activity) {
    debug('Vonage SMS does not support updating activities.');
  }

  async deleteActivity(context, reference) {
    debug('Vonage SMS does not support deleting activities.');
  }

  /**
   * Standard BotBuilder adapter method for continuing an existing conversation based on a conversation reference.
   * [BotBuilder reference docs](https://docs.microsoft.com/en-us/javascript/api/botbuilder-core/botadapter?view=botbuilder-ts-latest#continueconversation)
   * @param reference A conversation reference to be applied to future messages.
   * @param logic A bot logic function that will perform continuing action in the form `async(context) => { ... }`
   */
  async continueConversation(reference, logic) {
    const request = TurnContext.applyConversationReference(
      { type: 'event', name: 'continueConversation' },
      reference,
      true
    );
    const context = new TurnContext(this, request);

    return this.runMiddleware(context, logic);
  }

  /**
   * Standard BotBuilder adapter method for continuing an existing conversation based on a conversation reference.
   * [BotBuilder reference docs](https://docs.microsoft.com/en-us/javascript/api/botbuilder-core/botadapter?view=botbuilder-ts-latest#continueconversation)
   * @param reference A conversation reference to be applied to future messages.
   * @param logic A bot logic function that will perform continuing action in the form `async(context) => { ... }`
   */
  async continueConversation(reference, logic) {
    const request = TurnContext.applyConversationReference(
      { type: 'event', name: 'continueConversation' },
      reference,
      true
    );
    const context = new TurnContext(this, request);

    return this.runMiddleware(context, logic);
  }

  /** Message API >>> ProcessActivity >>> Bot
   * Accept an incoming webhook request and convert it into a TurnContext which can be
   * processed by the bot's logic.
   * @param req A request object from Restify or Express
   * @param res A response object from Restify or Express
   * @param logic A bot logic function in the form `async(context) => { ... }`
   */
  async processActivity(req, res, logic) {
    try {
      //console.log(req.body,res);
      const event = req.body;
      console.log('processActivity > req.body: ' + event.from.number);
      const activity = {
        id: event.message_uuid,
        timestamp: new Date(),
        channelId: 'vonage-sms',
        conversation: {
          id: event.from.number,
        },
        from: {
          id: event.from.number,
        },
        recipient: {
          id: event.to.number,
        },
        text: event.message.content.text,
        channelData: event,
        type: ActivityTypes.Message, // ActivityTypes.Event
      };

      // create a conversation reference
      const context = new TurnContext(this, activity);

      context.turnState.set('httpStatus', 200);

      await this.runMiddleware(context, logic);

      // send http response back
      res.status(context.turnState.get('httpStatus'));
      if (context.turnState.get('httpBody')) {
        res.send(context.turnState.get('httpBody'));
      } else {
        res.end();
      }
    } catch (err) {
      debug('Error sending message over channel', err);
    }
  }

}; // END VonageAdapter
compulim commented 3 years ago

@stevkan Ping.