I have a simple Bot as below:
[Serializable]
[Template(TemplateUsage.NotUnderstood, "I do not understand \"{0}\".", "Try again, I don't get \"{0}\".")]
class MyOrder
{
public string Subject;
public string Description;
public static IForm<MyOrder> BuildForm()
{
return new FormBuilder<MyOrder>()
.Field(nameof(MyOrder.Subject), "What Subject should I use?")
.Field(nameof(MyOrder.Description), "And what Description?")
.AddRemainingFields()
.OnCompletionAsync(MyFormComplete)
.Build();
}
private static async Task MyFormComplete(IDialogContext context, MyOrder order)
{
if (order != null)
{
await context.PostAsync($"Created. Number is 9833");
}
else
{
await context.PostAsync("Form returned empty response!");
}
}
Once the form is completed the MyFormComplete callback is made.
First question - How do I get access to the client details in that function? I need to know the Skype handle so that I can map it to a internal user.
Secondly - After completing the form I can't start a new one. No matter what I enter on the client it just keeps triggering the callback function. There must be a way to terminate the session/conversation so that the next text from the Skype client will start a new conversation/form. Yeah?
Worked out how to get the Skype caller id inside the Dialog's CompletionDelegate. Simply add the message's From details to the message.BotUserData inside the MessageController before building the dialog.
message.BotUserData = JObject.FromObject(message.From)
I can then access this on the context within that callback.
Related
How can I close or reset a conversation programmatically from Java app?. According to Dialogflow CX documentation "A session remains active and its data is stored for 30 minutes after the last request is sent for the session."
I want to keep the session active for less time. For example, if I want the session to be active for 5 minutes, when user sends a message 5 minutes or more after last message, conversation must start again and previous flows must be closed and context parameters must be deleted.
With Dialogflow ES it is posible using ContextsClient, however new version does not offer ContextsClient class.
Dialogflow CX uses State Handlers to control conversation paths, unlike Dialogflow ES which uses Contexts.
For Dialogflow CX, you can end the current session by using the END_SESSION symbolic transition target. Once the END_SESSION transition target is invoked, it clears the current session and the next user input will restart the session at the start page of the Default Start Flow.
To achieve your desired use case, you’ll have to create your own implementation for it. Note that the solution below will only work if you integrate your Dialogflow CX agent to a custom front-end.
First, you should add an Event Handler to all of your Pages - so that the Event Handler will be accessible in any part of the conversation flow. In this Event Handler, define a custom event - for example: clearsession. Then, set its Transition to End Session Page. Once the clearsession event is invoked, it will end the current session.
Then, using your own business logic, you can create a custom function that could act as a timer for each user query. Once the timer reaches 5 minutes, your custom application should send a detectIntent request to your CX agent programmatically. This detectIntent request must contain the current session ID and the custom event (from the previously created Event Handler).
Here’s a sample detectIntent request that invokes a custom event using the Java Client Library:
// [START dialogflow_cx_detect_intent_event]
import com.google.api.gax.rpc.ApiException;
import com.google.cloud.dialogflow.cx.v3.*;
import com.google.common.collect.Maps;
import java.io.IOException;
import java.util.List;
import java.util.Map;
public class DetectIntent {
// DialogFlow API Detect Intent sample with event input.
public static Map<String, QueryResult> detectIntentEvent(
String projectId,
String locationId,
String agentId,
String sessionId,
String languageCode,
String event)
throws IOException, ApiException {
SessionsSettings.Builder sessionsSettingsBuilder = SessionsSettings.newBuilder();
if (locationId.equals("global")) {
sessionsSettingsBuilder.setEndpoint("dialogflow.googleapis.com:443");
} else {
sessionsSettingsBuilder.setEndpoint(locationId + "-dialogflow.googleapis.com:443");
}
SessionsSettings sessionsSettings = sessionsSettingsBuilder.build();
Map<String, QueryResult> queryResults = Maps.newHashMap();
// Instantiates a client
try (SessionsClient sessionsClient = SessionsClient.create(sessionsSettings)) {
// Set the session name using the projectID (my-project-id), locationID (global), agentID
// (UUID), and sessionId (UUID).
SessionName session = SessionName.of(projectId, locationId, agentId, sessionId);
System.out.println("Session Path: " + session.toString());
EventInput.Builder eventInput = EventInput.newBuilder().setEvent(event);
// Build the query with the EventInput and language code (en-US).
QueryInput queryInput =
QueryInput.newBuilder().setEvent(eventInput).setLanguageCode(languageCode).build();
// Build the DetectIntentRequest with the SessionName and QueryInput.
DetectIntentRequest request =
DetectIntentRequest.newBuilder()
.setSession(session.toString())
.setQueryInput(queryInput)
.build();
// Performs the detect intent request.
DetectIntentResponse response = sessionsClient.detectIntent(request);
// Display the query result.
QueryResult queryResult = response.getQueryResult();
System.out.println("====================");
System.out.format(
"Detected Intent: %s (confidence: %f)\n",
queryResult.getIntent().getDisplayName(), queryResult.getIntentDetectionConfidence());
}
return queryResults;
}
public static void main(String[] args) {
String projectId = "<project-id>";
String locationId = "<location-id>";
String agentId = "<agent-id>";
String sessionId = "<current-session-id>";
String languageCode = "<language-code>";
String event = "clearsession";
try{
detectIntentEvent(projectId,locationId,agentId,sessionId, languageCode, event);
} catch (IOException e){
System.out.println(e.getMessage());
}
}
}
// [END dialogflow_cx_detect_intent_event]
I have a chatbot that works on localhost, and it's working great. I then added a new Bot Channels Registration on Azure for testing, and that works fine too. I did it by taking its Microsoft App ID and password and putting it into my appsettings.json file.
However, I need to add another Bot Channels Registration. When I test it on that registration, my bot returns a 401 unauthorized error. It's because that has a new App ID and password. But I already put the App ID and password from my first registration channel. I need both of them to work.
How can I allow my chatbot to accept multiple App IDs and passwords? Or how do I get rid of that level of security completely (ie. Allow ALL App IDs and passwords)?
The answer, as #Mick suggested, is to create a bot adapter for each channel. You can do something like this if you want it really dynamic:
BotController.cs
[HttpPost, HttpGet]
public async Task PostAsync()
{
var credentialProvider = new SimpleCredentialProvider(YourAppId, YourAppPassword); // for each adapter
Adapter = new BotFrameworkHttpAdapter(credentialProvider); // for each adapter
await Adapter.ProcessAsync(Request, Response, Bot);
}
With a custom ICredentialProvider the appid and password can be retrieved from anywhere:
public class MultiCredentialProvider : ICredentialProvider
{
public Dictionary<string, string> Credentials = new Dictionary<string, string>
{
{ "YOUR_MSAPP_ID_1", "YOUR_MSAPP_PASSWORD_1" },
{ "YOUR_MSAPP_ID_2", "YOUR_MSAPP_PASSWORD_2" }
};
public Task<bool> IsValidAppIdAsync(string appId)
{
return Task.FromResult(this.Credentials.ContainsKey(appId));
}
public Task<string> GetAppPasswordAsync(string appId)
{
return Task.FromResult(this.Credentials.ContainsKey(appId) ? this.Credentials[appId] : null);
}
public Task<bool> IsAuthenticationDisabledAsync()
{
return Task.FromResult(!this.Credentials.Any());
}
}
Then, in Startup.cs:
services.AddSingleton<ICredentialProvider, MultiCredentialProvider>();
I'm trying to achieve the following:
Have an unauthenticated user navigate to a web page, where a SignalR (core) client will connect to a hub (say Notifications hub).
Have the user perform an action and, when the operation is completed on the server, use SignalR to notify him of the completion.
The problem: when a user is logged, I find his SignalR connectionId by a connectionId-username map that is saved in memory. Then I do:
hub.SendConnectionAsync(connectionId, "Message", data);
If the user is not authenticated, I came up with using SessionId, and the map I save in memory is something that gives me a ConnectionId given a SessionId. The code snippet I use on the HubLifetimeManager is something like:
public override async Task OnConnectedAsync(HubConnectionContext connection)
{
await _wrappedHubLifetimeManager.OnConnectedAsync(connection);
_connections.Add(connection);
string userId;
if (connection.User.Identity.IsAuthenticated)
{
userId = connection.User.Identity.Name;
}
else
{
var httpContext = connection.GetHttpContext();
if (httpContext == null)
{
throw new Exception("HttpContext can't be null in a SignalR Hub!!");
}
var sessionId = httpContext.Session.Id;
userId = $"{Constants.AnonymousUserIdentifierPrefix}{sessionId}";
}
await _userTracker.AddUser(connection, new UserDetails(connection.ConnectionId, userId));
}
Problem: if my page is opened in an iframe, httpContext.Session.Id is the empty string, it looks like the cookies of my page opened in the iframe (among which is the Session cookie), are not added to the http requests performed by the javascript code executed inside the iframe...
More generally, how do you identify a user if he's not authenticated? Is there anything in the HttpRequest that you can use as a unique id, like machine name or ip?
If you want to identify an anonymous user you could use a custom http header generated on frontend. It can be accessed with IHttpContextAccessor in combination with custom IUserIdProvider:
public class CustomUserIdProvider : IUserIdProvider
{
private readonly IHttpContextAccessor _httpContextAccessor;
public CustomUserIdProvider(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
public string GetUserId(HubConnectionContext connection)
{
if (connection.User.Identity.IsAuthenticated)
{
return connection.User.Identity.Name;
}
var username = _httpContextAccessor.HttpContext?.Request.Headers["username"];
if (username.HasValue && !StringValues.IsNullOrEmpty(username.Value))
{
return username.Value;
}
return Guid.NewGuid().ToString();
}
}
Remember that in .NET Core you need to explicitly add IHttpContextAccessor to the DI container:
services.AddHttpContextAccessor();
services.AddSingleton<IUserIdProvider, CustomUserIdProvider>();
services.AddSignalR();
Then you can use the generated identifier in hub method like this:
public override async Task OnConnectedAsync(HubConnectionContext connection)
{
await _wrappedHubLifetimeManager.OnConnectedAsync(connection);
_connections.Add(connection);
string userId = connection.UserIdentifier;
await _userTracker.AddUser(connection, new UserDetails(connection.ConnectionId, userId));
}
Source: https://dejanstojanovic.net/aspnet/2020/march/custom-signalr-hub-authorization-in-aspnet-core/
How can I send a message to the user without the user sending me a message? Like for example CNN bot is sending messages every day in the morning by itself. How can I do that in the bot framework?
See this.
In fact, you do not strictly need to receive a message from the user first, but addressing manually can be error-prone (you have to know the user's and bot's channel account, the service URL, etc.)
And in turn (per #thegaram's message), that only works for some channels. For example, Skype requires that the user contact the bot before the bot can message the user.
Once contacted, you can store the user's channelAccount data once they contact you and use that to send them proactive messages. For example if the user has subscribed to hear sports scores for a particular team over time.
Any sort of unsolicited spam messages of course are prohibited by the policies of the Bot Framework (and most of the channels).
Yes you can do that. We called it Greeting from Bot. I have done it and sharing a sample code with you.
Write this code in your messageController or first dialog used in bot.
if (activity.Text == null)
{
ConnectorClient connector = new ConnectorClient(new Uri(activity.ServiceUrl));
Activity isActivityTyping = activity.CreateReply();
isActivityTyping.Type = ActivityTypes.Typing;
await connector.Conversations.ReplyToActivityAsync(isActivityTyping);
await Conversation.SendAsync(activity, () => new Dialogs.GreetDialog());
}
after this code you need to create a dialog GreetDialog. Below is the cs file code for your reference.
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Connector;
namespace GPP.Bot.Dialogs
{
[Serializable]
internal class GreetDialog : IDialog<object>
{
public async Task StartAsync(IDialogContext context)
{
context.Wait(Greeting);
}
private async Task Greeting(IDialogContext context, IAwaitable<IMessageActivity> argument)
{
var message = await argument;
if (string.IsNullOrEmpty(message.Text))
{
// Hero Card
var cardMsg = context.MakeMessage();
var attachment = BotWelcomeCard("Hello, I am a bot. Right now I am on training and in a prototype state", "");
cardMsg.Attachments.Add(attachment);
await context.PostAsync(cardMsg);
context.Call<object>(new ActionDialog(), AfterGreetingDialogCompleted);
}
else
{
context.Call<object>(new ActionDialog(), AfterGreetingDialogCompleted);
}
}
private static Attachment BotWelcomeCard(string responseFromQNAMaker, string userQuery)
{
var heroCard = new HeroCard
{
Title = userQuery,
Subtitle = "",
Text = responseFromQNAMaker,
Images = new List<CardImage> { new CardImage("https://i2.wp.com/lawyerist.com/lawyerist/wp-content/uploads/2016/08/docubot.gif?fit=322%2C294&ssl=1") },
Buttons = new List<CardAction> { new CardAction(ActionTypes.ImBack, "Show Menu", value: "Show Bot Menu") }
};
return heroCard.ToAttachment();
}
private async Task AfterGreetingDialogCompleted(IDialogContext context, IAwaitable<object> result)
{
context.Done<object>(new object());
}
}
}
this is a working code. Do let me know in case you face ant issue.
~cheers :)
I have a Action that sends a simple email:
[HttpPost, ActionName("Index")]
public ActionResult IndexPost(ContactForm contactForm)
{
if (ModelState.IsValid)
{
new EmailService().SendAsync(contactForm.Email, contactForm.Name, contactForm.Subject, contactForm.Body, true);
return RedirectToAction(MVC.Contact.Success());
}
return View(contactForm);
}
And a email service:
public void SendAsync(string fromEmail, string fromName, string subject, string body, bool isBodyHtml)
{
MailMessage mailMessage....
....
SmtpClient client = new SmtpClient(settingRepository.SmtpAddress, settingRepository.SmtpPort);
client.EnableSsl = settingRepository.SmtpSsl;
client.Credentials = new NetworkCredential(settingRepository.SmtpUserName, settingRepository.SmtpPassword);
client.SendCompleted += client_SendCompleted;
client.SendAsync(mailMessage, Tuple.Create(client, mailMessage));
}
private void client_SendCompleted(object sender, System.ComponentModel.AsyncCompletedEventArgs e)
{
Tuple<SmtpClient, MailMessage> data = (Tuple<SmtpClient, MailMessage>)e.UserState;
data.Item1.Dispose();
data.Item2.Dispose();
if (e.Error != null)
{
}
}
When I send a email, I am using Async method, then my method SendAsync return immediately, then RedirectToAction is called. But the response(in this case a redirect) isn´t sent by ASP.NET until client_SendCompleted is completed.
Here's what I'm trying to understand:
When watching the execution in Visual Studio debugger, the SendAsync returns immediately (and RedirectToAction is called), but nothing happens in the browser until email is sent?
If i put a breakpoint inside client_SendCompleted, the client stay at loading.... until I hit F5 at debugger.
This is by design. ASP.NET will automatically wait for any outstanding async work to finish before finishing the request if the async work was kicked off in a way that calls into the underlying SynchronizationContext. This is to ensure that if your async operation tries to interact with the HttpContext, HttpResponse, etc. it will still be around.
If you want to do true fire & forget, you need to wrap your call in ThreadPool.QueueUserWorkItem. This will force it to run on a new thread pool thread without going through the SynchronizationContext, so the request will then happily return.
Note however, that if for any reason the app domain were to go down while your send was still in progress (e.g. if you changed the web.config file, dropped a new file into bin, the app pool recycled, etc.) your async send would be abruptly interrupted. If you care about that, take a look at Phil Haacks WebBackgrounder for ASP.NET, which let's you queue and run background work (like sending an email) in such a way that will ensure it gracefully finishes in the case the app domain shuts down.
This is an interesting one. I've reproduced the unexpected behaviour, but I can't explain it. I'll keep digging.
Anyway the solution seems to be to queue a background thread, which kind of defeats the purpose in using SendAsync. You end up with this:
MailMessage mailMessage = new MailMessage(...);
SmtpClient client = new SmtpClient(...);
client.SendCompleted += (s, e) =>
{
client.Dispose();
mailMessage.Dispose();
};
ThreadPool.QueueUserWorkItem(o =>
client.SendAsync(mailMessage, Tuple.Create(client, mailMessage)));
Which may as well become:
ThreadPool.QueueUserWorkItem(o => {
using (SmtpClient client = new SmtpClient(...))
{
using (MailMessage mailMessage = new MailMessage(...))
{
client.Send(mailMessage, Tuple.Create(client, mailMessage));
}
}
});
With .Net 4.5.2, you can do this with ActionMailer.Net:
var mailer = new MailController();
var msg = mailer.SomeMailAction(recipient);
var tcs = new TaskCompletionSource<MailMessage>();
mailer.OnMailSentCallback = tcs.SetResult;
HostingEnvironment.QueueBackgroundWorkItem(async ct =>
{
msg.DeliverAsync();
await tcs.Task;
Trace.TraceInformation("Mail sent to " + recipient);
});
Please read this first: http://www.hanselman.com/blog/HowToRunBackgroundTasksInASPNET.aspx
I sent the bug to Microsoft Connect https://connect.microsoft.com/VisualStudio/feedback/details/688210/smtpclient-sendasync-blocking-my-asp-net-mvc-request