provecart-adk 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.
- provecart_adk-0.1.0/LICENSE +21 -0
- provecart_adk-0.1.0/PKG-INFO +138 -0
- provecart_adk-0.1.0/README.md +118 -0
- provecart_adk-0.1.0/pyproject.toml +29 -0
- provecart_adk-0.1.0/setup.cfg +4 -0
- provecart_adk-0.1.0/src/provecart_adk/__init__.py +283 -0
- provecart_adk-0.1.0/src/provecart_adk.egg-info/PKG-INFO +138 -0
- provecart_adk-0.1.0/src/provecart_adk.egg-info/SOURCES.txt +10 -0
- provecart_adk-0.1.0/src/provecart_adk.egg-info/dependency_links.txt +1 -0
- provecart_adk-0.1.0/src/provecart_adk.egg-info/requires.txt +6 -0
- provecart_adk-0.1.0/src/provecart_adk.egg-info/top_level.txt +1 -0
- provecart_adk-0.1.0/tests/test_provecart_adk.py +137 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ProveCart
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: provecart-adk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: ProveCart pre-action hook for Google ADK: check an agent's order, booking or reservation against the customer's signed mandate before a purchase tool runs.
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://provecart.com
|
|
7
|
+
Project-URL: Repository, https://github.com/provecart/provecart
|
|
8
|
+
Keywords: ai-agents,agent-commerce,google-adk,provecart,pre-payment-verification
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
12
|
+
Requires-Python: >=3.9
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Provides-Extra: adk
|
|
16
|
+
Requires-Dist: google-adk; extra == "adk"
|
|
17
|
+
Provides-Extra: test
|
|
18
|
+
Requires-Dist: pytest; extra == "test"
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# provecart-adk
|
|
22
|
+
|
|
23
|
+
A pre-action hook for Google ADK agents, from ProveCart.
|
|
24
|
+
|
|
25
|
+
An agent that buys for someone needs one check in the right place:
|
|
26
|
+
after it has built the order, and before any money moves. This
|
|
27
|
+
package puts that check on ADK's tool boundary. When a purchase
|
|
28
|
+
tool is about to run, the hook reads the pending order, booking or
|
|
29
|
+
reservation out of the tool arguments, compares it with the
|
|
30
|
+
customer's signed mandate, and only then decides whether the tool
|
|
31
|
+
call goes ahead.
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install provecart-adk
|
|
35
|
+
pip install "provecart-adk[adk]" # also installs google-adk
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## How it works
|
|
39
|
+
|
|
40
|
+
ProveCart returns one of three verdicts:
|
|
41
|
+
|
|
42
|
+
- **MATCH** — the tool runs. The callback returns `None`, which is
|
|
43
|
+
ADK's signal to proceed as normal.
|
|
44
|
+
- **BLOCK** — the tool does not run. ADK's documented refusal path
|
|
45
|
+
applies: a `before_tool_callback` that returns a dict skips the
|
|
46
|
+
tool call and that dict becomes the tool result. The dict carries
|
|
47
|
+
the verdict, the record id and the corrections, so the agent can
|
|
48
|
+
apply a fix that stays inside the customer's existing
|
|
49
|
+
authorization and check again.
|
|
50
|
+
- **REVIEW** — the tool does not run, and the refusal dict tells
|
|
51
|
+
the agent to stop and ask the customer. A review is never turned
|
|
52
|
+
into a guess.
|
|
53
|
+
|
|
54
|
+
A check that fails, a verdict that is not recognized, and a BLOCK
|
|
55
|
+
whose corrections use an unknown action all escalate the same way.
|
|
56
|
+
Nothing falls through to the purchase tool by accident.
|
|
57
|
+
|
|
58
|
+
## Use it two ways
|
|
59
|
+
|
|
60
|
+
As an agent callback:
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from provecart_adk import ProveCartGuard, make_before_tool_callback
|
|
64
|
+
|
|
65
|
+
guard = ProveCartGuard(client=my_provecart_client)
|
|
66
|
+
agent = LlmAgent(
|
|
67
|
+
name="shopper",
|
|
68
|
+
tools=[place_order],
|
|
69
|
+
before_tool_callback=make_before_tool_callback(guard),
|
|
70
|
+
)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Or as a plugin, registered once so it covers every agent and tool
|
|
74
|
+
in the app:
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from provecart_adk import ProveCartPlugin
|
|
78
|
+
|
|
79
|
+
plugin = ProveCartPlugin(client=my_provecart_client)
|
|
80
|
+
# Runner(..., plugins=[plugin]) or App(..., plugins=[plugin])
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## The client is yours to inject
|
|
84
|
+
|
|
85
|
+
This package does not hard-wire an HTTP client. Pass any object
|
|
86
|
+
with these methods, each returning a dict with `verdict`,
|
|
87
|
+
`corrections`, `record_id` and `max_attempts`:
|
|
88
|
+
|
|
89
|
+
- `check_cart(**payload)`
|
|
90
|
+
- `check_booking(**payload)`
|
|
91
|
+
- `check_reservation(**payload)`
|
|
92
|
+
|
|
93
|
+
If that object also provides
|
|
94
|
+
`route_verdict(response) -> (decision, detail)`, with decision one
|
|
95
|
+
of `proceed`, `fix` or `escalate`, the hook uses it. Otherwise the
|
|
96
|
+
routing built into this package applies: MATCH proceeds; BLOCK
|
|
97
|
+
becomes `fix` only when every correction is a known action; BLOCK
|
|
98
|
+
without usable corrections, REVIEW and anything unknown escalate.
|
|
99
|
+
A client exposing only `verify_cart` is adapted for cart checks.
|
|
100
|
+
|
|
101
|
+
## Which tools are watched
|
|
102
|
+
|
|
103
|
+
By default the guard watches common purchase names — `place_order`,
|
|
104
|
+
`checkout`, `capture_payment` and friends for carts; `book_flight`,
|
|
105
|
+
`create_booking` for bookings; `make_reservation`, `reserve_table`
|
|
106
|
+
for reservations — and ignores every other tool without calling the
|
|
107
|
+
client. Pass your own map to watch exactly your tool names:
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
guard = ProveCartGuard(
|
|
111
|
+
client=my_provecart_client,
|
|
112
|
+
tool_kinds={"submit_payment": "cart", "hold_room": "booking"},
|
|
113
|
+
)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Payloads are taken from the tool arguments as-is, or from a nested
|
|
117
|
+
`cart` / `booking` / `reservation` dict when the tool wraps its
|
|
118
|
+
order that way. Pass an `extractor(kind, args) -> dict` callable if
|
|
119
|
+
your tools shape their arguments differently.
|
|
120
|
+
|
|
121
|
+
## Offline example and tests
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
python examples/mock_example.py # MOCK mode, injected fake client
|
|
125
|
+
python -m pytest tests/ -q # offline; ADK is stubbed if absent
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
The example defaults to mock mode and makes no network calls on
|
|
129
|
+
import or on a normal run. Live use is an explicit opt-in: build
|
|
130
|
+
your real client and inject it, as the example marks.
|
|
131
|
+
|
|
132
|
+
## Notes
|
|
133
|
+
|
|
134
|
+
Published by ProveCart, a verification service for agent commerce
|
|
135
|
+
based in the Bay Area. Patent pending.
|
|
136
|
+
|
|
137
|
+
Docs: https://provecart.com/portal/docs ·
|
|
138
|
+
Source: https://github.com/provecart/provecart/tree/main/packages/provecart-adk
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# provecart-adk
|
|
2
|
+
|
|
3
|
+
A pre-action hook for Google ADK agents, from ProveCart.
|
|
4
|
+
|
|
5
|
+
An agent that buys for someone needs one check in the right place:
|
|
6
|
+
after it has built the order, and before any money moves. This
|
|
7
|
+
package puts that check on ADK's tool boundary. When a purchase
|
|
8
|
+
tool is about to run, the hook reads the pending order, booking or
|
|
9
|
+
reservation out of the tool arguments, compares it with the
|
|
10
|
+
customer's signed mandate, and only then decides whether the tool
|
|
11
|
+
call goes ahead.
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install provecart-adk
|
|
15
|
+
pip install "provecart-adk[adk]" # also installs google-adk
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## How it works
|
|
19
|
+
|
|
20
|
+
ProveCart returns one of three verdicts:
|
|
21
|
+
|
|
22
|
+
- **MATCH** — the tool runs. The callback returns `None`, which is
|
|
23
|
+
ADK's signal to proceed as normal.
|
|
24
|
+
- **BLOCK** — the tool does not run. ADK's documented refusal path
|
|
25
|
+
applies: a `before_tool_callback` that returns a dict skips the
|
|
26
|
+
tool call and that dict becomes the tool result. The dict carries
|
|
27
|
+
the verdict, the record id and the corrections, so the agent can
|
|
28
|
+
apply a fix that stays inside the customer's existing
|
|
29
|
+
authorization and check again.
|
|
30
|
+
- **REVIEW** — the tool does not run, and the refusal dict tells
|
|
31
|
+
the agent to stop and ask the customer. A review is never turned
|
|
32
|
+
into a guess.
|
|
33
|
+
|
|
34
|
+
A check that fails, a verdict that is not recognized, and a BLOCK
|
|
35
|
+
whose corrections use an unknown action all escalate the same way.
|
|
36
|
+
Nothing falls through to the purchase tool by accident.
|
|
37
|
+
|
|
38
|
+
## Use it two ways
|
|
39
|
+
|
|
40
|
+
As an agent callback:
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from provecart_adk import ProveCartGuard, make_before_tool_callback
|
|
44
|
+
|
|
45
|
+
guard = ProveCartGuard(client=my_provecart_client)
|
|
46
|
+
agent = LlmAgent(
|
|
47
|
+
name="shopper",
|
|
48
|
+
tools=[place_order],
|
|
49
|
+
before_tool_callback=make_before_tool_callback(guard),
|
|
50
|
+
)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Or as a plugin, registered once so it covers every agent and tool
|
|
54
|
+
in the app:
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from provecart_adk import ProveCartPlugin
|
|
58
|
+
|
|
59
|
+
plugin = ProveCartPlugin(client=my_provecart_client)
|
|
60
|
+
# Runner(..., plugins=[plugin]) or App(..., plugins=[plugin])
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## The client is yours to inject
|
|
64
|
+
|
|
65
|
+
This package does not hard-wire an HTTP client. Pass any object
|
|
66
|
+
with these methods, each returning a dict with `verdict`,
|
|
67
|
+
`corrections`, `record_id` and `max_attempts`:
|
|
68
|
+
|
|
69
|
+
- `check_cart(**payload)`
|
|
70
|
+
- `check_booking(**payload)`
|
|
71
|
+
- `check_reservation(**payload)`
|
|
72
|
+
|
|
73
|
+
If that object also provides
|
|
74
|
+
`route_verdict(response) -> (decision, detail)`, with decision one
|
|
75
|
+
of `proceed`, `fix` or `escalate`, the hook uses it. Otherwise the
|
|
76
|
+
routing built into this package applies: MATCH proceeds; BLOCK
|
|
77
|
+
becomes `fix` only when every correction is a known action; BLOCK
|
|
78
|
+
without usable corrections, REVIEW and anything unknown escalate.
|
|
79
|
+
A client exposing only `verify_cart` is adapted for cart checks.
|
|
80
|
+
|
|
81
|
+
## Which tools are watched
|
|
82
|
+
|
|
83
|
+
By default the guard watches common purchase names — `place_order`,
|
|
84
|
+
`checkout`, `capture_payment` and friends for carts; `book_flight`,
|
|
85
|
+
`create_booking` for bookings; `make_reservation`, `reserve_table`
|
|
86
|
+
for reservations — and ignores every other tool without calling the
|
|
87
|
+
client. Pass your own map to watch exactly your tool names:
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
guard = ProveCartGuard(
|
|
91
|
+
client=my_provecart_client,
|
|
92
|
+
tool_kinds={"submit_payment": "cart", "hold_room": "booking"},
|
|
93
|
+
)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Payloads are taken from the tool arguments as-is, or from a nested
|
|
97
|
+
`cart` / `booking` / `reservation` dict when the tool wraps its
|
|
98
|
+
order that way. Pass an `extractor(kind, args) -> dict` callable if
|
|
99
|
+
your tools shape their arguments differently.
|
|
100
|
+
|
|
101
|
+
## Offline example and tests
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
python examples/mock_example.py # MOCK mode, injected fake client
|
|
105
|
+
python -m pytest tests/ -q # offline; ADK is stubbed if absent
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
The example defaults to mock mode and makes no network calls on
|
|
109
|
+
import or on a normal run. Live use is an explicit opt-in: build
|
|
110
|
+
your real client and inject it, as the example marks.
|
|
111
|
+
|
|
112
|
+
## Notes
|
|
113
|
+
|
|
114
|
+
Published by ProveCart, a verification service for agent commerce
|
|
115
|
+
based in the Bay Area. Patent pending.
|
|
116
|
+
|
|
117
|
+
Docs: https://provecart.com/portal/docs ·
|
|
118
|
+
Source: https://github.com/provecart/provecart/tree/main/packages/provecart-adk
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "provecart-adk"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "ProveCart pre-action hook for Google ADK: check an agent's order, booking or reservation against the customer's signed mandate before a purchase tool runs."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = {text = "MIT"}
|
|
12
|
+
keywords = ["ai-agents", "agent-commerce", "google-adk", "provecart", "pre-payment-verification"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"License :: OSI Approved :: MIT License",
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Topic :: Software Development :: Libraries",
|
|
17
|
+
]
|
|
18
|
+
dependencies = []
|
|
19
|
+
|
|
20
|
+
[project.optional-dependencies]
|
|
21
|
+
adk = ["google-adk"]
|
|
22
|
+
test = ["pytest"]
|
|
23
|
+
|
|
24
|
+
[project.urls]
|
|
25
|
+
Homepage = "https://provecart.com"
|
|
26
|
+
Repository = "https://github.com/provecart/provecart"
|
|
27
|
+
|
|
28
|
+
[tool.setuptools.packages.find]
|
|
29
|
+
where = ["src"]
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
"""ProveCart pre-action hook for Google ADK.
|
|
2
|
+
|
|
3
|
+
Intercepts the moment before a purchase tool runs, checks the
|
|
4
|
+
pending order, booking or reservation against the customer's signed
|
|
5
|
+
mandate, and lets the tool run only on a MATCH.
|
|
6
|
+
|
|
7
|
+
Verdict routing (same semantics as the shared ProveCart client):
|
|
8
|
+
|
|
9
|
+
MATCH -> proceed (return None; ADK runs the tool)
|
|
10
|
+
BLOCK + known fixes -> do not run the tool; return a refusal dict
|
|
11
|
+
carrying the corrections, so the agent can
|
|
12
|
+
apply only fixes inside the customer's
|
|
13
|
+
existing authorization and re-check
|
|
14
|
+
BLOCK otherwise,
|
|
15
|
+
REVIEW, unknown -> do not run the tool; return a refusal dict
|
|
16
|
+
that escalates to the customer
|
|
17
|
+
|
|
18
|
+
In ADK, returning a dict from ``before_tool_callback`` skips the
|
|
19
|
+
tool call and uses that dict as the tool result. That documented
|
|
20
|
+
refusal path is what this package uses for every non-proceed case.
|
|
21
|
+
|
|
22
|
+
The client is injected and duck-typed. Any object with
|
|
23
|
+
``check_cart`` / ``check_booking`` / ``check_reservation`` methods
|
|
24
|
+
returning a dict with ``verdict``, ``corrections``, ``record_id``
|
|
25
|
+
and ``max_attempts`` works, as does an object exposing
|
|
26
|
+
``route_verdict(response) -> (decision, detail)``. If the client
|
|
27
|
+
supplies its own ``route_verdict`` it is used; otherwise the routing
|
|
28
|
+
in this module is used. A client with only the older
|
|
29
|
+
``verify_cart`` shape is adapted for cart checks.
|
|
30
|
+
|
|
31
|
+
No network call happens on import.
|
|
32
|
+
"""
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
from typing import Any, Callable, Dict, Mapping, Optional, Tuple
|
|
36
|
+
|
|
37
|
+
__version__ = "0.1.0"
|
|
38
|
+
__all__ = [
|
|
39
|
+
"KNOWN_ACTIONS",
|
|
40
|
+
"DEFAULT_TOOL_KINDS",
|
|
41
|
+
"ProveCartGuard",
|
|
42
|
+
"FakeProveCartClient",
|
|
43
|
+
"make_before_tool_callback",
|
|
44
|
+
"route_verdict",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
KNOWN_ACTIONS = {
|
|
48
|
+
"remove_item", "replace_item", "change_variant", "reduce_quantity",
|
|
49
|
+
"reduce_total", "change_currency", "correct_traveler_name",
|
|
50
|
+
"change_airport", "change_travel_date", "change_departure_time",
|
|
51
|
+
"add_itinerary", "rebuild_itinerary", "change_cabin",
|
|
52
|
+
"change_fare_terms", "add_hotel_stay", "change_room",
|
|
53
|
+
"change_provider", "change_location", "change_date", "change_time",
|
|
54
|
+
"change_party_size", "correct_reservation_name", "reduce_deposit",
|
|
55
|
+
"change_cancellation_terms",
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
# Default purchase-tool names -> check kind. Override per guard.
|
|
59
|
+
DEFAULT_TOOL_KINDS: Dict[str, str] = {
|
|
60
|
+
"place_order": "cart",
|
|
61
|
+
"checkout": "cart",
|
|
62
|
+
"create_order": "cart",
|
|
63
|
+
"capture_payment": "cart",
|
|
64
|
+
"pay": "cart",
|
|
65
|
+
"purchase": "cart",
|
|
66
|
+
"buy": "cart",
|
|
67
|
+
"create_booking": "booking",
|
|
68
|
+
"book_flight": "booking",
|
|
69
|
+
"book_hotel": "booking",
|
|
70
|
+
"book": "booking",
|
|
71
|
+
"make_reservation": "reservation",
|
|
72
|
+
"create_reservation": "reservation",
|
|
73
|
+
"reserve_table": "reservation",
|
|
74
|
+
"reserve": "reservation",
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
_CHECK_METHOD = {
|
|
78
|
+
"cart": "check_cart",
|
|
79
|
+
"booking": "check_booking",
|
|
80
|
+
"reservation": "check_reservation",
|
|
81
|
+
}
|
|
82
|
+
_PAYLOAD_KEYS = {"cart": "cart", "booking": "booking", "reservation": "reservation"}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def route_verdict(response: Mapping[str, Any]) -> Tuple[str, Dict[str, Any]]:
|
|
86
|
+
"""Return (decision, detail); decision in {proceed, fix, escalate}.
|
|
87
|
+
|
|
88
|
+
'fix' is returned only when the verdict is BLOCK and every
|
|
89
|
+
correction is a known action. Anything else non-MATCH escalates.
|
|
90
|
+
"""
|
|
91
|
+
verdict = (response or {}).get("verdict")
|
|
92
|
+
if verdict == "MATCH":
|
|
93
|
+
return "proceed", {"record_id": response.get("record_id")}
|
|
94
|
+
if verdict == "BLOCK":
|
|
95
|
+
corrections = response.get("corrections") or []
|
|
96
|
+
if corrections and all(
|
|
97
|
+
isinstance(c, Mapping) and c.get("action") in KNOWN_ACTIONS
|
|
98
|
+
for c in corrections
|
|
99
|
+
):
|
|
100
|
+
return "fix", {
|
|
101
|
+
"corrections": list(corrections),
|
|
102
|
+
"record_id": response.get("record_id"),
|
|
103
|
+
"max_attempts": response.get("max_attempts"),
|
|
104
|
+
}
|
|
105
|
+
return "escalate", {
|
|
106
|
+
"reason": "BLOCK without usable corrections",
|
|
107
|
+
"record_id": response.get("record_id"),
|
|
108
|
+
}
|
|
109
|
+
return "escalate", {
|
|
110
|
+
"reason": f"verdict {verdict} needs the buyer",
|
|
111
|
+
"record_id": response.get("record_id"),
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _tool_name(tool: Any) -> str:
|
|
116
|
+
if isinstance(tool, str):
|
|
117
|
+
return tool
|
|
118
|
+
return getattr(tool, "name", "") or str(tool)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class ProveCartGuard:
|
|
122
|
+
"""Framework-agnostic core; the ADK adapters below call it."""
|
|
123
|
+
|
|
124
|
+
def __init__(
|
|
125
|
+
self,
|
|
126
|
+
client: Any,
|
|
127
|
+
tool_kinds: Optional[Mapping[str, str]] = None,
|
|
128
|
+
extractor: Optional[Callable[[str, Dict[str, Any]], Dict[str, Any]]] = None,
|
|
129
|
+
):
|
|
130
|
+
if client is None:
|
|
131
|
+
raise ValueError("ProveCartGuard requires an injected client")
|
|
132
|
+
self.client = client
|
|
133
|
+
self.tool_kinds = dict(tool_kinds) if tool_kinds is not None else dict(DEFAULT_TOOL_KINDS)
|
|
134
|
+
self.extractor = extractor
|
|
135
|
+
|
|
136
|
+
def kind_for(self, tool: Any) -> Optional[str]:
|
|
137
|
+
return self.tool_kinds.get(_tool_name(tool))
|
|
138
|
+
|
|
139
|
+
def extract(self, kind: str, args: Dict[str, Any]) -> Dict[str, Any]:
|
|
140
|
+
if self.extractor is not None:
|
|
141
|
+
return dict(self.extractor(kind, dict(args)))
|
|
142
|
+
nested = args.get(_PAYLOAD_KEYS[kind])
|
|
143
|
+
if isinstance(nested, Mapping):
|
|
144
|
+
payload = dict(nested)
|
|
145
|
+
# Mandate / linkage fields may sit beside the nested payload.
|
|
146
|
+
for key in ("mandate_id", "previous_record_id"):
|
|
147
|
+
if key in args and key not in payload:
|
|
148
|
+
payload[key] = args[key]
|
|
149
|
+
return payload
|
|
150
|
+
return dict(args)
|
|
151
|
+
|
|
152
|
+
def check(self, kind: str, payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
153
|
+
method_name = _CHECK_METHOD[kind]
|
|
154
|
+
method = getattr(self.client, method_name, None)
|
|
155
|
+
if callable(method):
|
|
156
|
+
return dict(method(**payload))
|
|
157
|
+
if kind == "cart" and callable(getattr(self.client, "verify_cart", None)):
|
|
158
|
+
return dict(self.client.verify_cart(**payload))
|
|
159
|
+
raise TypeError(f"client does not provide {method_name}()")
|
|
160
|
+
|
|
161
|
+
def route(self, response: Dict[str, Any]) -> Tuple[str, Dict[str, Any]]:
|
|
162
|
+
client_route = getattr(self.client, "route_verdict", None)
|
|
163
|
+
if callable(client_route):
|
|
164
|
+
decision, detail = client_route(response)
|
|
165
|
+
return str(decision), dict(detail or {})
|
|
166
|
+
return route_verdict(response)
|
|
167
|
+
|
|
168
|
+
def before_tool(self, tool: Any, args: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
|
|
169
|
+
"""Return None to let the tool run, or a refusal dict to skip it."""
|
|
170
|
+
kind = self.kind_for(tool)
|
|
171
|
+
if kind is None:
|
|
172
|
+
return None
|
|
173
|
+
payload = self.extract(kind, dict(args or {}))
|
|
174
|
+
try:
|
|
175
|
+
response = self.check(kind, payload)
|
|
176
|
+
except Exception as exc: # a failed check never lets a purchase through
|
|
177
|
+
return self._refusal(kind, tool, None, "escalate",
|
|
178
|
+
{"reason": f"check failed: {exc}"})
|
|
179
|
+
decision, detail = self.route(response)
|
|
180
|
+
if decision == "proceed":
|
|
181
|
+
return None
|
|
182
|
+
return self._refusal(kind, tool, response, decision, detail)
|
|
183
|
+
|
|
184
|
+
@staticmethod
|
|
185
|
+
def _refusal(kind, tool, response, decision, detail):
|
|
186
|
+
response = response or {}
|
|
187
|
+
return {
|
|
188
|
+
"provecart_blocked": True,
|
|
189
|
+
"provecart_decision": decision,
|
|
190
|
+
"verdict": response.get("verdict"),
|
|
191
|
+
"check_kind": kind,
|
|
192
|
+
"tool": _tool_name(tool),
|
|
193
|
+
"corrections": response.get("corrections") or detail.get("corrections") or [],
|
|
194
|
+
"record_id": response.get("record_id") or detail.get("record_id"),
|
|
195
|
+
"max_attempts": response.get("max_attempts") or detail.get("max_attempts"),
|
|
196
|
+
"detail": detail,
|
|
197
|
+
"error": (
|
|
198
|
+
"ProveCart check did not return MATCH, so the purchase "
|
|
199
|
+
"tool was not run. On 'fix', apply only the listed "
|
|
200
|
+
"corrections inside the customer's existing authorization "
|
|
201
|
+
"and re-check with previous_record_id. On 'escalate', "
|
|
202
|
+
"stop and ask the customer."
|
|
203
|
+
),
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def make_before_tool_callback(guard: ProveCartGuard):
|
|
208
|
+
"""Build an ADK agent-level ``before_tool_callback``.
|
|
209
|
+
|
|
210
|
+
Documented ADK signature:
|
|
211
|
+
``(tool, args, tool_context) -> Optional[dict]`` — returning None
|
|
212
|
+
lets the tool run; returning a dict skips the tool and that dict
|
|
213
|
+
becomes the tool result.
|
|
214
|
+
"""
|
|
215
|
+
def before_tool_callback(tool=None, args=None, tool_context=None, **_kw):
|
|
216
|
+
return guard.before_tool(tool, args or {})
|
|
217
|
+
|
|
218
|
+
return before_tool_callback
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
try: # Real plugin base when google-adk is installed; stub-safe otherwise.
|
|
222
|
+
from google.adk.plugins.base_plugin import BasePlugin as _BasePlugin
|
|
223
|
+
_HAS_ADK = True
|
|
224
|
+
except Exception: # pragma: no cover - exercised in stubbed environments
|
|
225
|
+
_BasePlugin = object # type: ignore
|
|
226
|
+
_HAS_ADK = False
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class ProveCartPlugin(_BasePlugin): # type: ignore[misc]
|
|
230
|
+
"""ADK plugin form. Register once on the Runner / App.
|
|
231
|
+
|
|
232
|
+
Plugin signature in current ADK:
|
|
233
|
+
``async before_tool_callback(*, tool, tool_args, tool_context)``
|
|
234
|
+
returning None to proceed or a dict to skip the tool. Older /
|
|
235
|
+
alternate callers may pass ``args`` instead of ``tool_args``;
|
|
236
|
+
both are accepted.
|
|
237
|
+
"""
|
|
238
|
+
|
|
239
|
+
def __init__(self, guard: Optional[ProveCartGuard] = None, *,
|
|
240
|
+
client: Any = None, **guard_kwargs):
|
|
241
|
+
if _HAS_ADK:
|
|
242
|
+
try:
|
|
243
|
+
super().__init__(name="provecart")
|
|
244
|
+
except TypeError:
|
|
245
|
+
super().__init__()
|
|
246
|
+
self.guard = guard or ProveCartGuard(client, **guard_kwargs)
|
|
247
|
+
|
|
248
|
+
async def before_tool_callback(self, *, tool=None, tool_args=None,
|
|
249
|
+
args=None, tool_context=None, **_kw):
|
|
250
|
+
payload = tool_args if tool_args is not None else args
|
|
251
|
+
return self.guard.before_tool(tool, payload or {})
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
class FakeProveCartClient:
|
|
255
|
+
"""Offline fake for examples and tests. No network, ever.
|
|
256
|
+
|
|
257
|
+
Pass a fixed response dict, or a callable payload -> response.
|
|
258
|
+
Records every call in ``.calls`` as (kind, payload).
|
|
259
|
+
"""
|
|
260
|
+
|
|
261
|
+
def __init__(self, response=None):
|
|
262
|
+
self._response = response if response is not None else {
|
|
263
|
+
"verdict": "MATCH", "corrections": [], "record_id": "rec_mock_1",
|
|
264
|
+
"max_attempts": 3,
|
|
265
|
+
}
|
|
266
|
+
self.calls = []
|
|
267
|
+
|
|
268
|
+
def _handle(self, kind, **payload):
|
|
269
|
+
self.calls.append((kind, payload))
|
|
270
|
+
if callable(self._response):
|
|
271
|
+
return dict(self._response(payload))
|
|
272
|
+
return dict(self._response)
|
|
273
|
+
|
|
274
|
+
def check_cart(self, **payload):
|
|
275
|
+
return self._handle("cart", **payload)
|
|
276
|
+
|
|
277
|
+
def check_booking(self, **payload):
|
|
278
|
+
return self._handle("booking", **payload)
|
|
279
|
+
|
|
280
|
+
def check_reservation(self, **payload):
|
|
281
|
+
return self._handle("reservation", **payload)
|
|
282
|
+
|
|
283
|
+
# Deliberately no route_verdict: exercises this module's routing.
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: provecart-adk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: ProveCart pre-action hook for Google ADK: check an agent's order, booking or reservation against the customer's signed mandate before a purchase tool runs.
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://provecart.com
|
|
7
|
+
Project-URL: Repository, https://github.com/provecart/provecart
|
|
8
|
+
Keywords: ai-agents,agent-commerce,google-adk,provecart,pre-payment-verification
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
12
|
+
Requires-Python: >=3.9
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Provides-Extra: adk
|
|
16
|
+
Requires-Dist: google-adk; extra == "adk"
|
|
17
|
+
Provides-Extra: test
|
|
18
|
+
Requires-Dist: pytest; extra == "test"
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# provecart-adk
|
|
22
|
+
|
|
23
|
+
A pre-action hook for Google ADK agents, from ProveCart.
|
|
24
|
+
|
|
25
|
+
An agent that buys for someone needs one check in the right place:
|
|
26
|
+
after it has built the order, and before any money moves. This
|
|
27
|
+
package puts that check on ADK's tool boundary. When a purchase
|
|
28
|
+
tool is about to run, the hook reads the pending order, booking or
|
|
29
|
+
reservation out of the tool arguments, compares it with the
|
|
30
|
+
customer's signed mandate, and only then decides whether the tool
|
|
31
|
+
call goes ahead.
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install provecart-adk
|
|
35
|
+
pip install "provecart-adk[adk]" # also installs google-adk
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## How it works
|
|
39
|
+
|
|
40
|
+
ProveCart returns one of three verdicts:
|
|
41
|
+
|
|
42
|
+
- **MATCH** — the tool runs. The callback returns `None`, which is
|
|
43
|
+
ADK's signal to proceed as normal.
|
|
44
|
+
- **BLOCK** — the tool does not run. ADK's documented refusal path
|
|
45
|
+
applies: a `before_tool_callback` that returns a dict skips the
|
|
46
|
+
tool call and that dict becomes the tool result. The dict carries
|
|
47
|
+
the verdict, the record id and the corrections, so the agent can
|
|
48
|
+
apply a fix that stays inside the customer's existing
|
|
49
|
+
authorization and check again.
|
|
50
|
+
- **REVIEW** — the tool does not run, and the refusal dict tells
|
|
51
|
+
the agent to stop and ask the customer. A review is never turned
|
|
52
|
+
into a guess.
|
|
53
|
+
|
|
54
|
+
A check that fails, a verdict that is not recognized, and a BLOCK
|
|
55
|
+
whose corrections use an unknown action all escalate the same way.
|
|
56
|
+
Nothing falls through to the purchase tool by accident.
|
|
57
|
+
|
|
58
|
+
## Use it two ways
|
|
59
|
+
|
|
60
|
+
As an agent callback:
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from provecart_adk import ProveCartGuard, make_before_tool_callback
|
|
64
|
+
|
|
65
|
+
guard = ProveCartGuard(client=my_provecart_client)
|
|
66
|
+
agent = LlmAgent(
|
|
67
|
+
name="shopper",
|
|
68
|
+
tools=[place_order],
|
|
69
|
+
before_tool_callback=make_before_tool_callback(guard),
|
|
70
|
+
)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Or as a plugin, registered once so it covers every agent and tool
|
|
74
|
+
in the app:
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from provecart_adk import ProveCartPlugin
|
|
78
|
+
|
|
79
|
+
plugin = ProveCartPlugin(client=my_provecart_client)
|
|
80
|
+
# Runner(..., plugins=[plugin]) or App(..., plugins=[plugin])
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## The client is yours to inject
|
|
84
|
+
|
|
85
|
+
This package does not hard-wire an HTTP client. Pass any object
|
|
86
|
+
with these methods, each returning a dict with `verdict`,
|
|
87
|
+
`corrections`, `record_id` and `max_attempts`:
|
|
88
|
+
|
|
89
|
+
- `check_cart(**payload)`
|
|
90
|
+
- `check_booking(**payload)`
|
|
91
|
+
- `check_reservation(**payload)`
|
|
92
|
+
|
|
93
|
+
If that object also provides
|
|
94
|
+
`route_verdict(response) -> (decision, detail)`, with decision one
|
|
95
|
+
of `proceed`, `fix` or `escalate`, the hook uses it. Otherwise the
|
|
96
|
+
routing built into this package applies: MATCH proceeds; BLOCK
|
|
97
|
+
becomes `fix` only when every correction is a known action; BLOCK
|
|
98
|
+
without usable corrections, REVIEW and anything unknown escalate.
|
|
99
|
+
A client exposing only `verify_cart` is adapted for cart checks.
|
|
100
|
+
|
|
101
|
+
## Which tools are watched
|
|
102
|
+
|
|
103
|
+
By default the guard watches common purchase names — `place_order`,
|
|
104
|
+
`checkout`, `capture_payment` and friends for carts; `book_flight`,
|
|
105
|
+
`create_booking` for bookings; `make_reservation`, `reserve_table`
|
|
106
|
+
for reservations — and ignores every other tool without calling the
|
|
107
|
+
client. Pass your own map to watch exactly your tool names:
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
guard = ProveCartGuard(
|
|
111
|
+
client=my_provecart_client,
|
|
112
|
+
tool_kinds={"submit_payment": "cart", "hold_room": "booking"},
|
|
113
|
+
)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Payloads are taken from the tool arguments as-is, or from a nested
|
|
117
|
+
`cart` / `booking` / `reservation` dict when the tool wraps its
|
|
118
|
+
order that way. Pass an `extractor(kind, args) -> dict` callable if
|
|
119
|
+
your tools shape their arguments differently.
|
|
120
|
+
|
|
121
|
+
## Offline example and tests
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
python examples/mock_example.py # MOCK mode, injected fake client
|
|
125
|
+
python -m pytest tests/ -q # offline; ADK is stubbed if absent
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
The example defaults to mock mode and makes no network calls on
|
|
129
|
+
import or on a normal run. Live use is an explicit opt-in: build
|
|
130
|
+
your real client and inject it, as the example marks.
|
|
131
|
+
|
|
132
|
+
## Notes
|
|
133
|
+
|
|
134
|
+
Published by ProveCart, a verification service for agent commerce
|
|
135
|
+
based in the Bay Area. Patent pending.
|
|
136
|
+
|
|
137
|
+
Docs: https://provecart.com/portal/docs ·
|
|
138
|
+
Source: https://github.com/provecart/provecart/tree/main/packages/provecart-adk
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/provecart_adk/__init__.py
|
|
5
|
+
src/provecart_adk.egg-info/PKG-INFO
|
|
6
|
+
src/provecart_adk.egg-info/SOURCES.txt
|
|
7
|
+
src/provecart_adk.egg-info/dependency_links.txt
|
|
8
|
+
src/provecart_adk.egg-info/requires.txt
|
|
9
|
+
src/provecart_adk.egg-info/top_level.txt
|
|
10
|
+
tests/test_provecart_adk.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
provecart_adk
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""Offline tests for provecart-adk. No network; ADK stubbed if absent."""
|
|
2
|
+
import asyncio
|
|
3
|
+
import sys
|
|
4
|
+
import types
|
|
5
|
+
|
|
6
|
+
# Stub google.adk plugin base if the real package is not installed,
|
|
7
|
+
# so the plugin class can be imported and exercised offline.
|
|
8
|
+
try:
|
|
9
|
+
import google.adk.plugins.base_plugin # noqa: F401
|
|
10
|
+
HAS_ADK = True
|
|
11
|
+
except Exception:
|
|
12
|
+
HAS_ADK = False
|
|
13
|
+
google = types.ModuleType("google")
|
|
14
|
+
adk = types.ModuleType("google.adk")
|
|
15
|
+
plugins = types.ModuleType("google.adk.plugins")
|
|
16
|
+
base_plugin = types.ModuleType("google.adk.plugins.base_plugin")
|
|
17
|
+
|
|
18
|
+
class BasePlugin:
|
|
19
|
+
def __init__(self, name=None):
|
|
20
|
+
self.name = name
|
|
21
|
+
|
|
22
|
+
base_plugin.BasePlugin = BasePlugin
|
|
23
|
+
plugins.base_plugin = base_plugin
|
|
24
|
+
adk.plugins = plugins
|
|
25
|
+
google.adk = adk
|
|
26
|
+
sys.modules.setdefault("google", google)
|
|
27
|
+
sys.modules["google.adk"] = adk
|
|
28
|
+
sys.modules["google.adk.plugins"] = plugins
|
|
29
|
+
sys.modules["google.adk.plugins.base_plugin"] = base_plugin
|
|
30
|
+
|
|
31
|
+
sys.path.insert(0, "src")
|
|
32
|
+
|
|
33
|
+
from provecart_adk import ( # noqa: E402
|
|
34
|
+
FakeProveCartClient, ProveCartGuard, ProveCartPlugin,
|
|
35
|
+
make_before_tool_callback, route_verdict,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class Tool:
|
|
40
|
+
def __init__(self, name):
|
|
41
|
+
self.name = name
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
CART_ARGS = {"mandate_id": "m1", "merchant": {"name": "Shop"},
|
|
45
|
+
"items": [{"name": "Chair"}], "total_price": 10}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_route_match_proceeds():
|
|
49
|
+
assert route_verdict({"verdict": "MATCH", "record_id": "r1"})[0] == "proceed"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_route_block_known_fix():
|
|
53
|
+
d, detail = route_verdict({"verdict": "BLOCK", "record_id": "r",
|
|
54
|
+
"corrections": [{"action": "remove_item"}],
|
|
55
|
+
"max_attempts": 3})
|
|
56
|
+
assert d == "fix" and detail["max_attempts"] == 3
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_route_block_unknown_escalates():
|
|
60
|
+
d, _ = route_verdict({"verdict": "BLOCK",
|
|
61
|
+
"corrections": [{"action": "widen_budget"}]})
|
|
62
|
+
assert d == "escalate"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_route_review_and_unknown_escalate():
|
|
66
|
+
assert route_verdict({"verdict": "REVIEW"})[0] == "escalate"
|
|
67
|
+
assert route_verdict({"verdict": "???"})[0] == "escalate"
|
|
68
|
+
assert route_verdict({})[0] == "escalate"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_non_purchase_tool_passes_without_check():
|
|
72
|
+
fake = FakeProveCartClient()
|
|
73
|
+
guard = ProveCartGuard(fake)
|
|
74
|
+
assert guard.before_tool(Tool("search_catalog"), {"q": "x"}) is None
|
|
75
|
+
assert fake.calls == []
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_match_lets_tool_run():
|
|
79
|
+
fake = FakeProveCartClient()
|
|
80
|
+
cb = make_before_tool_callback(ProveCartGuard(fake))
|
|
81
|
+
assert cb(tool=Tool("place_order"), args=CART_ARGS, tool_context=None) is None
|
|
82
|
+
assert fake.calls and fake.calls[0][0] == "cart"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_block_returns_refusal_dict_with_corrections():
|
|
86
|
+
fake = FakeProveCartClient({"verdict": "BLOCK", "record_id": "r9",
|
|
87
|
+
"max_attempts": 2,
|
|
88
|
+
"corrections": [{"action": "reduce_quantity"}]})
|
|
89
|
+
cb = make_before_tool_callback(ProveCartGuard(fake))
|
|
90
|
+
out = cb(tool=Tool("checkout"), args=CART_ARGS, tool_context=None)
|
|
91
|
+
assert out["provecart_blocked"] is True
|
|
92
|
+
assert out["provecart_decision"] == "fix"
|
|
93
|
+
assert out["corrections"] == [{"action": "reduce_quantity"}]
|
|
94
|
+
assert out["record_id"] == "r9"
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_review_escalates_and_booking_and_reservation_kinds():
|
|
98
|
+
fake = FakeProveCartClient({"verdict": "REVIEW", "record_id": "r2",
|
|
99
|
+
"corrections": []})
|
|
100
|
+
guard = ProveCartGuard(fake)
|
|
101
|
+
out = guard.before_tool(Tool("book_flight"), {"booking": {"mandate_id": "m"}})
|
|
102
|
+
assert out["provecart_decision"] == "escalate" and out["check_kind"] == "booking"
|
|
103
|
+
out = guard.before_tool(Tool("reserve_table"), {"mandate_id": "m"})
|
|
104
|
+
assert out["check_kind"] == "reservation"
|
|
105
|
+
assert [c[0] for c in fake.calls] == ["booking", "reservation"]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def test_check_failure_escalates_not_proceeds():
|
|
109
|
+
class Boom:
|
|
110
|
+
def check_cart(self, **kw):
|
|
111
|
+
raise RuntimeError("down")
|
|
112
|
+
guard = ProveCartGuard(Boom())
|
|
113
|
+
out = guard.before_tool(Tool("place_order"), CART_ARGS)
|
|
114
|
+
assert out["provecart_decision"] == "escalate"
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def test_client_route_verdict_is_preferred():
|
|
118
|
+
class Own:
|
|
119
|
+
def check_cart(self, **kw):
|
|
120
|
+
return {"verdict": "MATCH", "record_id": "x"}
|
|
121
|
+
def route_verdict(self, response):
|
|
122
|
+
return "escalate", {"reason": "house rule"}
|
|
123
|
+
guard = ProveCartGuard(Own())
|
|
124
|
+
out = guard.before_tool(Tool("place_order"), CART_ARGS)
|
|
125
|
+
assert out["provecart_decision"] == "escalate"
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def test_plugin_callback_signature_both_arg_names():
|
|
129
|
+
fake = FakeProveCartClient()
|
|
130
|
+
plugin = ProveCartPlugin(client=fake)
|
|
131
|
+
run = asyncio.get_event_loop().run_until_complete
|
|
132
|
+
assert run(plugin.before_tool_callback(
|
|
133
|
+
tool=Tool("place_order"), tool_args=CART_ARGS, tool_context=None)) is None
|
|
134
|
+
assert run(plugin.before_tool_callback(
|
|
135
|
+
tool=Tool("place_order"), args=CART_ARGS, tool_context=None)) is None
|
|
136
|
+
assert len(fake.calls) == 2
|
|
137
|
+
print("HAS_REAL_ADK:", HAS_ADK)
|