Skip to content

Proactive AgentApplication #336

@tracyboehrer

Description

@tracyboehrer

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
    • Methods to get/save/delete references (if auto save isn't on)
    • Method to send activities
      • AgentApplication.Proactive.SendActivities(string conversationId, IActivity[] activities)
        • This would require references to have been stored, keyed by conversationId
        • This method would retrieve the references, and internally call Adapter.ContinueConversation
      • AgentApplication.Proactive.SendToRefence(ClaimsIdentity identity, ConversationReference ref, IActivity[] activities)
        • This method could be used if Identity and ConversationReference were provided by other means (see below)
  • 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 a list of Activity.
  • The Http endpoint logic could call AgentApplication.Proactive.SendToRefence(claims, references, activities)
  • 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, EchoSkill agent, CancellationToken cancellationToken) =>
{
    var sendRequest = await HttpHelper.ReadRequestAsync<EchoSkill.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.SendActivityToConversationAsync(sendRequest.ConversationId, sendRequest.Activity, cancellationToken);
    return Results.Ok(new
    {
        conversationId = sendRequest.ConversationId,
        status = result ? "Delivered" : "Failed"
    });
});

Example Proactive send

public async Task<bool> SendActivityToConversationAsync(string conversationId, IActivity activity, CancellationToken cancellationToken)
{
    if (string.IsNullOrEmpty(conversationId))
    {
        return false;
    }

    // This would be replaced by using methods on AgentApplication.Proactive.
    var key = ConversationReferenceRecord.GetKey(conversationId);
    var items = await _storage.ReadAsync([key], cancellationToken);
    if (items == null ||  items.Count == 0)
    {
        return false;
    }
    var conversationReferenceRecord = (ConversationReferenceRecord) items[key];

    await Options.Adapter!.ContinueConversationAsync(
        conversationReferenceRecord.Identity,
        conversationReferenceRecord.Reference!,
        async (ITurnContext turnContext, CancellationToken ct) =>
        {
            await turnContext.SendActivityAsync(activity, ct);
        },
        cancellationToken);

    return true;
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions