tollgate-sdk 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.
- tollgate_sdk-0.1.0/.gitignore +115 -0
- tollgate_sdk-0.1.0/PKG-INFO +133 -0
- tollgate_sdk-0.1.0/README.md +122 -0
- tollgate_sdk-0.1.0/examples/README.md +75 -0
- tollgate_sdk-0.1.0/examples/requirements.txt +2 -0
- tollgate_sdk-0.1.0/examples/sample_policy.yaml +15 -0
- tollgate_sdk-0.1.0/examples/support_agent.py +193 -0
- tollgate_sdk-0.1.0/pyproject.toml +42 -0
- tollgate_sdk-0.1.0/src/tollgate/__init__.py +22 -0
- tollgate_sdk-0.1.0/src/tollgate/client.py +326 -0
- tollgate_sdk-0.1.0/src/tollgate/exceptions.py +47 -0
- tollgate_sdk-0.1.0/src/tollgate/models.py +31 -0
- tollgate_sdk-0.1.0/tests/__init__.py +0 -0
- tollgate_sdk-0.1.0/tests/conftest.py +30 -0
- tollgate_sdk-0.1.0/tests/test_async.py +101 -0
- tollgate_sdk-0.1.0/tests/test_check_action.py +140 -0
- tollgate_sdk-0.1.0/tests/test_context_manager.py +40 -0
- tollgate_sdk-0.1.0/tests/test_guard.py +153 -0
- tollgate_sdk-0.1.0/tests/test_integration.py +162 -0
- tollgate_sdk-0.1.0/uv.lock +595 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
.Python
|
|
7
|
+
build/
|
|
8
|
+
develop-eggs/
|
|
9
|
+
dist/
|
|
10
|
+
downloads/
|
|
11
|
+
eggs/
|
|
12
|
+
.eggs/
|
|
13
|
+
/lib/
|
|
14
|
+
/lib64/
|
|
15
|
+
parts/
|
|
16
|
+
sdist/
|
|
17
|
+
var/
|
|
18
|
+
wheels/
|
|
19
|
+
*.egg-info/
|
|
20
|
+
.installed.cfg
|
|
21
|
+
*.egg
|
|
22
|
+
|
|
23
|
+
# Virtual environments
|
|
24
|
+
.venv/
|
|
25
|
+
venv/
|
|
26
|
+
ENV/
|
|
27
|
+
env/
|
|
28
|
+
|
|
29
|
+
# Environment variables
|
|
30
|
+
.env
|
|
31
|
+
.env.local
|
|
32
|
+
.env.*.local
|
|
33
|
+
|
|
34
|
+
# IDE
|
|
35
|
+
.idea/
|
|
36
|
+
.vscode/
|
|
37
|
+
*.swp
|
|
38
|
+
*.swo
|
|
39
|
+
*~
|
|
40
|
+
.project
|
|
41
|
+
.pydevproject
|
|
42
|
+
.settings/
|
|
43
|
+
|
|
44
|
+
# Testing
|
|
45
|
+
.pytest_cache/
|
|
46
|
+
.coverage
|
|
47
|
+
htmlcov/
|
|
48
|
+
.tox/
|
|
49
|
+
.nox/
|
|
50
|
+
|
|
51
|
+
# mypy
|
|
52
|
+
.mypy_cache/
|
|
53
|
+
.dmypy.json
|
|
54
|
+
dmypy.json
|
|
55
|
+
|
|
56
|
+
# Alembic
|
|
57
|
+
*.db
|
|
58
|
+
|
|
59
|
+
# OS
|
|
60
|
+
.DS_Store
|
|
61
|
+
Thumbs.db
|
|
62
|
+
|
|
63
|
+
# Logs
|
|
64
|
+
*.log
|
|
65
|
+
logs/
|
|
66
|
+
# Node / TypeScript
|
|
67
|
+
node_modules/
|
|
68
|
+
dist/
|
|
69
|
+
build/
|
|
70
|
+
*.tsbuildinfo
|
|
71
|
+
.turbo/
|
|
72
|
+
.next/
|
|
73
|
+
.vercel/
|
|
74
|
+
out/
|
|
75
|
+
|
|
76
|
+
# Package manager files
|
|
77
|
+
npm-debug.log*
|
|
78
|
+
yarn-debug.log*
|
|
79
|
+
yarn-error.log*
|
|
80
|
+
pnpm-debug.log*
|
|
81
|
+
|
|
82
|
+
# Environment files (already partially covered — extending)
|
|
83
|
+
.env
|
|
84
|
+
.env.local
|
|
85
|
+
.env.*.local
|
|
86
|
+
!.env.example
|
|
87
|
+
|
|
88
|
+
# Editor / IDE
|
|
89
|
+
.vscode/
|
|
90
|
+
.idea/
|
|
91
|
+
*.swp
|
|
92
|
+
*.swo
|
|
93
|
+
.DS_Store
|
|
94
|
+
|
|
95
|
+
# Logs
|
|
96
|
+
*.log
|
|
97
|
+
logs/
|
|
98
|
+
|
|
99
|
+
# Test outputs
|
|
100
|
+
coverage/
|
|
101
|
+
.nyc_output/
|
|
102
|
+
|
|
103
|
+
# Python tooling
|
|
104
|
+
.mypy_cache/
|
|
105
|
+
.pytest_cache/
|
|
106
|
+
.ruff_cache/
|
|
107
|
+
*.egg-info/
|
|
108
|
+
|
|
109
|
+
# Build / distribution artifacts
|
|
110
|
+
*.pyc
|
|
111
|
+
*.pyo
|
|
112
|
+
|
|
113
|
+
# Local scratch files
|
|
114
|
+
/tmp/
|
|
115
|
+
*.local
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tollgate-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for the Tollgate agent control plane
|
|
5
|
+
Project-URL: Homepage, https://tollgate.dev
|
|
6
|
+
Project-URL: Repository, https://github.com/tollgate/tollgate
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Requires-Dist: httpx>=0.27
|
|
9
|
+
Requires-Dist: pydantic>=2.0
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# tollgate-sdk
|
|
13
|
+
|
|
14
|
+
Python SDK for [Tollgate](https://tollgate.dev) — the policy and approval layer for AI agents.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install tollgate-sdk
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## 5-minute quickstart
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
from tollgate import Tollgate
|
|
26
|
+
|
|
27
|
+
tg = Tollgate(
|
|
28
|
+
api_key="tg_live_...",
|
|
29
|
+
base_url="http://localhost:8000", # your Tollgate API
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
@tg.guard("issue_refund")
|
|
33
|
+
def issue_refund(amount: float, customer_id: str) -> dict:
|
|
34
|
+
# Your actual refund logic here
|
|
35
|
+
return {"refunded": amount, "customer": customer_id}
|
|
36
|
+
|
|
37
|
+
# Safe to call — Tollgate checks policy before executing
|
|
38
|
+
result = issue_refund(amount=50.0, customer_id="c_123")
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
When the policy says `require_approval`, the decorator blocks until a human approves or rejects in Slack.
|
|
42
|
+
|
|
43
|
+
## Usage patterns
|
|
44
|
+
|
|
45
|
+
### Decorator (recommended)
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
@tg.guard("issue_refund")
|
|
49
|
+
def issue_refund(amount: float, customer_id: str) -> dict:
|
|
50
|
+
return stripe.refund(amount=amount, customer=customer_id)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Context manager
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
with tg.check("issue_refund", {"amount": 500, "customer_id": "c_123"}):
|
|
57
|
+
stripe.refund(amount=500, customer="c_123")
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Explicit
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
decision = tg.check_action("issue_refund", {"amount": 500})
|
|
64
|
+
if decision.allowed:
|
|
65
|
+
stripe.refund(...)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Error handling
|
|
69
|
+
|
|
70
|
+
| Exception | When | How to handle |
|
|
71
|
+
|-----------|------|---------------|
|
|
72
|
+
| `ActionDenied` | Policy denied or human rejected | Abort the action, inform the user |
|
|
73
|
+
| `ActionPending` | Approval timed out | Retry later or escalate |
|
|
74
|
+
| `TollgateAuthError` | Invalid API key | Check your `api_key` |
|
|
75
|
+
| `TollgateConnectionError` | Can't reach Tollgate API | Check network; consider `fail_open=True` |
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from tollgate import ActionDenied, ActionPending, TollgateConnectionError
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
result = issue_refund(amount=500, customer_id="c_123")
|
|
82
|
+
except ActionDenied as e:
|
|
83
|
+
print(f"Refund not allowed: {e.reason}")
|
|
84
|
+
except ActionPending as e:
|
|
85
|
+
print(f"Timed out waiting for approval: {e.action_id}")
|
|
86
|
+
except TollgateConnectionError:
|
|
87
|
+
print("Tollgate unreachable")
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## fail_open
|
|
91
|
+
|
|
92
|
+
By default, if Tollgate is unreachable the SDK raises `TollgateConnectionError` (safe — nothing proceeds). Set `fail_open=True` to allow actions through when Tollgate is down:
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
tg = Tollgate(api_key="...", fail_open=True)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
**Risk:** If Tollgate is down, all actions proceed without policy checks. Only use `fail_open=True` when availability matters more than safety for your use case.
|
|
99
|
+
|
|
100
|
+
## Async usage
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
from tollgate import AsyncTollgate
|
|
104
|
+
|
|
105
|
+
tg = AsyncTollgate(api_key="tg_live_...")
|
|
106
|
+
|
|
107
|
+
@tg.aguard("issue_refund")
|
|
108
|
+
async def issue_refund(amount: float, customer_id: str) -> dict:
|
|
109
|
+
return await stripe_async.refund(amount=amount)
|
|
110
|
+
|
|
111
|
+
# In an async context:
|
|
112
|
+
result = await issue_refund(amount=50.0, customer_id="c_123")
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Configuration
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
tg = Tollgate(
|
|
119
|
+
api_key="tg_live_...",
|
|
120
|
+
base_url="https://api.tollgate.dev", # override for local dev
|
|
121
|
+
poll_interval=2.0, # seconds between approval polls
|
|
122
|
+
max_wait=300.0, # max seconds to wait for approval
|
|
123
|
+
on_pending=lambda action_id: print(f"Waiting for approval: {action_id}"),
|
|
124
|
+
fail_open=False, # raise on network errors (default)
|
|
125
|
+
timeout=10.0, # HTTP request timeout
|
|
126
|
+
)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Links
|
|
130
|
+
|
|
131
|
+
- [Dashboard](https://tollgate.dev)
|
|
132
|
+
- [Docs](https://docs.tollgate.dev)
|
|
133
|
+
- [API Reference](https://api.tollgate.dev/v1/docs)
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# tollgate-sdk
|
|
2
|
+
|
|
3
|
+
Python SDK for [Tollgate](https://tollgate.dev) — the policy and approval layer for AI agents.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install tollgate-sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## 5-minute quickstart
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from tollgate import Tollgate
|
|
15
|
+
|
|
16
|
+
tg = Tollgate(
|
|
17
|
+
api_key="tg_live_...",
|
|
18
|
+
base_url="http://localhost:8000", # your Tollgate API
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
@tg.guard("issue_refund")
|
|
22
|
+
def issue_refund(amount: float, customer_id: str) -> dict:
|
|
23
|
+
# Your actual refund logic here
|
|
24
|
+
return {"refunded": amount, "customer": customer_id}
|
|
25
|
+
|
|
26
|
+
# Safe to call — Tollgate checks policy before executing
|
|
27
|
+
result = issue_refund(amount=50.0, customer_id="c_123")
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
When the policy says `require_approval`, the decorator blocks until a human approves or rejects in Slack.
|
|
31
|
+
|
|
32
|
+
## Usage patterns
|
|
33
|
+
|
|
34
|
+
### Decorator (recommended)
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
@tg.guard("issue_refund")
|
|
38
|
+
def issue_refund(amount: float, customer_id: str) -> dict:
|
|
39
|
+
return stripe.refund(amount=amount, customer=customer_id)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Context manager
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
with tg.check("issue_refund", {"amount": 500, "customer_id": "c_123"}):
|
|
46
|
+
stripe.refund(amount=500, customer="c_123")
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Explicit
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
decision = tg.check_action("issue_refund", {"amount": 500})
|
|
53
|
+
if decision.allowed:
|
|
54
|
+
stripe.refund(...)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Error handling
|
|
58
|
+
|
|
59
|
+
| Exception | When | How to handle |
|
|
60
|
+
|-----------|------|---------------|
|
|
61
|
+
| `ActionDenied` | Policy denied or human rejected | Abort the action, inform the user |
|
|
62
|
+
| `ActionPending` | Approval timed out | Retry later or escalate |
|
|
63
|
+
| `TollgateAuthError` | Invalid API key | Check your `api_key` |
|
|
64
|
+
| `TollgateConnectionError` | Can't reach Tollgate API | Check network; consider `fail_open=True` |
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from tollgate import ActionDenied, ActionPending, TollgateConnectionError
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
result = issue_refund(amount=500, customer_id="c_123")
|
|
71
|
+
except ActionDenied as e:
|
|
72
|
+
print(f"Refund not allowed: {e.reason}")
|
|
73
|
+
except ActionPending as e:
|
|
74
|
+
print(f"Timed out waiting for approval: {e.action_id}")
|
|
75
|
+
except TollgateConnectionError:
|
|
76
|
+
print("Tollgate unreachable")
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## fail_open
|
|
80
|
+
|
|
81
|
+
By default, if Tollgate is unreachable the SDK raises `TollgateConnectionError` (safe — nothing proceeds). Set `fail_open=True` to allow actions through when Tollgate is down:
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
tg = Tollgate(api_key="...", fail_open=True)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
**Risk:** If Tollgate is down, all actions proceed without policy checks. Only use `fail_open=True` when availability matters more than safety for your use case.
|
|
88
|
+
|
|
89
|
+
## Async usage
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
from tollgate import AsyncTollgate
|
|
93
|
+
|
|
94
|
+
tg = AsyncTollgate(api_key="tg_live_...")
|
|
95
|
+
|
|
96
|
+
@tg.aguard("issue_refund")
|
|
97
|
+
async def issue_refund(amount: float, customer_id: str) -> dict:
|
|
98
|
+
return await stripe_async.refund(amount=amount)
|
|
99
|
+
|
|
100
|
+
# In an async context:
|
|
101
|
+
result = await issue_refund(amount=50.0, customer_id="c_123")
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Configuration
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
tg = Tollgate(
|
|
108
|
+
api_key="tg_live_...",
|
|
109
|
+
base_url="https://api.tollgate.dev", # override for local dev
|
|
110
|
+
poll_interval=2.0, # seconds between approval polls
|
|
111
|
+
max_wait=300.0, # max seconds to wait for approval
|
|
112
|
+
on_pending=lambda action_id: print(f"Waiting for approval: {action_id}"),
|
|
113
|
+
fail_open=False, # raise on network errors (default)
|
|
114
|
+
timeout=10.0, # HTTP request timeout
|
|
115
|
+
)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Links
|
|
119
|
+
|
|
120
|
+
- [Dashboard](https://tollgate.dev)
|
|
121
|
+
- [Docs](https://docs.tollgate.dev)
|
|
122
|
+
- [API Reference](https://api.tollgate.dev/v1/docs)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Support Agent Example
|
|
2
|
+
|
|
3
|
+
A customer support agent powered by Claude that uses Tollgate to gate risky actions.
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
- Tollgate API running locally (`uvicorn tollgate.main:app --reload --port 8000` from `apps/api/`)
|
|
8
|
+
- Docker running (Postgres + Redis via `docker-compose up -d` from repo root)
|
|
9
|
+
- Slack workspace connected (see `apps/api/README.md` → Slack setup)
|
|
10
|
+
- ngrok tunnel active (`ngrok http 8000`)
|
|
11
|
+
|
|
12
|
+
## Step 1: Create an agent
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
# Sign up and get a JWT
|
|
16
|
+
JWT=$(curl -s -X POST http://localhost:8000/auth/signup \
|
|
17
|
+
-H "Content-Type: application/json" \
|
|
18
|
+
-d '{"email":"demo@example.com","password":"demo-pw-123","org_name":"Demo Org"}' \
|
|
19
|
+
| python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])")
|
|
20
|
+
|
|
21
|
+
# Create the agent
|
|
22
|
+
curl -s -X POST http://localhost:8000/agents \
|
|
23
|
+
-H "Authorization: Bearer $JWT" \
|
|
24
|
+
-H "Content-Type: application/json" \
|
|
25
|
+
-d '{"name":"support-agent-demo"}'
|
|
26
|
+
# Save the api_key from the response — you won't see it again
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Step 2: Upload the sample policy
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
AGENT_ID="<your-agent-id>"
|
|
33
|
+
POLICY=$(cat sample_policy.yaml)
|
|
34
|
+
|
|
35
|
+
curl -s -X POST "http://localhost:8000/agents/$AGENT_ID/policies" \
|
|
36
|
+
-H "Authorization: Bearer $JWT" \
|
|
37
|
+
-H "Content-Type: application/json" \
|
|
38
|
+
-d "{\"source_yaml\": $(python3 -c "import sys,json; print(json.dumps(open('sample_policy.yaml').read()))")}"
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The policy allows refunds ≤ $100 automatically, requires Slack approval for refunds > $100, denies `delete_account`, and allows everything else.
|
|
42
|
+
|
|
43
|
+
## Step 3: Set environment variables
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
export TOLLGATE_API_KEY="tg_live_..." # from Step 1
|
|
47
|
+
export TOLLGATE_BASE_URL="http://localhost:8000"
|
|
48
|
+
export ANTHROPIC_API_KEY="sk-ant-..." # your Anthropic API key
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Step 4: Run the agent
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pip install -r requirements.txt
|
|
55
|
+
python support_agent.py
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Step 5: Try it
|
|
59
|
+
|
|
60
|
+
Ask the agent to perform actions:
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
You: Issue a $50 refund for customer c_001
|
|
64
|
+
```
|
|
65
|
+
→ Allowed immediately (under $100 threshold)
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
You: Issue a $500 refund for customer c_002
|
|
69
|
+
```
|
|
70
|
+
→ Pauses and posts to your Slack `#approvals` channel. Check Slack, click Approve or Reject.
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
You: Delete account for customer c_003
|
|
74
|
+
```
|
|
75
|
+
→ Denied immediately by policy.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
version: 1
|
|
2
|
+
rules:
|
|
3
|
+
- action: issue_refund
|
|
4
|
+
when:
|
|
5
|
+
amount: { lte: 100 }
|
|
6
|
+
decide: allow
|
|
7
|
+
- action: issue_refund
|
|
8
|
+
when:
|
|
9
|
+
amount: { gt: 100 }
|
|
10
|
+
decide: require_approval
|
|
11
|
+
approvers: ["#approvals"]
|
|
12
|
+
- action: delete_account
|
|
13
|
+
decide: deny
|
|
14
|
+
reason: "Account deletion is not permitted via agent"
|
|
15
|
+
default: allow
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tollgate Example: AI Support Agent
|
|
3
|
+
|
|
4
|
+
A support agent backed by Claude that uses Tollgate to gate risky actions.
|
|
5
|
+
Three tools are protected: issue_refund, update_account, escalate_to_human.
|
|
6
|
+
|
|
7
|
+
Run: python support_agent.py
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import sys
|
|
12
|
+
|
|
13
|
+
import anthropic
|
|
14
|
+
|
|
15
|
+
from tollgate import ActionDenied, ActionPending, Tollgate
|
|
16
|
+
|
|
17
|
+
TOLLGATE_API_KEY = os.environ.get("TOLLGATE_API_KEY", "")
|
|
18
|
+
TOLLGATE_BASE_URL = os.environ.get("TOLLGATE_BASE_URL", "http://localhost:8000")
|
|
19
|
+
ANTHROPIC_API_KEY = os.environ.get("ANTHROPIC_API_KEY", "")
|
|
20
|
+
|
|
21
|
+
if not TOLLGATE_API_KEY:
|
|
22
|
+
print("Error: TOLLGATE_API_KEY is not set.")
|
|
23
|
+
sys.exit(1)
|
|
24
|
+
if not ANTHROPIC_API_KEY:
|
|
25
|
+
print("Error: ANTHROPIC_API_KEY is not set.")
|
|
26
|
+
sys.exit(1)
|
|
27
|
+
|
|
28
|
+
tg = Tollgate(
|
|
29
|
+
api_key=TOLLGATE_API_KEY,
|
|
30
|
+
base_url=TOLLGATE_BASE_URL,
|
|
31
|
+
on_pending=lambda action_id: print(f"\n[Tollgate] Waiting for approval in Slack... (action_id={action_id})"),
|
|
32
|
+
poll_interval=3.0,
|
|
33
|
+
max_wait=300.0,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
client = anthropic.Anthropic(api_key=ANTHROPIC_API_KEY)
|
|
37
|
+
|
|
38
|
+
TOOLS = [
|
|
39
|
+
{
|
|
40
|
+
"name": "issue_refund",
|
|
41
|
+
"description": "Issue a refund to a customer for a specified amount.",
|
|
42
|
+
"input_schema": {
|
|
43
|
+
"type": "object",
|
|
44
|
+
"properties": {
|
|
45
|
+
"amount": {"type": "number", "description": "Refund amount in USD"},
|
|
46
|
+
"customer_id": {"type": "string", "description": "Customer identifier"},
|
|
47
|
+
"reason": {"type": "string", "description": "Reason for the refund"},
|
|
48
|
+
},
|
|
49
|
+
"required": ["amount", "customer_id"],
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"name": "update_account",
|
|
54
|
+
"description": "Update a customer's account information.",
|
|
55
|
+
"input_schema": {
|
|
56
|
+
"type": "object",
|
|
57
|
+
"properties": {
|
|
58
|
+
"customer_id": {"type": "string", "description": "Customer identifier"},
|
|
59
|
+
"field": {"type": "string", "description": "Field to update (email, plan, etc.)"},
|
|
60
|
+
"value": {"type": "string", "description": "New value for the field"},
|
|
61
|
+
},
|
|
62
|
+
"required": ["customer_id", "field", "value"],
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"name": "escalate_to_human",
|
|
67
|
+
"description": "Escalate this support case to a human agent.",
|
|
68
|
+
"input_schema": {
|
|
69
|
+
"type": "object",
|
|
70
|
+
"properties": {
|
|
71
|
+
"customer_id": {"type": "string", "description": "Customer identifier"},
|
|
72
|
+
"reason": {"type": "string", "description": "Reason for escalation"},
|
|
73
|
+
"priority": {"type": "string", "enum": ["low", "medium", "high"], "description": "Priority level"},
|
|
74
|
+
},
|
|
75
|
+
"required": ["customer_id", "reason"],
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@tg.guard("issue_refund")
|
|
82
|
+
def issue_refund(amount: float, customer_id: str, reason: str = "") -> dict: # type: ignore[return]
|
|
83
|
+
"""Execute the refund (would call Stripe in production)."""
|
|
84
|
+
print(f"[Tool] Issuing refund: ${amount} to {customer_id}")
|
|
85
|
+
return {"status": "refunded", "amount": amount, "customer_id": customer_id}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@tg.guard("update_account")
|
|
89
|
+
def update_account(customer_id: str, field: str, value: str) -> dict: # type: ignore[return]
|
|
90
|
+
"""Execute the account update (would call your DB in production)."""
|
|
91
|
+
print(f"[Tool] Updating {field}={value} for {customer_id}")
|
|
92
|
+
return {"status": "updated", "customer_id": customer_id, "field": field, "value": value}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@tg.guard("escalate_to_human")
|
|
96
|
+
def escalate_to_human(customer_id: str, reason: str, priority: str = "medium") -> dict: # type: ignore[return]
|
|
97
|
+
"""Escalate to a human agent."""
|
|
98
|
+
print(f"[Tool] Escalating {customer_id} — {reason} (priority={priority})")
|
|
99
|
+
return {"status": "escalated", "customer_id": customer_id, "ticket_id": "TKT-001"}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def run_tool(name: str, inputs: dict) -> str: # type: ignore[type-arg]
|
|
103
|
+
"""Dispatch a tool call, catching Tollgate exceptions."""
|
|
104
|
+
try:
|
|
105
|
+
if name == "issue_refund":
|
|
106
|
+
result = issue_refund(**inputs)
|
|
107
|
+
elif name == "update_account":
|
|
108
|
+
result = update_account(**inputs)
|
|
109
|
+
elif name == "escalate_to_human":
|
|
110
|
+
result = escalate_to_human(**inputs)
|
|
111
|
+
else:
|
|
112
|
+
return f"Unknown tool: {name}"
|
|
113
|
+
return str(result)
|
|
114
|
+
except ActionDenied as e:
|
|
115
|
+
return f"[Tollgate] Action denied: {e.reason}"
|
|
116
|
+
except ActionPending as e:
|
|
117
|
+
return f"[Tollgate] Approval timed out after {e.timeout_seconds}s for action {e.action_id}"
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def chat_loop() -> None:
|
|
121
|
+
"""Run the support agent loop."""
|
|
122
|
+
print("Tollgate Support Agent — type 'quit' to exit")
|
|
123
|
+
print(f"Connected to Tollgate at {TOLLGATE_BASE_URL}\n")
|
|
124
|
+
messages: list[dict] = [] # type: ignore[type-arg]
|
|
125
|
+
|
|
126
|
+
while True:
|
|
127
|
+
try:
|
|
128
|
+
user_input = input("You: ").strip()
|
|
129
|
+
except (EOFError, KeyboardInterrupt):
|
|
130
|
+
print("\nGoodbye.")
|
|
131
|
+
break
|
|
132
|
+
|
|
133
|
+
if user_input.lower() in ("quit", "exit", "q"):
|
|
134
|
+
print("Goodbye.")
|
|
135
|
+
break
|
|
136
|
+
if not user_input:
|
|
137
|
+
continue
|
|
138
|
+
|
|
139
|
+
messages.append({"role": "user", "content": user_input})
|
|
140
|
+
|
|
141
|
+
response = client.messages.create(
|
|
142
|
+
model="claude-haiku-4-5-20251001",
|
|
143
|
+
max_tokens=1024,
|
|
144
|
+
system=(
|
|
145
|
+
"You are a helpful customer support agent. "
|
|
146
|
+
"Use the available tools to help customers with refunds, account updates, and escalations. "
|
|
147
|
+
"Always confirm the customer_id before taking actions."
|
|
148
|
+
),
|
|
149
|
+
tools=TOOLS, # type: ignore[arg-type]
|
|
150
|
+
messages=messages, # type: ignore[arg-type]
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
while response.stop_reason == "tool_use":
|
|
154
|
+
tool_results = []
|
|
155
|
+
assistant_content = []
|
|
156
|
+
|
|
157
|
+
for block in response.content:
|
|
158
|
+
assistant_content.append(block)
|
|
159
|
+
if block.type == "tool_use":
|
|
160
|
+
print(f"\n[Agent] Calling {block.name}({block.input})")
|
|
161
|
+
result = run_tool(block.name, block.input) # type: ignore[arg-type]
|
|
162
|
+
tool_results.append({
|
|
163
|
+
"type": "tool_result",
|
|
164
|
+
"tool_use_id": block.id,
|
|
165
|
+
"content": result,
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
messages.append({"role": "assistant", "content": assistant_content})
|
|
169
|
+
messages.append({"role": "user", "content": tool_results})
|
|
170
|
+
|
|
171
|
+
response = client.messages.create(
|
|
172
|
+
model="claude-haiku-4-5-20251001",
|
|
173
|
+
max_tokens=1024,
|
|
174
|
+
system=(
|
|
175
|
+
"You are a helpful customer support agent. "
|
|
176
|
+
"Use the available tools to help customers with refunds, account updates, and escalations."
|
|
177
|
+
),
|
|
178
|
+
tools=TOOLS, # type: ignore[arg-type]
|
|
179
|
+
messages=messages, # type: ignore[arg-type]
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
# Extract final text response
|
|
183
|
+
reply = ""
|
|
184
|
+
for block in response.content:
|
|
185
|
+
if hasattr(block, "text"):
|
|
186
|
+
reply += block.text
|
|
187
|
+
|
|
188
|
+
messages.append({"role": "assistant", "content": response.content})
|
|
189
|
+
print(f"\nAgent: {reply}\n")
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
if __name__ == "__main__":
|
|
193
|
+
chat_loop()
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "tollgate-sdk"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Python SDK for the Tollgate agent control plane"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"httpx>=0.27",
|
|
13
|
+
"pydantic>=2.0",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[project.urls]
|
|
17
|
+
Homepage = "https://tollgate.dev"
|
|
18
|
+
Repository = "https://github.com/tollgate/tollgate"
|
|
19
|
+
|
|
20
|
+
[tool.hatch.build.targets.wheel]
|
|
21
|
+
packages = ["src/tollgate"]
|
|
22
|
+
|
|
23
|
+
[dependency-groups]
|
|
24
|
+
dev = [
|
|
25
|
+
"mypy>=1.8",
|
|
26
|
+
"pytest>=8.0",
|
|
27
|
+
"pytest-asyncio>=0.23",
|
|
28
|
+
"respx>=0.21",
|
|
29
|
+
"anyio>=4.0",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[tool.pytest.ini_options]
|
|
33
|
+
asyncio_mode = "auto"
|
|
34
|
+
testpaths = ["tests"]
|
|
35
|
+
|
|
36
|
+
[tool.mypy]
|
|
37
|
+
strict = true
|
|
38
|
+
python_version = "3.10"
|
|
39
|
+
mypy_path = "src"
|
|
40
|
+
|
|
41
|
+
[tool.ruff.lint]
|
|
42
|
+
select = ["E", "F", "I"]
|