Skip to content

Commit acf29a0

Browse files
pontemontiJohan Brobergclaude
authored
Chat history API for Agent Framework (#132)
* fix(agentframework): address code review comments CRM-003 and CRM-004 - CRM-003: Add turn_context validation in send_chat_history_async method to ensure fail-fast behavior and consistency with docstring - CRM-004: Document empty message filtering behavior in _convert_chat_messages_to_history docstring and elevate log level from DEBUG to WARNING when messages are skipped Note: CRM-001 and CRM-002 (copyright header format) are not applicable because the existing format ("Microsoft. All rights reserved.") is required by the pyproject.toml linter configuration (notice-rgx), while CLAUDE.md specifies a different format. The linter-enforced format takes precedence to ensure CI passes. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: update copyright notices to reflect Microsoft Corporation and include license information * fix(mcp_tool_registration_service): add warning for messages with missing role during conversion * Code review fixes/pr 132 (#134) * refactor(agentframework): remove _async suffix from method names (CRM-010) Rename methods to follow Python conventions and codebase patterns: - send_chat_history_messages_async -> send_chat_history_messages - send_chat_history_async -> send_chat_history_from_store Rename test file accordingly: - test_send_chat_history_async.py -> test_send_chat_history.py Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(agentframework): improve role handling and logging (CRM-003, CRM-009, CRM-013) - CRM-003: Add defensive handling for role value access using hasattr check - CRM-009: Convert debug and warning logging to lazy format (% style) - CRM-013: Simplify redundant empty content check (remove `not content or`) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * test(agentframework): add missing test coverage (CRM-001, 004, 005, 006, 011, 012) - CRM-001: Add test for ChatMessageStore exception propagation - CRM-004: Add test for whitespace-only content filtering - CRM-005: Add test for None role handling - CRM-006: Add test for all messages filtered out scenario - CRM-011: Add test for default ToolOptions creation - CRM-012: Make UUID assertion more robust using uuid.UUID() Also adds test for defensive role handling (string role without .value) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(tooling): return success for empty chat history list (CRM-008) The core send_chat_history method now returns OperationResult.success() for empty lists instead of raising ValueError. This is consistent with the extension behavior and makes the API more forgiving. Updated test to verify new behavior. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * docs(agentframework): add Chat History API documentation (CRM-002) Update design.md to include: - send_chat_history_messages and send_chat_history_from_store methods - Parameter tables for both methods - Integration flow diagram showing message conversion - Message filtering behavior documentation - Example usage code Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * docs: add async method naming convention to CLAUDE.md (CRM-010) Document that _async suffix should NOT be used on async methods in this SDK since we only provide async versions. This prevents future naming inconsistencies. 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> --------- Co-authored-by: Johan Broberg <johanb@microsoft.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 9f1b1c0 commit acf29a0

9 files changed

Lines changed: 854 additions & 14 deletions

File tree

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ Place it before imports with one blank line after.
173173
- Use explicit `None` checks: `if x is not None:` not `if x:`
174174
- Local imports should be moved to top of file
175175
- Return defensive copies of mutable data to protect singletons
176+
- **Async method naming**: Do NOT use `_async` suffix on async methods. The `_async` suffix is only appropriate when providing both sync and async versions of the same method. Since this SDK is async-only, use plain method names (e.g., `send_chat_history_messages` not `send_chat_history_messages_async`)
176177

177178
### Type Hints - NEVER Use `Any`
178179

libraries/microsoft-agents-a365-tooling-extensions-agentframework/docs/design.md

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,87 @@ mcp_tool = MCPStreamableHTTPTool(
7676
)
7777
```
7878

79+
### Chat History API
80+
81+
The service provides methods to send chat history to the MCP platform for real-time threat protection analysis. This enables security scanning of conversation content.
82+
83+
#### send_chat_history_messages
84+
85+
The primary method for sending chat history. Converts Agent Framework `ChatMessage` objects to the `ChatHistoryMessage` format expected by the MCP platform.
86+
87+
```python
88+
from agent_framework import ChatMessage, Role
89+
90+
service = McpToolRegistrationService()
91+
92+
# Create messages
93+
messages = [
94+
ChatMessage(role=Role.USER, text="Hello, how are you?"),
95+
ChatMessage(role=Role.ASSISTANT, text="I'm doing well, thank you!"),
96+
]
97+
98+
# Send to MCP platform for threat protection
99+
result = await service.send_chat_history_messages(messages, turn_context)
100+
101+
if result.succeeded:
102+
print("Chat history sent successfully")
103+
else:
104+
print(f"Failed: {result.errors}")
105+
```
106+
107+
#### send_chat_history_from_store
108+
109+
A convenience method that extracts messages from a `ChatMessageStoreProtocol` and delegates to `send_chat_history_messages`.
110+
111+
```python
112+
# Using a ChatMessageStore directly
113+
result = await service.send_chat_history_from_store(
114+
thread.chat_message_store,
115+
turn_context
116+
)
117+
```
118+
119+
#### Chat History API Parameters
120+
121+
| Method | Parameter | Type | Description |
122+
|--------|-----------|------|-------------|
123+
| `send_chat_history_messages` | `chat_messages` | `Sequence[ChatMessage]` | Messages to send |
124+
| | `turn_context` | `TurnContext` | Conversation context |
125+
| | `tool_options` | `ToolOptions \| None` | Optional configuration |
126+
| `send_chat_history_from_store` | `chat_message_store` | `ChatMessageStoreProtocol` | Message store |
127+
| | `turn_context` | `TurnContext` | Conversation context |
128+
| | `tool_options` | `ToolOptions \| None` | Optional configuration |
129+
130+
#### Chat History Integration Flow
131+
132+
```
133+
Agent Framework ChatMessage objects
134+
135+
136+
McpToolRegistrationService.send_chat_history_messages()
137+
138+
├── Convert ChatMessage → ChatHistoryMessage
139+
│ ├── Extract role via .value property
140+
│ ├── Generate UUID if message_id is None
141+
│ ├── Filter out empty/whitespace content
142+
│ └── Filter out None roles
143+
144+
145+
McpToolServerConfigurationService.send_chat_history()
146+
147+
148+
MCP Platform Real-Time Threat Protection Endpoint
149+
```
150+
151+
#### Message Filtering Behavior
152+
153+
The conversion process filters out invalid messages:
154+
- Messages with `None` role are skipped (logged at WARNING level)
155+
- Messages with empty or whitespace-only content are skipped
156+
- If all messages are filtered out, the method returns success without calling the backend
157+
158+
This ensures only valid, meaningful messages are sent for threat analysis.
159+
79160
## File Structure
80161

81162
```

libraries/microsoft-agents-a365-tooling-extensions-agentframework/microsoft_agents_a365/tooling/extensions/agentframework/services/mcp_tool_registration_service.py

Lines changed: 186 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,24 @@
11
# Copyright (c) Microsoft Corporation.
22
# Licensed under the MIT License.
33

4-
from typing import Optional, List, Any, Union
54
import logging
5+
import uuid
6+
from datetime import datetime, timezone
7+
from typing import Any, List, Optional, Sequence, Union
68

7-
from agent_framework import ChatAgent, MCPStreamableHTTPTool
9+
from agent_framework import ChatAgent, ChatMessage, ChatMessageStoreProtocol, MCPStreamableHTTPTool
810
from agent_framework.azure import AzureOpenAIChatClient
911
from agent_framework.openai import OpenAIChatClient
1012

1113
from microsoft_agents.hosting.core import Authorization, TurnContext
1214

15+
from microsoft_agents_a365.runtime import OperationResult
1316
from microsoft_agents_a365.runtime.utility import Utility
17+
from microsoft_agents_a365.tooling.models import ChatHistoryMessage, ToolOptions
1418
from microsoft_agents_a365.tooling.services.mcp_tool_server_configuration_service import (
1519
McpToolServerConfigurationService,
1620
)
17-
from microsoft_agents_a365.tooling.models import ToolOptions
1821
from microsoft_agents_a365.tooling.utils.constants import Constants
19-
2022
from microsoft_agents_a365.tooling.utils.utility import (
2123
get_mcp_platform_authentication_scope,
2224
)
@@ -148,6 +150,186 @@ async def add_tool_servers_to_agent(
148150
self._logger.error(f"Failed to add tool servers to agent: {ex}")
149151
raise
150152

153+
def _convert_chat_messages_to_history(
154+
self,
155+
chat_messages: Sequence[ChatMessage],
156+
) -> List[ChatHistoryMessage]:
157+
"""
158+
Convert Agent Framework ChatMessage objects to ChatHistoryMessage format.
159+
160+
This internal helper method transforms Agent Framework's native ChatMessage
161+
objects into the ChatHistoryMessage format expected by the MCP platform's
162+
real-time threat protection endpoint.
163+
164+
Args:
165+
chat_messages: Sequence of ChatMessage objects to convert.
166+
167+
Returns:
168+
List of ChatHistoryMessage objects ready for the MCP platform.
169+
170+
Note:
171+
- If message_id is None, a new UUID is generated
172+
- Role is extracted via the .value property of the Role object
173+
- Timestamp is set to current UTC time (ChatMessage has no timestamp)
174+
- Messages with empty or whitespace-only content are filtered out and
175+
logged at WARNING level. This is because ChatHistoryMessage requires
176+
non-empty content for validation. The filtered messages will not be
177+
sent to the MCP platform.
178+
"""
179+
history_messages: List[ChatHistoryMessage] = []
180+
current_time = datetime.now(timezone.utc)
181+
182+
for msg in chat_messages:
183+
message_id = msg.message_id if msg.message_id is not None else str(uuid.uuid4())
184+
if msg.role is None:
185+
self._logger.warning(
186+
"Skipping message %s with missing role during conversion", message_id
187+
)
188+
continue
189+
# Defensive handling: use .value if role is an enum, otherwise convert to string
190+
role = msg.role.value if hasattr(msg.role, "value") else str(msg.role)
191+
content = msg.text if msg.text is not None else ""
192+
193+
# Skip messages with empty content as ChatHistoryMessage validates non-empty content
194+
if not content.strip():
195+
self._logger.warning(
196+
"Skipping message %s with empty content during conversion", message_id
197+
)
198+
continue
199+
200+
history_message = ChatHistoryMessage(
201+
id=message_id,
202+
role=role,
203+
content=content,
204+
timestamp=current_time,
205+
)
206+
history_messages.append(history_message)
207+
208+
self._logger.debug(
209+
"Converted message %s with role '%s' to ChatHistoryMessage", message_id, role
210+
)
211+
212+
return history_messages
213+
214+
async def send_chat_history_messages(
215+
self,
216+
chat_messages: Sequence[ChatMessage],
217+
turn_context: TurnContext,
218+
tool_options: Optional[ToolOptions] = None,
219+
) -> OperationResult:
220+
"""
221+
Send chat history messages to the MCP platform for real-time threat protection.
222+
223+
This is the primary implementation method that handles message conversion
224+
and delegation to the core tooling service.
225+
226+
Args:
227+
chat_messages: Sequence of Agent Framework ChatMessage objects to send.
228+
turn_context: TurnContext from the Agents SDK containing conversation info.
229+
tool_options: Optional configuration for the request. Defaults to
230+
AgentFramework-specific options if not provided.
231+
232+
Returns:
233+
OperationResult indicating success or failure of the operation.
234+
235+
Raises:
236+
ValueError: If chat_messages or turn_context is None.
237+
238+
Example:
239+
>>> service = McpToolRegistrationService()
240+
>>> messages = [ChatMessage(role=Role.USER, text="Hello")]
241+
>>> result = await service.send_chat_history_messages(messages, turn_context)
242+
>>> if result.succeeded:
243+
... print("Chat history sent successfully")
244+
"""
245+
# Input validation
246+
if chat_messages is None:
247+
raise ValueError("chat_messages cannot be None")
248+
249+
if turn_context is None:
250+
raise ValueError("turn_context cannot be None")
251+
252+
# Handle empty messages - return success with warning
253+
if len(chat_messages) == 0:
254+
self._logger.warning("Empty message list provided to send_chat_history_messages")
255+
return OperationResult.success()
256+
257+
self._logger.info(f"Send chat history initiated with {len(chat_messages)} messages")
258+
259+
# Use default options if not provided
260+
if tool_options is None:
261+
tool_options = ToolOptions(orchestrator_name=self._orchestrator_name)
262+
263+
# Convert messages to ChatHistoryMessage format
264+
history_messages = self._convert_chat_messages_to_history(chat_messages)
265+
266+
# Check if all messages were filtered out during conversion
267+
if len(history_messages) == 0:
268+
self._logger.warning("All messages were filtered out during conversion (empty content)")
269+
return OperationResult.success()
270+
271+
# Delegate to core service
272+
result = await self._mcp_server_configuration_service.send_chat_history(
273+
turn_context=turn_context,
274+
chat_history_messages=history_messages,
275+
options=tool_options,
276+
)
277+
278+
if result.succeeded:
279+
self._logger.info(
280+
f"Chat history sent successfully with {len(history_messages)} messages"
281+
)
282+
else:
283+
self._logger.error(f"Failed to send chat history: {result}")
284+
285+
return result
286+
287+
async def send_chat_history_from_store(
288+
self,
289+
chat_message_store: ChatMessageStoreProtocol,
290+
turn_context: TurnContext,
291+
tool_options: Optional[ToolOptions] = None,
292+
) -> OperationResult:
293+
"""
294+
Send chat history from a ChatMessageStore to the MCP platform.
295+
296+
This is a convenience method that extracts messages from the store
297+
and delegates to send_chat_history_messages().
298+
299+
Args:
300+
chat_message_store: ChatMessageStore containing the conversation history.
301+
turn_context: TurnContext from the Agents SDK containing conversation info.
302+
tool_options: Optional configuration for the request.
303+
304+
Returns:
305+
OperationResult indicating success or failure of the operation.
306+
307+
Raises:
308+
ValueError: If chat_message_store or turn_context is None.
309+
310+
Example:
311+
>>> service = McpToolRegistrationService()
312+
>>> result = await service.send_chat_history_from_store(
313+
... thread.chat_message_store, turn_context
314+
... )
315+
"""
316+
# Input validation
317+
if chat_message_store is None:
318+
raise ValueError("chat_message_store cannot be None")
319+
320+
if turn_context is None:
321+
raise ValueError("turn_context cannot be None")
322+
323+
# Extract messages from the store
324+
messages = await chat_message_store.list_messages()
325+
326+
# Delegate to the primary implementation
327+
return await self.send_chat_history_messages(
328+
chat_messages=messages,
329+
turn_context=turn_context,
330+
tool_options=tool_options,
331+
)
332+
151333
async def cleanup(self):
152334
"""Clean up any resources used by the service."""
153335
try:

libraries/microsoft-agents-a365-tooling/microsoft_agents_a365/tooling/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,6 @@
2727
"get_mcp_base_url",
2828
"build_mcp_server_url",
2929
]
30+
31+
# Enable namespace package extension for tooling-extensions-* packages
32+
__path__ = __import__("pkgutil").extend_path(__path__, __name__)

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -599,8 +599,13 @@ async def send_chat_history(
599599
# Validate input parameters
600600
if turn_context is None:
601601
raise ValueError("turn_context cannot be None")
602-
if chat_history_messages is None or len(chat_history_messages) == 0:
603-
raise ValueError("chat_history_messages cannot be None or empty")
602+
if chat_history_messages is None:
603+
raise ValueError("chat_history_messages cannot be None")
604+
605+
# Handle empty messages - return success with warning (consistent with extension behavior)
606+
if len(chat_history_messages) == 0:
607+
self._logger.warning("Empty message list provided to send_chat_history")
608+
return OperationResult.success()
604609

605610
# Extract required information from turn context
606611
if not turn_context.activity:
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.
3+
4+
"""Test package for Agent Framework tooling extensions."""
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.
3+
4+
"""Test package for Agent Framework tooling extension services."""

0 commit comments

Comments
 (0)