-
Notifications
You must be signed in to change notification settings - Fork 235
Description
Context
Currently, adding proactive features to an agent requires detailed steps to send a message, or create a conversation, proactively. This is currently done through Adapter.CreateConversation and Adapter.ContinueConversation.
For continue conversation (sending a messages to an existing conversation), it requires that the developer manually create a ConversationReference and provide the correct claims. It also requires the dev to have the knowledge about channel specific requirements, serviceUrls, and required claims.
The better route would be to save the TurnContext.Identity and the Activity.GetConversationReference as a pair. Then continuing that conversation would just require retrieving that pair from storage and calling Adapter.ContinueConversation.
But we can do better since this still requires additional work on the developer's part.
Proposal
- Add a feature class off AgentApplication (similar to the AgentApplication.AdaptiveCards)
- This would include
- An option to persist ConversationReference and Identity automatically
- Provide logic to support expiring stale references
- Helper methods to create a ConversationReference (Teams & WebChat).
- Methods to get/save/delete references
- Methods
AgentApplication.Proactive.SendActivityAsync(string conversationId, IActivity activity)- This would require references to have been stored, keyed by conversationId
AgentApplication.Proactive.SendActivityAsync(ClaimsIdentity identity, ConversationReference ref, IActivity activity)- This method could be used if Identity and ConversationReference were provided by other means (see below)
AgentApplication.Proactive.ContinueConversation(ClaimsIdentity identity, ConversationReference ref, TurnEventHandler handler)- Allows for more advanced proactive handling, including access to state
AgentApplication.Proactive.CreateConversationAsync- TBD
- An option to persist ConversationReference and Identity automatically
- Service Extensions that add some Http endpoints to externally trigger proactive (like this sample does)
/api/sendactivity(conversationId, [Activity])/api/sendtorefeence(claims, conversationReference, [Activity])
Scenario: External job
- Agent sends a request to an external service, including the TurnContext.Identity.Claims, and either the ConversationReference or the entire Activity.
- External service will make a request to
/api/sendtorefeence, with the Claims, ConversationReference, and an Activity. - The Http endpoint logic could call
AgentApplication.Proactive.SendActivityAsync(claims, references, activity) - The benefit of this scenario is no storage of the references is required (agent side)
Other
- This would likely require better use of ETags in storage to handle concurrency.
- Using conversationId alone assume they are unique across channels. If we can't be certain of that, then we'd need to include channelId in the arguments and storage keys.
- This is partly inspired by Proactive messaging sample using Agent SDK (.NET) #297 and credit is due.
- This would likely need to be expanded to return the list of activityId's that ConnectorClient returns. Such that an external service could later reference those Activities (think UpdateActivity at a later date).
Some code
Example endpoint
app.MapPost("/api/sendactivity", async (HttpRequest request, HttpResponse response, MyAgent agent, CancellationToken cancellationToken) =>
{
var sendRequest = await HttpHelper.ReadRequestAsync<SendActivityRequest>(request);
if (sendRequest == null || string.IsNullOrEmpty(sendRequest.ConversationId))
{
return Results.BadRequest(new
{
status = "Error",
error = new { code = "Validation", message = "An invalid request body was received." }
});
}
var result = await agent.Proactive.SendActivityAsync(sendRequest.ConversationId, sendRequest.Activity, cancellationToken);
return Results.Ok(new
{
conversationId = sendRequest.ConversationId,
status = result ? "Delivered" : "Failed"
});
});Example Proactive impl
class Proactive
{
public Proactive(AgentApplication app)
{
_app = app;
}
public async Task<bool> SendActivityAsync(ClaimsIdentity identity, string conversationId, IActivity activity, CancellationToken cancellationToken)
{
// Retrieve ConversationReference
var key = ConversationReferenceRecord.GetKey(conversationId);
var items = await _storage.ReadAsync([key], cancellationToken);
if (items == null || items.Count == 0)
{
return false;
}
return SendActivityAsync(identity, (ConversationReference) items[key], activity, cancellationToken);
}
public async Task<bool> SendActivityAsync(ClaimsIdentity identity, ConversationReference reference, IActivity activity, CancellationToken cancellationToken)
{
await _app.Options.Adapter!.ContinueConversationAsync(
identity,
reference,
async (ITurnContext turnContext, CancellationToken ct) =>
{
await turnContext.SendActivityAsync(activity, ct);
},
cancellationToken);
return true;
}
public Task ContinueConversationAsync(ClaimsIdentity identity, ConversationReference reference, TurnEventHandler handler, CancellationToken cancellationToken)
{
await _app.Options.Adapter!.ContinueConversationAsync(
identity,
reference,
async (ITurnContext turnContext, CancellationToken ct) =>
{
var turnState = _app.Options.TurnStateFactory();
await turnState.LoadStateAsync(turnContext, cancellationToken: ct);
await handler(turnContext, turnState, ct);
await turnState.SaveStateAsync(turnContext, cancellationToken: ct);
},
cancellationToken);
}
}