Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 42 additions & 3 deletions dotnet/agent-framework/sample-agent/Agent/MyAgent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,11 @@ await AgentMetrics.InvokeObservedAgentOperation(
/// <returns></returns>
protected async Task OnMessageAsync(ITurnContext turnContext, ITurnState turnState, CancellationToken cancellationToken)
{
if (turnContext is null)
{
throw new ArgumentNullException(nameof(turnContext));
}

// Log the user identity from Activity.From — set by the A365 platform on every message.
var fromAccount = turnContext.Activity.From;
_logger?.LogDebug(
Expand All @@ -208,7 +213,6 @@ protected async Task OnMessageAsync(ITurnContext turnContext, ITurnState turnSta
ObservabilityAuthHandlerName = ToolAuthHandlerName = OboAuthHandlerName;
}


await A365OtelWrapper.InvokeObservedAgentOperation(
"MessageProcessor",
turnContext,
Expand All @@ -219,7 +223,33 @@ await A365OtelWrapper.InvokeObservedAgentOperation(
_logger,
async () =>
{
// Start a Streaming Process to let clients that support streaming know that we are processing the request.
// Send an immediate acknowledgment — this arrives as a separate message before the LLM response.
// Each SendActivityAsync call produces a discrete Teams message, enabling the multiple-messages pattern.
// NOTE: For Teams agentic identities, streaming is buffered into a single message by the SDK;
// use SendActivityAsync for any messages that must arrive immediately.
await turnContext.SendActivityAsync(MessageFactory.Text("Got it — working on it…"), cancellationToken).ConfigureAwait(false);
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed
Comment thread
sellakumaran marked this conversation as resolved.
Dismissed

// Send typing indicator immediately on the main thread (awaited so it arrives before the LLM call starts).
await turnContext.SendActivityAsync(Activity.CreateTypingActivity(), cancellationToken).ConfigureAwait(false);

// Background loop refreshes the "..." animation every ~4s (it times out after ~5s).
// Only visible in 1:1 and small group chats.
using var typingCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
var typingTask = Task.Run(async () =>
{
try
{
while (!typingCts.IsCancellationRequested)
{
await Task.Delay(TimeSpan.FromSeconds(4), typingCts.Token).ConfigureAwait(false);
await turnContext.SendActivityAsync(Activity.CreateTypingActivity(), typingCts.Token).ConfigureAwait(false);
}
}
catch (OperationCanceledException) { /* expected on cancel */ }
}, typingCts.Token);

// StreamingResponse is best-effort: in Teams with agentic identity the SDK may buffer/downscale it.
// The ack + typing loop above handle the immediate UX; streaming remains for non-Teams / WebChat clients.
await turnContext.StreamingResponse.QueueInformativeUpdateAsync("Just a moment please..").ConfigureAwait(false);
try
{
Expand Down Expand Up @@ -252,7 +282,16 @@ await A365OtelWrapper.InvokeObservedAgentOperation(
}
finally
{
await turnContext.StreamingResponse.EndStreamAsync(cancellationToken).ConfigureAwait(false); // End the streaming response
typingCts.Cancel();
try
{
await typingTask.ConfigureAwait(false);
}
catch (OperationCanceledException)
{
// Expected: typingTask is canceled when typingCts is canceled; no further action required.
}
await turnContext.StreamingResponse.EndStreamAsync(cancellationToken).ConfigureAwait(false);
}
});
}
Expand Down
50 changes: 50 additions & 0 deletions dotnet/agent-framework/sample-agent/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,56 @@ The handler is registered twice in the constructor — once for agentic (A365 pr

To test with Agents Playground, use **Mock an Activity → Install application** to send a simulated `installationUpdate` activity.

## Sending Multiple Messages in Teams

Agent365 agents can send multiple discrete messages in response to a single user prompt in Teams. This is achieved by calling `SendActivityAsync` multiple times within a single turn.

> **Important**: Streaming responses are not supported for agentic identities in Teams. The SDK detects agentic identity and buffers the stream into a single message. Use `SendActivityAsync` directly to send immediate, discrete messages to the user.

The sample demonstrates this in `OnMessageAsync` ([MyAgent.cs](Agent/MyAgent.cs)) by sending an immediate acknowledgment before the LLM response:

```csharp
// Message 1: immediate ack — reaches the user right away
await turnContext.SendActivityAsync(MessageFactory.Text("Got it — working on it…"), cancellationToken);

// ... LLM processing ...

// Message 2: the LLM response (via StreamingResponse, buffered into one message for Teams agentic)
await turnContext.StreamingResponse.EndStreamAsync(cancellationToken);
```

Each `SendActivityAsync` call produces a separate Teams message. You can call it as many times as needed to send progress updates, partial results, or a final answer.

### Typing Indicators

For long-running operations, send a typing indicator to show a "..." progress animation in Teams:

```csharp
// Typing indicator loop — refreshes every ~4s for long-running operations.
using var typingCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
var typingTask = Task.Run(async () =>
{
try
{
while (!typingCts.IsCancellationRequested)
{
await turnContext.SendActivityAsync(Activity.CreateTypingActivity(), typingCts.Token);
await Task.Delay(TimeSpan.FromSeconds(4), typingCts.Token);
}
}
catch (OperationCanceledException) { /* expected on cancel */ }
}, typingCts.Token);

try { /* ... do work ... */ }
finally
{
typingCts.Cancel();
try { await typingTask; } catch (OperationCanceledException) { }
}
```

> **Note**: Typing indicators are only visible in 1:1 chats and small group chats — not in channels.

## Running the Agent

To set up and test this agent, refer to the [Configure Agent Testing](https://learn.microsoft.com/en-us/microsoft-agent-365/developer/testing?tabs=dotnet) guide for complete instructions.
Expand Down
Loading
Loading