本記事記述および実装内容は、多くの部分をAIで処理しており、人間チェックは甘いです
Foundry AgentをM365 SDKでTeams連携
はじめに
Microsoft 365 Agents SDK(Python)で カスタムエンジンエージェント を作り、App Service にホストして、ユーザー発話を Azure AI Foundry の Agent(Responses API) に転送する構成を、IaC(Bicep + azd)で一気通貫に構築した記録です。
- 想定読者: Azure / Microsoft 365 でエージェントを作りたいエンジニア
-
読むと分かること:
- M365 Agents SDK → App Service → Foundry Agent の最小構成と IaC
- Teams / M365 Copilot へ出すためのマニフェスト(
copilotAgents.customEngineAgents) - 実運用で踏んだ 3つのハマり と解決
結論
- ✅ M365 Agents SDK の
/api/messagesループに、Foundry のconversations+responses(agent_reference)を挿すだけで、Teams/Copilot から使える対話エージェントになる - ✅ Bot 用の Entra ID アプリは App Registration だけでなく Service Principal も必須(
az ad sp create)。これを忘れると無反応(502)になる - ✅ 会話状態は
state.conversation.get_value/set_value()で読み書きする(直感的な dict アクセスは例外) - ⚠️ サブスクリプションによっては App Service の VM クォータが 0 のリージョンがある。クォータのあるリージョンへ退避すれば回避できる
アーキテクチャ
-
Azure Bot Service: Teams / M365 Copilot チャネルを終端し
/api/messagesに転送 - App Service(Linux, Python): SDK のメッセージループ。inbound JWT を検証し Foundry を呼ぶ
-
Azure AI Foundry: プロジェクト +
gpt-4o-miniデプロイ。Prompt Agent を初回メッセージ時に冪等作成 - 認証: App Service の User-assigned Managed Identity → Foundry。Bot の inbound は Entra ID アプリ(ClientSecret)
環境
| 項目 | バージョン |
|---|---|
| Azure CLI | 2.81 |
| azd | 1.25 |
| Bicep | 0.41 |
| Python | 3.12 |
| microsoft-agents-* | hosting-aiohttp / hosting-core / authentication-msal / activity |
| azure-ai-projects | 2.2.0 |
Python の構成
src/ 配下はフラット構成で、App Service は python main.py で起動します(aiohttp を 0.0.0.0:$PORT で待ち受け)。
src/
├── main.py # エントリポイント。ロギング設定 → サーバ起動
├── start_server.py # aiohttp アプリ。/api/messages ルート + ヘルスチェック
├── agent.py # M365 Agents SDK 本体。メッセージループと状態管理
├── foundry_agent.py # Foundry Agent の作成・呼び出し (Responses API)
├── requirements.txt # 依存 (microsoft-agents-* / azure-ai-projects ほか)
├── startup.sh # App Service 起動コマンド
└── env.TEMPLATE # ローカル開発用の環境変数雛形
| ファイル | 役割 | ポイント |
|---|---|---|
main.py |
起動 |
microsoft_agents の logger を設定してから start_server を呼ぶ |
start_server.py |
HTTP 層 |
jwt_authorization_middleware で inbound JWT を検証。run_app(host="0.0.0.0", port=$PORT)
|
agent.py |
会話制御 |
@AGENT_APP.activity("message") で受信 → Foundry に転送 → 応答。会話状態を ConversationState に保持 |
foundry_agent.py |
Foundry 連携 |
DefaultAzureCredential(App Service の Managed Identity)で接続し Agent を冪等作成・呼び出し |
依存は最小限です。
python-dotenv
aiohttp
microsoft-agents-hosting-aiohttp
microsoft-agents-hosting-core
microsoft-agents-authentication-msal
microsoft-agents-activity
azure-ai-projects>=2.0.0
azure-identity
起動コマンドは Oryx に依存インストールを任せ、python main.py を実行するだけです。
#!/bin/bash
# App Service (Oryx) startup command. requirements は Oryx が自動インストールする。
python main.py
実装のキモ
アプリ本体(メッセージループ + Foundry 連携)
M365 Agents SDK の @AGENT_APP.activity("message") で受けて、Foundry に転送します。Foundry SDK は同期 I/O なので、aiohttp のイベントループをブロックしないよう asyncio.to_thread に逃がすのがポイントです。
@AGENT_APP.activity("message")
async def on_message(context: TurnContext, state: TurnState):
user_text = context.activity.text or ""
# 会話ごとに Foundry の conversation_id を ConversationState に保持して継続する
conversation_id = state.conversation.get_value("foundry_conversation_id")
try:
def _call() -> tuple[str, str]:
return _get_foundry_client().ask(user_text, conversation_id)
# 同期 SDK は別スレッドで実行してイベントループを塞がない
reply, new_conversation_id = await asyncio.to_thread(_call)
state.conversation.set_value("foundry_conversation_id", new_conversation_id)
await context.send_activity(reply)
except Exception as error: # noqa: BLE001
await context.send_activity(f"Foundry Agent の呼び出しに失敗しました: {error}")
Foundry Agent の作成と呼び出し(Responses API)
旧 beta.threads API は廃止されているため、conversations + responses を使います。agent_reference で persisted な Agent を参照します。
from azure.ai.projects import AIProjectClient
from azure.ai.projects.models import PromptAgentDefinition
from azure.identity import DefaultAzureCredential
project = AIProjectClient(endpoint=FOUNDRY_PROJECT_ENDPOINT, credential=DefaultAzureCredential())
# Agent を冪等作成(バージョン発行)
agent = project.agents.create_version(
agent_name=AGENT_NAME,
definition=PromptAgentDefinition(model=MODEL_DEPLOYMENT_NAME, instructions=INSTRUCTIONS),
)
# 会話 + 応答(マルチターンは conversation を再利用)
openai = project.get_openai_client()
conversation = openai.conversations.create()
response = openai.responses.create(
conversation=conversation.id,
extra_body={"agent_reference": {"name": agent.name, "type": "agent_reference"}},
input=user_message,
)
print(response.output_text)
IaC(Bicep)の構成
monitoring / identity / ai-foundry / app-service / bot-service / role-assignments をモジュール分割しています。Bot は Teams と M365 Copilot 用に 2 チャネル有効化します。
resource bot 'Microsoft.BotService/botServices@2022-09-15' = {
name: botName
location: 'global'
sku: { name: 'F0' }
kind: 'azurebot'
properties: {
endpoint: messagingEndpoint // https://<app>/api/messages
msaAppId: botAppId
msaAppType: 'SingleTenant'
msaAppTenantId: botAppTenantId
}
}
resource teamsChannel 'Microsoft.BotService/botServices/channels@2022-09-15' = {
parent: bot
name: 'MsTeamsChannel'
location: 'global'
properties: { channelName: 'MsTeamsChannel', properties: { isEnabled: true } }
}
resource m365Channel 'Microsoft.BotService/botServices/channels@2022-09-15' = {
parent: bot
name: 'M365Extensions'
location: 'global'
properties: { channelName: 'M365Extensions' }
}
Teams / M365 Copilot マニフェスト
カスタムエンジンエージェントは manifestVersion 1.21 以上、bots[].scopes に copilot、copilotAgents.customEngineAgents が必須です。
{
"$schema": "https://developer.microsoft.com/json-schemas/teams/v1.21/MicrosoftTeams.schema.json",
"manifestVersion": "1.21",
"bots": [
{ "botId": "<bot-app-id>", "scopes": ["personal", "team", "groupChat", "copilot"] }
],
"copilotAgents": {
"customEngineAgents": [ { "type": "bot", "id": "<bot-app-id>" } ]
}
}
Teams での設定手順
デプロイで作った appPackage/appPackage.zip(manifest.json + アイコン)を Teams アプリとして配布します。マニフェストに copilot スコープと customEngineAgents を入れてあるので、Teams に入れれば M365 Copilot 側にも自動的に現れます。
事前にテナントで カスタムアプリのアップロード(サイドロード) が許可されている必要があります(Teams 管理センターのセットアップポリシーで有効化)。
A. 自分だけで試す(サイドロード)
- Teams を開く → 左メニュー アプリ
- 下部の アプリを管理 → アプリをアップロード
-
カスタムアプリをアップロード →
appPackage/appPackage.zipを選択 → 追加 - M365 Copilot(Teams か microsoft365.com)を開く → 右側のエージェント一覧に追加したエージェントが表示 → 選んで会話
B. 組織に発行(管理者向け)
-
Teams 管理センター(
admin.teams.microsoft.com)→ Teams アプリ → アプリを管理 - 右上 アップロード →
appPackage.zipをアップロード(カタログに登録) - 対象アプリを開き、公開状態を許可 → 必要なら アプリのセットアップポリシー で配布対象ユーザーを指定
Bot の Messaging endpoint(https://<app>/api/messages)は Bicep で App Service のホスト名から設定済みです。Bot 用 Entra ID アプリは Service Principal まで作成しておくこと(後述のハマり 1)。
動作確認だけなら、Azure Portal の Bot リソース → Test in Web Chat が最速です。
ハマったポイントと解決策
1. WebChat 無反応 / 502 → Service Principal 未作成
az ad app create は アプリケーションオブジェクトだけ を作ります。Service Principal がないと client credentials でトークンが取れず、ボットが応答(送信)できません。
ログに出ていた実エラー:
ValueError: Failed to acquire token.
AADSTS7000229: The client application <bot-app-id> is missing service principal in the tenant <tenant-id>.
解決:
APP_ID=$(az ad app create --display-name "$APP_NAME" --sign-in-audience AzureADMyOrg --query appId -o tsv)
az ad sp create --id "$APP_ID" # ← これが必須(AADSTS7000229 回避)
2. 「エラーが発生しました」だけ返る → 会話状態 API の誤用
M365 Agents SDK の TurnState のスコープは クラス名キー(ConversationState)で登録されています。dict 風アクセスやプレフィックス指定は失敗します。
踏んだエラーの遷移:
AttributeError: 'str' object has no attribute 'turn_state' # state.conversation.get("key")
ValueError: Scope 'conversation' not found # state.get_value("conversation.key")
正しい書き方は、スコープオブジェクト経由の get_value / set_value:
cid = state.conversation.get_value("foundry_conversation_id") # 取得(無ければ None)
state.conversation.set_value("foundry_conversation_id", cid) # 設定
状態はターン開始時に load、終了時に save で永続化されます。
3. App Service が作れない → VM クォータ 0 のリージョン
サブスクリプションによっては特定リージョンの App Service VM クォータが 0 で、Free(F1) ですら作成できません。
InternalSubscriptionIsOverQuotaForSku
Current Limit (Total VMs): 0 / Amount required: 1
what-if で複数リージョンを試すと、クォータのあるリージョンが見つかります。今回は Foundry はそのまま、App Service と Bot だけ別リージョンに退避しました(App → Foundry はパブリックエンドポイント経由なので別リージョンでも問題なし)。
リージョン退避のための Bicep パラメータ分離
@description('App Service / Bot 用リージョン(VM クォータがあるリージョンを指定)')
param appServiceLocation string = location
module appService 'modules/app-service.bicep' = {
params: {
location: appServiceLocation // Foundry とは別リージョンに退避可能
// ...
}
}
検証結果
Direct Line 経由でデプロイ済みボットに実メッセージを送り、E2E を確認しました。
| # | シナリオ | 結果 |
|---|---|---|
| 1 | Bicep 構文チェック(warning 0) | ✅ |
| 2 | SDK import / API シグネチャ | ✅ |
| 3 |
azd up(infra + code deploy) |
✅ |
| 4 | App Service 稼働 / Bot 配線 | ✅ |
| 5 | Foundry Agent 作成(create_version) |
✅ |
| 6 | 会話 + Responses API(日本語応答) | ✅ |
| 7 | マルチターン記憶(名前を記憶) | ✅ |
実際の応答例(マルチターン):
> 私の名前はフクハラです。覚えてね。
< フクハラさん、お名前を覚えました!
> 私の名前は何でしたか?
< フクハラさんです。
まとめ
- M365 Agents SDK の薄いループに Foundry の Responses API を挿すだけで、Teams/Copilot 対応エージェントが作れる
- ハマりの大半は アプリ登録(SP 作成) と SDK の状態 API と クォータ。コードより周辺の Azure / Entra 側が落とし穴
- 次は OBO 認証で Graph を叩く、Semantic Kernel / Agent Framework を挟む、などの拡張が視野

