From beeef2ae282ec6d1a3c67dbf0e036489f3b973ff Mon Sep 17 00:00:00 2001 From: Sachin Mahajan Date: Sat, 25 Jul 2026 22:24:30 +0530 Subject: [PATCH 1/3] python: fix LineTooLong error in CopilotStudioAgent --- .../agent_framework_copilotstudio/_agent.py | 30 +++++++++ .../tests/test_line_too_long_bug.py | 62 +++++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 python/packages/copilotstudio/tests/test_line_too_long_bug.py diff --git a/python/packages/copilotstudio/agent_framework_copilotstudio/_agent.py b/python/packages/copilotstudio/agent_framework_copilotstudio/_agent.py index 0bc9c484327..ffa1ccc259a 100644 --- a/python/packages/copilotstudio/agent_framework_copilotstudio/_agent.py +++ b/python/packages/copilotstudio/agent_framework_copilotstudio/_agent.py @@ -5,6 +5,7 @@ from collections.abc import AsyncIterable, Awaitable, Sequence from typing import Any, Literal, TypedDict, overload +import aiohttp from agent_framework import ( AgentMiddlewareTypes, AgentResponse, @@ -20,11 +21,40 @@ from agent_framework._settings import load_settings from agent_framework._types import AgentRunInputs from agent_framework.exceptions import AgentException +from microsoft_agents.activity import Activity, ActivityTypes from microsoft_agents.copilotstudio.client import AgentType, ConnectionSettings, CopilotClient, PowerPlatformCloud from ._acquire_token import acquire_token +async def _patched_post_request( + self: CopilotClient, url: str, data: dict[str, Any], headers: dict[str, Any] +) -> AsyncIterable[Activity]: + async with aiohttp.ClientSession() as session, session.post(url, json=data, headers=headers) as response: + if response.status != 200: + raise aiohttp.ClientError(f"Error sending request: {response.status}") + event_type = None + buffer = b"" + async for chunk in response.content.iter_any(): + buffer += chunk + while b"\n" in buffer: + line, buffer = buffer.split(b"\n", 1) + if line.startswith(b"event:"): + event_type = line[6:].decode("utf-8").strip() + if line.startswith(b"data:") and event_type == "activity": + activity_data = line[5:].decode("utf-8").strip() + activity = Activity.model_validate_json(activity_data) + + if activity.type == ActivityTypes.message: + self._current_conversation_id = activity.conversation.id # pyright: ignore[reportPrivateUsage] + + yield activity + + +# Monkeypatch CopilotClient to handle data lines larger than 512KB without LineTooLong errors. +CopilotClient.post_request = _patched_post_request + + class CopilotStudioSettings(TypedDict, total=False): """Copilot Studio model settings. diff --git a/python/packages/copilotstudio/tests/test_line_too_long_bug.py b/python/packages/copilotstudio/tests/test_line_too_long_bug.py new file mode 100644 index 00000000000..6f9e8f9e98f --- /dev/null +++ b/python/packages/copilotstudio/tests/test_line_too_long_bug.py @@ -0,0 +1,62 @@ +# Copyright (c) Microsoft. All rights reserved. + +from collections.abc import AsyncIterable + +import pytest +from aiohttp import web +from microsoft_agents.activity import Activity +from microsoft_agents.copilotstudio.client import ConnectionSettings, CopilotClient +from agent_framework_copilotstudio import CopilotStudioAgent + + +async def mock_server_handler(request: web.Request) -> web.StreamResponse: + headers = {"Content-Type": "text/event-stream"} + response = web.StreamResponse(headers=headers) + await response.prepare(request) + + # 600KB line (larger than aiohttp default limit of 524288 bytes) + long_data = "x" * 600000 + + # Write event type line + await response.write(b"event: activity\n") + # Write data line + data_line = f'data: {{"type":"message","text":"{long_data}","conversation":{{"id":"test"}}}}\n'.encode("utf-8") + await response.write(data_line) + return response + + +@pytest.fixture +async def local_server() -> AsyncIterable[str]: + app = web.Application() + app.router.add_post("/test", mock_server_handler) + runner = web.AppRunner(app) + await runner.setup() + site = web.TCPSite(runner, "127.0.0.1", 0) + await site.start() + + address = runner.addresses[0] + port = address[1] + # Yield the URL to the test + yield f"http://127.0.0.1:{port}/test" + + await site.stop() + await runner.cleanup() + + +@pytest.mark.asyncio +async def test_copilot_client_post_request_long_line(local_server: str) -> None: + settings = ConnectionSettings( + environment_id="env", + agent_identifier="agent", + cloud=None, + copilot_agent_type=None, + custom_power_platform_cloud=None, + ) + client = CopilotClient(settings=settings, token="token") + + activities: list[Activity] = [] + async for activity in client.post_request(local_server, {}, {}): + activities.append(activity) + + assert len(activities) == 1 + assert activities[0].text == "x" * 600000 From 78588d1783a0e75573afcfe431c49ff66998438a Mon Sep 17 00:00:00 2001 From: Sachin Mahajan Date: Sat, 25 Jul 2026 22:43:55 +0530 Subject: [PATCH 2/3] python: apply review suggestions for CopilotStudioAgent fix --- .../agent_framework_copilotstudio/_agent.py | 15 ++++++++++++++- .../copilotstudio/tests/test_line_too_long_bug.py | 5 ++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/python/packages/copilotstudio/agent_framework_copilotstudio/_agent.py b/python/packages/copilotstudio/agent_framework_copilotstudio/_agent.py index ffa1ccc259a..1f7c61a5874 100644 --- a/python/packages/copilotstudio/agent_framework_copilotstudio/_agent.py +++ b/python/packages/copilotstudio/agent_framework_copilotstudio/_agent.py @@ -32,7 +32,8 @@ async def _patched_post_request( ) -> AsyncIterable[Activity]: async with aiohttp.ClientSession() as session, session.post(url, json=data, headers=headers) as response: if response.status != 200: - raise aiohttp.ClientError(f"Error sending request: {response.status}") + detail = await response.text() + raise AgentException(f"Copilot Studio request to {url} failed ({response.status}): {detail}") event_type = None buffer = b"" async for chunk in response.content.iter_any(): @@ -49,6 +50,18 @@ async def _patched_post_request( self._current_conversation_id = activity.conversation.id # pyright: ignore[reportPrivateUsage] yield activity + if buffer: + line = buffer + if line.startswith(b"event:"): + event_type = line[6:].decode("utf-8").strip() + if line.startswith(b"data:") and event_type == "activity": + activity_data = line[5:].decode("utf-8").strip() + activity = Activity.model_validate_json(activity_data) + + if activity.type == ActivityTypes.message: + self._current_conversation_id = activity.conversation.id # pyright: ignore[reportPrivateUsage] + + yield activity # Monkeypatch CopilotClient to handle data lines larger than 512KB without LineTooLong errors. diff --git a/python/packages/copilotstudio/tests/test_line_too_long_bug.py b/python/packages/copilotstudio/tests/test_line_too_long_bug.py index 6f9e8f9e98f..bb0d3d1b9a9 100644 --- a/python/packages/copilotstudio/tests/test_line_too_long_bug.py +++ b/python/packages/copilotstudio/tests/test_line_too_long_bug.py @@ -6,7 +6,10 @@ from aiohttp import web from microsoft_agents.activity import Activity from microsoft_agents.copilotstudio.client import ConnectionSettings, CopilotClient -from agent_framework_copilotstudio import CopilotStudioAgent + +import agent_framework_copilotstudio # noqa: F401 + +# Side-effect import: applies CopilotClient.post_request monkeypatch. async def mock_server_handler(request: web.Request) -> web.StreamResponse: From 4316e0ba80f5703016eb993ad5532ad1b678adab Mon Sep 17 00:00:00 2001 From: Sachin Mahajan Date: Tue, 28 Jul 2026 08:25:32 +0530 Subject: [PATCH 3/3] python: fix prek syntax checks and PEP8 formatting --- python/packages/copilotstudio/tests/test_line_too_long_bug.py | 2 +- .../packages/core/tests/core/test_function_invocation_logic.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/python/packages/copilotstudio/tests/test_line_too_long_bug.py b/python/packages/copilotstudio/tests/test_line_too_long_bug.py index bb0d3d1b9a9..95af89359a9 100644 --- a/python/packages/copilotstudio/tests/test_line_too_long_bug.py +++ b/python/packages/copilotstudio/tests/test_line_too_long_bug.py @@ -23,7 +23,7 @@ async def mock_server_handler(request: web.Request) -> web.StreamResponse: # Write event type line await response.write(b"event: activity\n") # Write data line - data_line = f'data: {{"type":"message","text":"{long_data}","conversation":{{"id":"test"}}}}\n'.encode("utf-8") + data_line = f'data: {{"type":"message","text":"{long_data}","conversation":{{"id":"test"}}}}\n'.encode() await response.write(data_line) return response diff --git a/python/packages/core/tests/core/test_function_invocation_logic.py b/python/packages/core/tests/core/test_function_invocation_logic.py index ae01d82a226..b950aab0f06 100644 --- a/python/packages/core/tests/core/test_function_invocation_logic.py +++ b/python/packages/core/tests/core/test_function_invocation_logic.py @@ -2374,6 +2374,7 @@ def test_replace_approval_contents_with_results_allows_reused_call_id_after_comp ("call_reused", "second output"), ] + def test_replace_approval_contents_with_results_uses_result_call_ids_for_placeholders() -> None: from agent_framework._tools import _collect_approval_responses, _replace_approval_contents_with_results