Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
[project]
name = "sap-cloud-sdk"

version = "0.49.0"
version = "0.49.1"
description = "SAP Cloud SDK for Python"
readme = "README.md"
license = "Apache-2.0"
Expand Down
2 changes: 2 additions & 0 deletions src/sap_cloud_sdk/core/telemetry/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
ATTR_EXTENSION_ITEM_NAME,
ATTR_EXTENSION_URL,
ATTR_SOLUTION_ID,
ATTR_JOULE_STUDIO_GSID,
ATTR_SUMMARY_TOTAL_OPERATION_COUNT,
ATTR_SUMMARY_TOTAL_DURATION_MS,
ATTR_SUMMARY_TOOL_CALL_COUNT,
Expand Down Expand Up @@ -89,6 +90,7 @@
"ATTR_EXTENSION_ITEM_NAME",
"ATTR_EXTENSION_URL",
"ATTR_SOLUTION_ID",
"ATTR_JOULE_STUDIO_GSID",
"ATTR_SUMMARY_TOTAL_OPERATION_COUNT",
"ATTR_SUMMARY_TOTAL_DURATION_MS",
"ATTR_SUMMARY_TOOL_CALL_COUNT",
Expand Down
62 changes: 47 additions & 15 deletions src/sap_cloud_sdk/core/telemetry/extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
ATTR_EXTENSION_ITEM_NAME = "sap.extension.extension.item.name"
ATTR_EXTENSION_URL = "sap.extension.extensionUrl"
ATTR_SOLUTION_ID = "sap.extension.solution_id"
ATTR_JOULE_STUDIO_GSID = "sap.extension.joule_studio_gsid"


class ExtensionType(str, Enum):
Expand All @@ -72,6 +73,7 @@ def extension_context(
item_name: str = "",
extension_url: str = "",
solution_id: str = "",
joule_studio_gsid: str = "",
) -> Generator[None, None, None]:
"""Set extension context in OTel baggage for propagation.

Expand All @@ -90,6 +92,7 @@ def extension_context(
- ``sap.extension.extension.item.name``: The tool or hook name
- ``sap.extension.extensionUrl``: The extension URL (when provided)
- ``sap.extension.solution_id``: The solution ID (when provided)
- ``sap.extension.joule_studio_gsid``: The global solution ID of Joule Studio (when provided)

Args:
capability_id: The capability ID for the extension
Expand All @@ -101,7 +104,8 @@ def extension_context(
extension_version: The version of the extension (e.g. ``"1"``).
item_name: The name of the specific tool or hook being called.
extension_url: The build extension URL (empty string if not available).
solution_id: The build solution ID (empty string if not available).
solution_id: The solution ID (empty string if not available).
joule_studio_gsid: The global solution ID of Joule Studio (empty string if not available).

Yields:
None. The context is active for the duration of the with block.
Expand All @@ -121,6 +125,7 @@ def extension_context(
extension_version="1",
item_name="create_ticket",
solution_id="my-solution-42",
joule_studio_gsid="gsid-value",
):
result = await mcp_client.call_tool("create_ticket", args)
```
Expand All @@ -141,6 +146,10 @@ def extension_context(
ctx = baggage.set_baggage(ATTR_EXTENSION_URL, extension_url, context=ctx)
if solution_id:
ctx = baggage.set_baggage(ATTR_SOLUTION_ID, solution_id, context=ctx)
if joule_studio_gsid:
ctx = baggage.set_baggage(
ATTR_JOULE_STUDIO_GSID, joule_studio_gsid, context=ctx
)

token = attach(ctx)
try:
Expand Down Expand Up @@ -168,6 +177,7 @@ def get_extension_context() -> dict[str, Any] | None:
- ``item_name``: The tool or hook name
- ``extension_url``: The extension URL (empty string if not set)
- ``solution_id``: The solution ID (empty string if not set)
- ``joule_studio_gsid``: The global solution ID of Joule Studio (empty string if not set)

Returns ``None`` if not in an extension context.

Expand All @@ -194,6 +204,7 @@ def get_extension_context() -> dict[str, Any] | None:
"item_name": baggage.get_baggage(ATTR_EXTENSION_ITEM_NAME),
"extension_url": baggage.get_baggage(ATTR_EXTENSION_URL) or "",
"solution_id": baggage.get_baggage(ATTR_SOLUTION_ID) or "",
"joule_studio_gsid": baggage.get_baggage(ATTR_JOULE_STUDIO_GSID) or "",
}


Expand Down Expand Up @@ -227,6 +238,7 @@ def get_extension_context() -> dict[str, Any] | None:
(ATTR_EXTENSION_ITEM_NAME, "ext_item_name"),
(ATTR_EXTENSION_URL, "ext_extension_url"),
(ATTR_SOLUTION_ID, "ext_solution_id"),
(ATTR_JOULE_STUDIO_GSID, "ext_joule_studio_gsid"),
]


Expand All @@ -239,14 +251,14 @@ def resolve_source_info(
key: str,
source_mapping: dict[str, Any] | None,
fallback_name: str,
) -> tuple[str, str, str, str, str]:
"""Resolve extension name, id, version, url, and solution_id from a source mapping.
) -> tuple[str, str, str, str, str, str]:
"""Resolve extension name, id, version, url, solution_id, and joule_studio_gsid from a source mapping.

Source mapping values may be ``ExtensionSourceInfo`` dataclass instances
(with attributes ``extension_name``, ``extension_id``,
``extension_version``, ``extension_url``, ``solution_id``) or plain dicts
``extension_version``, ``extension_url``, ``solution_id``, ``joule_studio_gsid``) or plain dicts
with camelCase keys (``extensionName``, ``extensionId``,
``extensionVersion``, ``extensionUrl``, ``solutionId``).
``extensionVersion``, ``extensionUrl``, ``solutionId``, ``jouleStudioGsid``).

Falls back to *fallback_name* for the name and empty strings for other
fields when the key is not found in the mapping.
Expand All @@ -260,11 +272,11 @@ def resolve_source_info(
name is empty.

Returns:
Tuple of ``(extension_name, extension_id, extension_version, extension_url, solution_id)``.
Tuple of ``(extension_name, extension_id, extension_version, extension_url, solution_id, joule_studio_gsid)``.
"""
info = (source_mapping or {}).get(key)
if info is None:
return (fallback_name or "unknown", "", "", "", "")
return (fallback_name or "unknown", "", "", "", "", "")
# SDK ExtensionSourceInfo dataclass (duck-typed to avoid circular import)
if hasattr(info, "extension_name"):
return (
Expand All @@ -273,6 +285,7 @@ def resolve_source_info(
str(info.extension_version) if info.extension_version else "",
getattr(info, "extension_url", "") or "",
getattr(info, "solution_id", "") or "",
getattr(info, "joule_studio_gsid", "") or "",
)
# Plain dict with camelCase keys (older SDK or manual construction)
if isinstance(info, dict):
Expand All @@ -282,8 +295,9 @@ def resolve_source_info(
str(info.get("extensionVersion", "")) or "",
info.get("extensionUrl") or "",
info.get("solutionId") or "",
info.get("jouleStudioGsid") or "",
)
return (fallback_name or "unknown", "", "", "", "")
return (fallback_name or "unknown", "", "", "", "", "")


# ---------------------------------------------------------------------------
Expand All @@ -300,6 +314,7 @@ def build_extension_span_attributes(
item_name: str,
extension_url: str = "",
solution_id: str = "",
joule_studio_gsid: str = "",
) -> dict[str, Any]:
"""Build the full set of ``sap.extension.*`` span attributes.

Expand All @@ -311,7 +326,8 @@ def build_extension_span_attributes(
capability: Extension capability ID (e.g. ``"default"``).
item_name: Name of the specific tool or hook being called.
extension_url: Build extension URL (empty string if not available).
solution_id: Build solution ID (empty string if not available).
solution_id: Solution ID (empty string if not available).
joule_studio_gsid: Global solution ID (empty string if not available).

Returns:
Dict with all ``sap.extension.*`` attribute keys.
Expand All @@ -329,6 +345,8 @@ def build_extension_span_attributes(
attrs[ATTR_EXTENSION_URL] = extension_url
if solution_id:
attrs[ATTR_SOLUTION_ID] = solution_id
if joule_studio_gsid:
attrs[ATTR_JOULE_STUDIO_GSID] = joule_studio_gsid
return attrs


Expand Down Expand Up @@ -447,9 +465,14 @@ async def call_extension_tool(
See Also:
:func:`call_extension_hook` for hook-based extensions.
"""
resolved_name, resolved_id, resolved_version, resolved_url, resolved_solution_id = (
resolve_source_info(tool_name, source_mapping, "unknown")
)
(
resolved_name,
resolved_id,
resolved_version,
resolved_url,
resolved_solution_id,
resolved_joule_studio_gsid,
) = resolve_source_info(tool_name, source_mapping, "unknown")

attrs = build_extension_span_attributes(
resolved_name,
Expand All @@ -460,6 +483,7 @@ async def call_extension_tool(
tool_name,
extension_url=resolved_url,
solution_id=resolved_solution_id,
joule_studio_gsid=resolved_joule_studio_gsid,
)

t0 = time.monotonic()
Expand All @@ -474,6 +498,7 @@ async def call_extension_tool(
item_name=tool_name,
extension_url=resolved_url,
solution_id=resolved_solution_id,
joule_studio_gsid=resolved_joule_studio_gsid,
),
_tracer.start_as_current_span(
f"extension_tool {tool_name}",
Expand Down Expand Up @@ -526,9 +551,14 @@ async def call_extension_hook(
Returns:
The hook's response.
"""
resolved_name, resolved_id, resolved_version, resolved_url, resolved_solution_id = (
resolve_source_info(hook_id, source_mapping, extension_name)
)
(
resolved_name,
resolved_id,
resolved_version,
resolved_url,
resolved_solution_id,
resolved_joule_studio_gsid,
) = resolve_source_info(hook_id, source_mapping, extension_name)

item_name = getattr(hook, "name", None) or hook_id

Expand All @@ -541,6 +571,7 @@ async def call_extension_hook(
item_name,
extension_url=resolved_url,
solution_id=resolved_solution_id,
joule_studio_gsid=resolved_joule_studio_gsid,
)

t0 = time.monotonic()
Expand All @@ -555,6 +586,7 @@ async def call_extension_hook(
item_name=item_name,
extension_url=resolved_url,
solution_id=resolved_solution_id,
joule_studio_gsid=resolved_joule_studio_gsid,
),
_tracer.start_as_current_span(
f"extension_hook {item_name}",
Expand Down
9 changes: 8 additions & 1 deletion src/sap_cloud_sdk/extensibility/_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -411,13 +411,16 @@ class ExtensionSourceInfo:
extension_url: Build extension URL, or empty string if not provided.
solution_id: Build solution ID extracted from extension_url, or empty
string if not available.
joule_studio_gsid: Global solution ID of Joule Studio, or empty string
if not available.
"""

extension_name: str
extension_version: str
extension_id: str
extension_url: str = ""
solution_id: str = ""
joule_studio_gsid: str = ""

@classmethod
def from_dict(cls, obj: Dict[str, Any]) -> ExtensionSourceInfo:
Expand All @@ -430,7 +433,8 @@ def from_dict(cls, obj: Dict[str, Any]) -> ExtensionSourceInfo:
"extensionVersion": "1",
"extensionId": "a1b2c3d4-...",
"extensionUrl": "https://...",
"solutionId": "f9cbd5c1-..."
"solutionId": "f9cbd5c1-...",
"jouleStudioGsid": "gsid-value-..."
}

Args:
Expand All @@ -445,6 +449,7 @@ def from_dict(cls, obj: Dict[str, Any]) -> ExtensionSourceInfo:
extension_id=obj.get("extensionId", ""),
extension_url=obj.get("extensionUrl") or "",
solution_id=obj.get("solutionId") or "",
joule_studio_gsid=obj.get("jouleStudioGsid") or "",
)

@classmethod
Expand All @@ -469,6 +474,7 @@ def from_value(cls, value: Any) -> ExtensionSourceInfo:
extension_id="",
extension_url="",
solution_id="",
joule_studio_gsid="",
)
if isinstance(value, dict):
return cls.from_dict(value)
Expand All @@ -478,6 +484,7 @@ def from_value(cls, value: Any) -> ExtensionSourceInfo:
extension_id="",
extension_url="",
solution_id="",
joule_studio_gsid="",
)


Expand Down
2 changes: 2 additions & 0 deletions src/sap_cloud_sdk/extensibility/_ums_transport.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
title
extensionVersion
solutionId
jouleStudioGsid
capabilityImplementations {
capabilityId
instruction { text }
Expand Down Expand Up @@ -327,6 +328,7 @@ def _build_source_mapping(
extension_version=node.get("extensionVersion", ""),
extension_id=node.get("id", ""),
solution_id=node.get("solutionId") or "",
joule_studio_gsid=node.get("jouleStudioGsid") or "",
)

for cap_impl in node.get("capabilityImplementations", []):
Expand Down
Loading
Loading