Agent 365 SDK를 사용하여, 에이전트는 설치 및 제거와 같은 플랫폼 활동 이벤트를 처리하고 한 턴 내에 여러 개의 개별 메시지를 보낼 수 있습니다. 이 문서에서는 에이전트가 요청을 처리하는 동안 사용자에게 답변하고 정보를 제공하는 주요 패턴을 설명합니다.
에이전트 설치 및 제거 이벤트 처리
사용자가 Teams 또는 기타 Agent 365가 호스팅하는 채널에서 에이전트를 설치하거나 제거하면, 플랫폼은 InstallationUpdate 활동(또는 agentInstanceCreated 이벤트라고도 함)을 전송합니다. 에이전트는 설치 시 환영 메시지, 제거 시 작별 메시지를 보내도록 이러한 이벤트를 처리할 수 있습니다.
| 액션 |
설명 |
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는 사용자의 신원을 포함하는 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 상수를 활동 액션을 비교하기 위해 제공합니다.
참고
.NET의 경우, 핸들러를 두 번 등록해야 합니다 — 한 번은 isAgenticOnly: true로 프로덕션 Agent 365 트래픽(선택적 agentic 인증 핸들러 포함)에, 또 한 번은 isAgenticOnly: false로 에이전트 플레이그라운드 또는 WebChat을 이용한 로컬 테스트에 등록합니다.
여러 개의 개별 메시지를 보내세요
Agent 365 에이전트는 단일 사용자 프롬프트에 응답하여 여러 개의 개별 메시지를 보낼 수 있습니다. 이를 위해 한 턴 내에서 SendActivityAsync (.NET), send_activity (Python), 또는 sendActivity (JavaScript)를 여러 번 호출하세요.
중요
Teams는 에이전트 정체성에 대한 스트리밍 응답을 지원하지 않습니다. SDK는 에이전트 정체성(agentic identity)을 감지하고 스트림을 단일 메시지로 버퍼링합니다.
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)
샘플은 on_message (host_agent_server.py)에서 LLM 응답 전에 즉시 확인 메시지를 보내는 패턴을 보여줍니다.
// 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) { }
}