Skip to content

Commit b3e88ff

Browse files
pontemontiJohan Brobergclaude
authored
Code review fixes/pr 105 v3 (#115)
* fix(runtime): implement thread-safe singleton with eager initialization Convert OperationResult.success() singleton from lazy to eager initialization at module level. Python's import lock ensures thread-safe initialization, eliminating the race condition in the previous check-and-create pattern. Addresses CRM-001 from code review. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(tooling): include error response body in HTTP error log message Add truncated error_text (max 500 chars) to the log message when HTTP errors occur in send_chat_history. This improves debugging by showing the actual error response from the MCP platform. Addresses CRM-002 from code review. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(tooling): reorder exception handlers for proper timeout handling Move asyncio.TimeoutError handler before aiohttp.ClientError to ensure timeouts are caught correctly. Since aiohttp.ServerTimeoutError inherits from both exceptions, the previous order could misclassify timeouts. Addresses CRM-003 from code review. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor(tooling): add type hints for local variables in send_chat_history Add Optional[str] type annotations for conversation_id, message_id, and user_message variables to improve code clarity and IDE support. Addresses CRM-004 from code review. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * docs(tooling): improve validation error messages in ChatHistoryMessage Update error messages to say "cannot be empty or whitespace-only" instead of just "cannot be empty" for clearer feedback when whitespace validation fails. Addresses CRM-005 from code review. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor(tooling): extract timeout and HTTP status code to constants Add DEFAULT_REQUEST_TIMEOUT_SECONDS and HTTP_STATUS_OK module-level constants to replace magic values. Improves maintainability and makes configuration easier to modify. Addresses CRM-006 from code review. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * docs(runtime): enhance defensive copy docstring in OperationResult.errors Move the defensive copy rationale into a prominent Note section in the docstring to make it more visible to developers. This clarifies why the property returns a copy rather than the internal list. Addresses CRM-007 from code review. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * test(tooling): use Mock(spec=TurnContext) for stricter interface validation Import TurnContext and use it as a spec for the mock fixture. This ensures the mock matches the actual TurnContext interface, catching potential issues if the API changes. Addresses CRM-008 from code review. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * docs(tooling): add usage example to send_chat_history docstring Add an Example section showing how to create ChatHistoryMessage objects and call send_chat_history with proper error handling. This helps developers understand the intended usage pattern. Addresses CRM-009 from code review. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Johan Broberg <johanb@microsoft.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 8f9b0d4 commit b3e88ff

4 files changed

Lines changed: 55 additions & 19 deletions

File tree

libraries/microsoft-agents-a365-runtime/microsoft_agents_a365/runtime/operation_result.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,13 @@ def errors(self) -> List[OperationError]:
4646
"""
4747
Get the list of errors that occurred during the operation.
4848
49+
Note:
50+
This property returns a defensive copy of the internal error list
51+
to prevent external modifications, which is especially important for
52+
protecting the singleton instance returned by success().
53+
4954
Returns:
5055
List[OperationError]: A copy of the list of operation errors.
51-
The returned list is a defensive copy to protect the singleton
52-
instance returned by success() from accidental modification.
5356
"""
5457
return list(self._errors)
5558

@@ -61,8 +64,6 @@ def success() -> "OperationResult":
6164
Returns:
6265
OperationResult: An OperationResult indicating a successful operation.
6366
"""
64-
if OperationResult._success_instance is None:
65-
OperationResult._success_instance = OperationResult(succeeded=True)
6667
return OperationResult._success_instance
6768

6869
@staticmethod
@@ -91,3 +92,7 @@ def __str__(self) -> str:
9192
else:
9293
error_messages = ", ".join(str(error.message) for error in self._errors)
9394
return f"Failed: {error_messages}" if error_messages else "Failed"
95+
96+
97+
# Module-level eager initialization (thread-safe by Python's import lock)
98+
OperationResult._success_instance = OperationResult(succeeded=True)

libraries/microsoft-agents-a365-tooling/microsoft_agents_a365/tooling/models/chat_history_message.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,11 @@ def __post_init__(self):
4242
or if timestamp is None.
4343
"""
4444
if not self.id or not self.id.strip():
45-
raise ValueError("id cannot be empty")
45+
raise ValueError("id cannot be empty or whitespace-only")
4646
if not self.role or not self.role.strip():
47-
raise ValueError("role cannot be empty")
47+
raise ValueError("role cannot be empty or whitespace-only")
4848
if not self.content or not self.content.strip():
49-
raise ValueError("content cannot be empty")
49+
raise ValueError("content cannot be empty or whitespace-only")
5050
if self.timestamp is None:
5151
raise ValueError("timestamp cannot be None")
5252

libraries/microsoft-agents-a365-tooling/microsoft_agents_a365/tooling/services/mcp_tool_server_configuration_service.py

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,17 @@
4545
from microsoft_agents_a365.runtime.utility import Utility as RuntimeUtility
4646

4747

48+
# ==============================================================================
49+
# CONSTANTS
50+
# ==============================================================================
51+
52+
# HTTP timeout in seconds for request operations
53+
DEFAULT_REQUEST_TIMEOUT_SECONDS = 30
54+
55+
# HTTP status code for successful response
56+
HTTP_STATUS_OK = 200
57+
58+
4859
# ==============================================================================
4960
# MAIN SERVICE CLASS
5061
# ==============================================================================
@@ -532,6 +543,20 @@ async def send_chat_history(
532543
ValueError: If turn_context is None, chat_history_messages is None or empty,
533544
turn_context.activity is None, or any of the required fields
534545
(conversation.id, activity.id, activity.text) are missing or empty.
546+
547+
Example:
548+
>>> from datetime import datetime, timezone
549+
>>> from microsoft_agents_a365.tooling.models import ChatHistoryMessage
550+
>>>
551+
>>> history = [
552+
... ChatHistoryMessage("msg-1", "user", "Hello", datetime.now(timezone.utc)),
553+
... ChatHistoryMessage("msg-2", "assistant", "Hi!", datetime.now(timezone.utc))
554+
... ]
555+
>>>
556+
>>> service = McpToolServerConfigurationService()
557+
>>> result = await service.send_chat_history(turn_context, history)
558+
>>> if result.succeeded:
559+
... print("Chat history sent successfully")
535560
"""
536561
# Validate input parameters
537562
if turn_context is None:
@@ -543,13 +568,15 @@ async def send_chat_history(
543568
if not turn_context.activity:
544569
raise ValueError("turn_context.activity cannot be None")
545570

546-
conversation_id = (
571+
conversation_id: Optional[str] = (
547572
turn_context.activity.conversation.id if turn_context.activity.conversation else None
548573
)
549-
message_id = turn_context.activity.id
550-
user_message = turn_context.activity.text
574+
message_id: Optional[str] = turn_context.activity.id
575+
user_message: Optional[str] = turn_context.activity.text
551576

552-
if conversation_id is None or (isinstance(conversation_id, str) and not conversation_id.strip()):
577+
if conversation_id is None or (
578+
isinstance(conversation_id, str) and not conversation_id.strip()
579+
):
553580
raise ValueError(
554581
"conversation_id cannot be empty or None (from turn_context.activity.conversation.id)"
555582
)
@@ -592,16 +619,17 @@ async def send_chat_history(
592619
json_data = json.dumps(request.to_dict())
593620

594621
# Send POST request with timeout to prevent indefinite hangs
595-
timeout = aiohttp.ClientTimeout(total=30) # 30 second timeout
622+
timeout = aiohttp.ClientTimeout(total=DEFAULT_REQUEST_TIMEOUT_SECONDS)
596623
async with aiohttp.ClientSession(timeout=timeout) as session:
597624
async with session.post(endpoint, headers=headers, data=json_data) as response:
598-
if response.status == 200:
625+
if response.status == HTTP_STATUS_OK:
599626
self._logger.info("Successfully sent chat history to MCP platform")
600627
return OperationResult.success()
601628
else:
602629
error_text = await response.text()
603630
self._logger.error(
604-
f"HTTP error sending chat history: HTTP {response.status}"
631+
f"HTTP error sending chat history: HTTP {response.status}. "
632+
f"Response: {error_text[:500]}"
605633
)
606634
# Use ClientResponseError for consistent error handling
607635
http_error = aiohttp.ClientResponseError(
@@ -613,14 +641,16 @@ async def send_chat_history(
613641
)
614642
return OperationResult.failed(OperationError(http_error))
615643

616-
except aiohttp.ClientError as http_ex:
617-
self._logger.error(f"HTTP error sending chat history to '{endpoint}': {str(http_ex)}")
618-
return OperationResult.failed(OperationError(http_ex))
619644
except asyncio.TimeoutError as timeout_ex:
645+
# Catch TimeoutError before ClientError since aiohttp.ServerTimeoutError
646+
# inherits from both asyncio.TimeoutError and aiohttp.ClientError
620647
self._logger.error(
621648
f"Request timeout sending chat history to '{endpoint}': {str(timeout_ex)}"
622649
)
623650
return OperationResult.failed(OperationError(timeout_ex))
651+
except aiohttp.ClientError as http_ex:
652+
self._logger.error(f"HTTP error sending chat history to '{endpoint}': {str(http_ex)}")
653+
return OperationResult.failed(OperationError(http_ex))
624654
except Exception as ex:
625655
self._logger.error(f"Failed to send chat history to '{endpoint}': {str(ex)}")
626656
return OperationResult.failed(OperationError(ex))

tests/tooling/services/test_send_chat_history.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from unittest.mock import AsyncMock, MagicMock, Mock, patch
88

99
import pytest
10+
from microsoft_agents.hosting.core import TurnContext
1011
from microsoft_agents_a365.tooling.models import ChatHistoryMessage
1112
from microsoft_agents_a365.tooling.services import McpToolServerConfigurationService
1213

@@ -16,8 +17,8 @@ class TestSendChatHistory:
1617

1718
@pytest.fixture
1819
def mock_turn_context(self):
19-
"""Create a mock TurnContext."""
20-
mock_context = Mock()
20+
"""Create a mock TurnContext with spec for stricter interface validation."""
21+
mock_context = Mock(spec=TurnContext)
2122
mock_activity = Mock()
2223
mock_conversation = Mock()
2324

0 commit comments

Comments
 (0)