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.
- shieldpi-0.1.0/PKG-INFO +161 -0
- shieldpi-0.1.0/README.md +131 -0
- shieldpi-0.1.0/pyproject.toml +40 -0
- shieldpi-0.1.0/setup.cfg +4 -0
- shieldpi-0.1.0/src/shieldpi/__init__.py +41 -0
- shieldpi-0.1.0/src/shieldpi/client.py +322 -0
- shieldpi-0.1.0/src/shieldpi/hooks/__init__.py +1 -0
- shieldpi-0.1.0/src/shieldpi/hooks/anthropic.py +89 -0
- shieldpi-0.1.0/src/shieldpi/hooks/langchain.py +234 -0
- shieldpi-0.1.0/src/shieldpi.egg-info/PKG-INFO +161 -0
- shieldpi-0.1.0/src/shieldpi.egg-info/SOURCES.txt +12 -0
- shieldpi-0.1.0/src/shieldpi.egg-info/dependency_links.txt +1 -0
- shieldpi-0.1.0/src/shieldpi.egg-info/requires.txt +11 -0
- shieldpi-0.1.0/src/shieldpi.egg-info/top_level.txt +1 -0
shieldpi-0.1.0/PKG-INFO
ADDED
|
@@ -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
|
shieldpi-0.1.0/README.md
ADDED
|
@@ -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"]
|
shieldpi-0.1.0/setup.cfg
ADDED
|
@@ -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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
shieldpi
|