Azure FunctionsでREST APIを作って、Copilot StudioとAzure AI Foundry Agent Serviceから呼び出してみました。色々と時間かけたポイントあったので、記事にしておきます。
Step
0. 前提
Azure Functionsの従量課金プランはWindowsの方がクラウド上にリソース多そうだったので、Windowsにしています。
種類 | Version | 備考 |
---|---|---|
OS | Ubuntu22.04.5 LTS | WSL2で動かしています |
Python | 3.12.9 | 3.13はVS CodeのExtensionで使えなかった |
Poetry | 2.1.3 | |
VS Code | 1.101.2 | |
Azure Functions Core Tools | 4.0.6821 |
VSCode 拡張
種類 | Version | 備考 |
---|---|---|
Azure Functions | 1.17.3 | |
Azurite | 3.34.0 |
Python パッケージ
種類 | Version | 備考 |
---|---|---|
azure-functions | 1.23.0 | |
fastapi | 0.115.14 | |
pydantic | 2.11.7 | |
python-dotenv | 1.1.1 | |
openapi-generator-cli | 7.14.0 | jdk4pyもインストール |
openapi-generator-cliは、以下のようにjdkもインストール。
poetry add openapi-generator-cli
poetry add openapi-generator-cli[jdk4py]
その他
- Azure上にサブスクリプションとリソースグループ作成済
- ローカルのプロジェクトのディレクトリ作成、Poetryの初期設定済
- Copilot Studioの基本設定済
- Azure AI Foundryの基本設定済
1. Azure Functions 実装
1.1. Azure Functions 作成
Azure Portalから作成します。
フレックス従量課金を選択。
Functionsの基本的な項目。あとはデフォルトのまま作成。
1.2. Azure Functions 実装
基本的な Azure Functions の Python でのHTTP Trigger実装方法は記事「Azure FunctionsにPythonでHTTP Triggerの関数実装と注意点」を参照してください。
今回は、以下のサンプルを改変して作っています。
レポジトリの重要部分だけピックアップすると以下のディレクトリ/ファイルです。
├── WrapperFunction
│ └── __init__.py
├── .env
├── function_app.py
├── host.json
└── requirements.txt
1.2.1. host.json
Using FastAPI Framework with Azure Functions内の指示に従っています。
routePrefix
を設定することで、今までエンドポイントのPrefixがapi
だったのが、無くなります。FastAPIだとこうするべきなのかまで調べていませんが、エンドポイントが変わるのが注意点です(多分、この設定起因でAzure Portalからテスト実行もできなくなる原因になっている気がします)。
{
"version": "2.0",
"logging": {
"applicationInsights": {
"samplingSettings": {
"isEnabled": true,
"excludedTypes": "Request"
}
}
},
"extensions": {
"http": {
"routePrefix": ""
}
},
"extensionBundle": {
"id": "Microsoft.Azure.Functions.ExtensionBundle",
"version": "[4.*, 5.0.0)"
}
}
1.2.2. function_app.py
シンプルなルートのスクリプトです。
今回は、シンプルに認証なしにしていますがhttp_auth_level=func.AuthLevel.ANONYMOUS
でキー認証を付加できます。
import azure.functions as func
from WrapperFunction import app as fastapi_app
app = func.AsgiFunctionApp(app=fastapi_app, http_auth_level=func.AuthLevel.ANONYMOUS)
1.2.3. WrapperFunction/__init__.py
今回のメインのスクリプトです。
getメソッド2つとpostメソッド1つで計3つをサンプルとして実装しています。
import os
from typing import Optional
from dotenv import load_dotenv
from fastapi import FastAPI, HTTPException, Query
from pydantic import BaseModel, Field
load_dotenv(override=True)
ITEMS = [
{"id": 1, "name": "みそラーメン", "price": 900},
{"id": 2, "name": "醤油ラーメン", "price": 600},
{"id": 3, "name": "塩ラーメン", "price": 700},
]
URL = (
os.getenv("WEBSITE_HOSTNAME")
if os.getenv("WEBSITE_SITE_NAME")
else "http://localhost:7071"
)
app = FastAPI(
title="Function API for agents",
version="1.0.0",
description="FastAPI on Azure Functions for agents",
servers=[
{
"url": URL,
"description": "Azure Function Endpoint",
}
],
)
# Copilot StudioのでImportできるバージョンに下げる
app.openapi_version = "3.0.3"
class Item(BaseModel):
id: int = Field(..., description="商品のID")
name: str = Field(..., description="商品の名前(例:とんこつラーメン)")
price: int = Field(..., description="商品の価格(単位:円)")
class ItemListResponse(BaseModel):
message: str = Field(..., description="メッセージ")
item: Optional[list[Item]] = Field(None, description="登録された商品の情報")
class ItemResponse(BaseModel):
message: str = Field(..., description="処理結果のメッセージ")
item: Optional[Item] = Field(None, description="登録された商品の情報")
@app.get("/items", response_model=ItemListResponse)
async def get_items(
q: Optional[str] = Query(
"", # No Default
title="検索クエリ",
max_length=50,
description="オプションの検索文字列。結果を絞り込みます。",
),
):
"""
アイテムの一覧を返します。検索条件`q`を入力してフィルタ可能です。
- **q**: 検索クエリ
"""
print(f"{q=}")
if q:
results = [item for item in ITEMS if q.lower() in item["name"].lower()]
else:
results = ITEMS
if results:
# q が渡らないケースもあるが省略
return {
"message": f"'{q}' に一致するレコードが{len(results)}件見つかりました。",
"item": results,
}
else:
return {
"message": f"'{q}' に一致するレコードは見つかりませんでした。",
"item": [],
}
@app.get(
"/items/{item_id}",
response_model=Item,
summary="アイテム情報を取得する",
)
def read_item(item_id: int):
"""
指定した ID のアイテム情報を取得します。
- **item_id**: 取得したいアイテムの一意な識別子
"""
# 実際はデータベースなどから取得する想定
item = next((item for item in ITEMS if item["id"] == item_id), None)
if item:
return item
else:
raise HTTPException(
status_code=404, detail=f"ID {item_id} の商品は見つかりませんでした。"
)
@app.post("/items", response_model=ItemResponse)
def create_item(item: Item):
"""
アイテムを登録します。
- **item**: アイテム情報
"""
return {"message": "アイテムを受け取りました。", "item": item}
1.2.4. requirements.txt
Poetryからrequirements.txt 作成。以下のコマンドで出しています。
--without-hashes
をつけないとすごく長くなります。
結果に出ている警告は無視しています。
$ poetry export -f requirements.txt --output requirements.txt --without-hashes
Warning: poetry-plugin-export will not be installed by default in a future version of Poetry.
In order to avoid a breaking change and make your automation forward-compatible, please install poetry-plugin-export explicitly. See https://python-poetry.org/docs/plugins/#using-plugins for details on how to install a plugin.
To disable this warning run 'poetry config warnings.export false'.
annotated-types==0.7.0 ; python_version >= "3.12" and python_version < "4.0"
anyio==4.9.0 ; python_version >= "3.12" and python_version < "4.0"
azure-functions==1.23.0 ; python_version >= "3.12" and python_version < "4.0"
fastapi==0.115.14 ; python_version >= "3.12" and python_version < "4.0"
idna==3.10 ; python_version >= "3.12" and python_version < "4.0"
markupsafe==3.0.2 ; python_version >= "3.12" and python_version < "4.0"
pydantic-core==2.33.2 ; python_version >= "3.12" and python_version < "4.0"
pydantic==2.11.7 ; python_version >= "3.12" and python_version < "4.0"
python-dotenv==1.1.1 ; python_version >= "3.12" and python_version < "4.0"
sniffio==1.3.1 ; python_version >= "3.12" and python_version < "4.0"
starlette==0.46.2 ; python_version >= "3.12" and python_version < "4.0"
typing-extensions==4.14.1 ; python_version >= "3.12" and python_version < "4.0"
typing-inspection==0.4.1 ; python_version >= "3.12" and python_version < "4.0"
werkzeug==3.1.3 ; python_version >= "3.12" and python_version < "4.0"
1.2.5. .env
FunctionのURLを設定しています。
WEBSITE_HOSTNAME="<Function URL>
1.3. Azure Functions テストとデプロイ
1.3.1. ローカルテスト
VS CodeのコマンドパレットでAzuriteを開始して、以下のコマンドでローカルテストをします。
host func start
FastAPIを使っているのでローカルテストは見やすいです。
http://localhost:7071/docs または http://localhost:7071/redoc
からAPIの定義を確認およびテストができます。
1.3.2. デプロイ
Azure Functionsをデプロイします。特記事項はないです。
1.4. Open API 定義取得
デプロイした先で https://hostname/docs または https://hostname/redoc のリンクから、または直接 https:///openapi.json からOpenAPIの定義ダウンロードします。
ダウンロードしたopenapi.json
はCopilot Studioにインポートする際にv2形式に変える必要があります(Azure AI Foundry側では不要なステップ)。API Management使うとこんな面倒なことは不要になるかもしれません。
openapi.json のファイルをカレントディレクトリに置いて、以下のコマンド実行します。
$ poetry run openapi-generator-cli generate -i openapi.json -g openapi -o ./swagger2-json
[main] INFO o.o.codegen.DefaultGenerator - Generating with dryRun=false
[main] INFO o.o.c.ignore.CodegenIgnoreProcessor - Output directory (/home/fukuhara/repositories/function-tool/./swagger2-json) does not exist, or is inaccessible. No file (.openapi-generator-ignore) will be evaluated.
[main] INFO o.o.codegen.DefaultGenerator - OpenAPI Generator: openapi (documentation)
[main] INFO o.o.codegen.DefaultGenerator - Generator 'openapi' is considered stable.
[main] INFO o.o.c.languages.OpenAPIGenerator - Output file name [outputFileName=openapi.json]
[main] INFO o.o.codegen.InlineModelResolver - Inline schema created as Location_inner. To have complete control of the model name, set the `title` field or use the modelNameMapping option (e.g. --model-name-mappings Location_inner=NewModel,ModelA=NewModelA in CLI) or inlineSchemaNameMapping option (--inline-schema-name-mappings Location_inner=NewModel,ModelA=NewModelA in CLI).
[main] INFO o.o.c.languages.OpenAPIGenerator - wrote file to /home/fukuhara/repositories/function-tool/./swagger2-json/openapi.json
[main] INFO o.o.codegen.TemplateManager - writing file /home/fukuhara/repositories/function-tool/./swagger2-json/README.md
[main] INFO o.o.codegen.TemplateManager - writing file /home/fukuhara/repositories/function-tool/./swagger2-json/.openapi-generator-ignore
[main] INFO o.o.codegen.TemplateManager - writing file /home/fukuhara/repositories/function-tool/./swagger2-json/.openapi-generator/VERSION
[main] INFO o.o.codegen.TemplateManager - writing file /home/fukuhara/repositories/function-tool/./swagger2-json/.openapi-generator/FILES
############################################################################################
# Thanks for using OpenAPI Generator. #
# We appreciate your support! Please consider donation to help us maintain this project. #
# https://opencollective.com/openapi_generator/donate #
############################################################################################
./swagger2-json
ディレクトリに以下のファイルが生成されます。
.
├── .openapi-generator
│ ├── FILES
│ └── VERSION
├── .openapi-generator-ignore
├── README.md
└── openapi.json
2. Copilot Studio Agent 登録
2.1. Copilot Studio Agent 登録
Agentの登録は省略します。
「ツール」タブで「+ツールを追加する」をクリック。
特に変更せずに次画面へ遷移。この画面でOpenAPI v3だとエラーが起きます。画面上だとエラーメッセージ詳細がわからないので、ブラウザの開発者ツールを開いて、HTTPのレスポンス見ると詳しい原因が出ています。
ここから各ツールを選択します(詳細省略)。基本的にはOpenAPI上にパラメータも定義しているので、各項目説明も自動入力されています(1つだけ抜けありました)。
「function」で検索して、先ほど登録したツールを選択。今回は試しに「Get Items」を追加。
「Create New Connection」を選択し、作成後、エージェントに追加します。
「概要」タブにいき、オーケストレーションは生成AIにやらせます。
2.2. 公開
2.3. Copilot / Teams から呼出
タブ「チャネル」で「Teams と Micorosoft 365 Copilot」を選択し、「エージェント を Microsoft 365 Copilot で使用可能にする」をONにして、「Microsoft 365 エージェントを表示する」または「Teams でエージェントを表示する」をクリックします。
これでTeams と Micorosoft 365 Copilotから使えるような設定になります。
Copilot 画面
3. Azure AI Foundry Agent Service 登録
3.1. Agent登録
細かい手順は省略してアクション追加部分のみ。
Azure AI Foundryからアクションを追加。
最後にopenapi.json
の内容をコピペで入れて完成です。こちらはわざわざv2に変換しないopenapi.jsonで大丈夫です。