Skip to content

Commit 7dd9d09

Browse files
authored
Merge branch 'main' into main
2 parents f7ea187 + 56e2831 commit 7dd9d09

File tree

8 files changed

+2878
-6
lines changed

8 files changed

+2878
-6
lines changed

docs/prd/azure-ai-foundry-chat-history-api-tasks.md

Lines changed: 703 additions & 0 deletions
Large diffs are not rendered by default.

docs/prd/azure-ai-foundry-chat-history-api.md

Lines changed: 878 additions & 0 deletions
Large diffs are not rendered by default.

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

Lines changed: 256 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,21 @@
1111

1212
# Standard library imports
1313
import logging
14-
from typing import Optional, List, Tuple
14+
from typing import List, Optional, Sequence, Tuple
1515

1616
# Third-party imports - Azure AI
17+
from azure.ai.agents import AgentsClient
18+
from azure.ai.agents.models import McpTool, ThreadMessage, ToolResources
1719
from azure.ai.projects import AIProjectClient
1820
from azure.identity import DefaultAzureCredential
19-
from azure.ai.agents.models import McpTool, ToolResources
2021
from microsoft_agents.hosting.core import Authorization, TurnContext
22+
23+
from microsoft_agents_a365.runtime import OperationError, OperationResult
2124
from microsoft_agents_a365.runtime.utility import Utility
25+
from microsoft_agents_a365.tooling.models import ChatHistoryMessage, ToolOptions
2226
from microsoft_agents_a365.tooling.services.mcp_tool_server_configuration_service import (
2327
McpToolServerConfigurationService,
2428
)
25-
from microsoft_agents_a365.tooling.models import ToolOptions
2629
from microsoft_agents_a365.tooling.utils.constants import Constants
2730
from microsoft_agents_a365.tooling.utils.utility import get_mcp_platform_authentication_scope
2831

@@ -217,3 +220,253 @@ async def _get_mcp_tool_definitions_and_resources(
217220
)
218221

219222
return (tool_definitions, combined_tool_resources)
223+
224+
# ============================================================================
225+
# Public Methods - Chat History API
226+
# ============================================================================
227+
228+
async def send_chat_history_messages(
229+
self,
230+
turn_context: TurnContext,
231+
messages: Sequence[ThreadMessage],
232+
tool_options: Optional[ToolOptions] = None,
233+
) -> OperationResult:
234+
"""
235+
Send Azure AI Foundry chat history messages to the MCP platform.
236+
237+
This method accepts a sequence of Azure AI Foundry ThreadMessage objects,
238+
converts them to ChatHistoryMessage format, and sends them to the MCP
239+
platform for real-time threat protection.
240+
241+
Args:
242+
turn_context: TurnContext from the Agents SDK containing conversation info.
243+
messages: Sequence of Azure AI Foundry ThreadMessage objects to send.
244+
tool_options: Optional configuration for the request.
245+
246+
Returns:
247+
OperationResult indicating success or failure.
248+
249+
Raises:
250+
ValueError: If turn_context or messages is None.
251+
252+
Example:
253+
>>> service = McpToolRegistrationService()
254+
>>> messages = await agents_client.messages.list(thread_id=thread_id)
255+
>>> result = await service.send_chat_history_messages(
256+
... turn_context, list(messages)
257+
... )
258+
>>> if result.succeeded:
259+
... print("Chat history sent successfully")
260+
"""
261+
# Input validation
262+
if turn_context is None:
263+
raise ValueError("turn_context cannot be None")
264+
if messages is None:
265+
raise ValueError("messages cannot be None")
266+
267+
self._logger.info(f"Sending {len(messages)} Azure AI Foundry messages as chat history")
268+
269+
# Set default options with orchestrator name
270+
if tool_options is None:
271+
tool_options = ToolOptions(orchestrator_name=self._orchestrator_name)
272+
elif tool_options.orchestrator_name is None:
273+
tool_options.orchestrator_name = self._orchestrator_name
274+
275+
try:
276+
# Convert ThreadMessage objects to ChatHistoryMessage format
277+
chat_history_messages = self._convert_thread_messages_to_chat_history(messages)
278+
279+
self._logger.debug(
280+
f"Converted {len(chat_history_messages)} messages to ChatHistoryMessage format"
281+
)
282+
283+
# Delegate to core service
284+
result = await self._mcp_server_configuration_service.send_chat_history(
285+
turn_context=turn_context,
286+
chat_history_messages=chat_history_messages,
287+
options=tool_options,
288+
)
289+
290+
if result.succeeded:
291+
self._logger.info(
292+
f"Chat history sent successfully with {len(chat_history_messages)} messages"
293+
)
294+
else:
295+
self._logger.error(f"Failed to send chat history: {result}")
296+
297+
return result
298+
299+
except ValueError:
300+
# Re-raise validation errors from the core service
301+
raise
302+
except Exception as ex:
303+
self._logger.error(f"Failed to send chat history messages: {ex}")
304+
return OperationResult.failed(OperationError(ex))
305+
306+
async def send_chat_history(
307+
self,
308+
agents_client: AgentsClient,
309+
thread_id: str,
310+
turn_context: TurnContext,
311+
tool_options: Optional[ToolOptions] = None,
312+
) -> OperationResult:
313+
"""
314+
Retrieve and send chat history from Azure AI Foundry to the MCP platform.
315+
316+
This method retrieves messages from the Azure AI Foundry Agents API using
317+
the provided client and thread ID, converts them to ChatHistoryMessage
318+
format, and sends them to the MCP platform.
319+
320+
Args:
321+
agents_client: The Azure AI Foundry AgentsClient instance.
322+
thread_id: The thread ID containing the messages to send.
323+
turn_context: TurnContext from the Agents SDK containing conversation info.
324+
tool_options: Optional configuration for the request.
325+
326+
Returns:
327+
OperationResult indicating success or failure.
328+
329+
Raises:
330+
ValueError: If agents_client, thread_id, or turn_context is None/empty.
331+
332+
Example:
333+
>>> from azure.ai.agents import AgentsClient
334+
>>> from azure.identity import DefaultAzureCredential
335+
>>>
336+
>>> client = AgentsClient(endpoint, credential=DefaultAzureCredential())
337+
>>> service = McpToolRegistrationService()
338+
>>> result = await service.send_chat_history(
339+
... client, thread_id, turn_context
340+
... )
341+
"""
342+
# Input validation
343+
if agents_client is None:
344+
raise ValueError("agents_client cannot be None")
345+
if thread_id is None or not thread_id.strip():
346+
raise ValueError("thread_id cannot be empty")
347+
if turn_context is None:
348+
raise ValueError("turn_context cannot be None")
349+
350+
try:
351+
# Retrieve messages from the thread
352+
messages: List[ThreadMessage] = []
353+
async for message in agents_client.messages.list(thread_id=thread_id):
354+
messages.append(message)
355+
356+
self._logger.info(f"Retrieved {len(messages)} messages from thread {thread_id}")
357+
358+
# Delegate to send_chat_history_messages
359+
return await self.send_chat_history_messages(
360+
turn_context=turn_context,
361+
messages=messages,
362+
tool_options=tool_options,
363+
)
364+
365+
except ValueError:
366+
# Re-raise validation errors
367+
raise
368+
except Exception as ex:
369+
self._logger.error(f"Failed to send chat history from thread {thread_id}: {ex}")
370+
return OperationResult.failed(OperationError(ex))
371+
372+
# ============================================================================
373+
# Private Methods - Message Conversion Helpers
374+
# ============================================================================
375+
376+
def _convert_thread_messages_to_chat_history(
377+
self,
378+
messages: Sequence[ThreadMessage],
379+
) -> List[ChatHistoryMessage]:
380+
"""
381+
Convert Azure AI Foundry ThreadMessage objects to ChatHistoryMessage format.
382+
383+
This internal helper method transforms Azure AI Foundry's native ThreadMessage
384+
objects into the ChatHistoryMessage format expected by the MCP platform's
385+
real-time threat protection endpoint.
386+
387+
Args:
388+
messages: Sequence of ThreadMessage objects to convert.
389+
390+
Returns:
391+
List of ChatHistoryMessage objects ready for the MCP platform.
392+
393+
Note:
394+
- Messages with None id, None role, or empty content are filtered out
395+
- Role is extracted via the .value property of the MessageRole enum
396+
- Timestamp is taken from message.created_at
397+
"""
398+
history_messages: List[ChatHistoryMessage] = []
399+
400+
for message in messages:
401+
# Skip None messages
402+
if message is None:
403+
self._logger.warning("Skipping null message")
404+
continue
405+
406+
# Skip messages with None id
407+
if message.id is None:
408+
self._logger.warning("Skipping message with null ID")
409+
continue
410+
411+
# Skip messages with None role
412+
if message.role is None:
413+
self._logger.warning(f"Skipping message with null role (ID: {message.id})")
414+
continue
415+
416+
# Extract content from message
417+
content = self._extract_content_from_message(message)
418+
419+
# Skip messages with empty content
420+
if not content or not content.strip():
421+
self._logger.warning(f"Skipping message {message.id} with empty content")
422+
continue
423+
424+
# Convert role enum to lowercase string
425+
role_value = message.role.value if hasattr(message.role, "value") else str(message.role)
426+
role = role_value.lower()
427+
428+
# Create ChatHistoryMessage
429+
history_message = ChatHistoryMessage(
430+
id=message.id,
431+
role=role,
432+
content=content,
433+
timestamp=message.created_at,
434+
)
435+
history_messages.append(history_message)
436+
437+
self._logger.debug(
438+
f"Converted message {message.id} with role '{role}' to ChatHistoryMessage"
439+
)
440+
441+
if len(history_messages) == 0 and len(messages) > 0:
442+
self._logger.warning("All messages were filtered out during conversion")
443+
444+
return history_messages
445+
446+
def _extract_content_from_message(self, message: ThreadMessage) -> str:
447+
"""
448+
Extract text content from a ThreadMessage's content items.
449+
450+
This method iterates through the message's content list and extracts
451+
text from MessageTextContent items, concatenating them with spaces.
452+
453+
Args:
454+
message: Azure AI Foundry ThreadMessage object.
455+
456+
Returns:
457+
Concatenated text content as string, or empty string if no text found.
458+
"""
459+
if message.content is None or len(message.content) == 0:
460+
return ""
461+
462+
text_parts: List[str] = []
463+
464+
for content_item in message.content:
465+
# Check for MessageTextContent by duck typing (has text attribute with value)
466+
# This handles both real SDK types and mock objects in tests
467+
if hasattr(content_item, "text") and content_item.text is not None:
468+
text_value = getattr(content_item.text, "value", None)
469+
if text_value is not None and text_value:
470+
text_parts.append(text_value)
471+
472+
return " ".join(text_parts)
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.

0 commit comments

Comments
 (0)