Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proactive Messaging Documentation #787

Closed
JeffCordes opened this issue Jul 24, 2018 · 34 comments

Comments

@JeffCordes
Copy link

commented Jul 24, 2018

bot framework has an extra endpoint at api/messages/proactive?BotAppId={appId} to receive post information for proactive messages. This is hidden and not mentioned in the current documentation here . This is also not mentioned on the default index page, only api/messages is.

Can we add sample code and documentation of how to post a proactive message to this endpoint, expected headers, required parameters in the json posted.

It seems like the current proactive example of using adapter.ContinueConversation is limited because it rely on the life-cycle of Ibot and adapter/context not being disposed, and they are re-instantiated and disposed every message.

@JeffCordes JeffCordes changed the title Poactive Messaging Documentation Proactive Messaging Documentation Jul 24, 2018

@drub0y

This comment has been minimized.

Copy link
Member

commented Jul 24, 2018

So this isn't documented because it's still in a state of flux. I would not suggest relying on that endpoint making the cut, but there's a discussion about it going on here in PR #461 where I was attempting to clean up the endpoint a bit and make it based on events (which #705 recently also moved the internal APIs toward now).

If you feel like this endpoint should be provided "in-the-box" please weigh in as to why. Otherwise take a look at some of the other discussion around Direct Line and how you might use that instead. Finally, the purely "do it yourself route" is always an option which is: write your own API controller and invoke the BotAdapter::ContinueConversation yourself from within that. The only wrinkle there right now is that the BotAdapter is only made available as a dependency to you in the more recent builds of the SDK (i.e. post -preview) which I don't think are available as a public package yet.

@JeffCordes

This comment has been minimized.

Copy link
Author

commented Jul 25, 2018

I think there is a business need for having the api endpoints, as well as connectors for other things in the future like triggering off of event grid subscription or an azure function output. I think these should be supported in an effort to make azure products compatible with other azure products. I am a consultant for a MS partner and the top ask from most companies interested in bots are handing off to live chat services, and sending proactive messages from external triggers in their architecture. Its usually the top ask because its not well documented or supported. It's probably the biggest bot "pain point" we are facing in the field. Having a well documented standard REST api to do this would be a big win.

Directline works for some things, but the real issue with it is that it is based on web sockets instead of REST and it does not fit into many architectures based on microservice ideas with cosmos/functions/eventgrid. And those are the types of solutions we are making. DL is better for the client side, like making a native mobile or desktop chat client where spawning and keeping alive listener threads is appropriate.

I was also excited about adapters at first thinking that would be the layer to tie other triggers in. The issue there is that the inbound (processactivity) and outbound (sendActivity) are tied together in the interface, and what we really needed to do was receive activities from one, and send activities on another. Since the adapter is in the context constructor and readonly, you can't change paths and route something back out to bot framework if you make a custom adapter (unless you rewrite all the code to talk to the bot framework services and that seems unnecessary).

You also could not create a custom API and call the existing initialized BotFrameworkAdapter because you could not access it (It sounds like your saying this was fixed for future builds with dependency injection). You could not Initialize your own BotFrameworkAdapter because of its constructor require things in extension classes. I guess you probably could, but that would be a pain also. You can't just initialize Ibot and call its onTurn because you need a context to pass it and context requires adapter.

Also the documented sample labeled for proactive messages does not actually send a proactive message. It just sleeps for 5 seconds in the on turn method then sends a normal message after holding the context so it does not get disposed. It does not solve the issue of sending a proactive message when you do not have ref of the context or adapter.

In any case after running down all the paths mentioned above trying to find a way to accomplish a real proactive messages, I found the proactive messages API in the source code. I would support it staying.

I do have the source pulled, and will try to see if I can access the botframework adapter through DI now. Is that logic in master or out on another branch?

@JonathanFingold

This comment has been minimized.

Copy link
Collaborator

commented Aug 4, 2018

@JeffCordes I have a proof of concept proactive message bot that will send the proactive message in response to receiving an event activity with a specific name. The bot will send the proactive message to the appropriate conversation in response to the event. If this will address your issue, I can include more details. The bot is written against the daily builds, but the methodology should work against the current public NuGet packages, too, with just a little tweaking.

@JeffCordes

This comment has been minimized.

Copy link
Author

commented Aug 6, 2018

@JonathanFingold I would like to look at the POC that you have made to see if it will fit our needs. We can run against the current version to review this also.

@JonathanFingold

This comment has been minimized.

Copy link
Collaborator

commented Aug 6, 2018

@JeffCordes , here's the POC, using daily build package v4.0.0.37444 of Microsoft.Bot.Builder.Integration.AspNet.Core. There may still be some breaking changes to state classes in the pipe, so double check which version you use.

  1. Start from the VSIX template.
  2. Update the Microsoft.Bot.Builder.Integration.AspNet.Core NuGet package for your project.
  3. Important: Update appsettings.json with a valid App ID and password. (Proactive messaging requires a functioning App ID.)
  4. Rename EchoState.cs to JobData.cs and update the contents. This will define the type of data to store in state for each job.
    using Microsoft.Bot.Schema;
    
    /// <summary>
    /// Class for storing job state.
    /// </summary>
    public class JobData
    {
    	public int JobNumber { get; set; } = 0;
    	public bool Completed { get; set; } = false;
    
    	/// <summary>
    	/// The conversation reference to which to send status updates.
    	/// </summary>
    	public ConversationReference Conversation { get; set; }
    }
  5. Add a JobState.cs class. This will define a generalized bot state middleware class to use for managing job state. This state will not be tied to a specific user or conversation.
    using Microsoft.Bot.Builder;
    
    public class JobState : BotState
    {
    	private const string StorageKey = "PractiveBot.JobState";
    
    	public JobState(IStorage storage) : base(storage, StorageKey) { }
    
    	protected override string GetStorageKey(ITurnContext context) => StorageKey;
    }
  6. Add a StateAccessors class. This defines a property accessor we can use to access the job log information that will be saved to state.
    using System.Collections.Generic;
    using Microsoft.Bot.Builder;
    
    public class StateAccessors
    {
        public static string JobDataName = $"{nameof(StateAccessors)}.{nameof(Proactive.JobData)}";
    
        public IStatePropertyAccessor<Dictionary<int, JobData>> JobData { get; set; }
    }
  7. Update Startup.cs created by the template. Specifically, we just need to update the using statements and the ConfigureServices method.
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using Microsoft.AspNetCore.Builder;
    using Microsoft.AspNetCore.Hosting;
    using Microsoft.Bot.Builder;
    using Microsoft.Bot.Builder.BotFramework;
    using Microsoft.Bot.Builder.Integration;
    using Microsoft.Bot.Builder.Integration.AspNet.Core;
    using Microsoft.Bot.Builder.TraceExtensions;
    using Microsoft.Extensions.Configuration;
    using Microsoft.Extensions.DependencyInjection;
    using Microsoft.Extensions.Options;
    public void ConfigureServices(IServiceCollection services)
    {
    	services.AddBot<ProactiveBot>(options =>
    	{
    		options.CredentialProvider = new ConfigurationCredentialProvider(Configuration);
    
    		// Set up error handling. (Trace output goes to the Emulator log, but not to the user.)
    		options.OnTurnError = async (context, exception) =>
    		{
    			await context.TraceActivityAsync("Proactive bot exception", exception);
    			await context.SendActivityAsync("Sorry, it looks like something went wrong!");
    		};
    
    		// Set up state management middleware.
    		IStorage dataStore = new MemoryStorage();
    		var state = new JobState(dataStore);
    		options.Middleware.Add(state);
    	});
    
    	// Create and register the job log state property accessor.
    	services.AddSingleton(sp =>
    	{
    		var options = sp.GetRequiredService<IOptions<BotFrameworkOptions>>().Value
    			?? throw new InvalidOperationException(
    				"BotFrameworkOptions must be configured prior to setting up the state accessors.");
    
    		var jobState = options.Middleware.OfType<JobState>().FirstOrDefault()
    			?? throw new InvalidOperationException(
    				"Job state must be defined and added before adding conversation-scoped state accessors.");
    
    		return new StateAccessors
    		{
    			JobData = jobState.CreateProperty<Dictionary<int, JobData>>(StateAccessors.JobDataName),
    		};
    	});
    }
  8. Update your bot file. (I've renamed my bot to ProactiveBot.)
    using System;
    using System.Collections.Generic;
    using System.Threading;
    using System.Threading.Tasks;
    using Microsoft.Bot.Builder;
    using Microsoft.Bot.Schema;
    using Microsoft.Extensions.Configuration;
    
    public class ProactiveBot : IBot
    {
        /// <summary>
        /// Random number generator for job numbers.
        /// </summary>
        private static Random NumberGenerator { get; } = new Random();
    
        public const string JobCompleteEventName = "jobComplete";
    
        private string AppId { get; }
    
        private StateAccessors StateAccessors { get; }
    
        public ProactiveBot(StateAccessors accessors, IConfiguration configuration)
        {
            this.StateAccessors = accessors ?? throw new ArgumentNullException(nameof(accessors));
            this.AppId = configuration["MicrosoftAppId"] ??
                throw new ArgumentNullException(
                    nameof(configuration),
                    "The bot must have an App ID in order to send proactive messages.");
        }
    
        public async Task OnTurnAsync(ITurnContext context, CancellationToken token)
        {
            if (context.Activity.Type != ActivityTypes.Message)
            {
                await HandleSystemActivity(context, token);
            }
            else
            {
                var text = context.Activity.AsMessageActivity()?.Text.Trim().ToLowerInvariant();
                switch (text)
                {
                    case "run":
                    case "run job":
                        var jobLog = await StateAccessors.JobData.GetAsync(context,()=>new Dictionary<int, JobData>());
                        var jobData = CreateJob(context, jobLog);
    
                        await context.SendActivityAsync($"We've started job {jobData.JobNumber} for you. We'll notify you when it's complete.");
                        break;
    
                    default:
                        // Check whether this is simulating a job completion event.
                        var parts = text?.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
                        if (parts?.Length is 2 && parts[0] is "done"
                            && int.TryParse(parts[1], out int jobNumber))
                        {
                            // If so, complete the job.
                            await ProcessJobCompletion(context, jobNumber);
                        }
                        else
                        {
                            // Otherwise, reply with a default message.
                            await context.SendActivityAsync(
                                "Type `run` or `run job` to start a new job." +
                                Environment.NewLine + Environment.NewLine +
                                "Or, type `done <jobNumber>` to simulate an event that signals that the job is complete.");
                        }
                        break;
                }
            }
        }
    
        private async Task HandleSystemActivity(ITurnContext context, CancellationToken token)
        {
            await context.SendActivityAsync($"Received an activity of type {context.Activity.Type}.");
    
            if (context.Activity.Type is ActivityTypes.Event)
            {
                var activity = context.Activity.AsEventActivity();
                if (activity.Name is JobCompleteEventName && activity.Value is int jobNumber)
                {
                    await ProcessJobCompletion(context, jobNumber);
                }
            }
        }
    
        private JobData CreateJob(ITurnContext context, Dictionary<int, JobData> jobLog)
        {
            // Generate a non-duplicate job number;
            int number;
            while (jobLog.ContainsKey(number = NumberGenerator.Next())) { }
    
            // Simulate creaing the job and logging it.
            var jobData = new JobData
            {
                JobNumber = number,
                Conversation = context.Activity.GetConversationReference(),
            };
            jobLog.Add(jobData.JobNumber, jobData);
    
            // Return the created job.
            return jobData;
        }
    
        private async Task ProcessJobCompletion(ITurnContext context, int jobNumber)
        {
            var jobLog = await StateAccessors.JobData.GetAsync(context);
            if (jobLog.ContainsKey(jobNumber))
            {
                var job = jobLog[jobNumber];
                if (job.Completed)
                {
                    await context.SendActivityAsync($"_error_ >>> job {jobNumber} completed earlier.");
                }
                else
                {
                    CompleteJob(context.Adapter, AppId, jobLog[jobNumber].Conversation, jobNumber);
                }
            }
            else
            {
                await context.SendActivityAsync($"_error_ >>> job {jobNumber} does not exist.");
            }
        }
    
        /// <summary>
        /// Complete the job and perform bookkeeping
        /// </summary>
        /// <param name="adapter"></param>
        /// <param name="appId"></param>
        /// <param name="conversation"></param>
        /// <param name="jobNumber"></param>
        /// <remarks>This schould execute outside of a turn.</remarks>
        private async void CompleteJob(BotAdapter adapter, string appId, ConversationReference conversation, int jobNumber)
        {
            // This prompts the channel to initiate a turn independent of the user.
            await adapter.ContinueConversationAsync(appId, conversation, async (context, token) =>
            {
                // Get the job log from state, and retrieve the job.
                var jobLog = await this.StateAccessors.JobData.GetAsync(context);
                var job = jobLog[jobNumber];
    
                // Perform bookkeeping.
                job.Completed = true;
    
                // Send the user a proactive confirmation message.
                await context.SendActivityAsync($"Job {job.JobNumber} is complete.");
            }, CancellationToken.None);
        }
    }
  9. Build and run your bot locally.
  10. Launch two instances of the v4 Emulator and attach both to your running bot. (You'll need to provide you bot's App ID and password the first time around.)
    1. Note that the conversation ID is different in the two emulators.
    2. In the first emulator, type run to start a job.
    3. Copy the job number from the log.
    4. In the second emulator, type done <jobNumber>, where <jobNumber> is the job number, without the angle brackets. (The bot code is designed to interpret this as if it were a jobComplete event.)
    5. Note that a proactive message is generated in the first emulator in response.
@JeffCordes

This comment has been minimized.

Copy link
Author

commented Aug 7, 2018

Thanks for this.

Is the assumption that for a real solution instead of using the bot emulator, you would have the event triggering code use the directline v3 API to authenticate, start a conversation, then send and activity?

Otherwise, I think my only concern with this approach is its kind of magic stringy. There is nothing stopping a user from typing a phrase that starts with "done."

Would it be better (if this is the approach going forward) To add a new activity type ActivityTypes.ProactiveMessage. In doing so we could separate proactive messages from user typed messages cleanly, and perhaps use the value and valueType parameters of the Activity to pass more complex data models back and forth for the messages? Maybe even a base interface for those datamodels that defines the minimum data needed to send (the other conversation id, etc)

@JonathanFingold @drub0y

@JonathanFingold

This comment has been minimized.

Copy link
Collaborator

commented Aug 7, 2018

@JeffCordes , Yes, processing the "done …" message is only a stand in for the event, to allow this scenario to be demonstrated in the emulator.

This example assumes a separate job processing system, that might use DirectLine to communicate with the bot. The bot would send information to the job system, and the job system would send event activities to the bot. Event activities include Name and Value properties, and Value can contain application defined data.

You could use the channel information and DirectLine to handle the back end authentication.

@Stevenic , @vishwacsena , @drub0y , This pretty much taps my knowledge of the system. Do any of you have anything to add?

@JeffCordes

This comment has been minimized.

Copy link
Author

commented Aug 7, 2018

@JonathanFingold

RE IEventActivity: If we reuse this type instead of making a new custom type for proactive messages can we add a ValueType parameter. That parameter currently only exists on ITraceActivity, but it would make IEventActivity more re-usable for this.

    /// <summary>
    /// Asynchronous external event
    /// </summary>
    public interface IEventActivity : IActivity
    {
        /// <summary>
        /// Name of the event
        /// </summary>
        string Name { get; set; }

        /// <summary>
        /// Open-ended value 
        /// </summary>
        object Value { get; set; }

        /// <summary>
        /// Unique string which identifies the format of the value object.
        /// </summary>
        string ValueType { get; set; }

        /// <summary>
        /// Reference to another conversation or activity
        /// </summary>
        ConversationReference RelatesTo { get; set; }
    }

@JonathanFingold

This comment has been minimized.

Copy link
Collaborator

commented Aug 7, 2018

@JeffCordes , That that would be a change to the underlying REST API, as opposed to this SDK. Extending the IEventActivity or Activity type with GetValue() and TryGetValue(out T value) methods might be the way to go, and might be a good feature request.

There are already similar extension methods for accessing the ChannelData property on message activities, see GetChannelData and TryGetChannelData(out T instance).

@JeffCordes

This comment has been minimized.

Copy link
Author

commented Aug 8, 2018

I feel like ChannelData is a good comparable. A few things here though... The GetValue and TryGetValue are never used in the code except for the unit tests. The unit tests are skewed because the setup uses a object initialized in the executing code, and that allows object.GetType() to work.

The real use case is that the Activity that contains the Channel data comes in through an api as a JObject. the executing code never knows the object type of the channel data node if you access it in middleware or in the onTurn method. context.Activity.ChannelData.GetType() there will return {Newtonsoft.Json.Linq.JObject}. Since TryGetValue uses GetType() to compare the generic type passed in, it will fail if you give it anything type besides jobject.

This is likely why Activity also has a ChannelId and most of the code logic uses that to identify the payload of channelData.

The same thing is true for using the event. At the point in time that you are in an onturn or middleware the value parameter will only resolve as Jobject. We will need an additional parameter to know what to try to deserialize that to.

@JonathanFingold

@JonathanFingold

This comment has been minimized.

Copy link
Collaborator

commented Aug 8, 2018

@JeffCordes , Can you open a new issue about how to get event data out of an event activity? I think it will get lost in this thread. I'll try to investigate, but I'm not sure how soon I can get to it.

If you are unblocked with respect to proactive messaging, I'd like to close this issue.

@JeffCordes

This comment has been minimized.

Copy link
Author

commented Aug 9, 2018

That is fine @JonathanFingold. There may be some more around this, but I also need some more time to put some more complex POCs together and see where we are. So yes we can handle in subsequent tickets. This has definitely reached TLDR; status..

@mvelosop

This comment has been minimized.

Copy link

commented Jan 11, 2019

Hi @drub0y, I'm sorry to hijack this thread, I thought this would be a good context.

I've been reading this issue and PR #461 several times, so I finally understood the only way to send a proactive message is by using DL to send an event with some value to do whatever is needed from the OnTurnAsync of the bot 😣.

I agree 100% with your rationale on PR #461, as I feel just that pain.

At some point I read that there's a way to send a proactive message creating a controller and handling it by yourself in the bot, so I was wondering if you could link me to some code I could peek at to try to implement that. Or just any other way to initiate a proactive message from the same computer where the bot is running without going through DL.

Thanks in advance!

@JeffCordes

This comment has been minimized.

Copy link
Author

commented Jan 11, 2019

@mvelosop you can use DI to inject the Adapter interface into an api controller, then use ContinueConversationAsync. You do have to pass in, or have in state enough information about the conversation to send the message. In this sample I am passing that into the api.

namespace ProactiveBot.Controllers
{
    [Route("api/[controller]")]
    public class ProactiveController : Controller
    {
        private readonly BotFrameworkAdapter _adapter;

        public ProactiveController(IAdapterIntegration adapter)
        {
            this._adapter = (BotFrameworkAdapter)adapter;
        }

        // POST api/<controller>
        [HttpPost]
        public async Task<ObjectResult> Post([FromBody]ProactiveMessage proMsg)
        {
            await _adapter.ContinueConversationAsync(proMsg.BotId, proMsg.Conversation, async (context, token) =>
            {
                await context.SendActivityAsync(proMsg.MessagePayload);
            }, new System.Threading.CancellationToken());

            return Ok("Message Sent");
        }

    }
}
public class ProactiveMessage
    {
        public string BotId { get; set; }
        public ConversationReference Conversation { get; set; }
        public string MessagePayload { get; set; }
    }

Then post to it in postman with that your conversation and user info in the json payload.

{
	"botId": "mybotid",
	"conversation": {
		"bot" : {
			"id": "cd797950-15a8-11e9-9bf2-9fa72650c0de",
    		"name": "Bot",
    		"role": "bot"	
		},
		"user" : {
			"id" : "191fc0f2-c0b1-4024-a565-c71928119cc2",
			"name": "User"
		},
		"conversation": {
			"id": "7fcc6d20-15ad-11e9-9bf2-9fa72650c0de|livechat"
		},
		"serviceUrl": "http://localhost:56836",
		"channelId": "emulator"
	},
	"messagePayload": "this is a proactive message"
}
@mvelosop

This comment has been minimized.

Copy link

commented Jan 11, 2019

Hi @JeffCordes, thanks so much for this!

I've spent so many hours reading and trying to figure it out and it was so simple I'm almost ashamed!

This should be included in the docs and in the sample app!

Thanks again! 😊

Best!

@JeffCordes

This comment has been minimized.

Copy link
Author

commented Jan 11, 2019

No worries. I put that sample i have in a repo if it can help anyone, in the interim.

https://github.com/JeffCordes/ProactiveMessageBot

@drub0y

This comment has been minimized.

Copy link
Member

commented Jan 11, 2019

So @JeffCordes certainly gave the best solution that I think you can have today. The problem that I have with this approach is that it puts some of your bot's logic into a separate piece of software (the controller) and forces you to work directly with the BotAdapter::ContinueConversationAsync API.

The approach that I was seeking with my integration work was that, yes, you'd still have something like the controller out there receiving some out-of-band notification and having the responsibility of triggering the bot itself to resume the conversation. Effectively this would allow the same kind of triggering as sending a backchannel event through DirectLine, but with your own delivery mechanism.

IMHO the bot should own 100% of the conversational responsibility. To not push some of that out of the bot into a controller (or other component) feels wrong to me. Imagine you needed access to some state inside that callback handler that is being passed to ContinueConversationAsync? Now your controller would need to have access to all the state property accessors and so on. What if your bot was in the middle of a Dialog flow? Now the controller also needs to know how to work with the Dialogs. It just spreads the conversational responsibility all over the place which creates fragility.

Now, you could pass the bot's OnTurnAsync method as the handler to ContinueConversationAsync and it what would happen then is that your bot would receive an Activity of type ActivityTypes.Event with the Name of ContinueConversation. Ok, so that would allow me to keep the conversational responsibility entirely inside my bot, but I can't really deliver any domain specific details using this approach because I'm not in charge of the details of the Activity that is passed into the bot (i.e. it's always a ContinueConversation event). Given that, that approach is kind of a dead end if I have multiple domain events/triggers that I want to cause my bot proactively resume a conversation.

What I imagined was something more along the lines of this:

    [Route("proactive")]
    public class ProactiveController
    {
        private readonly IAdapterIntegration _adapterIntegration;
        private readonly IBot _bot;

        public ProactiveController(IAdapterIntegration adapterIntegration, IBot bot)
        {
            _adapterIntegration = adapterIntegration ?? throw new ArgumentNullException(nameof(adapterIntegration));
            _bot = bot ?? throw new ArgumentNullException(nameof(bot));
        }

        [Route("events")]
        [HttpPost]
        public async Task<IActionResult> TriggerEvent(string botAppId, [FromBody]Activity activity, CancellationToken cancellationToken)
        {
            await ((BotAdapter)_adapterIntegration)
                .ContinueConversationAsync(
                botAppId,
                activity,
                _bot.OnTurnAsync,
                cancellationToken);

            return new OkResult();
        }
    }

There's a subtle difference here. Instead of ContinueConversationAsync taking just a ConversationReference and then, under the covers, synthesizing that ContinueConversation event, it now takes an Activity which effectively allows me to pass in any Activity I want just like I could through DirectLine to trigger the bot. Now this controller has very little conversational responsibility, it's just another proxy for activities to the bot. Finally, with the implementation that I was proposing, you wouldn't ever even write this functionality yourself. You would have just set an option that enabled this endpoint to be lit up for you by the ASP.NET integration layer and it would have done all the work for you.

I'd love to hear your thoughts on this. If it's something enough people think would be valuable maybe we can get a DCR in to add that ContinueConversationAsync overload. That's all we'd need from the core API, the rest is all at the integration layer.

@mvelosop

This comment has been minimized.

Copy link

commented Jan 11, 2019

Thanks a lot @JeffCordes I appreciate you having set that repo up! 😊

@JeffCordes

This comment has been minimized.

Copy link
Author

commented Jan 11, 2019

I like that concept @drub0y .

In our specific case the proactive message has separate business logic than other messages, So separate controller for separate functionality was not as big of an issue. We are actually doing a bit more than the example I posted. We are creating a custom botFrameworkAdapter, assigning it its own middleware chain, state accessors, and using ProcessActivityAsync() instead of ContinueConversation. This way we send our proactive message through an Isolated middleware chain.

That said if the integration you mentioned existed we would use it in many cases, assuming we could send send the message through the entire MW chain of the injected BFA.

@mvelosop

This comment has been minimized.

Copy link

commented Jan 11, 2019

Hi @drub0y, thanks for your insights on the subject, I find your approach very good exactly because it keeps all be bot-related logic inside the bot.

I have been working with Bot Builder v4 for just a few weeks, so I'm only aware of some basic scenarios. However, I think the ContinueConversation event makes sense, because it's like a disruption of the conversation flow, for it comes from an external event, so it might need to be treated differently. But then again, it's good that you, as a developer have the option to handle it they way that suits you better.

Actually, in the application I need this for, using the bot's OnTurnAsync might add some advantage. In this case I really want to continue the conversation, after the user clicks a button that takes her to a payment page.

Since the the bot is not directly aware of the user pressing the button, I go to a local controller, to initiate the order process and generate the continue conversation message, before redirecting to the actual payment page.

This way the conversation flows "naturally" after the user pressing the button, which would not be possible otherwise, at least for what I know now about Bot Builder.

I read the whole discussion on PR #461 and I'm completely on your side 😉, and even though the PR wasn't accepted, having these solutions (@JeffCordes's and yours) opens the door to many options and it's IMHO much easier to understand than the current proactive message sample.

I just couldn't believe (neither anyone else I commented this to) that, being on the same process, I'd have to send a request from my thread to DL just to get it back in the next thread.

This really sounds crazy, to put it lightly!

Anyway many thanks for this thread, I hope this makes it into the documentation!

Cheers to you both! 😊

@Unders0n

This comment has been minimized.

Copy link

commented Jan 27, 2019

wow, this is so huge and so not documented! :) Sorry i know my comment isn't much informative, but i was struggling with this topic in v3 for so long, and then saw that in v4 all been rewritten and was struggling in v4 like for a day and just now came to this topic to see there's actually a way to send REAL proactive messages. Just wanted to ask is a way mentioned at the end of the topic is recommended way for common proactive cases?

@mvelosop

This comment has been minimized.

Copy link

commented Jan 31, 2019

Hi @JeffCordes, @drub0y, and @Unders0n too 😉

I just wanted to post here that I tried first @JeffCordes's solution and it worked right away, no problem, but then...

After having sent the proactive message, I realized it just wasn't enough, because I also needed to change the bot state, so I went @drub0y's way.

In just a moment it was evident that this was the only solution to my problem, because the issue was not sending a proactive message was that the bot had to respond to an event and, as a result, update the state AND send the message.

@drub0y's solution worked just fine. Since I couldn't find a way to send a payload/value with the event, I injected an scoped accessor to both the API controller and the bot, to pass the payload so the bot could know what to do.

The accessor was something like this:

public class EventAccessor
{
    private EventValue _event;

    public enum EventValue
    {
        Pending,
        Started,
        Approved,
    }

    EventValue Event => _event;

    public void Start(string p1)
    {
        .../...
        _event = Event.Started;
    }
}

So the API controller executed _eventAccessor.Start("xxx");

and the bot could read _eventAccessor.Event

So what I found is:

  • If you just need to send a proactive message, use @JeffCordes's solution, because it's simpler.
  • If you have to change the bot state and then send a proactive message use @drub0y's solution or
    • Create a DirectLine client 😣
    • Setup ngrok for development 😣
    • Send the event to the bot 😣
    • Do whatever

Wished there was an easier way to send an event with a payload, but IMHO I prefer @drub0y's to all 😣!

Hope this helps.

@SarangRapid

This comment has been minimized.

Copy link

commented Mar 5, 2019

the real problem while invoking proactive message controller is to determine conversation id in payload, what if i want to notify selective users using proactive message when external triggering system is unaware of conversation id.

please help

@Unders0n

This comment has been minimized.

Copy link

commented Mar 5, 2019

@SarangRapid just store some simple collection where as key you can use any user's field you aware of (username, email or some id) and as value Conversationreference , then query by this field.

@SarangRapid

This comment has been minimized.

Copy link

commented Mar 8, 2019

how to send some custom data like greeting text through proactive message as it is treated as event in Bot even when type is message in Json Payload, can you please help

@Unders0n

This comment has been minimized.

Copy link

commented Mar 8, 2019

@SarangRapid not sure i clearly uinderstood your question. can ypu provide some details or code?

@SarangRapid

This comment has been minimized.

Copy link

commented Mar 8, 2019

Apologies for insufficient details, we have proactive controller which accepts activity type of objects sent as Json string, suggested by @drub0y (commented on Jan 11).

we are using this code in proactive controller :

await ((BotAdapter)adapterIntegration).
ContinueConversationAsync(proMsg.BotId, proMsg.Conversation, bot.OnTurnAsync, cancellationToken);`

and the proMsg object is of type :

public class ProactiveMessage    
    {    
        public string BotId { get; set; }                                                  
        public ConversationReference Conversation { get; set; }    
        public string MessagePayload { get; set; }    
    }

and we want to pass that 'MessagePayload' text to the bot so that it can echo it.
We also have tried text in place of MessagePayload.

@Unders0n

This comment has been minimized.

Copy link

commented Mar 8, 2019

your bot.OnTurnAsync is a callback that is calling on this proactive message right? So inside of it you can do whatever you want, including any echo and sending other activities, you can also read and write userdata state, mine is called BotCallbackHandler and im passing some params there:
private BotCallbackHandler CreateCallback(JobLog.JobData jobInfo)
{
return async (turnContext, token) =>
{
var txt = $"Some text";
await turnContext.SendActivityAsync(txt);
...

var _state = await _accessor.GetAsync(turnContext,
() => new SignInPhoneState());

                //if was in this conversation - close it and ask for review
                if (_state.TicketIdChatSession == jobInfo.Id)

...
}

Tell if that helped or i misunderstood smth

@damienestewart

This comment has been minimized.

Copy link

commented Apr 25, 2019

After fumbling through the documentation for a couple days now trying to figure out how to get this done, I finally came across this. Does the official documentation actually mention that we can access the Bot Adapter through DI? Either way, thank you all for this discussion!

@mvelosop

This comment has been minimized.

Copy link

commented Apr 25, 2019

Hi @damienestewart,

Nope the documentation doesn't say anything about this!

However I've put a blog post with some details you can see here: https://www.coderepo.blog/posts/how-to-send-proactive-message-bot-builder-v4/

I also have another repo with details about handling any external event, that I'll make public next Saturday (2019-04-27) in a talk on Global Azure Bootcamp 2019 at Madrid.

So stay tuned 😉.

Cheers.

@SarangRapid

This comment has been minimized.

Copy link

commented Apr 25, 2019

Hi,

Apologies, it's been a while, I almost forgot i had raised an issue, but fortunately i could resolve this issue.

PFB steps :

  1. Create API controller with constructor accepting parameters of type IAdapterIntegration and IBot.
 await adapter.ContinueConversationAsync("your app id", msg.Conversation, async (context, token) =>
                   {
                       MicrosoftAppCredentials.TrustServiceUrl(context.Activity.ServiceUrl);
                       await Task.Delay(2000);
                       context.Activity.Text = "your custom message from external api";
                       context.Activity.Type = ActivityTypes.Message;
                       await bot.OnTurnAsync(context, cancellationToken: cancellationToken);
                   }, new CancellationToken());

add above code to your controller method which will receive message to post on teams or other channel.

MicrosoftAppCredentials.TrustServiceUrl(context.Activity.ServiceUrl);

is important which will resolve issue of InternalServerError: Authorization for Microsoft App ID failed with status code Forbidden automatically.

msg.Conversation -> conversationreference json you sent from external API, you can store it in azure table storage against each user post user is authenticated successfully.

you can change activity type as per your requirement as you have total control here.

@mvelosop

This comment has been minimized.

Copy link

commented Apr 25, 2019

Hi @SarangRapid,

That's a nice solution and the TrustServiceUrl is a very good tip😊

I had used another way:

var adapter = _adapter as BotFrameworkAdapter;

 var claimsIdentity = new ClaimsIdentity(new List<Claim>
{
    // Adding claims for both Emulator and Channel.
    new Claim(AuthenticationConstants.AudienceClaim, botAppId),
    new Claim(AuthenticationConstants.AppIdClaim, botAppId),
});

await adapter.ProcessActivityAsync(claimsIdentity, activity, _bot.OnTurnAsync, default);

But your's is safer, because it's using only the interface (IAdapterIntegration) method. 👍

@mvelosop

This comment has been minimized.

Copy link

commented May 13, 2019

Hi @SarangRapid, I'm finishg up a new blog post and would like to include your solution, but would also like to include a better credit than just @SarangRapid, do you have an e-mail or something to contact you? you can find my e-mail in my blog: https://coderepo.blog.

Cheers 😊

@mvelosop

This comment has been minimized.

Copy link

commented May 15, 2019

BTW, I just published the blog post covering this issue: https://www.coderepo.blog/posts/how-to-receive-events-bot-framework-sdk-v4-web-api-bot/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
7 participants
You can’t perform that action at this time.