01AgentApplication の経路を分け、既定の応答を RouteRank.LAST にする確認済
Agents SDK の AgentApplication は、経路を順に評価し、既定では最初に一致した 1 つだけを動かす。何にでも一致する既定の応答に RouteRank.LAST を付け、固定の命令と正規表現の経路を先に評価させる。
- 固定の命令は message("/help")、引数付きの命令は message(re.compile(...)) で登録する。
- 何にでも一致する activity("message") には rank=RouteRank.LAST を付ける。付けずに先に登録すると、手元の試験では /help もこの経路に回った。
- 文字列の経路は大文字と小文字を区別した(/HELP は既定の応答に回った)。区別しないなら re.IGNORECASE の正規表現にする。
- error() に (context, error) を受ける関数を登録する。利用者には定型文を返し、詳細はログに書く。
- 経路の登録は register_routes() にまとめる。試験では同じ関数で組み立て、on_turn() に発言を流して確かめる。
"""agent_routes.py: command routes, a ranked catch-all and an error handler for an AgentApplication."""
import re
from microsoft_agents.hosting.core import AgentApplication, RouteRank, TurnContext, TurnState
HELP = "Commands: /help, status <request id> (for example: status R-2026-002)."
STATUS_RE = re.compile(r"^status\s+(R-\d{4}-\d{3})$", re.IGNORECASE)
async def on_help(context: TurnContext, _state: TurnState):
await context.send_activity(HELP)
async def on_status(context: TurnContext, _state: TurnState):
request_id = STATUS_RE.match(context.activity.text.strip()).group(1).upper()
await context.send_activity(f"Looking up {request_id} ...")
async def on_other(context: TurnContext, _state: TurnState):
await context.send_activity("I did not recognise that. Type /help for commands.")
async def on_error(context: TurnContext, error: Exception):
print(f"turn error: {type(error).__name__}") # write details to your log, not to the chat
await context.send_activity("Something went wrong. Please try again later.")
def register_routes(app: AgentApplication) -> AgentApplication:
app.conversation_update("membersAdded")(on_help)
app.message("/help")(on_help)
app.message(STATUS_RE)(on_status)
app.activity("message", rank=RouteRank.LAST)(on_other) # catch-all, evaluated after the routes above
app.error(on_error)
return app実行結果(2026-09-12)
'/help' -> ['Commands: /help, status <request id> (for example: status R-2026-002).']
'/HELP' -> ['I did not recognise that. Type /help for commands.']
'status r-2026-002' -> ['Looking up R-2026-002 ...']
'hello' -> ['I did not recognise that. Type /help for commands.']
catch-all registered first with LAST: ['Commands: /help, status <request id> (for example: status R-2026-002).']
catch-all registered first without rank: ['I did not recognise that. Type /help for commands.']Microsoft 365 Agents SDK(Python)の AgentApplication に、/help、status <依頼ID>(大文字小文字を区別しない正規表現)、それ以外、の 3 つの経路を登録する関数を書いて。それ以外の経路は RouteRank.LAST で登録し、error ハンドラーも付けて。
文書の表は RouteRank.First・Unspecified・Last と書くが、Python の 1.5.0 の定数は FIRST・DEFAULT・LAST だった。文書の Python 例にある turn_error も 1.5.0 には無く、error() を使った。名前は版で変わりうるので、使う版で確かめる。利用者の発言の全文をログに残さない。
出典の該当箇所
By default, evaluation stops at the first matching route. Use RouteRank.Last for a catch-all fallback that handles anything not matched by a more specific route.
When an activity arrives, the system evaluates routes in order until it finds a match. By default, only one route runs.
The packages should target Python 3.10 or greater
02エージェントの接続設定を環境変数から読み、aiohttp で待ち受ける確認済
Python の Agents SDK は、CONNECTIONS__SERVICE_CONNECTION__SETTINGS__ で始まる環境変数を load_configuration_from_env() で読み、MsalConnectionManager に渡す。起動前に設定を点検し、/api/messages を JWT の検証付きで待ち受ける。
- 設定名は CONNECTIONS__<接続名>__SETTINGS__<項目> の形にする。接続名 SERVICE_CONNECTION は必須である。
- AUTHTYPE で認証の方式を選ぶ(Certificate、UserManagedIdentity、SystemManagedIdentity など)。クライアントシークレットは避ける。
- 手元で試すときだけ ANONYMOUS_ALLOWED=True を置く。本番では JWT の検証を有効にする。
- check_settings() で、設定が無い、匿名のまま外に公開する、シークレットを使う、を起動前に止める。
- aiohttp の Application に jwt_authorization_middleware を付け、POST /api/messages を start_agent_process() に渡す。
"""app.py: build an Agents SDK agent from environment variables and serve it with aiohttp."""
import os
from aiohttp.web import Application, Request, Response, run_app
from microsoft_agents.activity import load_configuration_from_env
from microsoft_agents.authentication.msal import MsalConnectionManager
from microsoft_agents.hosting.aiohttp import CloudAdapter, jwt_authorization_middleware, start_agent_process
from microsoft_agents.hosting.core import AgentApplication, Authorization, MemoryStorage, TurnContext, TurnState
PREFIX = "CONNECTIONS__SERVICE_CONNECTION__SETTINGS__"
def check_settings(env):
"""Return problems that should stop start-up."""
keys = {k[len(PREFIX):] for k in env if k.startswith(PREFIX)}
anonymous = str(env.get(PREFIX + "ANONYMOUS_ALLOWED", "")).lower() == "true"
problems = []
if not keys:
problems.append("no SERVICE_CONNECTION settings")
if anonymous and env.get("AGENT_HOST", "localhost") not in ("localhost", "127.0.0.1"):
problems.append("ANONYMOUS_ALLOWED is for local development only")
if not anonymous and keys and "CLIENTID" not in keys and env.get(PREFIX + "AUTHTYPE") != "SystemManagedIdentity":
problems.append("CLIENTID is missing")
if "CLIENTSECRET" in keys:
problems.append("client secret configured: use Certificate or a managed identity instead")
return problems
def build_agent(env):
config = load_configuration_from_env(env)
storage = MemoryStorage()
connections = MsalConnectionManager(**config)
agent = AgentApplication[TurnState](
storage=storage,
adapter=CloudAdapter(connection_manager=connections),
authorization=Authorization(storage, connections, **config),
**config,
)
@agent.activity("message")
async def on_message(context: TurnContext, _state: TurnState):
await context.send_activity(f"you said: {context.activity.text}")
return agent, connections
def build_web_app(agent, connections):
async def entry_point(req: Request) -> Response:
return await start_agent_process(req, agent, agent.adapter)
web = Application(middlewares=[jwt_authorization_middleware])
web.router.add_post("/api/messages", entry_point)
web["agent_configuration"] = connections.get_default_connection_configuration()
web["agent_app"] = agent
web["adapter"] = agent.adapter
return web
if __name__ == "__main__":
issues = check_settings(os.environ)
if issues:
raise SystemExit("stopped: " + "; ".join(issues))
agent, connections = build_agent(os.environ)
run_app(build_web_app(agent, connections), host=os.environ.get("AGENT_HOST", "localhost"),
port=int(os.environ.get("PORT", 3978)))実行結果(2026-09-12)
empty -> ['no SERVICE_CONNECTION settings']
local anonymous -> ok
anonymous on a public host -> ['ANONYMOUS_ALLOWED is for local development only']
certificate -> ok
system managed identity -> ok
secret -> ['client secret configured: use Certificate or a managed identity instead']
['POST /api/messages']
no settings -> No service connection configuration provided.Microsoft 365 Agents SDK(Python)のエージェントを aiohttp で動かす app.py を書いて。設定は load_configuration_from_env(os.environ) で読み、MsalConnectionManager と CloudAdapter を作る。起動前に SERVICE_CONNECTION の有無、匿名許可の使い方、シークレットの有無を点検する関数も付けて。
接続の設定が 1 つも無いと MsalConnectionManager は ValueError で止まった。クイックスタートにある引数なしの CloudAdapter() も 1.5.0 では ValueError になり、接続の設定を渡す必要があった。ANONYMOUS_ALLOWED は手元の開発用で、外から届く場所では使わない。.env ファイルはリポジトリに入れない。
出典の該当箇所
The agent obtains MSAL configuration at runtime from environment variables using the helper function load_configuration_from_env().
Connection settings use the format CONNECTIONS__<CONNECTION_NAME>__SETTINGS__<PROPERTY>.
The connection manager requires at minimum a connection named SERVICE_CONNECTION.
CONNECTIONS__SERVICE_CONNECTION__SETTINGS__ANONYMOUS_ALLOWED=True
For production environments, ensure JWT validation is enabled through the CloudAdapter configuration.
03エージェントが呼ぶ参照用の API を Azure Functions の HTTP 関数で作る確認済
Python v2 のプログラミングモデルで、依頼 ID を受けて台帳の状態を JSON で返す HTTP 関数を作る。認証レベルは FUNCTION にし、呼び出し側はアクセスキーを x-functions-key ヘッダーで渡す。
- func.FunctionApp(http_auth_level=func.AuthLevel.FUNCTION) を作り、@app.route(route=..., methods=["GET"]) で関数を登録する。
- 入力は req.params から読む。欠けていれば状態コード 400 と理由を JSON で返す。
- 見つからない ID も found: false の JSON で返し、エージェントが返答の文を組み立てやすくする。
- 返す項目は英語のキーに固定する。REST API ツール用の OpenAPI の定義と名前を合わせる。
- データの場所は os.getenv("LEDGER_PATH") のようにアプリ設定から読み、コードに書かない。
- HTTP の関数は 230 秒以内に応答を返す。長い処理は受付だけを返して、後で処理する。
"""function_app.py: HTTP-triggered lookup that an agent or a Copilot Studio REST API tool calls."""
import csv
import json
import logging
import os
import azure.functions as func
app = func.FunctionApp(http_auth_level=func.AuthLevel.FUNCTION)
COLUMNS = {"文書名": "title", "種別": "doc_type", "状態": "state", "指摘件数": "findings", "完了日": "completed"}
def load_ledger(path):
with open(path, encoding="utf-8-sig", newline="") as f:
return {row["依頼ID"]: row for row in csv.DictReader(f)}
def json_response(body, status_code=200):
return func.HttpResponse(json.dumps(body, ensure_ascii=False), status_code=status_code,
mimetype="application/json")
@app.route(route="request_status", methods=["GET"])
def request_status(req: func.HttpRequest) -> func.HttpResponse:
request_id = (req.params.get("id") or "").strip().upper()
if not request_id:
return json_response({"error": "query parameter 'id' is required"}, 400)
row = load_ledger(os.getenv("LEDGER_PATH", "ledger.csv")).get(request_id)
if row is None:
return json_response({"id": request_id, "found": False})
logging.info("request_status looked up %s", request_id)
result = {"id": request_id, "found": True}
result.update({en: row[ja] for ja, en in COLUMNS.items()})
result["findings"] = int(result["findings"] or 0)
return json_response(result)実行結果(2026-09-12)
200 {"id": "R-2026-002", "found": true, "title": "文書02(架空)", "doc_type": "プレスリリース", "state": "受付", "findings": 9, "completed": ""}
200 {"id": "R-2099-001", "found": false}
400 {"error": "query parameter 'id' is required"}Azure Functions の Python v2 モデルで、GET /api/request_status?id=... を受け、CSV の台帳から該当行を英語のキーの JSON で返す関数を書いて。認証レベルは FUNCTION、id が無ければ 400、見つからなければ found: false を返して。
HTTP で起動した関数は、関数アプリのタイムアウト設定にかかわらず 230 秒で応答が打ち切られる。FUNCTION の認証レベルでは、キーを知っていれば誰でも呼べる。キーはキー保管庫かアプリ設定に置き、コードや OpenAPI の定義に書かない。手元の Core Tools では認証が強制されないので、キーの確認は Azure 上で行う。返す項目は業務に要るものに絞る。
出典の該当箇所
You can include the access key in the URL by using the ?code= query string or in the request header (x-functions-key).
Regardless of the function app timeout setting, 230 seconds is the maximum amount of time that an HTTP triggered function can take to respond to a request.
Determines what keys, if any, need to be present on the request in order to invoke the function.
By default, authorization isn't enforced locally for HTTP endpoints.
04HTTP 関数を build().get_user_function() で取り出して単体試験する確認済
Python v2 の関数は、デコレーターを付けた名前から build().get_user_function() で元の関数を取り出し、func.HttpRequest を渡して試験できる。Core Tools もネットワークも要らない。azure-functions 1.21.0 以降は関数を直接呼ぶこともできる。
- func.HttpRequest(method=..., url=..., body=..., params=...) で試験用の要求を作る。
- 名前.build().get_user_function() で取り出した関数に要求を渡し、状態コードと本文を確かめる。
- 文書の例は my_function を読み込みながら main.build() を呼んでいる。デコレーターを付けた関数の名前に読み替える。
- 正しい入力に加え、壊れた JSON や項目が欠けた入力も試験に入れる。
- azure-functions 1.21.0 以降なら count_findings(req) のように直接呼んでも同じ結果になった(手元の venv で確認)。
- unittest か pytest で実行し、配置の前に毎回通す。
"""Unit-test a Python v2 HTTP function without Core Tools (function and tests in one file for brevity)."""
import json
import sys
import unittest
import azure.functions as func
app = func.FunctionApp(http_auth_level=func.AuthLevel.FUNCTION)
@app.route(route="count_findings", methods=["POST"])
def count_findings(req: func.HttpRequest) -> func.HttpResponse:
try:
rows = req.get_json().get("rows", [])
except ValueError:
return func.HttpResponse("JSON body is required", status_code=400)
total = sum(int(r.get("findings", 0)) for r in rows)
return func.HttpResponse(json.dumps({"rows": len(rows), "findings": total}), mimetype="application/json")
def make_request(body: bytes) -> func.HttpRequest:
return func.HttpRequest(method="POST", url="/api/count_findings", body=body)
class CountFindingsTest(unittest.TestCase):
def call(self, body):
return count_findings.build().get_user_function()(make_request(body))
def test_sum(self):
resp = self.call(json.dumps({"rows": [{"findings": 2}, {"findings": "3"}]}).encode())
self.assertEqual(resp.status_code, 200)
self.assertEqual(json.loads(resp.get_body()), {"rows": 2, "findings": 5})
def test_missing_rows(self):
self.assertEqual(json.loads(self.call(b"{}").get_body()), {"rows": 0, "findings": 0})
def test_bad_json(self):
self.assertEqual(self.call(b"not json").status_code, 400)
def test_direct_call(self):
resp = count_findings(make_request(json.dumps({"rows": [{"findings": 4}]}).encode()))
self.assertEqual(json.loads(resp.get_body())["findings"], 4)
if __name__ == "__main__":
suite = unittest.defaultTestLoader.loadTestsFromTestCase(CountFindingsTest)
result = unittest.TextTestRunner(stream=sys.stdout, verbosity=2).run(suite)
print("tests passed" if result.wasSuccessful() else "tests failed")
sys.exit(0 if result.wasSuccessful() else 1)実行結果(2026-09-12)
test_bad_json (__main__.CountFindingsTest.test_bad_json) ... ok
test_direct_call (__main__.CountFindingsTest.test_direct_call) ... ok
test_missing_rows (__main__.CountFindingsTest.test_missing_rows) ... ok
test_sum (__main__.CountFindingsTest.test_sum) ... ok
----------------------------------------------------------------------
Ran 4 tests in 0.001s
OK
tests passedこの Azure Functions の Python v2 の HTTP 関数に unittest の試験を書いて。func.HttpRequest で要求を作り、関数名.build().get_user_function() で呼ぶ。正常な入力、壊れた JSON、項目の欠けた入力の 3 通りを確かめて。
この試験は関数の中の処理だけを確かめる。認証レベル、ルート、host.json の設定は通らないので、func start で手元のホストを動かして確かめる。文書の単体試験の例は変数名が合わず、そのままでは動かない。
出典の該当箇所
By using azure-functions >= 1.21.0, you can also call functions directly by using the Python interpreter without running Core Tools.
func_call = main.build().get_user_function()
For most bindings, you can create a mock input object by creating an instance of an appropriate class from the azure.functions package.
05Copilot Studio の REST API ツール用に OpenAPI v2 の定義を書き出す確認済
Copilot Studio の REST API ツールは OpenAPI v2 の JSON から作る。関数の入出力と説明を Python の辞書で持ち、説明の不足を点検してから out_openapi.json に書き出し、ツールの追加の画面で読み込む。
- エージェントの[Tools]で[Add a tool]→[New tool]→[REST API]を選び、書き出した JSON を読み込む。
- swagger は "2.0" にする。v3 の定義を渡すと自動で v2 に変換される。
- API 全体と各操作の description に、いつ使うかを具体的に書く。エージェントはこの説明でツールを選ぶ。
- 引数と応答の項目にも説明を付ける。説明が空だと画面で先に進めない。
- 認証は[API key]を選び、Parameter name に x-functions-key、Parameter location に Header を入れる。
- 使わせない操作(更新・削除など)は定義に入れないか、ツールの選択で外す。
"""Write an OpenAPI 2.0 (swagger) file for the request_status function, for a Copilot Studio REST API tool."""
import json
import os
HOST = os.environ.get("FUNCTION_HOST", "localhost:7071") # set to your function app's host name
SPEC = {
"swagger": "2.0",
"info": {
"title": "Request status",
"version": "1.0.0",
"description": "Looks up a document review request by its ID (for example R-2026-002). Use it when a user "
"asks about the state, number of findings or completion date of a request.",
},
"host": HOST,
"basePath": "/api",
"schemes": ["http" if HOST.startswith("localhost") else "https"],
"produces": ["application/json"],
"securityDefinitions": {"functionKey": {"type": "apiKey", "name": "x-functions-key", "in": "header"}},
"security": [{"functionKey": []}],
"paths": {
"/request_status": {
"get": {
"operationId": "GetRequestStatus",
"summary": "Get request status",
"description": "Returns title, document type, state, number of findings and completion date "
"for one request ID. Returns found=false when the ID is not in the ledger.",
"parameters": [{"name": "id", "in": "query", "required": True, "type": "string",
"description": "Request ID such as R-2026-002"}],
"responses": {
"200": {"description": "Lookup result", "schema": {"type": "object", "properties": {
"id": {"type": "string", "description": "Request ID"},
"found": {"type": "boolean", "description": "False when the ID is unknown"},
"title": {"type": "string", "description": "Document title"},
"doc_type": {"type": "string", "description": "Document type"},
"state": {"type": "string", "description": "Current review state"},
"findings": {"type": "integer", "description": "Number of findings"},
"completed": {"type": "string", "description": "Completion date, empty if open"}}}},
"400": {"description": "The id parameter is missing"},
},
}
}
},
}
def check(spec):
problems = []
if spec.get("swagger") != "2.0":
problems.append("REST API tools need an OpenAPI v2 (swagger 2.0) file")
if len(spec["info"].get("description", "")) < 40:
problems.append("API description is too short for tool selection")
for path, ops in spec["paths"].items():
for method, op in ops.items():
where = f"{method.upper()} {path}"
if len(op.get("description", "")) < 40:
problems.append(f"{where}: operation description is too short")
for p in op.get("parameters", []):
if not p.get("description"):
problems.append(f"{where}: parameter {p['name']} has no description")
for code, resp in op.get("responses", {}).items():
for name, prop in resp.get("schema", {}).get("properties", {}).items():
if not prop.get("description"):
problems.append(f"{where}: response {code} field {name} has no description")
return problems
if __name__ == "__main__":
issues = check(SPEC)
with open("out_openapi.json", "w", encoding="utf-8") as f:
json.dump(SPEC, f, ensure_ascii=False, indent=2)
print("wrote out_openapi.json; operations:", sum(len(v) for v in SPEC["paths"].values()))
print("issues:", issues or "none")実行結果(2026-09-12)
wrote out_openapi.json; operations: 1
issues: noneこの Azure Functions の GET /api/request_status?id=... を、Copilot Studio の REST API ツールで読み込める OpenAPI 2.0(swagger)の JSON にする Python スクリプトを書いて。操作と引数の説明が足りない箇所を一覧にする関数も付けて。
REST API ツールはプレビューで、本番での利用は想定されていないと文書にある。キーの値は定義ファイルに書かず、接続を作るときに入れる。環境のデータポリシーで HTTP 系の機能が止められていることがある。定義にある操作は、エージェントが自分の判断で呼びうる。
出典の該当箇所
You must create REST API tools from an OpenAPI v2 specification. This requirement is due to the behavior of Power Platform in processing API specifications.
Provide a detailed description, because your agent orchestration uses the description to determine when to use the particular tool.
Parameter location: How you send the key for the API. Select either Header or Query.
If any of the descriptions are blank, you must complete them before you can move forward.
Preview features aren't meant for production use and may have restricted functionality.
You can include the access key in the URL by using the ?code= query string or in the request header (x-functions-key).
06エージェントの処理から Azure Functions の API を呼び、時間切れを扱う確認済
Agents SDK の経路の処理の中で aiohttp の ClientSession を使い、関数の API を GET で呼ぶ。URL とキーは環境変数から読み、時間切れや接続の失敗では利用者に定型文を返す。
- API の URL を STATUS_API_URL、キーを STATUS_API_KEY のようにアプリ設定から読む。手元では Core Tools の http://localhost:7071 を既定にする。
- キーは x-functions-key ヘッダーで送る。
- ClientTimeout(total=...) で待ち時間の上限を決める。関数側の上限(230 秒)より短くし、利用者を待たせすぎない。
- ClientError と TimeoutError を受け、利用者には後で試すよう伝える。原因はログに書く。
- 応答の JSON から返答の文を作る部分は format_reply() に分け、単体で試験する。
- 処理は AGENT_APP.message(STATUS_RE)(on_status) のように経路へ登録する。
"""Agent handler that calls the request_status function and replies with the result."""
import os
import re
from aiohttp import ClientError, ClientSession, ClientTimeout
from microsoft_agents.hosting.core import TurnContext, TurnState
STATUS_RE = re.compile(r"^status\s+(R-\d{4}-\d{3})$", re.IGNORECASE)
TIMEOUT = ClientTimeout(total=float(os.environ.get("STATUS_API_TIMEOUT", "20")))
async def fetch_status(request_id):
url = os.environ.get("STATUS_API_URL", "http://localhost:7071/api/request_status")
headers = {}
function_key = os.environ.get("STATUS_API_KEY") # from app settings or Key Vault, never from code
if function_key:
headers["x-functions-key"] = function_key
async with ClientSession(timeout=TIMEOUT) as session:
async with session.get(url, params={"id": request_id}, headers=headers) as resp:
resp.raise_for_status()
return await resp.json()
def format_reply(data):
if not data.get("found"):
return f"{data.get('id')}: not found in the ledger."
done = data.get("completed") or "-"
return (f"{data['id']} {data['title']} ({data['doc_type']}): {data['state']}, "
f"findings {data['findings']}, completed {done}")
async def on_status(context: TurnContext, _state: TurnState):
request_id = STATUS_RE.match(context.activity.text.strip()).group(1).upper()
try:
data = await fetch_status(request_id)
except (ClientError, TimeoutError) as error:
print(f"status lookup failed: {type(error).__name__}")
await context.send_activity("The status service did not respond. Please try again later.")
return
await context.send_activity(format_reply(data))
# Register it on your AgentApplication, for example: AGENT_APP.message(STATUS_RE)(on_status)実行結果(2026-09-12)
R-2026-002 文書02(架空) (プレスリリース): 受付, findings 9, completed -
R-2026-999: not found in the ledger.
header sent: True
status lookup failed: ClientConnectorError
service down -> The status service did not respond. Please try again later.Microsoft 365 Agents SDK(Python)の経路の処理から、Azure Functions の GET /api/request_status を aiohttp で呼ぶコードを書いて。URL とキーは環境変数、キーは x-functions-key ヘッダー、待ち時間の上限つき、失敗時は利用者に定型文を返して。
関数のキーは 1 つを全員で共有する形になり、利用者ごとの権限では絞られない。返す情報は関数の側で業務に要る範囲に限る。キーは Key Vault かアプリ設定に置き、ログに出さない。応答の内容は利用者に見せる前に形を確かめる。
出典の該当箇所
You can include the access key in the URL by using the ?code= query string or in the request header (x-functions-key).
Regardless of the function app timeout setting, 230 seconds is the maximum amount of time that an HTTP triggered function can take to respond to a request.
http_trigger: http://localhost:7071/api/http_trigger
07Python から Copilot Studio のエージェントに質問する(Copilot Studio Client)確認済
microsoft-agents-copilotstudio-client は、Copilot Studio で作ったエージェントに Python から直接つなぐクライアントである。利用者としてサインインして得たトークンで CopilotClient を作り、会話を始めて質問を送る。
- Copilot Studio でエージェントを公開し、[Settings]→[Advanced]→[Metadata]の Schema name と Environment Id を控える。
- Entra ID に公開クライアント(ネイティブ)のアプリを登録し、Power Platform API の委任のアクセス許可 CopilotStudio.Copilots.Invoke を付ける。
- 環境 ID、スキーマ名、アプリ ID、テナント ID を COPILOTSTUDIOAGENT__ で始まる環境変数に置く。
- MSAL の PublicClientApplication で https://api.powerplatform.com/.default のトークンを利用者として取る。
- ConnectionSettings と CopilotClient を作り、start_conversation() で会話 ID を得てから ask_question() で質問する。
"""Ask a published Copilot Studio agent one question from Python (signed-in user)."""
import asyncio
import os
from msal import PublicClientApplication
from microsoft_agents.activity import ActivityTypes
from microsoft_agents.copilotstudio.client import ConnectionSettings, CopilotClient
SCOPES = ["https://api.powerplatform.com/.default"]
NAMES = ["ENVIRONMENTID", "SCHEMANAME", "AGENTAPPID", "TENANTID"]
def settings_from_env():
values = {n: os.environ.get("COPILOTSTUDIOAGENT__" + n, "") for n in NAMES}
missing = [n for n, v in values.items() if not v]
if missing:
raise SystemExit("set COPILOTSTUDIOAGENT__" + ", COPILOTSTUDIOAGENT__".join(missing))
return values
def acquire_user_token(app_id, tenant_id):
app = PublicClientApplication(client_id=app_id, authority=f"https://login.microsoftonline.com/{tenant_id}")
accounts = app.get_accounts()
result = app.acquire_token_silent(SCOPES, account=accounts[0]) if accounts else None
if not result:
result = app.acquire_token_interactive(scopes=SCOPES)
return result["access_token"]
async def ask_once(question):
v = settings_from_env()
settings = ConnectionSettings(
environment_id=v["ENVIRONMENTID"],
agent_identifier=v["SCHEMANAME"],
cloud=None,
copilot_agent_type=None,
custom_power_platform_cloud=None,
)
client = CopilotClient(settings, acquire_user_token(v["AGENTAPPID"], v["TENANTID"]))
conversation_id = None
async for activity in client.start_conversation(True):
conversation_id = activity.conversation.id
replies = []
async for reply in client.ask_question(question, conversation_id):
if reply.type == ActivityTypes.message and reply.text:
replies.append(reply.text)
return replies
if __name__ == "__main__":
for text in asyncio.run(ask_once(os.environ.get("QUESTION", "What can you help me with?"))):
print(text)microsoft-agents-copilotstudio-client を使い、Copilot Studio のエージェントに 1 つ質問して返答の文だけを表示する Python のコードを書いて。ID は COPILOTSTUDIOAGENT__ で始まる環境変数から読み、トークンは MSAL の対話サインインで取って。
このクライアントは利用者のトークンが要る。公式サンプルには、サーバー間(S2S)の接続にはまだ対応していないとある。GitHub Copilot ハーネスで作ったエージェントは正式には対象外である。手元の venv にこのパッケージを入れていないので、コードは構文の確認だけで、実行はしていない。
出典の該当箇所
The Copilot Studio Client is for connecting to and interacting with agents created in Microsoft Copilot Studio.
Currently, you can only use the Copilot Studio client library with Copilot Studio agents created by using the standard harness.
The CopilotStudio Client requires a User Token to operate.
S2S is not currently supported for Copilot Studio.
"scopes": ["https://api.powerplatform.com/.default"],