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
6 changes: 2 additions & 4 deletions .env_integration_tests.example
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,5 @@ CLOUD_SDK_CFG_DESTINATION_DEFAULT_IDENTITYZONE=your-identity-zone-here
CLOUD_SDK_CFG_SDM_DEFAULT_URI=https://your-sdm-api-uri-here
CLOUD_SDK_CFG_SDM_DEFAULT_UAA='{"url":"https://your-auth-url","clientid":"your-client-id","clientsecret":"your-client-secret","identityzone":"your-identity-zone"}'

CLOUD_SDK_CFG_HANA_AGENT_MEMORY_DEFAULT_APPLICATION_URL=https://your-agent-memory-api-url-here
CLOUD_SDK_CFG_HANA_AGENT_MEMORY_DEFAULT_UAA_URL=https://your-auth-url-here
CLOUD_SDK_CFG_HANA_AGENT_MEMORY_DEFAULT_UAA_CLIENTID=your-client-id-here
CLOUD_SDK_CFG_HANA_AGENT_MEMORY_DEFAULT_UAA_CLIENTSECRET=your-client-secret-here
CLOUD_SDK_CFG_HANA_AGENT_MEMORY_DEFAULT_URL=https://your-agent-memory-api-url-here
CLOUD_SDK_CFG_HANA_AGENT_MEMORY_DEFAULT_UAA='{"url":"https://your-auth-url","clientid":"your-client-id","clientsecret":"your-client-secret"}'
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ The Python SDK offers a clean, type-safe API following Python best practices whi
- **Agent Decorators**
- **AI Core Integration**
- **Audit Log Service**
- **Agent Memory Service**
- **Destination Service**
- **Document Management Service**
- **ObjectStore Service**
Expand All @@ -25,7 +26,6 @@ The Python SDK offers a clean, type-safe API following Python best practices whi

### Installation


#### uv

```bash
Expand All @@ -42,7 +42,7 @@ poetry add sap-cloud-sdk

```bash
pip install sap-cloud-sdk
````
```

### Environment Configuration

Expand All @@ -61,6 +61,7 @@ Each module has comprehensive usage guides:

- [Agent Decorators](src/sap_cloud_sdk/agent_decorators/user-guide.md)
- [AuditLog](src/sap_cloud_sdk/core/auditlog/user-guide.md)
- [Agent Memory](src/sap_cloud_sdk/agent_memory/user-guide.md)
- [Destination](src/sap_cloud_sdk/destination/user-guide.md)
- [DMS](src/sap_cloud_sdk/dms/user-guide.md)
- [ObjectStore](src/sap_cloud_sdk/objectstore/user-guide.md)
Expand Down
6 changes: 2 additions & 4 deletions docs/INTEGRATION_TESTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,8 @@ For Agent Memory integration tests, configure the following variables in `.env_i

```bash
# Agent Memory Configuration
CLOUD_SDK_CFG_AGENT_MEMORY_DEFAULT_URL=https://your-agent-memory-api-url
CLOUD_SDK_CFG_AGENT_MEMORY_DEFAULT_AUTH_URL=https://your-auth-url
CLOUD_SDK_CFG_AGENT_MEMORY_DEFAULT_CLIENTID=your-client-id
CLOUD_SDK_CFG_AGENT_MEMORY_DEFAULT_CLIENTSECRET=your-client-secret
CLOUD_SDK_CFG_HANA_AGENT_MEMORY_DEFAULT_URL=https://your-agent-memory-api-url
CLOUD_SDK_CFG_HANA_AGENT_MEMORY_DEFAULT_UAA='{"url":"https://your-auth-url","clientid":"your-client-id","clientsecret":"your-client-secret"}'
```

## Running Integration Tests
Expand Down
138 changes: 57 additions & 81 deletions src/sap_cloud_sdk/agent_memory/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,21 @@

Mount path convention::

/etc/secrets/appfnd/hana-agent-memory/default/url
/etc/secrets/appfnd/hana-agent-memory/default/uaa

``url`` is the Agent Memory service base URL (plain string).
``uaa`` is a JSON string with OAuth2 credentials containing at minimum:
``clientid``, ``clientsecret``, and ``url`` (UAA base URL).

/etc/secrets/appfnd/hana-agent-memory/default/{field_key}
Env fallback convention::

Keys: ``application_url``, ``uaa.url``, ``uaa.clientid``, ``uaa.clientsecret``

Env fallback convention (uppercased)::

CLOUD_SDK_CFG_HANA_AGENT_MEMORY_DEFAULT_APPLICATION_URL
CLOUD_SDK_CFG_HANA_AGENT_MEMORY_DEFAULT_UAA_URL
CLOUD_SDK_CFG_HANA_AGENT_MEMORY_DEFAULT_UAA_CLIENTID
CLOUD_SDK_CFG_HANA_AGENT_MEMORY_DEFAULT_UAA_CLIENTSECRET
CLOUD_SDK_CFG_HANA_AGENT_MEMORY_DEFAULT_URL
CLOUD_SDK_CFG_HANA_AGENT_MEMORY_DEFAULT_UAA
"""

from dataclasses import dataclass, field
import json
from dataclasses import dataclass
from typing import Optional

from sap_cloud_sdk.agent_memory.exceptions import AgentMemoryConfigError
Expand Down Expand Up @@ -60,11 +60,18 @@ class AgentMemoryConfig:
def __post_init__(self) -> None:
if not self.base_url:
raise AgentMemoryConfigError("base_url must be a non-empty string")


# NOTE: BindingData must NOT use `from __future__ import annotations`
# because the secret resolver checks `f.type is str` at runtime, which requires
# actual type objects rather than string annotations.
if self.token_url is not None and not self.token_url:
raise AgentMemoryConfigError(
"token_url must be a non-empty string when provided"
)
if self.client_id is not None and not self.client_id:
raise AgentMemoryConfigError(
"client_id must be a non-empty string when provided"
)
if self.client_secret is not None and not self.client_secret:
raise AgentMemoryConfigError(
"client_secret must be a non-empty string when provided"
)


@dataclass
Expand All @@ -74,73 +81,42 @@ class BindingData:
All fields must be plain ``str`` to satisfy the resolver contract.
"""

application_url: str = ""
uaa_url: str = field(default="", metadata={"secret": "uaa.url"})
uaa_clientid: str = field(default="", metadata={"secret": "uaa.clientid"})
uaa_clientsecret: str = field(default="", metadata={"secret": "uaa.clientsecret"})
url: str = ""
uaa: str = ""

def validate(self) -> None:
"""Raise ``AgentMemoryConfigError`` if any required field is empty."""
missing = [
f
for f in ("application_url", "uaa_url", "uaa_clientid", "uaa_clientsecret")
if not getattr(self, f)
]
if missing:
if not self.url:
raise AgentMemoryConfigError(
"Agent Memory binding is missing required field: url"
)
if not self.uaa:
raise AgentMemoryConfigError(
f"Agent Memory binding is missing required fields: {', '.join(missing)}"
"Agent Memory binding is missing required field: uaa"
)

def extract_config(self) -> AgentMemoryConfig:
"""Derive an ``AgentMemoryConfig`` from the raw binding fields."""
return AgentMemoryConfig(
base_url=self.application_url,
token_url=self.uaa_url.rstrip("/") + "/oauth/token",
client_id=self.uaa_clientid,
client_secret=self.uaa_clientsecret,
)


_ENV_PREFIX = "CLOUD_SDK_CFG_HANA_AGENT_MEMORY_DEFAULT"

# Explicit env var names — dots are not valid in shell variable names,
# so we define these directly rather than deriving them from BindingData metadata keys
# (which use dots to match the BTP mount-path file naming convention).
_ENV_VARS = {
"application_url": f"{_ENV_PREFIX}_APPLICATION_URL",
"uaa_url": f"{_ENV_PREFIX}_UAA_URL",
"uaa_clientid": f"{_ENV_PREFIX}_UAA_CLIENTID",
"uaa_clientsecret": f"{_ENV_PREFIX}_UAA_CLIENTSECRET",
}


def _load_binding_from_env() -> BindingData:
"""Read Agent Memory binding from environment variables.

Raises:
AgentMemoryConfigError: If any required variable is absent.
"""
import os

binding = BindingData()
missing: list[str] = []
for attr, var in _ENV_VARS.items():
value = os.environ.get(var)
if not value:
missing.append(var)
else:
setattr(binding, attr, value)
if missing:
raise AgentMemoryConfigError(
f"Missing required environment variables: {', '.join(missing)}"
)
return binding
"""Parse the UAA JSON string and return an ``AgentMemoryConfig``."""
try:
uaa_data = json.loads(self.uaa, strict=False)
except json.JSONDecodeError as e:
raise AgentMemoryConfigError(f"Failed to parse uaa JSON: {e}")

try:
return AgentMemoryConfig(
base_url=self.url,
token_url=uaa_data["url"].rstrip("/") + "/oauth/token",
client_id=uaa_data["clientid"],
client_secret=uaa_data["clientsecret"],
)
except KeyError as e:
raise AgentMemoryConfigError(f"Missing required field in uaa JSON: {e}")


def _load_config_from_env() -> AgentMemoryConfig:
"""Load Agent Memory configuration from a mounted volume or environment variables.

Tries (in order):
Uses the secret resolver with fallback order:
1. Mount at ``/etc/secrets/appfnd/hana-agent-memory/default/``
2. Environment variables ``CLOUD_SDK_CFG_HANA_AGENT_MEMORY_DEFAULT_*``

Expand All @@ -150,24 +126,24 @@ def _load_config_from_env() -> AgentMemoryConfig:
Raises:
AgentMemoryConfigError: If configuration cannot be loaded or is incomplete.
"""
from sap_cloud_sdk.core.secret_resolver.resolver import _load_from_mount
from sap_cloud_sdk.core.secret_resolver import (
read_from_mount_and_fallback_to_env_var,
)

mount_error: Exception | None = None
try:
binding = BindingData()
_load_from_mount("/etc/secrets/appfnd", "hana-agent-memory", "default", binding)
binding.validate()
return binding.extract_config()
except Exception as exc:
mount_error = exc

try:
binding = _load_binding_from_env()
read_from_mount_and_fallback_to_env_var(
base_volume_mount="/etc/secrets/appfnd",
base_var_name="CLOUD_SDK_CFG",
module="hana-agent-memory",
instance="default",
target=binding,
)
binding.validate()
return binding.extract_config()
except AgentMemoryConfigError:
raise
except Exception as exc:
raise AgentMemoryConfigError(
f"Failed to load Agent Memory configuration: mount={mount_error}; env={exc}"
f"Failed to load Agent Memory configuration: {exc}"
) from exc
41 changes: 32 additions & 9 deletions src/sap_cloud_sdk/agent_memory/user-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -772,14 +772,37 @@ environment variables or service binding.

## Configuration

`create_client()` resolves credentials automatically in the following order:
### Service Binding

1. **Mounted volume** — `/etc/secrets/appfnd/hana-agent-memory/default/{field}`
2. **Environment variables** — `CLOUD_SDK_CFG_HANA_AGENT_MEMORY_DEFAULT_*`
- **Mount path**: `$SERVICE_BINDING_ROOT/hana-agent-memory/default/` (defaults to `/etc/secrets/appfnd/hana-agent-memory/default/`)
- **Required keys**: `url` (Agent Memory service URL), `uaa` (JSON string with XSUAA credentials)
- **Env var fallback**: `CLOUD_SDK_CFG_HANA_AGENT_MEMORY_DEFAULT_{FIELD}` (uppercased)

| Environment Variable | Description |
| ---------------------------------------------------------- | ------------------------------------ |
| `CLOUD_SDK_CFG_HANA_AGENT_MEMORY_DEFAULT_APPLICATION_URL` | Base URL of the Agent Memory service |
| `CLOUD_SDK_CFG_HANA_AGENT_MEMORY_DEFAULT_UAA_URL` | OAuth2 authorization server base URL |
| `CLOUD_SDK_CFG_HANA_AGENT_MEMORY_DEFAULT_UAA_CLIENTID` | OAuth2 client ID |
| `CLOUD_SDK_CFG_HANA_AGENT_MEMORY_DEFAULT_UAA_CLIENTSECRET` | OAuth2 client secret |
> **Note:** `SERVICE_BINDING_ROOT` defaults to `/etc/secrets/appfnd` when not set. See the [Secret Resolver guide](../core/secret_resolver/user-guide.md) for details.

#### Mounted Secrets (Kubernetes)

```
$SERVICE_BINDING_ROOT/hana-agent-memory/default/
├── url
└── uaa
```

#### Environment Variables

```bash
export CLOUD_SDK_CFG_HANA_AGENT_MEMORY_DEFAULT_URL="https://agent-memory.example.com"
export CLOUD_SDK_CFG_HANA_AGENT_MEMORY_DEFAULT_UAA='{"clientid":"...","clientsecret":"...","url":"https://..."}'
```

#### UAA JSON Schema

The `uaa` key must contain a JSON string with the XSUAA credentials:

```json
{
"clientid": "sb-xxx",
"clientsecret": "xxx",
"url": "https://subdomain.authentication.region.hana.ondemand.com"
}
```
11 changes: 7 additions & 4 deletions tests/agent_memory/unit/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,13 @@ def test_uses_provided_config(self):

def test_reads_env_when_no_config_provided(self, monkeypatch):
"""Factory falls back to environment variables when no config given."""
monkeypatch.setenv("CLOUD_SDK_CFG_HANA_AGENT_MEMORY_DEFAULT_APPLICATION_URL", "http://memory.example.com")
monkeypatch.setenv("CLOUD_SDK_CFG_HANA_AGENT_MEMORY_DEFAULT_UAA_URL", "http://auth.example.com")
monkeypatch.setenv("CLOUD_SDK_CFG_HANA_AGENT_MEMORY_DEFAULT_UAA_CLIENTID", "client-id")
monkeypatch.setenv("CLOUD_SDK_CFG_HANA_AGENT_MEMORY_DEFAULT_UAA_CLIENTSECRET", "client-secret")
import json
monkeypatch.setenv("CLOUD_SDK_CFG_HANA_AGENT_MEMORY_DEFAULT_URL", "http://memory.example.com")
monkeypatch.setenv("CLOUD_SDK_CFG_HANA_AGENT_MEMORY_DEFAULT_UAA", json.dumps({
"url": "http://auth.example.com",
"clientid": "client-id",
"clientsecret": "client-secret",
}))
with patch("sap_cloud_sdk.agent_memory.HttpTransport") as MockTransport:
MockTransport.return_value = MagicMock(spec=HttpTransport)
client = create_client()
Expand Down
Loading
Loading