chore: import upstream snapshot with attribution
CodeQL / Analyze (csharp) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
dotnet-build-and-test / dotnet-test-functions (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-foundry-hosted-it (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
dotnet-build-and-test / Integration Test Report (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:39:25 +08:00
commit db620d33df
5151 changed files with 925932 additions and 0 deletions
@@ -0,0 +1,13 @@
# Copyright (c) Microsoft. All rights reserved.
import importlib.metadata
from ._acquire_token import acquire_token
from ._agent import CopilotStudioAgent
try:
__version__ = importlib.metadata.version(__name__)
except importlib.metadata.PackageNotFoundError:
__version__ = "0.0.0" # Fallback for development mode
__all__ = ["CopilotStudioAgent", "__version__", "acquire_token"]
@@ -0,0 +1,95 @@
# Copyright (c) Microsoft. All rights reserved.
# pyright: reportUnknownMemberType = false
# pyright: reportUnknownVariableType = false
# pyright: reportUnknownArgumentType = false
import logging
from typing import Any
from agent_framework.exceptions import AgentException
from msal import PublicClientApplication
logger = logging.getLogger(__name__)
# Default scopes for Power Platform API
DEFAULT_SCOPES = ["https://api.powerplatform.com/.default"]
def acquire_token(
*,
client_id: str,
tenant_id: str,
username: str | None = None,
token_cache: Any | None = None,
scopes: list[str] | None = None,
) -> str:
"""Acquire an authentication token using MSAL Public Client Application.
This function attempts to acquire a token silently first (using cached tokens),
and falls back to interactive authentication if needed.
Keyword Args:
client_id: The client ID of the application.
tenant_id: The tenant ID for authentication.
username: Optional username to filter accounts.
token_cache: Optional token cache for storing tokens.
scopes: Optional list of scopes. Defaults to Power Platform API scopes.
Returns:
The access token string.
Raises:
AgentException: If authentication token cannot be acquired.
"""
if not client_id:
raise ValueError("Client ID is required for token acquisition.")
if not tenant_id:
raise ValueError("Tenant ID is required for token acquisition.")
authority = f"https://login.microsoftonline.com/{tenant_id}"
target_scopes = scopes or DEFAULT_SCOPES
pca = PublicClientApplication(client_id=client_id, authority=authority, token_cache=token_cache)
accounts = pca.get_accounts(username=username)
token: str | None = None
# Try silent token acquisition first if we have cached accounts
if accounts:
try:
logger.debug("Attempting silent token acquisition")
response = pca.acquire_token_silent(scopes=target_scopes, account=accounts[0])
if response and "access_token" in response:
token = str(response["access_token"])
logger.debug("Successfully acquired token silently")
elif response and "error" in response:
logger.warning(
"Silent token acquisition failed: %s - %s", response.get("error"), response.get("error_description")
)
except Exception as ex:
logger.warning("Silent token acquisition failed with exception: %s", ex)
# Fall back to interactive authentication if silent acquisition failed
if not token:
try:
logger.debug("Attempting interactive token acquisition")
response = pca.acquire_token_interactive(scopes=target_scopes)
if response and "access_token" in response:
token = str(response["access_token"])
logger.debug("Successfully acquired token interactively")
elif response and "error" in response:
logger.error(
"Interactive token acquisition failed: %s - %s",
response.get("error"),
response.get("error_description"),
)
except Exception as ex:
logger.error("Interactive token acquisition failed with exception: %s", ex)
raise AgentException(f"Failed to acquire authentication token: {ex}") from ex
if not token:
raise AgentException("Authentication token cannot be acquired.")
return token
@@ -0,0 +1,348 @@
# Copyright (c) Microsoft. All rights reserved.
from __future__ import annotations
from collections.abc import AsyncIterable, Awaitable, Sequence
from typing import Any, Literal, TypedDict, overload
from agent_framework import (
AgentMiddlewareTypes,
AgentResponse,
AgentResponseUpdate,
AgentSession,
BaseAgent,
Content,
ContextProvider,
Message,
ResponseStream,
normalize_messages,
)
from agent_framework._settings import load_settings
from agent_framework._types import AgentRunInputs
from agent_framework.exceptions import AgentException
from microsoft_agents.copilotstudio.client import AgentType, ConnectionSettings, CopilotClient, PowerPlatformCloud
from ._acquire_token import acquire_token
class CopilotStudioSettings(TypedDict, total=False):
"""Copilot Studio model settings.
Settings are resolved in this order: explicit keyword arguments, values from an
explicitly provided .env file, then environment variables with the prefix
'COPILOTSTUDIOAGENT__'.
Keys:
environmentid: Environment ID of environment with the Copilot Studio App.
Can be set via environment variable COPILOTSTUDIOAGENT__ENVIRONMENTID.
schemaname: The agent identifier or schema name of the Copilot to use.
Can be set via environment variable COPILOTSTUDIOAGENT__SCHEMANAME.
agentappid: The app ID of the App Registration used to login.
Can be set via environment variable COPILOTSTUDIOAGENT__AGENTAPPID.
tenantid: The tenant ID of the App Registration used to login.
Can be set via environment variable COPILOTSTUDIOAGENT__TENANTID.
"""
environmentid: str | None
schemaname: str | None
agentappid: str | None
tenantid: str | None
class CopilotStudioAgent(BaseAgent):
"""A Copilot Studio Agent."""
def __init__(
self,
client: CopilotClient | None = None,
settings: ConnectionSettings | None = None,
*,
id: str | None = None,
name: str | None = None,
description: str | None = None,
context_providers: Sequence[ContextProvider] | None = None,
middleware: list[AgentMiddlewareTypes] | None = None,
environment_id: str | None = None,
agent_identifier: str | None = None,
client_id: str | None = None,
tenant_id: str | None = None,
token: str | None = None,
cloud: PowerPlatformCloud | None = None,
agent_type: AgentType | None = None,
custom_power_platform_cloud: str | None = None,
username: str | None = None,
token_cache: Any | None = None,
scopes: list[str] | None = None,
env_file_path: str | None = None,
env_file_encoding: str | None = None,
) -> None:
"""Initialize the Copilot Studio Agent.
Args:
client: Optional pre-configured CopilotClient instance. If not provided,
a new client will be created using the other parameters.
settings: Optional pre-configured ConnectionSettings. If not provided,
settings will be created from the other parameters.
Keyword Args:
id: id of the CopilotAgent
name: Name of the CopilotAgent
description: Description of the CopilotAgent
context_providers: Context Providers, to be used by the copilot agent.
middleware: Agent middleware used by the agent, should be a list of AgentMiddlewareTypes.
environment_id: Environment ID of the Power Platform environment containing
the Copilot Studio app. Can also be set via COPILOTSTUDIOAGENT__ENVIRONMENTID
environment variable.
agent_identifier: The agent identifier or schema name of the Copilot to use.
Can also be set via COPILOTSTUDIOAGENT__SCHEMANAME environment variable.
client_id: The app ID of the App Registration used for authentication.
Can also be set via COPILOTSTUDIOAGENT__AGENTAPPID environment variable.
tenant_id: The tenant ID of the App Registration used for authentication.
Can also be set via COPILOTSTUDIOAGENT__TENANTID environment variable.
token: Optional pre-acquired authentication token. If not provided,
token acquisition will be attempted using MSAL.
cloud: The Power Platform cloud to use (Public, GCC, etc.).
agent_type: The type of Copilot Studio agent (Copilot, Agent, etc.).
custom_power_platform_cloud: Custom Power Platform cloud URL if using
a custom environment.
username: Optional username for token acquisition.
token_cache: Optional token cache for storing authentication tokens.
scopes: Optional list of authentication scopes. Defaults to Power Platform
API scopes if not provided.
env_file_path: Optional path to .env file for loading configuration.
env_file_encoding: Encoding of the .env file, defaults to 'utf-8'.
Raises:
ValueError: If required configuration is missing or invalid.
"""
super().__init__(
id=id,
name=name,
description=description,
context_providers=context_providers,
middleware=middleware,
)
if not client:
copilot_studio_settings = load_settings(
CopilotStudioSettings,
env_prefix="COPILOTSTUDIOAGENT__",
environmentid=environment_id,
schemaname=agent_identifier,
agentappid=client_id,
tenantid=tenant_id,
env_file_path=env_file_path,
env_file_encoding=env_file_encoding,
)
resolved_environment_id = copilot_studio_settings.get("environmentid")
resolved_agent_identifier = copilot_studio_settings.get("schemaname")
resolved_client_id = copilot_studio_settings.get("agentappid")
resolved_tenant_id = copilot_studio_settings.get("tenantid")
if not settings:
if not resolved_environment_id:
raise ValueError(
"Copilot Studio environment ID is required. Set via 'environment_id' parameter "
"or 'COPILOTSTUDIOAGENT__ENVIRONMENTID' environment variable."
)
if not resolved_agent_identifier:
raise ValueError(
"Copilot Studio agent identifier/schema name is required. Set via 'agent_identifier' parameter "
"or 'COPILOTSTUDIOAGENT__SCHEMANAME' environment variable."
)
settings = ConnectionSettings(
environment_id=resolved_environment_id,
agent_identifier=resolved_agent_identifier,
cloud=cloud,
copilot_agent_type=agent_type,
custom_power_platform_cloud=custom_power_platform_cloud,
)
if not token:
if not resolved_client_id:
raise ValueError(
"Copilot Studio client ID is required. Set via 'client_id' parameter "
"or 'COPILOTSTUDIOAGENT__AGENTAPPID' environment variable."
)
if not resolved_tenant_id:
raise ValueError(
"Copilot Studio tenant ID is required. Set via 'tenant_id' parameter "
"or 'COPILOTSTUDIOAGENT__TENANTID' environment variable."
)
token = acquire_token(
client_id=resolved_client_id,
tenant_id=resolved_tenant_id,
username=username,
token_cache=token_cache,
scopes=scopes,
)
client = CopilotClient(settings=settings, token=token)
self.client = client
self.cloud = cloud
self.agent_type = agent_type
self.custom_power_platform_cloud = custom_power_platform_cloud
self.username = username
self.token_cache = token_cache
self.scopes = scopes
@overload
def run(
self,
messages: AgentRunInputs | None = None,
*,
stream: Literal[False] = False,
session: AgentSession | None = None,
) -> Awaitable[AgentResponse]: ...
@overload
def run(
self,
messages: AgentRunInputs | None = None,
*,
stream: Literal[True],
session: AgentSession | None = None,
) -> ResponseStream[AgentResponseUpdate, AgentResponse]: ...
def run(
self,
messages: AgentRunInputs | None = None,
*,
stream: bool = False,
session: AgentSession | None = None,
) -> Awaitable[AgentResponse] | ResponseStream[AgentResponseUpdate, AgentResponse]:
"""Get a response from the agent.
This method returns the final result of the agent's execution
as a single AgentResponse object. When stream=True, it returns
a ResponseStream that yields AgentResponseUpdate objects.
Args:
messages: The message(s) to send to the agent.
Keyword Args:
stream: Whether to stream the response. Defaults to False.
session: The conversation session associated with the message(s).
Returns:
When stream=False: An Awaitable[AgentResponse].
When stream=True: A ResponseStream of AgentResponseUpdate items.
"""
if stream:
return self._run_stream_impl(messages=messages, session=session)
return self._run_impl(messages=messages, session=session)
async def _run_impl(
self,
messages: AgentRunInputs | None = None,
*,
session: AgentSession | None = None,
) -> AgentResponse:
"""Non-streaming implementation of run."""
if not session:
session = self.create_session()
service_session_id = session.service_session_id
if service_session_id is None:
session.service_session_id = await self._start_new_conversation()
service_session_id = session.service_session_id
if not isinstance(service_session_id, str):
raise AgentException("CopilotStudioAgent requires service_session_id to be a string")
input_messages = normalize_messages(messages)
question = "\n".join([message.text for message in input_messages])
activities = self.client.ask_question(question, service_session_id)
response_messages: list[Message] = []
response_id: str | None = None
response_messages = [message async for message in self._process_activities(activities, streaming=False)]
response_id = response_messages[0].message_id if response_messages else None
return AgentResponse(messages=response_messages, response_id=response_id)
def _run_stream_impl(
self,
messages: AgentRunInputs | None = None,
*,
session: AgentSession | None = None,
) -> ResponseStream[AgentResponseUpdate, AgentResponse]:
"""Streaming implementation of run."""
async def _stream() -> AsyncIterable[AgentResponseUpdate]:
nonlocal session
if not session:
session = self.create_session()
service_session_id = session.service_session_id
if service_session_id is None:
session.service_session_id = await self._start_new_conversation()
service_session_id = session.service_session_id
if not isinstance(service_session_id, str):
raise AgentException("CopilotStudioAgent requires service_session_id to be a string")
input_messages = normalize_messages(messages)
question = "\n".join([message.text for message in input_messages])
activities = self.client.ask_question(question, service_session_id)
async for message in self._process_activities(activities, streaming=True):
yield AgentResponseUpdate(
role=message.role,
contents=message.contents,
author_name=message.author_name,
raw_representation=message.raw_representation,
response_id=message.message_id,
message_id=message.message_id,
)
def _finalize(updates: Sequence[AgentResponseUpdate]) -> AgentResponse[None]:
return AgentResponse.from_updates(updates)
return ResponseStream(_stream(), finalizer=_finalize)
async def _start_new_conversation(self) -> str:
"""Start a new conversation with the Copilot Studio agent.
Returns:
The conversation ID for the new conversation.
Raises:
AgentException: If the conversation could not be started.
"""
conversation_id: str | None = None
async for activity in self.client.start_conversation(emit_start_conversation_event=True):
if activity and activity.conversation and activity.conversation.id:
conversation_id = activity.conversation.id
if not conversation_id:
raise AgentException("Failed to start a new conversation.")
return conversation_id
async def _process_activities(self, activities: AsyncIterable[Any], streaming: bool) -> AsyncIterable[Message]:
"""Process activities from the Copilot Studio agent.
Args:
activities: Stream of activities from the agent.
streaming: Whether to process activities for streaming (typing activities)
or non-streaming (message activities) responses.
Yields:
Message objects created from the activities.
"""
async for activity in activities:
if activity.text and (
(activity.type == "message" and not streaming) or (activity.type == "typing" and streaming)
):
yield Message(
role="assistant",
contents=[Content.from_text(activity.text)],
author_name=activity.from_property.name if activity.from_property else None,
message_id=activity.id,
raw_representation=activity,
)