strands-deepkeep 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,13 @@
1
+ """DeepKeep AI Firewall hooks for Strands Agents."""
2
+
3
+ from strands_deepkeep.guardrail import (
4
+ DeepKeepAPIError,
5
+ DeepKeepClient,
6
+ DeepKeepGuardrail,
7
+ )
8
+
9
+ __all__ = [
10
+ "DeepKeepAPIError",
11
+ "DeepKeepClient",
12
+ "DeepKeepGuardrail",
13
+ ]
@@ -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,7 @@
1
+ strands_deepkeep/__init__.py,sha256=gSoqQLftHu8aiBnyhY91Io47EtRbyVlhjpzr9BEUvhM,248
2
+ strands_deepkeep/guardrail.py,sha256=C4WQpv3tsjRw_PMNrIjdHuPGa6DuQpvQdlu90-VPoyE,8703
3
+ strands_deepkeep-0.1.0.dist-info/licenses/LICENSE,sha256=gvDg5qsSNm4LlpTtK4v1K-DbrSfq4ZXGCyLKsjKxros,1065
4
+ strands_deepkeep-0.1.0.dist-info/METADATA,sha256=WS8JzfEveIklBIFYl_yoI9r1dpwIcZ--ZPxP50pFJCQ,3372
5
+ strands_deepkeep-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
6
+ strands_deepkeep-0.1.0.dist-info/top_level.txt,sha256=EDlhIPQW1bASQELWp0urde-JDwXCKTuA8BFk8ywjMvg,17
7
+ strands_deepkeep-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -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 @@
1
+ strands_deepkeep