Python: [BREAKING] cleanup of thread API and serialization (#893)

* cleanup of threads and serialization

* fix for sliding window

* fix redis test

* updated from comments

* updated context provider and threads

* updated lock

* add asyncio default

* fix redis tests

* fix tests

* fix tests

* renamed to invoking

* fixed tests

* fix for instructions
This commit is contained in:
Eduard van Valkenburg
2025-09-29 18:22:34 +02:00
committed by GitHub
Unverified
parent bf5931932e
commit 10d10364a9
52 changed files with 1642 additions and 1411 deletions
@@ -4,11 +4,14 @@ from collections.abc import AsyncIterable
from typing import Any, ClassVar
from agent_framework import (
AgentMiddlewares,
AgentRunResponse,
AgentRunResponseUpdate,
AgentThread,
AggregateContextProvider,
BaseAgent,
ChatMessage,
ContextProvider,
Role,
TextContent,
)
@@ -53,20 +56,16 @@ class CopilotStudioSettings(AFBaseSettings):
class CopilotStudioAgent(BaseAgent):
"""A Copilot Studio Agent."""
client: CopilotClient
settings: ConnectionSettings | 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,
*,
id: str | None = None,
name: str | None = None,
description: str | None = None,
context_providers: ContextProvider | list[ContextProvider] | AggregateContextProvider | None = None,
middleware: AgentMiddlewares | list[AgentMiddlewares] | None = None,
environment_id: str | None = None,
agent_identifier: str | None = None,
client_id: str | None = None,
@@ -88,6 +87,11 @@ class CopilotStudioAgent(BaseAgent):
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.
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 middlewares used by the agent.
environment_id: Environment ID of the Power Platform environment containing
the Copilot Studio app. Can also be set via COPILOTSTUDIOAGENT__ENVIRONMENTID
environment variable.
@@ -113,6 +117,13 @@ class CopilotStudioAgent(BaseAgent):
Raises:
ServiceInitializationError: If required configuration is missing or invalid.
"""
super().__init__(
id=id,
name=name,
description=description,
context_providers=context_providers,
middleware=middleware,
)
if not client:
try:
copilot_studio_settings = CopilotStudioSettings(
@@ -169,17 +180,13 @@ class CopilotStudioAgent(BaseAgent):
client = CopilotClient(settings=settings, token=token)
super().__init__(
client=client, # type: ignore[reportCallIssue]
settings=settings, # 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]
)
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
async def run(
self,
@@ -121,7 +121,6 @@ class TestCopilotStudioAgent:
with pytest.raises(ServiceInitializationError, match="agent identifier"):
CopilotStudioAgent()
@pytest.mark.asyncio
async def test_run_with_string_message(self, mock_copilot_client: MagicMock, mock_activity: MagicMock) -> None:
"""Test run method with string message."""
agent = CopilotStudioAgent(client=mock_copilot_client)
@@ -141,7 +140,6 @@ class TestCopilotStudioAgent:
assert content.text == "Test response"
assert response.messages[0].role == Role.ASSISTANT
@pytest.mark.asyncio
async def test_run_with_chat_message(self, mock_copilot_client: MagicMock, mock_activity: MagicMock) -> None:
"""Test run method with ChatMessage."""
agent = CopilotStudioAgent(client=mock_copilot_client)
@@ -162,7 +160,6 @@ class TestCopilotStudioAgent:
assert content.text == "Test response"
assert response.messages[0].role == Role.ASSISTANT
@pytest.mark.asyncio
async def test_run_with_thread(self, mock_copilot_client: MagicMock, mock_activity: MagicMock) -> None:
"""Test run method with existing thread."""
agent = CopilotStudioAgent(client=mock_copilot_client)
@@ -180,7 +177,6 @@ class TestCopilotStudioAgent:
assert len(response.messages) == 1
assert thread.service_thread_id == "test-conversation-id"
@pytest.mark.asyncio
async def test_run_start_conversation_failure(self, mock_copilot_client: MagicMock) -> None:
"""Test run method when conversation start fails."""
agent = CopilotStudioAgent(client=mock_copilot_client)
@@ -190,7 +186,6 @@ class TestCopilotStudioAgent:
with pytest.raises(ServiceException, match="Failed to start a new conversation"):
await agent.run("test message")
@pytest.mark.asyncio
async def test_run_stream_with_string_message(self, mock_copilot_client: MagicMock) -> None:
"""Test run_stream method with string message."""
agent = CopilotStudioAgent(client=mock_copilot_client)
@@ -217,7 +212,6 @@ class TestCopilotStudioAgent:
assert response_count == 1
@pytest.mark.asyncio
async def test_run_stream_with_thread(self, mock_copilot_client: MagicMock) -> None:
"""Test run_stream method with existing thread."""
agent = CopilotStudioAgent(client=mock_copilot_client)
@@ -246,7 +240,6 @@ class TestCopilotStudioAgent:
assert response_count == 1
assert thread.service_thread_id == "test-conversation-id"
@pytest.mark.asyncio
async def test_run_stream_no_typing_activity(self, mock_copilot_client: MagicMock) -> None:
"""Test run_stream method with non-typing activity."""
agent = CopilotStudioAgent(client=mock_copilot_client)
@@ -268,7 +261,6 @@ class TestCopilotStudioAgent:
assert response_count == 0
@pytest.mark.asyncio
async def test_run_multiple_activities(self, mock_copilot_client: MagicMock) -> None:
"""Test run method with multiple message activities."""
agent = CopilotStudioAgent(client=mock_copilot_client)
@@ -296,7 +288,6 @@ class TestCopilotStudioAgent:
assert isinstance(response, AgentRunResponse)
assert len(response.messages) == 2
@pytest.mark.asyncio
async def test_run_list_of_messages(self, mock_copilot_client: MagicMock, mock_activity: MagicMock) -> None:
"""Test run method with list of messages."""
agent = CopilotStudioAgent(client=mock_copilot_client)
@@ -313,7 +304,6 @@ class TestCopilotStudioAgent:
assert isinstance(response, AgentRunResponse)
assert len(response.messages) == 1
@pytest.mark.asyncio
async def test_run_stream_start_conversation_failure(self, mock_copilot_client: MagicMock) -> None:
"""Test run_stream method when conversation start fails."""
agent = CopilotStudioAgent(client=mock_copilot_client)