From efb19f995999f6581c57ed04e718b5dbeaa8e962 Mon Sep 17 00:00:00 2001 From: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> Date: Thu, 11 Sep 2025 15:09:20 -0700 Subject: [PATCH] Added CopilotStudioAgent implementation --- python/.env.example | 5 + python/packages/copilotstudio/LICENSE | 21 ++ .../agent_framework_copilotstudio/__init__.py | 13 + .../_acquire_token.py | 94 ++++++ .../agent_framework_copilotstudio/_agent.py | 282 ++++++++++++++++++ python/packages/copilotstudio/pyproject.toml | 94 ++++++ .../packages/copilotstudio/tests/__init__.py | 0 .../copilotstudio/tests/test_cross_package.py | 28 ++ .../packages/main/agent_framework/_agents.py | 30 +- .../agent_framework/copilotstudio/__init__.py | 24 ++ .../copilotstudio/__init__.pyi | 5 + .../agent_framework_workflow/_agent.py | 6 +- python/pyproject.toml | 4 +- python/uv.lock | 71 ++++- 14 files changed, 653 insertions(+), 24 deletions(-) create mode 100644 python/packages/copilotstudio/LICENSE create mode 100644 python/packages/copilotstudio/agent_framework_copilotstudio/__init__.py create mode 100644 python/packages/copilotstudio/agent_framework_copilotstudio/_acquire_token.py create mode 100644 python/packages/copilotstudio/agent_framework_copilotstudio/_agent.py create mode 100644 python/packages/copilotstudio/pyproject.toml create mode 100644 python/packages/copilotstudio/tests/__init__.py create mode 100644 python/packages/copilotstudio/tests/test_cross_package.py create mode 100644 python/packages/main/agent_framework/copilotstudio/__init__.py create mode 100644 python/packages/main/agent_framework/copilotstudio/__init__.pyi diff --git a/python/.env.example b/python/.env.example index 54cb688999..045b3f540e 100644 --- a/python/.env.example +++ b/python/.env.example @@ -17,3 +17,8 @@ AGENT_FRAMEWORK_ENABLE_SENSITIVE_DATA=true AGENT_FRAMEWORK_WORKFLOW_ENABLE_OTEL=true # Mem0 MEM0_API_KEY="" +# Copilot Studio +COPILOTSTUDIOAGENT__ENVIRONMENTID="" +COPILOTSTUDIOAGENT__SCHEMANAME="" +COPILOTSTUDIOAGENT__TENANTID="" +COPILOTSTUDIOAGENT__AGENTAPPID="" diff --git a/python/packages/copilotstudio/LICENSE b/python/packages/copilotstudio/LICENSE new file mode 100644 index 0000000000..9e841e7a26 --- /dev/null +++ b/python/packages/copilotstudio/LICENSE @@ -0,0 +1,21 @@ + MIT License + + Copyright (c) Microsoft Corporation. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE diff --git a/python/packages/copilotstudio/agent_framework_copilotstudio/__init__.py b/python/packages/copilotstudio/agent_framework_copilotstudio/__init__.py new file mode 100644 index 0000000000..dc892e2f39 --- /dev/null +++ b/python/packages/copilotstudio/agent_framework_copilotstudio/__init__.py @@ -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"] diff --git a/python/packages/copilotstudio/agent_framework_copilotstudio/_acquire_token.py b/python/packages/copilotstudio/agent_framework_copilotstudio/_acquire_token.py new file mode 100644 index 0000000000..f0334dc7d0 --- /dev/null +++ b/python/packages/copilotstudio/agent_framework_copilotstudio/_acquire_token.py @@ -0,0 +1,94 @@ +# 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 ServiceException +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. + + 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: + ServiceException: If authentication token cannot be acquired. + """ + if not client_id: + raise ServiceException("Client ID is required for token acquisition.") + + if not tenant_id: + raise ServiceException("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"]) # type: ignore[assignment] + 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"]) # type: ignore[assignment] + 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 ServiceException(f"Failed to acquire authentication token: {ex}") from ex + + if not token: + raise ServiceException("Authentication token cannot be acquired.") + + return token diff --git a/python/packages/copilotstudio/agent_framework_copilotstudio/_agent.py b/python/packages/copilotstudio/agent_framework_copilotstudio/_agent.py new file mode 100644 index 0000000000..fd914aa38e --- /dev/null +++ b/python/packages/copilotstudio/agent_framework_copilotstudio/_agent.py @@ -0,0 +1,282 @@ +# Copyright (c) Microsoft. All rights reserved. + +from collections.abc import AsyncIterable, MutableSequence +from typing import Any, ClassVar + +from agent_framework import ( + AgentRunResponse, + AgentRunResponseUpdate, + AgentThread, + BaseAgent, + ChatMessage, + Contents, + Role, + TextContent, + TextReasoningContent, +) +from agent_framework._pydantic import AFBaseSettings +from agent_framework.exceptions import ServiceException, ServiceInitializationError +from microsoft_agents.activity import Activity +from microsoft_agents.copilotstudio.client import AgentType, ConnectionSettings, CopilotClient, PowerPlatformCloud +from pydantic import ValidationError + +from ._acquire_token import acquire_token + + +class CopilotStudioSettings(AFBaseSettings): + """Copilot Studio model settings. + + The settings are first loaded from environment variables with the prefix 'COPILOTSTUDIOAGENT__'. + If the environment variables are not found, the settings can be loaded from a .env file + with the encoding 'utf-8'. If the settings are not found in the .env file, the settings + are ignored; however, validation will fail alerting that the settings are missing. + + Attributes: + environment_id: Environment ID of environment with the Copilot Studio App.. + (Env var COPILOTSTUDIOAGENT__ENVIRONMENTID) + agent_identifier: The agent identifier or schema name of the Copilot to use. + (Env var COPILOTSTUDIOAGENT__SCHEMANAME) + client_id: The app ID of the App Registration used to login. + (Env var COPILOTSTUDIOAGENT__AGENTAPPID) + tenant_id: The tenant ID of the App Registration used to login. + (Env var COPILOTSTUDIOAGENT__TENANTID) + Parameters: + env_file_path: If provided, the .env settings are read from this file path location. + env_file_encoding: The encoding of the .env file, defaults to 'utf-8'. + """ + + env_prefix: ClassVar[str] = "COPILOTSTUDIOAGENT__" + + environment_id: str | None = None + agent_identifier: str | None = None + client_id: str | None = None + tenant_id: str | None = None + + +class CopilotStudioAgent(BaseAgent): + """A Copilot Studio Agent.""" + + client: CopilotClient + settings: ConnectionSettings | None + environment_id: str | None + agent_identifier: str | None + client_id: str | None + tenant_id: str | None + token: str | None + cloud: PowerPlatformCloud | None + agent_type: AgentType | None + custom_power_platform_cloud: str | None + username: str | None + token_cache: Any | None + scopes: list[str] | None + + def __init__( + self, + client: CopilotClient | None = None, + settings: ConnectionSettings | 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: + if not client: + try: + copilot_studio_settings = CopilotStudioSettings( + environment_id=environment_id, + agent_identifier=agent_identifier, + client_id=client_id, + tenant_id=tenant_id, + env_file_path=env_file_path, + env_file_encoding=env_file_encoding, + ) + except ValidationError as ex: + raise ServiceInitializationError("Failed to create Copilot Studio settings.", ex) from ex + + if not settings: + if not copilot_studio_settings.environment_id: + raise ServiceInitializationError( + "Copilot Studio environment ID is required. Set via 'environment_id' parameter " + "or 'COPILOTSTUDIOAGENT__ENVIRONMENTID' environment variable." + ) + if not copilot_studio_settings.agent_identifier: + raise ServiceInitializationError( + "Copilot Studio agent identifier/schema name is required. Set via 'agent_identifier' parameter " + "or 'COPILOTSTUDIOAGENT__SCHEMANAME' environment variable." + ) + + settings = ConnectionSettings( + environment_id=copilot_studio_settings.environment_id, + agent_identifier=copilot_studio_settings.agent_identifier, + cloud=cloud, + copilot_agent_type=agent_type, + custom_power_platform_cloud=custom_power_platform_cloud, + ) + + if not token: + if not copilot_studio_settings.client_id: + raise ServiceInitializationError( + "Copilot Studio client ID is required. Set via 'client_id' parameter " + "or 'COPILOTSTUDIOAGENT__AGENTAPPID' environment variable." + ) + + if not copilot_studio_settings.tenant_id: + raise ServiceInitializationError( + "Copilot Studio tenant ID is required. Set via 'tenant_id' parameter " + "or 'COPILOTSTUDIOAGENT__TENANTID' environment variable." + ) + + token = acquire_token( + client_id=copilot_studio_settings.client_id, + tenant_id=copilot_studio_settings.tenant_id, + username=username, + token_cache=token_cache, + scopes=scopes, + ) + + client = CopilotClient(settings=settings, token=token) + + super().__init__( + client=client, # type: ignore[reportCallIssue] + settings=settings, # type: ignore[reportCallIssue] + environment_id=copilot_studio_settings.environment_id, # type: ignore[reportCallIssue] + agent_identifier=copilot_studio_settings.agent_identifier, # type: ignore[reportCallIssue] + client_id=copilot_studio_settings.client_id, # type: ignore[reportCallIssue] + tenant_id=copilot_studio_settings.tenant_id, # type: ignore[reportCallIssue] + token=token, # type: ignore[reportCallIssue] + cloud=cloud, # type: ignore[reportCallIssue] + agent_type=agent_type, # type: ignore[reportCallIssue] + custom_power_platform_cloud=custom_power_platform_cloud, # type: ignore[reportCallIssue] + username=username, # type: ignore[reportCallIssue] + token_cache=token_cache, # type: ignore[reportCallIssue] + scopes=scopes, # type: ignore[reportCallIssue] + ) + + async def run( + self, + messages: str | ChatMessage | list[str] | list[ChatMessage] | None = None, + *, + thread: AgentThread | None = None, + **kwargs: Any, + ) -> AgentRunResponse: + """Get a response from the agent. + + This method returns the final result of the agent's execution + as a single AgentRunResponse object. The caller is blocked until + the final result is available. + + Note: For streaming responses, use the run_stream method, which returns + intermediate steps and the final result as a stream of AgentRunResponseUpdate + objects. Streaming only the final result is not feasible because the timing of + the final result's availability is unknown, and blocking the caller until then + is undesirable in streaming scenarios. + + Args: + messages: The message(s) to send to the agent. + thread: The conversation thread associated with the message(s). + kwargs: Additional keyword arguments. + + Returns: + An agent response item. + """ + if not thread: + thread = self.get_new_thread() + thread.service_thread_id = await self._start_new_conversation() + + input_messages = self._normalize_messages(messages) + + question = "\n".join([message.text for message in input_messages]) + + activities = self.client.ask_question(question, thread.service_thread_id) + response_messages: list[ChatMessage] = [] + response_id: str | None = None + + async for message in self._process_activities(activities): + response_messages.append(message) + if not response_id: + response_id = message.message_id + + return AgentRunResponse(messages=response_messages, response_id=response_id) + + async def run_stream( + self, + messages: str | ChatMessage | list[str] | list[ChatMessage] | None = None, + *, + thread: AgentThread | None = None, + **kwargs: Any, + ) -> AsyncIterable[AgentRunResponseUpdate]: + """Run the agent as a stream. + + This method will return the intermediate steps and final results of the + agent's execution as a stream of AgentRunResponseUpdate objects to the caller. + + Note: An AgentRunResponseUpdate object contains a chunk of a message. + + Args: + messages: The message(s) to send to the agent. + thread: The conversation thread associated with the message(s). + kwargs: Additional keyword arguments. + + Yields: + An agent response item. + """ + if not thread: + thread = self.get_new_thread() + thread.service_thread_id = await self._start_new_conversation() + + input_messages = self._normalize_messages(messages) + + question = "\n".join([message.text for message in input_messages]) + + activities = self.client.ask_question(question, thread.service_thread_id) + + async for message in self._process_activities(activities): + yield AgentRunResponseUpdate( + role=message.role, + contents=message.contents, + additional_properties=message.additional_properties, + author_name=message.author_name, + raw_representation=message.raw_representation, + response_id=message.message_id, + message_id=message.message_id, + ) + + async def _start_new_conversation(self) -> str: + 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 ServiceException("Failed to start a new conversation.") + + return conversation_id + + async def _process_activities(self, activities: AsyncIterable[Activity]) -> AsyncIterable[ChatMessage]: + async for activity in activities: + if activity.type == "message": + yield self._create_chat_message_from_activity(activity, [TextContent(activity.text)]) + break + elif activity.type == "typing" or activity.type == "event": + yield self._create_chat_message_from_activity(activity, [TextReasoningContent(activity.text)]) + break + + def _create_chat_message_from_activity( + self, activity: Activity, contents: MutableSequence[Contents] + ) -> ChatMessage: + return ChatMessage( + role=Role.ASSISTANT, + contents=contents, + author_name=activity.from_property.name if activity.from_property else None, + message_id=activity.id, + raw_representation=activity, + ) diff --git a/python/packages/copilotstudio/pyproject.toml b/python/packages/copilotstudio/pyproject.toml new file mode 100644 index 0000000000..fcc91a16c2 --- /dev/null +++ b/python/packages/copilotstudio/pyproject.toml @@ -0,0 +1,94 @@ +[project] +name = "agent-framework-copilotstudio" +description = "Copilot Studio integration for Microsoft Agent Framework." +authors = [{ name = "Microsoft", email = "SK-Support@microsoft.com"}] +readme = "README.md" +requires-python = ">=3.10" +version = "0.1.0b1" +license-files = ["LICENSE"] +urls.homepage = "https://learn.microsoft.com/en-us/semantic-kernel/overview/" +urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" +urls.release_notes = "https://github.com/microsoft/agent-framework/releases?q=tag%3Apython-1&expanded=true" +urls.issues = "https://github.com/microsoft/agent-framework/issues" +classifiers = [ + "License :: OSI Approved :: MIT License", + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Framework :: Pydantic :: 2", + "Typing :: Typed", +] +dependencies = [ + "agent-framework", + "microsoft-agents-copilotstudio-client>=0.3.1", +] + +[tool.uv] +prerelease = "if-necessary-or-explicit" +environments = [ + "sys_platform == 'darwin'", + "sys_platform == 'linux'", + "sys_platform == 'win32'" +] + +[tool.uv-dynamic-versioning] +fallback-version = "0.0.0" +[tool.pytest.ini_options] +testpaths = 'tests' +addopts = "-ra -q -r fEX" +asyncio_mode = "auto" +asyncio_default_fixture_loop_scope = "function" +filterwarnings = [ + "ignore:Support for class-based `config` is deprecated:DeprecationWarning:pydantic.*" +] +timeout = 120 + +[tool.ruff] +extend = "../../pyproject.toml" + +[tool.coverage.run] +omit = [ + "**/__init__.py" +] + +[tool.pyright] +extend = "../../pyproject.toml" +exclude = ['tests'] + +[tool.mypy] +plugins = ['pydantic.mypy'] +strict = true +python_version = "3.10" +ignore_missing_imports = true +disallow_untyped_defs = true +no_implicit_optional = true +check_untyped_defs = true +warn_return_any = true +show_error_codes = true +warn_unused_ignores = false +disallow_incomplete_defs = true +disallow_untyped_decorators = true +disallow_any_unimported = true + +[tool.bandit] +targets = ["agent_framework_copilotstudio"] +exclude_dirs = ["tests"] + +[tool.poe] +executor.type = "uv" +include = "../../shared_tasks.toml" +[tool.poe.tasks] +mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_copilotstudio" +test = "pytest --cov=agent_framework_copilotstudio --cov-report=term-missing:skip-covered tests" + +[tool.uv.build-backend] +module-name = "agent_framework_copilotstudio" +module-root = "" + +[build-system] +requires = ["uv_build>=0.8.2,<0.9.0"] +build-backend = "uv_build" diff --git a/python/packages/copilotstudio/tests/__init__.py b/python/packages/copilotstudio/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/python/packages/copilotstudio/tests/test_cross_package.py b/python/packages/copilotstudio/tests/test_cross_package.py new file mode 100644 index 0000000000..228dafc504 --- /dev/null +++ b/python/packages/copilotstudio/tests/test_cross_package.py @@ -0,0 +1,28 @@ +# Copyright (c) Microsoft. All rights reserved. + + +def test_self_through_main() -> None: + try: + from agent_framework.copilotstudio import __version__ + except ImportError: + __version__ = None + + assert __version__ is not None + + +def test_self() -> None: + try: + from agent_framework_copilotstudio import __version__ + except ImportError: + __version__ = None + + assert __version__ is not None + + +def test_agent_framework() -> None: + try: + from agent_framework import __version__ + except ImportError: + __version__ = None + + assert __version__ is not None diff --git a/python/packages/main/agent_framework/_agents.py b/python/packages/main/agent_framework/_agents.py index 0981112521..9c15b5d3a2 100644 --- a/python/packages/main/agent_framework/_agents.py +++ b/python/packages/main/agent_framework/_agents.py @@ -170,6 +170,21 @@ class BaseAgent(AFBaseModel): await deserialize_thread_state(thread, serialized_thread, **kwargs) return thread + def _normalize_messages( + self, + messages: str | ChatMessage | Sequence[str] | Sequence[ChatMessage] | None = None, + ) -> list[ChatMessage]: + if messages is None: + return [] + + if isinstance(messages, str): + return [ChatMessage(role=Role.USER, text=messages)] + + if isinstance(messages, ChatMessage): + return [messages] + + return [ChatMessage(role=Role.USER, text=msg) if isinstance(msg, str) else msg for msg in messages] + # region ChatAgent @@ -669,21 +684,6 @@ class ChatAgent(BaseAgent): messages.extend(input_messages or []) return thread, messages - def _normalize_messages( - self, - messages: str | ChatMessage | Sequence[str] | Sequence[ChatMessage] | None = None, - ) -> list[ChatMessage]: - if messages is None: - return [] - - if isinstance(messages, str): - return [ChatMessage(role=Role.USER, text=messages)] - - if isinstance(messages, ChatMessage): - return [messages] - - return [ChatMessage(role=Role.USER, text=msg) if isinstance(msg, str) else msg for msg in messages] - def _get_agent_name(self) -> str: return self.name or "UnnamedAgent" diff --git a/python/packages/main/agent_framework/copilotstudio/__init__.py b/python/packages/main/agent_framework/copilotstudio/__init__.py new file mode 100644 index 0000000000..aff666452b --- /dev/null +++ b/python/packages/main/agent_framework/copilotstudio/__init__.py @@ -0,0 +1,24 @@ +# Copyright (c) Microsoft. All rights reserved. + +import importlib +from typing import Any + +PACKAGE_NAME = "agent_framework_copilotstudio" +PACKAGE_EXTRA = "copilotstudio" +_IMPORTS = ["CopilotStudioAgent", "__version__", "acquire_token"] + + +def __getattr__(name: str) -> Any: + if name in _IMPORTS: + try: + return getattr(importlib.import_module(PACKAGE_NAME), name) + except ModuleNotFoundError as exc: + raise ModuleNotFoundError( + f"The '{PACKAGE_EXTRA}' extra is not installed, " + f"please do `pip install agent-framework[{PACKAGE_EXTRA}]`" + ) from exc + raise AttributeError(f"Module {PACKAGE_NAME} has no attribute {name}.") + + +def __dir__() -> list[str]: + return _IMPORTS diff --git a/python/packages/main/agent_framework/copilotstudio/__init__.pyi b/python/packages/main/agent_framework/copilotstudio/__init__.pyi new file mode 100644 index 0000000000..c30a7b3031 --- /dev/null +++ b/python/packages/main/agent_framework/copilotstudio/__init__.pyi @@ -0,0 +1,5 @@ +# Copyright (c) Microsoft. All rights reserved. + +from agent_framework_copilotstudio import CopilotStudioAgent, __version__, acquire_token + +__all__ = ["CopilotStudioAgent", "__version__", "acquire_token"] diff --git a/python/packages/workflow/agent_framework_workflow/_agent.py b/python/packages/workflow/agent_framework_workflow/_agent.py index ffd9877679..94e1d6555e 100644 --- a/python/packages/workflow/agent_framework_workflow/_agent.py +++ b/python/packages/workflow/agent_framework_workflow/_agent.py @@ -2,7 +2,7 @@ import logging import uuid -from collections.abc import AsyncIterable +from collections.abc import AsyncIterable, Sequence from datetime import datetime from typing import TYPE_CHECKING, Any, ClassVar, TypedDict, cast @@ -199,7 +199,7 @@ class WorkflowAgent(BaseAgent): def _normalize_messages( self, - messages: str | ChatMessage | list[str] | list[ChatMessage] | None = None, + messages: str | ChatMessage | Sequence[str] | Sequence[ChatMessage] | None = None, ) -> list[ChatMessage]: """Normalize input messages to a list of ChatMessage objects.""" if messages is None: @@ -211,7 +211,7 @@ class WorkflowAgent(BaseAgent): if isinstance(messages, ChatMessage): return [messages] - normalized = [] + normalized: list[ChatMessage] = [] for msg in messages: if isinstance(msg, str): normalized.append(ChatMessage(role=Role.USER, contents=[TextContent(text=msg)])) diff --git a/python/pyproject.toml b/python/pyproject.toml index a8b1d7f979..2d6aa6d7c2 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -6,6 +6,7 @@ requires-python = ">=3.10" dependencies = [ "agent-framework", "agent-framework-azure", + "agent-framework-copilotstudio", "agent-framework-foundry", "agent-framework-mem0", "agent-framework-workflow", @@ -63,6 +64,7 @@ exclude = [ "packages/agent_framework_project.egg-info" ] [tool.uv.sources] agent-framework = { workspace = true } agent-framework-azure = { workspace = true } +agent-framework-copilotstudio = { workspace = true } agent-framework-foundry = { workspace = true } agent-framework-mem0 = { workspace = true } agent-framework-runtime = { workspace = true } @@ -193,7 +195,7 @@ build = "python run_tasks_in_packages_if_exists.py build" # combined checks check = ["fmt", "lint", "pyright", "mypy", "test", "markdown-code-lint", "samples-code-check"] pre-commit-check = ["fmt", "lint", "pyright", "markdown-code-lint", "samples-code-check"] -all-tests = "pytest --import-mode=importlib --cov=agent_framework --cov=agent_framework_azure --cov=agent_framework_foundry --cov=agent_framework_mem0 --cov=agent_framework_workflow --cov-report=term-missing:skip-covered packages/azure/tests packages/foundry/tests packages/main/tests packages/mem0/tests packages/workflow/tests" +all-tests = "pytest --import-mode=importlib --cov=agent_framework --cov=agent_framework_azure --cov=agent_framework_foundry --cov=agent_framework_mem0 --cov=agent_framework_workflow --cov-report=term-missing:skip-covered packages/azure/tests packages/copilotstudio/tests packages/foundry/tests packages/main/tests packages/mem0/tests packages/workflow/tests" [tool.poe.tasks.venv] cmd = "uv venv --clear --python $python" diff --git a/python/uv.lock b/python/uv.lock index 1eddb3c2c7..1542bcdca9 100644 --- a/python/uv.lock +++ b/python/uv.lock @@ -25,6 +25,7 @@ supported-markers = [ members = [ "agent-framework", "agent-framework-azure", + "agent-framework-copilotstudio", "agent-framework-foundry", "agent-framework-mem0", "agent-framework-project", @@ -51,7 +52,7 @@ source = { editable = "packages/main" } dependencies = [ { name = "azure-monitor-opentelemetry", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "azure-monitor-opentelemetry-exporter", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, - { name = "mcp", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "mcp", extra = ["ws"], marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "openai", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "opentelemetry-api", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "opentelemetry-exporter-otlp-proto-grpc", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -59,7 +60,6 @@ dependencies = [ { name = "pydantic", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "pydantic-settings", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, - { name = "websockets", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] [package.optional-dependencies] @@ -84,7 +84,7 @@ requires-dist = [ { name = "agent-framework-workflow", marker = "extra == 'workflow'", editable = "packages/workflow" }, { name = "azure-monitor-opentelemetry", specifier = ">=1.7.0" }, { name = "azure-monitor-opentelemetry-exporter", specifier = ">=1.0.0b41" }, - { name = "mcp", specifier = ">=1.12" }, + { name = "mcp", extras = ["ws"], specifier = ">=1.13" }, { name = "openai", specifier = ">=1.99.0" }, { name = "opentelemetry-api", specifier = "~=1.24" }, { name = "opentelemetry-exporter-otlp-proto-grpc", specifier = ">=1.36.0" }, @@ -92,7 +92,6 @@ requires-dist = [ { name = "pydantic", specifier = ">=2.11.7" }, { name = "pydantic-settings", specifier = ">=2.10.1" }, { name = "typing-extensions", specifier = ">=4.14.0" }, - { name = "websockets", specifier = ">=15.0.1" }, ] provides-extras = ["azure", "foundry", "workflow", "runtime"] @@ -111,6 +110,21 @@ requires-dist = [ { name = "azure-identity", specifier = ">=1.13" }, ] +[[package]] +name = "agent-framework-copilotstudio" +version = "0.1.0b1" +source = { editable = "packages/copilotstudio" } +dependencies = [ + { name = "agent-framework", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "microsoft-agents-copilotstudio-client", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] + +[package.metadata] +requires-dist = [ + { name = "agent-framework", editable = "packages/main" }, + { name = "microsoft-agents-copilotstudio-client", specifier = ">=0.3.1" }, +] + [[package]] name = "agent-framework-foundry" version = "0.1.0b1" @@ -152,6 +166,7 @@ source = { virtual = "." } dependencies = [ { name = "agent-framework", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "agent-framework-azure", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "agent-framework-copilotstudio", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "agent-framework-foundry", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "agent-framework-mem0", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "agent-framework-workflow", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -196,6 +211,7 @@ dev = [ requires-dist = [ { name = "agent-framework", editable = "packages/main" }, { name = "agent-framework-azure", editable = "packages/azure" }, + { name = "agent-framework-copilotstudio", editable = "packages/copilotstudio" }, { name = "agent-framework-foundry", editable = "packages/foundry" }, { name = "agent-framework-mem0", editable = "packages/mem0" }, { name = "agent-framework-workflow", editable = "packages/workflow" }, @@ -1052,7 +1068,7 @@ name = "exceptiongroup" version = "1.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "(python_full_version < '3.13' and sys_platform == 'darwin') or (python_full_version < '3.13' and sys_platform == 'linux') or (python_full_version < '3.13' and sys_platform == 'win32')" }, + { name = "typing-extensions", marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform == 'win32')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749 } wheels = [ @@ -1860,6 +1876,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/19/3f/d085c7f49ade6d273b185d61ec9405e672b6433f710ea64a90135a8dd445/mcp-1.13.1-py3-none-any.whl", hash = "sha256:c314e7c8bd477a23ba3ef472ee5a32880316c42d03e06dcfa31a1cc7a73b65df", size = 161494 }, ] +[package.optional-dependencies] +ws = [ + { name = "websockets", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] + [[package]] name = "mdit-py-plugins" version = "0.5.0" @@ -1899,6 +1920,46 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5b/f4/7fb9974bb1f81afb363a6b5a74f64609bf8e021d0cfe486909c6db6a8f3b/mem0ai-0.1.117-py3-none-any.whl", hash = "sha256:12159d6b9fef22a155f9b8b305999c52dfad20a46e7ccbced68677b6f48d4b57", size = 213063 }, ] +[[package]] +name = "microsoft-agents-activity" +version = "0.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/6a/dfc2fc0316b7dc4f6d24792b4a31a873b026be76792af1e0c3e65f843ef0/microsoft_agents_activity-0.3.1.tar.gz", hash = "sha256:c7567fc30f8e6f2a2d74cd65a1f7f31ade0d7ec9dd94531677d0d7b0648c77ee", size = 44886 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/8b/50ce2243e2900e94358f37009121145bb8224a388d95d704856aa2686667/microsoft_agents_activity-0.3.1-py3-none-any.whl", hash = "sha256:d7fc2e9cf2843ec8d6d42608b808b159a12cbb61e1fc7d7b1aaf29899f20746a", size = 111904 }, +] + +[[package]] +name = "microsoft-agents-copilotstudio-client" +version = "0.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "microsoft-agents-hosting-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/a5/2381ffd14d6a584f9f7ab80c7b6c634f658ea651b38702eb403c930d8396/microsoft_agents_copilotstudio_client-0.3.1.tar.gz", hash = "sha256:c529209241c9d11b7a6e8696f96a3d43121c10b49e44f00e5066f9cf5256f4f3", size = 5024 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/35/8b4e9c691f2ce89653007f358519bbadff1fe0d495c3723c9dbbfa962a33/microsoft_agents_copilotstudio_client-0.3.1-py3-none-any.whl", hash = "sha256:cac7485405325b990202452c9c14848cbdb25d13e6cdaf7bd3eca3a5c1fb3989", size = 7420 }, +] + +[[package]] +name = "microsoft-agents-hosting-core" +version = "0.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "isodate", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "microsoft-agents-activity", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "pyjwt", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "python-dotenv", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6a/14/a1365e0bab1486c2d16aabeb192ca90715794edf4e68be4815c245884420/microsoft_agents_hosting_core-0.3.1.tar.gz", hash = "sha256:0b76bda10e7a54ff3c86e56cbabaad5ac7a4c2a076c9833af3b2f4c86fa85e89", size = 81137 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/1b/543ddaa2daf8593911a02a07a6a78366d4a6a0053ec86a557c19fa97b60e/microsoft_agents_hosting_core-0.3.1-py3-none-any.whl", hash = "sha256:a4b41556b15321b74f539c5a0a89f70955459b7ec57e9e4b24e61bba27f1cbbc", size = 94573 }, +] + [[package]] name = "msal" version = "1.33.0"