Skip to content
Open
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
42 changes: 42 additions & 0 deletions docs/multiple-messages-tracking.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Multiple Messages Per Prompt — Issue #268 Tracking

Tracking implementation of the multiple-messages pattern across all Agent365-Samples.

**Pattern**: Send an immediate ack message → typing indicator loop → LLM response, all as separate discrete Teams messages.

> **Reference**: [GitHub Issue #268](https://github.com/microsoft/Agent365-devTools/issues/268)

---

## Progress

| Sample | Implementation File | Code | README | Committed | Tested (Agents Playground) |
|---|---|---|---|---|---|
| `dotnet/agent-framework` | `Agent/MyAgent.cs` | ✅ | ✅ | ✅ | ✅ |
| `dotnet/semantic-kernel` | `Agents/MyAgent.cs` | 🔧 | 🔧 | ⏳ | ✅ |
| `python/agent-framework` | `host_agent_server.py` | 🔧 | 🔧 | ⏳ | ❌ Blocked — `agent_framework` SDK API break (`ChatAgent` removed); pre-existing env issue |
| `python/openai` | `host_agent_server.py` | 🔧 | 🔧 | ⏳ | ⏳ |
| `python/claude` | `host_agent_server.py` | 🔧 | 🔧 | ⏳ | ⏳ |
| `python/crewai` | `host_agent_server.py` | 🔧 | 🔧 | ⏳ | ⏳ |
| `python/google-adk` | `hosting.py` | 🔧 | 🔧 | ⏳ | ⏳ |
| `nodejs/openai` | `src/agent.ts` | 🔧 | 🔧 | ⏳ | ✅ |
| `nodejs/claude` | `src/agent.ts` | 🔧 | 🔧 | ⏳ | ⏳ Needs ANTHROPIC_API_KEY |
| `nodejs/langchain` | `src/agent.ts` | 🔧 | 🔧 | ⏳ | ⏳ |
| `nodejs/langchain/quickstart-before` | `src/agent.ts` | 🔧 | 🔧 | ⏳ | ✅ |
| `nodejs/vercel-sdk` | `src/agent.ts` | 🔧 | 🔧 | ⏳ | ⏳ Needs ANTHROPIC_API_KEY |
| `nodejs/devin` | `src/agent.ts` | 🔧 | 🔧 | ⏳ | ⏳ Needs Devin credentials |
| `nodejs/perplexity` | `src/agent.ts` | 🔧 | 🔧 | ⏳ | ⏳ Needs PERPLEXITY_API_KEY |
| `nodejs/copilot-studio` | `src/agent.ts` | 🔧 | 🔧 | ⏳ | ⏳ |

**Legend**: ✅ Done · 🔧 Modified locally, not committed · ⏳ Pending

---

## Notes

- `python/google-adk` uses a different hosting pattern — `MyAgent(AgentApplication)` class in `hosting.py` rather than `host_agent_server.py`. Pattern applied to `message_handler` directly.
- `nodejs/copilot-studio` was missing the `InstallationUpdate` handler in the constructor — added as part of this work.
- `nodejs/langchain/quickstart-before` is a pre-refactor snapshot — fixed pre-existing TypeScript errors (`instructions` → `systemPrompt` for langchain 1.2.32+, added `@types/express`/`@types/node`).
- Node.js samples use a manual `setInterval` typing loop (~4s) even though `startTypingTimer: true` is set in the constructor. The manual loop is necessary for long-running LLM calls that exceed the ~5s typing indicator timeout.
- Python samples use `asyncio.create_task` for the typing loop since all aiohttp handlers run on the same event loop.
- C# (`dotnet/semantic-kernel`) uses a single typing indicator (no loop) sent before agent initialization; the streaming informative update takes over as the progress indicator once the LLM call starts. Unlike agent-framework which runs a full `Task.Run` typing loop alongside streaming.
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);

// 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