agent-control-evaluator-cisco 7.7.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.
- agent_control_evaluator_cisco/__init__.py +2 -0
- agent_control_evaluator_cisco/ai_defense/__init__.py +5 -0
- agent_control_evaluator_cisco/ai_defense/client.py +101 -0
- agent_control_evaluator_cisco/ai_defense/config.py +33 -0
- agent_control_evaluator_cisco/ai_defense/evaluator.py +229 -0
- agent_control_evaluator_cisco-7.7.0.dist-info/METADATA +194 -0
- agent_control_evaluator_cisco-7.7.0.dist-info/RECORD +9 -0
- agent_control_evaluator_cisco-7.7.0.dist-info/WHEEL +4 -0
- agent_control_evaluator_cisco-7.7.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
# Thin REST client for Cisco AI Defense Chat Inspection.
|
|
4
|
+
# Uses httpx.AsyncClient and the OpenAPI-defined endpoint/header.
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
11
|
+
AI_DEFENSE_HTTPX_AVAILABLE = True
|
|
12
|
+
except ImportError: # Narrow to import error only
|
|
13
|
+
httpx = None # type: ignore
|
|
14
|
+
AI_DEFENSE_HTTPX_AVAILABLE = False
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# Regions from ai_defense_api.json "servers" section
|
|
18
|
+
REGION_BASE_URLS: dict[str, str] = {
|
|
19
|
+
"us": "https://us.api.inspect.aidefense.security.cisco.com",
|
|
20
|
+
"ap": "https://ap.api.inspect.aidefense.security.cisco.com",
|
|
21
|
+
"eu": "https://eu.api.inspect.aidefense.security.cisco.com",
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def build_endpoint(base_url: str) -> str:
|
|
26
|
+
base = base_url.rstrip("/")
|
|
27
|
+
return f"{base}/api/v1/inspect/chat"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class AIDefenseClient:
|
|
32
|
+
"""Minimal async client for Cisco AI Defense Chat Inspection.
|
|
33
|
+
|
|
34
|
+
Attributes:
|
|
35
|
+
api_key: API key used for authentication header
|
|
36
|
+
endpoint_url: Full URL to POST /api/v1/inspect/chat
|
|
37
|
+
timeout_s: Timeout in seconds
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
api_key: str = field(repr=False)
|
|
41
|
+
endpoint_url: str
|
|
42
|
+
timeout_s: float
|
|
43
|
+
|
|
44
|
+
_client: httpx.AsyncClient | None = field( # type: ignore[name-defined]
|
|
45
|
+
default=None,
|
|
46
|
+
repr=False,
|
|
47
|
+
compare=False,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
async def _get_client(self) -> httpx.AsyncClient: # type: ignore[name-defined]
|
|
51
|
+
if not AI_DEFENSE_HTTPX_AVAILABLE: # pragma: no cover
|
|
52
|
+
raise RuntimeError("httpx not installed; cannot call Cisco AI Defense REST API")
|
|
53
|
+
if self._client is None or self._client.is_closed:
|
|
54
|
+
self._client = httpx.AsyncClient(timeout=self.timeout_s)
|
|
55
|
+
return self._client
|
|
56
|
+
|
|
57
|
+
async def chat_inspect(
|
|
58
|
+
self,
|
|
59
|
+
messages: list[dict[str, str]],
|
|
60
|
+
metadata: dict[str, Any] | None = None,
|
|
61
|
+
inspect_config: dict[str, Any] | None = None,
|
|
62
|
+
headers: dict[str, str] | None = None,
|
|
63
|
+
) -> dict[str, Any]:
|
|
64
|
+
client = await self._get_client()
|
|
65
|
+
|
|
66
|
+
req_headers: dict[str, str] = {
|
|
67
|
+
"X-Cisco-AI-Defense-API-Key": self.api_key,
|
|
68
|
+
"Content-Type": "application/json",
|
|
69
|
+
"Accept": "application/json",
|
|
70
|
+
}
|
|
71
|
+
if headers:
|
|
72
|
+
req_headers.update(headers)
|
|
73
|
+
|
|
74
|
+
payload: dict[str, Any] = {"messages": messages}
|
|
75
|
+
if metadata is not None:
|
|
76
|
+
payload["metadata"] = metadata
|
|
77
|
+
if inspect_config is not None:
|
|
78
|
+
payload["config"] = inspect_config
|
|
79
|
+
|
|
80
|
+
resp = await client.post(self.endpoint_url, json=payload, headers=req_headers)
|
|
81
|
+
resp.raise_for_status()
|
|
82
|
+
data = resp.json()
|
|
83
|
+
if not isinstance(data, dict):
|
|
84
|
+
raise RuntimeError("Invalid response payload: not a JSON object")
|
|
85
|
+
return data
|
|
86
|
+
|
|
87
|
+
async def aclose(self) -> None:
|
|
88
|
+
if self._client and not self._client.is_closed:
|
|
89
|
+
await self._client.aclose()
|
|
90
|
+
|
|
91
|
+
async def close(self) -> None:
|
|
92
|
+
"""Close the HTTP client and release resources."""
|
|
93
|
+
await self.aclose()
|
|
94
|
+
|
|
95
|
+
async def __aenter__(self) -> AIDefenseClient:
|
|
96
|
+
"""Async context manager entry."""
|
|
97
|
+
return self
|
|
98
|
+
|
|
99
|
+
async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
100
|
+
"""Async context manager exit."""
|
|
101
|
+
await self.close()
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Literal
|
|
4
|
+
|
|
5
|
+
from agent_control_evaluators import EvaluatorConfig
|
|
6
|
+
from pydantic import Field
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class CiscoAIDefenseConfig(EvaluatorConfig):
|
|
10
|
+
"""Configuration for Cisco AI Defense evaluator (REST).
|
|
11
|
+
|
|
12
|
+
Attributes:
|
|
13
|
+
api_key_env: Env var name for API key
|
|
14
|
+
region: Optional server region (us, ap, eu); ignored if api_url set
|
|
15
|
+
api_url: Optional full endpoint override
|
|
16
|
+
timeout_ms: Request timeout (milliseconds)
|
|
17
|
+
on_error: Error policy (allow=fail-open, deny=fail-closed)
|
|
18
|
+
payload_field: Force single-message role: input→user, output→assistant
|
|
19
|
+
messages_strategy: "single" (synthesize) or "history" (pass-through messages)
|
|
20
|
+
metadata: Optional metadata object to include (OpenAPI spec)
|
|
21
|
+
inspect_config: Optional Inspect API config passthrough (see OpenAPI spec)
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
api_key_env: str = "AI_DEFENSE_API_KEY"
|
|
25
|
+
region: Literal["us", "ap", "eu"] | None = "us"
|
|
26
|
+
api_url: str | None = None
|
|
27
|
+
timeout_ms: int = Field(default=15_000, ge=1)
|
|
28
|
+
on_error: Literal["allow", "deny"] = "allow"
|
|
29
|
+
payload_field: Literal["input", "output"] | None = None
|
|
30
|
+
messages_strategy: Literal["single", "history"] = "history"
|
|
31
|
+
metadata: dict[str, Any] | None = None
|
|
32
|
+
inspect_config: dict[str, Any] | None = None
|
|
33
|
+
include_raw_response: bool = False
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from agent_control_evaluators import (
|
|
9
|
+
Evaluator,
|
|
10
|
+
EvaluatorMetadata,
|
|
11
|
+
register_evaluator,
|
|
12
|
+
)
|
|
13
|
+
from agent_control_models import EvaluatorResult
|
|
14
|
+
|
|
15
|
+
from .client import AI_DEFENSE_HTTPX_AVAILABLE, REGION_BASE_URLS, AIDefenseClient, build_endpoint
|
|
16
|
+
from .config import CiscoAIDefenseConfig
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _resolve_package_version() -> str:
|
|
20
|
+
"""Return the installed package version, or a dev fallback during local imports."""
|
|
21
|
+
try:
|
|
22
|
+
return version("agent-control-evaluator-cisco")
|
|
23
|
+
except PackageNotFoundError:
|
|
24
|
+
return "0.0.0.dev"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
_PACKAGE_VERSION = _resolve_package_version()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _load_api_key(env_name: str) -> str:
|
|
31
|
+
key = os.getenv(env_name)
|
|
32
|
+
if not key:
|
|
33
|
+
raise RuntimeError(
|
|
34
|
+
f"Missing Cisco AI Defense API key in env '{env_name}'. Set it on the server."
|
|
35
|
+
)
|
|
36
|
+
return key
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _build_messages(
|
|
40
|
+
data: Any,
|
|
41
|
+
strategy: str,
|
|
42
|
+
payload_field: str | None,
|
|
43
|
+
) -> list[dict[str, str]]:
|
|
44
|
+
"""Build Chat Inspection messages from selected data.
|
|
45
|
+
|
|
46
|
+
- history: pass-through if data has 'messages' list; else fallback to single
|
|
47
|
+
- single: synthesize one message with role based on payload_field
|
|
48
|
+
"""
|
|
49
|
+
if strategy == "history":
|
|
50
|
+
if isinstance(data, dict) and isinstance(data.get("messages"), list):
|
|
51
|
+
msgs: list[dict[str, str]] = []
|
|
52
|
+
for m in data["messages"]:
|
|
53
|
+
if isinstance(m, dict) and "content" in m:
|
|
54
|
+
role = str(m.get("role", "user"))
|
|
55
|
+
content = str(m.get("content", ""))
|
|
56
|
+
msgs.append({"role": role, "content": content})
|
|
57
|
+
if msgs:
|
|
58
|
+
return msgs
|
|
59
|
+
# Fallback to single
|
|
60
|
+
|
|
61
|
+
role = "assistant" if payload_field == "output" else "user"
|
|
62
|
+
content = _coerce_message_content(data, payload_field)
|
|
63
|
+
return [{"role": role, "content": content}]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _coerce_message_content(data: Any, payload_field: str | None) -> str:
|
|
67
|
+
if data is None:
|
|
68
|
+
return ""
|
|
69
|
+
|
|
70
|
+
if isinstance(data, dict):
|
|
71
|
+
candidate: Any = None
|
|
72
|
+
preferred_keys: list[str] = []
|
|
73
|
+
if payload_field is not None:
|
|
74
|
+
preferred_keys.append(payload_field)
|
|
75
|
+
if payload_field == "output":
|
|
76
|
+
preferred_keys.extend(["output", "content", "text", "message"])
|
|
77
|
+
else:
|
|
78
|
+
preferred_keys.extend(["input", "content", "text", "message"])
|
|
79
|
+
|
|
80
|
+
for key in preferred_keys:
|
|
81
|
+
if key in data and data[key] is not None:
|
|
82
|
+
candidate = data[key]
|
|
83
|
+
break
|
|
84
|
+
|
|
85
|
+
if candidate is None:
|
|
86
|
+
candidate = data
|
|
87
|
+
return _stringify_message_content(candidate)
|
|
88
|
+
|
|
89
|
+
return _stringify_message_content(data)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _stringify_message_content(value: Any) -> str:
|
|
93
|
+
if value is None:
|
|
94
|
+
return ""
|
|
95
|
+
if isinstance(value, str):
|
|
96
|
+
return value
|
|
97
|
+
if isinstance(value, (int, float, bool)):
|
|
98
|
+
return str(value)
|
|
99
|
+
try:
|
|
100
|
+
return json.dumps(value, ensure_ascii=False, sort_keys=True, default=str)
|
|
101
|
+
except TypeError:
|
|
102
|
+
return str(value)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@register_evaluator
|
|
106
|
+
class CiscoAIDefenseEvaluator(Evaluator[CiscoAIDefenseConfig]):
|
|
107
|
+
"""Cisco AI Defense evaluator.
|
|
108
|
+
|
|
109
|
+
Maps InspectResponse.is_safe to EvaluatorResult.matched.
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
metadata = EvaluatorMetadata(
|
|
113
|
+
name="cisco.ai_defense",
|
|
114
|
+
version=_PACKAGE_VERSION,
|
|
115
|
+
description="Cisco AI Defense Chat Inspection integration",
|
|
116
|
+
requires_api_key=True,
|
|
117
|
+
timeout_ms=15000,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
config_model = CiscoAIDefenseConfig
|
|
121
|
+
|
|
122
|
+
@classmethod
|
|
123
|
+
def is_available(cls) -> bool:
|
|
124
|
+
"""Evaluator is available only if httpx dependency exists."""
|
|
125
|
+
return AI_DEFENSE_HTTPX_AVAILABLE
|
|
126
|
+
|
|
127
|
+
def __init__(self, config: CiscoAIDefenseConfig) -> None:
|
|
128
|
+
self.config = config
|
|
129
|
+
|
|
130
|
+
# Validate and resolve configuration eagerly to avoid per-call work.
|
|
131
|
+
# API key
|
|
132
|
+
try:
|
|
133
|
+
api_key = _load_api_key(self.config.api_key_env)
|
|
134
|
+
except RuntimeError as e:
|
|
135
|
+
# Fail fast during construction so misconfiguration is caught early.
|
|
136
|
+
raise ValueError(str(e)) from e
|
|
137
|
+
|
|
138
|
+
# Endpoint
|
|
139
|
+
if self.config.api_url:
|
|
140
|
+
endpoint_url = self.config.api_url
|
|
141
|
+
else:
|
|
142
|
+
base_url = REGION_BASE_URLS.get(self.config.region or "us", REGION_BASE_URLS["us"])
|
|
143
|
+
endpoint_url = build_endpoint(base_url)
|
|
144
|
+
|
|
145
|
+
# Timeout
|
|
146
|
+
timeout_s = float(self.config.timeout_ms) / 1000.0
|
|
147
|
+
|
|
148
|
+
# Create a single client instance for reuse.
|
|
149
|
+
self._client: AIDefenseClient = AIDefenseClient(
|
|
150
|
+
api_key=api_key,
|
|
151
|
+
endpoint_url=endpoint_url,
|
|
152
|
+
timeout_s=timeout_s,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
async def evaluate(self, data: Any) -> EvaluatorResult: # noqa: D401
|
|
156
|
+
# Null input: do not call external service; treat as no data
|
|
157
|
+
if data is None:
|
|
158
|
+
return EvaluatorResult(matched=False, confidence=1.0, message="No data")
|
|
159
|
+
|
|
160
|
+
messages = _build_messages(
|
|
161
|
+
data,
|
|
162
|
+
strategy=self.config.messages_strategy,
|
|
163
|
+
payload_field=self.config.payload_field,
|
|
164
|
+
)
|
|
165
|
+
if not messages:
|
|
166
|
+
return EvaluatorResult(matched=False, confidence=1.0, message="No data to inspect")
|
|
167
|
+
|
|
168
|
+
# Call REST API for Chat Inspection
|
|
169
|
+
try:
|
|
170
|
+
response: dict[str, Any] = await self._client.chat_inspect(
|
|
171
|
+
messages=messages,
|
|
172
|
+
metadata=self.config.metadata,
|
|
173
|
+
inspect_config=self.config.inspect_config,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
# Map is_safe to matched
|
|
177
|
+
is_safe = response.get("is_safe")
|
|
178
|
+
if isinstance(is_safe, bool):
|
|
179
|
+
matched = not is_safe
|
|
180
|
+
msg = "Content is unsafe" if matched else "Content is safe"
|
|
181
|
+
meta: dict[str, Any] = {
|
|
182
|
+
"severity": response.get("severity"),
|
|
183
|
+
"classifications": response.get("classifications"),
|
|
184
|
+
"rules": response.get("rules"),
|
|
185
|
+
"attack_technique": response.get("attack_technique"),
|
|
186
|
+
"event_id": response.get("event_id"),
|
|
187
|
+
}
|
|
188
|
+
if self.config.include_raw_response:
|
|
189
|
+
meta["raw"] = response
|
|
190
|
+
return EvaluatorResult(
|
|
191
|
+
matched=matched,
|
|
192
|
+
confidence=1.0,
|
|
193
|
+
message=msg,
|
|
194
|
+
metadata=meta,
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
# If no boolean is present, consider it an evaluator error
|
|
198
|
+
fallback = self.config.on_error
|
|
199
|
+
matched = fallback == "deny"
|
|
200
|
+
error_message = "Cisco AI Defense response missing 'is_safe'"
|
|
201
|
+
meta2: dict[str, Any] = {"fallback_action": fallback}
|
|
202
|
+
if self.config.include_raw_response:
|
|
203
|
+
meta2["raw"] = response
|
|
204
|
+
return EvaluatorResult(
|
|
205
|
+
matched=matched,
|
|
206
|
+
confidence=0.0,
|
|
207
|
+
message=error_message,
|
|
208
|
+
metadata=meta2,
|
|
209
|
+
error=None if matched else error_message,
|
|
210
|
+
)
|
|
211
|
+
except Exception as e: # noqa: BLE001
|
|
212
|
+
fallback = self.config.on_error
|
|
213
|
+
matched = fallback == "deny"
|
|
214
|
+
error_detail = str(e)
|
|
215
|
+
return EvaluatorResult(
|
|
216
|
+
matched=matched,
|
|
217
|
+
confidence=0.0,
|
|
218
|
+
message=f"Cisco AI Defense evaluation error: {error_detail}",
|
|
219
|
+
metadata={
|
|
220
|
+
"error": error_detail,
|
|
221
|
+
"error_type": type(e).__name__,
|
|
222
|
+
"fallback_action": fallback,
|
|
223
|
+
},
|
|
224
|
+
error=None if matched else error_detail,
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
async def aclose(self) -> None:
|
|
228
|
+
"""Close the underlying Cisco AI Defense client."""
|
|
229
|
+
await self._client.aclose()
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agent-control-evaluator-cisco
|
|
3
|
+
Version: 7.7.0
|
|
4
|
+
Summary: Cisco AI Defense evaluator for agent-control
|
|
5
|
+
Author: Cisco AI Defense Team
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Requires-Python: >=3.12
|
|
8
|
+
Requires-Dist: agent-control-evaluators>=7.7.0
|
|
9
|
+
Requires-Dist: agent-control-models>=7.7.0
|
|
10
|
+
Requires-Dist: httpx>=0.24.0
|
|
11
|
+
Provides-Extra: dev
|
|
12
|
+
Requires-Dist: mypy>=1.8.0; extra == 'dev'
|
|
13
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
|
|
14
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
|
|
15
|
+
Requires-Dist: pytest>=8.0.0; extra == 'dev'
|
|
16
|
+
Requires-Dist: ruff>=0.1.0; extra == 'dev'
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# Agent Control Evaluator - Cisco AI Defense
|
|
20
|
+
|
|
21
|
+
External evaluator that calls Cisco AI Defense Chat Inspection via REST and maps `InspectResponse.is_safe` to Agent Control decisions.
|
|
22
|
+
|
|
23
|
+
- Entry point name: `cisco.ai_defense`
|
|
24
|
+
- Transport: direct HTTP (httpx)
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
Canonical install path:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install "agent-control-evaluators[cisco]"
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Fallback direct wheel install:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install agent-control-evaluator-cisco
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
For local development:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
uv pip install -e evaluators/contrib/cisco
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
- Build wheel from the repo root (contrib package only):
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
make engine-build
|
|
50
|
+
(cd evaluators/contrib/cisco && make build)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
To run the server with this evaluator enabled, see `examples/cisco_ai_defense/README.md` for setup and seeding instructions.
|
|
54
|
+
|
|
55
|
+
## Configuration
|
|
56
|
+
|
|
57
|
+
Set the `AI_DEFENSE_API_KEY` environment variable:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
export AI_DEFENSE_API_KEY="<your_key>"
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Evaluator config fields (all optional unless stated):
|
|
64
|
+
|
|
65
|
+
- `api_key_env: str = "AI_DEFENSE_API_KEY"`
|
|
66
|
+
- `region: "us" | "ap" | "eu" | None = "us"` (ignored if `api_url` set)
|
|
67
|
+
- `api_url: str | None = None` (full endpoint override; e.g., `https://us.../api/v1/inspect/chat`)
|
|
68
|
+
- `timeout_ms: int = 15000`
|
|
69
|
+
- `on_error: "allow" | "deny" = "allow"` (fail-open or fail-closed on transport/response errors)
|
|
70
|
+
- `payload_field: "input" | "output" | None = None`
|
|
71
|
+
- When set, synthesizes a single message from that field; `input` → `role=user`, `output` → `role=assistant`.
|
|
72
|
+
- `messages_strategy: "single" | "history" = "history"`
|
|
73
|
+
- `history` forwards an existing `messages` list in the selected data if present; falls back to single otherwise.
|
|
74
|
+
- `metadata: dict[str, Any] | None = None` (forwarded to API per OpenAPI spec)
|
|
75
|
+
- `inspect_config: dict[str, Any] | None = None` (forwarded to API per OpenAPI spec)
|
|
76
|
+
- `include_raw_response: bool = false` (when true, includes the full provider response under `metadata.raw`)
|
|
77
|
+
|
|
78
|
+
## Available Evaluators
|
|
79
|
+
|
|
80
|
+
| Name | Description |
|
|
81
|
+
|------|-------------|
|
|
82
|
+
| `cisco.ai_defense` | Cisco AI Defense Chat Inspection |
|
|
83
|
+
|
|
84
|
+
Behavior mapping:
|
|
85
|
+
|
|
86
|
+
- `is_safe == false` → `EvaluatorResult.matched = true` (e.g., a `deny` action will block)
|
|
87
|
+
- `is_safe == true` → `matched = false`
|
|
88
|
+
- Errors or invalid responses → `matched = (on_error == "deny")`; error details in `metadata` (no `error` field is set; engine honors `matched` per `on_error`)
|
|
89
|
+
|
|
90
|
+
## Minimal server control configuration
|
|
91
|
+
|
|
92
|
+
Example using `messages_strategy: "history"` (for inputs that already have a `messages` list):
|
|
93
|
+
|
|
94
|
+
```
|
|
95
|
+
{
|
|
96
|
+
"description": "Apply Cisco AI Defense Security, Safety, and Privacy guardrails",
|
|
97
|
+
"enabled": true,
|
|
98
|
+
"execution": "server",
|
|
99
|
+
"scope": { "step_types": ["llm"], "stages": ["pre", "post"] },
|
|
100
|
+
"condition": {
|
|
101
|
+
"selector": { "path": "input" },
|
|
102
|
+
"evaluator": {
|
|
103
|
+
"name": "cisco.ai_defense",
|
|
104
|
+
"config": {
|
|
105
|
+
"api_key_env": "AI_DEFENSE_API_KEY",
|
|
106
|
+
"region": "us",
|
|
107
|
+
"timeout_ms": 15000,
|
|
108
|
+
"on_error": "allow",
|
|
109
|
+
"messages_strategy": "history"
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
"action": { "decision": "deny" },
|
|
114
|
+
"tags": ["ai_defense", "safety"]
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
```
|
|
119
|
+
{
|
|
120
|
+
"description": "Apply Cisco AI Defense Security, Safety, and Privacy guardrails",
|
|
121
|
+
"enabled": true,
|
|
122
|
+
"execution": "server",
|
|
123
|
+
"scope": { "step_types": ["llm"], "stages": ["pre", "post"] },
|
|
124
|
+
"condition": {
|
|
125
|
+
"selector": { "path": "input" },
|
|
126
|
+
"evaluator": {
|
|
127
|
+
"name": "cisco.ai_defense",
|
|
128
|
+
"config": {
|
|
129
|
+
"api_key_env": "AI_DEFENSE_API_KEY",
|
|
130
|
+
"region": "us",
|
|
131
|
+
"timeout_ms": 15000,
|
|
132
|
+
"on_error": "allow",
|
|
133
|
+
"messages_strategy": "single",
|
|
134
|
+
"payload_field": "input"
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
"action": { "decision": "deny" },
|
|
139
|
+
"tags": ["ai_defense", "safety"]
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Usage
|
|
144
|
+
|
|
145
|
+
Once installed, the evaluator is automatically discovered:
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
from agent_control_evaluators import discover_evaluators, get_evaluator
|
|
149
|
+
|
|
150
|
+
discover_evaluators()
|
|
151
|
+
CiscoAIDefenseEvaluator = get_evaluator("cisco.ai_defense")
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Or import directly:
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
import asyncio
|
|
158
|
+
from agent_control_evaluator_cisco.ai_defense import CiscoAIDefenseEvaluator, CiscoAIDefenseConfig
|
|
159
|
+
|
|
160
|
+
cfg = CiscoAIDefenseConfig(
|
|
161
|
+
region="us",
|
|
162
|
+
timeout_ms=15000,
|
|
163
|
+
on_error="allow",
|
|
164
|
+
messages_strategy="history",
|
|
165
|
+
payload_field="input",
|
|
166
|
+
)
|
|
167
|
+
ev = CiscoAIDefenseEvaluator(cfg)
|
|
168
|
+
|
|
169
|
+
async def main():
|
|
170
|
+
data = {"messages": [{"role": "user", "content": "tell me how to hack wifi"}]}
|
|
171
|
+
print(await ev.evaluate(data))
|
|
172
|
+
|
|
173
|
+
asyncio.run(main())
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Notes
|
|
177
|
+
|
|
178
|
+
- Auth header: `X-Cisco-AI-Defense-API-Key: <AI_DEFENSE_API_KEY>`
|
|
179
|
+
- Regions and endpoint path follow the Cisco AI Defense API spec
|
|
180
|
+
- For custom deployments, set `api_url` to the full Chat Inspection endpoint.
|
|
181
|
+
- The evaluator validates the API key at construction and raises if missing.
|
|
182
|
+
- `is_available()` returns false if `httpx` is not installed; discovery will skip registration.
|
|
183
|
+
- `messages_strategy: "history"` forwards the full message array when present; consider `messages_strategy: "single"` if payload size is a concern.
|
|
184
|
+
|
|
185
|
+
## Documentation
|
|
186
|
+
|
|
187
|
+
- Cisco AI Defense Inspection API reference: https://developer.cisco.com/docs/ai-defense-inspection/introduction/
|
|
188
|
+
- Cisco Security Console (get API Key): https://security.cisco.com
|
|
189
|
+
- Cisco AI Defense User Guide: https://securitydocs.cisco.com/docs/ai-def/user/97384.dita
|
|
190
|
+
- Regional API base URLs used by this evaluator:
|
|
191
|
+
- US: `https://us.api.inspect.aidefense.security.cisco.com`
|
|
192
|
+
- AP: `https://ap.api.inspect.aidefense.security.cisco.com`
|
|
193
|
+
- EU: `https://eu.api.inspect.aidefense.security.cisco.com`
|
|
194
|
+
- Chat Inspection path: `/api/v1/inspect/chat`
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
agent_control_evaluator_cisco/__init__.py,sha256=8DDvq0Peb__lGVcurhl4UvirKbOlgwleoZit4kOpYd4,14
|
|
2
|
+
agent_control_evaluator_cisco/ai_defense/__init__.py,sha256=OqDF1J2GcagNASxsHSUD8O08rYQt7cmfkac_rSUtdcE,152
|
|
3
|
+
agent_control_evaluator_cisco/ai_defense/client.py,sha256=qRHV5P8tneGdG-NylbSSR3WubteYOS55GP5vfVc-5Bc,3304
|
|
4
|
+
agent_control_evaluator_cisco/ai_defense/config.py,sha256=54qIBL7QQAt5AvxildR7xPCZ6QN-Rw2ac1V_kIw-cfU,1373
|
|
5
|
+
agent_control_evaluator_cisco/ai_defense/evaluator.py,sha256=HlKkZRmzTdqI0Q44kgJYeP-aMLeSgefwhPHnkgQqHEg,7870
|
|
6
|
+
agent_control_evaluator_cisco-7.7.0.dist-info/METADATA,sha256=sACAcDeWRsVp5dZadVLRB8OD3t1gJGQfmPwBkHg3hBE,6175
|
|
7
|
+
agent_control_evaluator_cisco-7.7.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
8
|
+
agent_control_evaluator_cisco-7.7.0.dist-info/entry_points.txt,sha256=UxIxKYokYK7Bd7Cc18sAfwdKKEIrqUhaD9mZBadS5S8,111
|
|
9
|
+
agent_control_evaluator_cisco-7.7.0.dist-info/RECORD,,
|