0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Foundry AgentをM365 SDKでTeams連携

0
Last updated at Posted at 2026-06-10

本記事記述および実装内容は、多くの部分を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 + responsesagent_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 のリージョンがある。クォータのあるリージョンへ退避すれば回避できる

Copilot画面
image.png

Teams画面
image.png

アーキテクチャ

  • 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/ の構成
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 を冪等作成・呼び出し

依存は最小限です。

src/requirements.txt
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 を実行するだけです。

src/startup.sh
#!/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 に逃がすのがポイントです。

src/agent.py
@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 を参照します。

src/foundry_agent.py
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 チャネル有効化します。

infra/modules/bot-service.bicep
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[].scopescopilotcopilotAgents.customEngineAgents が必須です。

appPackage/manifest.json
{
  "$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.zipmanifest.json + アイコン)を Teams アプリとして配布します。マニフェストに copilot スコープと customEngineAgents を入れてあるので、Teams に入れれば M365 Copilot 側にも自動的に現れます。

事前にテナントで カスタムアプリのアップロード(サイドロード) が許可されている必要があります(Teams 管理センターのセットアップポリシーで有効化)。

A. 自分だけで試す(サイドロード)

  1. Teams を開く → 左メニュー アプリ
  2. 下部の アプリを管理アプリをアップロード
  3. カスタムアプリをアップロードappPackage/appPackage.zip を選択 → 追加
  4. M365 Copilot(Teams か microsoft365.com)を開く → 右側のエージェント一覧に追加したエージェントが表示 → 選んで会話

B. 組織に発行(管理者向け)

  1. Teams 管理センターadmin.teams.microsoft.com)→ Teams アプリアプリを管理
  2. 右上 アップロードappPackage.zip をアップロード(カタログに登録)
  3. 対象アプリを開き、公開状態を許可 → 必要なら アプリのセットアップポリシー で配布対象ユーザーを指定

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

解決:

scripts/create-app-registration.sh
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 パラメータ分離
infra/main.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 を挟む、などの拡張が視野

参考リンク

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?