Microsoft Foundry Agent Service × Streamlit × Cosmos DB で会話履歴付き AI チャットアプリを構築する
はじめに
AI チャットアプリを構築する際、「会話履歴をどこにどう保存するか」は設計上の重要なポイントです。本記事では、Microsoft Foundry Agent Service の Conversations API を活用し、Streamlit で UI を作成、Cosmos DB でメタデータを管理する構成を検証しました。
Azure Developer CLI(azd)と Bicep によるインフラ構築から、アプリケーションの実装、デプロイまでの一連の流れを紹介します。
本記事で扱う構成
| コンポーネント | 技術 |
|---|---|
| AI バックエンド | Microsoft Foundry Agent Service(gpt-5.4-mini) |
| 会話メッセージ保存 | Foundry Agent Service(Conversations API) |
| 会話メタデータ保存 | Azure Cosmos DB(NoSQL / Serverless) |
| Web UI | Streamlit(Python 3.13) |
| ホスティング | Azure App Service(B1 / Linux) |
| 認証 | Easy Auth(Microsoft Entra ID) |
| IaC | Bicep + Azure Developer CLI(azd) |
会話履歴に関する機能
- 会話履歴の保存と新規スレッド追加、過去スレッドの再開が可能。
- 会話履歴の削除、検索は対象外。
アーキテクチャ
システム構成図
┌──────────────────────────────────────────────┐
│ Azure │
│ │
User ──── HTTPS ───► │ App Service (Easy Auth / Entra ID) │
│ │ │
│ ▼ │
│ Streamlit App (Python 3.13) │
│ │ │ │
│ ▼ ▼ │
│ Foundry Agent Cosmos DB │
│ Service (NoSQL/Serverless) │
│ ┌──────────┐ ┌──────────────────┐ │
│ │ Agent │ │ 会話メタデータ │ │
│ │ Conver- │◄───────│ (userId, title, │ │
│ │ sations │threadId│ threadId, etc.) │ │
│ │ Responses│ └──────────────────┘ │
│ └──────────┘ │
│ │ ▲ │
│ │ │ 会話履歴の詳細 │
│ │ │ (メッセージ本文) │
│ │ │ を取得 │
│ ▼ │
│ gpt-5.4-mini │
└──────────────────────────────────────────────┘
データ管理の役割分担
本構成の設計上の核心は、会話データの責務を Foundry Agent Service と Cosmos DB で明確に分離する点です。
| データ | 管理先 | 理由 |
|---|---|---|
| 会話メッセージ履歴 | Foundry Agent Service | Conversations API がメッセージの永続化・取得を標準機能として提供 |
| ユーザーと会話の紐付け | Cosmos DB | Conversations API にはユーザー別の一覧取得機能がないため |
| 会話タイトル・更新日時 | Cosmos DB | UI 表示用のメタデータをユーザー別に高速検索 |
| Conversation ID | Cosmos DB | Foundry 側の会話への参照を保持 |
| AI の推論・応答生成 | Foundry Agent Service | Agent が Conversation コンテキストを参照して応答を生成 |
メッセージ本文は Cosmos DB に保存しません。threadId(Conversation ID)を介して Foundry Agent Service から取得します。これによりデータの重複を排除し、Cosmos DB のドキュメントサイズを最小限に抑えています。
プロジェクト構成
conversation_history/
├── README.md # 設計ドキュメント
├── azure.yaml # azd プロジェクト設定
├── requirements.txt # Python 依存関係
├── pyproject.toml # uv プロジェクト設定
│
├── infra/ # Bicep テンプレート
│ ├── main.bicep # メインテンプレート
│ ├── main.bicepparam # パラメータファイル
│ └── modules/
│ ├── foundry.bicep # Microsoft Foundry + Project + モデルデプロイ
│ ├── cosmos.bicep # Cosmos DB
│ └── app.bicep # App Service + Easy Auth + RBAC
│
├── scripts/
│ ├── preprovision.sh # Entra ID App Registration 作成
│ └── postprovision.sh # リダイレクト URI 設定 + 検証
│
└── src/
├── app.py # Streamlit メインアプリ
├── agent_service.py # Foundry Agent Service 操作
└── cosmos_service.py # Cosmos DB 操作
インフラ構築(Bicep)
Azure リソース一覧
| リソース | Bicep モジュール | SKU / 構成 |
|---|---|---|
| リソースグループ | main.bicep | - |
| Microsoft Foundry | foundry.bicep | S0(AIServices) |
| Foundry Project | foundry.bicep | - |
| gpt-5.4-mini デプロイメント | foundry.bicep | GlobalStandard / capacity: 10 |
| Cosmos DB | cosmos.bicep | Serverless |
| App Service Plan | app.bicep | B1(Linux) |
| App Service | app.bicep | Python 3.13 |
Foundry リソース(foundry.bicep)
Microsoft Foundry リソースは AIServices Kind で作成し、プロジェクトとモデルデプロイメントを子リソースとして定義します。
CognitiveServices accounts の Kind について
Microsoft.CognitiveServices/accounts リソースの kind には主に 3 種類あり、利用できる機能が異なります。
| Kind | Cognitive APIs | OpenAI モデル | Foundry Project | 用途 |
|---|---|---|---|---|
AIServices |
✅ | ✅ | ✅ | 推奨。統合型 |
OpenAI |
❌ | ✅ | ✅ | OpenAI モデル専用 |
CognitiveServices |
✅ | ❌ | ❌ | レガシー。Vision/Speech 等のみ |
-
AIServices— Cognitive APIs(Vision, Speech 等)と OpenAI モデル(GPT, DALL-E 等)の両方にアクセス可能な統合アカウント。Foundry Project の親リソースとして使えるため、新規プロジェクトではこれが推奨される -
OpenAI— GPT/DALL-E 等の OpenAI モデル専用。従来の Cognitive APIs は使えない -
CognitiveServices— 旧来の Cognitive Services 用。OpenAI モデルは使えず、Foundry 統合も限定的
本構成では、Agent Service + Project 管理 + モデルデプロイをすべて 1 リソース配下でまとめられる AIServices を選択しています。
resource foundryAccount 'Microsoft.CognitiveServices/accounts@2025-10-01-preview' = {
name: foundryName
location: location
kind: 'AIServices'
sku: { name: 'S0' }
identity: { type: 'SystemAssigned' }
properties: {
allowProjectManagement: true
customSubDomainName: foundryName
publicNetworkAccess: 'Enabled'
disableLocalAuth: true
}
}
resource foundryProject 'Microsoft.CognitiveServices/accounts/projects@2025-10-01-preview' = {
parent: foundryAccount
name: projectName
location: location
identity: { type: 'SystemAssigned' }
properties: { displayName: projectName }
}
resource modelDeployment 'Microsoft.CognitiveServices/accounts/deployments@2025-10-01-preview' = {
parent: foundryAccount
name: 'gpt-5-4-mini'
sku: { name: 'GlobalStandard', capacity: 10 }
properties: {
model: {
format: 'OpenAI'
name: 'gpt-5.4-mini'
version: '2026-03-17'
}
}
}
disableLocalAuth: true を設定することで、API キーによるアクセスを無効化し、マネージド ID による AAD 認証のみを許可しています。
Cosmos DB(cosmos.bicep)
Serverless モードで作成します。検証用途のため、最もコスト効率の良い構成です。
resource cosmosAccount 'Microsoft.DocumentDB/databaseAccounts@2024-05-15' = {
name: accountName
location: location
kind: 'GlobalDocumentDB'
properties: {
databaseAccountOfferType: 'Standard'
capabilities: [{ name: 'EnableServerless' }]
locations: [{ locationName: location, failoverPriority: 0 }]
consistencyPolicy: { defaultConsistencyLevel: 'Session' }
}
}
コンテナのパーティションキーは /userId です。ユーザーごとの会話一覧取得が主要なアクセスパターンのため、この設計がクエリ効率を最大化します。
App Service + RBAC(app.bicep)
App Service には SystemAssigned マネージド ID を付与し、以下の 2 つの RBAC ロールを割り当てます。
// Foundry への Cognitive Services User ロール
resource cogServicesRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
scope: foundryAccount
name: guid(foundryAccount.id, webApp.id, 'CognitiveServicesUser')
properties: {
roleDefinitionId: subscriptionResourceId(
'Microsoft.Authorization/roleDefinitions',
'a97b65f3-24c7-4388-baec-2e87135dc908'
)
principalId: webApp.identity.principalId
principalType: 'ServicePrincipal'
}
}
// Cosmos DB データプレーンの読み書き権限
resource cosmosRoleAssignment 'Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments@2024-05-15' = {
parent: cosmosAccount
name: guid(cosmosAccount.id, webApp.id, 'CosmosDBDataContributor')
properties: {
roleDefinitionId: '${cosmosAccount.id}/sqlRoleDefinitions/00000000-0000-0000-0000-000000000002'
principalId: webApp.identity.principalId
scope: cosmosAccount.id
}
}
Cosmos DB のデータプレーン RBAC(sqlRoleAssignments)は、Azure の標準 RBAC(Microsoft.Authorization/roleAssignments)とは異なる仕組みです。Cosmos DB 固有の組み込みロール 00000000-0000-0000-0000-000000000002(Built-in Data Contributor)を使用します。
認証(Easy Auth)
Easy Auth の設定は Bicep の authsettingsV2 リソースで行います。
resource authSettings 'Microsoft.Web/sites/config@2023-12-01' = if (!empty(authClientId)) {
parent: webApp
name: 'authsettingsV2'
properties: {
platform: { enabled: true }
globalValidation: {
requireAuthentication: true
unauthenticatedClientAction: 'RedirectToLoginPage'
redirectToProvider: 'azureactivedirectory'
}
identityProviders: {
azureActiveDirectory: {
enabled: true
registration: {
clientId: authClientId
clientSecretSettingName: 'MICROSOFT_PROVIDER_AUTHENTICATION_SECRET'
openIdIssuer: 'https://login.microsoftonline.com/${subscription().tenantId}/v2.0'
}
validation: {
allowedAudiences: [
'api://${authClientId}'
authClientId
]
}
}
}
login: { tokenStore: { enabled: true } }
}
}
azd フック(preprovision / postprovision)
Entra ID App Registration は Bicep では作成できないため、azd のフックスクリプトで処理します。
azd フックの仕組み
azd フックは azure.yaml で定義されたライフサイクルイベントに連動してスクリプトを自動実行する仕組みです。
# azure.yaml
hooks:
preprovision: # Bicep デプロイの「前」に実行
shell: sh
run: scripts/preprovision.sh
postprovision: # Bicep デプロイの「後」に実行
shell: sh
run: scripts/postprovision.sh
azd provision や azd up を実行すると、以下の順序で処理されます:
azd up
├─ 1. preprovision フック → preprovision.sh 実行
│ (App Registration 作成、Client Secret 生成)
├─ 2. Bicep デプロイ → Azure リソース作成
├─ 3. postprovision フック → postprovision.sh 実行
│ (リダイレクト URI 設定、ID Token 有効化)
└─ 4. アプリデプロイ → ソースコードを App Service へ
つまり「Bicep では扱えない操作(Entra ID App Registration 等)」を、Bicep の前後に差し込めるのがフックの役割です。他にも predeploy / postdeploy などのイベントが用意されています。
preprovision.sh の処理
APP_NAME="convhistory-${AZURE_ENV_NAME}"
# App Registration 作成
APP_ID=$(az ad app create \
--display-name "$APP_NAME" \
--sign-in-audience AzureADMyOrg \
--query appId -o tsv)
azd env set AZURE_AUTH_CLIENT_ID "$APP_ID"
# Client Secret 作成
SECRET=$(az ad app credential reset --id "$APP_ID" \
--append --display-name "EasyAuth" --query password -o tsv)
azd env set AZURE_AUTH_CLIENT_SECRET "$SECRET"
postprovision.sh の処理
APP_URL=$(azd env get-value APP_SERVICE_URL)
REDIRECT_URI="${APP_URL}/.auth/login/aad/callback"
az ad app update --id "$APP_OBJECT_ID" \
--web-redirect-uris "$REDIRECT_URI" \
--enable-id-token-issuance true
アプリケーション実装
依存パッケージ
streamlit>=1.37.0
azure-ai-projects>=2.0.0
azure-identity>=1.19.0
azure-cosmos>=4.9.0
Agent Service(agent_service.py)
Foundry Agent Service の操作を担当するモジュールです。azure-ai-projects SDK v2.x の最新 API を使用しています。
Agent の定義と作成
from azure.ai.projects import AIProjectClient
from azure.ai.projects.models import PromptAgentDefinition
from azure.identity import DefaultAzureCredential
AGENT_NAME = "chat-agent"
MODEL_NAME = "gpt-5-4-mini"
INSTRUCTIONS = "You are a helpful assistant. Respond in the same language as the user."
def _get_project_client() -> AIProjectClient:
return AIProjectClient(
endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
credential=DefaultAzureCredential(),
)
def _ensure_agent() -> str:
client = _get_project_client()
# 既存 Agent の確認
try:
client.agents.get(agent_name=AGENT_NAME)
return AGENT_NAME
except Exception:
pass
# 新規作成
definition = PromptAgentDefinition(
model=MODEL_NAME,
instructions=INSTRUCTIONS,
)
client.agents.create_version(
agent_name=AGENT_NAME,
definition=definition,
description="Chat agent for conversation history app",
)
return AGENT_NAME
Agent は PromptAgentDefinition で定義し、agents.create_version API で登録します。アプリ初回起動時に自動作成される仕組みです。
Conversation の作成とメッセージ取得
def create_thread() -> str:
"""新しい Conversation を作成し、conversation_id を返す。"""
client = _get_openai_client()
conversation = client.conversations.create()
return conversation.id
def get_messages(conversation_id: str) -> list[dict]:
"""Conversation 内のメッセージ履歴を取得する。"""
client = _get_openai_client()
items = client.conversations.items.list(conversation_id)
result = []
for item in items.data:
if getattr(item, "type", None) != "message":
continue
role = getattr(item, "role", "user")
content_parts = getattr(item, "content", [])
text = ""
if isinstance(content_parts, str):
text = content_parts
elif isinstance(content_parts, list):
for part in content_parts:
if hasattr(part, "text"):
text += part.text
if text:
result.append({"role": role, "content": text})
return list(reversed(result)) # 古い順に並び替え
conversations.items.list() はデフォルトで新しい順に返すため、reversed() で古い順(チャット UI 向け)に並び替えています。
メッセージ送信と応答取得
def send_message(conversation_id: str, content: str) -> str:
client = _get_openai_client()
agent_name = _ensure_agent()
# ユーザーメッセージを Conversation に追加
client.conversations.items.create(
conversation_id=conversation_id,
items=[{"type": "message", "role": "user", "content": content}],
)
# Agent で応答を生成(Responses API + agent_reference)
response = client.responses.create(
conversation=conversation_id,
extra_body={
"agent_reference": {"name": agent_name, "type": "agent_reference"}
},
)
return response.output_text or ""
Conversations API と Responses API を組み合わせて使用します。conversations.items.create() でユーザーメッセージを追加し、responses.create() に conversation パラメータと agent_reference を渡すことで、Agent がその会話コンテキストを踏まえた応答を生成します。
Cosmos DB Service(cosmos_service.py)
会話メタデータの CRUD 操作を担当します。DefaultAzureCredential を使用したマネージド ID 認証で接続します。
from azure.cosmos import CosmosClient
from azure.identity import DefaultAzureCredential
def _get_container():
client = CosmosClient(
os.environ["AZURE_COSMOS_ENDPOINT"],
credential=DefaultAzureCredential(),
)
database = client.get_database_client(os.environ["COSMOS_DATABASE_NAME"])
return database.get_container_client(os.environ["COSMOS_CONTAINER_NAME"])
ドキュメント構造
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"userId": "entra-user-object-id",
"threadId": "conv_abc123def456",
"title": "会話タイトル(最初のメッセージから自動生成)",
"createdAt": "2026-04-09T00:00:00+00:00",
"updatedAt": "2026-04-09T00:05:00+00:00"
}
Streamlit アプリ(app.py)
メインの UI アプリケーションです。
def get_user_id() -> str:
"""Easy Auth ヘッダーからユーザー ID を取得する。"""
headers = st.context.headers
return headers.get("X-Ms-Client-Principal-Id", "local-dev-user")
Easy Auth が処理した認証情報は HTTP ヘッダー X-Ms-Client-Principal-Id 経由で取得できます。アプリコード側での認証実装は不要です。
処理フロー
新規会話の開始:
1. ユーザーがメッセージを入力
2. Conversations API で新しい Conversation を作成 → conversation_id を取得
3. Cosmos DB に会話メタデータを登録(userId, threadId, title, timestamps)
4. Conversation にユーザーメッセージを追加
5. Responses API で Agent の応答を生成
6. UI にメッセージを表示
7. Cosmos DB の updatedAt を更新
過去の会話の再開:
1. サイドバーから過去の会話を選択
2. Cosmos DB から threadId(conversation_id)を取得
3. Conversations API からメッセージ履歴を取得して UI に表示
4. 以降は通常のチャットフロー
デプロイ手順
前提条件
- Azure Developer CLI (azd)
- Azure CLI (az)
- uv(Python パッケージ管理)
- 対象 Azure サブスクリプションへのアクセス権
デプロイ
cd conversation_history
# Azure 認証
azd auth login
# 環境の初期化
azd init
# サブスクリプションとリージョンの設定
azd env set AZURE_SUBSCRIPTION_ID <your-subscription-id>
azd env set AZURE_LOCATION eastus2
# プロビジョニング + デプロイ
azd up
azd up は以下を順番に実行します:
- preprovision — Entra ID App Registration + Client Secret 作成
- Bicep デプロイ — 全 Azure リソースの作成
- postprovision — リダイレクト URI 設定 + ID Token 発行有効化 + Client Secret 検証
- アプリデプロイ — ソースコードを App Service にデプロイ
クリーンアップ
azd down --purge
まとめ
本記事では、Microsoft Foundry Agent Service、Streamlit、Cosmos DB を組み合わせた会話履歴付き AI チャットアプリの構成を検証しました。
設計のポイント
- メッセージ本体の保存は Foundry Agent Service(Conversations API)に任せ、Cosmos DB はメタデータのみを管理することで、データの重複を避けつつ会話の一覧表示・再開を実現
- マネージド ID + RBAC でシークレット管理を最小限に抑制(Foundry・Cosmos DB ともに API キーを使わない)
- Easy Auth でアプリコードに認証ロジックを持たせない構成

