How to change locale for a conversation in runtme - botframework

I'm using botframework composer with multi language and want each user to be able to select preferred language/locale. After resolving the local code for his selection with a choice dialog, how can I set it in conversation so that his locale setting in his device will be overruled for rest of conversation?
Changing locale in emulator works fine, want same behaviour after user selection.
Setting turn.locale works for one turn, but is reset on next turn.

supposing you don't have control over the client, which would be the best.
You can resort to an old overload on the ever-growing hierarchy of bot adapters that hasn't been marked as deprecated.
You'd have to use the PostAsync method (api/post-messages endpoint) in the following controller (showing the one created by the current set of bot framework templates just for comparison):
[Route("api")]
[ApiController]
public class BotController : ControllerBase
{
private readonly IBotFrameworkHttpAdapter StreamingAdapter;
private readonly BotFrameworkAdapter PostAdapter;
private readonly ConversationLocales ConversationLocales;
private readonly IBot Bot;
public BotController(
IBotFrameworkHttpAdapter streamingAdapter,
BotFrameworkAdapter postAdapter,
ConversationLocales conversationLocales,
IBot bot)
{
StreamingAdapter = streamingAdapter;
PostAdapter = postAdapter;
Bot = bot;
}
[HttpPost("messages"), HttpGet("messages")]
public async Task PostOrStreamingAsync()
{
// Delegate the processing of the HTTP POST to the adapter.
// The adapter will invoke the bot.
await StreamingAdapter.ProcessAsync(Request, Response, Bot);
}
[HttpPost("post-messages")]
public async Task<InvokeResponse> PostAsync([FromBody] Activity activity)
{
var savedLocale = ConversationLocales.GetLocaleForConversation(activity.Conversation.Id);
activity.Locale = savedLocale ?? activity.Locale;
return await PostAdapter.ProcessActivityAsync(string.Empty, activity, Bot.OnTurnAsync, default);
}
}
That's supposing you implement a ConversationLocales service that allows you to keep the selected locale for each conversation id.
In the code above we're using the BotFrameworkAdapter adapter instead of IBotFrameworkHttpAdapter, however the AdapterWithErrorHandler used in the templates inherits indirectly from BotFrameworkAdapter, so you could do something like this in ConfigureServices to register "both" adapters:
services.AddSingleton<AdapterWithErrorHandler>();
services.AddSingleton<IBotFrameworkHttpAdapter>(sp => sp.GetRequiredService<AdapterWithErrorHandler>());
services.AddSingleton<BotFrameworkAdapter>(sp => sp.GetRequiredService<AdapterWithErrorHandler>());
To have a single adapter instance.
Using this method the adapter won't be able to use the bot channel streaming endpoints, but that shouldn't be much of a trouble, as long as you don't use the speech client.
You can also read some other details that might be relevan to you in my blog post How does a Bot Builder v4 bot work?, it's a bit dated but still valid.
UPDATE - Found a better solution 😊
This one works with the current wave of adapters and uses the messages pipeline, so it's "modern".
It also requires you to use a custom runtime, that you'll customize as follows.
1 - Create the following middleware
public class LocaleSelectionMiddleware : IMiddleware
{
private readonly IStatePropertyAccessor<string> _userLocale;
public LocaleSelectionMiddleware(UserState userState)
{
_userLocale = userState.CreateProperty<string>("locale");
}
public async Task OnTurnAsync(ITurnContext turnContext, NextDelegate next, CancellationToken cancellationToken = default)
{
if (turnContext is null)
{
throw new ArgumentNullException(nameof(turnContext));
}
var userLocale = await _userLocale.GetAsync(turnContext, () => turnContext.Activity.Locale);
turnContext.Activity.Locale = userLocale;
(turnContext as TurnContext).Locale = userLocale;
await next(cancellationToken).ConfigureAwait(false);
}
}
2 - Configure the middleware in the adapter in GetBotAdapter() in Startup.cs
public class Startup
{
public Startup(IWebHostEnvironment env, IConfiguration configuration)
{
this.HostingEnvironment = env;
this.Configuration = configuration;
}
//...
public BotFrameworkHttpAdapter GetBotAdapter(IStorage storage, BotSettings settings, UserState userState, ConversationState conversationState, IServiceProvider s)
{
var adapter = IsSkill(settings)
? new BotFrameworkHttpAdapter(new ConfigurationCredentialProvider(this.Configuration), s.GetService<AuthenticationConfiguration>())
: new BotFrameworkHttpAdapter(new ConfigurationCredentialProvider(this.Configuration));
adapter
.UseStorage(storage)
.UseBotState(userState, conversationState)
.Use(new RegisterClassMiddleware<IConfiguration>(Configuration))
.Use(new LocaleSelectionMiddleware(userState)) // <-- Add the middleware here
.Use(s.GetService<TelemetryInitializerMiddleware>());
//...
return adapter;
}
//...
}
3 - Set the user.locale property in any dialog
Set the user.locale property from any dialog, and the next turn will have the desired locale, and will be persisted in the user state, until they change it again.

Related

Store Workflow Activity Data When Publishing

I Need to store a specific activity data in another collection in database whenever a user publish a workflow in elsa.
I dont find any documentation, Please suggest me some resource or suggestion to achieve this. I have try to implement this with middleware. The Middleware code is
namespace WorkFlowV3
{
// You may need to install the Microsoft.AspNetCore.Http.Abstractions package into your project
public class CustomMiddleware
{
private readonly RequestDelegate _next;
static HttpClient client = new HttpClient();
public CustomMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext httpContext)
{
//Write Custom Logic Here....
client.BaseAddress = new Uri("#");
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue("application/json"));
string path = "/api/test-middleware-call";
HttpResponseMessage response = await client.GetAsync(path);
await _next(httpContext);
}
}
// Extension method used to add the middleware to the HTTP request pipeline.
public static class CustomMiddlewareExtensions
{
public static IApplicationBuilder UseCustomMiddleware(this IApplicationBuilder builder)
{
return builder.UseMiddleware<CustomMiddleware>();
}
}
}
But in this process, I cant fetch the specific activity data.
The easiest way to store information in your own DB in response to the "workflow published" event is by implementing a notification handler (from MediatR) that handles the WorkflowDefinitionPublished notification.
For example:
public class MyWorkflowPublishedhandler : INotificationhandler<WorkflowDefinitionPublished>
{
private readonly IMyDatabaseStore _someRepository;
public MyWorkflowPublishedhandler(IMyDatabaseStore someRepository)
{
_someRepository = someRepository;
}
public async Task Handle(WorkflowDefinitionPublished notification, CancellationToken cancellationToken)
{
var workflowDefinition = notification.WorkflowDefinition;
// Your logic to do a thing.
}
}
To register this handler, from your Startup or Program class, add the following code:
services.AddNotificationHandler<MyWorkflowPublishedhandler>();
Your handler will be invoked every time a workflow gets published.

Access UserProfile from NotifyBot

I'm trying to kind of merge the functionality of EchoBot and ProactiveBot. A bot that parses user commands, which can also be triggered through a URL to send some users a message based on the contents of their UserProfile.
I managed to get both parts to work, but I'm having trouble accessing the UserProfiles from the NotifyController, or solving the problem another way. I tried a few things, read many of the tutorials and also this excellent explanation, but I'm still stuck.
So my question is: How do I rewrite NotifyController so it calls a method in my Bot class (the one implementing ActivityHandler) - just like BotController (from EchoBot) does?
Or, if that's better: How do I get a reference to the UserProfile in the message handler in NotifyController?
Thanks!
Alright, I got it to work.
Basically I duplicated all the code for the conversationReferences, because those get stored in the Bot and are used in NotifyController.
Some code, in case somebody else is trying to do the same:
Startup.cs
Create another Singleton.
services.AddSingleton<ConcurrentDictionary<string, UserProfile>>();
In the bot class
I have a variable storing UserProfiles just like the one for ConversationReferences
private readonly ConcurrentDictionary<string, ConversationReference> _conversationReferences;
private readonly ConcurrentDictionary<string, UserProfile> _userProfiles;
The constructor now starts like this
public RmindrBot(ConversationState conversationState,
UserState userState,
ConcurrentDictionary<string, ConversationReference> conversationReferences,
ConcurrentDictionary<string, UserProfile> userProfiles)
{
_conversationReferences = conversationReferences;
_userProfiles = userProfiles;
...
(I think this is the most surprising bit for me - didn't think the framework would just magically figure out the new signature.)
A method for adding references:
private void AddConversationReference(Activity activity)
{
var conversationReference = activity.GetConversationReference();
_conversationReferences.AddOrUpdate(
conversationReference.User.Id,
conversationReference,
(key, newValue) => conversationReference);
}
...which is called in OnMessageActivityAsync:
var userProfile = await userStateAccessors.GetAsync(turnContext, () => new UserProfile());
userProfile.ID = turnContext.Activity.From.Id;
AddUserProfile(userProfile);
NotifyController
private readonly ConcurrentDictionary<string, UserProfile> _userProfiles;
public NotifyController(
IBotFrameworkHttpAdapter adapter,
IConfiguration configuration,
ConcurrentDictionary<string, ConversationReference> conversationReferences)
ConcurrentDictionary<string, ConversationReference> conversationReferences,
ConcurrentDictionary<string, UserProfile> userProfiles)
{
_adapter = adapter;
_conversationReferences = conversationReferences;
_userProfiles = userProfiles;
...
And then we can finally use it in the callback method and match the user to their ID:
private async Task BotCallback(ITurnContext turnContext, CancellationToken cancellationToken)
{
var userProfile = _userProfiles[turnContext.Activity.From.Id];
await turnContext.SendActivityAsync("proactive hello " + userProfile.ID);
}

Access IStatePropertyAccessor botstate in a controller in v4

If you need to access the state in botframework v4 outside of a bot context (that does not implement IBot), for instance a Controller, how could you easily get a hold of this state object? The problem is that you can't really inject it directly because it needs to be initialized with a ChannelId and ConversationId.
The following approach works but looks a bit strange as I am using the TestAdapter to initialize the TurnContext. Isn't there a better, more obvious method to get a hold of the state?
public class SampleController : ControllerBase
{
private readonly MyBotAccessors _botAccessors;
public SampleController(MyBotAccessors botAccessors)
{
_botAccessors = botAccessors;
}
}
public async Task Reset(string conversationKey, string channelId)
{
var turnContext = new TurnContext(new TestAdapter(), new Activity { ChannelId = channelId, Conversation = new ConversationAccount { Id = conversationKey } });
await _botAccessors.MyContextState.SetAsync(turnContext, new MyContextState().Reset());
await _botAccessors.ConversationState.SaveChangesAsync(turnContext);
}

botframework v4, accessing appsettings.json

How do I read the appsettings.json file in my botframework (v4) app? I see the configuration is set up in the Startup.cs, but how do I access the settings in other classes?
One of the goals of the v4 ASP.NET core integration was to be idiomatic to existing .NET Core patterns. One of the things this means is that when you implement an IBot and add it with AddBot<TBot>, it becomes a participant in dependency injection just like an ASP.NET MVC controller would. This means that any services you might need to access, including configuration types such as IOptions<T>, will be injected into your bot via the constructor if you ask for them.
In this case, you just want to leverage the "options pattern" from the Configuration APIs and that would look something like this:
Startup.cs
public class Startup
{
private readonly IConfiguration _configuration;
public Startup(IConfiguration configuration)
{
_configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
}
public void ConfigureServices(IServiceCollection services)
{
// Bind MySettings to a section named "mySettings" from config
services.Configure<MySettings>(_configuration.GetSection("mySettings"));
// Add the bot
services.AddBot<MyBot>();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseBotFramework();
}
}
MyBot.cs
public class MyBot : IBot
{
private readonly IOptions<MySettings> _mySettings;
public MyBot(IOptions<MySettings> mySettings)
{
_mySettings = mySettings ?? throw new ArgumentNullException(nameof(mySettings));
}
public async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default(CancellationToken))
{
// use _mySettings here however you like here
}
}

How to persist policy authorization results for users in ASP.NET Core, MVC 6?

Currently I have a simple custom policy handler that looks like so:
protected override void Handle(AuthorizationContext context, UserPolicyRequirement requirement)
{
// authorize user against policy requirements
if (_authorizationTask.AuthorizeUserAgainstPolicy(context.User, requirement))
{
// User passed policy req's
context.Succeed(requirement);
}
}
Problem is, this authorization step takes a long time to execute, but this is required in many different areas of the website. Is there any readily available mechanisms to save/cache the results of this policy authorization so that I only need to do this once per session?
I am currently using Windows Authentication, if that helps.
If per session way does not cause any problem, you can use Session to store user data. Simple implementation is something like below:
First you need a service to get user data from any store
public interface IGetUserDataService
{
<type> GetUserData();
}
I assume that there is Session configuration(see) and IGetUserDataService implementation.
Then you need to create a middleware to handle Session
public class SessionMiddleware
{
private readonly RequestDelegate _next;
private readonly IGetUserDataService _getUserDataService;
public SessionMiddleware(RequestDelegate next, IGetUserDataService getUserDataService)
{
_next = next;
_getUserDataService = getUserDataService;
}
public async Task Invoke(HttpContext context)
{
//user data is obtained only once then is stored in Session
if (context.Session.Get("UserData") == null)
{
context.Session.Set("UserData", getUserDataService.GetData());
}
await _next.Invoke(context);
}
}
//In Startup.cs
app.UseMiddleware<SessionMiddleware>();
Finally get and use session data in handler
public class YourHandler : AuthorizationHandler<YourRequirement>
{
private readonly IHttpContextAccessor _accessor;
public YourHandler(IHttpContextAccessor accessor)
{
_accessor = accessor;
}
protected override void Handle(AuthorizationContext context, PermissionRequirement requirement)
{
var userData =(<type>)_accessor.HttpContext.Session.Get("UserData");
// check
}
}

Resources