Skip to content

Commit 198f07f

Browse files
Merge branch 'main' into sap_om_poc
2 parents 6db14fa + a558a8e commit 198f07f

26 files changed

Lines changed: 748 additions & 368 deletions

File tree

‎.pre-commit-config.yaml‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,17 @@ repos:
99
hooks:
1010
- id: ruff-format
1111
name: ruff format
12-
entry: uvx ruff format --force-exclude --check
12+
entry: uv run ruff format --force-exclude --check
1313
language: system
1414
types: [python]
1515
- id: ruff-check
1616
name: ruff check
17-
entry: uvx ruff check --force-exclude --fix
17+
entry: uv run ruff check --force-exclude --fix
1818
language: system
1919
types: [python]
2020
- id: ty-check
2121
name: ty type check
22-
entry: uvx ty check .
22+
entry: uv run ty check .
2323
language: system
2424
types: [python]
2525
pass_filenames: false

‎docs/DEVELOPMENT.md‎

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

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

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

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

55+
> **Linter versions are pinned.** `ruff` and `ty` are pinned to exact versions in the
56+
> `pyproject.toml` `[dependency-groups] dev` section (and locked in `uv.lock`) for consistent runs across local and CI.
57+
5558
## Pre-commit Hooks (Recommended)
5659

5760
Automatically run quality checks before each commit using pre-commit hooks.

‎pyproject.toml‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "sap-cloud-sdk"
3-
version = "0.40.0"
3+
version = "0.39.2"
44
description = "SAP Cloud SDK for Python"
55
readme = "README.md"
66
license = "Apache-2.0"
@@ -64,9 +64,9 @@ dev = [
6464
"pytest-asyncio>=1.0.0",
6565
"pytest-bdd>=7.2.0",
6666
"python-dotenv>=1.0.0",
67-
"ty>=0.0.21",
67+
"ty==0.0.64",
6868
"cryptography>=46.0.3",
69-
"ruff>=0.8.0",
69+
"ruff==0.16.0",
7070
"starlette>=0.40.0",
7171
"anyio>=3.6.2",
7272
"httpx>=0.27.0",

‎src/sap_cloud_sdk/adms/user-guide.md‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ relation = client.relations.create(
4545

4646
# Upload bytes to the presigned URL (outside SDK)
4747
import requests
48+
4849
upload_url = relation.document.document_content_upload_urls[0]
4950
requests.put(upload_url, data=open("Invoice.pdf", "rb"))
5051
```
@@ -82,6 +83,7 @@ client = create_client(user_jwt=request.headers["Authorization"].split()[1])
8283
```python
8384
from sap_cloud_sdk.adms import create_client, TokenCache
8485

86+
8587
# By default tokens are cached in-process via InMemoryTokenCache.
8688
# For multi-instance deployments (Kyma replicas > 1, CF instances > 1),
8789
# implement your own TokenCache subclass backed by the shared cache your
@@ -91,13 +93,21 @@ class MySharedCache(TokenCache):
9193
def set(self, key, token, ttl_seconds): ...
9294
def delete(self, key): ...
9395

96+
9497
client = create_client(token_cache=MySharedCache())
9598
```
9699

97100
## Async Client
98101

99102
```python
100-
from sap_cloud_sdk.adms import create_async_client, BaseType, CreateDocumentInput, CreateDocumentRelationInput, RelationQueryOptions
103+
from sap_cloud_sdk.adms import (
104+
create_async_client,
105+
BaseType,
106+
CreateDocumentInput,
107+
CreateDocumentRelationInput,
108+
RelationQueryOptions,
109+
)
110+
101111

102112
async def main():
103113
async with create_async_client() as client:
@@ -203,6 +213,7 @@ job = client.jobs.start_zip_download(params)
203213

204214
# Poll until terminal state
205215
import time
216+
206217
while not job.job_status or not job.job_status.is_terminal():
207218
time.sleep(2)
208219
job = client.jobs.get_status(job.job_id)

‎src/sap_cloud_sdk/agent_decorators/user-guide.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ from sap_cloud_sdk.agent_decorators import (
2121
```python
2222
from sap_cloud_sdk.agent_decorators import prompt_section, agent_model
2323

24+
2425
# Define a prompt with a coded default
2526
@prompt_section(
2627
key="prompts.system",
@@ -30,6 +31,7 @@ from sap_cloud_sdk.agent_decorators import prompt_section, agent_model
3031
def system_prompt() -> str:
3132
return "You are a helpful assistant."
3233

34+
3335
# Define the model selection
3436
@agent_model(key="config.model", label="LLM Model")
3537
def model_name() -> str:
@@ -45,6 +47,7 @@ Expose a prompt section for editing.
4547
```python
4648
from sap_cloud_sdk.agent_decorators import prompt_section
4749

50+
4851
@prompt_section(
4952
key="prompts.identity",
5053
label="Agent Identity",
@@ -62,6 +65,7 @@ Expose a configuration value for editing.
6265
```python
6366
from sap_cloud_sdk.agent_decorators import agent_config
6467

68+
6569
@agent_config(
6670
key="config.temperature",
6771
label="Temperature",
@@ -78,6 +82,7 @@ Expose a model selection. The `description` parameter is optional.
7882
```python
7983
from sap_cloud_sdk.agent_decorators import agent_model
8084

85+
8186
@agent_model(
8287
key="config.model",
8388
label="Default Model",
@@ -93,6 +98,7 @@ def default_model() -> str:
9398
from sap_cloud_sdk.agent_decorators.exceptions import AgentDecoratorError
9499

95100
try:
101+
96102
@prompt_section(key="", label="L", description="D")
97103
def bad():
98104
return ""

‎src/sap_cloud_sdk/agent_memory/user-guide.md‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -714,6 +714,7 @@ elif total <= PAGE_SIZE:
714714
agent_id="my-agent", invoker_id="user-123", limit=total
715715
)
716716
else:
717+
717718
def iter_all_memories(client, agent_id, invoker_id, page_size=PAGE_SIZE):
718719
offset = 0
719720
while True:
@@ -781,7 +782,8 @@ The default `threshold` of `0.6` may be too strict for your data. Try a lower va
781782
782783
```python
783784
results = client.search_memories(
784-
agent_id="my-agent", invoker_id="user-123",
785+
agent_id="my-agent",
786+
invoker_id="user-123",
785787
query="user display preferences",
786788
threshold=0.3,
787789
)
@@ -933,10 +935,10 @@ from sap_cloud_sdk.agent_memory.factory.langgraph_checkpoint import create_check
933935
key="config.checkpointer.ttl_seconds",
934936
label="Thread TTL (seconds)",
935937
description="Evict inactive conversation threads after this period of "
936-
"inactivity. Set to 0 to disable eviction.",
938+
"inactivity. Set to 0 to disable eviction.",
937939
)
938940
def thread_ttl_seconds() -> int:
939-
return 3600 # 1 hour
941+
return 3600 # 1 hour
940942
941943
942944
class MyAgent:

‎src/sap_cloud_sdk/agentgateway/_customer.py‎

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -642,7 +642,7 @@ async def _list_server_tools(
642642
async with streamable_http_client(url, http_client=http_client) as (
643643
read,
644644
write,
645-
_,
645+
*_,
646646
):
647647
async with ClientSession(read, write) as session:
648648
init_result = await session.initialize()
@@ -672,6 +672,25 @@ async def _list_server_tools(
672672
]
673673

674674

675+
def _log_mcp_server_error(ord_id: str, exc: BaseException) -> None:
676+
# Unwrap ExceptionGroup from anyio to surface the real HTTP error body
677+
if isinstance(exc, BaseExceptionGroup):
678+
for inner in exc.exceptions:
679+
_log_mcp_server_error(ord_id, inner)
680+
return
681+
if isinstance(exc, httpx.HTTPStatusError):
682+
logger.error(
683+
"Failed to load tools from %s (HTTP %d): %s",
684+
ord_id,
685+
exc.response.status_code,
686+
exc.response.text[:500],
687+
)
688+
else:
689+
logger.exception(
690+
"Failed to load tools from %s — skipping", ord_id, exc_info=exc
691+
)
692+
693+
675694
async def get_mcp_tools_customer(
676695
credentials: CustomerCredentials,
677696
system_token: str,
@@ -689,16 +708,14 @@ async def get_mcp_tools_customer(
689708
690709
Returns:
691710
List of MCPTool objects from all servers.
692-
693-
Raises:
694-
AgentGatewaySDKError: If integrationDependencies is empty.
695711
"""
696712
dependencies = credentials.integration_dependencies
697713

698714
if not dependencies:
699-
raise AgentGatewaySDKError(
715+
logger.warning(
700716
"integrationDependencies is empty in credentials — no MCP servers configured."
701717
)
718+
return []
702719

703720
logger.info("Discovering tools from %d MCP server(s)", len(dependencies))
704721

@@ -717,8 +734,8 @@ async def get_mcp_tools_customer(
717734
server_tools = await _list_server_tools(url, system_token, timeout)
718735
tools.extend(server_tools)
719736
logger.debug("Loaded %d tool(s) from %s", len(server_tools), dep.ord_id)
720-
except Exception:
721-
logger.exception("Failed to load tools from %s — skipping", dep.ord_id)
737+
except Exception as exc:
738+
_log_mcp_server_error(dep.ord_id, exc)
722739

723740
logger.info(
724741
"Loaded %d MCP tool(s) from %d server(s)", len(tools), len(dependencies)
@@ -758,7 +775,7 @@ async def call_mcp_tool_customer(
758775
async with streamable_http_client(tool.url, http_client=http_client) as (
759776
read,
760777
write,
761-
_,
778+
*_,
762779
):
763780
async with ClientSession(read, write) as session:
764781
await session.initialize()
@@ -769,4 +786,9 @@ async def call_mcp_tool_customer(
769786
return ""
770787

771788
first = result.content[0]
772-
return str(getattr(first, "text", ""))
789+
text = str(getattr(first, "text", ""))
790+
791+
if result.isError:
792+
logger.error("Tool '%s' returned an error: %s", tool.name, text)
793+
794+
return text

‎src/sap_cloud_sdk/agentgateway/_lob.py‎

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,26 @@ def _fetch_user_auth_sync():
290290
return token, gateway_url
291291

292292

293+
def _log_mcp_server_error(fragment_name: str, exc: BaseException) -> None:
294+
if isinstance(exc, BaseExceptionGroup):
295+
for inner in exc.exceptions:
296+
_log_mcp_server_error(fragment_name, inner)
297+
return
298+
if isinstance(exc, httpx.HTTPStatusError):
299+
logger.error(
300+
"Failed to load tools from fragment '%s' (HTTP %d): %s",
301+
fragment_name,
302+
exc.response.status_code,
303+
exc.response.text[:500],
304+
)
305+
else:
306+
logger.exception(
307+
"Failed to load tools from fragment '%s' — skipping",
308+
fragment_name,
309+
exc_info=exc,
310+
)
311+
312+
293313
async def list_server_tools(
294314
dest_url: str, auth_token: str, fragment_name: str, timeout: float
295315
) -> list[MCPTool]:
@@ -313,7 +333,7 @@ async def list_server_tools(
313333
async with streamable_http_client(dest_url, http_client=http_client) as (
314334
read,
315335
write,
316-
_,
336+
*_,
317337
):
318338
async with ClientSession(read, write) as session:
319339
init_result = await session.initialize()
@@ -388,11 +408,8 @@ async def get_mcp_tools_lob(
388408
len(server_tools),
389409
fragment_name,
390410
)
391-
except Exception:
392-
logger.exception(
393-
"Failed to load tools from fragment '%s' — skipping",
394-
fragment_name,
395-
)
411+
except Exception as exc:
412+
_log_mcp_server_error(fragment_name, exc)
396413

397414
logger.info("Loaded %d MCP tool(s) from %d fragment(s)", len(tools), len(fragments))
398415
return tools
@@ -427,7 +444,7 @@ async def call_mcp_tool_lob(
427444
async with streamable_http_client(tool.url, http_client=http_client) as (
428445
read,
429446
write,
430-
_,
447+
*_,
431448
):
432449
async with ClientSession(read, write) as session:
433450
await session.initialize()
@@ -436,7 +453,12 @@ async def call_mcp_tool_lob(
436453
logger.warning("Tool '%s' returned empty content", tool.name)
437454
return ""
438455
first = result.content[0]
439-
return str(getattr(first, "text", ""))
456+
text = str(getattr(first, "text", ""))
457+
458+
if result.isError:
459+
logger.error("Tool '%s' returned an error: %s", tool.name, text)
460+
461+
return text
440462

441463

442464
async def _fetch_agent_card(

‎src/sap_cloud_sdk/agentgateway/user-guide.md‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,7 @@ from sap_cloud_sdk.agentgateway import AgentCardFilter
250250

251251
AgentCardFilter(
252252
agent_names=[], # agent card names to include (matched against card JSON `name`); empty = no filter
253-
ord_ids=[], # ORD IDs to include (extracted from fragment URL); empty = no filter
253+
ord_ids=[], # ORD IDs to include (extracted from fragment URL); empty = no filter
254254
)
255255
```
256256

@@ -261,12 +261,14 @@ Both fields default to empty lists. Filters are applied with AND semantics: if b
261261
```python
262262
@dataclass
263263
class Agent:
264-
ord_id: str # ORD ID from fragment ordId property
264+
ord_id: str # ORD ID from fragment ordId property
265265
agent_card: AgentCard
266266

267+
267268
@dataclass
268269
class AgentCard:
269-
raw: dict # full parsed JSON from /.well-known/agent-card.json
270+
raw: dict # full parsed JSON from /.well-known/agent-card.json
271+
270272

271273
@dataclass
272274
class MCPTool:

0 commit comments

Comments
 (0)