diff --git a/pyproject.toml b/pyproject.toml index 491412ad..641483d7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "sap-cloud-sdk" -version = "0.27.0" +version = "0.27.1" description = "SAP Cloud SDK for Python" readme = "README.md" license = "Apache-2.0" diff --git a/src/sap_cloud_sdk/agentgateway/converters.py b/src/sap_cloud_sdk/agentgateway/converters.py index fa8f8587..84a4e12a 100644 --- a/src/sap_cloud_sdk/agentgateway/converters.py +++ b/src/sap_cloud_sdk/agentgateway/converters.py @@ -74,7 +74,10 @@ async def run(**kwargs) -> str: # Build args schema from input_schema properties = mcp_tool.input_schema.get("properties", {}) - fields: dict[str, Any] = {k: (str, ...) for k in properties} + required = set(mcp_tool.input_schema.get("required", [])) + fields: dict[str, Any] = { + k: (str, ...) if k in required else (str | None, None) for k in properties + } args_schema = create_model(f"{mcp_tool.name}_args", **fields) if fields else None return StructuredTool.from_function( diff --git a/tests/agentgateway/unit/test_converters.py b/tests/agentgateway/unit/test_converters.py index 21c7b020..0ac908fc 100644 --- a/tests/agentgateway/unit/test_converters.py +++ b/tests/agentgateway/unit/test_converters.py @@ -78,6 +78,34 @@ def test_handles_empty_input_schema(self): assert result.name == "simple_tool" assert result.args_schema is not None + def test_optional_fields_not_required_in_args_schema(self): + """Fields absent from 'required' must be optional in the generated Pydantic model.""" + tool = MCPTool( + name="get_supplier_bid", + server_name="ariba", + description="Gets all supplier bids for the specified event", + input_schema={ + "type": "object", + "required": ["eventid"], + "properties": { + "eventid": {"description": "Unique identifier of the event"}, + "showdeclinedreason": {"description": "Show supplier decline reason"}, + "datafetchmode": {"description": "Level of detail for the response"}, + }, + }, + url="https://example.com/mcp", + ) + + result = mcp_tool_to_langchain(tool, AsyncMock(return_value="result"), lambda: "token") + + from pydantic import BaseModel + + assert result.args_schema is not None and isinstance(result.args_schema, type) and issubclass(result.args_schema, BaseModel) + fields = result.args_schema.model_fields + assert fields["eventid"].is_required(), "eventid should be required" + assert not fields["showdeclinedreason"].is_required(), "showdeclinedreason should be optional" + assert not fields["datafetchmode"].is_required(), "datafetchmode should be optional" + def test_handles_input_schema_without_properties(self): """Handle MCPTool with input schema but no properties.""" tool = MCPTool( diff --git a/tests/core/integration/telemetry/_agent.py b/tests/core/integration/telemetry/_agent.py index b44ada81..a0d0343e 100644 --- a/tests/core/integration/telemetry/_agent.py +++ b/tests/core/integration/telemetry/_agent.py @@ -17,9 +17,9 @@ def build_langgraph_agent(): from typing import Annotated from langchain_core.messages import BaseMessage - from langchain_litellm import ChatLiteLLM # ty: ignore[unresolved-import] - from langgraph.graph import END, StateGraph # ty: ignore[unresolved-import] - from langgraph.graph.message import add_messages # ty: ignore[unresolved-import] + from langchain_litellm import ChatLiteLLM + from langgraph.graph import END, StateGraph + from langgraph.graph.message import add_messages except ImportError: pytest.skip("langchain-litellm or langgraph not installed") diff --git a/tests/core/integration/telemetry/test_telemetry_bdd.py b/tests/core/integration/telemetry/test_telemetry_bdd.py index d53e1464..aecbc017 100644 --- a/tests/core/integration/telemetry/test_telemetry_bdd.py +++ b/tests/core/integration/telemetry/test_telemetry_bdd.py @@ -127,7 +127,7 @@ def _llm_call(): Uses ChatLiteLLM so Traceloop's LangChain instrumentor fires and emits a span with gen_ai.usage.* token counts. """ - from langchain_litellm import ChatLiteLLM # ty: ignore[unresolved-import] + from langchain_litellm import ChatLiteLLM from langchain_core.messages import HumanMessage as LCHumanMessage model_name = os.environ.get("AICORE_MODEL", "anthropic--claude-4.5-sonnet") llm = ChatLiteLLM(model=f"sap/{model_name}")