pushary 2.1.2__tar.gz → 2.2.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.
- {pushary-2.1.2 → pushary-2.2.0}/PKG-INFO +5 -1
- {pushary-2.1.2 → pushary-2.2.0}/README.md +4 -0
- {pushary-2.1.2 → pushary-2.2.0}/pyproject.toml +1 -1
- {pushary-2.1.2 → pushary-2.2.0}/src/pushary/__init__.py +1 -1
- {pushary-2.1.2 → pushary-2.2.0}/src/pushary/adapters.py +5 -0
- {pushary-2.1.2 → pushary-2.2.0}/tests/test_adapters.py +49 -0
- {pushary-2.1.2 → pushary-2.2.0}/.gitignore +0 -0
- {pushary-2.1.2 → pushary-2.2.0}/LICENSE +0 -0
- {pushary-2.1.2 → pushary-2.2.0}/examples/agno/README.md +0 -0
- {pushary-2.1.2 → pushary-2.2.0}/examples/dify/README.md +0 -0
- {pushary-2.1.2 → pushary-2.2.0}/examples/llamaindex/README.md +0 -0
- {pushary-2.1.2 → pushary-2.2.0}/examples/microsoft/README.md +0 -0
- {pushary-2.1.2 → pushary-2.2.0}/examples/strands/README.md +0 -0
- {pushary-2.1.2 → pushary-2.2.0}/src/pushary/_types.py +0 -0
- {pushary-2.1.2 → pushary-2.2.0}/src/pushary/_util.py +0 -0
- {pushary-2.1.2 → pushary-2.2.0}/src/pushary/client.py +0 -0
- {pushary-2.1.2 → pushary-2.2.0}/src/pushary/decisions.py +0 -0
- {pushary-2.1.2 → pushary-2.2.0}/src/pushary/errors.py +0 -0
- {pushary-2.1.2 → pushary-2.2.0}/src/pushary/keys.py +0 -0
- {pushary-2.1.2 → pushary-2.2.0}/src/pushary/py.typed +0 -0
- {pushary-2.1.2 → pushary-2.2.0}/src/pushary/webhook.py +0 -0
- {pushary-2.1.2 → pushary-2.2.0}/tests/test_agent.py +0 -0
- {pushary-2.1.2 → pushary-2.2.0}/tests/test_client.py +0 -0
- {pushary-2.1.2 → pushary-2.2.0}/tests/test_webhook.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: pushary
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.0
|
|
4
4
|
Summary: The decision layer for AI agents. Your agent asks, your user decides on their phone.
|
|
5
5
|
Project-URL: Homepage, https://pushary.com
|
|
6
6
|
Project-URL: Documentation, https://pushary.com/docs
|
|
@@ -287,3 +287,7 @@ Shared approval gates now bind decision identity to the customer, session, tool
|
|
|
287
287
|
These keys intentionally differ from 2.0. Do not replay completed operations during upgrade or silently migrate an old approval onto a new key. Drain pending runs on their original version, or explicitly request fresh review while preserving your application's operation idempotency. `policy=False` now records the full subject and presentation on the human decision; a failed legacy policy endpoint retains the prior minimal fallback for compatibility.
|
|
288
288
|
|
|
289
289
|
`decisions.create` and `decisions.ask` accept `presentation`, using the existing server-validated shape with `label`, `effect`, optional `risk`, and `changes` that reference keys in `parameters`. Shared `ask_human` and `create_durable_decision` helpers forward that field plus target, actor, environment, parameters, placeholder, expiry, reachability, and callback settings. Parameter values remain the source of truth for display; presentation changes name a parameter rather than carrying a second copy of its value.
|
|
290
|
+
|
|
291
|
+
### Upgrading to 2.2
|
|
292
|
+
|
|
293
|
+
`ApprovalAsk` accepts `context`. The shared approval gate sends it with the decision, so the approver can read what the notification cannot fit, such as a tool's full arguments. Context is part of the approval identity when set. An ask without context keeps the key it had in 2.1, so pending approvals carry over.
|
|
@@ -259,3 +259,7 @@ Shared approval gates now bind decision identity to the customer, session, tool
|
|
|
259
259
|
These keys intentionally differ from 2.0. Do not replay completed operations during upgrade or silently migrate an old approval onto a new key. Drain pending runs on their original version, or explicitly request fresh review while preserving your application's operation idempotency. `policy=False` now records the full subject and presentation on the human decision; a failed legacy policy endpoint retains the prior minimal fallback for compatibility.
|
|
260
260
|
|
|
261
261
|
`decisions.create` and `decisions.ask` accept `presentation`, using the existing server-validated shape with `label`, `effect`, optional `risk`, and `changes` that reference keys in `parameters`. Shared `ask_human` and `create_durable_decision` helpers forward that field plus target, actor, environment, parameters, placeholder, expiry, reachability, and callback settings. Parameter values remain the source of truth for display; presentation changes name a parameter rather than carrying a second copy of its value.
|
|
262
|
+
|
|
263
|
+
### Upgrading to 2.2
|
|
264
|
+
|
|
265
|
+
`ApprovalAsk` accepts `context`. The shared approval gate sends it with the decision, so the approver can read what the notification cannot fit, such as a tool's full arguments. Context is part of the approval identity when set. An ask without context keeps the key it had in 2.1, so pending approvals carry over.
|
|
@@ -153,6 +153,7 @@ def _gate_key(ask: "ApprovalAsk", facts: Optional[Dict[str, Any]]) -> str:
|
|
|
153
153
|
"input": ask.input,
|
|
154
154
|
"parameters": facts,
|
|
155
155
|
"presentation": ask.presentation,
|
|
156
|
+
**({"context": ask.context} if ask.context is not None else {}),
|
|
156
157
|
})
|
|
157
158
|
|
|
158
159
|
|
|
@@ -251,6 +252,7 @@ class ApprovalAsk:
|
|
|
251
252
|
"input",
|
|
252
253
|
"parameters",
|
|
253
254
|
"presentation",
|
|
255
|
+
"context",
|
|
254
256
|
)
|
|
255
257
|
|
|
256
258
|
def __init__(
|
|
@@ -267,6 +269,7 @@ class ApprovalAsk:
|
|
|
267
269
|
input: Any = None,
|
|
268
270
|
parameters: Optional[Dict[str, Any]] = None,
|
|
269
271
|
presentation: Optional[Dict[str, Any]] = None,
|
|
272
|
+
context: Optional[str] = None,
|
|
270
273
|
) -> None:
|
|
271
274
|
self.tool_name = tool_name
|
|
272
275
|
self.call_id = call_id
|
|
@@ -286,6 +289,7 @@ class ApprovalAsk:
|
|
|
286
289
|
#: Explicit facts for policy, used instead of deriving them from ``input``.
|
|
287
290
|
self.parameters = parameters
|
|
288
291
|
self.presentation = presentation
|
|
292
|
+
self.context = context
|
|
289
293
|
|
|
290
294
|
|
|
291
295
|
class ApprovalDecision:
|
|
@@ -689,6 +693,7 @@ class AdapterKernel:
|
|
|
689
693
|
ask.question,
|
|
690
694
|
type="confirm",
|
|
691
695
|
external_id=ask.external_id,
|
|
696
|
+
context=ask.context,
|
|
692
697
|
agent_name=agent_name,
|
|
693
698
|
expires_in_seconds=expires_in_seconds,
|
|
694
699
|
timeout_seconds=timeout_seconds,
|
|
@@ -268,6 +268,55 @@ class GateTests(unittest.TestCase):
|
|
|
268
268
|
self.assertEqual(keys[0], keys[1])
|
|
269
269
|
self.assertNotEqual(keys[0], keys[2])
|
|
270
270
|
|
|
271
|
+
def test_shows_the_approver_the_context(self):
|
|
272
|
+
decisions = FakeDecisions(ask_result={"answered": True, "approved": True, "value": "yes", "decisionId": "d1"})
|
|
273
|
+
with WithFakeClient(FakeClient(decisions=decisions)):
|
|
274
|
+
kernel.create_gate()(
|
|
275
|
+
ApprovalAsk(
|
|
276
|
+
tool_name="issue_refund",
|
|
277
|
+
call_id="call_1",
|
|
278
|
+
session_id="sess_1",
|
|
279
|
+
question="Approve?",
|
|
280
|
+
external_id="user_1",
|
|
281
|
+
context='Tool arguments: {"amount": 99999}',
|
|
282
|
+
)
|
|
283
|
+
)
|
|
284
|
+
self.assertEqual(decisions.ask_calls[0]["context"], 'Tool arguments: {"amount": 99999}')
|
|
285
|
+
|
|
286
|
+
def test_context_is_part_of_the_approval_identity(self):
|
|
287
|
+
decisions = FakeDecisions(ask_result={"answered": True, "approved": True, "value": "yes", "decisionId": "d1"})
|
|
288
|
+
with WithFakeClient(FakeClient(decisions=decisions)):
|
|
289
|
+
gate = kernel.create_gate()
|
|
290
|
+
for context in ("Refund 5", "Refund 99999"):
|
|
291
|
+
gate(
|
|
292
|
+
ApprovalAsk(
|
|
293
|
+
tool_name="issue_refund",
|
|
294
|
+
call_id="call_1",
|
|
295
|
+
session_id="sess_1",
|
|
296
|
+
question="Approve?",
|
|
297
|
+
external_id="user_1",
|
|
298
|
+
context=context,
|
|
299
|
+
)
|
|
300
|
+
)
|
|
301
|
+
first, second = (call["idempotency_key"] for call in decisions.ask_calls)
|
|
302
|
+
self.assertNotEqual(first, second)
|
|
303
|
+
|
|
304
|
+
def test_an_ask_without_context_keeps_the_key_it_had_before_context_existed(self):
|
|
305
|
+
decisions = FakeDecisions(ask_result={"answered": True, "approved": True, "value": "yes", "decisionId": "d1"})
|
|
306
|
+
with WithFakeClient(FakeClient(decisions=decisions)):
|
|
307
|
+
kernel.create_gate()(
|
|
308
|
+
ApprovalAsk(
|
|
309
|
+
tool_name="issue_refund",
|
|
310
|
+
call_id="call_1",
|
|
311
|
+
session_id="sess_1",
|
|
312
|
+
question="Approve?",
|
|
313
|
+
external_id="user_1",
|
|
314
|
+
input={"amount": 5},
|
|
315
|
+
)
|
|
316
|
+
)
|
|
317
|
+
self.assertIsNone(decisions.ask_calls[0]["context"])
|
|
318
|
+
self.assertEqual(decisions.ask_calls[0]["idempotency_key"], "135e90787543c78d72b7008ce463da08d1ce8769")
|
|
319
|
+
|
|
271
320
|
def test_allows_without_opening_a_decision_or_paging_anyone(self):
|
|
272
321
|
decisions = FakeDecisions()
|
|
273
322
|
client = FakeClient(
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|