Multiple QnA Service in one Bot using C# - c#

I do have 3 QnA Service. I want them to be used in a single BOT at the same time. How can this be implemented using C#. My initial idea is to put the KB ID and Sub Key into an array (how to implement that or would an array works?).. I saw some code in Node.JS but I cannot figure it out how to convert the code in C#.
public class QnaDialog : QnAMakerDialog
{
public QnaDialog() : base(
new QnAMakerService(new QnAMakerAttribute(ConfigurationManager.AppSettings["QnaSubscriptionKey1"],
ConfigurationManager.AppSettings["QnaKnowledgebaseId1"], "Hmm, I wasn't able to find an article about that. Can you try asking in a different way?", 0.5)),
new QnAMakerService(new QnAMakerAttribute(ConfigurationManager.AppSettings["QnaSubscriptionKey2"],
ConfigurationManager.AppSettings["QnaKnowledgebaseId2"], "Hmm, I wasn't able to find an article about that. Can you try asking in a different way?", 0.5)),
new QnAMakerService(new QnAMakerAttribute(ConfigurationManager.AppSettings["QnaSubscriptionKey3"],
ConfigurationManager.AppSettings["QnaKnowledgebaseId4"], "Hmm, I wasn't able to find an article about that. Can you try asking in a different way?", 0.5))
)
{
}
}

You can use several QnAMaker knowledge bases in a single bot by providing several services in the attributes.
A basic implementation using QnAMakerDialog from Nuget package BotBuilder.CognitiveServices would be:
[Serializable]
[QnAMaker("QnaSubscriptionKey1", "QnaKnowledgebaseId1", "Hmm, I wasn't able to find an article about that. Can you try asking in a different way?", 0.50, 3)]
[QnAMaker("QnaSubscriptionKey2", "QnaKnowledgebaseId2", "Hmm, I wasn't able to find an article about that. Can you try asking in a different way?", 0.5, 3)]
[QnAMaker("QnaSubscriptionKey3", "QnaKnowledgebaseId3", "Hmm, I wasn't able to find an article about that. Can you try asking in a different way?", 0.5, 3)]
public class RootDialog : QnAMakerDialog
{
}
BUT (yes, there is a "but") you may encounter an exception during the treatment of your messages in some cases. As QnAMakerDialog is open-sourced (sources are here) you can easily discover that the problem is in the implementation of the return of the services calls, in MessageReceivedAsync:
var sendDefaultMessageAndWait = true;
qnaMakerResults = tasks.FirstOrDefault(x => x.Result.ServiceCfg != null)?.Result;
if (tasks.Count(x => x.Result.Answers?.Count > 0) > 0)
{
var maxValue = tasks.Max(x => x.Result.Answers[0].Score);
qnaMakerResults = tasks.First(x => x.Result.Answers[0].Score == maxValue).Result;
if (qnaMakerResults != null && qnaMakerResults.Answers != null && qnaMakerResults.Answers.Count > 0)
{
if (this.IsConfidentAnswer(qnaMakerResults))
{
await this.RespondFromQnAMakerResultAsync(context, message, qnaMakerResults);
await this.DefaultWaitNextMessageAsync(context, message, qnaMakerResults);
}
else
{
feedbackRecord = new FeedbackRecord { UserId = message.From.Id, UserQuestion = message.Text };
await this.QnAFeedbackStepAsync(context, qnaMakerResults);
}
sendDefaultMessageAndWait = false;
}
}
if (sendDefaultMessageAndWait)
{
await context.PostAsync(qnaMakerResults.ServiceCfg.DefaultMessage);
await this.DefaultWaitNextMessageAsync(context, message, qnaMakerResults);
}
In this code, this line of code will break if not all of your services have an answer for your question (ie: if at least one of your QnAMaker KB does not have an answer for your question)
tasks.Max(x => x.Result.Answers[0].Score);
Workaround: you can implement you own QnAMakerDialog by getting the sources and fixing the method like this for example:
public async Task MessageReceivedAsync(IDialogContext context, IAwaitable<IMessageActivity> argument)
{
var message = await argument;
if (message != null && !string.IsNullOrEmpty(message.Text))
{
var tasks = this.services.Select(s => s.QueryServiceAsync(message.Text)).ToArray();
await Task.WhenAll(tasks);
if (tasks.Any())
{
var sendDefaultMessageAndWait = true;
qnaMakerResults = tasks.FirstOrDefault(x => x.Result.ServiceCfg != null)?.Result;
var qnaMakerFoundResults = tasks.Where(x => x.Result.Answers.Any()).ToList();
if (qnaMakerFoundResults.Any())
{
var maxValue = qnaMakerFoundResults.Max(x => x.Result.Answers[0].Score);
qnaMakerResults = qnaMakerFoundResults.First(x => x.Result.Answers[0].Score == maxValue).Result;
if (qnaMakerResults?.Answers != null && qnaMakerResults.Answers.Count > 0)
{
if (this.IsConfidentAnswer(qnaMakerResults))
{
await this.RespondFromQnAMakerResultAsync(context, message, qnaMakerResults);
await this.DefaultWaitNextMessageAsync(context, message, qnaMakerResults);
}
else
{
feedbackRecord = new FeedbackRecord { UserId = message.From.Id, UserQuestion = message.Text };
await this.QnAFeedbackStepAsync(context, qnaMakerResults);
}
sendDefaultMessageAndWait = false;
}
}
if (sendDefaultMessageAndWait)
{
await context.PostAsync(qnaMakerResults.ServiceCfg.DefaultMessage);
await this.DefaultWaitNextMessageAsync(context, message, qnaMakerResults);
}
}
}
}

Related

Correct way to use DomainManager Class on insert and update entity (Aggregate Root)

This question it's about ABP framework implementation of DDD,in the Domain layer there a class named "Entity"Manager, this class is a factory for the domain entity and have the method CreateAsync for entity instantiation. I want to reuse this method (or a better approach) to update my entity without redoing the same logic on Application layer. This is the method on AporteManager.
public async Task<Aporte> CreateAsync(
AporteValidation valid
)
{
var icontaBancaria = await _contaBancariaRepository.WithDetailsAsync(
x => x.Banco, x => x.FonteDeRecurso, x => x.CodigoDeAplicacao, x => x.Fundo, x => x.FonteDeRecursoSICONFI);
var contaBancaria = icontaBancaria.Where(x => x.Id == valid.ContaBancariaId).FirstOrDefault();
valid.FonteDeRecursoId = contaBancaria.FonteDeRecurso?.Id;
valid.CodigoDeAplicacaoId = contaBancaria.CodigoDeAplicacao?.Id;
return new Aporte(
valid
);
}
And this is my update method on Application:
[Authorize(ContabilidadePermissions.Aporte.Edit)]
public async Task UpdateAsync(int id, CreateUpdateAporteDto input)
{
try
{
var aporte = await _aporteRepository.GetAsync(id);
if (aporte is not null)
{
var validation = ObjectMapper.Map<CreateUpdateAporteDto, AporteValidation>(input);
var aporteValidado = await _aporteManager.CreateAsync(validation);
aporte = ObjectMapper.Map(aporteValidado, aporte);
await _aporteRepository.UpdateAsync(aporte);
//aporte = ObjectMapper.Map(input, aporte);
//await _aporteRepository.UpdateAsync(aporte);
}
else
{
throw new EntidadeNaoExisteParaSerAtualizadaException();
}
}
catch (Exception ex)
{
throw new RegistroJaExisteGenericoException("Aporte", "Código");
}
}
Although UpdateAsync is not working, I'm not just looking to a solution, but a correct, maintainable and simple DDD solution. Here is a sample of the project: https://github.com/heitorgiacominibrasil/DomainManager
you must see the tutorials in the docs.abp.io
https://docs.abp.io/en/abp/latest/Tutorials/Part-6?UI=MVC&DB=Mongo
AuthorManager: The Domain Service
https://docs.abp.io/en/abp/latest/Tutorials/Part-6?UI=MVC&DB=Mongo#authormanager-the-domain-service

.NET Core API -> Updating an object also update previously loaded object

I have implemented a system to log every create/update/delete actions on all controllers.
Basically, the idea is simple as when I update an object in the Update controller, I first do a Get then Update so I have access to original object and updated object as follow.
public async Task<IActionResult> Update([FromRoute] Guid groupId, [FromRoute] Guid licenseId, [FromBody] UpdateLicenseRequest request)
{
var cLicense = await _licenseService.GetAsync(groupId, licenseId);
if (cLicense == null)
return BadRequest(_localizer["LicenseNotExist"]);
var uLicense = await _licenseService.UpdateAsync(cLicense, _mapper.Map<License>(request));
if (uLicense == null)
return BadRequest();
await _auditService.CreateAsync(AuditTypeEnum.SVAULT_LICENSE_UPDATE, cLicense, uLicense);
return Ok(_mapper.Map<LicenseResponse>(uLicense));
}
Here is the detail of the GetAsync method :
public async Task<License> GetAsync(Guid groupId, Guid licenseId)
{
RSUser cRSUser = await _cloudService.GetCloudAsync(groupId);
var rxLicense = await Task.Run(() =>
{
return cRSUser.getLicense(licenseId.ToString());
});
List<LicenseState> lstStates = await _context.LicenseStates.ToListAsync();
List<LicenseType> lstTypes = await _context.LicenseTypes.ToListAsync();
List<LicenseStatus> lstStatus = await _context.LicenseStatus.ToListAsync();
List<LicenseProduct> lstProducts = await _context.LicenseProducts.ToListAsync();
Dictionary<string, Group> dcGroups = _context.GroupProperties.AsNoTracking().Where(w => w.Name == GroupProperties.community_id.ToString() && (w.Value == rxLicense.P_community_id || w.Value == rxLicense.P_community_customer_id || w.Value == rxLicense.P_community_reseller_id)).Include(w => w.Group).ToDictionary(w => w.Value, w => w.Group);
if (rxLicense == null)
return null;
return License.ToObject(rxLicense, dcGroups, lstStates, lstTypes, lstStatus, lstProducts);
}
Also find the detail of the Update method :
public async Task<License> UpdateAsync(License cLicense, License nLicense)
{
RSUser cRSUser = await _cloudService.GetCloudAsync(cLicense.Group.Id);
var result = await Task.Run(() =>
{
cLicense.TypeId = nLicense.TypeId;
cLicense.DataLimit = nLicense.DataLimit;
cLicense.Email = nLicense.Email;
cLicense.Notes = nLicense.Notes;
if (cLicense.TypeId == 8)
cLicense.DataLimit = 0;
return cRSUser.updateLicense(cLicense.ToRSUserClass(), new List<RX_licensemode>() { new RX_licensemode() { P_id = nLicense.ProductId.ToString().ToUpper() } }); ;
});
if (result != RSC_errorcode.success)
return null;
return await GetAsync(cLicense.Group.Id, cLicense.Id);
}
When I do debug and make a stop on cLicense, I see that this object is the current License (which is what I want) but as soon as the UpdateAsync method runs, cLicense is also updated while I want it to remain as the original object.
Does anyone has already had same behavior ? I'm sure this is a normal process but I don't know how to have it working as I wish.
Feel free to ask if anything is not clear enough.
Regards,
When you pass cLicense to UpdateAsync, you aren't passing a copy, you are passing THE cLicence. In UpdateAsync you are setting the properties of cLicense to the values of nLicense, which is why you are seeing the values change.

My Discord Bot won't accept Users as parameters for commands

I'm currently working on a Discord bot to learn how to code one. I thought I had it down, but when I try to use the following command, it does nothing:
[Command("ping")]
public async Task Ping(IUser user)
{
await Context.Channel.SendMessageAsync(user.ToString());
}
It's part of a public class, and if I use any other parameter type (e.g. IChannel, bool, int) it works. It's just this one parameter type. It also doesn't log any errors or exceptions. Any ideas?
[Command("ping")]
public async Task Ping(IUser user)
{
await Context.Channel.SendMessageAsync(user.ToString());
}
Your code id perfect. But think about this, the user is of the type IUser and your conversion to sting makes it vague. Instead try this:
[Command("ping")]
public async Task Ping(SocketGuildUser user)
{
await Context.Channel.SendMessageAsync(user.Username);
}
If you want to ping the user try user.Mention.
Also when I started learning I made a bot as well. Here is the source code. Its very very very basic. It definitely will help.
You could try using this workaround for your bot:
public async Task SampleCommand(string user="", [Remainder]string message="")
{
IUser subject = null;
if (user != "")
{
var guilds = (await Context.Client.GetGuildsAsync(Discord.CacheMode.AllowDownload));
var users = new List<IUser>();
foreach (var g in guilds)
users.AddRange(await g.GetUsersAsync(CacheMode.AllowDownload));
users = users.GroupBy(o => o.Id).Select(o => o.First()).ToList();
var search = users.Where(o => o.Username.ToLower().Contains(user.ToLower()) || Context.Message.MentionedUserIds.Contains(o.Id) || o.ToString().ToLower().Contains(user.ToLower())).ToArray();
if (search.Length == 0)
{
await ReplyAsync("***Error!*** *Couldn't find that user.*");
return;
}
else if (search.Length > 1)
{
await ReplyAsync("***Error!*** *Found more than one matching users.*");
return;
}
subject = search.First();
}
// ...
// execute command
Or you could wrap that in a method for easier access and reusability.
Basically, what it does is it looks for available users that match the given string (in nickname, username or mentions. You could also make it check for IDs if you so desire).
Edit: In my case I'm allowing people to mention anyone who shares the server with the bot, but in your case it might be more benefitial to just use the Context.Guild instead and cancel the command in case of DMs.
I ended up taking Reynevan's advice, and wrote a method for converting a mention into an IUser. Just call CustomUserTypereader.GetUser(mention_parameter, Context.Guild);
using System.Threading.Tasks;
using Discord;
public class CustomUserTypereader
{
public static async Task<IUser> GetUserFromString(string s, IGuild server)
{
if (s.IndexOf('#') == -1 || s.Replace("<", "").Replace(">", "").Length != s.Length - 2)
throw new System.Exception("Not a valid user mention.");
string idStr = s.Replace("<", "").Replace(">", "").Replace("#", "");
try
{
ulong id = ulong.Parse(idStr);
return await server.GetUserAsync(id);
}
catch
{
throw new System.Exception("Could not parse User ID. Are you sure the user is still on the server?");
}
}
}

Skip displaying form fields based on user confirmation

I have some 10 properties in a class and based on those properties I have a form that asks for user input. I want to know if there's any mechanism that after initial 4-5 questions I ask user if he/she wants to ans more and if reply is yes then next set of fields/questions are asked.
I tried doing it with SetDefine but issue with SetDefine is that its called with each field so it asks the user to confirm with each fiels but I want it to only confirm with 1st optional field and based on the answer either skip all or get all.
public async Task StartAsync(IDialogContext context)
{
await context.PostAsync($"Welcome to the Order helper!");
var orderFormDialog = FormDialog.FromForm(BuildOrderAdvanceStepSearchForm, FormOptions.PromptInStart);
context.Call(orderFormDialog, ResumeAfterOrdersFormDialog);
}
private IForm<OrderSearchQuery> BuildOrderAdvanceStepSearchForm()
{
return new FormBuilder<OrderSearchQuery>()
.Field(nameof(OrderSearchQuery.ItemNumber))
.Field(nameof(OrderSearchQuery.Draft))
.Field(nameof(OrderSearchQuery.Dispatched))
.Field(nameof(OrderSearchQuery.InTransit))
.Field(nameof(OrderSearchQuery.Delivered))
//.Confirm("Do you want to search with more options?.")
//.Field(nameof(OrderSearchQuery.AddField1))
.Field(new FieldReflector<OrderSearchQuery>(nameof(OrderSearchQuery.AddField1))
.SetDefine(OrderAdvanceStepConfirmation))
.Field(new FieldReflector<OrderSearchQuery>(nameof(OrderSearchQuery.AddField2))
.SetDefine(OrderAdvanceStepConfirmation))
.Field(new FieldReflector<OrderSearchQuery>(nameof(OrderSearchQuery.AddField3))
.SetDefine(OrderAdvanceStepConfirmation))
.Field(new FieldReflector<OrderSearchQuery>(nameof(OrderSearchQuery.AddField4))
.SetDefine(OrderAdvanceStepConfirmation))
.Field(new FieldReflector<OrderSearchQuery>(nameof(OrderSearchQuery.AddField5))
.SetDefine(OrderAdvanceStepConfirmation))
.Build();
}
private static async Task<bool> OrderAdvanceStepConfirmation(OrderSearchQuery state, Field<OrderSearchQuery> field)
{
field.SetPrompt(new PromptAttribute($"Do you want to search with more options?."));
return true;
}
private async Task ResumeAfterordersFormDialog(IDialogContext context, IAwaitable<OrderSearchQuery> result)
{
try
{
var searchQuery = await result;
await context.PostAsync($"Ok. Searching for orders...");
var count = 100;
if (count > 1)
{
await context.PostAsync($"I found total of 100 orders");
await context.PostAsync($"To get order details, you will need to provide more info...");
}
else
{
await context.PostAsync($"I found the order you were looking for...");
await context.PostAsync($"Now I can provide you information related to Consumer Package, Multi-Pack, Shelf Tray & Unit Load for this order.");
}
}
catch (FormCanceledException ex)
{
string reply;
if (ex.InnerException == null)
{
reply = "You have canceled the operation. Quitting from the order Search";
}
else
{
reply = $"Oops! Something went wrong :( Technical Details: {ex.InnerException.Message}";
}
await context.PostAsync(reply);
}
finally
{
//context.Done<object>(null);
}
}
I want it to only confirm with 1st optional field and based on the answer either skip all or get all.
You can use SetNext of FieldReflector.
For example create a enum for confirmation for example like this:
public enum Confirmation
{
Yes,
No
}
public Confirmation? Corfirm { get; set; }
And then you can build the Form like this:
return new FormBuilder<OrderSearchQuery>()
.Field(nameof(ItemNumber))
.Field(nameof(Draft))
.Field(new FieldReflector<OrderSearchQuery>(nameof(Corfirm))
.SetNext((value, state) =>
{
var selection = (Confirmation)value;
if (selection == Confirmation.Yes)
{
//go to step 1
return new NextStep();
}
else
{
//skip the following steps
state.Stpe1 = null;
state.Step2 = null;
return new NextStep(StepDirection.Complete);
}
})
)
.Field(nameof(Stpe1))
.Field(nameof(Step2)).Build();

Hangfire get last execution time

I'm using hangfire 1.5.3. In my recurring job I want to call a service that uses the time since the last run. Unfortunately the LastExecution is set to the current time, because the job data was updated before executing the job.
Job
public abstract class RecurringJobBase
{
protected RecurringJobDto GetJob(string jobId)
{
using (var connection = JobStorage.Current.GetConnection())
{
return connection.GetRecurringJobs().FirstOrDefault(p => p.Id == jobId);
}
}
protected DateTime GetLastRun(string jobId)
{
var job = GetJob(jobId);
if (job != null && job.LastExecution.HasValue)
{
return job.LastExecution.Value.ToLocalTime();
}
return DateTime.Today;
}
}
public class NotifyQueryFilterSubscribersJob : RecurringJobBase
{
public const string JobId = "NotifyQueryFilterSubscribersJob";
private readonly IEntityFilterChangeNotificationService _notificationService;
public NotifyQueryFilterSubscribersJob(IEntityFilterChangeNotificationService notificationService)
{
_notificationService = notificationService;
}
public void Run()
{
var lastRun = GetLastRun(JobId);
_notificationService.CheckChangesAndSendNotifications(DateTime.Now - lastRun);
}
}
Register
RecurringJob.AddOrUpdate<NotifyQueryFilterSubscribersJob>(NotifyQueryFilterSubscribersJob.JobId, job => job.Run(), Cron.Minutely, TimeZoneInfo.Local);
I know, that it is configured as minutely, so I could calculate the time roughly. But I'd like to have a configuration independent implementation. So my Question is: How can I implement RecurringJobBase.GetLastRun to return the time of the previous run?
To address my comment above, where you might have more than one type of recurring job running but want to check previous states, you can check that the previous job info actually relates to this type of job by the following (although this feels a bit hacky/convoluted).
If you're passing the PerformContext into the job method than you can use this:
var jobName = performContext.BackgroundJob.Job.ToString();
var currentJobId = int.Parse(performContext.BackgroundJob.Id);
JobData jobFoundInfo = null;
using (var connection = JobStorage.Current.GetConnection()) {
var decrementId = currentJobId;
while (decrementId > currentJobId - 50 && decrementId > 1) { // try up to 50 jobs previously
decrementId--;
var jobInfo = connection.GetJobData(decrementId.ToString());
if (jobInfo.Job.ToString().Equals(jobName)) { // **THIS IS THE CHECK**
jobFoundInfo = jobInfo;
break;
}
}
if (jobFoundInfo == null) {
throw new Exception($"Could not find the previous run for job with name {jobName}");
}
return jobFoundInfo;
}
You could take advantage of the fact you already stated - "Unfortunately the LastExecution is set to the current time, because the job data was updated before executing the job".
The job includes the "LastJobId" property which seems to be an incremented Id. Hence, you should be able to get the "real" previous job by decrement LastJobId and querying the job data for that Id.
var currentJob = connection.GetRecurringJobs().FirstOrDefault(p => p.Id == CheckForExpiredPasswordsId);
if (currentJob == null)
{
return null; // Or whatever suits you
}
var previousJob = connection.GetJobData((Convert.ToInt32(currentJob.LastJobId) - 1).ToString());
return previousJob.CreatedAt;
Note that this is the time of creation, not execution. But it might be accurate enough for you. Bear in mind the edge case when it is your first run, hence there will be no previous job.
After digging around, I came up with the following solution.
var lastSucceded = JobStorage.Current.GetMonitoringApi().SucceededJobs(0, 1000).OrderByDescending(j => j.Value.SucceededAt).FirstOrDefault(j => j.Value.Job.Method.Name == "MethodName" && j.Value.Job.Type.FullName == "NameSpace.To.Class.Containing.The.Method").Value;
var lastExec = lastSucceded.SucceededAt?.AddMilliseconds(Convert.ToDouble(-lastSucceded.TotalDuration));
It's not perfect but i think a little cleaner than the other solutions.
Hopefully they will implement an official way soon.
The answer by #Marius Steinbach is often good enough but if you have thousands of job executions (my case) loading all of them from DB doesn't seem that great. So finally I decided to write a simple SQL query and use it directly (this is for PostgreSQL storage though changing it to SqlServer should be straightforward):
private async Task<DateTime?> GetLastSuccessfulExecutionTime(string jobType)
{
await using var conn = new NpgsqlConnection(_connectionString);
if (conn.State == ConnectionState.Closed)
conn.Open();
await using var cmd = new NpgsqlCommand(#"
SELECT s.data FROM hangfire.job j
LEFT JOIN hangfire.state s ON j.stateid = s.id
WHERE j.invocationdata LIKE $1 AND j.statename = $2
ORDER BY s.createdat DESC
LIMIT 1", conn)
{
Parameters =
{
new() { Value = $"%{jobType}%" } ,
new() { Value = SucceededState.StateName }
}
};
var result = await cmd.ExecuteScalarAsync();
if (result is not string data)
return null;
var stateData = JsonSerializer.Deserialize<Dictionary<string, string>>(data);
return JobHelper.DeserializeNullableDateTime(stateData?.GetValueOrDefault("SucceededAt"));
}
Use this method that return Last exucution time and Next execution time of one job. this method return last and next execution time of one job.
public static (DateTime?, DateTime?) GetExecutionDateTimes(string jobName)
{
DateTime? lastExecutionDateTime = null;
DateTime? nextExecutionDateTime = null;
using (var connection = JobStorage.Current.GetConnection())
{
var job = connection.GetRecurringJobs().FirstOrDefault(p => p.Id == jobName);
if (job != null && job.LastExecution.HasValue)
lastExecutionDateTime = job.LastExecution;
if (job != null && job.NextExecution.HasValue)
nextExecutionDateTime = job.NextExecution;
}
return (lastExecutionDateTime, nextExecutionDateTime);
}

Categories

Resources