deepintshield 1.0.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.
- deepintshield/__init__.py +54 -0
- deepintshield/agent.py +148 -0
- deepintshield/client.py +291 -0
- deepintshield/config.py +34 -0
- deepintshield/errors.py +38 -0
- deepintshield/providers/__init__.py +48 -0
- deepintshield/providers/anthropic.py +22 -0
- deepintshield/providers/bedrock.py +33 -0
- deepintshield/providers/genai.py +22 -0
- deepintshield/providers/langchain.py +22 -0
- deepintshield/providers/langgraph.py +119 -0
- deepintshield/providers/litellm.py +34 -0
- deepintshield/providers/openai.py +22 -0
- deepintshield/providers/pydanticai.py +39 -0
- deepintshield/rag.py +76 -0
- deepintshield/types.py +81 -0
- deepintshield/version.py +1 -0
- deepintshield-1.0.0.dist-info/METADATA +236 -0
- deepintshield-1.0.0.dist-info/RECORD +22 -0
- deepintshield-1.0.0.dist-info/WHEEL +5 -0
- deepintshield-1.0.0.dist-info/licenses/LICENSE +190 -0
- deepintshield-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DeepintShield — unified Python SDK.
|
|
3
|
+
|
|
4
|
+
Quick start
|
|
5
|
+
-----------
|
|
6
|
+
|
|
7
|
+
from deepintshield import DeepintShield
|
|
8
|
+
|
|
9
|
+
shield = DeepintShield(virtual_key="sk-...")
|
|
10
|
+
openai_client = shield.openai()
|
|
11
|
+
response = openai_client.chat.completions.create(
|
|
12
|
+
model="gpt-4o-mini",
|
|
13
|
+
messages=[{"role": "user", "content": "hello"}],
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
The gateway defaults to ``https://app.deepintshield.com`` and is overridable via
|
|
17
|
+
``base_url=...`` or the ``DEEPINTSHIELD_BASE_URL`` env var.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from .client import DeepintShield
|
|
21
|
+
from .config import DEFAULT_BASE_URL, ShieldConfig
|
|
22
|
+
from .errors import DeepintShieldBlockedError, DeepintShieldError
|
|
23
|
+
from .rag import allowed_chunk_ids, build_chunk, filter_chunks
|
|
24
|
+
from .types import (
|
|
25
|
+
NON_BLOCKING_DECISIONS,
|
|
26
|
+
GuardrailDecision,
|
|
27
|
+
GuardrailResult,
|
|
28
|
+
GuardrailStage,
|
|
29
|
+
RetrievedChunk,
|
|
30
|
+
ToolInvocation,
|
|
31
|
+
)
|
|
32
|
+
from .version import __version__
|
|
33
|
+
|
|
34
|
+
# Backwards-compatible alias.
|
|
35
|
+
DeepintShieldClient = DeepintShield
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
"__version__",
|
|
39
|
+
"DEFAULT_BASE_URL",
|
|
40
|
+
"DeepintShield",
|
|
41
|
+
"DeepintShieldClient",
|
|
42
|
+
"DeepintShieldBlockedError",
|
|
43
|
+
"DeepintShieldError",
|
|
44
|
+
"GuardrailDecision",
|
|
45
|
+
"GuardrailResult",
|
|
46
|
+
"GuardrailStage",
|
|
47
|
+
"NON_BLOCKING_DECISIONS",
|
|
48
|
+
"RetrievedChunk",
|
|
49
|
+
"ShieldConfig",
|
|
50
|
+
"ToolInvocation",
|
|
51
|
+
"allowed_chunk_ids",
|
|
52
|
+
"build_chunk",
|
|
53
|
+
"filter_chunks",
|
|
54
|
+
]
|
deepintshield/agent.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import functools
|
|
4
|
+
import json
|
|
5
|
+
from typing import TYPE_CHECKING, Any, Callable, Mapping
|
|
6
|
+
|
|
7
|
+
from .errors import DeepintShieldBlockedError
|
|
8
|
+
from .types import GuardrailResult, ToolInvocation
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from .client import DeepintShield
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AgentSurface:
|
|
15
|
+
"""
|
|
16
|
+
Agentic guardrails: input/output scanning plus tool/MCP evaluation.
|
|
17
|
+
|
|
18
|
+
>>> shield = DeepintShield.from_env()
|
|
19
|
+
>>>
|
|
20
|
+
>>> @shield.agent.tool
|
|
21
|
+
>>> def read_file(path: str) -> str: ...
|
|
22
|
+
>>>
|
|
23
|
+
>>> shield.agent.check_input("user message")
|
|
24
|
+
>>> shield.agent.check_output("assistant reply")
|
|
25
|
+
>>> shield.agent.evaluate_tool(name="read_file", args={"path": "/tmp"})
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, client: "DeepintShield") -> None:
|
|
29
|
+
self._client = client
|
|
30
|
+
|
|
31
|
+
# ── stage helpers ────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
def check_input(self, text: str, **kwargs: Any) -> GuardrailResult:
|
|
34
|
+
return self._client.guard(stage="input", input=text, **kwargs)
|
|
35
|
+
|
|
36
|
+
def check_output(self, text: str, **kwargs: Any) -> GuardrailResult:
|
|
37
|
+
return self._client.guard(stage="output", output=text, **kwargs)
|
|
38
|
+
|
|
39
|
+
# ── tool invocation ─────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
def evaluate_tool(
|
|
42
|
+
self,
|
|
43
|
+
invocation: ToolInvocation | Mapping[str, Any] | None = None,
|
|
44
|
+
*,
|
|
45
|
+
name: str | None = None,
|
|
46
|
+
args: Any = None,
|
|
47
|
+
server_label: str = "",
|
|
48
|
+
action_class: str = "read",
|
|
49
|
+
domains: list[str] | None = None,
|
|
50
|
+
metadata: Mapping[str, Any] | None = None,
|
|
51
|
+
actor_type: str = "agent",
|
|
52
|
+
raise_on_block: bool = True,
|
|
53
|
+
**kwargs: Any,
|
|
54
|
+
) -> GuardrailResult:
|
|
55
|
+
if invocation is None:
|
|
56
|
+
if name is None:
|
|
57
|
+
raise ValueError("evaluate_tool requires either invocation or name=...")
|
|
58
|
+
invocation = ToolInvocation(
|
|
59
|
+
tool_name=name,
|
|
60
|
+
tool_input=args if args is not None else {},
|
|
61
|
+
server_label=server_label,
|
|
62
|
+
action_class=action_class,
|
|
63
|
+
domains=list(domains or []),
|
|
64
|
+
metadata=dict(metadata or {}),
|
|
65
|
+
)
|
|
66
|
+
tool = invocation if isinstance(invocation, ToolInvocation) else ToolInvocation(**dict(invocation))
|
|
67
|
+
tool_input = (
|
|
68
|
+
tool.tool_input
|
|
69
|
+
if isinstance(tool.tool_input, str)
|
|
70
|
+
else json.dumps(tool.tool_input, default=str, sort_keys=True)
|
|
71
|
+
)
|
|
72
|
+
return self._client.guard(
|
|
73
|
+
stage="mcp" if tool.server_label else "action",
|
|
74
|
+
actor_type=actor_type,
|
|
75
|
+
tool_input=tool_input,
|
|
76
|
+
server_label=tool.server_label or None,
|
|
77
|
+
tool_name=tool.tool_name,
|
|
78
|
+
action_class=tool.action_class,
|
|
79
|
+
domains=tool.domains,
|
|
80
|
+
metadata=tool.metadata,
|
|
81
|
+
raise_on_block=raise_on_block,
|
|
82
|
+
**kwargs,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# ── decorator ───────────────────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
def tool(
|
|
88
|
+
self,
|
|
89
|
+
func: Callable | None = None,
|
|
90
|
+
*,
|
|
91
|
+
action_class: str = "read",
|
|
92
|
+
server_label: str = "",
|
|
93
|
+
name: str | None = None,
|
|
94
|
+
) -> Callable:
|
|
95
|
+
"""
|
|
96
|
+
Decorator that guards a function call through DeepintShield before execution.
|
|
97
|
+
|
|
98
|
+
>>> @shield.agent.tool(action_class="write")
|
|
99
|
+
... def write_file(path: str, content: str) -> None: ...
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
def decorate(fn: Callable) -> Callable:
|
|
103
|
+
tool_name = name or fn.__name__
|
|
104
|
+
|
|
105
|
+
@functools.wraps(fn)
|
|
106
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
107
|
+
self.evaluate_tool(
|
|
108
|
+
name=tool_name,
|
|
109
|
+
args={"args": list(args), "kwargs": dict(kwargs)},
|
|
110
|
+
action_class=action_class,
|
|
111
|
+
server_label=server_label,
|
|
112
|
+
)
|
|
113
|
+
return fn(*args, **kwargs)
|
|
114
|
+
|
|
115
|
+
return wrapper
|
|
116
|
+
|
|
117
|
+
if func is not None and callable(func):
|
|
118
|
+
return decorate(func)
|
|
119
|
+
return decorate
|
|
120
|
+
|
|
121
|
+
# ── full loop helper ────────────────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
def guard_turn(
|
|
124
|
+
self,
|
|
125
|
+
*,
|
|
126
|
+
user_input: str,
|
|
127
|
+
model_output: str | None = None,
|
|
128
|
+
tool_calls: list[Mapping[str, Any]] | None = None,
|
|
129
|
+
raise_on_block: bool = True,
|
|
130
|
+
) -> dict[str, GuardrailResult]:
|
|
131
|
+
"""
|
|
132
|
+
Evaluate a single agent turn: input, tool calls (if any), output (if any).
|
|
133
|
+
Returns a dict of stage -> result.
|
|
134
|
+
"""
|
|
135
|
+
results: dict[str, GuardrailResult] = {}
|
|
136
|
+
results["input"] = self.check_input(user_input, **{"raise_on_block": raise_on_block}) # type: ignore[arg-type]
|
|
137
|
+
for call in tool_calls or []:
|
|
138
|
+
key = f"tool:{call.get('name') or call.get('tool_name')}"
|
|
139
|
+
results[key] = self.evaluate_tool(
|
|
140
|
+
name=call.get("name") or call.get("tool_name", "unknown"),
|
|
141
|
+
args=call.get("args") or call.get("tool_input"),
|
|
142
|
+
action_class=call.get("action_class", "read"),
|
|
143
|
+
server_label=call.get("server_label", ""),
|
|
144
|
+
raise_on_block=raise_on_block,
|
|
145
|
+
)
|
|
146
|
+
if model_output is not None:
|
|
147
|
+
results["output"] = self.check_output(model_output, raise_on_block=raise_on_block)
|
|
148
|
+
return results
|
deepintshield/client.py
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from typing import Any, Iterable, Mapping
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from .config import DEFAULT_BASE_URL, ShieldConfig
|
|
10
|
+
from .errors import DeepintShieldBlockedError, DeepintShieldError
|
|
11
|
+
from .types import GuardrailResult, RetrievedChunk, ToolInvocation
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class DeepintShield:
|
|
15
|
+
"""
|
|
16
|
+
Unified DeepintShield client.
|
|
17
|
+
|
|
18
|
+
>>> shield = DeepintShield(virtual_key="sk-...")
|
|
19
|
+
>>> openai = shield.openai() # native openai.OpenAI pointed at the gateway
|
|
20
|
+
>>> resp = shield.chat(model="gpt-4o-mini", messages=[...])
|
|
21
|
+
>>> shield.rag.filter(query="...", chunks=[...])
|
|
22
|
+
>>> shield.agent.evaluate_tool(name="read_file", args={"path": "/tmp"})
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
virtual_key: str | None = None,
|
|
28
|
+
*,
|
|
29
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
30
|
+
timeout: float = 30.0,
|
|
31
|
+
default_headers: Mapping[str, str] | None = None,
|
|
32
|
+
app_name: str = "deepintshield",
|
|
33
|
+
agent_name: str = "deepintshield-agent",
|
|
34
|
+
requester: str = "sdk-user",
|
|
35
|
+
requester_role: str = "member",
|
|
36
|
+
persist: bool = True,
|
|
37
|
+
) -> None:
|
|
38
|
+
self.base_url = (base_url or DEFAULT_BASE_URL).rstrip("/")
|
|
39
|
+
self.virtual_key = (virtual_key or "").strip() or None
|
|
40
|
+
self.timeout = timeout
|
|
41
|
+
self.default_headers = dict(default_headers or {})
|
|
42
|
+
self.app_name = app_name
|
|
43
|
+
self.agent_name = agent_name
|
|
44
|
+
self.requester = requester
|
|
45
|
+
self.requester_role = requester_role
|
|
46
|
+
self.persist = persist
|
|
47
|
+
self._client = httpx.Client(timeout=timeout)
|
|
48
|
+
|
|
49
|
+
from .rag import RAGSurface
|
|
50
|
+
from .agent import AgentSurface
|
|
51
|
+
from .providers import ProviderRegistry
|
|
52
|
+
|
|
53
|
+
self.rag = RAGSurface(self)
|
|
54
|
+
self.agent = AgentSurface(self)
|
|
55
|
+
self.providers = ProviderRegistry(self)
|
|
56
|
+
|
|
57
|
+
# ─────────────────────────── constructors / context ──────────────────────
|
|
58
|
+
|
|
59
|
+
@classmethod
|
|
60
|
+
def from_env(cls) -> "DeepintShield":
|
|
61
|
+
cfg = ShieldConfig.from_env()
|
|
62
|
+
return cls(
|
|
63
|
+
virtual_key=cfg.virtual_key,
|
|
64
|
+
base_url=cfg.base_url,
|
|
65
|
+
timeout=cfg.timeout,
|
|
66
|
+
app_name=cfg.app_name,
|
|
67
|
+
agent_name=cfg.agent_name,
|
|
68
|
+
requester=cfg.requester,
|
|
69
|
+
requester_role=cfg.requester_role,
|
|
70
|
+
persist=cfg.persist,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
@classmethod
|
|
74
|
+
def from_config(cls, config: ShieldConfig) -> "DeepintShield":
|
|
75
|
+
return cls(
|
|
76
|
+
virtual_key=config.virtual_key,
|
|
77
|
+
base_url=config.base_url,
|
|
78
|
+
timeout=config.timeout,
|
|
79
|
+
default_headers=config.default_headers,
|
|
80
|
+
app_name=config.app_name,
|
|
81
|
+
agent_name=config.agent_name,
|
|
82
|
+
requester=config.requester,
|
|
83
|
+
requester_role=config.requester_role,
|
|
84
|
+
persist=config.persist,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
def __enter__(self) -> "DeepintShield":
|
|
88
|
+
return self
|
|
89
|
+
|
|
90
|
+
def __exit__(self, *_args: object) -> None:
|
|
91
|
+
self.close()
|
|
92
|
+
|
|
93
|
+
def close(self) -> None:
|
|
94
|
+
self._client.close()
|
|
95
|
+
|
|
96
|
+
# ─────────────────────────── keys and headers ────────────────────────────
|
|
97
|
+
|
|
98
|
+
def virtual_key_or_raise(self) -> str:
|
|
99
|
+
if not self.virtual_key:
|
|
100
|
+
raise DeepintShieldError("DEEPINTSHIELD_VIRTUAL_KEY is required")
|
|
101
|
+
return self.virtual_key
|
|
102
|
+
|
|
103
|
+
def api_key(self) -> str:
|
|
104
|
+
return self.virtual_key_or_raise()
|
|
105
|
+
|
|
106
|
+
def headers(self, extra: Mapping[str, str] | None = None) -> dict[str, str]:
|
|
107
|
+
out = {"content-type": "application/json", **self.default_headers}
|
|
108
|
+
if self.virtual_key:
|
|
109
|
+
out["x-bf-vk"] = self.virtual_key
|
|
110
|
+
if extra:
|
|
111
|
+
out.update(dict(extra))
|
|
112
|
+
return out
|
|
113
|
+
|
|
114
|
+
# ─────────────────────────── provider endpoints ──────────────────────────
|
|
115
|
+
|
|
116
|
+
def endpoint(self, provider: str) -> str:
|
|
117
|
+
return f"{self.base_url}/{provider.strip('/')}"
|
|
118
|
+
|
|
119
|
+
def openai_base_url(self) -> str:
|
|
120
|
+
return self.endpoint("openai")
|
|
121
|
+
|
|
122
|
+
def anthropic_base_url(self) -> str:
|
|
123
|
+
return self.endpoint("anthropic")
|
|
124
|
+
|
|
125
|
+
def bedrock_endpoint_url(self) -> str:
|
|
126
|
+
return self.endpoint("bedrock")
|
|
127
|
+
|
|
128
|
+
def genai_base_url(self) -> str:
|
|
129
|
+
return self.endpoint("genai")
|
|
130
|
+
|
|
131
|
+
def langchain_base_url(self) -> str:
|
|
132
|
+
return self.endpoint("langchain")
|
|
133
|
+
|
|
134
|
+
def litellm_base_url(self) -> str:
|
|
135
|
+
return self.endpoint("litellm")
|
|
136
|
+
|
|
137
|
+
def pydanticai_base_url(self) -> str:
|
|
138
|
+
return self.endpoint("pydanticai")
|
|
139
|
+
|
|
140
|
+
def openai_passthrough_base_url(self) -> str:
|
|
141
|
+
return f"{self.base_url}/openai_passthrough/v1"
|
|
142
|
+
|
|
143
|
+
def anthropic_passthrough_base_url(self) -> str:
|
|
144
|
+
return f"{self.base_url}/anthropic_passthrough"
|
|
145
|
+
|
|
146
|
+
def genai_passthrough_base_url(self) -> str:
|
|
147
|
+
return f"{self.base_url}/genai_passthrough"
|
|
148
|
+
|
|
149
|
+
# ─────────────────────────── provider shortcuts ──────────────────────────
|
|
150
|
+
|
|
151
|
+
def openai(self, *, passthrough: bool = False, **kwargs: Any):
|
|
152
|
+
from .providers.openai import build_client
|
|
153
|
+
return build_client(self, passthrough=passthrough, **kwargs)
|
|
154
|
+
|
|
155
|
+
def anthropic(self, *, passthrough: bool = False, **kwargs: Any):
|
|
156
|
+
from .providers.anthropic import build_client
|
|
157
|
+
return build_client(self, passthrough=passthrough, **kwargs)
|
|
158
|
+
|
|
159
|
+
def bedrock(self, **kwargs: Any):
|
|
160
|
+
from .providers.bedrock import build_client
|
|
161
|
+
return build_client(self, **kwargs)
|
|
162
|
+
|
|
163
|
+
def genai(self, *, passthrough: bool = False, **kwargs: Any):
|
|
164
|
+
from .providers.genai import build_client
|
|
165
|
+
return build_client(self, passthrough=passthrough, **kwargs)
|
|
166
|
+
|
|
167
|
+
def langchain(self, model: str = "gpt-4o-mini", **kwargs: Any):
|
|
168
|
+
from .providers.langchain import build_client
|
|
169
|
+
return build_client(self, model=model, **kwargs)
|
|
170
|
+
|
|
171
|
+
def langgraph(self):
|
|
172
|
+
from .providers.langgraph import LangGraphShield
|
|
173
|
+
return LangGraphShield(self)
|
|
174
|
+
|
|
175
|
+
def litellm(self):
|
|
176
|
+
from .providers.litellm import LiteLLMShield
|
|
177
|
+
return LiteLLMShield(self)
|
|
178
|
+
|
|
179
|
+
def pydanticai(self, model: str = "gpt-4o-mini", **kwargs: Any):
|
|
180
|
+
from .providers.pydanticai import build_agent
|
|
181
|
+
return build_agent(self, model=model, **kwargs)
|
|
182
|
+
|
|
183
|
+
# ─────────────────────────── HTTP + guardrails ───────────────────────────
|
|
184
|
+
|
|
185
|
+
def request(
|
|
186
|
+
self,
|
|
187
|
+
method: str,
|
|
188
|
+
path: str,
|
|
189
|
+
*,
|
|
190
|
+
json_body: Mapping[str, Any] | None = None,
|
|
191
|
+
extra_headers: Mapping[str, str] | None = None,
|
|
192
|
+
) -> dict[str, Any]:
|
|
193
|
+
response = self._client.request(
|
|
194
|
+
method=method,
|
|
195
|
+
url=f"{self.base_url}{path}",
|
|
196
|
+
headers=self.headers(extra_headers),
|
|
197
|
+
json=json_body,
|
|
198
|
+
)
|
|
199
|
+
try:
|
|
200
|
+
payload = response.json()
|
|
201
|
+
except ValueError:
|
|
202
|
+
payload = {"raw": response.text}
|
|
203
|
+
if response.status_code >= 400:
|
|
204
|
+
raise DeepintShieldError.from_response(response.status_code, payload)
|
|
205
|
+
return payload
|
|
206
|
+
|
|
207
|
+
def chat(
|
|
208
|
+
self,
|
|
209
|
+
*,
|
|
210
|
+
model: str,
|
|
211
|
+
messages: list[dict[str, Any]],
|
|
212
|
+
stream: bool = False,
|
|
213
|
+
extra_headers: Mapping[str, str] | None = None,
|
|
214
|
+
**kwargs: Any,
|
|
215
|
+
) -> dict[str, Any]:
|
|
216
|
+
"""Unified chat completion via the gateway OpenAI-compatible endpoint."""
|
|
217
|
+
return self.request(
|
|
218
|
+
"POST",
|
|
219
|
+
"/v1/chat/completions",
|
|
220
|
+
json_body={"model": model, "messages": messages, "stream": stream, **kwargs},
|
|
221
|
+
extra_headers=extra_headers,
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
def evaluate_guardrail(
|
|
225
|
+
self,
|
|
226
|
+
*,
|
|
227
|
+
stage: str,
|
|
228
|
+
actor_type: str = "sdk_user",
|
|
229
|
+
actor_id: str | None = None,
|
|
230
|
+
actor_role: str | None = None,
|
|
231
|
+
actor_customer_id: str | None = None,
|
|
232
|
+
actor_team_id: str | None = None,
|
|
233
|
+
model: str | None = None,
|
|
234
|
+
provider: str | None = None,
|
|
235
|
+
input: str | None = None,
|
|
236
|
+
output: str | None = None,
|
|
237
|
+
tool_input: str | None = None,
|
|
238
|
+
server_label: str | None = None,
|
|
239
|
+
tool_name: str | None = None,
|
|
240
|
+
action_class: str | None = None,
|
|
241
|
+
domains: list[str] | None = None,
|
|
242
|
+
app_name: str | None = None,
|
|
243
|
+
agent_name: str | None = None,
|
|
244
|
+
metadata: Mapping[str, Any] | None = None,
|
|
245
|
+
persist: bool | None = None,
|
|
246
|
+
) -> GuardrailResult:
|
|
247
|
+
body: dict[str, Any] = {
|
|
248
|
+
"stage": stage,
|
|
249
|
+
"actor_type": actor_type,
|
|
250
|
+
"actor_id": actor_id or self.requester,
|
|
251
|
+
"actor_role": actor_role or self.requester_role,
|
|
252
|
+
"model": model,
|
|
253
|
+
"provider": provider,
|
|
254
|
+
"input": input,
|
|
255
|
+
"output": output,
|
|
256
|
+
"tool_input": tool_input,
|
|
257
|
+
"server_label": server_label,
|
|
258
|
+
"tool_name": tool_name,
|
|
259
|
+
"action_class": action_class,
|
|
260
|
+
"domains": domains or [],
|
|
261
|
+
"app_name": app_name or self.app_name,
|
|
262
|
+
"agent_name": agent_name or self.agent_name,
|
|
263
|
+
"persist": self.persist if persist is None else persist,
|
|
264
|
+
}
|
|
265
|
+
if actor_customer_id:
|
|
266
|
+
body["actor_customer_id"] = actor_customer_id
|
|
267
|
+
if actor_team_id:
|
|
268
|
+
body["actor_team_id"] = actor_team_id
|
|
269
|
+
if metadata:
|
|
270
|
+
body["metadata"] = dict(metadata)
|
|
271
|
+
payload = self.request("POST", "/api/guardrails/evaluate", json_body=body)
|
|
272
|
+
return GuardrailResult.from_response(stage, payload)
|
|
273
|
+
|
|
274
|
+
def guard(
|
|
275
|
+
self,
|
|
276
|
+
*,
|
|
277
|
+
stage: str,
|
|
278
|
+
raise_on_block: bool = True,
|
|
279
|
+
**kwargs: Any,
|
|
280
|
+
) -> GuardrailResult:
|
|
281
|
+
"""Evaluate a guardrail and optionally raise on block."""
|
|
282
|
+
result = self.evaluate_guardrail(stage=stage, **kwargs)
|
|
283
|
+
if raise_on_block and result.blocked:
|
|
284
|
+
raise DeepintShieldBlockedError(
|
|
285
|
+
f"DeepintShield blocked at stage={stage}: {result.reason or result.decision}",
|
|
286
|
+
stage=stage,
|
|
287
|
+
decision=result.decision,
|
|
288
|
+
reason=result.reason,
|
|
289
|
+
payload=result.raw,
|
|
290
|
+
)
|
|
291
|
+
return result
|
deepintshield/config.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
DEFAULT_BASE_URL = "https://app.deepintshield.com"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(slots=True)
|
|
11
|
+
class ShieldConfig:
|
|
12
|
+
virtual_key: str = ""
|
|
13
|
+
base_url: str = DEFAULT_BASE_URL
|
|
14
|
+
timeout: float = 30.0
|
|
15
|
+
app_name: str = "deepintshield"
|
|
16
|
+
agent_name: str = "deepintshield-agent"
|
|
17
|
+
requester: str = "sdk-user"
|
|
18
|
+
requester_role: str = "member"
|
|
19
|
+
persist: bool = True
|
|
20
|
+
default_headers: dict[str, str] = field(default_factory=dict)
|
|
21
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
22
|
+
|
|
23
|
+
@classmethod
|
|
24
|
+
def from_env(cls) -> "ShieldConfig":
|
|
25
|
+
return cls(
|
|
26
|
+
virtual_key=(os.getenv("DEEPINTSHIELD_VIRTUAL_KEY") or "").strip(),
|
|
27
|
+
base_url=os.getenv("DEEPINTSHIELD_BASE_URL", DEFAULT_BASE_URL).rstrip("/"),
|
|
28
|
+
timeout=float(os.getenv("DEEPINTSHIELD_TIMEOUT", "30")),
|
|
29
|
+
app_name=os.getenv("DEEPINTSHIELD_APP_NAME", "deepintshield"),
|
|
30
|
+
agent_name=os.getenv("DEEPINTSHIELD_AGENT_NAME", "deepintshield-agent"),
|
|
31
|
+
requester=os.getenv("DEEPINTSHIELD_REQUESTER", "sdk-user"),
|
|
32
|
+
requester_role=os.getenv("DEEPINTSHIELD_REQUESTER_ROLE", "member"),
|
|
33
|
+
persist=os.getenv("DEEPINTSHIELD_PERSIST", "true").lower() not in {"0", "false", "no"},
|
|
34
|
+
)
|
deepintshield/errors.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class DeepintShieldError(Exception):
|
|
7
|
+
def __init__(self, message: str, status_code: int | None = None, payload: dict | None = None) -> None:
|
|
8
|
+
super().__init__(message)
|
|
9
|
+
self.message = message
|
|
10
|
+
self.status_code = status_code
|
|
11
|
+
self.payload = payload or {}
|
|
12
|
+
|
|
13
|
+
@classmethod
|
|
14
|
+
def from_response(cls, status_code: int, payload: dict | None) -> "DeepintShieldError":
|
|
15
|
+
error = (payload or {}).get("error", {})
|
|
16
|
+
message = (
|
|
17
|
+
error.get("message")
|
|
18
|
+
or (payload or {}).get("message")
|
|
19
|
+
or f"DeepintShield request failed with status {status_code}"
|
|
20
|
+
)
|
|
21
|
+
return cls(message=message, status_code=status_code, payload=payload or {})
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class DeepintShieldBlockedError(DeepintShieldError):
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
message: str,
|
|
28
|
+
*,
|
|
29
|
+
stage: str | None = None,
|
|
30
|
+
decision: str | None = None,
|
|
31
|
+
reason: str | None = None,
|
|
32
|
+
status_code: int | None = None,
|
|
33
|
+
payload: dict[str, Any] | None = None,
|
|
34
|
+
) -> None:
|
|
35
|
+
super().__init__(message=message, status_code=status_code, payload=payload)
|
|
36
|
+
self.stage = stage
|
|
37
|
+
self.decision = decision
|
|
38
|
+
self.reason = reason
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from ..client import DeepintShield
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ProviderRegistry:
|
|
10
|
+
"""Lazy accessor for provider builders, so optional deps stay optional."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, client: "DeepintShield") -> None:
|
|
13
|
+
self._client = client
|
|
14
|
+
|
|
15
|
+
def openai(self, **kwargs: Any):
|
|
16
|
+
from . import openai as mod
|
|
17
|
+
return mod.build_client(self._client, **kwargs)
|
|
18
|
+
|
|
19
|
+
def anthropic(self, **kwargs: Any):
|
|
20
|
+
from . import anthropic as mod
|
|
21
|
+
return mod.build_client(self._client, **kwargs)
|
|
22
|
+
|
|
23
|
+
def bedrock(self, **kwargs: Any):
|
|
24
|
+
from . import bedrock as mod
|
|
25
|
+
return mod.build_client(self._client, **kwargs)
|
|
26
|
+
|
|
27
|
+
def genai(self, **kwargs: Any):
|
|
28
|
+
from . import genai as mod
|
|
29
|
+
return mod.build_client(self._client, **kwargs)
|
|
30
|
+
|
|
31
|
+
def langchain(self, **kwargs: Any):
|
|
32
|
+
from . import langchain as mod
|
|
33
|
+
return mod.build_client(self._client, **kwargs)
|
|
34
|
+
|
|
35
|
+
def langgraph(self):
|
|
36
|
+
from .langgraph import LangGraphShield
|
|
37
|
+
return LangGraphShield(self._client)
|
|
38
|
+
|
|
39
|
+
def litellm(self):
|
|
40
|
+
from .litellm import LiteLLMShield
|
|
41
|
+
return LiteLLMShield(self._client)
|
|
42
|
+
|
|
43
|
+
def pydanticai(self, **kwargs: Any):
|
|
44
|
+
from . import pydanticai as mod
|
|
45
|
+
return mod.build_agent(self._client, **kwargs)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
__all__ = ["ProviderRegistry"]
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from ..client import DeepintShield
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def build_client(shield: "DeepintShield", *, passthrough: bool = False, **kwargs: Any):
|
|
10
|
+
"""Return a native ``anthropic.Anthropic`` client pointed at the gateway."""
|
|
11
|
+
try:
|
|
12
|
+
import anthropic
|
|
13
|
+
except ImportError as exc: # pragma: no cover
|
|
14
|
+
raise ImportError("Install anthropic: pip install 'deepintshield[anthropic]'") from exc
|
|
15
|
+
|
|
16
|
+
base_url = shield.anthropic_passthrough_base_url() if passthrough else shield.anthropic_base_url()
|
|
17
|
+
return anthropic.Anthropic(
|
|
18
|
+
base_url=kwargs.pop("base_url", base_url),
|
|
19
|
+
api_key=kwargs.pop("api_key", shield.api_key()),
|
|
20
|
+
default_headers={**shield.headers(), **(kwargs.pop("default_headers", None) or {})},
|
|
21
|
+
**kwargs,
|
|
22
|
+
)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from typing import TYPE_CHECKING, Any
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from ..client import DeepintShield
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def build_client(shield: "DeepintShield", *, region_name: str | None = None, **kwargs: Any):
|
|
11
|
+
"""Return a boto3 ``bedrock-runtime`` client routed through the gateway."""
|
|
12
|
+
try:
|
|
13
|
+
import boto3
|
|
14
|
+
except ImportError as exc: # pragma: no cover
|
|
15
|
+
raise ImportError("Install boto3: pip install 'deepintshield[bedrock]'") from exc
|
|
16
|
+
|
|
17
|
+
key = shield.api_key()
|
|
18
|
+
client = boto3.client(
|
|
19
|
+
service_name="bedrock-runtime",
|
|
20
|
+
endpoint_url=kwargs.pop("endpoint_url", shield.bedrock_endpoint_url()),
|
|
21
|
+
region_name=region_name or os.getenv("AWS_REGION", "us-west-2"),
|
|
22
|
+
aws_access_key_id=kwargs.pop("aws_access_key_id", key),
|
|
23
|
+
aws_secret_access_key=kwargs.pop("aws_secret_access_key", key),
|
|
24
|
+
**kwargs,
|
|
25
|
+
)
|
|
26
|
+
headers = shield.headers()
|
|
27
|
+
|
|
28
|
+
def _inject_headers(request, **_kwargs):
|
|
29
|
+
for name, value in headers.items():
|
|
30
|
+
request.headers.add_header(name, value)
|
|
31
|
+
|
|
32
|
+
client.meta.events.register_first("before-sign.bedrock-runtime.*", _inject_headers)
|
|
33
|
+
return client
|