strands-deepkeep 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- strands_deepkeep-0.1.0/LICENSE +21 -0
- strands_deepkeep-0.1.0/MANIFEST.in +4 -0
- strands_deepkeep-0.1.0/PKG-INFO +97 -0
- strands_deepkeep-0.1.0/README.md +68 -0
- strands_deepkeep-0.1.0/UPSTREAM_SUBMISSION.md +32 -0
- strands_deepkeep-0.1.0/pyproject.toml +44 -0
- strands_deepkeep-0.1.0/setup.cfg +4 -0
- strands_deepkeep-0.1.0/strands_deepkeep/__init__.py +13 -0
- strands_deepkeep-0.1.0/strands_deepkeep/guardrail.py +251 -0
- strands_deepkeep-0.1.0/strands_deepkeep.egg-info/PKG-INFO +97 -0
- strands_deepkeep-0.1.0/strands_deepkeep.egg-info/SOURCES.txt +14 -0
- strands_deepkeep-0.1.0/strands_deepkeep.egg-info/dependency_links.txt +1 -0
- strands_deepkeep-0.1.0/strands_deepkeep.egg-info/requires.txt +6 -0
- strands_deepkeep-0.1.0/strands_deepkeep.egg-info/top_level.txt +1 -0
- strands_deepkeep-0.1.0/tests/test_client.py +45 -0
- strands_deepkeep-0.1.0/tests/test_guardrail.py +122 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 DeepKeep
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: strands-deepkeep
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: DeepKeep AI Firewall hook integration for Strands Agents
|
|
5
|
+
Author-email: DeepKeep <info@deepkeep.ai>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Documentation, https://github.com/Deepkeepai/strands-deepkeep#readme
|
|
8
|
+
Project-URL: Homepage, https://deepkeep.ai
|
|
9
|
+
Keywords: deepkeep,strands,hooks,moderation,guardrails
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Classifier: Topic :: Security
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: httpx>=0.27.0
|
|
24
|
+
Requires-Dist: strands-agents>=1.0.0
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
27
|
+
Requires-Dist: respx>=0.21; extra == "dev"
|
|
28
|
+
Dynamic: license-file
|
|
29
|
+
|
|
30
|
+
# strands-deepkeep
|
|
31
|
+
|
|
32
|
+
Strands Agents hook provider for the [DeepKeep AI Firewall](https://deepkeep.ai/capabilities/ai-firewall).
|
|
33
|
+
|
|
34
|
+
DeepKeep runs before a Strands invocation reaches the model and after the model returns. It can block unsafe content, pass safe content through, and apply redacted or modified text returned by DeepKeep.
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install strands-deepkeep
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Quick start
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
import os
|
|
46
|
+
|
|
47
|
+
from strands import Agent
|
|
48
|
+
from strands_deepkeep import DeepKeepGuardrail
|
|
49
|
+
|
|
50
|
+
agent = Agent(
|
|
51
|
+
plugins=[
|
|
52
|
+
DeepKeepGuardrail(
|
|
53
|
+
api_key=os.environ["DEEPKEEP_API_KEY"],
|
|
54
|
+
base_url=os.environ["DEEPKEEP_BASE_URL"],
|
|
55
|
+
pre_model="input-firewall-id",
|
|
56
|
+
post_model="output-firewall-id",
|
|
57
|
+
)
|
|
58
|
+
]
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
result = agent("Summarize this customer support request")
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`pre_model` and `post_model` are DeepKeep firewall IDs. Each value is sent to the matching moderation endpoint as the `model` field.
|
|
65
|
+
|
|
66
|
+
## Behavior
|
|
67
|
+
|
|
68
|
+
- `allow` and `alert` decisions let the invocation continue.
|
|
69
|
+
- `block` decisions cancel the invocation before the model or clear blocked model output after the model call.
|
|
70
|
+
- `redact` and `modify` decisions replace the latest user text before the invocation or replace generated assistant text after the model call.
|
|
71
|
+
- DeepKeep API decisions are stored in `invocation_state` as `deepkeep_pre` and `deepkeep_post` for audit and logging.
|
|
72
|
+
|
|
73
|
+
## Environment
|
|
74
|
+
|
|
75
|
+
You can pass credentials directly or set:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
export DEEPKEEP_API_KEY="dk-..."
|
|
79
|
+
export DEEPKEEP_BASE_URL="https://your-deepkeep-instance.example"
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Strands catalog entry
|
|
83
|
+
|
|
84
|
+
Submit the integration to Strands by adding `site/src/content/catalog/strands-deepkeep.yaml` to `strands-agents/harness-sdk`:
|
|
85
|
+
|
|
86
|
+
```yaml
|
|
87
|
+
name: strands-deepkeep
|
|
88
|
+
description: DeepKeep AI Firewall guardrails for Strands Agents using lifecycle hooks around model input and output.
|
|
89
|
+
integrationType: plugin
|
|
90
|
+
languages:
|
|
91
|
+
python:
|
|
92
|
+
package: strands-deepkeep
|
|
93
|
+
github: https://github.com/Deepkeepai/strands-deepkeep
|
|
94
|
+
docsUrl: https://github.com/Deepkeepai/strands-deepkeep#readme
|
|
95
|
+
addedDate: 2026-08-26
|
|
96
|
+
maintainedBy: partner
|
|
97
|
+
```
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# strands-deepkeep
|
|
2
|
+
|
|
3
|
+
Strands Agents hook provider for the [DeepKeep AI Firewall](https://deepkeep.ai/capabilities/ai-firewall).
|
|
4
|
+
|
|
5
|
+
DeepKeep runs before a Strands invocation reaches the model and after the model returns. It can block unsafe content, pass safe content through, and apply redacted or modified text returned by DeepKeep.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install strands-deepkeep
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick start
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
import os
|
|
17
|
+
|
|
18
|
+
from strands import Agent
|
|
19
|
+
from strands_deepkeep import DeepKeepGuardrail
|
|
20
|
+
|
|
21
|
+
agent = Agent(
|
|
22
|
+
plugins=[
|
|
23
|
+
DeepKeepGuardrail(
|
|
24
|
+
api_key=os.environ["DEEPKEEP_API_KEY"],
|
|
25
|
+
base_url=os.environ["DEEPKEEP_BASE_URL"],
|
|
26
|
+
pre_model="input-firewall-id",
|
|
27
|
+
post_model="output-firewall-id",
|
|
28
|
+
)
|
|
29
|
+
]
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
result = agent("Summarize this customer support request")
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`pre_model` and `post_model` are DeepKeep firewall IDs. Each value is sent to the matching moderation endpoint as the `model` field.
|
|
36
|
+
|
|
37
|
+
## Behavior
|
|
38
|
+
|
|
39
|
+
- `allow` and `alert` decisions let the invocation continue.
|
|
40
|
+
- `block` decisions cancel the invocation before the model or clear blocked model output after the model call.
|
|
41
|
+
- `redact` and `modify` decisions replace the latest user text before the invocation or replace generated assistant text after the model call.
|
|
42
|
+
- DeepKeep API decisions are stored in `invocation_state` as `deepkeep_pre` and `deepkeep_post` for audit and logging.
|
|
43
|
+
|
|
44
|
+
## Environment
|
|
45
|
+
|
|
46
|
+
You can pass credentials directly or set:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
export DEEPKEEP_API_KEY="dk-..."
|
|
50
|
+
export DEEPKEEP_BASE_URL="https://your-deepkeep-instance.example"
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Strands catalog entry
|
|
54
|
+
|
|
55
|
+
Submit the integration to Strands by adding `site/src/content/catalog/strands-deepkeep.yaml` to `strands-agents/harness-sdk`:
|
|
56
|
+
|
|
57
|
+
```yaml
|
|
58
|
+
name: strands-deepkeep
|
|
59
|
+
description: DeepKeep AI Firewall guardrails for Strands Agents using lifecycle hooks around model input and output.
|
|
60
|
+
integrationType: plugin
|
|
61
|
+
languages:
|
|
62
|
+
python:
|
|
63
|
+
package: strands-deepkeep
|
|
64
|
+
github: https://github.com/Deepkeepai/strands-deepkeep
|
|
65
|
+
docsUrl: https://github.com/Deepkeepai/strands-deepkeep#readme
|
|
66
|
+
addedDate: 2026-08-26
|
|
67
|
+
maintainedBy: partner
|
|
68
|
+
```
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Strands Integration Submission
|
|
2
|
+
|
|
3
|
+
Ask Strands Agents to list DeepKeep under plugins as a reusable guardrail integration named DeepKeep AI Firewall.
|
|
4
|
+
|
|
5
|
+
## Positioning
|
|
6
|
+
|
|
7
|
+
DeepKeep AI Firewall
|
|
8
|
+
|
|
9
|
+
Use [DeepKeep AI Firewall](https://deepkeep.ai/capabilities/ai-firewall) as runtime guardrails for Strands Agents. Add `DeepKeepGuardrail` as a plugin to run DeepKeep pre-moderation before agent input reaches the model and post-moderation after model responses. Supports allow, block, redact, modify, and alert workflows for prompt injection, jailbreaks, sensitive data leakage, unsafe content, and other AI risks.
|
|
10
|
+
|
|
11
|
+
## Why plugin
|
|
12
|
+
|
|
13
|
+
DeepKeep enforcement should run deterministically around agent invocations and model responses. Strands plugins are the documented packaging mechanism for multiple lifecycle hooks, and `BeforeInvocationEvent` plus `AfterModelCallEvent` provide the correct extension points.
|
|
14
|
+
|
|
15
|
+
## Catalog file
|
|
16
|
+
|
|
17
|
+
Create `site/src/content/catalog/strands-deepkeep.yaml`:
|
|
18
|
+
|
|
19
|
+
```yaml
|
|
20
|
+
name: strands-deepkeep
|
|
21
|
+
description: DeepKeep AI Firewall guardrails for Strands Agents using lifecycle hooks around model input and output.
|
|
22
|
+
integrationType: plugin
|
|
23
|
+
languages:
|
|
24
|
+
python:
|
|
25
|
+
package: strands-deepkeep
|
|
26
|
+
github: https://github.com/Deepkeepai/strands-deepkeep
|
|
27
|
+
docsUrl: https://github.com/Deepkeepai/strands-deepkeep#readme
|
|
28
|
+
addedDate: 2026-08-26
|
|
29
|
+
maintainedBy: partner
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
DeepKeep is distributed as an external package, so Strands core does not need to maintain vendor-specific API code. The integration uses the documented Strands hook provider interface and can be listed as an approved plugin once accepted by maintainers.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "strands-deepkeep"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "DeepKeep AI Firewall hook integration for Strands Agents"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "DeepKeep", email = "info@deepkeep.ai"}
|
|
14
|
+
]
|
|
15
|
+
keywords = ["deepkeep", "strands", "hooks", "moderation", "guardrails"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Programming Language :: Python :: 3.13",
|
|
24
|
+
"Programming Language :: Python :: 3.14",
|
|
25
|
+
"Topic :: Security",
|
|
26
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
27
|
+
]
|
|
28
|
+
dependencies = [
|
|
29
|
+
"httpx>=0.27.0",
|
|
30
|
+
"strands-agents>=1.0.0",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.urls]
|
|
34
|
+
Documentation = "https://github.com/Deepkeepai/strands-deepkeep#readme"
|
|
35
|
+
Homepage = "https://deepkeep.ai"
|
|
36
|
+
|
|
37
|
+
[project.optional-dependencies]
|
|
38
|
+
dev = [
|
|
39
|
+
"pytest>=8.0",
|
|
40
|
+
"respx>=0.21",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
[tool.setuptools.packages.find]
|
|
44
|
+
include = ["strands_deepkeep*"]
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
from strands.plugins import Plugin
|
|
10
|
+
except ModuleNotFoundError: # pragma: no cover - used only when Strands is not installed in a test environment.
|
|
11
|
+
class Plugin: # type: ignore[no-redef]
|
|
12
|
+
def __init__(self) -> None:
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class DeepKeepAPIError(Exception):
|
|
17
|
+
"""Raised when the DeepKeep API returns an error or a network request fails."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, message: str, status_code: int | None = None) -> None:
|
|
20
|
+
super().__init__(message)
|
|
21
|
+
self.status_code = status_code
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class DeepKeepClient:
|
|
25
|
+
"""HTTP client for DeepKeep OpenAI-compatible moderation endpoints."""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
*,
|
|
30
|
+
api_key: str | None = None,
|
|
31
|
+
base_url: str | None = None,
|
|
32
|
+
timeout: float = 30.0,
|
|
33
|
+
) -> None:
|
|
34
|
+
self.api_key = api_key or os.environ.get("DEEPKEEP_API_KEY")
|
|
35
|
+
self.base_url = (base_url or os.environ.get("DEEPKEEP_BASE_URL") or "").rstrip("/")
|
|
36
|
+
self.timeout = timeout
|
|
37
|
+
|
|
38
|
+
if not self.api_key:
|
|
39
|
+
raise ValueError("DeepKeep API key is required. Pass api_key or set DEEPKEEP_API_KEY.")
|
|
40
|
+
if not self.base_url:
|
|
41
|
+
raise ValueError("DeepKeep base URL is required. Pass base_url or set DEEPKEEP_BASE_URL.")
|
|
42
|
+
|
|
43
|
+
def pre_moderate(
|
|
44
|
+
self,
|
|
45
|
+
*,
|
|
46
|
+
model: str,
|
|
47
|
+
input: str | list[str],
|
|
48
|
+
title: str = "",
|
|
49
|
+
chat: str = "",
|
|
50
|
+
) -> dict[str, Any]:
|
|
51
|
+
return self._post(
|
|
52
|
+
"/api/v3/openai/moderations/pre",
|
|
53
|
+
{"model": model, "input": input, "title": title, "chat": chat},
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
def post_moderate(
|
|
57
|
+
self,
|
|
58
|
+
*,
|
|
59
|
+
model: str,
|
|
60
|
+
output: str | list[str],
|
|
61
|
+
title: str = "",
|
|
62
|
+
chat: str = "",
|
|
63
|
+
) -> dict[str, Any]:
|
|
64
|
+
return self._post(
|
|
65
|
+
"/api/v3/openai/moderations/post",
|
|
66
|
+
{"model": model, "output": output, "title": title, "chat": chat},
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
def _post(self, path: str, payload: dict[str, Any]) -> dict[str, Any]:
|
|
70
|
+
try:
|
|
71
|
+
with httpx.Client(timeout=self.timeout) as client:
|
|
72
|
+
response = client.post(
|
|
73
|
+
f"{self.base_url}{path}",
|
|
74
|
+
headers={"X-API-Key": self.api_key},
|
|
75
|
+
json=payload,
|
|
76
|
+
)
|
|
77
|
+
except httpx.RequestError as exc:
|
|
78
|
+
raise DeepKeepAPIError(f"DeepKeep network error: {exc}") from exc
|
|
79
|
+
|
|
80
|
+
if response.status_code >= 400:
|
|
81
|
+
raise DeepKeepAPIError(
|
|
82
|
+
f"DeepKeep API error {response.status_code}: {response.text}",
|
|
83
|
+
status_code=response.status_code,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
return response.json()
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class DeepKeepGuardrail(Plugin):
|
|
90
|
+
"""Strands hook provider that applies DeepKeep pre- and post-moderation."""
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def name(self) -> str:
|
|
94
|
+
return "deepkeep-guardrail"
|
|
95
|
+
|
|
96
|
+
def __init__(
|
|
97
|
+
self,
|
|
98
|
+
*,
|
|
99
|
+
api_key: str | None = None,
|
|
100
|
+
base_url: str | None = None,
|
|
101
|
+
pre_model: str | None = None,
|
|
102
|
+
post_model: str | None = None,
|
|
103
|
+
title: str = "",
|
|
104
|
+
chat: str = "",
|
|
105
|
+
timeout: float = 30.0,
|
|
106
|
+
block_message: str = "DeepKeep blocked this content.",
|
|
107
|
+
) -> None:
|
|
108
|
+
if not pre_model and not post_model:
|
|
109
|
+
raise ValueError("At least one of pre_model or post_model is required.")
|
|
110
|
+
|
|
111
|
+
super().__init__()
|
|
112
|
+
|
|
113
|
+
self.client = DeepKeepClient(api_key=api_key, base_url=base_url, timeout=timeout)
|
|
114
|
+
self.pre_model = pre_model
|
|
115
|
+
self.post_model = post_model
|
|
116
|
+
self.title = title
|
|
117
|
+
self.chat = chat
|
|
118
|
+
self.block_message = block_message
|
|
119
|
+
|
|
120
|
+
def register_hooks(self, registry: Any) -> None:
|
|
121
|
+
from strands.hooks import AfterModelCallEvent, BeforeInvocationEvent
|
|
122
|
+
|
|
123
|
+
if self.pre_model:
|
|
124
|
+
registry.add_callback(BeforeInvocationEvent, self.before_invocation)
|
|
125
|
+
if self.post_model:
|
|
126
|
+
registry.add_callback(AfterModelCallEvent, self.after_model_call)
|
|
127
|
+
|
|
128
|
+
def init_agent(self, agent: Any) -> None:
|
|
129
|
+
from strands.hooks import AfterModelCallEvent, BeforeInvocationEvent
|
|
130
|
+
|
|
131
|
+
if self.pre_model:
|
|
132
|
+
agent.add_hook(self.before_invocation, BeforeInvocationEvent)
|
|
133
|
+
if self.post_model:
|
|
134
|
+
agent.add_hook(self.after_model_call, AfterModelCallEvent)
|
|
135
|
+
|
|
136
|
+
def before_invocation(self, event: Any) -> None:
|
|
137
|
+
messages = getattr(event, "messages", None)
|
|
138
|
+
content = _last_user_text(messages)
|
|
139
|
+
result = self.client.pre_moderate(
|
|
140
|
+
model=self.pre_model or "",
|
|
141
|
+
input=content,
|
|
142
|
+
title=self.title,
|
|
143
|
+
chat=self.chat,
|
|
144
|
+
)
|
|
145
|
+
_store_decision(event, "deepkeep_pre", result)
|
|
146
|
+
|
|
147
|
+
action = _highest_priority_action(result)
|
|
148
|
+
if action == "block":
|
|
149
|
+
event.cancel = self.block_message
|
|
150
|
+
elif action in {"redact", "modify"} and messages is not None:
|
|
151
|
+
event.messages = _replace_last_user_text(messages, _modified_content(result))
|
|
152
|
+
|
|
153
|
+
def after_model_call(self, event: Any) -> None:
|
|
154
|
+
stop_response = getattr(event, "stop_response", None)
|
|
155
|
+
message = getattr(stop_response, "message", None)
|
|
156
|
+
content = _message_text(message)
|
|
157
|
+
result = self.client.post_moderate(
|
|
158
|
+
model=self.post_model or "",
|
|
159
|
+
output=content,
|
|
160
|
+
title=self.title,
|
|
161
|
+
chat=self.chat,
|
|
162
|
+
)
|
|
163
|
+
_store_decision(event, "deepkeep_post", result)
|
|
164
|
+
|
|
165
|
+
action = _highest_priority_action(result)
|
|
166
|
+
if action == "block" and message is not None:
|
|
167
|
+
_replace_message_text(message, "")
|
|
168
|
+
_add_message_metadata(message, result)
|
|
169
|
+
elif action in {"redact", "modify"} and message is not None:
|
|
170
|
+
_replace_message_text(message, _modified_content(result))
|
|
171
|
+
_add_message_metadata(message, result)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _store_decision(event: Any, key: str, result: dict[str, Any]) -> None:
|
|
175
|
+
invocation_state = getattr(event, "invocation_state", None)
|
|
176
|
+
if isinstance(invocation_state, dict):
|
|
177
|
+
invocation_state[key] = result
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _last_user_text(messages: Any) -> str:
|
|
181
|
+
if not messages:
|
|
182
|
+
return ""
|
|
183
|
+
for message in reversed(messages):
|
|
184
|
+
if isinstance(message, dict) and message.get("role") == "user":
|
|
185
|
+
return _message_text(message)
|
|
186
|
+
return _message_text(messages[-1])
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _message_text(message: Any) -> str:
|
|
190
|
+
if not isinstance(message, dict):
|
|
191
|
+
return ""
|
|
192
|
+
parts: list[str] = []
|
|
193
|
+
for block in message.get("content", []):
|
|
194
|
+
if isinstance(block, dict) and isinstance(block.get("text"), str):
|
|
195
|
+
parts.append(block["text"])
|
|
196
|
+
return "\n".join(parts)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _replace_last_user_text(messages: list[dict[str, Any]], text: str) -> list[dict[str, Any]]:
|
|
200
|
+
updated = [dict(message) for message in messages]
|
|
201
|
+
for index in range(len(updated) - 1, -1, -1):
|
|
202
|
+
if updated[index].get("role") == "user":
|
|
203
|
+
_replace_message_text(updated[index], text)
|
|
204
|
+
return updated
|
|
205
|
+
if updated:
|
|
206
|
+
_replace_message_text(updated[-1], text)
|
|
207
|
+
return updated
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _replace_message_text(message: dict[str, Any], text: str) -> None:
|
|
211
|
+
content = message.get("content")
|
|
212
|
+
if not isinstance(content, list):
|
|
213
|
+
message["content"] = [{"text": text}]
|
|
214
|
+
return
|
|
215
|
+
|
|
216
|
+
for block in content:
|
|
217
|
+
if isinstance(block, dict) and "text" in block:
|
|
218
|
+
block["text"] = text
|
|
219
|
+
return
|
|
220
|
+
content.insert(0, {"text": text})
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _add_message_metadata(message: dict[str, Any], result: dict[str, Any]) -> None:
|
|
224
|
+
metadata = message.setdefault("metadata", {})
|
|
225
|
+
custom = metadata.setdefault("custom", {})
|
|
226
|
+
custom["deepkeep"] = result
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _highest_priority_action(result: dict[str, Any]) -> str | None:
|
|
230
|
+
priority = {"block": 3, "redact": 2, "modify": 2, "alert": 1}
|
|
231
|
+
selected: str | None = None
|
|
232
|
+
for item in result.get("verbosity", []):
|
|
233
|
+
action = item.get("details", {}).get("guardrail_action")
|
|
234
|
+
if action in priority and (selected is None or priority[action] > priority[selected]):
|
|
235
|
+
selected = action
|
|
236
|
+
return selected
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _modified_content(result: dict[str, Any]) -> str:
|
|
240
|
+
modified_content: str | None = None
|
|
241
|
+
for item in result.get("verbosity", []):
|
|
242
|
+
for modified in item.get("details", {}).get("modified", []):
|
|
243
|
+
content = modified.get("content")
|
|
244
|
+
if isinstance(content, str):
|
|
245
|
+
modified_content = content
|
|
246
|
+
if modified_content is not None:
|
|
247
|
+
return modified_content
|
|
248
|
+
inputs = result.get("inputs", [])
|
|
249
|
+
if inputs and isinstance(inputs[0], dict) and isinstance(inputs[0].get("content"), str):
|
|
250
|
+
return inputs[0]["content"]
|
|
251
|
+
return ""
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: strands-deepkeep
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: DeepKeep AI Firewall hook integration for Strands Agents
|
|
5
|
+
Author-email: DeepKeep <info@deepkeep.ai>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Documentation, https://github.com/Deepkeepai/strands-deepkeep#readme
|
|
8
|
+
Project-URL: Homepage, https://deepkeep.ai
|
|
9
|
+
Keywords: deepkeep,strands,hooks,moderation,guardrails
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Classifier: Topic :: Security
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: httpx>=0.27.0
|
|
24
|
+
Requires-Dist: strands-agents>=1.0.0
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
27
|
+
Requires-Dist: respx>=0.21; extra == "dev"
|
|
28
|
+
Dynamic: license-file
|
|
29
|
+
|
|
30
|
+
# strands-deepkeep
|
|
31
|
+
|
|
32
|
+
Strands Agents hook provider for the [DeepKeep AI Firewall](https://deepkeep.ai/capabilities/ai-firewall).
|
|
33
|
+
|
|
34
|
+
DeepKeep runs before a Strands invocation reaches the model and after the model returns. It can block unsafe content, pass safe content through, and apply redacted or modified text returned by DeepKeep.
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install strands-deepkeep
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Quick start
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
import os
|
|
46
|
+
|
|
47
|
+
from strands import Agent
|
|
48
|
+
from strands_deepkeep import DeepKeepGuardrail
|
|
49
|
+
|
|
50
|
+
agent = Agent(
|
|
51
|
+
plugins=[
|
|
52
|
+
DeepKeepGuardrail(
|
|
53
|
+
api_key=os.environ["DEEPKEEP_API_KEY"],
|
|
54
|
+
base_url=os.environ["DEEPKEEP_BASE_URL"],
|
|
55
|
+
pre_model="input-firewall-id",
|
|
56
|
+
post_model="output-firewall-id",
|
|
57
|
+
)
|
|
58
|
+
]
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
result = agent("Summarize this customer support request")
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`pre_model` and `post_model` are DeepKeep firewall IDs. Each value is sent to the matching moderation endpoint as the `model` field.
|
|
65
|
+
|
|
66
|
+
## Behavior
|
|
67
|
+
|
|
68
|
+
- `allow` and `alert` decisions let the invocation continue.
|
|
69
|
+
- `block` decisions cancel the invocation before the model or clear blocked model output after the model call.
|
|
70
|
+
- `redact` and `modify` decisions replace the latest user text before the invocation or replace generated assistant text after the model call.
|
|
71
|
+
- DeepKeep API decisions are stored in `invocation_state` as `deepkeep_pre` and `deepkeep_post` for audit and logging.
|
|
72
|
+
|
|
73
|
+
## Environment
|
|
74
|
+
|
|
75
|
+
You can pass credentials directly or set:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
export DEEPKEEP_API_KEY="dk-..."
|
|
79
|
+
export DEEPKEEP_BASE_URL="https://your-deepkeep-instance.example"
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Strands catalog entry
|
|
83
|
+
|
|
84
|
+
Submit the integration to Strands by adding `site/src/content/catalog/strands-deepkeep.yaml` to `strands-agents/harness-sdk`:
|
|
85
|
+
|
|
86
|
+
```yaml
|
|
87
|
+
name: strands-deepkeep
|
|
88
|
+
description: DeepKeep AI Firewall guardrails for Strands Agents using lifecycle hooks around model input and output.
|
|
89
|
+
integrationType: plugin
|
|
90
|
+
languages:
|
|
91
|
+
python:
|
|
92
|
+
package: strands-deepkeep
|
|
93
|
+
github: https://github.com/Deepkeepai/strands-deepkeep
|
|
94
|
+
docsUrl: https://github.com/Deepkeepai/strands-deepkeep#readme
|
|
95
|
+
addedDate: 2026-08-26
|
|
96
|
+
maintainedBy: partner
|
|
97
|
+
```
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
MANIFEST.in
|
|
3
|
+
README.md
|
|
4
|
+
UPSTREAM_SUBMISSION.md
|
|
5
|
+
pyproject.toml
|
|
6
|
+
strands_deepkeep/__init__.py
|
|
7
|
+
strands_deepkeep/guardrail.py
|
|
8
|
+
strands_deepkeep.egg-info/PKG-INFO
|
|
9
|
+
strands_deepkeep.egg-info/SOURCES.txt
|
|
10
|
+
strands_deepkeep.egg-info/dependency_links.txt
|
|
11
|
+
strands_deepkeep.egg-info/requires.txt
|
|
12
|
+
strands_deepkeep.egg-info/top_level.txt
|
|
13
|
+
tests/test_client.py
|
|
14
|
+
tests/test_guardrail.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
strands_deepkeep
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
import respx
|
|
3
|
+
from httpx import Response
|
|
4
|
+
|
|
5
|
+
from strands_deepkeep import DeepKeepAPIError, DeepKeepClient
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@respx.mock
|
|
9
|
+
def test_pre_moderate_posts_to_pre_endpoint():
|
|
10
|
+
route = respx.post("https://deepkeep.example/api/v3/openai/moderations/pre").mock(
|
|
11
|
+
return_value=Response(200, json={"flagged": False, "verbosity": []})
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
client = DeepKeepClient(api_key="dk-test", base_url="https://deepkeep.example")
|
|
15
|
+
result = client.pre_moderate(model="fw-input", input="hello")
|
|
16
|
+
|
|
17
|
+
assert result["flagged"] is False
|
|
18
|
+
assert route.calls.last.request.headers["X-API-Key"] == "dk-test"
|
|
19
|
+
assert route.calls.last.request.content == b'{"model":"fw-input","input":"hello","title":"","chat":""}'
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@respx.mock
|
|
23
|
+
def test_post_moderate_posts_to_post_endpoint():
|
|
24
|
+
route = respx.post("https://deepkeep.example/api/v3/openai/moderations/post").mock(
|
|
25
|
+
return_value=Response(200, json={"flagged": False, "verbosity": []})
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
client = DeepKeepClient(api_key="dk-test", base_url="https://deepkeep.example/")
|
|
29
|
+
result = client.post_moderate(model="fw-output", output="hello")
|
|
30
|
+
|
|
31
|
+
assert result["flagged"] is False
|
|
32
|
+
assert route.calls.last.request.content == b'{"model":"fw-output","output":"hello","title":"","chat":""}'
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@respx.mock
|
|
36
|
+
def test_api_error_keeps_status_code():
|
|
37
|
+
respx.post("https://deepkeep.example/api/v3/openai/moderations/pre").mock(
|
|
38
|
+
return_value=Response(404, json={"detail": "No firewall with the provided identifier."})
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
client = DeepKeepClient(api_key="dk-test", base_url="https://deepkeep.example")
|
|
42
|
+
with pytest.raises(DeepKeepAPIError) as exc_info:
|
|
43
|
+
client.pre_moderate(model="missing", input="hello")
|
|
44
|
+
|
|
45
|
+
assert exc_info.value.status_code == 404
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from strands_deepkeep.guardrail import (
|
|
7
|
+
DeepKeepGuardrail,
|
|
8
|
+
_highest_priority_action,
|
|
9
|
+
_modified_content,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class FakeClient:
|
|
14
|
+
def __init__(self, result: dict[str, Any]) -> None:
|
|
15
|
+
self.result = result
|
|
16
|
+
self.calls: list[dict[str, Any]] = []
|
|
17
|
+
|
|
18
|
+
def pre_moderate(self, **kwargs: Any) -> dict[str, Any]:
|
|
19
|
+
self.calls.append(kwargs)
|
|
20
|
+
return self.result
|
|
21
|
+
|
|
22
|
+
def post_moderate(self, **kwargs: Any) -> dict[str, Any]:
|
|
23
|
+
self.calls.append(kwargs)
|
|
24
|
+
return self.result
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class FakeBeforeInvocationEvent:
|
|
29
|
+
messages: list[dict[str, Any]]
|
|
30
|
+
invocation_state: dict[str, Any] = field(default_factory=dict)
|
|
31
|
+
cancel: bool | str = False
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class FakeStopResponse:
|
|
36
|
+
message: dict[str, Any]
|
|
37
|
+
stop_reason: str = "end_turn"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class FakeAfterModelCallEvent:
|
|
42
|
+
stop_response: FakeStopResponse
|
|
43
|
+
invocation_state: dict[str, Any] = field(default_factory=dict)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_highest_priority_action_prefers_block():
|
|
47
|
+
result = {
|
|
48
|
+
"verbosity": [
|
|
49
|
+
{"details": {"guardrail_action": "alert"}},
|
|
50
|
+
{"details": {"guardrail_action": "modify"}},
|
|
51
|
+
{"details": {"guardrail_action": "block"}},
|
|
52
|
+
]
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
assert _highest_priority_action(result) == "block"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_modified_content_uses_latest_modified_item():
|
|
59
|
+
result = {
|
|
60
|
+
"verbosity": [
|
|
61
|
+
{"details": {"modified": [{"content": "first"}]}},
|
|
62
|
+
{"details": {"modified": [{"content": "second"}]}},
|
|
63
|
+
]
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
assert _modified_content(result) == "second"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_guardrail_requires_at_least_one_model():
|
|
70
|
+
with pytest.raises(ValueError):
|
|
71
|
+
DeepKeepGuardrail(api_key="dk-test", base_url="https://deepkeep.example")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_before_invocation_replaces_redacted_user_text():
|
|
75
|
+
guardrail = DeepKeepGuardrail(api_key="dk-test", base_url="https://deepkeep.example", pre_model="fw-input")
|
|
76
|
+
result = {"verbosity": [{"details": {"guardrail_action": "redact", "modified": [{"content": "safe"}]}}]}
|
|
77
|
+
guardrail.client = FakeClient(result)
|
|
78
|
+
event = FakeBeforeInvocationEvent(messages=[{"role": "user", "content": [{"text": "secret"}]}])
|
|
79
|
+
|
|
80
|
+
guardrail.before_invocation(event)
|
|
81
|
+
|
|
82
|
+
assert event.messages[0]["content"][0]["text"] == "safe"
|
|
83
|
+
assert event.cancel is False
|
|
84
|
+
assert event.invocation_state["deepkeep_pre"] == result
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_before_invocation_cancels_on_block():
|
|
88
|
+
guardrail = DeepKeepGuardrail(api_key="dk-test", base_url="https://deepkeep.example", pre_model="fw-input")
|
|
89
|
+
result = {"verbosity": [{"details": {"guardrail_action": "block"}}]}
|
|
90
|
+
guardrail.client = FakeClient(result)
|
|
91
|
+
event = FakeBeforeInvocationEvent(messages=[{"role": "user", "content": [{"text": "bad"}]}])
|
|
92
|
+
|
|
93
|
+
guardrail.before_invocation(event)
|
|
94
|
+
|
|
95
|
+
assert event.cancel == "DeepKeep blocked this content."
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_after_model_call_replaces_redacted_output():
|
|
99
|
+
guardrail = DeepKeepGuardrail(api_key="dk-test", base_url="https://deepkeep.example", post_model="fw-output")
|
|
100
|
+
result = {"verbosity": [{"details": {"guardrail_action": "modify", "modified": [{"content": "safe output"}]}}]}
|
|
101
|
+
guardrail.client = FakeClient(result)
|
|
102
|
+
message = {"role": "assistant", "content": [{"text": "raw output"}]}
|
|
103
|
+
event = FakeAfterModelCallEvent(stop_response=FakeStopResponse(message=message))
|
|
104
|
+
|
|
105
|
+
guardrail.after_model_call(event)
|
|
106
|
+
|
|
107
|
+
assert message["content"][0]["text"] == "safe output"
|
|
108
|
+
assert message["metadata"]["custom"]["deepkeep"] == result
|
|
109
|
+
assert event.invocation_state["deepkeep_post"] == result
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_after_model_call_clears_blocked_output():
|
|
113
|
+
guardrail = DeepKeepGuardrail(api_key="dk-test", base_url="https://deepkeep.example", post_model="fw-output")
|
|
114
|
+
result = {"verbosity": [{"details": {"guardrail_action": "block"}}]}
|
|
115
|
+
guardrail.client = FakeClient(result)
|
|
116
|
+
message = {"role": "assistant", "content": [{"text": "unsafe output"}]}
|
|
117
|
+
event = FakeAfterModelCallEvent(stop_response=FakeStopResponse(message=message))
|
|
118
|
+
|
|
119
|
+
guardrail.after_model_call(event)
|
|
120
|
+
|
|
121
|
+
assert message["content"][0]["text"] == ""
|
|
122
|
+
assert message["metadata"]["custom"]["deepkeep"] == result
|