shieldpi 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.
@@ -0,0 +1,161 @@
1
+ Metadata-Version: 2.4
2
+ Name: shieldpi
3
+ Version: 0.1.0
4
+ Summary: ShieldPi Watchtower — live agent monitoring SDK for Python
5
+ Author-email: ShieldPi <support@shieldpi.io>
6
+ License: Apache-2.0
7
+ Project-URL: Homepage, https://shieldpi.io
8
+ Project-URL: Documentation, https://docs.shieldpi.io/sdks/python
9
+ Project-URL: Repository, https://github.com/ShieldPi1/shieldpi-watchtower
10
+ Keywords: llm,security,agents,monitoring,ai-security
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: Apache Software License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Security
20
+ Requires-Python: >=3.9
21
+ Description-Content-Type: text/markdown
22
+ Requires-Dist: httpx>=0.24.0
23
+ Provides-Extra: langchain
24
+ Requires-Dist: langchain-core>=0.1.0; extra == "langchain"
25
+ Provides-Extra: anthropic
26
+ Requires-Dist: anthropic>=0.25.0; extra == "anthropic"
27
+ Provides-Extra: dev
28
+ Requires-Dist: pytest>=7; extra == "dev"
29
+ Requires-Dist: pytest-asyncio>=0.21; extra == "dev"
30
+
31
+ # ShieldPi Python SDK
32
+
33
+ Official Python SDK for the ShieldPi LLM Security Scanner API.
34
+
35
+ ## Installation
36
+
37
+ ```bash
38
+ pip install shieldpi
39
+ ```
40
+
41
+ ## Quick Start
42
+
43
+ ```python
44
+ from shieldpi import ShieldPi
45
+
46
+ client = ShieldPi(api_key="your-api-key")
47
+
48
+ # Create a target
49
+ target = client.targets.create(
50
+ name="My Chatbot",
51
+ url="https://my-chatbot.com/api/chat",
52
+ scan_mode="api"
53
+ )
54
+
55
+ # Run a scan
56
+ scan = client.scans.create(target_id=target.id)
57
+
58
+ # Wait for completion
59
+ result = client.scans.wait(scan.id)
60
+
61
+ # Get the report
62
+ print(f"Security Grade: {result.grade}")
63
+ print(f"Score: {result.score}/100")
64
+ print(f"Findings: {result.total_findings}")
65
+ ```
66
+
67
+ ## API Reference
68
+
69
+ ### Authentication
70
+ ```python
71
+ client = ShieldPi(api_key="sk-...", base_url="https://api.shieldpi.io/api")
72
+ ```
73
+
74
+ ### Targets
75
+ ```python
76
+ client.targets.list()
77
+ client.targets.create(name="...", url="...", scan_mode="model")
78
+ client.targets.get(target_id)
79
+ client.targets.delete(target_id)
80
+ ```
81
+
82
+ ### Scans
83
+ ```python
84
+ client.scans.create(target_id=target_id)
85
+ client.scans.get(scan_id)
86
+ client.scans.list()
87
+ client.scans.wait(scan_id, timeout=3600)
88
+ ```
89
+
90
+ ### Reports
91
+ ```python
92
+ client.scans.download_report(scan_id, format="pdf")
93
+ client.scans.download_report(scan_id, format="json")
94
+ ```
95
+
96
+ ## Live Agent Monitoring
97
+
98
+ For real-time agent monitoring, use the `Monitor` class:
99
+
100
+ ```python
101
+ from shieldpi import Monitor
102
+
103
+ monitor = Monitor(sdk_key="shpi_live_...")
104
+
105
+ with monitor.start_session(
106
+ agent_name="invoice-bot",
107
+ stated_goal="help users file invoices",
108
+ ) as session:
109
+ session.log_user_message("How do I file a Q1 invoice?")
110
+ session.log_tool_call("search_docs", {"query": "Q1 invoice filing"})
111
+ session.log_tool_result("search_docs", {"results": [...]})
112
+ session.log_final_response("Here's how to file a Q1 invoice...")
113
+ ```
114
+
115
+ ### LangChain Integration
116
+
117
+ ```python
118
+ from shieldpi import Monitor
119
+ from shieldpi.hooks.langchain import ShieldPiCallbackHandler
120
+
121
+ monitor = Monitor(sdk_key="shpi_live_...")
122
+ callbacks = [ShieldPiCallbackHandler(
123
+ monitor,
124
+ agent_name="my-agent",
125
+ stated_goal="help users with their accounts",
126
+ )]
127
+
128
+ agent.invoke({"input": "..."}, config={"callbacks": callbacks})
129
+ ```
130
+
131
+ ### Anthropic Tool Use
132
+
133
+ ```python
134
+ from anthropic import Anthropic
135
+ from shieldpi import Monitor
136
+ from shieldpi.hooks.anthropic import monitored_tool_use
137
+
138
+ anth = Anthropic()
139
+ monitor = Monitor(sdk_key="shpi_live_...")
140
+
141
+ with monitored_tool_use(monitor, agent_name="invoice-bot") as session:
142
+ session.log_user_message("Help me file an invoice")
143
+ response = anth.messages.create(
144
+ model="claude-opus-4-20250514",
145
+ messages=[{"role": "user", "content": "Help me file an invoice"}],
146
+ tools=[...],
147
+ )
148
+ session.observe_anthropic_response(response)
149
+ ```
150
+
151
+ ## Configuration
152
+
153
+ Environment variables:
154
+
155
+ - `SHIELDPI_API_KEY` — API key for scanner operations
156
+ - `SHIELDPI_SDK_KEY` — SDK key for live monitoring
157
+ - `SHIELDPI_BASE_URL` — Override the API base URL (default: `https://api.shieldpi.io/api`)
158
+
159
+ ## License
160
+
161
+ Apache-2.0
@@ -0,0 +1,131 @@
1
+ # ShieldPi Python SDK
2
+
3
+ Official Python SDK for the ShieldPi LLM Security Scanner API.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install shieldpi
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```python
14
+ from shieldpi import ShieldPi
15
+
16
+ client = ShieldPi(api_key="your-api-key")
17
+
18
+ # Create a target
19
+ target = client.targets.create(
20
+ name="My Chatbot",
21
+ url="https://my-chatbot.com/api/chat",
22
+ scan_mode="api"
23
+ )
24
+
25
+ # Run a scan
26
+ scan = client.scans.create(target_id=target.id)
27
+
28
+ # Wait for completion
29
+ result = client.scans.wait(scan.id)
30
+
31
+ # Get the report
32
+ print(f"Security Grade: {result.grade}")
33
+ print(f"Score: {result.score}/100")
34
+ print(f"Findings: {result.total_findings}")
35
+ ```
36
+
37
+ ## API Reference
38
+
39
+ ### Authentication
40
+ ```python
41
+ client = ShieldPi(api_key="sk-...", base_url="https://api.shieldpi.io/api")
42
+ ```
43
+
44
+ ### Targets
45
+ ```python
46
+ client.targets.list()
47
+ client.targets.create(name="...", url="...", scan_mode="model")
48
+ client.targets.get(target_id)
49
+ client.targets.delete(target_id)
50
+ ```
51
+
52
+ ### Scans
53
+ ```python
54
+ client.scans.create(target_id=target_id)
55
+ client.scans.get(scan_id)
56
+ client.scans.list()
57
+ client.scans.wait(scan_id, timeout=3600)
58
+ ```
59
+
60
+ ### Reports
61
+ ```python
62
+ client.scans.download_report(scan_id, format="pdf")
63
+ client.scans.download_report(scan_id, format="json")
64
+ ```
65
+
66
+ ## Live Agent Monitoring
67
+
68
+ For real-time agent monitoring, use the `Monitor` class:
69
+
70
+ ```python
71
+ from shieldpi import Monitor
72
+
73
+ monitor = Monitor(sdk_key="shpi_live_...")
74
+
75
+ with monitor.start_session(
76
+ agent_name="invoice-bot",
77
+ stated_goal="help users file invoices",
78
+ ) as session:
79
+ session.log_user_message("How do I file a Q1 invoice?")
80
+ session.log_tool_call("search_docs", {"query": "Q1 invoice filing"})
81
+ session.log_tool_result("search_docs", {"results": [...]})
82
+ session.log_final_response("Here's how to file a Q1 invoice...")
83
+ ```
84
+
85
+ ### LangChain Integration
86
+
87
+ ```python
88
+ from shieldpi import Monitor
89
+ from shieldpi.hooks.langchain import ShieldPiCallbackHandler
90
+
91
+ monitor = Monitor(sdk_key="shpi_live_...")
92
+ callbacks = [ShieldPiCallbackHandler(
93
+ monitor,
94
+ agent_name="my-agent",
95
+ stated_goal="help users with their accounts",
96
+ )]
97
+
98
+ agent.invoke({"input": "..."}, config={"callbacks": callbacks})
99
+ ```
100
+
101
+ ### Anthropic Tool Use
102
+
103
+ ```python
104
+ from anthropic import Anthropic
105
+ from shieldpi import Monitor
106
+ from shieldpi.hooks.anthropic import monitored_tool_use
107
+
108
+ anth = Anthropic()
109
+ monitor = Monitor(sdk_key="shpi_live_...")
110
+
111
+ with monitored_tool_use(monitor, agent_name="invoice-bot") as session:
112
+ session.log_user_message("Help me file an invoice")
113
+ response = anth.messages.create(
114
+ model="claude-opus-4-20250514",
115
+ messages=[{"role": "user", "content": "Help me file an invoice"}],
116
+ tools=[...],
117
+ )
118
+ session.observe_anthropic_response(response)
119
+ ```
120
+
121
+ ## Configuration
122
+
123
+ Environment variables:
124
+
125
+ - `SHIELDPI_API_KEY` — API key for scanner operations
126
+ - `SHIELDPI_SDK_KEY` — SDK key for live monitoring
127
+ - `SHIELDPI_BASE_URL` — Override the API base URL (default: `https://api.shieldpi.io/api`)
128
+
129
+ ## License
130
+
131
+ Apache-2.0
@@ -0,0 +1,40 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "shieldpi"
7
+ version = "0.1.0"
8
+ description = "ShieldPi Watchtower — live agent monitoring SDK for Python"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "Apache-2.0" }
12
+ authors = [{ name = "ShieldPi", email = "support@shieldpi.io" }]
13
+ keywords = ["llm", "security", "agents", "monitoring", "ai-security"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: Apache Software License",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.9",
20
+ "Programming Language :: Python :: 3.10",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Topic :: Security",
24
+ ]
25
+ dependencies = [
26
+ "httpx>=0.24.0",
27
+ ]
28
+
29
+ [project.optional-dependencies]
30
+ langchain = ["langchain-core>=0.1.0"]
31
+ anthropic = ["anthropic>=0.25.0"]
32
+ dev = ["pytest>=7", "pytest-asyncio>=0.21"]
33
+
34
+ [project.urls]
35
+ Homepage = "https://shieldpi.io"
36
+ Documentation = "https://docs.shieldpi.io/sdks/python"
37
+ Repository = "https://github.com/ShieldPi1/shieldpi-watchtower"
38
+
39
+ [tool.setuptools.packages.find]
40
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,41 @@
1
+ """ShieldPi Watchtower — Python SDK.
2
+
3
+ Usage:
4
+
5
+ from shieldpi import Monitor
6
+
7
+ monitor = Monitor(sdk_key="shpi_live_...")
8
+ session = monitor.start_session(agent_name="invoice-bot", stated_goal="help users with invoices")
9
+
10
+ session.log_user_message("How do I file an invoice?")
11
+ session.log_tool_call("search_docs", {"query": "invoice filing"})
12
+ session.log_tool_result("search_docs", {"results": [...]})
13
+ session.log_final_response("Here's how to file an invoice...")
14
+
15
+ session.end()
16
+
17
+ For LangChain:
18
+
19
+ from shieldpi import Monitor
20
+ from shieldpi.hooks.langchain import ShieldPiCallbackHandler
21
+
22
+ monitor = Monitor(sdk_key="shpi_live_...")
23
+ callbacks = [ShieldPiCallbackHandler(monitor, agent_name="my-agent")]
24
+
25
+ agent.run("...", callbacks=callbacks)
26
+
27
+ For Anthropic tool use:
28
+
29
+ from shieldpi import Monitor
30
+ from shieldpi.hooks.anthropic import monitored_tool_use
31
+
32
+ monitor = Monitor(sdk_key="shpi_live_...")
33
+ with monitored_tool_use(monitor, agent_name="my-agent") as session:
34
+ result = anthropic_client.messages.create(...)
35
+ session.observe_anthropic_response(result)
36
+ """
37
+
38
+ from shieldpi.client import Monitor, Session, ShieldPiError
39
+
40
+ __version__ = "0.1.0"
41
+ __all__ = ["Monitor", "Session", "ShieldPiError", "__version__"]
@@ -0,0 +1,322 @@
1
+ """ShieldPi SDK core — Monitor + Session objects.
2
+
3
+ Design:
4
+ - ``Monitor`` is created once with an SDK key and a base URL.
5
+ - ``Session`` is created per agent conversation/task via ``monitor.start_session``.
6
+ - Every ``log_*`` call fires an async HTTP POST that never blocks the agent.
7
+ Failures are caught, counted, and (optionally) raised via a ``raise_on_error``
8
+ flag. The default is silent failure — we never break production because a
9
+ monitoring call timed out.
10
+ - Batching happens automatically for high-throughput agents via the
11
+ ``batch_size`` + ``flush_interval_s`` knobs on the ``Monitor``.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import atexit
17
+ import logging
18
+ import os
19
+ import queue
20
+ import threading
21
+ import time
22
+ from dataclasses import dataclass, field
23
+ from typing import Any, Optional
24
+
25
+ try:
26
+ import httpx
27
+ except ImportError as exc: # pragma: no cover
28
+ raise RuntimeError(
29
+ "shieldpi requires httpx — install with `pip install shieldpi`"
30
+ ) from exc
31
+
32
+
33
+ logger = logging.getLogger("shieldpi")
34
+
35
+
36
+ class ShieldPiError(RuntimeError):
37
+ """Base class for SDK errors."""
38
+
39
+
40
+ _DEFAULT_BASE_URL = "https://api.shieldpi.io/api/agent-monitor"
41
+ _USER_AGENT = "shieldpi-python/0.1.0"
42
+
43
+
44
+ # ── Event envelopes ─────────────────────────────────────────────────
45
+
46
+
47
+ @dataclass
48
+ class _QueuedEvent:
49
+ path: str # "/event" or "/events/batch"
50
+ body: dict
51
+ session: "Session"
52
+
53
+
54
+ # ── Monitor ─────────────────────────────────────────────────────────
55
+
56
+
57
+ class Monitor:
58
+ """SDK entrypoint. One instance per process.
59
+
60
+ Args:
61
+ sdk_key: Your per-target SDK key. Starts with ``shpi_live_``.
62
+ base_url: Override the API base URL (defaults to prod).
63
+ raise_on_error: If True, ingest failures raise ``ShieldPiError``.
64
+ Default False — monitoring must never break the agent.
65
+ batch_size: Max events to buffer before flushing.
66
+ flush_interval_s: Max seconds between flushes.
67
+ timeout_s: HTTP timeout for each request.
68
+ """
69
+
70
+ def __init__(
71
+ self,
72
+ sdk_key: Optional[str] = None,
73
+ *,
74
+ base_url: Optional[str] = None,
75
+ raise_on_error: bool = False,
76
+ batch_size: int = 20,
77
+ flush_interval_s: float = 1.0,
78
+ timeout_s: float = 5.0,
79
+ ) -> None:
80
+ self.sdk_key = sdk_key or os.environ.get("SHIELDPI_SDK_KEY", "")
81
+ if not self.sdk_key:
82
+ raise ShieldPiError(
83
+ "ShieldPi SDK key is required. Pass sdk_key= or set SHIELDPI_SDK_KEY"
84
+ )
85
+ self.base_url = (base_url or os.environ.get("SHIELDPI_BASE_URL") or _DEFAULT_BASE_URL).rstrip("/")
86
+ self.raise_on_error = raise_on_error
87
+ self.batch_size = max(1, batch_size)
88
+ self.flush_interval_s = max(0.1, flush_interval_s)
89
+ self.timeout_s = timeout_s
90
+
91
+ self._client = httpx.Client(
92
+ timeout=timeout_s,
93
+ headers={
94
+ "Authorization": f"Bearer {self.sdk_key}",
95
+ "User-Agent": _USER_AGENT,
96
+ "Content-Type": "application/json",
97
+ },
98
+ )
99
+ self._queue: queue.Queue[_QueuedEvent] = queue.Queue(maxsize=10_000)
100
+ self._shutdown = threading.Event()
101
+ self._worker = threading.Thread(
102
+ target=self._worker_loop, name="shieldpi-flusher", daemon=True
103
+ )
104
+ self._worker.start()
105
+ atexit.register(self.flush)
106
+ atexit.register(self.close)
107
+
108
+ # Counters
109
+ self.events_sent = 0
110
+ self.events_failed = 0
111
+
112
+ # ── Session API ────────────────────────────────────────────────
113
+
114
+ def start_session(
115
+ self,
116
+ *,
117
+ external_id: Optional[str] = None,
118
+ agent_name: Optional[str] = None,
119
+ framework: Optional[str] = None,
120
+ stated_goal: Optional[str] = None,
121
+ metadata: Optional[dict[str, Any]] = None,
122
+ ) -> "Session":
123
+ """Create a new live monitoring session. Returns a Session object."""
124
+ body = {
125
+ "external_id": external_id,
126
+ "agent_name": agent_name,
127
+ "framework": framework,
128
+ "stated_goal": stated_goal,
129
+ "metadata": metadata,
130
+ }
131
+ try:
132
+ resp = self._client.post(f"{self.base_url}/session/start", json=body)
133
+ resp.raise_for_status()
134
+ data = resp.json()
135
+ except Exception as exc:
136
+ if self.raise_on_error:
137
+ raise ShieldPiError(f"start_session failed: {exc}") from exc
138
+ logger.warning("[shieldpi] start_session failed, returning offline session: %s", exc)
139
+ return Session(self, session_id="offline", offline=True)
140
+ return Session(self, session_id=data["session_id"])
141
+
142
+ # ── Ingest ─────────────────────────────────────────────────────
143
+
144
+ def _enqueue(self, session: "Session", body: dict) -> None:
145
+ try:
146
+ self._queue.put_nowait(_QueuedEvent(path="/event", body=body, session=session))
147
+ except queue.Full:
148
+ self.events_failed += 1
149
+ logger.warning("[shieldpi] event queue full — dropping event")
150
+
151
+ # ── Worker thread ──────────────────────────────────────────────
152
+
153
+ def _worker_loop(self) -> None:
154
+ buffer: list[_QueuedEvent] = []
155
+ last_flush = time.time()
156
+ while not self._shutdown.is_set():
157
+ try:
158
+ timeout = max(0.05, self.flush_interval_s - (time.time() - last_flush))
159
+ item = self._queue.get(timeout=timeout)
160
+ buffer.append(item)
161
+ except queue.Empty:
162
+ pass
163
+ now = time.time()
164
+ if buffer and (
165
+ len(buffer) >= self.batch_size
166
+ or (now - last_flush) >= self.flush_interval_s
167
+ ):
168
+ self._flush_buffer(buffer)
169
+ buffer.clear()
170
+ last_flush = now
171
+ # Final flush on shutdown
172
+ if buffer:
173
+ self._flush_buffer(buffer)
174
+
175
+ def _flush_buffer(self, buffer: list[_QueuedEvent]) -> None:
176
+ # Group by single vs batch. Right now everything goes in a batch call.
177
+ body = {"events": [ev.body for ev in buffer]}
178
+ try:
179
+ resp = self._client.post(f"{self.base_url}/events/batch", json=body)
180
+ if resp.status_code >= 400:
181
+ self.events_failed += len(buffer)
182
+ logger.warning("[shieldpi] batch ingest %d: %s", resp.status_code, resp.text[:200])
183
+ return
184
+ self.events_sent += len(buffer)
185
+ except Exception as exc:
186
+ self.events_failed += len(buffer)
187
+ if self.raise_on_error:
188
+ raise ShieldPiError(f"ingest failed: {exc}") from exc
189
+ logger.warning("[shieldpi] ingest failed: %s", exc)
190
+
191
+ def flush(self) -> None:
192
+ """Force-drain the queue. Called on atexit."""
193
+ deadline = time.time() + 5.0
194
+ while not self._queue.empty() and time.time() < deadline:
195
+ time.sleep(0.05)
196
+
197
+ def close(self) -> None:
198
+ self._shutdown.set()
199
+ try:
200
+ self._worker.join(timeout=2.0)
201
+ except Exception:
202
+ pass
203
+ try:
204
+ self._client.close()
205
+ except Exception:
206
+ pass
207
+
208
+
209
+ # ── Session ─────────────────────────────────────────────────────────
210
+
211
+
212
+ class Session:
213
+ """A live monitoring session for one agent conversation or task.
214
+
215
+ Call ``log_*`` methods for each step the agent takes. They never block.
216
+ Call ``end()`` when the session is over (or let atexit handle it).
217
+ """
218
+
219
+ def __init__(self, monitor: Monitor, session_id: str, *, offline: bool = False) -> None:
220
+ self._monitor = monitor
221
+ self.session_id = session_id
222
+ self._offline = offline
223
+
224
+ # ── log_* helpers ──────────────────────────────────────────────
225
+
226
+ def log_user_message(self, content: str, *, metadata: Optional[dict] = None) -> None:
227
+ self._enqueue("user_message", content=content, actor="user", metadata=metadata)
228
+
229
+ def log_llm_call(self, content: str, *, metadata: Optional[dict] = None) -> None:
230
+ self._enqueue("llm_call", content=content, actor="assistant", metadata=metadata)
231
+
232
+ def log_final_response(self, content: str, *, metadata: Optional[dict] = None) -> None:
233
+ self._enqueue("final_response", content=content, actor="assistant", metadata=metadata)
234
+
235
+ def log_tool_call(
236
+ self,
237
+ tool_name: str,
238
+ tool_args: Optional[dict] = None,
239
+ *,
240
+ metadata: Optional[dict] = None,
241
+ ) -> None:
242
+ self._enqueue(
243
+ "tool_call",
244
+ tool_name=tool_name,
245
+ tool_args=tool_args,
246
+ actor="assistant",
247
+ metadata=metadata,
248
+ )
249
+
250
+ def log_tool_result(
251
+ self,
252
+ tool_name: str,
253
+ tool_result: Any,
254
+ *,
255
+ metadata: Optional[dict] = None,
256
+ ) -> None:
257
+ if tool_result is not None and not isinstance(tool_result, dict):
258
+ tool_result = {"value": tool_result}
259
+ self._enqueue(
260
+ "tool_result",
261
+ tool_name=tool_name,
262
+ tool_result=tool_result,
263
+ actor="tool",
264
+ metadata=metadata,
265
+ )
266
+
267
+ def log_memory_write(
268
+ self,
269
+ memory_key: str,
270
+ memory_value: str,
271
+ *,
272
+ metadata: Optional[dict] = None,
273
+ ) -> None:
274
+ self._enqueue(
275
+ "memory_write",
276
+ memory_key=memory_key,
277
+ memory_value=memory_value,
278
+ actor="system",
279
+ metadata=metadata,
280
+ )
281
+
282
+ def log_memory_read(
283
+ self,
284
+ memory_key: str,
285
+ *,
286
+ metadata: Optional[dict] = None,
287
+ ) -> None:
288
+ self._enqueue("memory_read", memory_key=memory_key, actor="system", metadata=metadata)
289
+
290
+ def log_error(self, message: str, *, metadata: Optional[dict] = None) -> None:
291
+ self._enqueue("error", content=message, actor="system", metadata=metadata)
292
+
293
+ def end(self, status: str = "ended") -> None:
294
+ if self._offline:
295
+ return
296
+ try:
297
+ resp = self._monitor._client.post(
298
+ f"{self._monitor.base_url}/session/end",
299
+ json={"session_id": self.session_id, "status": status},
300
+ )
301
+ resp.raise_for_status()
302
+ except Exception as exc:
303
+ if self._monitor.raise_on_error:
304
+ raise ShieldPiError(f"end_session failed: {exc}") from exc
305
+ logger.warning("[shieldpi] end_session failed: %s", exc)
306
+
307
+ # ── Internal ───────────────────────────────────────────────────
308
+
309
+ def _enqueue(self, event_type: str, **fields) -> None:
310
+ if self._offline:
311
+ return
312
+ body: dict[str, Any] = {"session_id": self.session_id, "event_type": event_type}
313
+ for k, v in fields.items():
314
+ if v is not None:
315
+ body[k] = v
316
+ self._monitor._enqueue(self, body)
317
+
318
+ def __enter__(self) -> "Session":
319
+ return self
320
+
321
+ def __exit__(self, exc_type, exc_val, exc_tb) -> None:
322
+ self.end(status="ended" if exc_type is None else "abandoned")
@@ -0,0 +1 @@
1
+ """Framework-specific instrumentation hooks for the ShieldPi SDK."""
@@ -0,0 +1,89 @@
1
+ """Anthropic tool-use instrumentation for ShieldPi.
2
+
3
+ Wraps the Anthropic Python SDK messages.create call so every tool_use block
4
+ and subsequent tool_result is logged to a ShieldPi session.
5
+
6
+ Usage:
7
+
8
+ from anthropic import Anthropic
9
+ from shieldpi import Monitor
10
+ from shieldpi.hooks.anthropic import monitored_tool_use
11
+
12
+ anth = Anthropic()
13
+ monitor = Monitor(sdk_key="shpi_live_...")
14
+
15
+ with monitored_tool_use(monitor, agent_name="invoice-bot") as session:
16
+ session.log_user_message("Help me file an invoice")
17
+ response = anth.messages.create(
18
+ model="claude-opus-4-20250514",
19
+ messages=[{"role": "user", "content": "Help me file an invoice"}],
20
+ tools=[...],
21
+ )
22
+ session.observe_anthropic_response(response)
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import logging
28
+ from contextlib import contextmanager
29
+ from typing import Any, Iterator, Optional
30
+
31
+ from shieldpi.client import Monitor, Session
32
+
33
+ logger = logging.getLogger("shieldpi.anthropic")
34
+
35
+
36
+ class _AnthropicAwareSession:
37
+ """Thin wrapper that adds ``observe_anthropic_response`` to a Session."""
38
+
39
+ def __init__(self, session: Session) -> None:
40
+ self._session = session
41
+
42
+ def __getattr__(self, name: str) -> Any:
43
+ return getattr(self._session, name)
44
+
45
+ def observe_anthropic_response(self, response: Any) -> None:
46
+ """Walk an Anthropic messages.create response and emit events."""
47
+ try:
48
+ blocks = getattr(response, "content", None) or []
49
+ for block in blocks:
50
+ btype = getattr(block, "type", None) or (block.get("type") if isinstance(block, dict) else None)
51
+ if btype == "text":
52
+ text = getattr(block, "text", None) or (block.get("text") if isinstance(block, dict) else "")
53
+ if text:
54
+ self._session.log_llm_call(text[:4000])
55
+ elif btype == "tool_use":
56
+ name = getattr(block, "name", None) or (block.get("name") if isinstance(block, dict) else "unknown_tool")
57
+ args = getattr(block, "input", None) or (block.get("input") if isinstance(block, dict) else {}) or {}
58
+ if not isinstance(args, dict):
59
+ args = {"input": str(args)}
60
+ self._session.log_tool_call(name, args)
61
+ except Exception as exc:
62
+ logger.warning("[shieldpi.anthropic] observe_anthropic_response failed: %s", exc)
63
+
64
+ def observe_tool_result(self, tool_name: str, result: Any) -> None:
65
+ """Call this after you execute a tool_use block locally."""
66
+ self._session.log_tool_result(tool_name, result)
67
+
68
+
69
+ @contextmanager
70
+ def monitored_tool_use(
71
+ monitor: Monitor,
72
+ *,
73
+ agent_name: Optional[str] = None,
74
+ stated_goal: Optional[str] = None,
75
+ external_id: Optional[str] = None,
76
+ ) -> Iterator[_AnthropicAwareSession]:
77
+ session = monitor.start_session(
78
+ external_id=external_id,
79
+ agent_name=agent_name,
80
+ framework="anthropic",
81
+ stated_goal=stated_goal,
82
+ )
83
+ wrapper = _AnthropicAwareSession(session)
84
+ try:
85
+ yield wrapper
86
+ session.end(status="ended")
87
+ except BaseException:
88
+ session.end(status="abandoned")
89
+ raise
@@ -0,0 +1,234 @@
1
+ """LangChain callback handler for ShieldPi.
2
+
3
+ Usage:
4
+
5
+ from shieldpi import Monitor
6
+ from shieldpi.hooks.langchain import ShieldPiCallbackHandler
7
+
8
+ monitor = Monitor(sdk_key="shpi_live_...")
9
+ handler = ShieldPiCallbackHandler(
10
+ monitor,
11
+ agent_name="invoice-bot",
12
+ stated_goal="help users file invoices",
13
+ )
14
+
15
+ agent.invoke({"input": "..."}, config={"callbacks": [handler]})
16
+
17
+ The handler creates one Session per chain run. It hooks into every LangChain
18
+ event (chat model start/end, tool start/end, agent action, agent finish) and
19
+ translates each into a ShieldPi event.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import logging
25
+ from typing import Any, Optional
26
+ from uuid import UUID
27
+
28
+ try:
29
+ from langchain_core.callbacks.base import BaseCallbackHandler
30
+ except ImportError as exc: # pragma: no cover
31
+ raise RuntimeError(
32
+ "shieldpi[langchain] requires langchain-core — install with "
33
+ "`pip install shieldpi[langchain]`"
34
+ ) from exc
35
+
36
+ from shieldpi.client import Monitor, Session
37
+
38
+ logger = logging.getLogger("shieldpi.langchain")
39
+
40
+
41
+ class ShieldPiCallbackHandler(BaseCallbackHandler):
42
+ """Instruments a LangChain run with a ShieldPi live monitoring session."""
43
+
44
+ def __init__(
45
+ self,
46
+ monitor: Monitor,
47
+ *,
48
+ agent_name: Optional[str] = None,
49
+ stated_goal: Optional[str] = None,
50
+ framework: str = "langchain",
51
+ external_id: Optional[str] = None,
52
+ ) -> None:
53
+ self.monitor = monitor
54
+ self.agent_name = agent_name
55
+ self.stated_goal = stated_goal
56
+ self.framework = framework
57
+ self.external_id = external_id
58
+ self._sessions: dict[UUID, Session] = {}
59
+
60
+ # ── Root run lifecycle ─────────────────────────────────────────
61
+
62
+ def _ensure_session(self, run_id: UUID) -> Session:
63
+ if run_id in self._sessions:
64
+ return self._sessions[run_id]
65
+ session = self.monitor.start_session(
66
+ external_id=self.external_id or str(run_id),
67
+ agent_name=self.agent_name,
68
+ framework=self.framework,
69
+ stated_goal=self.stated_goal,
70
+ )
71
+ self._sessions[run_id] = session
72
+ return session
73
+
74
+ def _finish_session(self, run_id: UUID, status: str = "ended") -> None:
75
+ session = self._sessions.pop(run_id, None)
76
+ if session is not None:
77
+ try:
78
+ session.end(status=status)
79
+ except Exception:
80
+ logger.warning("[shieldpi.langchain] end_session failed", exc_info=True)
81
+
82
+ # ── LangChain callbacks ────────────────────────────────────────
83
+
84
+ def on_chain_start(
85
+ self,
86
+ serialized: dict[str, Any],
87
+ inputs: dict[str, Any],
88
+ *,
89
+ run_id: UUID,
90
+ parent_run_id: Optional[UUID] = None,
91
+ **kwargs: Any,
92
+ ) -> None:
93
+ if parent_run_id is not None:
94
+ return # only the root chain creates a session
95
+ session = self._ensure_session(run_id)
96
+ user_input = inputs.get("input") or inputs.get("question") or str(inputs)
97
+ if isinstance(user_input, str) and user_input:
98
+ session.log_user_message(user_input)
99
+
100
+ def on_chain_end(
101
+ self,
102
+ outputs: dict[str, Any],
103
+ *,
104
+ run_id: UUID,
105
+ parent_run_id: Optional[UUID] = None,
106
+ **kwargs: Any,
107
+ ) -> None:
108
+ if parent_run_id is not None:
109
+ return
110
+ session = self._sessions.get(run_id)
111
+ if session is not None:
112
+ output = outputs.get("output") or outputs.get("answer") or str(outputs)
113
+ if isinstance(output, str) and output:
114
+ session.log_final_response(output)
115
+ self._finish_session(run_id, status="ended")
116
+
117
+ def on_chain_error(
118
+ self,
119
+ error: BaseException,
120
+ *,
121
+ run_id: UUID,
122
+ parent_run_id: Optional[UUID] = None,
123
+ **kwargs: Any,
124
+ ) -> None:
125
+ if parent_run_id is not None:
126
+ return
127
+ session = self._sessions.get(run_id)
128
+ if session is not None:
129
+ session.log_error(str(error))
130
+ self._finish_session(run_id, status="abandoned")
131
+
132
+ # ── LLM calls ──────────────────────────────────────────────────
133
+
134
+ def on_llm_start(
135
+ self,
136
+ serialized: dict[str, Any],
137
+ prompts: list[str],
138
+ *,
139
+ run_id: UUID,
140
+ parent_run_id: Optional[UUID] = None,
141
+ **kwargs: Any,
142
+ ) -> None:
143
+ root = self._root_run_id(run_id, parent_run_id)
144
+ session = self._sessions.get(root)
145
+ if session is None:
146
+ return
147
+ for prompt in prompts:
148
+ session.log_llm_call(prompt[:4000])
149
+
150
+ def on_chat_model_start(
151
+ self,
152
+ serialized: dict[str, Any],
153
+ messages: list[list[Any]],
154
+ *,
155
+ run_id: UUID,
156
+ parent_run_id: Optional[UUID] = None,
157
+ **kwargs: Any,
158
+ ) -> None:
159
+ root = self._root_run_id(run_id, parent_run_id)
160
+ session = self._sessions.get(root)
161
+ if session is None:
162
+ return
163
+ for batch in messages:
164
+ for m in batch:
165
+ content = getattr(m, "content", None) or str(m)
166
+ if isinstance(content, str) and content:
167
+ session.log_llm_call(content[:4000])
168
+
169
+ # ── Tool calls ─────────────────────────────────────────────────
170
+
171
+ def on_tool_start(
172
+ self,
173
+ serialized: dict[str, Any],
174
+ input_str: str,
175
+ *,
176
+ run_id: UUID,
177
+ parent_run_id: Optional[UUID] = None,
178
+ **kwargs: Any,
179
+ ) -> None:
180
+ root = self._root_run_id(run_id, parent_run_id)
181
+ session = self._sessions.get(root)
182
+ if session is None:
183
+ return
184
+ tool_name = (serialized or {}).get("name") or "unknown_tool"
185
+ args = kwargs.get("inputs") or {"input": input_str}
186
+ if not isinstance(args, dict):
187
+ args = {"input": str(args)}
188
+ session.log_tool_call(tool_name, args)
189
+
190
+ def on_tool_end(
191
+ self,
192
+ output: Any,
193
+ *,
194
+ run_id: UUID,
195
+ parent_run_id: Optional[UUID] = None,
196
+ **kwargs: Any,
197
+ ) -> None:
198
+ root = self._root_run_id(run_id, parent_run_id)
199
+ session = self._sessions.get(root)
200
+ if session is None:
201
+ return
202
+ tool_name = kwargs.get("name") or "unknown_tool"
203
+ session.log_tool_result(tool_name, output)
204
+
205
+ def on_tool_error(
206
+ self,
207
+ error: BaseException,
208
+ *,
209
+ run_id: UUID,
210
+ parent_run_id: Optional[UUID] = None,
211
+ **kwargs: Any,
212
+ ) -> None:
213
+ root = self._root_run_id(run_id, parent_run_id)
214
+ session = self._sessions.get(root)
215
+ if session is None:
216
+ return
217
+ session.log_error(f"Tool error: {error}")
218
+
219
+ # ── Helpers ────────────────────────────────────────────────────
220
+
221
+ def _root_run_id(self, run_id: UUID, parent_run_id: Optional[UUID]) -> UUID:
222
+ """Find the root run that owns the Session. LangChain nests runs;
223
+ we attach the Session to the outermost chain run_id only."""
224
+ # Simple heuristic: if run_id is a known session, use it; otherwise
225
+ # fall back to parent_run_id. Deeply nested agents may skip this and
226
+ # that's acceptable — events just drop silently for non-root runs.
227
+ if run_id in self._sessions:
228
+ return run_id
229
+ if parent_run_id is not None and parent_run_id in self._sessions:
230
+ return parent_run_id
231
+ # Last resort: attach to any active session
232
+ if self._sessions:
233
+ return next(iter(self._sessions.keys()))
234
+ return run_id
@@ -0,0 +1,161 @@
1
+ Metadata-Version: 2.4
2
+ Name: shieldpi
3
+ Version: 0.1.0
4
+ Summary: ShieldPi Watchtower — live agent monitoring SDK for Python
5
+ Author-email: ShieldPi <support@shieldpi.io>
6
+ License: Apache-2.0
7
+ Project-URL: Homepage, https://shieldpi.io
8
+ Project-URL: Documentation, https://docs.shieldpi.io/sdks/python
9
+ Project-URL: Repository, https://github.com/ShieldPi1/shieldpi-watchtower
10
+ Keywords: llm,security,agents,monitoring,ai-security
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: Apache Software License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Security
20
+ Requires-Python: >=3.9
21
+ Description-Content-Type: text/markdown
22
+ Requires-Dist: httpx>=0.24.0
23
+ Provides-Extra: langchain
24
+ Requires-Dist: langchain-core>=0.1.0; extra == "langchain"
25
+ Provides-Extra: anthropic
26
+ Requires-Dist: anthropic>=0.25.0; extra == "anthropic"
27
+ Provides-Extra: dev
28
+ Requires-Dist: pytest>=7; extra == "dev"
29
+ Requires-Dist: pytest-asyncio>=0.21; extra == "dev"
30
+
31
+ # ShieldPi Python SDK
32
+
33
+ Official Python SDK for the ShieldPi LLM Security Scanner API.
34
+
35
+ ## Installation
36
+
37
+ ```bash
38
+ pip install shieldpi
39
+ ```
40
+
41
+ ## Quick Start
42
+
43
+ ```python
44
+ from shieldpi import ShieldPi
45
+
46
+ client = ShieldPi(api_key="your-api-key")
47
+
48
+ # Create a target
49
+ target = client.targets.create(
50
+ name="My Chatbot",
51
+ url="https://my-chatbot.com/api/chat",
52
+ scan_mode="api"
53
+ )
54
+
55
+ # Run a scan
56
+ scan = client.scans.create(target_id=target.id)
57
+
58
+ # Wait for completion
59
+ result = client.scans.wait(scan.id)
60
+
61
+ # Get the report
62
+ print(f"Security Grade: {result.grade}")
63
+ print(f"Score: {result.score}/100")
64
+ print(f"Findings: {result.total_findings}")
65
+ ```
66
+
67
+ ## API Reference
68
+
69
+ ### Authentication
70
+ ```python
71
+ client = ShieldPi(api_key="sk-...", base_url="https://api.shieldpi.io/api")
72
+ ```
73
+
74
+ ### Targets
75
+ ```python
76
+ client.targets.list()
77
+ client.targets.create(name="...", url="...", scan_mode="model")
78
+ client.targets.get(target_id)
79
+ client.targets.delete(target_id)
80
+ ```
81
+
82
+ ### Scans
83
+ ```python
84
+ client.scans.create(target_id=target_id)
85
+ client.scans.get(scan_id)
86
+ client.scans.list()
87
+ client.scans.wait(scan_id, timeout=3600)
88
+ ```
89
+
90
+ ### Reports
91
+ ```python
92
+ client.scans.download_report(scan_id, format="pdf")
93
+ client.scans.download_report(scan_id, format="json")
94
+ ```
95
+
96
+ ## Live Agent Monitoring
97
+
98
+ For real-time agent monitoring, use the `Monitor` class:
99
+
100
+ ```python
101
+ from shieldpi import Monitor
102
+
103
+ monitor = Monitor(sdk_key="shpi_live_...")
104
+
105
+ with monitor.start_session(
106
+ agent_name="invoice-bot",
107
+ stated_goal="help users file invoices",
108
+ ) as session:
109
+ session.log_user_message("How do I file a Q1 invoice?")
110
+ session.log_tool_call("search_docs", {"query": "Q1 invoice filing"})
111
+ session.log_tool_result("search_docs", {"results": [...]})
112
+ session.log_final_response("Here's how to file a Q1 invoice...")
113
+ ```
114
+
115
+ ### LangChain Integration
116
+
117
+ ```python
118
+ from shieldpi import Monitor
119
+ from shieldpi.hooks.langchain import ShieldPiCallbackHandler
120
+
121
+ monitor = Monitor(sdk_key="shpi_live_...")
122
+ callbacks = [ShieldPiCallbackHandler(
123
+ monitor,
124
+ agent_name="my-agent",
125
+ stated_goal="help users with their accounts",
126
+ )]
127
+
128
+ agent.invoke({"input": "..."}, config={"callbacks": callbacks})
129
+ ```
130
+
131
+ ### Anthropic Tool Use
132
+
133
+ ```python
134
+ from anthropic import Anthropic
135
+ from shieldpi import Monitor
136
+ from shieldpi.hooks.anthropic import monitored_tool_use
137
+
138
+ anth = Anthropic()
139
+ monitor = Monitor(sdk_key="shpi_live_...")
140
+
141
+ with monitored_tool_use(monitor, agent_name="invoice-bot") as session:
142
+ session.log_user_message("Help me file an invoice")
143
+ response = anth.messages.create(
144
+ model="claude-opus-4-20250514",
145
+ messages=[{"role": "user", "content": "Help me file an invoice"}],
146
+ tools=[...],
147
+ )
148
+ session.observe_anthropic_response(response)
149
+ ```
150
+
151
+ ## Configuration
152
+
153
+ Environment variables:
154
+
155
+ - `SHIELDPI_API_KEY` — API key for scanner operations
156
+ - `SHIELDPI_SDK_KEY` — SDK key for live monitoring
157
+ - `SHIELDPI_BASE_URL` — Override the API base URL (default: `https://api.shieldpi.io/api`)
158
+
159
+ ## License
160
+
161
+ Apache-2.0
@@ -0,0 +1,12 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/shieldpi/__init__.py
4
+ src/shieldpi/client.py
5
+ src/shieldpi.egg-info/PKG-INFO
6
+ src/shieldpi.egg-info/SOURCES.txt
7
+ src/shieldpi.egg-info/dependency_links.txt
8
+ src/shieldpi.egg-info/requires.txt
9
+ src/shieldpi.egg-info/top_level.txt
10
+ src/shieldpi/hooks/__init__.py
11
+ src/shieldpi/hooks/anthropic.py
12
+ src/shieldpi/hooks/langchain.py
@@ -0,0 +1,11 @@
1
+ httpx>=0.24.0
2
+
3
+ [anthropic]
4
+ anthropic>=0.25.0
5
+
6
+ [dev]
7
+ pytest>=7
8
+ pytest-asyncio>=0.21
9
+
10
+ [langchain]
11
+ langchain-core>=0.1.0
@@ -0,0 +1 @@
1
+ shieldpi