エージェント 365 SDK を使用すると、エージェントはインストールやアンインストールなどのプラットフォーム アクティビティ イベントを処理し、1 ターン内に複数の個別のメッセージを送信できます。 この記事では、エージェントが要求を処理している間にユーザーに応答し、それらを常に通知するための主要なパターンについて説明します。
エージェントのインストールイベントとアンインストールイベントを処理する
ユーザーが Teams またはその他のエージェント 365 ホストチャネルでエージェントをインストールまたはアンインストールすると、プラットフォームは InstallationUpdate アクティビティ ( agentInstanceCreated イベントとも呼ばれます) を送信します。 エージェントは、これらのイベントを処理して、インストール時にウェルカム メッセージを送信し、アンインストール時に別れのメッセージを送信できます。
| アクション |
Description |
add |
ユーザーがエージェントをインストールする |
remove |
ユーザーがエージェントをアンインストールする |
通知ハンドラーとは異なり、ユーザーがアクティブなセッションを持つ前または後にインストールまたはアンインストール イベントが発生するため、 InstallationUpdate ハンドラーは認証を必要としません。
インストールおよびアンインストール ハンドラーを登録する
エージェントの初期化で、 InstallationUpdate アクティビティの種類のアクティビティ ハンドラーを登録します。
@agent_app.activity("installationUpdate")
async def on_installation_update(context: TurnContext, state: TurnState):
action = context.activity.action
from_prop = context.activity.from_property
logger.info(
"InstallationUpdate received — Action: '%s', DisplayName: '%s', UserId: '%s'",
action or "(none)",
getattr(from_prop, "name", "(unknown)") if from_prop else "(unknown)",
getattr(from_prop, "id", "(unknown)") if from_prop else "(unknown)",
)
if action == "add":
await context.send_activity("Thank you for hiring me! Looking forward to assisting you in your professional journey!")
elif action == "remove":
await context.send_activity("Thank you for your time, I enjoyed working with you.")
Activity.action は、エージェントのインストール時に "add" するか、アンインストール時に "remove" するように設定された文字列です。
Activity.from_property は、ユーザーの ID を含む ChannelAccount インスタンスです。
// In your agent class constructor:
this.onActivity(ActivityTypes.InstallationUpdate, async (context: TurnContext, state: TurnState) => {
await this.handleInstallationUpdateActivity(context, state);
});
// Handler method:
async handleInstallationUpdateActivity(context: TurnContext, state: TurnState): Promise<void> {
const from = context.activity?.from;
console.log(`InstallationUpdate received — Action: '${context.activity.action ?? "(none)"}', DisplayName: '${from?.name ?? "(unknown)"}', UserId: '${from?.id ?? "(unknown)"}'`);
if (context.activity.action === 'add') {
await context.sendActivity('Thank you for hiring me! Looking forward to assisting you in your professional journey!');
} else if (context.activity.action === 'remove') {
await context.sendActivity('Thank you for your time, I enjoyed working with you.');
}
}
ActivityTypes は、 @microsoft/agents-activityからインポートされたアクティビティ型定数の列挙型です。
Activity.action は、エージェントのインストール時に 'add' するか、アンインストール時に 'remove' するように設定された文字列です。
// In your agent class constructor:
OnActivity(ActivityTypes.InstallationUpdate, OnInstallationUpdateAsync, isAgenticOnly: true, autoSignInHandlers: agenticInstallHandlers);
OnActivity(ActivityTypes.InstallationUpdate, OnInstallationUpdateAsync, isAgenticOnly: false);
// Handler method:
protected async Task OnInstallationUpdateAsync(ITurnContext turnContext, ITurnState turnState, CancellationToken cancellationToken)
{
_logger?.LogInformation(
"InstallationUpdate received — Action: '{Action}', DisplayName: '{Name}', UserId: '{Id}'",
turnContext.Activity.Action ?? "(none)",
turnContext.Activity.From?.Name ?? "(unknown)",
turnContext.Activity.From?.Id ?? "(unknown)");
if (turnContext.Activity.Action == InstallationUpdateActionTypes.Add)
{
await turnContext.SendActivityAsync(MessageFactory.Text("Thank you for hiring me! Looking forward to assisting you in your professional journey!"), cancellationToken);
}
else if (turnContext.Activity.Action == InstallationUpdateActionTypes.Remove)
{
await turnContext.SendActivityAsync(MessageFactory.Text("Thank you for your time, I enjoyed working with you."), cancellationToken);
}
}
ActivityTypes は、アクティビティ型定数の列挙型です。
InstallationUpdateActionTypes には、アクティビティ アクションを比較するための Add 定数と Remove 定数が用意されています。
Note
.NET の場合は、ハンドラーを 2 回登録します。1 回は、運用エージェント 365 トラフィックの isAgenticOnly: true とオプションのエージェント認証ハンドラー、1 回は Agents Playground または WebChat を使用したローカル テスト用の isAgenticOnly: false です。
複数のメッセージを送信する
エージェント 365 エージェントは、1 つのユーザー プロンプトに応答して複数の個別メッセージを送信できます。 これを行うには、 SendActivityAsync (.NET)、 send_activity (Python)、または sendActivity (JavaScript) を 1 ターン内で複数回呼び出します。
Important
Teams では、エージェント ID のストリーミング応答はサポートされていません。 SDK はエージェント ID を検出し、ストリームを 1 つのメッセージにバッファーします。
SendActivityAsync、send_activity、またはsendActivityを直接使用して、ユーザーに直接個別のメッセージを送信します。
次の例は、LLM 応答の前に即時受信確認を送信することでパターンを示しています。
@agent_app.activity("message")
async def on_message(context: TurnContext, state: TurnState):
# Message 1: immediate ack — reaches the user right away
await context.send_activity("Got it — working on it…")
# ... LLM processing ...
# Message 2: the LLM response
await context.send_activity(response)
このサンプルでは、LLM 応答の前に即時受信確認を送信することで、on_message (host_agent_server.py) でこのパターンを示します。
// Message 1: immediate ack — reaches the user right away
await context.sendActivity('Got it — working on it…');
// ... LLM processing ...
// Message 2: the LLM response
await context.sendActivity(modelResponse);
このサンプルは、メッセージ アクティビティ ハンドラー (agent.ts) でこのパターンを示しています。
// 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);
このサンプルは、 OnMessageAsync (MyAgent.cs) でこのパターンを示しています。
sendActivity、send_activity、またはSendActivityAsyncを呼び出すたびに、個別のメッセージが作成されます。 進行状況の更新、部分的な結果、または最終的な回答を送信するために必要な回数呼び出すことができます。
入力インジケーター
Teams では、入力インジケーターが ... の進行状況アニメーションとして表示されます。
- 組み込みのビジュアル タイムアウトは約 5 秒で、4 秒ごとにループで更新する必要があります。
- チャネルではなく、1 対 1 のチャットと小さなグループ チャットにのみ表示されます。
エージェントは、LLM が要求を処理している間、 ... アニメーションを維持するために、4 秒ごとにループで入力インジケーターを送信します。
# Message 1: immediate ack — reaches the user right away
await context.send_activity("Got it — working on it…")
# Send typing indicator immediately (awaited so it arrives before the LLM call starts).
await context.send_activity(Activity(type="typing"))
# Background loop refreshes the "..." animation every ~4s (it times out after ~5s).
async def _typing_loop():
try:
while True:
await asyncio.sleep(4)
await context.send_activity(Activity(type="typing"))
except asyncio.CancelledError:
pass # Expected on cancel.
typing_task = asyncio.create_task(_typing_loop())
try:
response = await agent.process_user_message(...)
await context.send_activity(response)
finally:
typing_task.cancel()
try:
await typing_task
except asyncio.CancelledError:
pass
let typingInterval: ReturnType<typeof setInterval> | undefined;
const startTypingLoop = () => {
typingInterval = setInterval(async () => {
await context.sendActivity(Activity.fromObject({ type: ActivityTypes.Typing }));
}, 4000);
};
const stopTypingLoop = () => { clearInterval(typingInterval); };
startTypingLoop();
try {
// ... LLM processing ...
} finally {
stopTypingLoop();
}
// 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 Task.Delay(TimeSpan.FromSeconds(4), typingCts.Token);
await turnContext.SendActivityAsync(Activity.CreateTypingActivity(), typingCts.Token);
}
}
catch (OperationCanceledException) { /* expected on cancel */ }
}, typingCts.Token);
try { /* ... do work ... */ }
finally
{
typingCts.Cancel();
try { await typingTask; } catch (OperationCanceledException) { }
}