Skip to content

Commit 62f6f7c

Browse files
Copilotpontemonti
andauthored
Add API to send chat history to MCP platform for threat protection (#105)
* Initial plan * Add chat history models and send_chat_history method Co-authored-by: pontemonti <7850950+pontemonti@users.noreply.github.com> * Add unit tests for chat history models and operation result classes Co-authored-by: pontemonti <7850950+pontemonti@users.noreply.github.com> * Fix type annotation for chat_history_messages parameter Co-authored-by: pontemonti <7850950+pontemonti@users.noreply.github.com> * Add usage example for send_chat_history API Co-authored-by: pontemonti <7850950+pontemonti@users.noreply.github.com> * Address PR feedback: update copyright headers, remove example file, use TurnContext, remove auth Co-authored-by: pontemonti <7850950+pontemonti@users.noreply.github.com> * Add type annotation for turn_context parameter Co-authored-by: pontemonti <7850950+pontemonti@users.noreply.github.com> * Remove TYPE_CHECKING and always import TurnContext, add microsoft-agents-hosting-core dependency Co-authored-by: pontemonti <7850950+pontemonti@users.noreply.github.com> * Add comprehensive unit tests for send_chat_history method Co-authored-by: pontemonti <7850950+pontemonti@users.noreply.github.com> * Apply suggestion from @pontemonti * Run ruff format to fix code formatting Co-authored-by: pontemonti <7850950+pontemonti@users.noreply.github.com> * Use consistent datetime import style in test_send_chat_history.py Co-authored-by: pontemonti <7850950+pontemonti@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: pontemonti <7850950+pontemonti@users.noreply.github.com> Co-authored-by: Johan Broberg <johan@pontemonti.net>
1 parent 698a8ba commit 62f6f7c

19 files changed

Lines changed: 1347 additions & 14 deletions

File tree

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1-
# Copyright (c) Microsoft. All rights reserved.
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.
23

34
from .environment_utils import get_observability_authentication_scope
5+
from .operation_error import OperationError
6+
from .operation_result import OperationResult
47
from .power_platform_api_discovery import ClusterCategory, PowerPlatformApiDiscovery
58
from .utility import Utility
69

@@ -9,6 +12,8 @@
912
"PowerPlatformApiDiscovery",
1013
"ClusterCategory",
1114
"Utility",
15+
"OperationError",
16+
"OperationResult",
1217
]
1318

1419
__path__ = __import__("pkgutil").extend_path(__path__, __name__)
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.
3+
4+
"""
5+
Encapsulates an error from an operation.
6+
"""
7+
8+
9+
class OperationError:
10+
"""
11+
Represents an error that occurred during an operation.
12+
13+
This class wraps an exception and provides a consistent interface for
14+
accessing error information.
15+
"""
16+
17+
def __init__(self, exception: Exception):
18+
"""
19+
Initialize a new instance of the OperationError class.
20+
21+
Args:
22+
exception: The exception associated with the error.
23+
24+
Raises:
25+
ValueError: If exception is None.
26+
"""
27+
if exception is None:
28+
raise ValueError("exception cannot be None")
29+
self._exception = exception
30+
31+
@property
32+
def exception(self) -> Exception:
33+
"""
34+
Get the exception associated with the error.
35+
36+
Returns:
37+
Exception: The exception associated with the error.
38+
"""
39+
return self._exception
40+
41+
@property
42+
def message(self) -> str:
43+
"""
44+
Get the message associated with the error.
45+
46+
Returns:
47+
str: The error message from the exception.
48+
"""
49+
return str(self._exception)
50+
51+
def __str__(self) -> str:
52+
"""
53+
Return a string representation of the error.
54+
55+
Returns:
56+
str: A string representation of the error.
57+
"""
58+
return str(self._exception)
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.
3+
4+
"""
5+
Represents the result of an operation.
6+
"""
7+
8+
from typing import List, Optional
9+
10+
from .operation_error import OperationError
11+
12+
13+
class OperationResult:
14+
"""
15+
Represents the result of an operation.
16+
17+
This class encapsulates the success or failure state of an operation along
18+
with any associated errors.
19+
"""
20+
21+
_success_instance: Optional["OperationResult"] = None
22+
23+
def __init__(self, succeeded: bool, errors: Optional[List[OperationError]] = None):
24+
"""
25+
Initialize a new instance of the OperationResult class.
26+
27+
Args:
28+
succeeded: Flag indicating whether the operation succeeded.
29+
errors: Optional list of errors that occurred during the operation.
30+
"""
31+
self._succeeded = succeeded
32+
self._errors = errors if errors is not None else []
33+
34+
@property
35+
def succeeded(self) -> bool:
36+
"""
37+
Get a flag indicating whether the operation succeeded.
38+
39+
Returns:
40+
bool: True if the operation succeeded, otherwise False.
41+
"""
42+
return self._succeeded
43+
44+
@property
45+
def errors(self) -> List[OperationError]:
46+
"""
47+
Get the list of errors that occurred during the operation.
48+
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+
54+
Returns:
55+
List[OperationError]: A copy of the list of operation errors.
56+
"""
57+
return list(self._errors)
58+
59+
@staticmethod
60+
def success() -> "OperationResult":
61+
"""
62+
Return an OperationResult indicating a successful operation.
63+
64+
Returns:
65+
OperationResult: An OperationResult indicating a successful operation.
66+
"""
67+
return OperationResult._success_instance
68+
69+
@staticmethod
70+
def failed(*errors: OperationError) -> "OperationResult":
71+
"""
72+
Create an OperationResult indicating a failed operation.
73+
74+
Args:
75+
*errors: Variable number of OperationError instances.
76+
77+
Returns:
78+
OperationResult: An OperationResult indicating a failed operation.
79+
"""
80+
error_list = list(errors) if errors else []
81+
return OperationResult(succeeded=False, errors=error_list)
82+
83+
def __str__(self) -> str:
84+
"""
85+
Convert the value of the current OperationResult object to its string representation.
86+
87+
Returns:
88+
str: A string representation of the current OperationResult object.
89+
"""
90+
if self._succeeded:
91+
return "Succeeded"
92+
else:
93+
error_messages = ", ".join(str(error.message) for error in self._errors)
94+
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)
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Changelog
2+
3+
All notable changes to the `microsoft-agents-a365-tooling` package will be documented in this file.
4+
5+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7+
8+
## [Unreleased]
9+
10+
### Added
11+
12+
- Added `send_chat_history` method to `McpToolServerConfigurationService` for sending chat conversation history to the MCP platform for real-time threat protection analysis
13+
- Added `ChatHistoryMessage` Pydantic model for representing individual messages in chat history
14+
- Added `ChatMessageRequest` Pydantic model for the chat history API request payload
15+
- Added `py.typed` marker for PEP 561 compliance, enabling type checker support
Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
1-
# Copyright (c) Microsoft. All rights reserved.
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.
23

34
"""
45
Common models for MCP tooling.
56
67
This module defines data models used across the MCP tooling framework.
78
"""
89

10+
from .chat_history_message import ChatHistoryMessage
11+
from .chat_message_request import ChatMessageRequest
912
from .mcp_server_config import MCPServerConfig
1013
from .tool_options import ToolOptions
1114

12-
__all__ = ["MCPServerConfig", "ToolOptions"]
15+
__all__ = ["MCPServerConfig", "ToolOptions", "ChatHistoryMessage", "ChatMessageRequest"]
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.
3+
4+
"""Chat history message model."""
5+
6+
from datetime import datetime
7+
from typing import Literal, Optional
8+
9+
from pydantic import BaseModel, ConfigDict, Field, field_validator
10+
11+
12+
class ChatHistoryMessage(BaseModel):
13+
"""
14+
Represents a single message in the chat history.
15+
16+
This model is used to capture individual messages exchanged between
17+
users and the AI assistant for threat protection analysis and
18+
compliance monitoring.
19+
20+
Attributes:
21+
id: Optional unique identifier for the message.
22+
role: The role of the message sender (user, assistant, or system).
23+
content: The text content of the message.
24+
timestamp: Optional timestamp when the message was created.
25+
26+
Example:
27+
>>> message = ChatHistoryMessage(role="user", content="Hello, how can you help?")
28+
>>> print(message.role)
29+
'user'
30+
>>> print(message.content)
31+
'Hello, how can you help?'
32+
"""
33+
34+
model_config = ConfigDict(populate_by_name=True)
35+
36+
id: Optional[str] = Field(default=None, description="Unique message identifier")
37+
role: Literal["user", "assistant", "system"] = Field(
38+
..., description="The role of the message sender"
39+
)
40+
content: str = Field(..., description="The message content")
41+
timestamp: Optional[datetime] = Field(default=None, description="When the message was created")
42+
43+
@field_validator("content")
44+
@classmethod
45+
def content_not_empty(cls, v: str) -> str:
46+
"""Validate that content is not empty or whitespace-only."""
47+
if not v or not v.strip():
48+
raise ValueError("content cannot be empty or whitespace")
49+
return v
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.
3+
4+
"""Chat message request model."""
5+
6+
from typing import List
7+
8+
from pydantic import BaseModel, ConfigDict, Field, field_validator
9+
10+
from .chat_history_message import ChatHistoryMessage
11+
12+
13+
class ChatMessageRequest(BaseModel):
14+
"""
15+
Request payload for sending chat history to MCP platform.
16+
17+
This model represents the complete request body sent to the MCP platform's
18+
chat history endpoint for threat protection analysis. It includes the
19+
current conversation context and historical messages.
20+
21+
The model uses field aliases to serialize to camelCase JSON format
22+
as required by the MCP platform API.
23+
24+
Attributes:
25+
conversation_id: Unique identifier for the conversation.
26+
message_id: Unique identifier for the current message.
27+
user_message: The current user message being processed.
28+
chat_history: List of previous messages in the conversation.
29+
30+
Example:
31+
>>> from microsoft_agents_a365.tooling.models import ChatHistoryMessage
32+
>>> request = ChatMessageRequest(
33+
... conversation_id="conv-123",
34+
... message_id="msg-456",
35+
... user_message="What is the weather today?",
36+
... chat_history=[
37+
... ChatHistoryMessage(role="user", content="Hello"),
38+
... ChatHistoryMessage(role="assistant", content="Hi there!"),
39+
... ]
40+
... )
41+
>>> # Serialize to camelCase JSON
42+
>>> json_dict = request.model_dump(by_alias=True)
43+
>>> print(json_dict["conversationId"])
44+
'conv-123'
45+
"""
46+
47+
model_config = ConfigDict(populate_by_name=True)
48+
49+
conversation_id: str = Field(
50+
..., alias="conversationId", description="Unique conversation identifier"
51+
)
52+
message_id: str = Field(..., alias="messageId", description="Current message identifier")
53+
user_message: str = Field(..., alias="userMessage", description="The current user message")
54+
chat_history: List[ChatHistoryMessage] = Field(
55+
..., alias="chatHistory", description="Previous messages in the conversation"
56+
)
57+
58+
@field_validator("conversation_id", "message_id", "user_message")
59+
@classmethod
60+
def not_empty(cls, v: str) -> str:
61+
"""Validate that string fields are not empty or whitespace-only."""
62+
if not v or not v.strip():
63+
raise ValueError("Field cannot be empty or whitespace")
64+
return v

‎libraries/microsoft-agents-a365-tooling/microsoft_agents_a365/tooling/py.typed‎

Whitespace-only changes.

0 commit comments

Comments
 (0)