microsoft-agents-a365-tooling-extensions-openai 0.1.0.dev30__tar.gz → 0.2.1.dev0__tar.gz
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.
- {microsoft_agents_a365_tooling_extensions_openai-0.1.0.dev30 → microsoft_agents_a365_tooling_extensions_openai-0.2.1.dev0}/PKG-INFO +5 -3
- {microsoft_agents_a365_tooling_extensions_openai-0.1.0.dev30 → microsoft_agents_a365_tooling_extensions_openai-0.2.1.dev0}/README.md +2 -2
- microsoft_agents_a365_tooling_extensions_openai-0.2.1.dev0/microsoft_agents_a365/tooling/extensions/openai/__init__.py +22 -0
- microsoft_agents_a365_tooling_extensions_openai-0.2.1.dev0/microsoft_agents_a365/tooling/extensions/openai/mcp_tool_registration_service.py +669 -0
- {microsoft_agents_a365_tooling_extensions_openai-0.1.0.dev30 → microsoft_agents_a365_tooling_extensions_openai-0.2.1.dev0}/microsoft_agents_a365_tooling_extensions_openai.egg-info/PKG-INFO +5 -3
- {microsoft_agents_a365_tooling_extensions_openai-0.1.0.dev30 → microsoft_agents_a365_tooling_extensions_openai-0.2.1.dev0}/microsoft_agents_a365_tooling_extensions_openai.egg-info/SOURCES.txt +3 -0
- {microsoft_agents_a365_tooling_extensions_openai-0.1.0.dev30 → microsoft_agents_a365_tooling_extensions_openai-0.2.1.dev0}/microsoft_agents_a365_tooling_extensions_openai.egg-info/top_level.txt +1 -0
- {microsoft_agents_a365_tooling_extensions_openai-0.1.0.dev30 → microsoft_agents_a365_tooling_extensions_openai-0.2.1.dev0}/pyproject.toml +4 -0
- {microsoft_agents_a365_tooling_extensions_openai-0.1.0.dev30 → microsoft_agents_a365_tooling_extensions_openai-0.2.1.dev0}/setup.py +1 -1
- microsoft_agents_a365_tooling_extensions_openai-0.1.0.dev30/microsoft_agents_a365/tooling/extensions/openai/__init__.py +0 -11
- microsoft_agents_a365_tooling_extensions_openai-0.1.0.dev30/microsoft_agents_a365/tooling/extensions/openai/mcp_tool_registration_service.py +0 -223
- {microsoft_agents_a365_tooling_extensions_openai-0.1.0.dev30 → microsoft_agents_a365_tooling_extensions_openai-0.2.1.dev0}/microsoft_agents_a365_tooling_extensions_openai.egg-info/dependency_links.txt +0 -0
- {microsoft_agents_a365_tooling_extensions_openai-0.1.0.dev30 → microsoft_agents_a365_tooling_extensions_openai-0.2.1.dev0}/microsoft_agents_a365_tooling_extensions_openai.egg-info/requires.txt +0 -0
- {microsoft_agents_a365_tooling_extensions_openai-0.1.0.dev30 → microsoft_agents_a365_tooling_extensions_openai-0.2.1.dev0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: microsoft-agents-a365-tooling-extensions-openai
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.2.1.dev0
|
|
4
4
|
Summary: OpenAI integration for Agent 365 Tooling SDK
|
|
5
5
|
Author-email: Microsoft <support@microsoft.com>
|
|
6
6
|
License: MIT
|
|
@@ -18,6 +18,7 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
|
18
18
|
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
19
19
|
Requires-Python: >=3.11
|
|
20
20
|
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
21
22
|
Requires-Dist: microsoft-agents-a365-tooling>=0.0.0
|
|
22
23
|
Requires-Dist: openai-agents
|
|
23
24
|
Requires-Dist: asyncio-throttle
|
|
@@ -30,6 +31,7 @@ Requires-Dist: mypy>=1.0.0; extra == "dev"
|
|
|
30
31
|
Provides-Extra: test
|
|
31
32
|
Requires-Dist: pytest>=7.0.0; extra == "test"
|
|
32
33
|
Requires-Dist: pytest-asyncio>=0.21.0; extra == "test"
|
|
34
|
+
Dynamic: license-file
|
|
33
35
|
|
|
34
36
|
# microsoft-agents-a365-tooling-extensions-openai
|
|
35
37
|
|
|
@@ -53,7 +55,7 @@ For usage examples and detailed documentation, see the [Tooling documentation](h
|
|
|
53
55
|
For issues, questions, or feedback:
|
|
54
56
|
|
|
55
57
|
- File issues in the [GitHub Issues](https://github.com/microsoft/Agent365-python/issues) section
|
|
56
|
-
- See the [main documentation](
|
|
58
|
+
- See the [main documentation](../../README.md) for more information
|
|
57
59
|
|
|
58
60
|
## Trademarks
|
|
59
61
|
|
|
@@ -63,4 +65,4 @@ For issues, questions, or feedback:
|
|
|
63
65
|
|
|
64
66
|
Copyright (c) Microsoft Corporation. All rights reserved.
|
|
65
67
|
|
|
66
|
-
Licensed under the MIT License - see the [LICENSE](
|
|
68
|
+
Licensed under the MIT License - see the [LICENSE](../../LICENSE.md) file for details.
|
|
@@ -20,7 +20,7 @@ For usage examples and detailed documentation, see the [Tooling documentation](h
|
|
|
20
20
|
For issues, questions, or feedback:
|
|
21
21
|
|
|
22
22
|
- File issues in the [GitHub Issues](https://github.com/microsoft/Agent365-python/issues) section
|
|
23
|
-
- See the [main documentation](
|
|
23
|
+
- See the [main documentation](../../README.md) for more information
|
|
24
24
|
|
|
25
25
|
## Trademarks
|
|
26
26
|
|
|
@@ -30,4 +30,4 @@ For issues, questions, or feedback:
|
|
|
30
30
|
|
|
31
31
|
Copyright (c) Microsoft Corporation. All rights reserved.
|
|
32
32
|
|
|
33
|
-
Licensed under the MIT License - see the [LICENSE](
|
|
33
|
+
Licensed under the MIT License - see the [LICENSE](../../LICENSE.md) file for details.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
OpenAI extensions for Microsoft Agent 365 Tooling SDK.
|
|
6
|
+
|
|
7
|
+
Tooling and utilities specifically for OpenAI framework integration.
|
|
8
|
+
Provides OpenAI-specific helper utilities including:
|
|
9
|
+
- McpToolRegistrationService: Service for MCP tool registration and chat history management
|
|
10
|
+
|
|
11
|
+
For type hints, use the types directly from the OpenAI Agents SDK:
|
|
12
|
+
- agents.memory.Session: Protocol for session objects
|
|
13
|
+
- agents.items.TResponseInputItem: Type for input message items
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from .mcp_tool_registration_service import McpToolRegistrationService
|
|
17
|
+
|
|
18
|
+
__version__ = "1.0.0"
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"McpToolRegistrationService",
|
|
22
|
+
]
|
|
@@ -0,0 +1,669 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
MCP Tool Registration Service for OpenAI.
|
|
6
|
+
|
|
7
|
+
This module provides OpenAI-specific extensions for MCP tool registration,
|
|
8
|
+
including methods to send chat history from OpenAI Sessions and message lists.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
import uuid
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from datetime import datetime, timezone
|
|
15
|
+
from typing import Dict, List, Optional
|
|
16
|
+
|
|
17
|
+
from agents import Agent
|
|
18
|
+
from agents.items import TResponseInputItem
|
|
19
|
+
from agents.mcp import (
|
|
20
|
+
MCPServerStreamableHttp,
|
|
21
|
+
MCPServerStreamableHttpParams,
|
|
22
|
+
)
|
|
23
|
+
from agents.memory import Session
|
|
24
|
+
from microsoft_agents.hosting.core import Authorization, TurnContext
|
|
25
|
+
|
|
26
|
+
from microsoft_agents_a365.runtime import OperationError, OperationResult
|
|
27
|
+
from microsoft_agents_a365.runtime.utility import Utility
|
|
28
|
+
from microsoft_agents_a365.tooling.models import ChatHistoryMessage, ToolOptions
|
|
29
|
+
from microsoft_agents_a365.tooling.services.mcp_tool_server_configuration_service import (
|
|
30
|
+
McpToolServerConfigurationService,
|
|
31
|
+
)
|
|
32
|
+
from microsoft_agents_a365.tooling.utils.constants import Constants
|
|
33
|
+
from microsoft_agents_a365.tooling.utils.utility import (
|
|
34
|
+
get_mcp_platform_authentication_scope,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class MCPServerInfo:
|
|
40
|
+
"""Information about an MCP server"""
|
|
41
|
+
|
|
42
|
+
name: str
|
|
43
|
+
url: str
|
|
44
|
+
server_type: str = "streamable_http" # hosted, streamable_http, sse, stdio
|
|
45
|
+
headers: Optional[Dict[str, str]] = None
|
|
46
|
+
require_approval: str = "never"
|
|
47
|
+
timeout: int = 30 # Timeout in seconds (will be converted to milliseconds for MCPServerStreamableHttpParams)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class McpToolRegistrationService:
|
|
51
|
+
"""Service for managing MCP tools and servers for an agent"""
|
|
52
|
+
|
|
53
|
+
_orchestrator_name: str = "OpenAI"
|
|
54
|
+
|
|
55
|
+
def __init__(self, logger: Optional[logging.Logger] = None):
|
|
56
|
+
"""
|
|
57
|
+
Initialize the MCP Tool Registration Service for OpenAI.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
logger: Logger instance for logging operations.
|
|
61
|
+
"""
|
|
62
|
+
self._logger = logger or logging.getLogger(self.__class__.__name__)
|
|
63
|
+
self.config_service = McpToolServerConfigurationService(logger=self._logger)
|
|
64
|
+
|
|
65
|
+
async def add_tool_servers_to_agent(
|
|
66
|
+
self,
|
|
67
|
+
agent: Agent,
|
|
68
|
+
auth: Authorization,
|
|
69
|
+
auth_handler_name: str,
|
|
70
|
+
context: TurnContext,
|
|
71
|
+
auth_token: Optional[str] = None,
|
|
72
|
+
) -> Agent:
|
|
73
|
+
"""
|
|
74
|
+
Add new MCP servers to the agent by creating a new Agent instance.
|
|
75
|
+
|
|
76
|
+
Note: Due to OpenAI Agents SDK limitations, MCP servers must be set during
|
|
77
|
+
Agent creation. If new servers are found, this method creates a new Agent
|
|
78
|
+
instance with all MCP servers (existing + new) properly initialized.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
agent: The existing agent to add servers to
|
|
82
|
+
auth: Authorization handler for token exchange.
|
|
83
|
+
auth_handler_name: Name of the authorization handler.
|
|
84
|
+
context: Turn context for the current operation.
|
|
85
|
+
auth_token: Authentication token to access the MCP servers.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
New Agent instance with all MCP servers, or original agent if no new servers
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
if auth_token is None or auth_token.strip() == "":
|
|
92
|
+
scopes = get_mcp_platform_authentication_scope()
|
|
93
|
+
authToken = await auth.exchange_token(context, scopes, auth_handler_name)
|
|
94
|
+
auth_token = authToken.token
|
|
95
|
+
|
|
96
|
+
# Get MCP server configurations from the configuration service
|
|
97
|
+
# mcp_server_configs = []
|
|
98
|
+
# TODO: radevika: Update once the common project is merged.
|
|
99
|
+
|
|
100
|
+
options = ToolOptions(orchestrator_name=self._orchestrator_name)
|
|
101
|
+
agentic_app_id = Utility.resolve_agent_identity(context, auth_token)
|
|
102
|
+
self._logger.info(f"Listing MCP tool servers for agent {agentic_app_id}")
|
|
103
|
+
mcp_server_configs = await self.config_service.list_tool_servers(
|
|
104
|
+
agentic_app_id=agentic_app_id,
|
|
105
|
+
auth_token=auth_token,
|
|
106
|
+
options=options,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
self._logger.info(f"Loaded {len(mcp_server_configs)} MCP server configurations")
|
|
110
|
+
|
|
111
|
+
# Convert MCP server configs to MCPServerInfo objects
|
|
112
|
+
mcp_servers_info = []
|
|
113
|
+
for server_config in mcp_server_configs:
|
|
114
|
+
# Use mcp_server_name if available (not None or empty), otherwise fall back to mcp_server_unique_name
|
|
115
|
+
server_name = server_config.mcp_server_name or server_config.mcp_server_unique_name
|
|
116
|
+
# Use the URL from config (always populated by the configuration service)
|
|
117
|
+
server_url = server_config.url
|
|
118
|
+
server_info = MCPServerInfo(
|
|
119
|
+
name=server_name,
|
|
120
|
+
url=server_url,
|
|
121
|
+
)
|
|
122
|
+
mcp_servers_info.append(server_info)
|
|
123
|
+
|
|
124
|
+
# Get existing MCP servers from the agent
|
|
125
|
+
existing_mcp_servers = (
|
|
126
|
+
list(agent.mcp_servers) if hasattr(agent, "mcp_servers") and agent.mcp_servers else []
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# Prepare new MCP servers to add
|
|
130
|
+
new_mcp_servers = []
|
|
131
|
+
connected_servers = []
|
|
132
|
+
|
|
133
|
+
existing_server_urls = []
|
|
134
|
+
for server in existing_mcp_servers:
|
|
135
|
+
# Check for URL in params dict (MCPServerStreamableHttp stores URL in params["url"])
|
|
136
|
+
if (
|
|
137
|
+
hasattr(server, "params")
|
|
138
|
+
and isinstance(server.params, dict)
|
|
139
|
+
and "url" in server.params
|
|
140
|
+
):
|
|
141
|
+
existing_server_urls.append(server.params["url"])
|
|
142
|
+
elif hasattr(server, "params") and hasattr(server.params, "url"):
|
|
143
|
+
existing_server_urls.append(server.params.url)
|
|
144
|
+
elif hasattr(server, "url"):
|
|
145
|
+
existing_server_urls.append(server.url)
|
|
146
|
+
|
|
147
|
+
for si in mcp_servers_info:
|
|
148
|
+
# Check if MCP server already exists
|
|
149
|
+
|
|
150
|
+
if si.url not in existing_server_urls:
|
|
151
|
+
try:
|
|
152
|
+
# Prepare headers with authorization
|
|
153
|
+
headers = si.headers or {}
|
|
154
|
+
if auth_token:
|
|
155
|
+
headers[Constants.Headers.AUTHORIZATION] = (
|
|
156
|
+
f"{Constants.Headers.BEARER_PREFIX} {auth_token}"
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
headers[Constants.Headers.USER_AGENT] = Utility.get_user_agent_header(
|
|
160
|
+
self._orchestrator_name
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
# Create MCPServerStreamableHttpParams with proper configuration
|
|
164
|
+
params = MCPServerStreamableHttpParams(url=si.url, headers=headers)
|
|
165
|
+
|
|
166
|
+
# Create MCP server
|
|
167
|
+
mcp_server = MCPServerStreamableHttp(params=params, name=si.name)
|
|
168
|
+
|
|
169
|
+
# CRITICAL: Connect the server before adding it to the agent
|
|
170
|
+
# This fixes the "Server not initialized. Make sure you call `connect()` first." error
|
|
171
|
+
# TODO: When App Manifest scenario lits up for onboarding agent, we need to pull a flag and disconnect if the flag is disabled.
|
|
172
|
+
await mcp_server.connect()
|
|
173
|
+
|
|
174
|
+
new_mcp_servers.append(mcp_server)
|
|
175
|
+
connected_servers.append(mcp_server)
|
|
176
|
+
|
|
177
|
+
existing_server_urls.append(si.url)
|
|
178
|
+
self._logger.info(
|
|
179
|
+
f"Successfully connected to MCP server '{si.name}' at {si.url}"
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
except Exception as e:
|
|
183
|
+
# Log the error but continue with other servers
|
|
184
|
+
self._logger.warning(
|
|
185
|
+
f"Failed to connect to MCP server {si.name} at {si.url}: {e}"
|
|
186
|
+
)
|
|
187
|
+
continue
|
|
188
|
+
|
|
189
|
+
# If we have new servers, we need to recreate the agent
|
|
190
|
+
# The OpenAI Agents SDK requires MCP servers to be set during agent creation
|
|
191
|
+
if new_mcp_servers:
|
|
192
|
+
try:
|
|
193
|
+
self._logger.info(f"Recreating agent with {len(new_mcp_servers)} new MCP servers")
|
|
194
|
+
all_mcp_servers = existing_mcp_servers + new_mcp_servers
|
|
195
|
+
|
|
196
|
+
# Recreate the agent with all MCP servers
|
|
197
|
+
new_agent = Agent(
|
|
198
|
+
name=agent.name,
|
|
199
|
+
model=agent.model,
|
|
200
|
+
model_settings=agent.model_settings
|
|
201
|
+
if hasattr(agent, "model_settings")
|
|
202
|
+
else None,
|
|
203
|
+
instructions=agent.instructions,
|
|
204
|
+
tools=agent.tools,
|
|
205
|
+
mcp_servers=all_mcp_servers,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
# Copy agent attributes to preserve state
|
|
209
|
+
for attr_name in ["name", "model", "instructions", "tools"]:
|
|
210
|
+
if hasattr(agent, attr_name):
|
|
211
|
+
setattr(new_agent, attr_name, getattr(agent, attr_name))
|
|
212
|
+
|
|
213
|
+
# Store connected servers for potential cleanup
|
|
214
|
+
if not hasattr(self, "_connected_servers"):
|
|
215
|
+
self._connected_servers = []
|
|
216
|
+
self._connected_servers.extend(connected_servers)
|
|
217
|
+
|
|
218
|
+
self._logger.info(
|
|
219
|
+
f"Agent recreated successfully with {len(all_mcp_servers)} total MCP servers"
|
|
220
|
+
)
|
|
221
|
+
# Return the new agent (caller needs to replace the old one)
|
|
222
|
+
return new_agent
|
|
223
|
+
|
|
224
|
+
except Exception as e:
|
|
225
|
+
# Clean up connected servers if agent creation fails
|
|
226
|
+
self._logger.error(f"Failed to recreate agent with new MCP servers: {e}")
|
|
227
|
+
await self._cleanup_servers(connected_servers)
|
|
228
|
+
raise
|
|
229
|
+
|
|
230
|
+
self._logger.info("No new MCP servers to add to agent")
|
|
231
|
+
return agent
|
|
232
|
+
|
|
233
|
+
async def _cleanup_servers(self, servers: List[MCPServerStreamableHttp]) -> None:
|
|
234
|
+
"""Clean up connected MCP servers"""
|
|
235
|
+
for server in servers:
|
|
236
|
+
try:
|
|
237
|
+
if hasattr(server, "cleanup"):
|
|
238
|
+
await server.cleanup()
|
|
239
|
+
except Exception as e:
|
|
240
|
+
# Log cleanup errors but don't raise them
|
|
241
|
+
self._logger.debug(f"Error during server cleanup: {e}")
|
|
242
|
+
|
|
243
|
+
async def cleanup_all_servers(self) -> None:
|
|
244
|
+
"""Clean up all connected MCP servers"""
|
|
245
|
+
if hasattr(self, "_connected_servers"):
|
|
246
|
+
await self._cleanup_servers(self._connected_servers)
|
|
247
|
+
self._connected_servers = []
|
|
248
|
+
|
|
249
|
+
# --------------------------------------------------------------------------
|
|
250
|
+
# SEND CHAT HISTORY - OpenAI-specific implementations
|
|
251
|
+
# --------------------------------------------------------------------------
|
|
252
|
+
|
|
253
|
+
async def send_chat_history(
|
|
254
|
+
self,
|
|
255
|
+
turn_context: TurnContext,
|
|
256
|
+
session: Session,
|
|
257
|
+
limit: Optional[int] = None,
|
|
258
|
+
options: Optional[ToolOptions] = None,
|
|
259
|
+
) -> OperationResult:
|
|
260
|
+
"""
|
|
261
|
+
Extract chat history from an OpenAI Session and send it to the MCP platform.
|
|
262
|
+
|
|
263
|
+
This method extracts messages from an OpenAI Session object using get_items()
|
|
264
|
+
and sends them to the MCP platform for real-time threat protection.
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
turn_context: TurnContext from the Agents SDK containing conversation info.
|
|
268
|
+
Must have a valid activity with conversation.id, activity.id,
|
|
269
|
+
and activity.text.
|
|
270
|
+
session: OpenAI Session instance to extract messages from. Must support
|
|
271
|
+
the get_items() method which returns a list of TResponseInputItem.
|
|
272
|
+
limit: Optional maximum number of items to retrieve from session.
|
|
273
|
+
If None, retrieves all items.
|
|
274
|
+
options: Optional ToolOptions for customization. If not provided,
|
|
275
|
+
uses default options with orchestrator_name="OpenAI".
|
|
276
|
+
|
|
277
|
+
Returns:
|
|
278
|
+
OperationResult indicating success or failure. On success, returns
|
|
279
|
+
OperationResult.success(). On failure, returns OperationResult.failed()
|
|
280
|
+
with error details.
|
|
281
|
+
|
|
282
|
+
Raises:
|
|
283
|
+
ValueError: If turn_context is None or session is None.
|
|
284
|
+
|
|
285
|
+
Example:
|
|
286
|
+
>>> from agents import Agent, Runner
|
|
287
|
+
>>> from microsoft_agents_a365.tooling.extensions.openai import (
|
|
288
|
+
... McpToolRegistrationService
|
|
289
|
+
... )
|
|
290
|
+
>>>
|
|
291
|
+
>>> service = McpToolRegistrationService()
|
|
292
|
+
>>> agent = Agent(name="my-agent", model="gpt-4")
|
|
293
|
+
>>>
|
|
294
|
+
>>> # In your agent handler:
|
|
295
|
+
>>> async with Runner.run(agent, messages) as result:
|
|
296
|
+
... session = result.session
|
|
297
|
+
... op_result = await service.send_chat_history(
|
|
298
|
+
... turn_context, session
|
|
299
|
+
... )
|
|
300
|
+
... if op_result.succeeded:
|
|
301
|
+
... print("Chat history sent successfully")
|
|
302
|
+
"""
|
|
303
|
+
# Validate inputs
|
|
304
|
+
if turn_context is None:
|
|
305
|
+
raise ValueError("turn_context cannot be None")
|
|
306
|
+
if session is None:
|
|
307
|
+
raise ValueError("session cannot be None")
|
|
308
|
+
|
|
309
|
+
try:
|
|
310
|
+
# Extract messages from session
|
|
311
|
+
self._logger.info("Extracting messages from OpenAI session")
|
|
312
|
+
if limit is not None:
|
|
313
|
+
messages = session.get_items(limit=limit)
|
|
314
|
+
else:
|
|
315
|
+
messages = session.get_items()
|
|
316
|
+
|
|
317
|
+
self._logger.debug(f"Retrieved {len(messages)} items from session")
|
|
318
|
+
|
|
319
|
+
# Delegate to the list-based method
|
|
320
|
+
return await self.send_chat_history_messages(
|
|
321
|
+
turn_context=turn_context,
|
|
322
|
+
messages=messages,
|
|
323
|
+
options=options,
|
|
324
|
+
)
|
|
325
|
+
except ValueError:
|
|
326
|
+
# Re-raise validation errors
|
|
327
|
+
raise
|
|
328
|
+
except Exception as ex:
|
|
329
|
+
self._logger.error(f"Failed to send chat history from session: {ex}")
|
|
330
|
+
return OperationResult.failed(OperationError(ex))
|
|
331
|
+
|
|
332
|
+
async def send_chat_history_messages(
|
|
333
|
+
self,
|
|
334
|
+
turn_context: TurnContext,
|
|
335
|
+
messages: List[TResponseInputItem],
|
|
336
|
+
options: Optional[ToolOptions] = None,
|
|
337
|
+
) -> OperationResult:
|
|
338
|
+
"""
|
|
339
|
+
Send OpenAI chat history messages to the MCP platform for threat protection.
|
|
340
|
+
|
|
341
|
+
This method accepts a list of OpenAI TResponseInputItem messages, converts
|
|
342
|
+
them to ChatHistoryMessage format, and sends them to the MCP platform.
|
|
343
|
+
|
|
344
|
+
Args:
|
|
345
|
+
turn_context: TurnContext from the Agents SDK containing conversation info.
|
|
346
|
+
Must have a valid activity with conversation.id, activity.id,
|
|
347
|
+
and activity.text.
|
|
348
|
+
messages: List of OpenAI TResponseInputItem messages to send. Supports
|
|
349
|
+
UserMessage, AssistantMessage, SystemMessage, and other OpenAI
|
|
350
|
+
message types.
|
|
351
|
+
options: Optional ToolOptions for customization. If not provided,
|
|
352
|
+
uses default options with orchestrator_name="OpenAI".
|
|
353
|
+
|
|
354
|
+
Returns:
|
|
355
|
+
OperationResult indicating success or failure. On success, returns
|
|
356
|
+
OperationResult.success(). On failure, returns OperationResult.failed()
|
|
357
|
+
with error details.
|
|
358
|
+
|
|
359
|
+
Raises:
|
|
360
|
+
ValueError: If turn_context is None or messages is None.
|
|
361
|
+
|
|
362
|
+
Example:
|
|
363
|
+
>>> from microsoft_agents_a365.tooling.extensions.openai import (
|
|
364
|
+
... McpToolRegistrationService
|
|
365
|
+
... )
|
|
366
|
+
>>>
|
|
367
|
+
>>> service = McpToolRegistrationService()
|
|
368
|
+
>>> messages = [
|
|
369
|
+
... {"role": "user", "content": "Hello"},
|
|
370
|
+
... {"role": "assistant", "content": "Hi there!"},
|
|
371
|
+
... ]
|
|
372
|
+
>>>
|
|
373
|
+
>>> result = await service.send_chat_history_messages(
|
|
374
|
+
... turn_context, messages
|
|
375
|
+
... )
|
|
376
|
+
>>> if result.succeeded:
|
|
377
|
+
... print("Chat history sent successfully")
|
|
378
|
+
"""
|
|
379
|
+
# Validate inputs
|
|
380
|
+
if turn_context is None:
|
|
381
|
+
raise ValueError("turn_context cannot be None")
|
|
382
|
+
if messages is None:
|
|
383
|
+
raise ValueError("messages cannot be None")
|
|
384
|
+
|
|
385
|
+
# Handle empty list as no-op
|
|
386
|
+
if len(messages) == 0:
|
|
387
|
+
self._logger.info("Empty message list provided, returning success")
|
|
388
|
+
return OperationResult.success()
|
|
389
|
+
|
|
390
|
+
self._logger.info(f"Sending {len(messages)} OpenAI messages as chat history")
|
|
391
|
+
|
|
392
|
+
# Set default options
|
|
393
|
+
if options is None:
|
|
394
|
+
options = ToolOptions(orchestrator_name=self._orchestrator_name)
|
|
395
|
+
elif options.orchestrator_name is None:
|
|
396
|
+
options.orchestrator_name = self._orchestrator_name
|
|
397
|
+
|
|
398
|
+
try:
|
|
399
|
+
# Convert OpenAI messages to ChatHistoryMessage format
|
|
400
|
+
chat_history_messages = self._convert_openai_messages_to_chat_history(messages)
|
|
401
|
+
|
|
402
|
+
if len(chat_history_messages) == 0:
|
|
403
|
+
self._logger.warning("No messages could be converted to chat history format")
|
|
404
|
+
return OperationResult.success()
|
|
405
|
+
|
|
406
|
+
self._logger.debug(
|
|
407
|
+
f"Converted {len(chat_history_messages)} messages to ChatHistoryMessage format"
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
# Delegate to core service
|
|
411
|
+
return await self.config_service.send_chat_history(
|
|
412
|
+
turn_context=turn_context,
|
|
413
|
+
chat_history_messages=chat_history_messages,
|
|
414
|
+
options=options,
|
|
415
|
+
)
|
|
416
|
+
except ValueError:
|
|
417
|
+
# Re-raise validation errors from the core service
|
|
418
|
+
raise
|
|
419
|
+
except Exception as ex:
|
|
420
|
+
self._logger.error(f"Failed to send chat history messages: {ex}")
|
|
421
|
+
return OperationResult.failed(OperationError(ex))
|
|
422
|
+
|
|
423
|
+
# --------------------------------------------------------------------------
|
|
424
|
+
# PRIVATE HELPER METHODS - Message Conversion
|
|
425
|
+
# --------------------------------------------------------------------------
|
|
426
|
+
|
|
427
|
+
def _convert_openai_messages_to_chat_history(
|
|
428
|
+
self, messages: List[TResponseInputItem]
|
|
429
|
+
) -> List[ChatHistoryMessage]:
|
|
430
|
+
"""
|
|
431
|
+
Convert a list of OpenAI messages to ChatHistoryMessage format.
|
|
432
|
+
|
|
433
|
+
Args:
|
|
434
|
+
messages: List of OpenAI TResponseInputItem messages.
|
|
435
|
+
|
|
436
|
+
Returns:
|
|
437
|
+
List of ChatHistoryMessage objects. Messages that cannot be converted
|
|
438
|
+
are filtered out with a warning log.
|
|
439
|
+
"""
|
|
440
|
+
chat_history_messages: List[ChatHistoryMessage] = []
|
|
441
|
+
|
|
442
|
+
for idx, message in enumerate(messages):
|
|
443
|
+
converted = self._convert_single_message(message, idx)
|
|
444
|
+
if converted is not None:
|
|
445
|
+
chat_history_messages.append(converted)
|
|
446
|
+
|
|
447
|
+
self._logger.info(
|
|
448
|
+
f"Converted {len(chat_history_messages)} of {len(messages)} messages "
|
|
449
|
+
"to ChatHistoryMessage format"
|
|
450
|
+
)
|
|
451
|
+
return chat_history_messages
|
|
452
|
+
|
|
453
|
+
def _convert_single_message(
|
|
454
|
+
self, message: TResponseInputItem, index: int = 0
|
|
455
|
+
) -> Optional[ChatHistoryMessage]:
|
|
456
|
+
"""
|
|
457
|
+
Convert a single OpenAI message to ChatHistoryMessage format.
|
|
458
|
+
|
|
459
|
+
Args:
|
|
460
|
+
message: Single OpenAI TResponseInputItem message.
|
|
461
|
+
index: Index of the message in the list (for logging).
|
|
462
|
+
|
|
463
|
+
Returns:
|
|
464
|
+
ChatHistoryMessage object or None if conversion fails.
|
|
465
|
+
"""
|
|
466
|
+
try:
|
|
467
|
+
role = self._extract_role(message)
|
|
468
|
+
content = self._extract_content(message)
|
|
469
|
+
msg_id = self._extract_id(message)
|
|
470
|
+
timestamp = self._extract_timestamp(message)
|
|
471
|
+
|
|
472
|
+
self._logger.debug(
|
|
473
|
+
f"Converting message {index}: role={role}, "
|
|
474
|
+
f"has_id={msg_id is not None}, has_timestamp={timestamp is not None}"
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
# Skip messages with empty content after extraction
|
|
478
|
+
# The ChatHistoryMessage validator requires non-empty content
|
|
479
|
+
if not content or not content.strip():
|
|
480
|
+
self._logger.warning(f"Message {index} has empty content, skipping")
|
|
481
|
+
return None
|
|
482
|
+
|
|
483
|
+
return ChatHistoryMessage(
|
|
484
|
+
id=msg_id,
|
|
485
|
+
role=role,
|
|
486
|
+
content=content,
|
|
487
|
+
timestamp=timestamp,
|
|
488
|
+
)
|
|
489
|
+
except Exception as ex:
|
|
490
|
+
self._logger.error(f"Failed to convert message {index}: {ex}")
|
|
491
|
+
return None
|
|
492
|
+
|
|
493
|
+
def _extract_role(self, message: TResponseInputItem) -> str:
|
|
494
|
+
"""
|
|
495
|
+
Extract the role from an OpenAI message.
|
|
496
|
+
|
|
497
|
+
Role mapping:
|
|
498
|
+
- UserMessage or role="user" -> "user"
|
|
499
|
+
- AssistantMessage or role="assistant" -> "assistant"
|
|
500
|
+
- SystemMessage or role="system" -> "system"
|
|
501
|
+
- ResponseOutputMessage with role="assistant" -> "assistant"
|
|
502
|
+
- Unknown types -> "user" (default fallback with warning)
|
|
503
|
+
|
|
504
|
+
Args:
|
|
505
|
+
message: OpenAI message object.
|
|
506
|
+
|
|
507
|
+
Returns:
|
|
508
|
+
Role string: "user", "assistant", or "system".
|
|
509
|
+
"""
|
|
510
|
+
# Check for role attribute directly
|
|
511
|
+
if hasattr(message, "role"):
|
|
512
|
+
role = message.role
|
|
513
|
+
if role in ("user", "assistant", "system"):
|
|
514
|
+
return role
|
|
515
|
+
|
|
516
|
+
# Check message type by class name
|
|
517
|
+
type_name = type(message).__name__
|
|
518
|
+
|
|
519
|
+
if "UserMessage" in type_name or "user" in type_name.lower():
|
|
520
|
+
return "user"
|
|
521
|
+
elif "AssistantMessage" in type_name or "assistant" in type_name.lower():
|
|
522
|
+
return "assistant"
|
|
523
|
+
elif "SystemMessage" in type_name or "system" in type_name.lower():
|
|
524
|
+
return "system"
|
|
525
|
+
elif "ResponseOutputMessage" in type_name:
|
|
526
|
+
# ResponseOutputMessage typically has role attribute
|
|
527
|
+
if hasattr(message, "role") and message.role == "assistant":
|
|
528
|
+
return "assistant"
|
|
529
|
+
return "assistant" # Default for response output
|
|
530
|
+
|
|
531
|
+
# For dict-like objects
|
|
532
|
+
if isinstance(message, dict):
|
|
533
|
+
role = message.get("role", "")
|
|
534
|
+
if role in ("user", "assistant", "system"):
|
|
535
|
+
return role
|
|
536
|
+
|
|
537
|
+
# Default fallback with warning
|
|
538
|
+
self._logger.warning(f"Unknown message type {type_name}, defaulting to 'user' role")
|
|
539
|
+
return "user"
|
|
540
|
+
|
|
541
|
+
def _extract_content(self, message: TResponseInputItem) -> str:
|
|
542
|
+
"""
|
|
543
|
+
Extract text content from an OpenAI message.
|
|
544
|
+
|
|
545
|
+
Content extraction priority:
|
|
546
|
+
1. If message has .content as string -> use directly
|
|
547
|
+
2. If message has .content as list -> concatenate all text parts
|
|
548
|
+
3. If message has .text attribute -> use directly
|
|
549
|
+
4. If content is empty/None -> return empty string with warning
|
|
550
|
+
|
|
551
|
+
Args:
|
|
552
|
+
message: OpenAI message object.
|
|
553
|
+
|
|
554
|
+
Returns:
|
|
555
|
+
Extracted text content as string.
|
|
556
|
+
"""
|
|
557
|
+
content = ""
|
|
558
|
+
|
|
559
|
+
# Try .content attribute first
|
|
560
|
+
if hasattr(message, "content"):
|
|
561
|
+
raw_content = message.content
|
|
562
|
+
|
|
563
|
+
if isinstance(raw_content, str):
|
|
564
|
+
content = raw_content
|
|
565
|
+
elif isinstance(raw_content, list):
|
|
566
|
+
# Concatenate text parts from content list
|
|
567
|
+
text_parts = []
|
|
568
|
+
for part in raw_content:
|
|
569
|
+
if isinstance(part, str):
|
|
570
|
+
text_parts.append(part)
|
|
571
|
+
elif hasattr(part, "text"):
|
|
572
|
+
text_parts.append(str(part.text))
|
|
573
|
+
elif isinstance(part, dict):
|
|
574
|
+
if "text" in part:
|
|
575
|
+
text_parts.append(str(part["text"]))
|
|
576
|
+
elif part.get("type") == "text" and "text" in part:
|
|
577
|
+
text_parts.append(str(part["text"]))
|
|
578
|
+
content = " ".join(text_parts)
|
|
579
|
+
|
|
580
|
+
# Try .text attribute as fallback
|
|
581
|
+
if not content and hasattr(message, "text"):
|
|
582
|
+
content = str(message.text) if message.text else ""
|
|
583
|
+
|
|
584
|
+
# Try dict-like access
|
|
585
|
+
if not content and isinstance(message, dict):
|
|
586
|
+
content = message.get("content", "") or message.get("text", "") or ""
|
|
587
|
+
if isinstance(content, list):
|
|
588
|
+
text_parts = []
|
|
589
|
+
for part in content:
|
|
590
|
+
if isinstance(part, str):
|
|
591
|
+
text_parts.append(part)
|
|
592
|
+
elif isinstance(part, dict) and "text" in part:
|
|
593
|
+
text_parts.append(str(part["text"]))
|
|
594
|
+
content = " ".join(text_parts)
|
|
595
|
+
|
|
596
|
+
if not content:
|
|
597
|
+
self._logger.warning("Message has empty content, using empty string")
|
|
598
|
+
|
|
599
|
+
return content
|
|
600
|
+
|
|
601
|
+
def _extract_id(self, message: TResponseInputItem) -> str:
|
|
602
|
+
"""
|
|
603
|
+
Extract or generate a unique ID for the message.
|
|
604
|
+
|
|
605
|
+
If the message has an existing ID, it is preserved. Otherwise,
|
|
606
|
+
a new UUID is generated.
|
|
607
|
+
|
|
608
|
+
Args:
|
|
609
|
+
message: OpenAI message object.
|
|
610
|
+
|
|
611
|
+
Returns:
|
|
612
|
+
Message ID as string.
|
|
613
|
+
"""
|
|
614
|
+
# Try to get existing ID
|
|
615
|
+
existing_id = None
|
|
616
|
+
|
|
617
|
+
if hasattr(message, "id") and message.id:
|
|
618
|
+
existing_id = str(message.id)
|
|
619
|
+
elif isinstance(message, dict) and message.get("id"):
|
|
620
|
+
existing_id = str(message["id"])
|
|
621
|
+
|
|
622
|
+
if existing_id:
|
|
623
|
+
return existing_id
|
|
624
|
+
|
|
625
|
+
# Generate new UUID
|
|
626
|
+
generated_id = str(uuid.uuid4())
|
|
627
|
+
self._logger.debug(f"Generated UUID {generated_id} for message without ID")
|
|
628
|
+
return generated_id
|
|
629
|
+
|
|
630
|
+
def _extract_timestamp(self, message: TResponseInputItem) -> datetime:
|
|
631
|
+
"""
|
|
632
|
+
Extract or generate a timestamp for the message.
|
|
633
|
+
|
|
634
|
+
If the message has an existing timestamp, it is preserved. Otherwise,
|
|
635
|
+
the current UTC time is used.
|
|
636
|
+
|
|
637
|
+
Args:
|
|
638
|
+
message: OpenAI message object.
|
|
639
|
+
|
|
640
|
+
Returns:
|
|
641
|
+
Timestamp as datetime object.
|
|
642
|
+
"""
|
|
643
|
+
# Try to get existing timestamp
|
|
644
|
+
existing_timestamp = None
|
|
645
|
+
|
|
646
|
+
if hasattr(message, "timestamp") and message.timestamp:
|
|
647
|
+
existing_timestamp = message.timestamp
|
|
648
|
+
elif hasattr(message, "created_at") and message.created_at:
|
|
649
|
+
existing_timestamp = message.created_at
|
|
650
|
+
elif isinstance(message, dict):
|
|
651
|
+
existing_timestamp = message.get("timestamp") or message.get("created_at")
|
|
652
|
+
|
|
653
|
+
if existing_timestamp:
|
|
654
|
+
# Convert to datetime if needed
|
|
655
|
+
if isinstance(existing_timestamp, datetime):
|
|
656
|
+
return existing_timestamp
|
|
657
|
+
elif isinstance(existing_timestamp, (int, float)):
|
|
658
|
+
# Unix timestamp
|
|
659
|
+
return datetime.fromtimestamp(existing_timestamp, tz=timezone.utc)
|
|
660
|
+
elif isinstance(existing_timestamp, str):
|
|
661
|
+
# Try ISO format parsing
|
|
662
|
+
try:
|
|
663
|
+
return datetime.fromisoformat(existing_timestamp.replace("Z", "+00:00"))
|
|
664
|
+
except ValueError:
|
|
665
|
+
pass
|
|
666
|
+
|
|
667
|
+
# Use current UTC time
|
|
668
|
+
self._logger.debug("Using current UTC time for message without timestamp")
|
|
669
|
+
return datetime.now(timezone.utc)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: microsoft-agents-a365-tooling-extensions-openai
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.2.1.dev0
|
|
4
4
|
Summary: OpenAI integration for Agent 365 Tooling SDK
|
|
5
5
|
Author-email: Microsoft <support@microsoft.com>
|
|
6
6
|
License: MIT
|
|
@@ -18,6 +18,7 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
|
18
18
|
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
19
19
|
Requires-Python: >=3.11
|
|
20
20
|
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
21
22
|
Requires-Dist: microsoft-agents-a365-tooling>=0.0.0
|
|
22
23
|
Requires-Dist: openai-agents
|
|
23
24
|
Requires-Dist: asyncio-throttle
|
|
@@ -30,6 +31,7 @@ Requires-Dist: mypy>=1.0.0; extra == "dev"
|
|
|
30
31
|
Provides-Extra: test
|
|
31
32
|
Requires-Dist: pytest>=7.0.0; extra == "test"
|
|
32
33
|
Requires-Dist: pytest-asyncio>=0.21.0; extra == "test"
|
|
34
|
+
Dynamic: license-file
|
|
33
35
|
|
|
34
36
|
# microsoft-agents-a365-tooling-extensions-openai
|
|
35
37
|
|
|
@@ -53,7 +55,7 @@ For usage examples and detailed documentation, see the [Tooling documentation](h
|
|
|
53
55
|
For issues, questions, or feedback:
|
|
54
56
|
|
|
55
57
|
- File issues in the [GitHub Issues](https://github.com/microsoft/Agent365-python/issues) section
|
|
56
|
-
- See the [main documentation](
|
|
58
|
+
- See the [main documentation](../../README.md) for more information
|
|
57
59
|
|
|
58
60
|
## Trademarks
|
|
59
61
|
|
|
@@ -63,4 +65,4 @@ For issues, questions, or feedback:
|
|
|
63
65
|
|
|
64
66
|
Copyright (c) Microsoft Corporation. All rights reserved.
|
|
65
67
|
|
|
66
|
-
Licensed under the MIT License - see the [LICENSE](
|
|
68
|
+
Licensed under the MIT License - see the [LICENSE](../../LICENSE.md) file for details.
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
README.md
|
|
2
2
|
pyproject.toml
|
|
3
3
|
setup.py
|
|
4
|
+
../../LICENSE
|
|
5
|
+
docs/../../LICENSE
|
|
6
|
+
microsoft_agents_a365/../../LICENSE
|
|
4
7
|
microsoft_agents_a365/tooling/extensions/openai/__init__.py
|
|
5
8
|
microsoft_agents_a365/tooling/extensions/openai/mcp_tool_registration_service.py
|
|
6
9
|
microsoft_agents_a365_tooling_extensions_openai.egg-info/PKG-INFO
|
|
@@ -65,6 +65,10 @@ target-version = ['py311']
|
|
|
65
65
|
line-length = 100
|
|
66
66
|
target-version = "py311"
|
|
67
67
|
|
|
68
|
+
[tool.ruff.lint.flake8-copyright]
|
|
69
|
+
notice-rgx = "# Copyright \\(c\\) Microsoft Corporation\\.\\r?\\n# Licensed under the MIT License\\."
|
|
70
|
+
min-file-size = 1
|
|
71
|
+
|
|
68
72
|
[tool.mypy]
|
|
69
73
|
python_version = "3.11"
|
|
70
74
|
strict = true
|
|
@@ -13,7 +13,7 @@ package_version = environ.get("AGENT365_PYTHON_SDK_PACKAGE_VERSION", "0.0.0")
|
|
|
13
13
|
helper_path = Path(__file__).parent.parent.parent / "versioning" / "helper"
|
|
14
14
|
sys.path.insert(0, str(helper_path))
|
|
15
15
|
|
|
16
|
-
from setup_utils import get_dynamic_dependencies
|
|
16
|
+
from setup_utils import get_dynamic_dependencies # noqa: E402
|
|
17
17
|
|
|
18
18
|
# Use minimum version strategy:
|
|
19
19
|
# - Internal packages get: >= current_base_version (e.g., >= 0.1.0)
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
# Copyright (c) Microsoft Corporation.
|
|
2
|
-
# Licensed under the MIT License.
|
|
3
|
-
|
|
4
|
-
"""
|
|
5
|
-
OpenAI extensions for Microsoft Agent 365 Tooling SDK
|
|
6
|
-
|
|
7
|
-
Tooling and utilities specifically for OpenAI framework integration.
|
|
8
|
-
Provides OpenAI-specific helper utilities.
|
|
9
|
-
"""
|
|
10
|
-
|
|
11
|
-
__version__ = "1.0.0"
|
|
@@ -1,223 +0,0 @@
|
|
|
1
|
-
# Copyright (c) Microsoft. All rights reserved.
|
|
2
|
-
|
|
3
|
-
from typing import Dict, Optional
|
|
4
|
-
from dataclasses import dataclass
|
|
5
|
-
import logging
|
|
6
|
-
|
|
7
|
-
from agents import Agent
|
|
8
|
-
|
|
9
|
-
from microsoft_agents.hosting.core import Authorization, TurnContext
|
|
10
|
-
|
|
11
|
-
from agents.mcp import (
|
|
12
|
-
MCPServerStreamableHttp,
|
|
13
|
-
MCPServerStreamableHttpParams,
|
|
14
|
-
)
|
|
15
|
-
from microsoft_agents_a365.runtime.utility import Utility
|
|
16
|
-
from microsoft_agents_a365.tooling.services.mcp_tool_server_configuration_service import (
|
|
17
|
-
McpToolServerConfigurationService,
|
|
18
|
-
)
|
|
19
|
-
|
|
20
|
-
from microsoft_agents_a365.tooling.utils.utility import (
|
|
21
|
-
get_mcp_platform_authentication_scope,
|
|
22
|
-
)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
# TODO: This is not needed. Remove this.
|
|
26
|
-
@dataclass
|
|
27
|
-
class MCPServerInfo:
|
|
28
|
-
"""Information about an MCP server"""
|
|
29
|
-
|
|
30
|
-
name: str
|
|
31
|
-
url: str
|
|
32
|
-
server_type: str = "streamable_http" # hosted, streamable_http, sse, stdio
|
|
33
|
-
headers: Optional[Dict[str, str]] = None
|
|
34
|
-
require_approval: str = "never"
|
|
35
|
-
timeout: int = 30 # Timeout in seconds (will be converted to milliseconds for MCPServerStreamableHttpParams)
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
class McpToolRegistrationService:
|
|
39
|
-
"""Service for managing MCP tools and servers for an agent"""
|
|
40
|
-
|
|
41
|
-
def __init__(self, logger: Optional[logging.Logger] = None):
|
|
42
|
-
"""
|
|
43
|
-
Initialize the MCP Tool Registration Service for OpenAI.
|
|
44
|
-
|
|
45
|
-
Args:
|
|
46
|
-
logger: Logger instance for logging operations.
|
|
47
|
-
"""
|
|
48
|
-
self._logger = logger or logging.getLogger(self.__class__.__name__)
|
|
49
|
-
self.config_service = McpToolServerConfigurationService(logger=self._logger)
|
|
50
|
-
|
|
51
|
-
async def add_tool_servers_to_agent(
|
|
52
|
-
self,
|
|
53
|
-
agent: Agent,
|
|
54
|
-
auth: Authorization,
|
|
55
|
-
auth_handler_name: str,
|
|
56
|
-
context: TurnContext,
|
|
57
|
-
auth_token: Optional[str] = None,
|
|
58
|
-
):
|
|
59
|
-
"""
|
|
60
|
-
Add new MCP servers to the agent by creating a new Agent instance.
|
|
61
|
-
|
|
62
|
-
Note: Due to OpenAI Agents SDK limitations, MCP servers must be set during
|
|
63
|
-
Agent creation. If new servers are found, this method creates a new Agent
|
|
64
|
-
instance with all MCP servers (existing + new) properly initialized.
|
|
65
|
-
|
|
66
|
-
Args:
|
|
67
|
-
agent: The existing agent to add servers to
|
|
68
|
-
auth: Authorization handler for token exchange.
|
|
69
|
-
auth_handler_name: Name of the authorization handler.
|
|
70
|
-
context: Turn context for the current operation.
|
|
71
|
-
auth_token: Authentication token to access the MCP servers.
|
|
72
|
-
|
|
73
|
-
Returns:
|
|
74
|
-
New Agent instance with all MCP servers, or original agent if no new servers
|
|
75
|
-
"""
|
|
76
|
-
|
|
77
|
-
if not auth_token:
|
|
78
|
-
scopes = get_mcp_platform_authentication_scope()
|
|
79
|
-
authToken = await auth.exchange_token(context, scopes, auth_handler_name)
|
|
80
|
-
auth_token = authToken.token
|
|
81
|
-
|
|
82
|
-
# Get MCP server configurations from the configuration service
|
|
83
|
-
# mcp_server_configs = []
|
|
84
|
-
# TODO: radevika: Update once the common project is merged.
|
|
85
|
-
|
|
86
|
-
agentic_app_id = Utility.resolve_agent_identity(context, auth_token)
|
|
87
|
-
self._logger.info(f"Listing MCP tool servers for agent {agentic_app_id}")
|
|
88
|
-
mcp_server_configs = await self.config_service.list_tool_servers(
|
|
89
|
-
agentic_app_id=agentic_app_id,
|
|
90
|
-
auth_token=auth_token,
|
|
91
|
-
)
|
|
92
|
-
|
|
93
|
-
self._logger.info(f"Loaded {len(mcp_server_configs)} MCP server configurations")
|
|
94
|
-
|
|
95
|
-
# Convert MCP server configs to MCPServerInfo objects
|
|
96
|
-
mcp_servers_info = []
|
|
97
|
-
for server_config in mcp_server_configs:
|
|
98
|
-
server_info = MCPServerInfo(
|
|
99
|
-
name=server_config.mcp_server_name,
|
|
100
|
-
url=server_config.mcp_server_unique_name,
|
|
101
|
-
)
|
|
102
|
-
mcp_servers_info.append(server_info)
|
|
103
|
-
|
|
104
|
-
# Get existing MCP servers from the agent
|
|
105
|
-
existing_mcp_servers = (
|
|
106
|
-
list(agent.mcp_servers) if hasattr(agent, "mcp_servers") and agent.mcp_servers else []
|
|
107
|
-
)
|
|
108
|
-
|
|
109
|
-
# Prepare new MCP servers to add
|
|
110
|
-
new_mcp_servers = []
|
|
111
|
-
connected_servers = []
|
|
112
|
-
|
|
113
|
-
existing_server_urls = []
|
|
114
|
-
for server in existing_mcp_servers:
|
|
115
|
-
# Check for URL in params dict (MCPServerStreamableHttp stores URL in params["url"])
|
|
116
|
-
if (
|
|
117
|
-
hasattr(server, "params")
|
|
118
|
-
and isinstance(server.params, dict)
|
|
119
|
-
and "url" in server.params
|
|
120
|
-
):
|
|
121
|
-
existing_server_urls.append(server.params["url"])
|
|
122
|
-
elif hasattr(server, "params") and hasattr(server.params, "url"):
|
|
123
|
-
existing_server_urls.append(server.params.url)
|
|
124
|
-
elif hasattr(server, "url"):
|
|
125
|
-
existing_server_urls.append(server.url)
|
|
126
|
-
|
|
127
|
-
for si in mcp_servers_info:
|
|
128
|
-
# Check if MCP server already exists
|
|
129
|
-
|
|
130
|
-
if si.url not in existing_server_urls:
|
|
131
|
-
try:
|
|
132
|
-
# Prepare headers with authorization
|
|
133
|
-
headers = si.headers or {}
|
|
134
|
-
if auth_token:
|
|
135
|
-
headers["Authorization"] = f"Bearer {auth_token}"
|
|
136
|
-
|
|
137
|
-
# Create MCPServerStreamableHttpParams with proper configuration
|
|
138
|
-
params = MCPServerStreamableHttpParams(url=si.url, headers=headers)
|
|
139
|
-
|
|
140
|
-
# Create MCP server
|
|
141
|
-
mcp_server = MCPServerStreamableHttp(params=params, name=si.name)
|
|
142
|
-
|
|
143
|
-
# CRITICAL: Connect the server before adding it to the agent
|
|
144
|
-
# This fixes the "Server not initialized. Make sure you call `connect()` first." error
|
|
145
|
-
# TODO: When App Manifest scenario lits up for onboarding agent, we need to pull a flag and disconnect if the flag is disabled.
|
|
146
|
-
await mcp_server.connect()
|
|
147
|
-
|
|
148
|
-
new_mcp_servers.append(mcp_server)
|
|
149
|
-
connected_servers.append(mcp_server)
|
|
150
|
-
|
|
151
|
-
existing_server_urls.append(si.url)
|
|
152
|
-
self._logger.info(
|
|
153
|
-
f"Successfully connected to MCP server '{si.name}' at {si.url}"
|
|
154
|
-
)
|
|
155
|
-
|
|
156
|
-
except Exception as e:
|
|
157
|
-
# Log the error but continue with other servers
|
|
158
|
-
self._logger.warning(
|
|
159
|
-
f"Failed to connect to MCP server {si.name} at {si.url}: {e}"
|
|
160
|
-
)
|
|
161
|
-
continue
|
|
162
|
-
|
|
163
|
-
# If we have new servers, we need to recreate the agent
|
|
164
|
-
# The OpenAI Agents SDK requires MCP servers to be set during agent creation
|
|
165
|
-
if new_mcp_servers:
|
|
166
|
-
try:
|
|
167
|
-
self._logger.info(f"Recreating agent with {len(new_mcp_servers)} new MCP servers")
|
|
168
|
-
all_mcp_servers = existing_mcp_servers + new_mcp_servers
|
|
169
|
-
|
|
170
|
-
# Recreate the agent with all MCP servers
|
|
171
|
-
from agents import Agent
|
|
172
|
-
|
|
173
|
-
new_agent = Agent(
|
|
174
|
-
name=agent.name,
|
|
175
|
-
model=agent.model,
|
|
176
|
-
model_settings=agent.model_settings
|
|
177
|
-
if hasattr(agent, "model_settings")
|
|
178
|
-
else None,
|
|
179
|
-
instructions=agent.instructions,
|
|
180
|
-
tools=agent.tools,
|
|
181
|
-
mcp_servers=all_mcp_servers,
|
|
182
|
-
)
|
|
183
|
-
|
|
184
|
-
# Copy agent attributes to preserve state
|
|
185
|
-
for attr_name in ["name", "model", "instructions", "tools"]:
|
|
186
|
-
if hasattr(agent, attr_name):
|
|
187
|
-
setattr(new_agent, attr_name, getattr(agent, attr_name))
|
|
188
|
-
|
|
189
|
-
# Store connected servers for potential cleanup
|
|
190
|
-
if not hasattr(self, "_connected_servers"):
|
|
191
|
-
self._connected_servers = []
|
|
192
|
-
self._connected_servers.extend(connected_servers)
|
|
193
|
-
|
|
194
|
-
self._logger.info(
|
|
195
|
-
f"Agent recreated successfully with {len(all_mcp_servers)} total MCP servers"
|
|
196
|
-
)
|
|
197
|
-
# Return the new agent (caller needs to replace the old one)
|
|
198
|
-
return new_agent
|
|
199
|
-
|
|
200
|
-
except Exception as e:
|
|
201
|
-
# Clean up connected servers if agent creation fails
|
|
202
|
-
self._logger.error(f"Failed to recreate agent with new MCP servers: {e}")
|
|
203
|
-
await self._cleanup_servers(connected_servers)
|
|
204
|
-
raise e
|
|
205
|
-
|
|
206
|
-
self._logger.info("No new MCP servers to add to agent")
|
|
207
|
-
return agent
|
|
208
|
-
|
|
209
|
-
async def _cleanup_servers(self, servers):
|
|
210
|
-
"""Clean up connected MCP servers"""
|
|
211
|
-
for server in servers:
|
|
212
|
-
try:
|
|
213
|
-
if hasattr(server, "cleanup"):
|
|
214
|
-
await server.cleanup()
|
|
215
|
-
except Exception as e:
|
|
216
|
-
# Log cleanup errors but don't raise them
|
|
217
|
-
self._logger.debug(f"Error during server cleanup: {e}")
|
|
218
|
-
|
|
219
|
-
async def cleanup_all_servers(self):
|
|
220
|
-
"""Clean up all connected MCP servers"""
|
|
221
|
-
if hasattr(self, "_connected_servers"):
|
|
222
|
-
await self._cleanup_servers(self._connected_servers)
|
|
223
|
-
self._connected_servers = []
|
|
File without changes
|
|
File without changes
|