Skip to content

Commit 0a4ff67

Browse files
Merge remote-tracking branch 'origin/sap_om_poc' into sap_om_poc
2 parents 8b4f46c + 0cb2a35 commit 0a4ff67

15 files changed

Lines changed: 1066 additions & 301 deletions

File tree

.env_integration_tests.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ CLOUD_SDK_CFG_ADMS_DEFAULT_RESOURCE=urn:sap:identity:application:provider:name:y
1111
CLOUD_SDK_CFG_AGW_DEFAULT_LANDSCAPE=your-landscape-here
1212
CLOUD_SDK_CFG_AGW_DEFAULT_TENANT_SUBDOMAIN=your-tenant-subdomain-here
1313
CLOUD_SDK_CFG_AGW_DEFAULT_USER_TOKEN=your-user-jwt-here
14+
CLOUD_SDK_CFG_AGW_DEFAULT_SAMPLE_MCP_TOOL=your-sample-mcp-tool-name-here
1415

1516
# AGENT MEMORY
1617
CLOUD_SDK_CFG_HANA_AGENT_MEMORY_DEFAULT_APPLICATION_URL=https://your-agent-memory-api-url-here

docs/DEVELOPMENT.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ Tip:
3434
## Type Check
3535

3636
```bash
37-
uv run ty check .
37+
uvx ty check .
3838
```
3939

4040
## Code Quality Checks
@@ -49,7 +49,7 @@ uv run ruff check .
4949
uv run ruff format --check .
5050

5151
# Type check
52-
uv run ty check .
52+
uvx ty check .
5353
```
5454

5555
## Pre-commit Hooks (Recommended)

docs/INTEGRATION_TESTS.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,9 @@ CLOUD_SDK_CFG_AGW_DEFAULT_TENANT_SUBDOMAIN=your-tenant-subdomain-here
6262
# User JWT for token exchange scenarios (get_user_auth)
6363
# If not set, user auth scenarios are automatically skipped
6464
CLOUD_SDK_CFG_AGW_DEFAULT_USER_TOKEN=your-user-jwt-here
65+
66+
# Name of a MCP tool available in your environment
67+
CLOUD_SDK_CFG_AGW_DEFAULT_SAMPLE_MCP_TOOL=your-sample-mcp-tool-name-here
6568
```
6669

6770
### Agent Memory Integration Tests

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "sap-cloud-sdk"
3-
version = "0.27.1"
3+
version = "0.29.0"
44
description = "SAP Cloud SDK for Python"
55
readme = "README.md"
66
license = "Apache-2.0"

src/sap_cloud_sdk/agentgateway/converters.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
from typing import TYPE_CHECKING, Any, Callable
1111

12-
from pydantic import create_model
12+
from pydantic import Field, create_model
1313

1414
from sap_cloud_sdk.agentgateway._models import MCPTool
1515

@@ -21,6 +21,8 @@ def mcp_tool_to_langchain(
2121
mcp_tool: MCPTool,
2222
call_tool: Callable,
2323
get_user_token: Callable[[], str],
24+
*,
25+
omit_none: bool = True,
2426
) -> StructuredTool:
2527
"""Convert MCPTool to LangChain StructuredTool.
2628
@@ -31,6 +33,8 @@ def mcp_tool_to_langchain(
3133
mcp_tool: MCPTool object from list_mcp_tools().
3234
call_tool: Callable to invoke the MCP tool (e.g., agw_client.call_mcp_tool).
3335
get_user_token: Callable that returns the user's JWT token.
36+
omit_none: If True (default), optional parameters with a None value are not
37+
forwarded to call_tool. Set to False to forward None values explicitly.
3438
3539
Returns:
3640
LangChain StructuredTool that invokes the MCP tool.
@@ -66,17 +70,21 @@ def mcp_tool_to_langchain(
6670
) from None
6771

6872
async def run(**kwargs) -> str:
73+
resolved = (
74+
{k: v for k, v in kwargs.items() if v is not None} if omit_none else kwargs
75+
)
6976
return await call_tool(
7077
mcp_tool,
7178
user_token=get_user_token,
72-
**kwargs,
79+
**resolved,
7380
)
7481

7582
# Build args schema from input_schema
7683
properties = mcp_tool.input_schema.get("properties", {})
7784
required = set(mcp_tool.input_schema.get("required", []))
7885
fields: dict[str, Any] = {
79-
k: (str, ...) if k in required else (str | None, None) for k in properties
86+
k: (str, ...) if k in required else (str | None, Field(default=None))
87+
for k in properties
8088
}
8189
args_schema = create_model(f"{mcp_tool.name}_args", **fields) if fields else None
8290

src/sap_cloud_sdk/agentgateway/user-guide.md

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,7 @@ config = ClientConfig(timeout=30.0)
5252
agw_client = create_client(tenant_subdomain="my-tenant", config=config)
5353

5454
# Discover tools (auto-discovered from destination fragments)
55-
tools = await agw_client.list_mcp_tools()
56-
57-
# Discover tools with user principal propagation
55+
# Pass user_token to use principal propagation when listing tools
5856
tools = await agw_client.list_mcp_tools(user_token="user-jwt")
5957

6058
# Invoke a tool (user_token required for principal propagation)
@@ -74,7 +72,7 @@ from sap_cloud_sdk.agentgateway import create_client
7472
from sap_cloud_sdk.agentgateway.converters import mcp_tool_to_langchain
7573

7674
agw_client = create_client(tenant_subdomain="my-tenant")
77-
tools = await agw_client.list_mcp_tools()
75+
tools = await agw_client.list_mcp_tools(user_token="user-jwt")
7876

7977
langchain_tools = [
8078
mcp_tool_to_langchain(
@@ -89,6 +87,17 @@ langchain_tools = [
8987
llm_with_tools = llm.bind_tools(langchain_tools)
9088
```
9189

90+
By default, optional tool parameters that resolve to `None` are not forwarded to `call_mcp_tool`. Set `omit_none=False` to forward them explicitly:
91+
92+
```python
93+
mcp_tool_to_langchain(
94+
t,
95+
agw_client.call_mcp_tool,
96+
get_user_token=lambda: request.headers["Authorization"],
97+
omit_none=False,
98+
)
99+
```
100+
92101
## Concepts
93102

94103
### Agent Types

src/sap_cloud_sdk/core/auditlog_ng/__init__.py

Lines changed: 146 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
ready-to-use AuditClient.
88
99
Usage:
10+
explicit config:
11+
1012
from sap_cloud_sdk.core.auditlog_ng import create_client, AuditLogNGConfig
1113
1214
config = AuditLogNGConfig(
@@ -18,12 +20,24 @@
1820
)
1921
client = create_client(config=config)
2022
23+
Usage:
24+
resolve from a Destination:
25+
26+
from sap_cloud_sdk.core.auditlog_ng import create_client
27+
28+
client = create_client(
29+
destination_name="my-audit-destination",
30+
destination_instance="my-binding-instance",
31+
fragment_name="prod-fragment", # optional
32+
)
33+
2134
# Send an audit event (protobuf message)
2235
event_id = client.send(event, "DataAccess")
2336
client.close()
2437
"""
2538

2639
from typing import Optional
40+
from enum import Enum
2741

2842
from sap_cloud_sdk.core.auditlog_ng.client import AuditClient
2943
from sap_cloud_sdk.core.auditlog_ng.config import (
@@ -43,9 +57,103 @@
4357
)
4458

4559

60+
class _DestinationProperties(Enum):
61+
DEPLOYMENT_ID = "deploymentId"
62+
DEPLOYMENT_REGION = "deploymentRegion"
63+
NAMESPACE = "namespace"
64+
65+
66+
def _get_config_from_destination(
67+
destination_name: Optional[str],
68+
destination_instance: Optional[str],
69+
fragment_name: Optional[str] = None,
70+
) -> dict[str, str]:
71+
"""Resolve endpoint, deployment_id and namespace from a named Destination.
72+
73+
The destination must expose these custom properties:
74+
75+
- ``deploymentId`` (or ``deploymentRegion`` as fallback when absent/empty)
76+
- ``namespace``
77+
78+
The destination ``url`` is used as the OTLP endpoint.
79+
The lookup is always performed at ``ConsumptionLevel.SUBACCOUNT``.
80+
81+
Args:
82+
destination_name: Name of the destination to resolve.
83+
destination_instance: Destination service binding instance name,
84+
passed as ``instance=`` to ``destination.create_client()``.
85+
fragment_name: Optional fragment name merged into the destination
86+
before resolution. Wrapped in ``ConsumptionOptions`` when provided.
87+
88+
Returns:
89+
dict with keys ``endpoint``, ``deployment_id``, ``namespace``
90+
when destination is found.
91+
92+
Returns:
93+
None: If destination is not found.
94+
95+
Return:
96+
ValueError: If required properties are missing.
97+
"""
98+
# Lazy import — keeps destination an optional dependency; importing auditlog_ng
99+
# in environments without the destination package continues to work.
100+
from sap_cloud_sdk.destination import (
101+
ConsumptionOptions,
102+
ConsumptionLevel,
103+
create_client as _dest_create_client,
104+
)
105+
106+
dest_client = _dest_create_client(instance=destination_instance)
107+
options = (
108+
ConsumptionOptions(
109+
fragment_name=fragment_name, fragment_level=ConsumptionLevel.SUBACCOUNT
110+
)
111+
if fragment_name
112+
else None
113+
)
114+
115+
destination = dest_client.get_destination(
116+
name=destination_name, options=options, level=ConsumptionLevel.SUBACCOUNT
117+
)
118+
119+
if destination is None:
120+
return {}
121+
122+
endpoint = destination.url
123+
props = destination.properties
124+
125+
deployment_id = props.get(_DestinationProperties.DEPLOYMENT_ID.value) or ""
126+
if not deployment_id:
127+
deployment_id = props.get(_DestinationProperties.DEPLOYMENT_REGION.value) or ""
128+
if not deployment_id:
129+
raise ValueError(
130+
f"Destination '{destination_name}' must provide either the "
131+
f"'{_DestinationProperties.DEPLOYMENT_ID.value}' or "
132+
f"'{_DestinationProperties.DEPLOYMENT_REGION.value}' property"
133+
)
134+
135+
namespace = props.get(_DestinationProperties.NAMESPACE.value) or ""
136+
if not namespace:
137+
raise ValueError(
138+
f"Destination '{destination_name}' must provide the "
139+
f"'{_DestinationProperties.NAMESPACE.value}' property"
140+
)
141+
142+
return {
143+
"endpoint": endpoint,
144+
"deployment_id": deployment_id,
145+
"namespace": namespace,
146+
}
147+
148+
46149
def create_client(
47150
*,
48151
config: Optional[AuditLogNGConfig] = None,
152+
# Destination-based resolution
153+
destination_name: Optional[str] = "AuditLogV3_Destination",
154+
destination_instance: Optional[str] = "default",
155+
fragment_name: Optional[str] = None,
156+
# Explicit connection parameters
49157
endpoint: Optional[str] = None,
50158
deployment_id: Optional[str] = None,
51159
namespace: Optional[str] = None,
@@ -61,13 +169,30 @@ def create_client(
61169
) -> AuditClient:
62170
"""Create an AuditClient for sending audit events over OTLP/gRPC.
63171
64-
Either pass a pre-built ``config`` **or** the individual keyword arguments.
65-
When ``config`` is provided the remaining keyword arguments are ignored.
172+
Three mutually exclusive ways to provide configuration (evaluated in order):
173+
174+
1. **Explicit config object** — pass a pre-built :class:`AuditLogNGConfig`
175+
via ``config``; all other keyword arguments are ignored.
176+
177+
2. **Destination-based resolution** — pass ``destination_name`` and
178+
``destination_instance`` (both required); ``fragment_name`` is optional.
179+
The Destination module resolves the named destination at subaccount level
180+
and extracts ``endpoint``, ``deployment_id`` (with fallback to
181+
``deploymentRegion``), and ``namespace`` from its properties.
182+
183+
3. **Explicit keyword arguments** — pass ``endpoint``, ``deployment_id``,
184+
and ``namespace`` directly.
66185
67186
Args:
68187
_telemetry_source: Internal parameter for telemetry. Not for external use.
69188
config: Optional explicit configuration. If provided, all other
70-
keyword arguments are ignored.
189+
keyword arguments are ignored.
190+
destination_name: Name of the SAP Destination to resolve. Must be
191+
combined with ``destination_instance`` to enter the destination path.
192+
destination_instance: Destination service binding instance name, passed
193+
as ``instance=`` to ``destination.create_client()``. Must be combined
194+
with ``destination_name`` to enter the destination path.
195+
fragment_name: Optional destination fragment name merged before resolution.
71196
endpoint: OTLP gRPC endpoint (``host:port``).
72197
deployment_id: Deployment identifier.
73198
namespace: Namespace identifier.
@@ -85,16 +210,28 @@ def create_client(
85210
86211
Raises:
87212
ClientCreationError: If client creation fails.
88-
ValueError: If required parameters are missing.
213+
ValueError: If required parameters are missing or destination
214+
resolution fails.
89215
"""
90216
try:
91217
if config is None:
92218
try:
93-
if not endpoint or not deployment_id or not namespace:
94-
raise ValueError(
95-
"endpoint, deployment_id, and namespace are required "
96-
"when config is not provided"
97-
)
219+
resolved = _get_config_from_destination(
220+
destination_name=destination_name,
221+
destination_instance=destination_instance,
222+
fragment_name=fragment_name,
223+
)
224+
if resolved:
225+
endpoint = resolved["endpoint"]
226+
deployment_id = resolved["deployment_id"]
227+
namespace = resolved["namespace"]
228+
else:
229+
if not endpoint or not deployment_id or not namespace:
230+
raise ValueError(
231+
"endpoint, deployment_id, and namespace are required "
232+
"when config or valid destination is not provided"
233+
)
234+
98235
config = AuditLogNGConfig(
99236
endpoint=endpoint,
100237
deployment_id=deployment_id,

0 commit comments

Comments
 (0)