Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
30a75cb
Initial plan
Copilot Jan 8, 2026
70b54d2
Add chat history models and send_chat_history method
Copilot Jan 8, 2026
f7bf975
Add unit tests for chat history models and operation result classes
Copilot Jan 8, 2026
34a8605
Fix type annotation for chat_history_messages parameter
Copilot Jan 8, 2026
6545d57
Add usage example for send_chat_history API
Copilot Jan 8, 2026
6c49d71
Address PR feedback: update copyright headers, remove example file, u…
Copilot Jan 8, 2026
4660d6c
Add type annotation for turn_context parameter
Copilot Jan 8, 2026
38206e1
Remove TYPE_CHECKING and always import TurnContext, add microsoft-age…
Copilot Jan 8, 2026
4443b7d
Add comprehensive unit tests for send_chat_history method
Copilot Jan 8, 2026
daed65e
Apply suggestion from @pontemonti
pontemonti Jan 8, 2026
1b6f196
Run ruff format to fix code formatting
Copilot Jan 8, 2026
a79f8d9
Use consistent datetime import style in test_send_chat_history.py
Copilot Jan 8, 2026
e93e24e
Code review fixes/pr 105 (#114)
pontemonti Jan 17, 2026
440549b
fix(tooling): add whitespace validation and type hints to model classes
Jan 18, 2026
22c3760
fix(tooling): improve send_chat_history validation and error handling
Jan 18, 2026
6f22dde
refactor(tooling): extract endpoint path to constant
Jan 18, 2026
8f9b0d4
test(tooling): add whitespace and empty list validation tests
Jan 18, 2026
b3e88ff
Code review fixes/pr 105 v3 (#115)
pontemonti Jan 18, 2026
971ee1d
Update model classes to use pydantic instead of `@dataclass`.
Jan 20, 2026
1a7c827
chore: add changelog for new chat history API features
Jan 20, 2026
c3c77dc
style: format ChatHistoryMessage instantiation for improved readability
Jan 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# Copyright (c) Microsoft. All rights reserved.
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

from .environment_utils import get_observability_authentication_scope
from .operation_error import OperationError
from .operation_result import OperationResult
from .power_platform_api_discovery import ClusterCategory, PowerPlatformApiDiscovery
from .utility import Utility

Expand All @@ -9,6 +12,8 @@
"PowerPlatformApiDiscovery",
"ClusterCategory",
"Utility",
"OperationError",
"OperationResult",
]

__path__ = __import__("pkgutil").extend_path(__path__, __name__)
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

"""
Encapsulates an error from an operation.
"""


class OperationError:
"""
Represents an error that occurred during an operation.

This class wraps an exception and provides a consistent interface for
accessing error information.
"""

def __init__(self, exception: Exception):
"""
Initialize a new instance of the OperationError class.

Args:
exception: The exception associated with the error.

Raises:
ValueError: If exception is None.
"""
if exception is None:
raise ValueError("exception cannot be None")
self._exception = exception

@property
def exception(self) -> Exception:
"""
Get the exception associated with the error.

Returns:
Exception: The exception associated with the error.
"""
return self._exception

@property
def message(self) -> str:
"""
Get the message associated with the error.

Returns:
str: The error message from the exception.
"""
return str(self._exception)

def __str__(self) -> str:
"""
Return a string representation of the error.

Returns:
str: A string representation of the error.
"""
return str(self._exception)
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

"""
Represents the result of an operation.
"""

from typing import List, Optional

from .operation_error import OperationError


class OperationResult:
"""
Represents the result of an operation.

This class encapsulates the success or failure state of an operation along
with any associated errors.
"""

_success_instance: Optional["OperationResult"] = None

def __init__(self, succeeded: bool, errors: Optional[List[OperationError]] = None):
"""
Initialize a new instance of the OperationResult class.

Args:
succeeded: Flag indicating whether the operation succeeded.
errors: Optional list of errors that occurred during the operation.
"""
self._succeeded = succeeded
self._errors = errors if errors is not None else []

@property
def succeeded(self) -> bool:
"""
Get a flag indicating whether the operation succeeded.

Returns:
bool: True if the operation succeeded, otherwise False.
"""
return self._succeeded

@property
def errors(self) -> List[OperationError]:
"""
Get the list of errors that occurred during the operation.

Note:
This property returns a defensive copy of the internal error list
to prevent external modifications, which is especially important for
protecting the singleton instance returned by success().

Returns:
List[OperationError]: A copy of the list of operation errors.
"""
return list(self._errors)

@staticmethod
def success() -> "OperationResult":
"""
Return an OperationResult indicating a successful operation.

Returns:
OperationResult: An OperationResult indicating a successful operation.
"""
return OperationResult._success_instance

@staticmethod
def failed(*errors: OperationError) -> "OperationResult":
"""
Create an OperationResult indicating a failed operation.

Args:
*errors: Variable number of OperationError instances.

Returns:
OperationResult: An OperationResult indicating a failed operation.
"""
error_list = list(errors) if errors else []
return OperationResult(succeeded=False, errors=error_list)

def __str__(self) -> str:
"""
Convert the value of the current OperationResult object to its string representation.

Returns:
str: A string representation of the current OperationResult object.
"""
if self._succeeded:
return "Succeeded"
else:
error_messages = ", ".join(str(error.message) for error in self._errors)
return f"Failed: {error_messages}" if error_messages else "Failed"


# Module-level eager initialization (thread-safe by Python's import lock)
OperationResult._success_instance = OperationResult(succeeded=True)
15 changes: 15 additions & 0 deletions libraries/microsoft-agents-a365-tooling/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Changelog

All notable changes to the `microsoft-agents-a365-tooling` package will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- Added `send_chat_history` method to `McpToolServerConfigurationService` for sending chat conversation history to the MCP platform for real-time threat protection analysis
- Added `ChatHistoryMessage` Pydantic model for representing individual messages in chat history
- Added `ChatMessageRequest` Pydantic model for the chat history API request payload
- Added `py.typed` marker for PEP 561 compliance, enabling type checker support
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
# Copyright (c) Microsoft. All rights reserved.
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

"""
Common models for MCP tooling.

This module defines data models used across the MCP tooling framework.
"""

from .chat_history_message import ChatHistoryMessage
from .chat_message_request import ChatMessageRequest
from .mcp_server_config import MCPServerConfig
from .tool_options import ToolOptions

__all__ = ["MCPServerConfig", "ToolOptions"]
__all__ = ["MCPServerConfig", "ToolOptions", "ChatHistoryMessage", "ChatMessageRequest"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

"""Chat history message model."""

from datetime import datetime
from typing import Literal, Optional

from pydantic import BaseModel, ConfigDict, Field, field_validator


class ChatHistoryMessage(BaseModel):
"""
Represents a single message in the chat history.

This model is used to capture individual messages exchanged between
users and the AI assistant for threat protection analysis and
compliance monitoring.

Attributes:
id: Optional unique identifier for the message.
role: The role of the message sender (user, assistant, or system).
content: The text content of the message.
timestamp: Optional timestamp when the message was created.

Example:
>>> message = ChatHistoryMessage(role="user", content="Hello, how can you help?")
>>> print(message.role)
'user'
>>> print(message.content)
'Hello, how can you help?'
"""

model_config = ConfigDict(populate_by_name=True)

id: Optional[str] = Field(default=None, description="Unique message identifier")
role: Literal["user", "assistant", "system"] = Field(
..., description="The role of the message sender"
)
content: str = Field(..., description="The message content")
timestamp: Optional[datetime] = Field(default=None, description="When the message was created")

@field_validator("content")
@classmethod
def content_not_empty(cls, v: str) -> str:
"""Validate that content is not empty or whitespace-only."""
if not v or not v.strip():
raise ValueError("content cannot be empty or whitespace")
return v
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

"""Chat message request model."""

from typing import List

from pydantic import BaseModel, ConfigDict, Field, field_validator

from .chat_history_message import ChatHistoryMessage


class ChatMessageRequest(BaseModel):
"""
Request payload for sending chat history to MCP platform.

This model represents the complete request body sent to the MCP platform's
chat history endpoint for threat protection analysis. It includes the
current conversation context and historical messages.

The model uses field aliases to serialize to camelCase JSON format
as required by the MCP platform API.

Attributes:
conversation_id: Unique identifier for the conversation.
message_id: Unique identifier for the current message.
user_message: The current user message being processed.
chat_history: List of previous messages in the conversation.

Example:
>>> from microsoft_agents_a365.tooling.models import ChatHistoryMessage
>>> request = ChatMessageRequest(
... conversation_id="conv-123",
... message_id="msg-456",
... user_message="What is the weather today?",
... chat_history=[
... ChatHistoryMessage(role="user", content="Hello"),
... ChatHistoryMessage(role="assistant", content="Hi there!"),
... ]
... )
>>> # Serialize to camelCase JSON
>>> json_dict = request.model_dump(by_alias=True)
>>> print(json_dict["conversationId"])
'conv-123'
"""

model_config = ConfigDict(populate_by_name=True)

conversation_id: str = Field(
..., alias="conversationId", description="Unique conversation identifier"
)
message_id: str = Field(..., alias="messageId", description="Current message identifier")
user_message: str = Field(..., alias="userMessage", description="The current user message")
chat_history: List[ChatHistoryMessage] = Field(
..., alias="chatHistory", description="Previous messages in the conversation"
)

@field_validator("conversation_id", "message_id", "user_message")
@classmethod
def not_empty(cls, v: str) -> str:
"""Validate that string fields are not empty or whitespace-only."""
if not v or not v.strip():
raise ValueError("Field cannot be empty or whitespace")
return v
Loading
Loading