mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Python: Pin meta core (#2597)
* pin core dependency in the meta package * fix for mcp latest * fix mypy * test fix * test fix * fix tests for content parsing
This commit is contained in:
committed by
GitHub
Unverified
parent
931197ba95
commit
a81279d960
@@ -5,7 +5,7 @@ import logging
|
||||
import re
|
||||
import sys
|
||||
from abc import abstractmethod
|
||||
from collections.abc import Collection
|
||||
from collections.abc import Collection, Sequence
|
||||
from contextlib import AsyncExitStack, _AsyncGeneratorContextManager # type: ignore
|
||||
from datetime import timedelta
|
||||
from functools import partial
|
||||
@@ -22,7 +22,16 @@ from mcp.shared.session import RequestResponder
|
||||
from pydantic import BaseModel, Field, create_model
|
||||
|
||||
from ._tools import AIFunction, HostedMCPSpecificApproval
|
||||
from ._types import ChatMessage, Contents, DataContent, Role, TextContent, UriContent
|
||||
from ._types import (
|
||||
ChatMessage,
|
||||
Contents,
|
||||
DataContent,
|
||||
FunctionCallContent,
|
||||
FunctionResultContent,
|
||||
Role,
|
||||
TextContent,
|
||||
UriContent,
|
||||
)
|
||||
from .exceptions import ToolException, ToolExecutionException
|
||||
|
||||
if sys.version_info >= (3, 11):
|
||||
@@ -61,7 +70,7 @@ def _mcp_prompt_message_to_chat_message(
|
||||
"""Convert a MCP container type to a Agent Framework type."""
|
||||
return ChatMessage(
|
||||
role=Role(value=mcp_type.role),
|
||||
contents=[_mcp_type_to_ai_content(mcp_type.content)],
|
||||
contents=_mcp_type_to_ai_content(mcp_type.content),
|
||||
raw_representation=mcp_type,
|
||||
)
|
||||
|
||||
@@ -87,8 +96,7 @@ def _mcp_call_tool_result_to_ai_contents(
|
||||
A list of Agent Framework content items with metadata merged into
|
||||
additional_properties.
|
||||
"""
|
||||
# Extract _meta field using getattr for compatibility
|
||||
meta_data = getattr(mcp_type, "_meta", None)
|
||||
meta_data = mcp_type.meta
|
||||
|
||||
# Prepare merged metadata once if present
|
||||
merged_meta_props = None
|
||||
@@ -104,53 +112,104 @@ def _mcp_call_tool_result_to_ai_contents(
|
||||
# Convert each content item and merge metadata
|
||||
result_contents = []
|
||||
for item in mcp_type.content:
|
||||
content = _mcp_type_to_ai_content(item)
|
||||
contents = _mcp_type_to_ai_content(item)
|
||||
|
||||
if merged_meta_props:
|
||||
existing_props = getattr(content, "additional_properties", None) or {}
|
||||
# Merge with content-specific properties, letting content-specific props override
|
||||
final_props = merged_meta_props.copy()
|
||||
final_props.update(existing_props)
|
||||
content.additional_properties = final_props
|
||||
result_contents.append(content)
|
||||
|
||||
for content in contents:
|
||||
existing_props = getattr(content, "additional_properties", None) or {}
|
||||
# Merge with content-specific properties, letting content-specific props override
|
||||
final_props = merged_meta_props.copy()
|
||||
final_props.update(existing_props)
|
||||
content.additional_properties = final_props
|
||||
result_contents.extend(contents)
|
||||
return result_contents
|
||||
|
||||
|
||||
def _mcp_type_to_ai_content(
|
||||
mcp_type: types.ImageContent | types.TextContent | types.AudioContent | types.EmbeddedResource | types.ResourceLink,
|
||||
) -> Contents:
|
||||
mcp_type: types.ImageContent
|
||||
| types.TextContent
|
||||
| types.AudioContent
|
||||
| types.EmbeddedResource
|
||||
| types.ResourceLink
|
||||
| types.ToolUseContent
|
||||
| types.ToolResultContent
|
||||
| Sequence[
|
||||
types.ImageContent
|
||||
| types.TextContent
|
||||
| types.AudioContent
|
||||
| types.EmbeddedResource
|
||||
| types.ResourceLink
|
||||
| types.ToolUseContent
|
||||
| types.ToolResultContent
|
||||
],
|
||||
) -> list[Contents]:
|
||||
"""Convert a MCP type to a Agent Framework type."""
|
||||
match mcp_type:
|
||||
case types.TextContent():
|
||||
return TextContent(text=mcp_type.text, raw_representation=mcp_type)
|
||||
case types.ImageContent() | types.AudioContent():
|
||||
return DataContent(
|
||||
uri=mcp_type.data,
|
||||
media_type=mcp_type.mimeType,
|
||||
raw_representation=mcp_type,
|
||||
)
|
||||
case types.ResourceLink():
|
||||
return UriContent(
|
||||
uri=str(mcp_type.uri),
|
||||
media_type=mcp_type.mimeType or "application/json",
|
||||
raw_representation=mcp_type,
|
||||
)
|
||||
case _:
|
||||
match mcp_type.resource:
|
||||
case types.TextResourceContents():
|
||||
return TextContent(
|
||||
text=mcp_type.resource.text,
|
||||
mcp_types = mcp_type if isinstance(mcp_type, Sequence) else [mcp_type]
|
||||
return_types: list[Contents] = []
|
||||
for mcp_type in mcp_types:
|
||||
match mcp_type:
|
||||
case types.TextContent():
|
||||
return_types.append(TextContent(text=mcp_type.text, raw_representation=mcp_type))
|
||||
case types.ImageContent() | types.AudioContent():
|
||||
return_types.append(
|
||||
DataContent(
|
||||
uri=mcp_type.data,
|
||||
media_type=mcp_type.mimeType,
|
||||
raw_representation=mcp_type,
|
||||
additional_properties=(mcp_type.annotations.model_dump() if mcp_type.annotations else None),
|
||||
)
|
||||
case types.BlobResourceContents():
|
||||
return DataContent(
|
||||
uri=mcp_type.resource.blob,
|
||||
media_type=mcp_type.resource.mimeType,
|
||||
)
|
||||
case types.ResourceLink():
|
||||
return_types.append(
|
||||
UriContent(
|
||||
uri=str(mcp_type.uri),
|
||||
media_type=mcp_type.mimeType or "application/json",
|
||||
raw_representation=mcp_type,
|
||||
additional_properties=(mcp_type.annotations.model_dump() if mcp_type.annotations else None),
|
||||
)
|
||||
)
|
||||
case types.ToolUseContent():
|
||||
return_types.append(
|
||||
FunctionCallContent(
|
||||
call_id=mcp_type.id,
|
||||
name=mcp_type.name,
|
||||
arguments=mcp_type.input,
|
||||
raw_representation=mcp_type,
|
||||
)
|
||||
)
|
||||
case types.ToolResultContent():
|
||||
return_types.append(
|
||||
FunctionResultContent(
|
||||
call_id=mcp_type.toolUseId,
|
||||
result=_mcp_type_to_ai_content(mcp_type.content)
|
||||
if mcp_type.content
|
||||
else mcp_type.structuredContent,
|
||||
exception=Exception() if mcp_type.isError else None,
|
||||
raw_representation=mcp_type,
|
||||
)
|
||||
)
|
||||
case types.EmbeddedResource():
|
||||
match mcp_type.resource:
|
||||
case types.TextResourceContents():
|
||||
return_types.append(
|
||||
TextContent(
|
||||
text=mcp_type.resource.text,
|
||||
raw_representation=mcp_type,
|
||||
additional_properties=(
|
||||
mcp_type.annotations.model_dump() if mcp_type.annotations else None
|
||||
),
|
||||
)
|
||||
)
|
||||
case types.BlobResourceContents():
|
||||
return_types.append(
|
||||
DataContent(
|
||||
uri=mcp_type.resource.blob,
|
||||
media_type=mcp_type.resource.mimeType,
|
||||
raw_representation=mcp_type,
|
||||
additional_properties=(
|
||||
mcp_type.annotations.model_dump() if mcp_type.annotations else None
|
||||
),
|
||||
)
|
||||
)
|
||||
return return_types
|
||||
|
||||
|
||||
def _ai_content_to_mcp_types(
|
||||
|
||||
@@ -91,9 +91,10 @@ def test_mcp_call_tool_result_to_ai_contents():
|
||||
def test_mcp_call_tool_result_with_meta_error():
|
||||
"""Test conversion from MCP tool result with _meta field containing isError=True."""
|
||||
# Create a mock CallToolResult with _meta field containing error information
|
||||
mcp_result = types.CallToolResult(content=[types.TextContent(type="text", text="Error occurred")])
|
||||
# Simulate _meta field with isError=True
|
||||
mcp_result._meta = {"isError": True, "errorCode": "TOOL_ERROR", "errorMessage": "Tool execution failed"}
|
||||
mcp_result = types.CallToolResult(
|
||||
content=[types.TextContent(type="text", text="Error occurred")],
|
||||
_meta={"isError": True, "errorCode": "TOOL_ERROR", "errorMessage": "Tool execution failed"},
|
||||
)
|
||||
|
||||
ai_contents = _mcp_call_tool_result_to_ai_contents(mcp_result)
|
||||
|
||||
@@ -115,15 +116,16 @@ def test_mcp_call_tool_result_with_meta_arbitrary_data():
|
||||
MCP server chooses to provide. This test uses example metadata to verify that
|
||||
whatever is provided gets preserved in additional_properties.
|
||||
"""
|
||||
mcp_result = types.CallToolResult(content=[types.TextContent(type="text", text="Success result")])
|
||||
# Example _meta field - different MCP servers may provide completely different structures
|
||||
mcp_result._meta = {
|
||||
"serverVersion": "2.1.0",
|
||||
"executionId": "exec_abc123",
|
||||
"metrics": {"responseTime": 1.25, "memoryUsed": "64MB"},
|
||||
"source": "example-mcp-server",
|
||||
"customField": "arbitrary_value",
|
||||
}
|
||||
mcp_result = types.CallToolResult(
|
||||
content=[types.TextContent(type="text", text="Success result")],
|
||||
_meta={
|
||||
"serverVersion": "2.1.0",
|
||||
"executionId": "exec_abc123",
|
||||
"metrics": {"responseTime": 1.25, "memoryUsed": "64MB"},
|
||||
"source": "example-mcp-server",
|
||||
"customField": "arbitrary_value",
|
||||
},
|
||||
)
|
||||
|
||||
ai_contents = _mcp_call_tool_result_to_ai_contents(mcp_result)
|
||||
|
||||
@@ -145,8 +147,7 @@ def test_mcp_call_tool_result_with_meta_merging_existing_properties():
|
||||
"""Test that _meta data merges correctly with existing additional_properties."""
|
||||
# Create content with existing additional_properties
|
||||
text_content = types.TextContent(type="text", text="Test content")
|
||||
mcp_result = types.CallToolResult(content=[text_content])
|
||||
mcp_result._meta = {"newField": "newValue", "isError": False}
|
||||
mcp_result = types.CallToolResult(content=[text_content], _meta={"newField": "newValue", "isError": False})
|
||||
|
||||
ai_contents = _mcp_call_tool_result_to_ai_contents(mcp_result)
|
||||
|
||||
@@ -159,30 +160,6 @@ def test_mcp_call_tool_result_with_meta_merging_existing_properties():
|
||||
assert content.additional_properties["isError"] is False
|
||||
|
||||
|
||||
def test_mcp_call_tool_result_with_meta_object_attributes():
|
||||
"""Test conversion when _meta is an object with attributes rather than a dict."""
|
||||
|
||||
class MetaObject:
|
||||
def __init__(self):
|
||||
self.isError = True
|
||||
self.requestId = "req-12345"
|
||||
self.executionTime = 2.5
|
||||
|
||||
mcp_result = types.CallToolResult(content=[types.TextContent(type="text", text="Object meta test")])
|
||||
mcp_result._meta = MetaObject()
|
||||
|
||||
ai_contents = _mcp_call_tool_result_to_ai_contents(mcp_result)
|
||||
|
||||
assert len(ai_contents) == 1
|
||||
content = ai_contents[0]
|
||||
|
||||
# Check that object attributes are extracted correctly
|
||||
assert content.additional_properties is not None
|
||||
assert content.additional_properties["isError"] is True
|
||||
assert content.additional_properties["requestId"] == "req-12345"
|
||||
assert content.additional_properties["executionTime"] == 2.5
|
||||
|
||||
|
||||
def test_mcp_call_tool_result_with_meta_none():
|
||||
"""Test that missing _meta field is handled gracefully."""
|
||||
mcp_result = types.CallToolResult(content=[types.TextContent(type="text", text="No meta test")])
|
||||
@@ -200,21 +177,6 @@ def test_mcp_call_tool_result_with_meta_none():
|
||||
assert props is None or props == {}
|
||||
|
||||
|
||||
def test_mcp_call_tool_result_with_meta_non_dict_value():
|
||||
"""Test conversion when _meta contains a non-dict value."""
|
||||
mcp_result = types.CallToolResult(content=[types.TextContent(type="text", text="Non-dict meta test")])
|
||||
mcp_result._meta = "simple string meta"
|
||||
|
||||
ai_contents = _mcp_call_tool_result_to_ai_contents(mcp_result)
|
||||
|
||||
assert len(ai_contents) == 1
|
||||
content = ai_contents[0]
|
||||
|
||||
# Non-dict _meta should be stored under '_meta' key
|
||||
assert content.additional_properties is not None
|
||||
assert content.additional_properties["_meta"] == "simple string meta"
|
||||
|
||||
|
||||
def test_mcp_call_tool_result_regression_successful_workflow():
|
||||
"""Regression test to ensure existing successful workflows remain unchanged."""
|
||||
# Test the original successful workflow still works
|
||||
@@ -247,7 +209,7 @@ def test_mcp_call_tool_result_regression_successful_workflow():
|
||||
def test_mcp_content_types_to_ai_content_text():
|
||||
"""Test conversion of MCP text content to AI content."""
|
||||
mcp_content = types.TextContent(type="text", text="Sample text")
|
||||
ai_content = _mcp_type_to_ai_content(mcp_content)
|
||||
ai_content = _mcp_type_to_ai_content(mcp_content)[0]
|
||||
|
||||
assert isinstance(ai_content, TextContent)
|
||||
assert ai_content.text == "Sample text"
|
||||
@@ -257,7 +219,7 @@ def test_mcp_content_types_to_ai_content_text():
|
||||
def test_mcp_content_types_to_ai_content_image():
|
||||
"""Test conversion of MCP image content to AI content."""
|
||||
mcp_content = types.ImageContent(type="image", data="data:image/jpeg;base64,abc", mimeType="image/jpeg")
|
||||
ai_content = _mcp_type_to_ai_content(mcp_content)
|
||||
ai_content = _mcp_type_to_ai_content(mcp_content)[0]
|
||||
|
||||
assert isinstance(ai_content, DataContent)
|
||||
assert ai_content.uri == "data:image/jpeg;base64,abc"
|
||||
@@ -268,7 +230,7 @@ def test_mcp_content_types_to_ai_content_image():
|
||||
def test_mcp_content_types_to_ai_content_audio():
|
||||
"""Test conversion of MCP audio content to AI content."""
|
||||
mcp_content = types.AudioContent(type="audio", data="data:audio/wav;base64,def", mimeType="audio/wav")
|
||||
ai_content = _mcp_type_to_ai_content(mcp_content)
|
||||
ai_content = _mcp_type_to_ai_content(mcp_content)[0]
|
||||
|
||||
assert isinstance(ai_content, DataContent)
|
||||
assert ai_content.uri == "data:audio/wav;base64,def"
|
||||
@@ -284,7 +246,7 @@ def test_mcp_content_types_to_ai_content_resource_link():
|
||||
name="test_resource",
|
||||
mimeType="application/json",
|
||||
)
|
||||
ai_content = _mcp_type_to_ai_content(mcp_content)
|
||||
ai_content = _mcp_type_to_ai_content(mcp_content)[0]
|
||||
|
||||
assert isinstance(ai_content, UriContent)
|
||||
assert ai_content.uri == "https://example.com/resource"
|
||||
@@ -300,7 +262,7 @@ def test_mcp_content_types_to_ai_content_embedded_resource_text():
|
||||
text="Embedded text content",
|
||||
)
|
||||
mcp_content = types.EmbeddedResource(type="resource", resource=text_resource)
|
||||
ai_content = _mcp_type_to_ai_content(mcp_content)
|
||||
ai_content = _mcp_type_to_ai_content(mcp_content)[0]
|
||||
|
||||
assert isinstance(ai_content, TextContent)
|
||||
assert ai_content.text == "Embedded text content"
|
||||
@@ -316,7 +278,7 @@ def test_mcp_content_types_to_ai_content_embedded_resource_blob():
|
||||
blob="data:application/octet-stream;base64,dGVzdCBkYXRh",
|
||||
)
|
||||
mcp_content = types.EmbeddedResource(type="resource", resource=blob_resource)
|
||||
ai_content = _mcp_type_to_ai_content(mcp_content)
|
||||
ai_content = _mcp_type_to_ai_content(mcp_content)[0]
|
||||
|
||||
assert isinstance(ai_content, DataContent)
|
||||
assert ai_content.uri == "data:application/octet-stream;base64,dGVzdCBkYXRh"
|
||||
@@ -650,9 +612,9 @@ async def test_mcp_tool_call_tool_with_meta_integration():
|
||||
|
||||
# Create a CallToolResult with _meta field
|
||||
tool_result = types.CallToolResult(
|
||||
content=[types.TextContent(type="text", text="Tool executed with metadata")]
|
||||
content=[types.TextContent(type="text", text="Tool executed with metadata")],
|
||||
_meta={"executionTime": 1.5, "cost": {"usd": 0.002}, "isError": False, "toolVersion": "1.2.3"},
|
||||
)
|
||||
tool_result._meta = {"executionTime": 1.5, "cost": {"usd": 0.002}, "isError": False, "toolVersion": "1.2.3"}
|
||||
|
||||
self.session.call_tool = AsyncMock(return_value=tool_result)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user