openai-agents-agentsearch 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.
@@ -0,0 +1,6 @@
1
+ __pycache__/
2
+ *.pyc
3
+ dist/
4
+ .pytest_cache/
5
+ *.egg-info/
6
+ .venv/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 AgentSearch / Angel Mendez
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,76 @@
1
+ Metadata-Version: 2.5
2
+ Name: openai-agents-agentsearch
3
+ Version: 0.1.0
4
+ Summary: OpenAI Agents SDK function tools for AgentSearch: free live web search, plus pay-per-call extract and JS render
5
+ Project-URL: Homepage, https://agentsearchhq.com/?utm_source=openai-agents-agentsearch&utm_medium=package
6
+ Project-URL: Documentation, https://agentsearchhq.com/agents.md?utm_source=openai-agents-agentsearch&utm_medium=package
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Keywords: agents,agentsearch,openai,openai-agents,tools,web-search,x402
10
+ Requires-Python: >=3.10
11
+ Requires-Dist: httpx>=0.25
12
+ Requires-Dist: openai-agents>=0.2
13
+ Requires-Dist: pydantic>=2
14
+ Provides-Extra: paid
15
+ Requires-Dist: x402[evm]>=2.0; extra == 'paid'
16
+ Provides-Extra: test
17
+ Requires-Dist: pytest>=7; extra == 'test'
18
+ Description-Content-Type: text/markdown
19
+
20
+ # openai-agents-agentsearch
21
+
22
+ Function tools for the [OpenAI Agents SDK](https://openai.github.io/openai-agents-python/) backed by [AgentSearch](https://agentsearchhq.com/?utm_source=openai-agents-agentsearch&utm_medium=package): live web search for agents. Free search needs no API key.
23
+
24
+ | Tool | Cost |
25
+ |---|---|
26
+ | `agentsearch_search` | **Free** (25 searches/day per user, up to 5 results, no key). Paid and unlimited if a wallet is set. |
27
+ | `agentsearch_extract` | $0.005/call (USDC on Base via x402) |
28
+ | `agentsearch_render` | $0.005/call (headless browser for JS pages) |
29
+
30
+ ## Install
31
+
32
+ ```bash
33
+ pip install -U openai-agents-agentsearch
34
+ ```
35
+
36
+ ## Quickstart
37
+
38
+ ```python
39
+ from agents import Agent, Runner
40
+ from openai_agents_agentsearch import agentsearch_search, agentsearch_tools
41
+
42
+ agent = Agent(
43
+ name="Researcher",
44
+ instructions="Answer with current facts. Cite URLs.",
45
+ tools=[agentsearch_search], # or agentsearch_tools() for search + extract + render
46
+ )
47
+ result = Runner.run_sync(agent, "What changed in the latest OpenAI Agents SDK release?")
48
+ print(result.final_output)
49
+ ```
50
+
51
+ ## Paid mode
52
+
53
+ Install the `paid` extra (`pip install "openai-agents-agentsearch[paid]"`) and set `AGENTSEARCH_WALLET_PRIVATE_KEY` to the private key of a **new wallet made only for this**, funded with a little USDC on Base (e.g. $1; no ETH needed). Each call costs $0.005 and is paid over x402 through the Pocket Network Agentic Portal using the official x402 Python SDK. With a key set, search also goes paid and unlimited.
54
+
55
+ | Env var | Default |
56
+ |---|---|
57
+ | `AGENTSEARCH_MAX_TOTAL_ATOMIC` | `1000000` ($1.00 per process) |
58
+ | `AGENTSEARCH_MAX_PER_CALL_ATOMIC` | `5000` ($0.005; can only be lowered) |
59
+ | `AGENTSEARCH_TIMEOUT_MS` | `30000` |
60
+
61
+ Limits are checked before anything is signed. The key is never logged or returned. Paid results include a `payment` object.
62
+
63
+ ## Errors
64
+
65
+ Tools return `"Error: ..."` strings instead of raising. A 429 from the free relay means the daily limit (25 searches/day per user) was hit, and the message explains how to upgrade.
66
+
67
+ ## Development
68
+
69
+ ```bash
70
+ pip install -e ".[test]" && pytest # unit tests (mocked)
71
+ AGENTSEARCH_LIVE=1 pytest -k live # one real free search
72
+ ```
73
+
74
+ Docs: https://agentsearchhq.com/agents.md?utm_source=openai-agents-agentsearch&utm_medium=package
75
+
76
+ MIT
@@ -0,0 +1,57 @@
1
+ # openai-agents-agentsearch
2
+
3
+ Function tools for the [OpenAI Agents SDK](https://openai.github.io/openai-agents-python/) backed by [AgentSearch](https://agentsearchhq.com/?utm_source=openai-agents-agentsearch&utm_medium=package): live web search for agents. Free search needs no API key.
4
+
5
+ | Tool | Cost |
6
+ |---|---|
7
+ | `agentsearch_search` | **Free** (25 searches/day per user, up to 5 results, no key). Paid and unlimited if a wallet is set. |
8
+ | `agentsearch_extract` | $0.005/call (USDC on Base via x402) |
9
+ | `agentsearch_render` | $0.005/call (headless browser for JS pages) |
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ pip install -U openai-agents-agentsearch
15
+ ```
16
+
17
+ ## Quickstart
18
+
19
+ ```python
20
+ from agents import Agent, Runner
21
+ from openai_agents_agentsearch import agentsearch_search, agentsearch_tools
22
+
23
+ agent = Agent(
24
+ name="Researcher",
25
+ instructions="Answer with current facts. Cite URLs.",
26
+ tools=[agentsearch_search], # or agentsearch_tools() for search + extract + render
27
+ )
28
+ result = Runner.run_sync(agent, "What changed in the latest OpenAI Agents SDK release?")
29
+ print(result.final_output)
30
+ ```
31
+
32
+ ## Paid mode
33
+
34
+ Install the `paid` extra (`pip install "openai-agents-agentsearch[paid]"`) and set `AGENTSEARCH_WALLET_PRIVATE_KEY` to the private key of a **new wallet made only for this**, funded with a little USDC on Base (e.g. $1; no ETH needed). Each call costs $0.005 and is paid over x402 through the Pocket Network Agentic Portal using the official x402 Python SDK. With a key set, search also goes paid and unlimited.
35
+
36
+ | Env var | Default |
37
+ |---|---|
38
+ | `AGENTSEARCH_MAX_TOTAL_ATOMIC` | `1000000` ($1.00 per process) |
39
+ | `AGENTSEARCH_MAX_PER_CALL_ATOMIC` | `5000` ($0.005; can only be lowered) |
40
+ | `AGENTSEARCH_TIMEOUT_MS` | `30000` |
41
+
42
+ Limits are checked before anything is signed. The key is never logged or returned. Paid results include a `payment` object.
43
+
44
+ ## Errors
45
+
46
+ Tools return `"Error: ..."` strings instead of raising. A 429 from the free relay means the daily limit (25 searches/day per user) was hit, and the message explains how to upgrade.
47
+
48
+ ## Development
49
+
50
+ ```bash
51
+ pip install -e ".[test]" && pytest # unit tests (mocked)
52
+ AGENTSEARCH_LIVE=1 pytest -k live # one real free search
53
+ ```
54
+
55
+ Docs: https://agentsearchhq.com/agents.md?utm_source=openai-agents-agentsearch&utm_medium=package
56
+
57
+ MIT
@@ -0,0 +1,13 @@
1
+ """OpenAI Agents SDK function tools for AgentSearch."""
2
+ from importlib import metadata
3
+
4
+ from ._client import AgentSearchError, FREE_LIMIT_MESSAGE, reset_session_budget, session_spent_atomic
5
+ from .tools import agentsearch_extract, agentsearch_render, agentsearch_search, agentsearch_tools
6
+
7
+ try:
8
+ __version__ = metadata.version("openai-agents-agentsearch")
9
+ except metadata.PackageNotFoundError: # pragma: no cover
10
+ __version__ = ""
11
+
12
+ __all__ = ["agentsearch_search", "agentsearch_extract", "agentsearch_render", "agentsearch_tools", "AgentSearchError",
13
+ "FREE_LIMIT_MESSAGE", "reset_session_budget", "session_spent_atomic", "__version__"]
@@ -0,0 +1,264 @@
1
+ """HTTP layer: free relay search and x402 pay-per-call via the Pocket Network Agentic Portal.
2
+
3
+ The wallet key is read from env, used only to sign, and never logged or returned.
4
+ """
5
+ from __future__ import annotations
6
+
7
+ import json
8
+ import os
9
+ import re
10
+ import threading
11
+ from typing import Any, Optional
12
+
13
+ import httpx
14
+
15
+ FREE_RELAY_URL = "https://relay.agentsearchhq.com"
16
+ FREE_RELAY_DAILY_LIMIT = 25
17
+ DEFAULT_PORTAL_URL = "https://agent.pocket.network"
18
+ BASE_NETWORK = "eip155:8453"
19
+ USDC_BASE = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
20
+ PORTAL_PAYEE = "0xF732ea490c5766071785a2310523f7fA2CEbB829"
21
+ MAX_PER_CALL_ATOMIC = 5000 # $0.005 hard ceiling
22
+ DEFAULT_SESSION_CAP_ATOMIC = 1_000_000 # $1.00
23
+ KEY_ENV_VARS = ("AGENTSEARCH_WALLET_PRIVATE_KEY", "POCKET_PRIVATE_KEY")
24
+ SERVICES = {
25
+ "search": ("agentsearch-web-search-v1", "/v1/search"),
26
+ "extract": ("agentsearch-web-extract-v1", "/v1/extract"),
27
+ "render": ("agentsearch-web-render-v1", "/v1/render"),
28
+ }
29
+ DOCS = "https://agentsearchhq.com/?utm_source=openai-agents-agentsearch&utm_medium=package"
30
+
31
+ FREE_LIMIT_MESSAGE = (
32
+ f"Free search limit reached ({FREE_RELAY_DAILY_LIMIT} searches per day per user; resets daily). "
33
+ "For unlimited search plus extract/render, set AGENTSEARCH_WALLET_PRIVATE_KEY to the key of a NEW wallet "
34
+ "funded with a little USDC on Base ($0.005 per call via x402, capped by AGENTSEARCH_MAX_TOTAL_ATOMIC, "
35
+ f"default $1.00) and install openai-agents-agentsearch[paid]. Docs: {DOCS}"
36
+ )
37
+
38
+
39
+ def paid_only_message(name: str) -> str:
40
+ return (
41
+ f"{name} is paid ($0.005 per call in USDC on Base via x402). No wallet is configured, so no call was made. "
42
+ "Set AGENTSEARCH_WALLET_PRIVATE_KEY to the key of a NEW wallet funded with a little USDC on Base and "
43
+ f"install openai-agents-agentsearch[paid]. Docs: {DOCS}"
44
+ )
45
+
46
+
47
+ class AgentSearchError(Exception):
48
+ def __init__(self, message: str, status: Optional[int] = None):
49
+ super().__init__(message)
50
+ self.status = status
51
+
52
+
53
+ def timeout_s() -> float:
54
+ try:
55
+ n = int(os.environ.get("AGENTSEARCH_TIMEOUT_MS", ""))
56
+ if 1000 <= n <= 300_000:
57
+ return n / 1000
58
+ except ValueError:
59
+ pass
60
+ return 30.0
61
+
62
+
63
+ def _strip_provider(d: Any) -> Any:
64
+ if isinstance(d, dict):
65
+ d.pop("provider", None)
66
+ return d
67
+
68
+
69
+ def free_search(query: str, max_results: Optional[int] = None, client: Optional[httpx.Client] = None) -> Any:
70
+ body: dict[str, Any] = {"query": query}
71
+ if max_results is not None:
72
+ body["max_results"] = max_results
73
+ c = client or httpx.Client(timeout=timeout_s())
74
+ try:
75
+ r = c.post(f"{FREE_RELAY_URL}/v1/search", json=body, headers={"accept": "application/json"})
76
+ finally:
77
+ if client is None:
78
+ c.close()
79
+ if r.status_code == 429:
80
+ raise AgentSearchError(FREE_LIMIT_MESSAGE, 429)
81
+ if r.status_code >= 400:
82
+ raise AgentSearchError(f"AgentSearch relay HTTP {r.status_code}: {r.text[:500]}", r.status_code)
83
+ try:
84
+ return _strip_provider(r.json())
85
+ except ValueError:
86
+ return {"raw": r.text}
87
+
88
+
89
+ # ---------- paid mode ----------
90
+
91
+ def has_wallet_key() -> bool:
92
+ return any((os.environ.get(n) or "").strip() for n in KEY_ENV_VARS)
93
+
94
+
95
+ def _read_wallet_key() -> Optional[str]:
96
+ for name in KEY_ENV_VARS:
97
+ raw = (os.environ.get(name) or "").strip()
98
+ if not raw:
99
+ continue
100
+ if not re.fullmatch(r"(0x)?[0-9a-fA-F]{64}", raw):
101
+ raise AgentSearchError(
102
+ f"{name} is set but is not a private key (expected 64 hex characters, optional 0x prefix). "
103
+ "The value is not shown. Nothing was signed."
104
+ )
105
+ return raw if raw.startswith("0x") else "0x" + raw
106
+ return None
107
+
108
+
109
+ def _atomic_env(names: tuple[str, ...], fallback: int) -> int:
110
+ for name in names:
111
+ raw = (os.environ.get(name) or "").strip()
112
+ if not raw:
113
+ continue
114
+ if not raw.isdigit():
115
+ raise AgentSearchError(f"{name} must be a whole number of USDC atomic units (1000000 = $1.00). Nothing was signed.")
116
+ return int(raw)
117
+ return fallback
118
+
119
+
120
+ def usd(atomic: int) -> str:
121
+ return f"${atomic // 1_000_000}.{atomic % 1_000_000:06d}"
122
+
123
+
124
+ class _Budget:
125
+ def __init__(self) -> None:
126
+ self.spent = 0
127
+ self.lock = threading.Lock()
128
+
129
+
130
+ _budget = _Budget()
131
+
132
+
133
+ def reset_session_budget() -> None:
134
+ global _budget
135
+ _budget = _Budget()
136
+
137
+
138
+ def session_spent_atomic() -> int:
139
+ return _budget.spent
140
+
141
+
142
+ def _eq(a: Any, b: str) -> bool:
143
+ return isinstance(a, str) and a.lower() == b.lower()
144
+
145
+
146
+ def select_accept(accepts: list[Any], ceiling: int) -> Any:
147
+ why = []
148
+ for a in accepts or []:
149
+ if a.scheme != "exact":
150
+ why.append(f"{a.scheme} scheme"); continue
151
+ if a.network != BASE_NETWORK:
152
+ why.append(f"network {a.network}"); continue
153
+ if not _eq(a.asset, USDC_BASE):
154
+ why.append(f"asset {a.asset} is not USDC on Base"); continue
155
+ if not _eq(a.pay_to, PORTAL_PAYEE):
156
+ why.append(f"payee {a.pay_to} is not the portal's"); continue
157
+ if not str(a.amount).isdigit() or int(a.amount) <= 0:
158
+ why.append(f"amount {a.amount}"); continue
159
+ if int(a.amount) > ceiling:
160
+ why.append(f"amount {a.amount} exceeds the cap of {ceiling} atomic units"); continue
161
+ return a
162
+ raise AgentSearchError(f"No acceptable payment option ({'; '.join(why) or 'none offered'}). Nothing was signed.")
163
+
164
+
165
+ def call_portal(op: str, body: dict[str, Any], client: Optional[httpx.Client] = None) -> dict[str, Any]:
166
+ """Unpaid POST -> 402 -> limits checked -> sign (x402 SDK) -> retry once with PAYMENT-SIGNATURE."""
167
+ try:
168
+ from eth_account import Account
169
+ from x402 import x402ClientSync
170
+ from x402.http import x402HTTPClientSync
171
+ from x402.mechanisms.evm.exact import register_exact_evm_client
172
+ except ImportError as e: # pragma: no cover
173
+ raise AgentSearchError(
174
+ "Paid calls need the x402 SDK: pip install 'openai-agents-agentsearch[paid]'. Nothing was signed."
175
+ ) from e
176
+
177
+ key = _read_wallet_key()
178
+ if not key:
179
+ raise AgentSearchError(paid_only_message(f"agentsearch_{op}"))
180
+ portal = (os.environ.get("AGENTSEARCH_PORTAL_URL") or DEFAULT_PORTAL_URL).strip().rstrip("/")
181
+ if not portal.startswith("https://") and not re.match(r"http://(localhost|127\.)", portal):
182
+ raise AgentSearchError("AGENTSEARCH_PORTAL_URL must be https://. Nothing was signed.")
183
+ session_cap = _atomic_env(("AGENTSEARCH_MAX_TOTAL_ATOMIC", "POCKET_MAX_TOTAL_ATOMIC"), DEFAULT_SESSION_CAP_ATOMIC)
184
+ per_call = min(_atomic_env(("AGENTSEARCH_MAX_PER_CALL_ATOMIC", "POCKET_MAX_PER_CALL_ATOMIC"), MAX_PER_CALL_ATOMIC), MAX_PER_CALL_ATOMIC)
185
+ service_id, path = SERVICES[op]
186
+ url = f"{portal}/v1/{service_id}{path}"
187
+
188
+ c = client or httpx.Client(timeout=timeout_s())
189
+ try:
190
+ unpaid = c.post(url, json=body)
191
+ if unpaid.status_code != 402:
192
+ raise AgentSearchError(f"Expected 402 from the portal, got {unpaid.status_code}: {unpaid.text[:300]}. Nothing was signed.", unpaid.status_code)
193
+ http = x402HTTPClientSync(x402ClientSync())
194
+ try:
195
+ required = http.get_payment_required_response(lambda n: unpaid.headers.get(n), unpaid.content)
196
+ except Exception as e:
197
+ raise AgentSearchError(f"The portal's 402 is not valid x402 v2 ({e}). Nothing was signed.")
198
+ if getattr(required, "x402_version", None) != 2:
199
+ raise AgentSearchError("Unsupported x402 version. Nothing was signed.")
200
+ res_url = (getattr(getattr(required, "resource", None), "url", "") or "").rstrip("/")
201
+ if not res_url.endswith(f"/v1/{service_id}"):
202
+ raise AgentSearchError(f"The 402 is for resource {res_url}, not {service_id}. Nothing was signed.")
203
+
204
+ with _budget.lock:
205
+ remaining = max(session_cap - _budget.spent, 0)
206
+ ceiling = min(per_call, remaining)
207
+ if ceiling <= 0:
208
+ raise AgentSearchError(
209
+ f"Session spend cap reached: {usd(_budget.spent)} of {usd(session_cap)} signed. "
210
+ "Raise AGENTSEARCH_MAX_TOTAL_ATOMIC to spend more. Nothing was signed."
211
+ )
212
+ chosen = select_accept(list(required.accepts), ceiling)
213
+ amount = int(chosen.amount)
214
+
215
+ def selector(_v: int, options: list[Any]) -> Any:
216
+ for o in options:
217
+ if o.scheme == chosen.scheme and o.network == chosen.network and _eq(o.asset, chosen.asset) and _eq(o.pay_to, chosen.pay_to) and str(o.amount) == str(chosen.amount):
218
+ return o
219
+ raise AgentSearchError("Selected option disappeared. Nothing was signed.")
220
+
221
+ account = Account.from_key(key)
222
+ signer_client = register_exact_evm_client(x402ClientSync(selector), account, networks=BASE_NETWORK)
223
+ payer = x402HTTPClientSync(signer_client)
224
+ signed = payer.create_payment_payload(required)
225
+ auth = (signed.payload or {}).get("authorization") or {}
226
+ if not _eq(auth.get("to"), PORTAL_PAYEE) or int(auth.get("value", 0)) != amount:
227
+ raise AgentSearchError("The signed authorization does not match the approved terms; it was discarded and not sent.")
228
+ _budget.spent += amount # counted at signing
229
+
230
+ try:
231
+ paid = c.post(url, json=body, headers=payer.encode_payment_signature_header(signed))
232
+ except Exception as e:
233
+ raise AgentSearchError(f"A payment of {usd(amount)} was signed but the paid request failed ({e}). Check the wallet before retrying.")
234
+ if not 200 <= paid.status_code < 300:
235
+ raise AgentSearchError(
236
+ f"The portal answered {paid.status_code}: {paid.text[:300]}. Non-2xx answers are not settled; "
237
+ f"the signed {usd(amount)} still counts against this session's cap.", paid.status_code)
238
+ receipt = None
239
+ try:
240
+ receipt = payer.get_payment_settle_response(lambda n: paid.headers.get(n))
241
+ except Exception:
242
+ pass
243
+ tx = getattr(receipt, "transaction", None) if receipt else None
244
+ payment = {
245
+ "amountAtomic": str(amount), "amountUsd": usd(amount), "asset": "USDC", "network": BASE_NETWORK,
246
+ "payTo": PORTAL_PAYEE, "payer": account.address, "serviceId": service_id,
247
+ "settled": bool(getattr(receipt, "success", False)), "transaction": tx,
248
+ }
249
+ if tx:
250
+ payment["explorer"] = f"https://basescan.org/tx/{tx}"
251
+ try:
252
+ env = paid.json()
253
+ except ValueError:
254
+ env = {"raw": paid.text}
255
+ data = env.get("data", env) if isinstance(env, dict) else env
256
+ out = _strip_provider(data) if isinstance(data, dict) else {"data": data}
257
+ return {**out, "payment": payment}
258
+ finally:
259
+ if client is None:
260
+ c.close()
261
+
262
+
263
+ def dumps(v: Any) -> str:
264
+ return json.dumps(v, ensure_ascii=False)
@@ -0,0 +1,32 @@
1
+ """Framework-neutral operations. Each returns a JSON string, or an "Error: ..." string (never raises)."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Any, Optional
5
+
6
+ from . import _client as c
7
+
8
+
9
+ def search(query: str, max_results: Optional[int] = None, mode: str = "auto") -> str:
10
+ try:
11
+ if not query or not str(query).strip():
12
+ raise c.AgentSearchError("query must not be empty")
13
+ if max_results is not None:
14
+ max_results = max(1, min(int(max_results), 5))
15
+ if mode == "paid" or (mode == "auto" and c.has_wallet_key()):
16
+ body: dict[str, Any] = {"query": query}
17
+ if max_results:
18
+ body["max_results"] = max_results
19
+ return c.dumps(c.call_portal("search", body))
20
+ return c.dumps(c.free_search(query, max_results))
21
+ except Exception as e:
22
+ return f"Error: {e}"
23
+
24
+
25
+ def paid(op: str, **kwargs: Any) -> str:
26
+ try:
27
+ if not c.has_wallet_key():
28
+ raise c.AgentSearchError(c.paid_only_message(f"agentsearch_{op}"))
29
+ body = {k: v for k, v in kwargs.items() if v is not None}
30
+ return c.dumps(c.call_portal(op, body))
31
+ except Exception as e:
32
+ return f"Error: {e}"
@@ -0,0 +1,47 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import List, Literal, Optional
4
+
5
+ from agents import FunctionTool, function_tool
6
+
7
+ from . import _ops
8
+
9
+
10
+ @function_tool
11
+ def agentsearch_search(query: str, max_results: Optional[int] = None) -> str:
12
+ """Live web search. Returns JSON with up to 5 ranked results (title, url, content). Free by default.
13
+
14
+ Args:
15
+ query: the search query.
16
+ max_results: 1-5, default 5.
17
+ """
18
+ return _ops.search(query, max_results)
19
+
20
+
21
+ @function_tool
22
+ def agentsearch_extract(url: str, max_chars: Optional[int] = None) -> str:
23
+ """Fetch a URL and return its main content as clean markdown. Paid: $0.005 per call (needs a wallet).
24
+
25
+ Args:
26
+ url: http(s) URL to extract.
27
+ max_chars: optional cap on returned characters.
28
+ """
29
+ return _ops.paid("extract", url=url, formats=["markdown"], max_chars=max_chars)
30
+
31
+
32
+ @function_tool
33
+ def agentsearch_render(url: str, max_chars: Optional[int] = None,
34
+ wait_until: Optional[Literal["domcontentloaded", "load", "networkidle"]] = None) -> str:
35
+ """Render a JavaScript-heavy page in a headless browser and return markdown. Paid: $0.005 per call (needs a wallet).
36
+
37
+ Args:
38
+ url: http(s) URL to render.
39
+ max_chars: optional cap on returned characters.
40
+ wait_until: page load event to wait for.
41
+ """
42
+ return _ops.paid("render", url=url, formats=["markdown"], max_chars=max_chars, wait_until=wait_until)
43
+
44
+
45
+ def agentsearch_tools(include_paid: bool = True) -> List[FunctionTool]:
46
+ """All AgentSearch tools. Pass include_paid=False for free search only."""
47
+ return [agentsearch_search, agentsearch_extract, agentsearch_render] if include_paid else [agentsearch_search]
@@ -0,0 +1,28 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "openai-agents-agentsearch"
7
+ version = "0.1.0"
8
+ description = "OpenAI Agents SDK function tools for AgentSearch: free live web search, plus pay-per-call extract and JS render"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.10"
12
+ dependencies = ["openai-agents>=0.2", "httpx>=0.25", "pydantic>=2"]
13
+ keywords = ["openai-agents", "openai", "agentsearch", "web-search", "tools", "agents", "x402"]
14
+
15
+ [project.optional-dependencies]
16
+ paid = ["x402[evm]>=2.0"]
17
+ test = ["pytest>=7"]
18
+
19
+ [project.urls]
20
+ Homepage = "https://agentsearchhq.com/?utm_source=openai-agents-agentsearch&utm_medium=package"
21
+ Documentation = "https://agentsearchhq.com/agents.md?utm_source=openai-agents-agentsearch&utm_medium=package"
22
+
23
+ [tool.hatch.build.targets.wheel]
24
+ packages = ["openai_agents_agentsearch"]
25
+
26
+ [tool.hatch.build.targets.sdist]
27
+ include = ["openai_agents_agentsearch", "tests", "README.md", "LICENSE", "pyproject.toml"]
28
+
@@ -0,0 +1,52 @@
1
+ import json
2
+ import os
3
+
4
+ import httpx
5
+ import pytest
6
+
7
+
8
+ def mock(monkeypatch, mod, status, body):
9
+ real = httpx.Client
10
+ seen = []
11
+
12
+ def handler(r):
13
+ seen.append(r)
14
+ return httpx.Response(status, json=body)
15
+ monkeypatch.setattr(mod.httpx, "Client", lambda **kw: real(transport=httpx.MockTransport(handler)))
16
+ for n in ("AGENTSEARCH_WALLET_PRIVATE_KEY", "POCKET_PRIVATE_KEY"):
17
+ monkeypatch.delenv(n, raising=False)
18
+ return seen
19
+
20
+ import asyncio
21
+
22
+ from openai_agents_agentsearch import agentsearch_search, agentsearch_tools, _client
23
+
24
+
25
+ def call(tool, args):
26
+ from agents.tool_context import ToolContext
27
+ a = json.dumps(args)
28
+ ctx = ToolContext(context=None, tool_name=tool.name, tool_call_id="1", tool_arguments=a)
29
+ return asyncio.run(tool.on_invoke_tool(ctx, a))
30
+
31
+
32
+ def test_tools_list():
33
+ assert [t.name for t in agentsearch_tools()] == ["agentsearch_search", "agentsearch_extract", "agentsearch_render"]
34
+ assert "query" in agentsearch_search.params_json_schema["properties"]
35
+
36
+
37
+ def test_free_search(monkeypatch):
38
+ mock(monkeypatch, _client, 200, {"results": [{"title": "t", "url": "https://a", "content": "c"}]})
39
+ out = json.loads(call(agentsearch_search, {"query": "hi", "max_results": 1}))
40
+ assert out["results"][0]["url"] == "https://a"
41
+
42
+
43
+ def test_paid_without_wallet(monkeypatch):
44
+ seen = mock(monkeypatch, _client, 200, {})
45
+ assert "no call was made" in call(agentsearch_tools()[1], {"url": "https://example.com", "max_chars": None}) and not seen
46
+
47
+
48
+ @pytest.mark.skipif(not os.environ.get("AGENTSEARCH_LIVE"), reason="set AGENTSEARCH_LIVE=1")
49
+ def test_live_free_search():
50
+ from openai_agents_agentsearch import _ops
51
+ out = json.loads(_ops.search("OpenAI Agents SDK function tools", 2, "free"))
52
+ assert out["results"][0]["url"].startswith("http")