microsoft-agents-a365-tooling-extensions-semantickernel 0.2.1.dev7__py3-none-any.whl → 0.2.1.dev10__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -13,15 +13,23 @@ servers to agents.
13
13
  import logging
14
14
  import os
15
15
  import re
16
- from typing import Any, Optional
16
+ import uuid
17
+ from datetime import datetime, timezone
18
+ from typing import Any, List, Optional, Sequence
19
+
20
+ # Third-party imports
17
21
  from semantic_kernel import kernel as sk
18
22
  from semantic_kernel.connectors.mcp import MCPStreamableHttpPlugin
23
+ from semantic_kernel.contents import AuthorRole, ChatHistory, ChatMessageContent
24
+
25
+ # Local imports
19
26
  from microsoft_agents.hosting.core import Authorization, TurnContext
27
+ from microsoft_agents_a365.runtime import OperationError, OperationResult
20
28
  from microsoft_agents_a365.runtime.utility import Utility
29
+ from microsoft_agents_a365.tooling.models import ChatHistoryMessage, ToolOptions
21
30
  from microsoft_agents_a365.tooling.services.mcp_tool_server_configuration_service import (
22
31
  McpToolServerConfigurationService,
23
32
  )
24
- from microsoft_agents_a365.tooling.models import ToolOptions
25
33
  from microsoft_agents_a365.tooling.utils.constants import Constants
26
34
  from microsoft_agents_a365.tooling.utils.utility import (
27
35
  get_mcp_platform_authentication_scope,
@@ -119,7 +127,9 @@ class McpToolRegistrationService:
119
127
  for server in servers:
120
128
  try:
121
129
  headers = {
122
- Constants.Headers.AUTHORIZATION: f"{Constants.Headers.BEARER_PREFIX} {auth_token}",
130
+ Constants.Headers.AUTHORIZATION: (
131
+ f"{Constants.Headers.BEARER_PREFIX} {auth_token}"
132
+ ),
123
133
  }
124
134
 
125
135
  headers[Constants.Headers.USER_AGENT] = Utility.get_user_agent_header(
@@ -129,7 +139,8 @@ class McpToolRegistrationService:
129
139
  # Use the URL from server (always populated by the configuration service)
130
140
  server_url = server.url
131
141
 
132
- # Use mcp_server_name if available (not None or empty), otherwise fall back to mcp_server_unique_name
142
+ # Use mcp_server_name if available (not None or empty),
143
+ # otherwise fall back to mcp_server_unique_name
133
144
  server_name = server.mcp_server_name or server.mcp_server_unique_name
134
145
 
135
146
  plugin = MCPStreamableHttpPlugin(
@@ -144,10 +155,11 @@ class McpToolRegistrationService:
144
155
  # Add plugin to kernel
145
156
  kernel.add_plugin(plugin, server_name)
146
157
 
147
- # Store reference to keep plugin alive throughout application lifecycle
148
- # By storing plugin references in _connected_plugins, we prevent Python's garbage collector from cleaning up the plugin objects
149
- # The connections remain active throughout the application lifecycle
150
- # Tools can be successfully invoked because their underlying connections are still alive
158
+ # Store reference to keep plugin alive throughout application lifecycle.
159
+ # By storing plugin references in _connected_plugins, we prevent
160
+ # Python's garbage collector from cleaning up the plugin objects.
161
+ # The connections remain active throughout the application lifecycle.
162
+ # Tools can be invoked because their underlying connections stay alive.
151
163
  self._connected_plugins.append(plugin)
152
164
 
153
165
  self._logger.info(
@@ -181,6 +193,354 @@ class McpToolRegistrationService:
181
193
  clean_name = re.sub(r"[^a-zA-Z0-9_]", "_", server_name)
182
194
  return f"{clean_name}Tools"
183
195
 
196
+ # ============================================================================
197
+ # SEND CHAT HISTORY - Semantic Kernel-specific implementations
198
+ # ============================================================================
199
+
200
+ async def send_chat_history(
201
+ self,
202
+ turn_context: TurnContext,
203
+ chat_history: ChatHistory,
204
+ limit: Optional[int] = None,
205
+ options: Optional[ToolOptions] = None,
206
+ ) -> OperationResult:
207
+ """
208
+ Send Semantic Kernel chat history to the MCP platform.
209
+
210
+ This method extracts messages from a Semantic Kernel ChatHistory object,
211
+ converts them to ChatHistoryMessage format, and sends them to the MCP
212
+ platform for real-time threat protection.
213
+
214
+ Args:
215
+ turn_context: TurnContext from the Agents SDK containing conversation info.
216
+ chat_history: Semantic Kernel ChatHistory object containing messages.
217
+ limit: Optional maximum number of messages to send. If specified,
218
+ sends the most recent N messages. If None, sends all messages.
219
+ options: Optional configuration for the request.
220
+
221
+ Returns:
222
+ OperationResult indicating success or failure.
223
+
224
+ Raises:
225
+ ValueError: If turn_context or chat_history is None.
226
+
227
+ Example:
228
+ >>> from semantic_kernel.contents import ChatHistory
229
+ >>> from microsoft_agents_a365.tooling.extensions.semantickernel import (
230
+ ... McpToolRegistrationService
231
+ ... )
232
+ >>>
233
+ >>> service = McpToolRegistrationService()
234
+ >>> chat_history = ChatHistory()
235
+ >>> chat_history.add_user_message("Hello!")
236
+ >>> chat_history.add_assistant_message("Hi there!")
237
+ >>>
238
+ >>> result = await service.send_chat_history(
239
+ ... turn_context, chat_history, limit=50
240
+ ... )
241
+ >>> if result.succeeded:
242
+ ... print("Chat history sent successfully")
243
+ """
244
+ # Validate inputs
245
+ if turn_context is None:
246
+ raise ValueError("turn_context cannot be None")
247
+ if chat_history is None:
248
+ raise ValueError("chat_history cannot be None")
249
+
250
+ try:
251
+ # Extract messages from ChatHistory
252
+ messages = list(chat_history.messages)
253
+ self._logger.debug(f"Extracted {len(messages)} messages from ChatHistory")
254
+
255
+ # Apply limit if specified
256
+ if limit is not None and limit > 0 and len(messages) > limit:
257
+ self._logger.info(f"Applying limit of {limit} to {len(messages)} messages")
258
+ messages = messages[-limit:] # Take the most recent N messages
259
+
260
+ # Delegate to the list-based method
261
+ return await self.send_chat_history_messages(
262
+ turn_context=turn_context,
263
+ messages=messages,
264
+ options=options,
265
+ )
266
+ except ValueError:
267
+ # Re-raise validation errors
268
+ raise
269
+ except Exception as ex:
270
+ self._logger.error(f"Failed to send chat history: {ex}")
271
+ return OperationResult.failed(OperationError(ex))
272
+
273
+ async def send_chat_history_messages(
274
+ self,
275
+ turn_context: TurnContext,
276
+ messages: Sequence[ChatMessageContent],
277
+ options: Optional[ToolOptions] = None,
278
+ ) -> OperationResult:
279
+ """
280
+ Send Semantic Kernel chat history messages to the MCP platform.
281
+
282
+ This method accepts a sequence of Semantic Kernel ChatMessageContent objects,
283
+ converts them to ChatHistoryMessage format, and sends them to the MCP
284
+ platform for real-time threat protection.
285
+
286
+ Args:
287
+ turn_context: TurnContext from the Agents SDK containing conversation info.
288
+ messages: Sequence of Semantic Kernel ChatMessageContent objects to send.
289
+ options: Optional configuration for the request.
290
+
291
+ Returns:
292
+ OperationResult indicating success or failure.
293
+
294
+ Raises:
295
+ ValueError: If turn_context or messages is None.
296
+
297
+ Example:
298
+ >>> from semantic_kernel.contents import ChatMessageContent, AuthorRole
299
+ >>> from microsoft_agents_a365.tooling.extensions.semantickernel import (
300
+ ... McpToolRegistrationService
301
+ ... )
302
+ >>>
303
+ >>> service = McpToolRegistrationService()
304
+ >>> messages = [
305
+ ... ChatMessageContent(role=AuthorRole.USER, content="Hello!"),
306
+ ... ChatMessageContent(role=AuthorRole.ASSISTANT, content="Hi there!"),
307
+ ... ]
308
+ >>>
309
+ >>> result = await service.send_chat_history_messages(
310
+ ... turn_context, messages
311
+ ... )
312
+ >>> if result.succeeded:
313
+ ... print("Chat history sent successfully")
314
+ """
315
+ # Validate inputs
316
+ if turn_context is None:
317
+ raise ValueError("turn_context cannot be None")
318
+ if messages is None:
319
+ raise ValueError("messages cannot be None")
320
+
321
+ self._logger.info(f"Sending {len(messages)} Semantic Kernel messages as chat history")
322
+
323
+ # Set default options
324
+ if options is None:
325
+ options = ToolOptions(orchestrator_name=self._orchestrator_name)
326
+ elif options.orchestrator_name is None:
327
+ options.orchestrator_name = self._orchestrator_name
328
+
329
+ try:
330
+ # Convert Semantic Kernel messages to ChatHistoryMessage format
331
+ chat_history_messages = self._convert_sk_messages_to_chat_history(messages)
332
+
333
+ # Call core service even with empty chat_history_messages
334
+ if len(chat_history_messages) == 0:
335
+ self._logger.info(
336
+ "Empty chat history messages (either no input or all filtered), "
337
+ "still sending to register user message"
338
+ )
339
+
340
+ self._logger.debug(
341
+ f"Converted {len(chat_history_messages)} messages to ChatHistoryMessage format"
342
+ )
343
+
344
+ # Delegate to core service
345
+ return await self._mcp_server_configuration_service.send_chat_history(
346
+ turn_context=turn_context,
347
+ chat_history_messages=chat_history_messages,
348
+ options=options,
349
+ )
350
+ except ValueError:
351
+ # Re-raise validation errors from the core service
352
+ raise
353
+ except Exception as ex:
354
+ self._logger.error(f"Failed to send chat history messages: {ex}")
355
+ return OperationResult.failed(OperationError(ex))
356
+
357
+ # ============================================================================
358
+ # PRIVATE HELPER METHODS - Message Conversion
359
+ # ============================================================================
360
+
361
+ def _convert_sk_messages_to_chat_history(
362
+ self,
363
+ messages: Sequence[ChatMessageContent],
364
+ ) -> List[ChatHistoryMessage]:
365
+ """
366
+ Convert Semantic Kernel ChatMessageContent objects to ChatHistoryMessage format.
367
+
368
+ Args:
369
+ messages: Sequence of Semantic Kernel ChatMessageContent objects.
370
+
371
+ Returns:
372
+ List of ChatHistoryMessage objects. Messages that cannot be converted
373
+ are filtered out with a warning log.
374
+ """
375
+ chat_history_messages: List[ChatHistoryMessage] = []
376
+
377
+ for idx, message in enumerate(messages):
378
+ converted = self._convert_single_sk_message(message, idx)
379
+ if converted is not None:
380
+ chat_history_messages.append(converted)
381
+
382
+ self._logger.info(
383
+ f"Converted {len(chat_history_messages)} of {len(messages)} messages "
384
+ "to ChatHistoryMessage format"
385
+ )
386
+ return chat_history_messages
387
+
388
+ def _convert_single_sk_message(
389
+ self,
390
+ message: ChatMessageContent,
391
+ index: int = 0,
392
+ ) -> Optional[ChatHistoryMessage]:
393
+ """
394
+ Convert a single Semantic Kernel message to ChatHistoryMessage format.
395
+
396
+ Args:
397
+ message: Single Semantic Kernel ChatMessageContent message.
398
+ index: Index of the message in the list (for logging).
399
+
400
+ Returns:
401
+ ChatHistoryMessage object or None if conversion fails.
402
+ """
403
+ try:
404
+ # Skip None messages
405
+ if message is None:
406
+ self._logger.warning(f"Skipping null message at index {index}")
407
+ return None
408
+
409
+ # Map role to string
410
+ role = self._map_author_role(message.role)
411
+
412
+ # Extract content
413
+ content = self._extract_content(message)
414
+ if not content or not content.strip():
415
+ self._logger.warning(f"Skipping message at index {index} with empty content")
416
+ return None
417
+
418
+ # Extract or generate ID
419
+ msg_id = self._extract_or_generate_id(message, index)
420
+
421
+ # Extract or generate timestamp
422
+ timestamp = self._extract_or_generate_timestamp(message, index)
423
+
424
+ self._logger.debug(
425
+ f"Converting message {index}: role={role}, "
426
+ f"id={msg_id}, has_timestamp={timestamp is not None}"
427
+ )
428
+
429
+ return ChatHistoryMessage(
430
+ id=msg_id,
431
+ role=role,
432
+ content=content,
433
+ timestamp=timestamp,
434
+ )
435
+ except Exception as ex:
436
+ self._logger.error(f"Failed to convert message at index {index}: {ex}")
437
+ return None
438
+
439
+ def _map_author_role(self, role: AuthorRole) -> str:
440
+ """
441
+ Map Semantic Kernel AuthorRole enum to lowercase string.
442
+
443
+ Args:
444
+ role: AuthorRole enum value.
445
+
446
+ Returns:
447
+ Lowercase string representation of the role.
448
+ """
449
+ return role.name.lower()
450
+
451
+ def _extract_content(self, message: ChatMessageContent) -> str:
452
+ """
453
+ Extract text content from a ChatMessageContent.
454
+
455
+ Args:
456
+ message: Semantic Kernel ChatMessageContent object.
457
+
458
+ Returns:
459
+ Content string (may be empty). Returns empty string for unexpected
460
+ types to avoid unintentionally exposing sensitive data.
461
+ """
462
+ content = message.content
463
+
464
+ if content is None:
465
+ return ""
466
+
467
+ # If content is already a string, return it directly
468
+ if isinstance(content, str):
469
+ return content
470
+
471
+ # For unexpected types, log a warning and return empty string to avoid
472
+ # unintentionally stringifying objects that might contain sensitive data
473
+ content_type = type(content).__name__
474
+ self._logger.warning(
475
+ f"Unexpected content type '{content_type}' encountered. "
476
+ "Returning empty string to avoid potential data exposure."
477
+ )
478
+ return ""
479
+
480
+ def _extract_or_generate_id(
481
+ self,
482
+ message: ChatMessageContent,
483
+ index: int,
484
+ ) -> str:
485
+ """
486
+ Extract message ID from metadata or generate a new UUID.
487
+
488
+ Args:
489
+ message: Semantic Kernel ChatMessageContent object.
490
+ index: Message index for logging.
491
+
492
+ Returns:
493
+ Message ID string.
494
+ """
495
+ # Try to get existing ID from metadata
496
+ if message.metadata and "id" in message.metadata:
497
+ existing_id = message.metadata["id"]
498
+ if existing_id:
499
+ return str(existing_id)
500
+
501
+ # Generate new UUID
502
+ generated_id = str(uuid.uuid4())
503
+ self._logger.debug(f"Generated UUID {generated_id} for message at index {index}")
504
+ return generated_id
505
+
506
+ def _extract_or_generate_timestamp(
507
+ self,
508
+ message: ChatMessageContent,
509
+ index: int,
510
+ ) -> datetime:
511
+ """
512
+ Extract timestamp from metadata or generate current UTC time.
513
+
514
+ Args:
515
+ message: Semantic Kernel ChatMessageContent object.
516
+ index: Message index for logging.
517
+
518
+ Returns:
519
+ Timestamp as datetime object.
520
+ """
521
+ # Try to get existing timestamp from metadata
522
+ if message.metadata:
523
+ existing_timestamp = message.metadata.get("timestamp") or message.metadata.get(
524
+ "created_at"
525
+ )
526
+ if existing_timestamp:
527
+ if isinstance(existing_timestamp, datetime):
528
+ return existing_timestamp
529
+ elif isinstance(existing_timestamp, (int, float)):
530
+ # Unix timestamp
531
+ return datetime.fromtimestamp(existing_timestamp, tz=timezone.utc)
532
+ elif isinstance(existing_timestamp, str):
533
+ try:
534
+ return datetime.fromisoformat(existing_timestamp.replace("Z", "+00:00"))
535
+ except (ValueError, TypeError) as ex:
536
+ self._logger.debug(
537
+ f"Failed to parse timestamp '{existing_timestamp}' at index {index}: {ex}"
538
+ )
539
+
540
+ # Use current UTC time
541
+ self._logger.debug(f"Using current UTC time for message at index {index}")
542
+ return datetime.now(timezone.utc)
543
+
184
544
  # ============================================================================
185
545
  # Cleanup Methods
186
546
  # ============================================================================
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: microsoft-agents-a365-tooling-extensions-semantickernel
3
- Version: 0.2.1.dev7
3
+ Version: 0.2.1.dev10
4
4
  Summary: Semantic Kernel integration for Agent 365 Tooling SDK
5
5
  Author-email: Microsoft <support@microsoft.com>
6
6
  License: MIT
@@ -1,7 +1,7 @@
1
1
  microsoft_agents_a365/tooling/extensions/semantickernel/__init__.py,sha256=IvWvvW3Yajk0vX0Il0zisWbN0F-2EIWBmSpAkTeEETw,398
2
2
  microsoft_agents_a365/tooling/extensions/semantickernel/services/__init__.py,sha256=gS6eaKMSKI0PCvXzjUt5gx8xvwMcy2Ym0XLj8cJOqvQ,373
3
- microsoft_agents_a365/tooling/extensions/semantickernel/services/mcp_tool_registration_service.py,sha256=kwl95y7yUfeWMXzy3eLWrIf8c58nVKNQ81gQd1AZcjY,8449
4
- microsoft_agents_a365_tooling_extensions_semantickernel-0.2.1.dev7.dist-info/METADATA,sha256=eRNAnGPM7EcsvLOrEUWyn6ugZiG7gnMmKer_0wJAl4Y,3364
5
- microsoft_agents_a365_tooling_extensions_semantickernel-0.2.1.dev7.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
6
- microsoft_agents_a365_tooling_extensions_semantickernel-0.2.1.dev7.dist-info/top_level.txt,sha256=G3c2_4sy5_EM_BWO67SbK2tKj4G8XFn-QXRbh8g9Lgk,22
7
- microsoft_agents_a365_tooling_extensions_semantickernel-0.2.1.dev7.dist-info/RECORD,,
3
+ microsoft_agents_a365/tooling/extensions/semantickernel/services/mcp_tool_registration_service.py,sha256=BYQdXnwqYgpnC1lddH8ibJqNG0Rc69oidPqxprMwIsc,22052
4
+ microsoft_agents_a365_tooling_extensions_semantickernel-0.2.1.dev10.dist-info/METADATA,sha256=jDx-obviiNxGWsxNZw838bKGosOf0WN8Xmyn-B1wwHw,3365
5
+ microsoft_agents_a365_tooling_extensions_semantickernel-0.2.1.dev10.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
6
+ microsoft_agents_a365_tooling_extensions_semantickernel-0.2.1.dev10.dist-info/top_level.txt,sha256=G3c2_4sy5_EM_BWO67SbK2tKj4G8XFn-QXRbh8g9Lgk,22
7
+ microsoft_agents_a365_tooling_extensions_semantickernel-0.2.1.dev10.dist-info/RECORD,,