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
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ The Python SDK offers a clean, type-safe API following Python best practices whi
- **AI Core Integration**
- **Audit Log Service**
- **Audit Log NG**
- **Content Filtering & Prompt Shield** *(enabled by default from 0.28.0)*
- **Destination Service**
- **Document Management Service**
- **Extensibility**
Expand All @@ -26,6 +27,12 @@ The Python SDK offers a clean, type-safe API following Python best practices whi
- **Data Anonymization Service**
- **Print Service**

> **Breaking change in 0.28.0:** `set_aicore_config()` now automatically enables
> Azure Content Safety filtering and prompt shield for all SAP AI Core model calls.
> No code change is required. To disable: set `ORCH_FILTER_ENABLED=false` or call
> `set_filtering(enabled=False)` after `set_aicore_config()`.
> See the [Orchestration user guide](src/sap_cloud_sdk/orchestration/user-guide.md).

## Requirements and Setup

- **Python**: 3.11 or higher
Expand Down
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.27.1"
version = "0.28.0"
description = "SAP Cloud SDK for Python"
readme = "README.md"
license = "Apache-2.0"
Expand Down
12 changes: 12 additions & 0 deletions src/sap_cloud_sdk/aicore/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,5 +145,17 @@ def set_aicore_config(instance_name: str = "aicore-instance") -> None:
# Log configuration completion (excluding sensitive information)
logger.info("AI Core configuration has been set successfully")

# Activate content filtering for all sap/* LiteLLM model calls.
# Lazy import avoids circular dependency between aicore and orchestration.
# Filtering is ON by default (threshold 4, prompt_shield=True).
# Set ORCH_FILTER_ENABLED=false to disable, or call set_filtering() to override.
try:
from sap_cloud_sdk.orchestration._litellm_patch import _install
from sap_cloud_sdk.orchestration._models import FilteringModuleConfig

_install(FilteringModuleConfig.from_env())
except Exception as e:
logger.warning("Could not activate orchestration filtering: %s", e)


__all__ = ["set_aicore_config"]
51 changes: 51 additions & 0 deletions src/sap_cloud_sdk/aicore/user-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,57 @@ The `set_aicore_config()` function:
2. **Configures environment variables** for LiteLLM to use AI Core
3. **Normalizes URLs** by adding required suffixes (`/oauth/token` for auth, `/v2` for base URL)
4. **Sets resource group** (defaults to "default" if not specified)
5. **Activates content filtering** — Azure Content Safety + prompt shield enabled by default *(new in 0.28.0)*

---

## Content Filtering (enabled by default from 0.28.0)

`set_aicore_config()` automatically activates content filtering for all model calls.
No additional code is required.

**Default thresholds:**

| Category | Default | Meaning |
|---|---|---|
| Hate, Violence, Sexual, Self-harm | `4` | Block medium+ severity |
| Prompt shield | `true` | Block jailbreak + indirect injection attempts |

To override thresholds via environment variables (set before calling `set_aicore_config()`):

```bash
ORCH_FILTER_SELF_HARM=0 # strict — block any detected self-harm content
ORCH_FILTER_VIOLENCE=2
ORCH_FILTER_ENABLED=false # disable filtering entirely
```

To override programmatically (call after `set_aicore_config()`):

```python
from sap_cloud_sdk.orchestration import set_filtering
set_filtering(self_harm=0, violence=0)
```

To catch blocked requests:

```python
from sap_cloud_sdk.orchestration import ContentFilteredError
from sap_cloud_sdk.orchestration._litellm_patch import extract_filter_blocked

try:
response = completion(model="sap/anthropic--claude-4.5-sonnet", messages=[...])
except ContentFilteredError as e:
print("Blocked by content safety policy.")
except Exception as e:
blocked = extract_filter_blocked(e)
if blocked:
print("Blocked by content safety policy.")
raise
```

See the [Orchestration user guide](../orchestration/user-guide.md) for full documentation.

---

### Credentials Loaded

Expand Down
1 change: 1 addition & 0 deletions src/sap_cloud_sdk/core/telemetry/module.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class Module(str, Enum):
DMS = "dms"
EXTENSIBILITY = "extensibility"
OBJECTSTORE = "objectstore"
ORCHESTRATION = "orchestration"
PRINT = "print"
TELEMETRY = "telemetry"

Expand Down
3 changes: 3 additions & 0 deletions src/sap_cloud_sdk/core/telemetry/operation.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,5 +201,8 @@ class Operation(str, Enum):
AGENT_MEMORY_GET_RETENTION_CONFIG = "get_retention_config"
AGENT_MEMORY_UPDATE_RETENTION_CONFIG = "update_retention_config"

# Orchestration Operations
ORCHESTRATION_SET_FILTERING = "set_filtering"

def __str__(self) -> str:
return self.value
120 changes: 120 additions & 0 deletions src/sap_cloud_sdk/orchestration/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
"""SAP AI Core Orchestration — content filtering and prompt shield.

Filtering is **enabled by default** when ``set_aicore_config()`` is called.
No additional code is required. To override thresholds, use ``set_filtering()``
or set ``ORCH_FILTER_*`` environment variables.

See user-guide.md for full documentation.
"""

from __future__ import annotations

import logging
from typing import Literal

from sap_cloud_sdk.core.telemetry.metrics_decorator import record_metrics
from sap_cloud_sdk.core.telemetry.module import Module
from sap_cloud_sdk.core.telemetry.operation import Operation

from ._litellm_patch import _install, extract_filter_blocked
from ._models import ContentFilterConfig, FilteringModuleConfig, PromptShieldConfig
from .exceptions import ContentFilteredError, OrchestrationError

logger = logging.getLogger(__name__)

__all__ = [
"set_filtering",
"ContentFilterConfig",
"PromptShieldConfig",
"FilteringModuleConfig",
"ContentFilteredError",
"OrchestrationError",
"extract_filter_blocked",
]


@record_metrics(Module.ORCHESTRATION, Operation.ORCHESTRATION_SET_FILTERING)
def set_filtering(
*,
hate: Literal[0, 2, 4, 6] | None = None,
violence: Literal[0, 2, 4, 6] | None = None,
sexual: Literal[0, 2, 4, 6] | None = None,
self_harm: Literal[0, 2, 4, 6] | None = None,
prompt_shield: bool | None = None,
directions: set[Literal["input", "output"]] | None = None,
enabled: bool | None = None,
) -> None:
"""Override content filtering thresholds programmatically.

Filtering is already activated by ``set_aicore_config()`` — this function
is only needed to override specific thresholds at runtime. Any argument
not provided retains its current value (from env vars or defaults).

Args:
hate: Azure Content Safety hate severity. 0=strict, 2=low+, 4=medium+ (default), 6=off.
violence: Azure Content Safety violence severity.
sexual: Azure Content Safety sexual severity.
self_harm: Azure Content Safety self-harm severity.
prompt_shield: Enable/disable jailbreak + indirect injection detection (input-only).
directions: Set of directions to filter. Default is ``{"input", "output"}``.
enabled: Set ``False`` to disable filtering entirely.

Examples:
Tighten two thresholds::

set_filtering(self_harm=0, violence=0)

Disable filtering entirely::

set_filtering(enabled=False)
"""
if enabled is False:
_install(None)
return

# No args at all — just re-apply env-based config (respects ORCH_FILTER_ENABLED)
if all(
v is None
for v in [hate, violence, sexual, self_harm, prompt_shield, directions, enabled]
):
_install(FilteringModuleConfig.from_env())
return

# Some args provided — start from env-based config then override
base = FilteringModuleConfig.from_env() or FilteringModuleConfig()

# Build effective threshold — override only the provided args.
def _effective_filter(
existing: ContentFilterConfig | None,
) -> ContentFilterConfig | None:
if existing is None and directions is not None and "input" not in directions:
return None
src = existing or ContentFilterConfig()
return ContentFilterConfig(
hate=hate if hate is not None else src.hate,
violence=violence if violence is not None else src.violence,
sexual=sexual if sexual is not None else src.sexual,
self_harm=self_harm if self_harm is not None else src.self_harm,
)

new_input = (
_effective_filter(base.input_filter)
if (directions is None or "input" in (directions or {"input", "output"}))
else None
)
new_output = (
_effective_filter(base.output_filter)
if (directions is None or "output" in (directions or {"input", "output"}))
else None
)

new_shield = base.prompt_shield
if prompt_shield is not None:
new_shield = PromptShieldConfig(enabled=prompt_shield)

cfg = FilteringModuleConfig(
input_filter=new_input,
output_filter=new_output,
prompt_shield=new_shield,
)
_install(cfg)
Loading
Loading