oneshot-python 0.36.0__tar.gz → 0.38.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.
- {oneshot_python-0.36.0 → oneshot_python-0.38.0}/PKG-INFO +1 -1
- {oneshot_python-0.36.0 → oneshot_python-0.38.0}/README.md +13 -2
- {oneshot_python-0.36.0 → oneshot_python-0.38.0}/oneshot/__init__.py +2 -0
- {oneshot_python-0.36.0 → oneshot_python-0.38.0}/oneshot/_types.py +1 -0
- {oneshot_python-0.36.0 → oneshot_python-0.38.0}/oneshot/client.py +256 -27
- oneshot_python-0.38.0/oneshot/lookup.py +252 -0
- oneshot_python-0.38.0/oneshot/lookup_manifest.py +2050 -0
- {oneshot_python-0.36.0 → oneshot_python-0.38.0}/pyproject.toml +1 -1
- {oneshot_python-0.36.0 → oneshot_python-0.38.0}/tests/test_compute.py +46 -0
- oneshot_python-0.38.0/tests/test_compute_manifest.py +142 -0
- oneshot_python-0.38.0/tests/test_compute_outputs.py +47 -0
- oneshot_python-0.38.0/tests/test_declared_routes.py +110 -0
- oneshot_python-0.38.0/tests/test_declared_worker_reads.py +77 -0
- {oneshot_python-0.36.0 → oneshot_python-0.38.0}/tests/test_domains.py +3 -3
- oneshot_python-0.38.0/tests/test_domains_signed.py +78 -0
- oneshot_python-0.38.0/tests/test_inbox_notifications.py +84 -0
- oneshot_python-0.38.0/tests/test_lookup.py +266 -0
- oneshot_python-0.38.0/tests/test_paid_call_proof.py +185 -0
- oneshot_python-0.38.0/tests/test_update_build.py +38 -0
- {oneshot_python-0.36.0 → oneshot_python-0.38.0}/.gitignore +0 -0
- {oneshot_python-0.36.0 → oneshot_python-0.38.0}/oneshot/_errors.py +0 -0
- {oneshot_python-0.36.0 → oneshot_python-0.38.0}/oneshot/physical_mail.py +0 -0
- {oneshot_python-0.36.0 → oneshot_python-0.38.0}/oneshot/x402.py +0 -0
- {oneshot_python-0.36.0 → oneshot_python-0.38.0}/tests/__init__.py +0 -0
- {oneshot_python-0.36.0 → oneshot_python-0.38.0}/tests/conftest.py +0 -0
- {oneshot_python-0.36.0 → oneshot_python-0.38.0}/tests/test_access_token.py +0 -0
- {oneshot_python-0.36.0 → oneshot_python-0.38.0}/tests/test_approvals.py +0 -0
- {oneshot_python-0.36.0 → oneshot_python-0.38.0}/tests/test_balance.py +0 -0
- {oneshot_python-0.36.0 → oneshot_python-0.38.0}/tests/test_browser.py +0 -0
- {oneshot_python-0.36.0 → oneshot_python-0.38.0}/tests/test_budgets.py +0 -0
- {oneshot_python-0.36.0 → oneshot_python-0.38.0}/tests/test_charge_amount.py +0 -0
- {oneshot_python-0.36.0 → oneshot_python-0.38.0}/tests/test_email_payload.py +0 -0
- {oneshot_python-0.36.0 → oneshot_python-0.38.0}/tests/test_emergency_error.py +0 -0
- {oneshot_python-0.36.0 → oneshot_python-0.38.0}/tests/test_hunts.py +0 -0
- {oneshot_python-0.36.0 → oneshot_python-0.38.0}/tests/test_inbound_voice.py +0 -0
- {oneshot_python-0.36.0 → oneshot_python-0.38.0}/tests/test_linkedin.py +0 -0
- {oneshot_python-0.36.0 → oneshot_python-0.38.0}/tests/test_max_cost_header.py +0 -0
- {oneshot_python-0.36.0 → oneshot_python-0.38.0}/tests/test_payment_rejection.py +0 -0
- {oneshot_python-0.36.0 → oneshot_python-0.38.0}/tests/test_phones_pending.py +0 -0
- {oneshot_python-0.36.0 → oneshot_python-0.38.0}/tests/test_physical_mail.py +0 -0
- {oneshot_python-0.36.0 → oneshot_python-0.38.0}/tests/test_poll_backoff.py +0 -0
- {oneshot_python-0.36.0 → oneshot_python-0.38.0}/tests/test_request_id.py +0 -0
- {oneshot_python-0.36.0 → oneshot_python-0.38.0}/tests/test_signed_job_and_analytics.py +0 -0
- {oneshot_python-0.36.0 → oneshot_python-0.38.0}/tests/test_tag_receipt_value.py +0 -0
- {oneshot_python-0.36.0 → oneshot_python-0.38.0}/tests/test_top_up_credits.py +0 -0
- {oneshot_python-0.36.0 → oneshot_python-0.38.0}/tests/test_wait_false.py +0 -0
- {oneshot_python-0.36.0 → oneshot_python-0.38.0}/tests/test_x402.py +0 -0
- {oneshot_python-0.36.0 → oneshot_python-0.38.0}/uv.lock +0 -0
|
@@ -194,6 +194,7 @@ print(goal["goal_id"])
|
|
|
194
194
|
# Observe
|
|
195
195
|
status = client.get_compute_goal(goal["goal_id"])
|
|
196
196
|
tasks = client.get_compute_tasks(goal["goal_id"])
|
|
197
|
+
outputs = client.get_compute_outputs(goal["goal_id"]) # what the tasks produced; read one with get_compute_output(goal_id, key)
|
|
197
198
|
budget = client.get_compute_budget(goal["goal_id"])
|
|
198
199
|
|
|
199
200
|
# Top up a recurring goal (paid)
|
|
@@ -212,7 +213,7 @@ Every method has an `a*` async mirror (`acompute`, `aget_compute_goal`, …).
|
|
|
212
213
|
|
|
213
214
|
### Physical mail
|
|
214
215
|
|
|
215
|
-
`client.physical_mail` supports U.S. letters and 4×6 postcards through `upload_artwork`, `validate_address`, `preview`, `get_quote`, `approve`, `send`, `get_order`, `recover`, and `cancel`. Async variants prefix the method with `a`. Review each proof and price, then call `approve(..., approved=True)` explicitly. Persist `idempotency_key` before sending. `recover(key)` retrieves the original order without a second charge. Postal delivery does not prove the letter was read; paid receipts and fulfillment events are separate.
|
|
216
|
+
`client.physical_mail` supports U.S. letters and 4×6 postcards through `upload_artwork`, `validate_address`, `preview`, `get_quote`, `approve`, `send`, `get_order`, `recover`, and `cancel`. Async variants prefix the method with `a`. Review each proof and price, then call `approve(..., approved=True)` explicitly. Persist `idempotency_key` before sending. `recover(key)` retrieves the original order without a second charge. Postal delivery does not prove the letter was read; paid receipts and fulfillment events are separate. Mail provider credentials stay on OneShot's server.
|
|
216
217
|
|
|
217
218
|
### Account login and 2FA (0.24.0+)
|
|
218
219
|
|
|
@@ -314,6 +315,16 @@ client.hunts.leaderboard() # public predictor leaderboard
|
|
|
314
315
|
|
|
315
316
|
Async variants prefix the method with `a` (`alist`, `astatus`, `apredict`, `acapture`, `acollection`, `aleaderboard`). `list`/`status`/`collection`/`leaderboard` are free reads and work for both wallet and access-token sessions. `predict`/`capture` debit a stake and require Soul Hunt's wallet-signed write proof, which an access-token (credits-only) session cannot produce — those two raise `ValidationError` locally rather than reaching the network. The client never forwards the OneShot bearer token to Soul Hunt; only the wallet address and, for wallet sessions, the signed `x-agent-proof` go out.
|
|
316
317
|
|
|
318
|
+
## Declared routes and inbox methods (#1273)
|
|
319
|
+
|
|
320
|
+
Every route in the API registry states an agent tool. The ones without a typed method are in the lookup
|
|
321
|
+
manifest and reached with `call_lookup(name, **fields)`, for example `call_lookup("company_filings", cik="320193")`
|
|
322
|
+
(`acall_lookup` for async): a free route is a signed
|
|
323
|
+
call, a paid one goes through `call_tool` and its quote. LangChain and GAME build their tools from the same
|
|
324
|
+
manifest. The inbox, SMS inbox and notification routes also have methods: `inbox`, `inbox_get`, `sms_inbox`,
|
|
325
|
+
`sms_inbox_get`, `notifications`, `mark_notification_read`, `mark_all_notifications_read` (each with an `a`
|
|
326
|
+
async form).
|
|
327
|
+
|
|
317
328
|
## Requirements
|
|
318
329
|
|
|
319
330
|
- Python 3.10+
|
|
@@ -323,7 +334,7 @@ Async variants prefix the method with `a` (`alist`, `astatus`, `apredict`, `acap
|
|
|
323
334
|
## Links
|
|
324
335
|
|
|
325
336
|
- [Documentation](https://docs.oneshotagent.com/sdk/installation#install-via-pip-python)
|
|
326
|
-
- [LangChain integration](https://pypi.org/project/langchain-oneshot/): 48 tools as LangChain BaseTool
|
|
337
|
+
- [LangChain integration](https://pypi.org/project/langchain-oneshot/): 48 tools plus the lookup tools as LangChain BaseTool
|
|
327
338
|
- [GAME plugin](https://pypi.org/project/game-plugin-oneshot/): Virtuals Protocol integration
|
|
328
339
|
- [TypeScript SDK](https://www.npmjs.com/package/@oneshot-agent/sdk)
|
|
329
340
|
- [MCP Server](https://www.npmjs.com/package/@oneshot-agent/mcp-server)
|
|
@@ -39,9 +39,11 @@ from oneshot._types import (
|
|
|
39
39
|
DomainPoolStatusResult,
|
|
40
40
|
)
|
|
41
41
|
from oneshot.client import OneShotClient
|
|
42
|
+
from oneshot.lookup import lookup_tools
|
|
42
43
|
from oneshot.x402 import sign_payment_authorization
|
|
43
44
|
|
|
44
45
|
__all__ = [
|
|
46
|
+
"lookup_tools",
|
|
45
47
|
"PhysicalMail", "PostalAddress", "MailQuote", "MailOrder",
|
|
46
48
|
"OneShotClient",
|
|
47
49
|
"OneShotError",
|
|
@@ -44,6 +44,7 @@ from oneshot._types import (
|
|
|
44
44
|
DomainPoolListResult,
|
|
45
45
|
DomainPoolStatusResult,
|
|
46
46
|
)
|
|
47
|
+
from oneshot.lookup import PAID_PRICING, find_lookup_tool, lookup_query, lookup_request
|
|
47
48
|
from oneshot.x402 import (
|
|
48
49
|
build_zero_cost_authorization,
|
|
49
50
|
encode_payment_header,
|
|
@@ -59,11 +60,13 @@ try:
|
|
|
59
60
|
|
|
60
61
|
SDK_VERSION = _pkg_version("oneshot-python")
|
|
61
62
|
except Exception: # pragma: no cover - editable/source runs without dist metadata
|
|
62
|
-
SDK_VERSION = "0.
|
|
63
|
+
SDK_VERSION = "0.38.0"
|
|
63
64
|
|
|
64
65
|
_BASE_URL = "https://win.oneshotagent.com"
|
|
65
66
|
_HUNT_BASE_URL = "https://api.soulhunt.ai"
|
|
66
67
|
_ACCESS_TOKEN_RE = re.compile(r"^oneshot_[0-9a-f]{64}$")
|
|
68
|
+
# Endpoints whose admission reads a "submit"-scoped agent proof (same list as the TypeScript SDK).
|
|
69
|
+
_DURABLE_ADMISSION_ENDPOINT = re.compile(r"(enrich/(profile|email)|verify/email|physical-mail/send)$")
|
|
67
70
|
_CHAIN_ID = 8453
|
|
68
71
|
_USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
|
|
69
72
|
|
|
@@ -72,6 +75,7 @@ _USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
|
|
|
72
75
|
_POLL_BACKOFF = (0.3, 0.6, 1.0, 2.0) # seconds
|
|
73
76
|
_POLL_RETRY_BASE = 2.0 # seconds, transient-error retry backoff base
|
|
74
77
|
_MAX_POLL_RETRIES = 3
|
|
78
|
+
_BUILD_TRANSPORT_KWARGS = ("max_cost", "timeout_sec", "wait", "approval_id", "idempotency_key")
|
|
75
79
|
|
|
76
80
|
|
|
77
81
|
def _build_email_payload(
|
|
@@ -157,6 +161,24 @@ def _parse_policy_rejection(resp: Any) -> Optional[OneShotError]:
|
|
|
157
161
|
return None
|
|
158
162
|
|
|
159
163
|
|
|
164
|
+
def _parse_invalid_proof(resp: Any) -> Optional[tuple[str, str]]:
|
|
165
|
+
"""``(reason, message)`` when ``resp`` is the API's 401 ``invalid_proof`` refusal (#1437), else None."""
|
|
166
|
+
if resp.status_code != 401:
|
|
167
|
+
return None
|
|
168
|
+
try:
|
|
169
|
+
data = resp.json()
|
|
170
|
+
except Exception: # noqa: BLE001 - non-JSON body
|
|
171
|
+
return None
|
|
172
|
+
if not isinstance(data, dict) or data.get("error") != "invalid_proof":
|
|
173
|
+
return None
|
|
174
|
+
reason = data.get("reason")
|
|
175
|
+
message = data.get("message")
|
|
176
|
+
return (
|
|
177
|
+
reason if isinstance(reason, str) else "invalid",
|
|
178
|
+
message if isinstance(message, str) else "The agent proof could not be verified.",
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
|
|
160
182
|
def _parse_budget_rejection(resp: Any) -> Optional[BudgetExceededError]:
|
|
161
183
|
"""Map a 403 ``budget_exceeded`` body onto :class:`BudgetExceededError`.
|
|
162
184
|
|
|
@@ -631,6 +653,24 @@ class OneShotClient:
|
|
|
631
653
|
self._log(f"Failed to sign read proof (continuing without): {e}")
|
|
632
654
|
return headers
|
|
633
655
|
|
|
656
|
+
def _prove_wallet(self, headers: dict[str, str], endpoint: str) -> dict[str, str]:
|
|
657
|
+
"""Add a signed agent proof to a paid-call leg that carries no signed
|
|
658
|
+
payment. The API spends an agent's credits, and shows them on a quote,
|
|
659
|
+
only for a caller that has proven it holds the wallet: ``X-Agent-ID``
|
|
660
|
+
alone is a public address. A leg with a real payment signature needs
|
|
661
|
+
none. The endpoints with durable admission read a ``submit`` proof.
|
|
662
|
+
An access-token session is already bound to its agent. A signing
|
|
663
|
+
failure is logged and the leg goes without, so the call is priced as
|
|
664
|
+
if the agent held no credits."""
|
|
665
|
+
if self._access_token:
|
|
666
|
+
return headers
|
|
667
|
+
scope = "submit" if _DURABLE_ADMISSION_ENDPOINT.search(endpoint) else "write"
|
|
668
|
+
try:
|
|
669
|
+
headers["x-agent-proof"] = sign_read_proof(self._private_key, self.address, scope=scope)
|
|
670
|
+
except Exception as e: # noqa: BLE001
|
|
671
|
+
self._log(f"Failed to sign agent proof (continuing without; credits will not be used): {e}")
|
|
672
|
+
return headers
|
|
673
|
+
|
|
634
674
|
def _write_headers(self, domain_name: str = "OneShot Agent Auth") -> dict[str, str]:
|
|
635
675
|
"""Like ``_read_headers`` but with a ``write``-scoped proof, for the
|
|
636
676
|
signed mutation routes (budgets). The server enforces the proof on
|
|
@@ -646,6 +686,29 @@ class OneShotClient:
|
|
|
646
686
|
if self.debug:
|
|
647
687
|
print(f"[OneShot] {msg}")
|
|
648
688
|
|
|
689
|
+
async def _post_proved(self, client: httpx.AsyncClient, url: str, build_headers: Any, payload: dict[str, Any]) -> Any:
|
|
690
|
+
"""POST a leg that proves the wallet. ``build_headers`` returns the headers
|
|
691
|
+
with a freshly signed proof each time it is called.
|
|
692
|
+
|
|
693
|
+
The API refuses a request whose proof it rejected, saying why (#1437). A
|
|
694
|
+
proof outside its window or sent twice is cured by signing a new one, so
|
|
695
|
+
the leg is sent again once. Any other reason, or a second refusal, is
|
|
696
|
+
raised with the reason. Nothing is paid in answer to this refusal.
|
|
697
|
+
"""
|
|
698
|
+
resp = await client.post(url, headers=build_headers(), json=payload)
|
|
699
|
+
refusal = _parse_invalid_proof(resp)
|
|
700
|
+
if refusal is None:
|
|
701
|
+
return resp
|
|
702
|
+
reason, message = refusal
|
|
703
|
+
if reason in ("expired", "already_used"):
|
|
704
|
+
self._log(f"Wallet proof refused ({reason}); signing a new proof and retrying once")
|
|
705
|
+
resp = await client.post(url, headers=build_headers(), json=payload)
|
|
706
|
+
refusal = _parse_invalid_proof(resp)
|
|
707
|
+
if refusal is None:
|
|
708
|
+
return resp
|
|
709
|
+
reason, message = refusal
|
|
710
|
+
raise OneShotError(f"The API refused the wallet proof ({reason}): {message}")
|
|
711
|
+
|
|
649
712
|
async def aensure_budgets_synced(self) -> None:
|
|
650
713
|
"""Push ``budgets`` / ``alert_email`` to the server once (async).
|
|
651
714
|
|
|
@@ -754,6 +817,44 @@ class OneShotClient:
|
|
|
754
817
|
)
|
|
755
818
|
)
|
|
756
819
|
|
|
820
|
+
def call_lookup(self, name: str, /, **params: Any) -> Any:
|
|
821
|
+
"""Call a lookup tool by its manifest name, e.g. ``call_lookup("company_filings", cik="320193")``.
|
|
822
|
+
|
|
823
|
+
The tool's name is positional only, so a tool whose own input is called
|
|
824
|
+
``name`` can be given it: ``call_lookup("company_legal_identifier", name="Apple Inc.")``.
|
|
825
|
+
|
|
826
|
+
Sends the params through :meth:`call_tool`. Params that are ``None`` are
|
|
827
|
+
left out. An unknown name raises ``ValidationError``; see
|
|
828
|
+
:func:`oneshot.lookup.lookup_tools` for the names.
|
|
829
|
+
"""
|
|
830
|
+
entry = find_lookup_tool(name)
|
|
831
|
+
method, pricing, path, payload = lookup_request(entry, params)
|
|
832
|
+
if pricing in PAID_PRICING:
|
|
833
|
+
return self.call_tool(path, payload)
|
|
834
|
+
return asyncio.get_event_loop().run_until_complete(self._acall_free_lookup(method, path, payload))
|
|
835
|
+
|
|
836
|
+
async def acall_lookup(self, name: str, /, **params: Any) -> Any:
|
|
837
|
+
"""Async form of :meth:`call_lookup`, sent through :meth:`acall_tool`."""
|
|
838
|
+
entry = find_lookup_tool(name)
|
|
839
|
+
method, pricing, path, payload = lookup_request(entry, params)
|
|
840
|
+
if pricing in PAID_PRICING:
|
|
841
|
+
return await self.acall_tool(path, payload)
|
|
842
|
+
return await self._acall_free_lookup(method, path, payload)
|
|
843
|
+
|
|
844
|
+
async def _acall_free_lookup(self, method: str, path: str, payload: dict[str, Any]) -> Any:
|
|
845
|
+
"""A route with no payment of its own (a goal read or control, an inbox, a notification, a number or
|
|
846
|
+
account setting): a GET sends the params as the query, the other methods are write-scoped signed calls;
|
|
847
|
+
the ``{"success", "data"}`` envelope is stripped as the compute methods do."""
|
|
848
|
+
if method == "GET":
|
|
849
|
+
return self._unwrap_data(await self.acall_free_get(path, lookup_query(payload) or None))
|
|
850
|
+
if method == "PATCH":
|
|
851
|
+
return self._unwrap_data(await self.acall_free_patch(path, payload, scope="write"))
|
|
852
|
+
if method == "PUT":
|
|
853
|
+
return self._unwrap_data(await self.acall_free_put(path, payload))
|
|
854
|
+
if method == "DELETE":
|
|
855
|
+
return self._unwrap_data(await self.acall_free_delete(path, scope="write"))
|
|
856
|
+
return self._unwrap_data(await self.acall_free_post(path, payload, scope="write"))
|
|
857
|
+
|
|
757
858
|
def wait_for_result(
|
|
758
859
|
self,
|
|
759
860
|
request_id: str,
|
|
@@ -850,10 +951,16 @@ class OneShotClient:
|
|
|
850
951
|
# before they ever get to the 402 dance unless x-quote-id rides
|
|
851
952
|
# on this very first attempt, so an already-obtained quote_id
|
|
852
953
|
# must be sent from the start, not just on the paid retry.
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
954
|
+
initial_headers_base = self._with_approval(self._headers(max_cost=max_cost, idempotency_key=idempotency_key), approval_id)
|
|
955
|
+
|
|
956
|
+
def _initial_headers() -> dict[str, str]:
|
|
957
|
+
headers = dict(initial_headers_base)
|
|
958
|
+
self._prove_wallet(headers, endpoint)
|
|
959
|
+
if quote_id:
|
|
960
|
+
headers["x-quote-id"] = quote_id
|
|
961
|
+
return headers
|
|
962
|
+
|
|
963
|
+
resp = await self._post_proved(client, url, _initial_headers, payload)
|
|
857
964
|
|
|
858
965
|
# The spend-budget gate rejects at quote time (403) so nothing is
|
|
859
966
|
# signed; surface it as the typed error.
|
|
@@ -981,13 +1088,21 @@ class OneShotClient:
|
|
|
981
1088
|
resource=parsed_req.get("resource"),
|
|
982
1089
|
extensions=parsed_req.get("extensions"),
|
|
983
1090
|
)
|
|
984
|
-
|
|
1091
|
+
zero_cost_base = {
|
|
985
1092
|
**self._with_approval(self._headers(max_cost=max_cost, idempotency_key=idempotency_key), approval_id),
|
|
986
1093
|
"payment-signature": encode_payment_header(auth),
|
|
987
1094
|
}
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
1095
|
+
|
|
1096
|
+
def _zero_cost_headers() -> dict[str, str]:
|
|
1097
|
+
# The zero-cost authorization is an unsigned stub, so this leg
|
|
1098
|
+
# still has to prove the wallet for the credits to be spent.
|
|
1099
|
+
headers = dict(zero_cost_base)
|
|
1100
|
+
self._prove_wallet(headers, endpoint)
|
|
1101
|
+
if quote_id:
|
|
1102
|
+
headers["x-quote-id"] = quote_id
|
|
1103
|
+
return headers
|
|
1104
|
+
|
|
1105
|
+
resp2 = await self._post_proved(client, url, _zero_cost_headers, payload)
|
|
991
1106
|
else:
|
|
992
1107
|
auth = sign_payment_authorization(
|
|
993
1108
|
private_key=self._private_key,
|
|
@@ -1368,6 +1483,32 @@ class OneShotClient:
|
|
|
1368
1483
|
"""List tasks under a goal. Async."""
|
|
1369
1484
|
return self._unwrap_data(await self.acall_free_get(f"/v1/compute/{goal_id}/tasks"))
|
|
1370
1485
|
|
|
1486
|
+
def get_compute_outputs(self, goal_id: str) -> Any:
|
|
1487
|
+
"""List what a goal produced: the files its sandbox tasks left and the
|
|
1488
|
+
tool results too large for a task row. The goal's short answer is
|
|
1489
|
+
``result`` on ``get_compute_goal``. Blocking."""
|
|
1490
|
+
return asyncio.get_event_loop().run_until_complete(
|
|
1491
|
+
self.aget_compute_outputs(goal_id)
|
|
1492
|
+
)
|
|
1493
|
+
|
|
1494
|
+
async def aget_compute_outputs(self, goal_id: str) -> Any:
|
|
1495
|
+
"""List what a goal produced. Async."""
|
|
1496
|
+
return self._unwrap_data(await self.acall_free_get(f"/v1/compute/{goal_id}/outputs"))
|
|
1497
|
+
|
|
1498
|
+
def get_compute_output(self, goal_id: str, key: str) -> Any:
|
|
1499
|
+
"""Read one output of a goal by the ``key`` from ``get_compute_outputs``
|
|
1500
|
+
(or a task's ``outputs``). ``content`` is None when the output was too
|
|
1501
|
+
large to keep (``readable`` False). Blocking."""
|
|
1502
|
+
return asyncio.get_event_loop().run_until_complete(
|
|
1503
|
+
self.aget_compute_output(goal_id, key)
|
|
1504
|
+
)
|
|
1505
|
+
|
|
1506
|
+
async def aget_compute_output(self, goal_id: str, key: str) -> Any:
|
|
1507
|
+
"""Read one output of a goal. Async."""
|
|
1508
|
+
# The key is a path: keep its slashes, escape everything else.
|
|
1509
|
+
path = "/".join(quote(part, safe="") for part in key.split("/"))
|
|
1510
|
+
return self._unwrap_data(await self.acall_free_get(f"/v1/compute/{goal_id}/outputs/{path}"))
|
|
1511
|
+
|
|
1371
1512
|
def get_compute_budget(self, goal_id: str) -> Any:
|
|
1372
1513
|
"""Get budget breakdown. Blocking."""
|
|
1373
1514
|
return asyncio.get_event_loop().run_until_complete(
|
|
@@ -1379,7 +1520,12 @@ class OneShotClient:
|
|
|
1379
1520
|
return self._unwrap_data(await self.acall_free_get(f"/v1/compute/{goal_id}/budget"))
|
|
1380
1521
|
|
|
1381
1522
|
def cancel_compute_goal(self, goal_id: str, reason: Optional[str] = None) -> Any:
|
|
1382
|
-
"""Cancel a goal; remaining budget is credited. Blocking.
|
|
1523
|
+
"""Cancel a goal; remaining budget is credited. Blocking.
|
|
1524
|
+
|
|
1525
|
+
When reservations are still in flight the server answers 202 and the
|
|
1526
|
+
refund is deferred: the result then has ``status == "pending_settlement"``
|
|
1527
|
+
(no exception is raised).
|
|
1528
|
+
"""
|
|
1383
1529
|
return asyncio.get_event_loop().run_until_complete(
|
|
1384
1530
|
self.acancel_compute_goal(goal_id, reason)
|
|
1385
1531
|
)
|
|
@@ -1387,11 +1533,17 @@ class OneShotClient:
|
|
|
1387
1533
|
async def acancel_compute_goal(
|
|
1388
1534
|
self, goal_id: str, reason: Optional[str] = None
|
|
1389
1535
|
) -> Any:
|
|
1390
|
-
"""Cancel a goal; remaining budget is credited. Async.
|
|
1536
|
+
"""Cancel a goal; remaining budget is credited. Async.
|
|
1537
|
+
|
|
1538
|
+
A 202 ``pending_settlement`` answer is returned as a result, not raised.
|
|
1539
|
+
"""
|
|
1391
1540
|
payload: dict[str, Any] = {"reason": reason} if reason is not None else {}
|
|
1392
|
-
|
|
1541
|
+
result = self._unwrap_data(
|
|
1393
1542
|
await self.acall_free_post(f"/v1/compute/{goal_id}/cancel", payload, scope="write")
|
|
1394
1543
|
)
|
|
1544
|
+
if isinstance(result, dict) and result.get("status") == "pending_settlement":
|
|
1545
|
+
result = {"goal_id": goal_id, **{k: v for k, v in result.items() if k != "success"}}
|
|
1546
|
+
return result
|
|
1395
1547
|
|
|
1396
1548
|
def respond_to_compute_task(
|
|
1397
1549
|
self,
|
|
@@ -1460,7 +1612,11 @@ class OneShotClient:
|
|
|
1460
1612
|
)
|
|
1461
1613
|
|
|
1462
1614
|
async def afund_compute_goal(self, goal_id: str, amount: float) -> Any:
|
|
1463
|
-
"""Top up budget on a recurring goal
|
|
1615
|
+
"""Top up budget on a recurring goal, or on any goal that ended with
|
|
1616
|
+
``end_reason == "budget_exhausted"`` (it is set back to ``active`` and woken, keeping
|
|
1617
|
+
its tasks and workspace). Other endings and passed deadlines are refused; a refund
|
|
1618
|
+
still settling answers a retryable ``refund_pending`` (409). Quote+pay (x402) via
|
|
1619
|
+
`acall_tool`."""
|
|
1464
1620
|
if not amount or amount <= 0:
|
|
1465
1621
|
raise ValidationError("amount must be a positive number", "amount")
|
|
1466
1622
|
return await self.acall_tool(
|
|
@@ -1949,29 +2105,29 @@ class OneShotClient:
|
|
|
1949
2105
|
"""Enrich a company from domain, name, LinkedIn URL, or ticker. Async."""
|
|
1950
2106
|
return await self.acall_tool("/v1/tools/enrich/company", kwargs)
|
|
1951
2107
|
|
|
1952
|
-
def
|
|
2108
|
+
def business_find(self, **kwargs: Any) -> Any:
|
|
1953
2109
|
"""Discover local businesses (restaurants, contractors, practices) by category/keywords x location. Blocking."""
|
|
1954
|
-
return self.call_tool("/v1/tools/
|
|
2110
|
+
return self.call_tool("/v1/tools/find/business", kwargs)
|
|
1955
2111
|
|
|
1956
|
-
async def
|
|
2112
|
+
async def abusiness_find(self, **kwargs: Any) -> Any:
|
|
1957
2113
|
"""Discover local businesses (restaurants, contractors, practices) by category/keywords x location. Async."""
|
|
1958
|
-
return await self.acall_tool("/v1/tools/
|
|
2114
|
+
return await self.acall_tool("/v1/tools/find/business", kwargs)
|
|
1959
2115
|
|
|
1960
|
-
def
|
|
2116
|
+
def business_resolve(self, name: str, **kwargs: Any) -> Any:
|
|
1961
2117
|
"""Resolve a business name + one locating field (address/city/postal_code/phone) to its domain, phone, status. Blocking."""
|
|
1962
|
-
return self.call_tool("/v1/tools/
|
|
2118
|
+
return self.call_tool("/v1/tools/resolve/business", {"name": name, **kwargs})
|
|
1963
2119
|
|
|
1964
|
-
async def
|
|
2120
|
+
async def abusiness_resolve(self, name: str, **kwargs: Any) -> Any:
|
|
1965
2121
|
"""Resolve a business name + one locating field (address/city/postal_code/phone) to its domain, phone, status. Async."""
|
|
1966
|
-
return await self.acall_tool("/v1/tools/
|
|
2122
|
+
return await self.acall_tool("/v1/tools/resolve/business", {"name": name, **kwargs})
|
|
1967
2123
|
|
|
1968
|
-
def
|
|
2124
|
+
def record_solicitations_find(self, naics: list[str], **kwargs: Any) -> Any:
|
|
1969
2125
|
"""Federal Sources Sought / Presolicitation notices (SAM.gov) by NAICS code with the contracting officer's contact. Result carries `data_as_of` (snapshot behind the rows). Blocking."""
|
|
1970
|
-
return self.call_tool("/v1/tools/
|
|
2126
|
+
return self.call_tool("/v1/tools/find/record-solicitations", {"naics": naics, **kwargs})
|
|
1971
2127
|
|
|
1972
|
-
async def
|
|
2128
|
+
async def arecord_solicitations_find(self, naics: list[str], **kwargs: Any) -> Any:
|
|
1973
2129
|
"""Federal Sources Sought / Presolicitation notices (SAM.gov) by NAICS code with the contracting officer's contact. Result carries `data_as_of` (snapshot behind the rows). Async."""
|
|
1974
|
-
return await self.acall_tool("/v1/tools/
|
|
2130
|
+
return await self.acall_tool("/v1/tools/find/record-solicitations", {"naics": naics, **kwargs})
|
|
1975
2131
|
|
|
1976
2132
|
def find_email(self, company_domain: str, *, full_name: Optional[str] = None, first_name: Optional[str] = None, last_name: Optional[str] = None, **kwargs: Any) -> Any:
|
|
1977
2133
|
"""Find a person's email address. Blocking."""
|
|
@@ -2077,6 +2233,16 @@ class OneShotClient:
|
|
|
2077
2233
|
"""Build a website. Async."""
|
|
2078
2234
|
return await self.acall_tool("/v1/tools/build", {"product": product, **kwargs})
|
|
2079
2235
|
|
|
2236
|
+
def update_build(self, build_id: str, changes: str, **kwargs: Any) -> Any:
|
|
2237
|
+
"""Apply natural-language changes to an existing build (same route as build). Blocking."""
|
|
2238
|
+
transport = {k: kwargs.pop(k) for k in _BUILD_TRANSPORT_KWARGS if k in kwargs}
|
|
2239
|
+
return self.call_tool("/v1/tools/build", {"build_id": build_id, "changes": changes, **kwargs}, **transport)
|
|
2240
|
+
|
|
2241
|
+
async def aupdate_build(self, build_id: str, changes: str, **kwargs: Any) -> Any:
|
|
2242
|
+
"""Apply natural-language changes to an existing build (same route as build). Async."""
|
|
2243
|
+
transport = {k: kwargs.pop(k) for k in _BUILD_TRANSPORT_KWARGS if k in kwargs}
|
|
2244
|
+
return await self.acall_tool("/v1/tools/build", {"build_id": build_id, "changes": changes, **kwargs}, **transport)
|
|
2245
|
+
|
|
2080
2246
|
def get_balance(self) -> dict:
|
|
2081
2247
|
"""Get unified balance (on-chain USDC + credits). Blocking."""
|
|
2082
2248
|
return self.call_free_get("/v1/tools/balance")
|
|
@@ -2085,6 +2251,69 @@ class OneShotClient:
|
|
|
2085
2251
|
"""Get unified balance (on-chain USDC + credits). Async."""
|
|
2086
2252
|
return await self.acall_free_get("/v1/tools/balance")
|
|
2087
2253
|
|
|
2254
|
+
# Inboxes and notifications (#1273). Free signed reads and writes of the agent's own data.
|
|
2255
|
+
|
|
2256
|
+
@staticmethod
|
|
2257
|
+
def _inbox_params(**values: Any) -> Optional[dict[str, str]]:
|
|
2258
|
+
params = {k: ("true" if v is True else "false" if v is False else str(v)) for k, v in values.items() if v is not None}
|
|
2259
|
+
return params or None
|
|
2260
|
+
|
|
2261
|
+
def inbox(self, *, since: Optional[str] = None, limit: Optional[int] = None, include_body: Optional[bool] = None) -> Any:
|
|
2262
|
+
"""List emails received by the agent, newest first. Free. Blocking."""
|
|
2263
|
+
return self.call_free_get("/v1/tools/inbox", self._inbox_params(since=since, limit=limit, include_body=include_body))
|
|
2264
|
+
|
|
2265
|
+
async def ainbox(self, *, since: Optional[str] = None, limit: Optional[int] = None, include_body: Optional[bool] = None) -> Any:
|
|
2266
|
+
"""List emails received by the agent, newest first. Free. Async."""
|
|
2267
|
+
return await self.acall_free_get("/v1/tools/inbox", self._inbox_params(since=since, limit=limit, include_body=include_body))
|
|
2268
|
+
|
|
2269
|
+
def inbox_get(self, email_id: str) -> Any:
|
|
2270
|
+
"""One received email with its body and attachments. Free. Blocking."""
|
|
2271
|
+
return self.call_free_get(f"/v1/tools/inbox/{quote(email_id, safe='')}")
|
|
2272
|
+
|
|
2273
|
+
async def ainbox_get(self, email_id: str) -> Any:
|
|
2274
|
+
"""One received email with its body and attachments. Free. Async."""
|
|
2275
|
+
return await self.acall_free_get(f"/v1/tools/inbox/{quote(email_id, safe='')}")
|
|
2276
|
+
|
|
2277
|
+
def sms_inbox(self, *, since: Optional[str] = None, limit: Optional[int] = None, from_number: Optional[str] = None) -> Any:
|
|
2278
|
+
"""List text messages received by the agent. Free. Blocking."""
|
|
2279
|
+
return self.call_free_get("/v1/tools/sms/inbox", self._inbox_params(since=since, limit=limit, **{"from": from_number}))
|
|
2280
|
+
|
|
2281
|
+
async def asms_inbox(self, *, since: Optional[str] = None, limit: Optional[int] = None, from_number: Optional[str] = None) -> Any:
|
|
2282
|
+
"""List text messages received by the agent. Free. Async."""
|
|
2283
|
+
return await self.acall_free_get("/v1/tools/sms/inbox", self._inbox_params(since=since, limit=limit, **{"from": from_number}))
|
|
2284
|
+
|
|
2285
|
+
def sms_inbox_get(self, message_id: str) -> Any:
|
|
2286
|
+
"""One received text message. Free. Blocking."""
|
|
2287
|
+
return self.call_free_get(f"/v1/tools/sms/inbox/{quote(message_id, safe='')}")
|
|
2288
|
+
|
|
2289
|
+
async def asms_inbox_get(self, message_id: str) -> Any:
|
|
2290
|
+
"""One received text message. Free. Async."""
|
|
2291
|
+
return await self.acall_free_get(f"/v1/tools/sms/inbox/{quote(message_id, safe='')}")
|
|
2292
|
+
|
|
2293
|
+
def notifications(self, *, unread: Optional[bool] = None, limit: Optional[int] = None) -> Any:
|
|
2294
|
+
"""List the agent's notifications (job completions, failures, budget warnings). Free. Blocking."""
|
|
2295
|
+
return self.call_free_get("/v1/tools/notifications", self._inbox_params(unread=unread, limit=limit))
|
|
2296
|
+
|
|
2297
|
+
async def anotifications(self, *, unread: Optional[bool] = None, limit: Optional[int] = None) -> Any:
|
|
2298
|
+
"""List the agent's notifications (job completions, failures, budget warnings). Free. Async."""
|
|
2299
|
+
return await self.acall_free_get("/v1/tools/notifications", self._inbox_params(unread=unread, limit=limit))
|
|
2300
|
+
|
|
2301
|
+
def mark_notification_read(self, notification_id: str) -> Any:
|
|
2302
|
+
"""Mark one notification as read. Free. Blocking."""
|
|
2303
|
+
return self.call_free_patch(f"/v1/tools/notifications/{quote(notification_id, safe='')}/read", scope="write")
|
|
2304
|
+
|
|
2305
|
+
async def amark_notification_read(self, notification_id: str) -> Any:
|
|
2306
|
+
"""Mark one notification as read. Free. Async."""
|
|
2307
|
+
return await self.acall_free_patch(f"/v1/tools/notifications/{quote(notification_id, safe='')}/read", scope="write")
|
|
2308
|
+
|
|
2309
|
+
def mark_all_notifications_read(self) -> Any:
|
|
2310
|
+
"""Mark every notification as read. Free. Blocking."""
|
|
2311
|
+
return self.call_free_patch("/v1/tools/notifications/read-all", scope="write")
|
|
2312
|
+
|
|
2313
|
+
async def amark_all_notifications_read(self) -> Any:
|
|
2314
|
+
"""Mark every notification as read. Free. Async."""
|
|
2315
|
+
return await self.acall_free_patch("/v1/tools/notifications/read-all", scope="write")
|
|
2316
|
+
|
|
2088
2317
|
# `pause`/`resume` sit behind the API's paidEndpointLimiter (a rate limiter,
|
|
2089
2318
|
# not a price) — no x402 quote, so they use `call_free_post`, not `call_tool`.
|
|
2090
2319
|
|
|
@@ -2118,7 +2347,7 @@ class OneShotClient:
|
|
|
2118
2347
|
|
|
2119
2348
|
async def apause_domain(self, domain: str) -> DomainPoolStatusResult:
|
|
2120
2349
|
"""Take a domain out of rotation without releasing it. Async."""
|
|
2121
|
-
return await self.acall_free_post(self._domain_action_path(domain, "pause"))
|
|
2350
|
+
return await self.acall_free_post(self._domain_action_path(domain, "pause"), scope="write")
|
|
2122
2351
|
|
|
2123
2352
|
def resume_domain(self, domain: str) -> DomainPoolStatusResult:
|
|
2124
2353
|
"""Put a paused domain back into rotation. Blocking."""
|
|
@@ -2130,7 +2359,7 @@ class OneShotClient:
|
|
|
2130
2359
|
Reputation pauses (`pause_reason='low_reputation'`) auto-recover once the
|
|
2131
2360
|
domain is warmed again; a manual pause needs this call.
|
|
2132
2361
|
"""
|
|
2133
|
-
return await self.acall_free_post(self._domain_action_path(domain, "resume"))
|
|
2362
|
+
return await self.acall_free_post(self._domain_action_path(domain, "resume"), scope="write")
|
|
2134
2363
|
|
|
2135
2364
|
@staticmethod
|
|
2136
2365
|
def _domain_action_path(domain: str, action: str) -> str:
|