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 × Streamlit × Cosmos DB で会話履歴付き AI アプリ構築

0
Posted at

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 を選択しています。

infra/modules/foundry.bicep
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 モードで作成します。検証用途のため、最もコスト効率の良い構成です。

infra/modules/cosmos.bicep
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 ロールを割り当てます。

infra/modules/app.bicep
// 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 リソースで行います。

infra/modules/app.bicep
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
# azure.yaml
hooks:
  preprovision:        # Bicep デプロイの「前」に実行
    shell: sh
    run: scripts/preprovision.sh
  postprovision:       # Bicep デプロイの「後」に実行
    shell: sh
    run: scripts/postprovision.sh

azd provisionazd 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 の処理

scripts/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 の処理

scripts/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

アプリケーション実装

依存パッケージ

requirements.txt
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 の定義と作成

src/agent_service.py
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 の作成とメッセージ取得

src/agent_service.py
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 向け)に並び替えています。

メッセージ送信と応答取得

src/agent_service.py
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 認証で接続します。

src/cosmos_service.py
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"
}

保存されたCosmosDBをPortal上で見てみました。
image.png

Streamlit アプリ(app.py)

メインの UI アプリケーションです。

image.png

src/app.py
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. 以降は通常のチャットフロー

デプロイ手順

前提条件

デプロイ

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 は以下を順番に実行します:

  1. preprovision — Entra ID App Registration + Client Secret 作成
  2. Bicep デプロイ — 全 Azure リソースの作成
  3. postprovision — リダイレクト URI 設定 + ID Token 発行有効化 + Client Secret 検証
  4. アプリデプロイ — ソースコードを 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 でアプリコードに認証ロジックを持たせない構成
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?