crewai-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,78 @@
1
+ Metadata-Version: 2.5
2
+ Name: crewai-agentsearch
3
+ Version: 0.1.0
4
+ Summary: CrewAI tools for AgentSearch: free live web search, plus pay-per-call extract and JS render ($0.005 USDC on Base via x402)
5
+ Project-URL: Homepage, https://agentsearchhq.com/?utm_source=crewai-agentsearch&utm_medium=package
6
+ Project-URL: Documentation, https://agentsearchhq.com/agents.md?utm_source=crewai-agentsearch&utm_medium=package
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Keywords: agents,agentsearch,crewai,tools,web-search,x402
10
+ Requires-Python: >=3.10
11
+ Requires-Dist: crewai>=0.100
12
+ Requires-Dist: httpx>=0.25
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
+ # crewai-agentsearch
21
+
22
+ [CrewAI](https://www.crewai.com/) tools for [AgentSearch](https://agentsearchhq.com/?utm_source=crewai-agentsearch&utm_medium=package): live web search for agents. Free search needs no API key.
23
+
24
+ | Tool | Name | Cost |
25
+ |---|---|---|
26
+ | `AgentSearchTool` | `agentsearch_search` | **Free** (25 searches/day per user, up to 5 results, no key). Paid and unlimited if a wallet is set. |
27
+ | `AgentSearchExtractTool` | `agentsearch_extract` | $0.005/call (USDC on Base via x402) |
28
+ | `AgentSearchRenderTool` | `agentsearch_render` | $0.005/call (headless browser for JS pages) |
29
+
30
+ ## Install
31
+
32
+ ```bash
33
+ pip install -U crewai-agentsearch
34
+ ```
35
+
36
+ ## Quickstart
37
+
38
+ ```python
39
+ from crewai import Agent, Crew, Task
40
+ from crewai_agentsearch import AgentSearchTool
41
+
42
+ search = AgentSearchTool()
43
+ print(search.run(query="latest CrewAI release", max_results=3)) # JSON string
44
+
45
+ researcher = Agent(role="Researcher", goal="Find current facts with sources",
46
+ backstory="You always cite URLs.", tools=[search])
47
+ task = Task(description="What changed in the latest CrewAI release?", expected_output="3 bullets with URLs", agent=researcher)
48
+ print(Crew(agents=[researcher], tasks=[task]).kickoff())
49
+ ```
50
+
51
+ Add `AgentSearchExtractTool()` and `AgentSearchRenderTool()` to `tools` once paid mode is set up.
52
+
53
+ ## Paid mode
54
+
55
+ Install the `paid` extra (`pip install "crewai-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.
56
+
57
+ | Env var | Default |
58
+ |---|---|
59
+ | `AGENTSEARCH_MAX_TOTAL_ATOMIC` | `1000000` ($1.00 per process) |
60
+ | `AGENTSEARCH_MAX_PER_CALL_ATOMIC` | `5000` ($0.005; can only be lowered) |
61
+ | `AGENTSEARCH_TIMEOUT_MS` | `30000` |
62
+
63
+ Limits are checked before anything is signed. The key is never logged or returned. Paid results include a `payment` object.
64
+
65
+ ## Errors
66
+
67
+ 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.
68
+
69
+ ## Development
70
+
71
+ ```bash
72
+ pip install -e ".[test]" && pytest # unit tests (mocked)
73
+ AGENTSEARCH_LIVE=1 pytest -k live # one real free search
74
+ ```
75
+
76
+ Docs: https://agentsearchhq.com/agents.md?utm_source=crewai-agentsearch&utm_medium=package
77
+
78
+ MIT
@@ -0,0 +1,59 @@
1
+ # crewai-agentsearch
2
+
3
+ [CrewAI](https://www.crewai.com/) tools for [AgentSearch](https://agentsearchhq.com/?utm_source=crewai-agentsearch&utm_medium=package): live web search for agents. Free search needs no API key.
4
+
5
+ | Tool | Name | Cost |
6
+ |---|---|---|
7
+ | `AgentSearchTool` | `agentsearch_search` | **Free** (25 searches/day per user, up to 5 results, no key). Paid and unlimited if a wallet is set. |
8
+ | `AgentSearchExtractTool` | `agentsearch_extract` | $0.005/call (USDC on Base via x402) |
9
+ | `AgentSearchRenderTool` | `agentsearch_render` | $0.005/call (headless browser for JS pages) |
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ pip install -U crewai-agentsearch
15
+ ```
16
+
17
+ ## Quickstart
18
+
19
+ ```python
20
+ from crewai import Agent, Crew, Task
21
+ from crewai_agentsearch import AgentSearchTool
22
+
23
+ search = AgentSearchTool()
24
+ print(search.run(query="latest CrewAI release", max_results=3)) # JSON string
25
+
26
+ researcher = Agent(role="Researcher", goal="Find current facts with sources",
27
+ backstory="You always cite URLs.", tools=[search])
28
+ task = Task(description="What changed in the latest CrewAI release?", expected_output="3 bullets with URLs", agent=researcher)
29
+ print(Crew(agents=[researcher], tasks=[task]).kickoff())
30
+ ```
31
+
32
+ Add `AgentSearchExtractTool()` and `AgentSearchRenderTool()` to `tools` once paid mode is set up.
33
+
34
+ ## Paid mode
35
+
36
+ Install the `paid` extra (`pip install "crewai-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.
37
+
38
+ | Env var | Default |
39
+ |---|---|
40
+ | `AGENTSEARCH_MAX_TOTAL_ATOMIC` | `1000000` ($1.00 per process) |
41
+ | `AGENTSEARCH_MAX_PER_CALL_ATOMIC` | `5000` ($0.005; can only be lowered) |
42
+ | `AGENTSEARCH_TIMEOUT_MS` | `30000` |
43
+
44
+ Limits are checked before anything is signed. The key is never logged or returned. Paid results include a `payment` object.
45
+
46
+ ## Errors
47
+
48
+ 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.
49
+
50
+ ## Development
51
+
52
+ ```bash
53
+ pip install -e ".[test]" && pytest # unit tests (mocked)
54
+ AGENTSEARCH_LIVE=1 pytest -k live # one real free search
55
+ ```
56
+
57
+ Docs: https://agentsearchhq.com/agents.md?utm_source=crewai-agentsearch&utm_medium=package
58
+
59
+ MIT
@@ -0,0 +1,13 @@
1
+ """CrewAI 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 AgentSearchExtractTool, AgentSearchRenderTool, AgentSearchTool
6
+
7
+ try:
8
+ __version__ = metadata.version("crewai-agentsearch")
9
+ except metadata.PackageNotFoundError: # pragma: no cover
10
+ __version__ = ""
11
+
12
+ __all__ = ["AgentSearchTool", "AgentSearchExtractTool", "AgentSearchRenderTool", "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=crewai-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 crewai-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 crewai-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 'crewai-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}"
File without changes
@@ -0,0 +1,60 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Literal, Optional, Type
4
+
5
+ from crewai.tools import BaseTool
6
+ from pydantic import BaseModel, Field
7
+
8
+ from . import _ops
9
+
10
+
11
+ class SearchInput(BaseModel):
12
+ query: str = Field(description="Search query", min_length=1)
13
+ max_results: Optional[int] = Field(default=None, ge=1, le=5, description="1-5, default 5")
14
+
15
+
16
+ class ExtractInput(BaseModel):
17
+ url: str = Field(description="http(s) URL to extract")
18
+ formats: Optional[list[str]] = Field(default=None, description='e.g. ["markdown"]')
19
+ max_chars: Optional[int] = Field(default=None, gt=0)
20
+
21
+
22
+ class RenderInput(BaseModel):
23
+ url: str = Field(description="http(s) URL to render")
24
+ formats: Optional[list[Literal["markdown", "text", "html", "links"]]] = None
25
+ max_chars: Optional[int] = Field(default=None, gt=0)
26
+ wait_until: Optional[Literal["domcontentloaded", "load", "networkidle"]] = None
27
+ wait_for_selector: Optional[str] = None
28
+ wait_ms: Optional[int] = Field(default=None, ge=0)
29
+
30
+
31
+ class AgentSearchTool(BaseTool):
32
+ """Live web search. Free by default (25/day per user); paid and unlimited when a wallet key is set."""
33
+
34
+ name: str = "agentsearch_search"
35
+ description: str = "Live web search. Returns up to 5 ranked results (title, url, content). Use for current facts or finding URLs."
36
+ args_schema: Type[BaseModel] = SearchInput
37
+ mode: Literal["auto", "free", "paid"] = "auto"
38
+
39
+ def _run(self, query: str, max_results: Optional[int] = None, **_: Any) -> str:
40
+ return _ops.search(query, max_results, self.mode)
41
+
42
+
43
+ class AgentSearchExtractTool(BaseTool):
44
+ name: str = "agentsearch_extract"
45
+ description: str = "Fetch a URL and return its main content as clean text/markdown. Paid: $0.005 per call (needs a wallet)."
46
+ args_schema: Type[BaseModel] = ExtractInput
47
+
48
+ def _run(self, url: str, formats: Optional[list[str]] = None, max_chars: Optional[int] = None, **_: Any) -> str:
49
+ return _ops.paid("extract", url=url, formats=formats, max_chars=max_chars)
50
+
51
+
52
+ class AgentSearchRenderTool(BaseTool):
53
+ name: str = "agentsearch_render"
54
+ description: str = "Render a JavaScript-heavy page in a headless browser and return markdown/text/html/links. Paid: $0.005 per call (needs a wallet)."
55
+ args_schema: Type[BaseModel] = RenderInput
56
+
57
+ def _run(self, url: str, formats: Optional[list[str]] = None, max_chars: Optional[int] = None,
58
+ wait_until: Optional[str] = None, wait_for_selector: Optional[str] = None, wait_ms: Optional[int] = None, **_: Any) -> str:
59
+ return _ops.paid("render", url=url, formats=formats, max_chars=max_chars, wait_until=wait_until,
60
+ wait_for_selector=wait_for_selector, wait_ms=wait_ms)
@@ -0,0 +1,28 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "crewai-agentsearch"
7
+ version = "0.1.0"
8
+ description = "CrewAI tools for AgentSearch: free live web search, plus pay-per-call extract and JS render ($0.005 USDC on Base via x402)"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.10"
12
+ dependencies = ["crewai>=0.100", "httpx>=0.25", "pydantic>=2"]
13
+ keywords = ["crewai", "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=crewai-agentsearch&utm_medium=package"
21
+ Documentation = "https://agentsearchhq.com/agents.md?utm_source=crewai-agentsearch&utm_medium=package"
22
+
23
+ [tool.hatch.build.targets.wheel]
24
+ packages = ["crewai_agentsearch"]
25
+
26
+ [tool.hatch.build.targets.sdist]
27
+ include = ["crewai_agentsearch", "tests", "README.md", "LICENSE", "pyproject.toml"]
28
+
@@ -0,0 +1,43 @@
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
+ from crewai_agentsearch import AgentSearchExtractTool, AgentSearchTool, _client
21
+
22
+
23
+ def test_free_search(monkeypatch):
24
+ seen = mock(monkeypatch, _client, 200, {"results": [{"title": "t", "url": "https://a", "content": "c"}], "provider": "x"})
25
+ out = json.loads(AgentSearchTool().run(query="hi", max_results=2))
26
+ assert out["results"][0]["url"] == "https://a" and "provider" not in out
27
+ assert str(seen[0].url) == "https://relay.agentsearchhq.com/v1/search"
28
+
29
+
30
+ def test_free_limit(monkeypatch):
31
+ mock(monkeypatch, _client, 429, {})
32
+ assert "Free search limit reached" in AgentSearchTool().run(query="hi")
33
+
34
+
35
+ def test_paid_without_wallet(monkeypatch):
36
+ seen = mock(monkeypatch, _client, 200, {})
37
+ assert "no call was made" in AgentSearchExtractTool().run(url="https://example.com") and not seen
38
+
39
+
40
+ @pytest.mark.skipif(not os.environ.get("AGENTSEARCH_LIVE"), reason="set AGENTSEARCH_LIVE=1")
41
+ def test_live_free_search():
42
+ out = json.loads(AgentSearchTool(mode="free").run(query="CrewAI custom tools", max_results=2))
43
+ assert out["results"][0]["url"].startswith("http")