I'm using TranscriptLoggerMiddleware to log transcript to Azure blobs.
Now, I want to add additional information to the activity, for example, account ID.
Ideally I want the account ID to be the top level folder when creating the blobs so one can easily locate all conversations for a given account.
The logger is only passed the activity without any context. So I'm looking at the Entities activity property which I can potentially use for storing my account ID.
Is this a valid approach?
Any other ideas on how to implement this?
Answering my own question.
This worked for me:
public class SetEntityMiddleware : IMiddleware
{
private readonly BotState _userState;
private readonly IStatePropertyAccessor<UserProfileState> _userProfileState;
public SetEntityMiddleware(UserState userState)
{
_userState = userState;
_userProfileState = userState.CreateProperty<UserProfileState>(nameof(UserProfileState));
}
public async Task OnTurnAsync(ITurnContext turnContext, NextDelegate next, CancellationToken cancellationToken = default)
{
var userProfile = await _userProfileState.GetAsync(turnContext, () => new UserProfileState(), cancellationToken);
this.SetEntity(turnContext.Activity, userProfile);
turnContext.OnSendActivities(async (ctx, activities, nextSend) =>
{
var userProfile = await _userProfileState.GetAsync(ctx, () => new UserProfileState(), cancellationToken);
foreach (var activity in activities)
{
this.SetEntity(activity, userProfile);
}
return await nextSend().ConfigureAwait(false);
});
await next(cancellationToken).ConfigureAwait(false);
}
private void SetEntity(Activity activity, UserProfileState userProfile)
{
if (activity.Type == ActivityTypes.Message &&
!string.IsNullOrEmpty(userProfile.AccountNumber))
{
var entity = new Entity();
entity.Type = "userProfile";
entity.Properties["accountNumber"] = userProfile.AccountNumber;
if (activity.Entities == null)
{
activity.Entities = new List<Entity>();
}
activity.Entities.Add(entity);
}
}
}
Related
I have inserted connectionstring in "[AbpTenantConnectionStrings]" table for same tenant id.
My objective is to create both the DB and migrage the DB
DB
while running below code only "Default Db is creating", can anyone let me know how to create and migrate other DB as well. what changes need to be done in the below code.
var tenants = await _tenantRepository.GetListAsync(includeDetails: true);
var migratedDatabaseSchemas = new HashSet<string>();
foreach (var tenant in tenants)
{
using (_currentTenant.Change(tenant.Id))
{
if (tenant.ConnectionStrings.Any())
{
var tenantConnectionStrings = tenant.ConnectionStrings
.Select(x => x.Value)
.ToList();
if (!migratedDatabaseSchemas.IsSupersetOf(tenantConnectionStrings))
{
await MigrateDatabaseSchemaAsync(tenant);
migratedDatabaseSchemas.AddIfNotContains(tenantConnectionStrings);
}
}
await SeedDataAsync(tenant);
}
After the below changes able to migrate or create the Db based on the connection-string inserted in "AbpTenantConnectionStrings":
public async Task MigrateAsync()
{
var initialMigrationAdded = AddInitialMigrationIfNotExist();
//var str = (await _connectionResolver.ResolveAsync("Default1"));
if (initialMigrationAdded)
{
return;
}
Logger.LogInformation("Started database migrations...");
await MigrateDatabaseSchemaAsync();
await SeedDataAsync();
Logger.LogInformation($"Successfully completed host database migrations.");
var tenants = await _tenantRepository.GetListAsync(includeDetails: true);
var migratedDatabaseSchemas = new HashSet<string>();
foreach (var tenant in tenants)
{
using (_currentTenant.Change(tenant.Id))
{
if (tenant.ConnectionStrings.Any())
{
var tenantConnectionStrings = tenant.ConnectionStrings
.Select(x => x.Value)
.ToList();
tenantConnectionStrings.ForEach(async s =>
{
await MigrateDatabaseSchemaAsync(s);
// migratedDatabaseSchemas.AddIfNotContains(s);
});
//if (!migratedDatabaseSchemas.IsSupersetOf(tenantConnectionStrings))
//{
// await MigrateDatabaseSchemaAsync(tenant);
// migratedDatabaseSchemas.AddIfNotContains(tenantConnectionStrings);
//}
}
await SeedDataAsync(tenant);
}
Logger.LogInformation($"Successfully completed {tenant.Name} tenant database migrations.");
}
Logger.LogInformation("Successfully completed all database migrations.");
Logger.LogInformation("You can safely end this process...");
}
private async Task MigrateDatabaseSchemaAsync(string str)
{
//Logger.LogInformation(
// $"Migrating schema for {(tenant == null ? "host" : tenant.Name + " tenant")} database...");
foreach (var migrator in _dbSchemaMigrators)
{
await migrator.MigrateAsync(str);
}
}
public interface IBookStoreDbSchemaMigrator
{
Task MigrateAsync();
Task MigrateAsync(string con);
}
public class EntityFrameworkCoreBookStoreDbSchemaMigrator
: IBookStoreDbSchemaMigrator, ITransientDependency
{
private readonly IServiceProvider _serviceProvider;
public EntityFrameworkCoreBookStoreDbSchemaMigrator(
IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public async Task MigrateAsync()
{
/* We intentionally resolving the BookStoreMigrationsDbContext
* from IServiceProvider (instead of directly injecting it)
* to properly get the connection string of the current tenant in the
* current scope.
*/
await _serviceProvider.GetRequiredService<BookStoreMigrationsDbContext>()
.Database
.MigrateAsync();
}
public async Task MigrateAsync(string con)
{
var context = _serviceProvider
.GetRequiredService<BookStoreMigrationsDbContext>();
context.Database.SetConnectionString(con);
context.Database.Migrate();
}
}
Let me know if anyone has suggestions or a better approach.
I want to start a user dialog right after a welcome message has been displayed in my bot - without any initial user interaction.
Code snippet to accomplish that:
public RootDialogBot(BotServices services, BotAccessors accessors, ILoggerFactory loggerFactory)
{
if (loggerFactory == null)
{
throw new System.ArgumentNullException(nameof(loggerFactory));
}
_logger = loggerFactory.CreateLogger<RootDialogBot>();
_accessors = accessors ?? throw new System.ArgumentNullException(nameof(accessors));
_botServices = services ?? throw new System.ArgumentNullException(nameof(services));
_studentProfileAccessor = _accessors.UserState.CreateProperty<StudentProfile>("studentProfile");
if (!_botServices.QnAServices.ContainsKey("QnAMaker"))
{
throw new System.ArgumentException($"Invalid configuration. Please check your '.bot' file for a QnA service named QnAMaker'.");
}
if (!_botServices.LuisServices.ContainsKey("LUIS"))
{
throw new System.ArgumentException($"Invalid configuration. Please check your '.bot' file for a Luis service named LUIS'.");
}
.Add(new Activity2MainDialog(_accessors.UserState, Activity2MainDialog))
.Add(new Activity2LedFailToWorkDialog(_accessors.UserState, Activity2LedFailToWorkDialog));
}
public async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default(CancellationToken))
{
...
if (turnContext.Activity.Type == ActivityTypes.ConversationUpdate)
{
if (turnContext.Activity.MembersAdded != null)
{
// Save the new turn count into the conversation state.
await _accessors.UserState.SaveChangesAsync(turnContext, false, cancellationToken);
await _accessors.ConversationState.SaveChangesAsync(turnContext, false, cancellationToken);
var message = "Welcome!";
await SendWelcomeMessageAsync(turnContext, dc, message,cancellationToken); //Welcome message
}
}
...
}
private static async Task SendWelcomeMessageAsync(ITurnContext turnContext, DialogContext dc,string message, CancellationToken cancellationToken)
{
foreach (var member in turnContext.Activity.MembersAdded)
{
if (member.Id != turnContext.Activity.Recipient.Id)
{
await turnContext.SendActivityAsync(message, cancellationToken: cancellationToken);
await dc.BeginDialogAsync(Activity2MainDialog, "activity2MainDialog", cancellationToken);
}
}
}
The dialog (Activity2MainDialog) works fine until it reaches a return await stepContext.ContinueDialogAsync(cancellationToken); call.
Then it halts.
I believe it has something to do with the conversation state but I still couldn't find a solution for that.
Code snippet of the return await stepContext.ContinueDialogAsync(cancellationToken); call
public class Activity2MainDialog : ComponentDialog
{
private static BellaMain BellaMain = new BellaMain();
private static FTDMain FTDMain = new FTDMain();
private readonly IStatePropertyAccessor<StudentProfile> _studentProfileAccessor;
...
public Activity2MainDialog(UserState userState, string dialogMainId)
: base(dialogMainId)
{
InitialDialogId = Id;
_studentProfileAccessor = userState.CreateProperty<StudentProfile>("studentProfile");
WaterfallStep[] waterfallSteps = new WaterfallStep[]
{
MsgWelcomeStepAsync
...
};
// Add named dialogs to the DialogSet. These names are saved in the dialog state.
AddDialog(new WaterfallDialog(dialogMainId, waterfallSteps));
AddDialog(new TextPrompt(nameof(TextPrompt)));
AddDialog(new ChoicePrompt(nameof(ChoicePrompt)));
AddDialog(new ConfirmPrompt(nameof(ConfirmPrompt)));
}
private async Task<DialogTurnResult> MsgWelcomeStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
await stepContext.Context.SendActivityAsync("**Oi**", "Oi", cancellationToken: cancellationToken);
return await stepContext.ContinueDialogAsync(cancellationToken);
}
private async Task<DialogTurnResult> QuestGoAheadStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
message = "Vamos nessa?";
await stepContext.Context.SendActivityAsync(message , message , cancellationToken: cancellationToken);
retryPromptMessage = message;
return await stepContext.PromptAsync(nameof(ChoicePrompt),
new PromptOptions
{
Prompt = null,
RetryPrompt = MessageFactory.Text(retryPromptMessage, retryPromptSpeakMessage), InputHints.ExpectingInput),
Choices = new[]
{
new Choice {Value = "Sim", Synonyms = new List<string> {"yes","yeah","esta","ta","esta","ok","vamos","curti","curtir","claro","tá","sei","top"}},
new Choice {Value = "Não", Synonyms = new List<string> {"no"}}
}.ToList(),
Style = ListStyle.Auto
});
}
Thoughts on how to fix it? Thx
I'm fairly certain the issue is the ContinueDialog call. You need to end that step with:
return await stepContext.NextAsync(null, cancellationToken);
See CoreBot for more example code.
If this doesn't fix your issue, let me know and I'll adjust my answer.
I am exploring the ASPBoilerplate framework and what interests me more is its real time notification capability. However, I am having this error.
Here is my Notification publisher class:
public class Publisher : Hub, ITransientDependency
{
private readonly INotificationPublisher _notificationPublisher;
public Publisher(INotificationPublisher notificationPublisher)
{
_notificationPublisher = notificationPublisher;
}
//Send a general notification to all subscribed users in current tenant (tenant in the session)
public async Task Publish_Announcement(string announcementMessage)
{
//Example "LowDiskWarningMessage" content for English -> "Attention! Only {remainingDiskInMb} MBs left on the disk!"
var data = new MessageNotificationData(announcementMessage);
await _notificationPublisher.PublishAsync("abp.notifications.received", data, severity: NotificationSeverity.Info);
}
}
And I am testing if it will notify all online users whenever a new user was created.
public override async Task<UserDto> Create(CreateUserDto input)
{
CheckCreatePermission();
var user = ObjectMapper.Map<User>(input);
user.TenantId = AbpSession.TenantId;
user.IsEmailConfirmed = true;
await _userManager.InitializeOptionsAsync(AbpSession.TenantId);
CheckErrors(await _userManager.CreateAsync(user, input.Password));
if (input.RoleNames != null)
{
CheckErrors(await _userManager.SetRoles(user, input.RoleNames));
}
CurrentUnitOfWork.SaveChanges();
//I cannot call the publisher class since it has dependencies in its contructor.
new Publisher().Publish_Announcement("Hi");
return MapToEntityDto(user);
}
Or am I just doing it wrong?
Please help. Thanks!
Basically, the Notifications is based on Publisher / Subsriber pattern and while the notifications are published through the system, the users who have subscribed to that notification will receive it.
Publish Notifications
public class MyService : ITransientDependency
{
private readonly INotificationPublisher _notificationPublisher;
public MyService(INotificationPublisher notificationPublisher)
{
_notificationPublisher = notificationPublisher;
}
//Send a general notification to a specific user
public async Task Publish_SentFriendshipRequest(string senderUserName, string friendshipMessage, UserIdentifier targetUserId)
{
await _notificationPublisher.PublishAsync("SentFriendshipRequest", new SentFriendshipRequestNotificationData(senderUserName, friendshipMessage), userIds: new[] { targetUserId });
}
//Send an entity notification to a specific user
public async Task Publish_CommentPhoto(string commenterUserName, string comment, Guid photoId, UserIdentifier photoOwnerUserId)
{
await _notificationPublisher.PublishAsync("CommentPhoto", new CommentPhotoNotificationData(commenterUserName, comment), new EntityIdentifier(typeof(Photo), photoId), userIds: new[] { photoOwnerUserId });
}
//Send a general notification to all subscribed users in current tenant (tenant in the session)
public async Task Publish_LowDisk(int remainingDiskInMb)
{
//Example "LowDiskWarningMessage" content for English -> "Attention! Only {remainingDiskInMb} MBs left on the disk!"
var data = new LocalizableMessageNotificationData(new LocalizableString("LowDiskWarningMessage", "MyLocalizationSourceName"));
data["remainingDiskInMb"] = remainingDiskInMb;
await _notificationPublisher.PublishAsync("System.LowDisk", data, severity: NotificationSeverity.Warn);
}
}
Subscribe to Notifications
public class MyService : ITransientDependency
{
private readonly INotificationSubscriptionManager _notificationSubscriptionManager;
public MyService(INotificationSubscriptionManager notificationSubscriptionManager)
{
_notificationSubscriptionManager = notificationSubscriptionManager;
}
//Subscribe to a general notification
public async Task Subscribe_SentFriendshipRequest(int? tenantId, long userId)
{
await _notificationSubscriptionManager.SubscribeAsync(new UserIdentifier(tenantId, userId), "SentFriendshipRequest");
}
//Subscribe to an entity notification
public async Task Subscribe_CommentPhoto(int? tenantId, long userId, Guid photoId)
{
await _notificationSubscriptionManager.SubscribeAsync(new UserIdentifier(tenantId, userId), "CommentPhoto", new EntityIdentifier(typeof(Photo), photoId));
}
}
Client-Side
This is done by signalr and you need to capture the event once it's triggered.
abp.event.on('abp.notifications.received', function (userNotification) {
if (userNotification.notification.data.type === 'Abp.Notifications.LocalizableMessageNotificationData') {
var localizedText = abp.localization.localize(
userNotification.notification.data.message.name,
userNotification.notification.data.message.sourceName
);
$.each(userNotification.notification.data.properties, function (key, value) {
localizedText = localizedText.replace('{' + key + '}', value);
});
alert('New localized notification: ' + localizedText);
} else if (userNotification.notification.data.type === 'Abp.Notifications.MessageNotificationData') {
alert('New simple notification: ' + userNotification.notification.data.message);
}
});
You can find more information here
I have a scorable dialog that will check if the incoming message is equal to "resetconversation". When true, the dialog resets the dialog stack in the PostAsync method.
When the postasync method is hit, the dialog stack resets as expected but when I again send a message the stack reappears and the conversation proceeds as if it was never reset.
[note]: I have noticed that this issue happens when a PromptDialog.Choice() is expecting an input.
Below is the scorable registration and the scorable dialog class.
Thanks!
Registering my scorable dialog:
Conversation.UpdateContainer(
builder =>
{
builder.RegisterModule(new AzureModule(Assembly.GetExecutingAssembly()));
string conn = ConfigurationManager.ConnectionStrings["BotDataContextConnectionString"].ConnectionString;
var store = new SqlBotDataStore(conn);
builder.Register(c => store)
.Keyed<IBotDataStore<BotData>>(AzureModule.Key_DataStore)
.AsSelf()
.SingleInstance();
//Scorable to check if dialog stack reset has been requested
builder.RegisterType<HandleResetRequestScorable>()
.AsSelf()
.As<IScorable<IActivity, double>>();
});
Scorable Dialog:
[Serializable]
public class HandleResetRequestScorable : ScorableBase<IActivity, bool, double>
{
private readonly IBotToUser _botToUser;
private readonly IDialogStack _dialogStack;
private bool _isMessageType { get; set; }
public HandleResetRequestScorable(IBotToUser botToUser, IDialogStack dialogTask)
{
SetField.NotNull(out _botToUser, nameof(_botToUser), botToUser);
//SetField.NotNull(out _dialogStack, nameof(_dialogStack), dialogTask);
}
protected override async Task<bool> PrepareAsync(IActivity activity, CancellationToken token)
{
string message = string.Empty;
if (activity.Type == ActivityTypes.Event)
{
_isMessageType = false;
message = activity.AsEventActivity().Name.ToLower();
}
else
{
_isMessageType = true;
message = activity.AsMessageActivity().Text.ToLower();
}
if (message == "resetconversation")
return true;
return false;
}
protected override bool HasScore(IActivity item, bool state)
{
return state;
}
protected override double GetScore(IActivity item, bool state)
{
return state ? 1.0 : 0;
}
protected override async Task PostAsync(IActivity activity, bool state, CancellationToken token)
{
try
{
var message = activity as IMessageActivity;
string[] data = message.Value.ToString().Split('|');
using (var scope = DialogModule.BeginLifetimeScope(Conversation.Container, activity as Activity))
{
//get the conversation related data
var botData = scope.Resolve<IBotData>();
await botData.LoadAsync(default(CancellationToken));
//get the dialog stack
var convoStack = scope.Resolve<IDialogStack>();
convoStack.Reset();
if (_isMessageType)
{
await _botToUser.PostAsync("Conversation reset complete.");
await _botToUser.PostAsync("Hi");
}
await botData.FlushAsync(default(CancellationToken));
}
}
catch
{
throw;
}
}
protected override Task DoneAsync(IActivity item, bool state, CancellationToken token)
{
//throw new NotImplementedException();
return Task.CompletedTask;
}
}
}
I have a bot where I am doing something like this:
1) A new Activity (message) arrives.
2) I dispatch the message to a RootDialog.
3) Depending on some Logic, RootDialog can:
a) Call a LuisDialog (handling natural language)
b) Call a CustomDialog (handles some business logic).
But when the user state is reset, and the flow leads to an intention inside the LuisDialog, it calls the intention method twice. Just the first time the state is empty, then it works fine.
Let me show you the code:
MessagesController:
public async Task<HttpResponseMessage> Post([FromBody]Activity activity)
{
if (activity.Type == ActivityTypes.Message)
{
ConnectorClient connector = new ConnectorClient(new Uri(activity.ServiceUrl));
try
{
await Conversation.SendAsync(activity, () => new RootDialog());
}
catch (HttpRequestException e)
{
...
}
}
RootDialog:
public class RootDialog : IDialog<object>
{
public async Task StartAsync(IDialogContext context)
{
await MessageReceivedAsync(context, Awaitable.FromItem(context.Activity.AsMessageActivity()));
}
private async Task MessageReceivedAsync(IDialogContext context, IAwaitable<IMessageActivity> awaitable)
{
bool value = DoSomeCustomLogic();
if (value)
{
string message = DoSomething();
await context.PostAsync(message);
} else {
bool value2 = DoSomeCustomLogic2();
if (value2)
{
var answerValidationDialog = new ValidateAnswerWithUserDialog();
context.Call(answerValidationDialog, ValidateAnswerWithUserDialogCompletedCallback);
} else {
var luisDialog = new LuisDialog();
await context.Forward(luisDialog,LuisDialogCompletedCallback, context.Activity, CancellationToken.None);
}
}
}
Callbacks only do context.Done(true);
And LuisDialog has an Intention which goes like this:
[LuisIntent(LuisUtils.INTENT_MENU_SALUTE)]
public async Task SaluteOrMenu(IDialogContext context, LuisResult result)
{
if (LuisUtils.IntentScoreIsHighEnough(result))
{
string userName = context.Activity.From.Name;
ContextHelper helper = new ContextHelper(MessageReceived);
await helper.AskUserToDoSomeInitialAction(context, saluteWord, userName);
context.Done(true);
}
else
{
await None(context, result);
}
}
And finally class ContextHelper:
public class ContextHelper
{
private Func<IDialogContext, IAwaitable<IMessageActivity>, Task> MessageReceived;
public ContextHelper(Func<IDialogContext, IAwaitable<IMessageActivity>, Task> messageReceived)
{
MessageReceived = messageReceived;
}
public async Task AskUserToDoSomeInitialAction(IDialogContext context, string saluteWord, string userName)
{
var reply = context.MakeMessage();
List<CardAction> buttons = BuildInitialOptionActions();
List<CardImage> images = BuildInitialOptionImages();
string initialText = $"Hi stranger!"
var card = new HeroCard
{
Title = "Hello!"
Text = initialText,
Buttons = buttons,
Images = images
};
reply.Attachments = new List<Attachment> { card.ToAttachment() };
await context.PostAsync(reply);
context.Wait(AfterUserChoseOptionInSalute);
}
private async Task AfterUserChoseOptionInSalute(IDialogContext context, IAwaitable<IMessageActivity> result)
{
await ReDispatchMessageReceivedToDialog(context);
}
private async Task ReDispatchMessageReceivedToDialog(IDialogContext context)
{
await MessageReceived(context, Awaitable.FromItem(context.Activity.AsMessageActivity()));
}
}
The SaluteOrMenu Intention gets called twice (only the first time I interact with the bot or when I delete the state. After Debugging I saw that after doing context.Wait(AfterUserChoseOptionInSalute);, the bot calls that function (instead of waiting for an event to call it)
Any ideas?
Thanks in advance.
I found the line that was wrong. It was on the first dialog (the RootDialog):
public async Task StartAsync(IDialogContext context)
{
await MessageReceivedAsync(context, Awaitable.FromItem(context.Activity.AsMessageActivity()));
}
That is a line that re dispatches a message with the incoming activity. I had it somewhere in some chunk of code and (don't know why), thought it was a good idea to use it on StartAsync. So, because of that, two calls were occurring.
Dumb me.
I just changed it to this and worked:
public async Task StartAsync(IDialogContext context)
{
context.Wait(MessageReceivedAsync);
}