Skip to content
Open
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
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "sap-cloud-sdk"
version = "0.53.0"
version = "0.53.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 @@ -43,6 +43,7 @@
ATTR_SUMMARY_TOOL_CALL_COUNT,
ATTR_SUMMARY_HOOK_CALL_COUNT,
ATTR_SUMMARY_HAS_INSTRUCTION,
ATTR_SUMMARY_JOULE_STUDIO_GSID,
resolve_source_info,
build_extension_span_attributes,
reset_tool_call_metrics,
Expand Down Expand Up @@ -96,6 +97,7 @@
"ATTR_SUMMARY_TOOL_CALL_COUNT",
"ATTR_SUMMARY_HOOK_CALL_COUNT",
"ATTR_SUMMARY_HAS_INSTRUCTION",
"ATTR_SUMMARY_JOULE_STUDIO_GSID",
"resolve_source_info",
"build_extension_span_attributes",
"reset_tool_call_metrics",
Expand Down
6 changes: 6 additions & 0 deletions src/sap_cloud_sdk/core/telemetry/extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ def get_extension_context() -> dict[str, Any] | None:
ATTR_SUMMARY_TOOL_CALL_COUNT = "sap.extension.summary.toolCallCount"
ATTR_SUMMARY_HOOK_CALL_COUNT = "sap.extension.summary.hookCallCount"
ATTR_SUMMARY_HAS_INSTRUCTION = "sap.extension.summary.hasInstruction"
ATTR_SUMMARY_JOULE_STUDIO_GSID = "sap.extension.joule_studio_gsid"

# ---------------------------------------------------------------------------
# Private state
Expand Down Expand Up @@ -612,6 +613,7 @@ def emit_extensions_summary_span(
hook_call_count: int,
has_instruction: bool,
total_duration_ms: float,
joule_studio_gsid: str = "",
) -> None:
"""Emit a sibling summary span with aggregate extension metrics.

Expand All @@ -633,6 +635,8 @@ def emit_extensions_summary_span(
into the system prompt.
total_duration_ms: Wall-clock sum (milliseconds) of all extension
operations.
joule_studio_gsid: Global solution ID of Joule Studio (empty string
if not available).
"""
total = tool_call_count + hook_call_count + (1 if has_instruction else 0)
attrs = {
Expand All @@ -642,6 +646,8 @@ def emit_extensions_summary_span(
ATTR_SUMMARY_HOOK_CALL_COUNT: hook_call_count,
ATTR_SUMMARY_HAS_INSTRUCTION: has_instruction,
}
if joule_studio_gsid:
attrs[ATTR_SUMMARY_JOULE_STUDIO_GSID] = joule_studio_gsid
span = _tracer.start_span("agent_extensions_summary", attributes=attrs)
span.end()

Expand Down
4 changes: 4 additions & 0 deletions src/sap_cloud_sdk/extensibility/_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -641,6 +641,9 @@ class ExtensionCapabilityImplementation:
hooks: List of hooks attached for this extension capability.
source: Per-tool and per-hook attribution mapping. ``None`` when the
backend does not provide source information.
joule_studio_gsid: Global solution ID of Joule Studio. Set when a
single Joule Studio extension contributes to this capability;
empty string otherwise.
"""

capability_id: str
Expand All @@ -649,6 +652,7 @@ class ExtensionCapabilityImplementation:
instruction: Optional[str] = None
hooks: List[Hook] = field(default_factory=list)
source: Optional[ExtensionSourceMapping] = None
joule_studio_gsid: str = ""

@classmethod
def from_dict(cls, obj: Dict[str, Any]) -> ExtensionCapabilityImplementation:
Expand Down
10 changes: 10 additions & 0 deletions src/sap_cloud_sdk/extensibility/_ums_transport.py
Original file line number Diff line number Diff line change
Expand Up @@ -404,13 +404,23 @@ def _transform_ums_response(

instruction = "\n\n".join(instructions) if instructions else None

joule_studio_gsid = next(
(
node.get("jouleStudioGsid") or ""
for node in nodes
if node.get("jouleStudioGsid")
),
"",
)

return ExtensionCapabilityImplementation(
capability_id=capability_id,
extension_names=extension_names,
mcp_servers=mcp_servers,
instruction=instruction,
hooks=hooks,
source=source,
joule_studio_gsid=joule_studio_gsid,
)


Expand Down
32 changes: 32 additions & 0 deletions tests/core/unit/telemetry/test_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
ATTR_SUMMARY_TOOL_CALL_COUNT,
ATTR_SUMMARY_HOOK_CALL_COUNT,
ATTR_SUMMARY_HAS_INSTRUCTION,
ATTR_SUMMARY_JOULE_STUDIO_GSID,
resolve_source_info,
build_extension_span_attributes,
reset_tool_call_metrics,
Expand Down Expand Up @@ -1074,6 +1075,37 @@ def test_no_instruction_count(self):
attrs = mock_tracer.start_span.call_args[1]["attributes"]
assert attrs[ATTR_SUMMARY_TOTAL_OPERATION_COUNT] == 1 # no +1

def test_joule_studio_gsid_included_when_provided(self):
with patch("sap_cloud_sdk.core.telemetry.extensions._tracer") as mock_tracer:
mock_span = MagicMock()
mock_tracer.start_span.return_value = mock_span

emit_extensions_summary_span(
tool_call_count=1,
hook_call_count=0,
has_instruction=False,
total_duration_ms=100.0,
joule_studio_gsid="019ffc41-01a5-7b73-bda2-cb33b2eae292",
)

attrs = mock_tracer.start_span.call_args[1]["attributes"]
assert attrs[ATTR_SUMMARY_JOULE_STUDIO_GSID] == "019ffc41-01a5-7b73-bda2-cb33b2eae292"

def test_joule_studio_gsid_omitted_when_empty(self):
with patch("sap_cloud_sdk.core.telemetry.extensions._tracer") as mock_tracer:
mock_span = MagicMock()
mock_tracer.start_span.return_value = mock_span

emit_extensions_summary_span(
tool_call_count=1,
hook_call_count=0,
has_instruction=False,
total_duration_ms=100.0,
)

attrs = mock_tracer.start_span.call_args[1]["attributes"]
assert ATTR_SUMMARY_JOULE_STUDIO_GSID not in attrs


# ---------------------------------------------------------------------------
# ExtensionContextLogFilter
Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading