llama-index-tools-clawfetch 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.
- llama_index_tools_clawfetch-0.1.0/.gitignore +7 -0
- llama_index_tools_clawfetch-0.1.0/CHANGELOG.md +6 -0
- llama_index_tools_clawfetch-0.1.0/LICENSE +21 -0
- llama_index_tools_clawfetch-0.1.0/PKG-INFO +82 -0
- llama_index_tools_clawfetch-0.1.0/README.md +63 -0
- llama_index_tools_clawfetch-0.1.0/llama_index/tools/clawfetch/__init__.py +3 -0
- llama_index_tools_clawfetch-0.1.0/llama_index/tools/clawfetch/base.py +128 -0
- llama_index_tools_clawfetch-0.1.0/llama_index/tools/clawfetch/py.typed +0 -0
- llama_index_tools_clawfetch-0.1.0/pyproject.toml +45 -0
- llama_index_tools_clawfetch-0.1.0/tests/test_clawfetch_tool_spec.py +85 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ClawFetch
|
|
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,82 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: llama-index-tools-clawfetch
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: llama-index tools clawfetch integration — pay-per-call web intelligence for AI agents (x402, USDC on Base)
|
|
5
|
+
Project-URL: Homepage, https://api.clawfetch.ai
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Keywords: ai-agents,clawfetch,llama-index,tools,web-scraping,x402
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Requires-Dist: clawfetch<0.3,>=0.2.2
|
|
15
|
+
Requires-Dist: llama-index-core<0.15,>=0.12
|
|
16
|
+
Provides-Extra: test
|
|
17
|
+
Requires-Dist: pytest>=8; extra == 'test'
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# llama-index-tools-clawfetch
|
|
21
|
+
|
|
22
|
+
LlamaIndex tool spec for [ClawFetch](https://api.clawfetch.ai), a pay-per-call web intelligence API for AI agents. Each call is paid with an x402 micropayment in USDC on Base.
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install llama-index-tools-clawfetch
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Wallet
|
|
31
|
+
|
|
32
|
+
You need a Base wallet private key that holds USDC. There is no API key and no signup. Payments are gasless EIP-3009 transfers signed locally by the [`clawfetch`](https://pypi.org/project/clawfetch/) SDK, so the wallet does not need ETH. Use a dedicated wallet with a small balance.
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
export CLAWFETCH_PRIVATE_KEY=0x...
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Quickstart
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from llama_index.tools.clawfetch import ClawFetchToolSpec
|
|
42
|
+
|
|
43
|
+
spec = ClawFetchToolSpec() # reads CLAWFETCH_PRIVATE_KEY
|
|
44
|
+
# or: ClawFetchToolSpec(private_key="0x...", max_chars=8000)
|
|
45
|
+
|
|
46
|
+
print(spec.fetch("https://example.com"))
|
|
47
|
+
|
|
48
|
+
tools = spec.to_tool_list()
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
To use the tools with an agent:
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from llama_index.core.agent.workflow import FunctionAgent
|
|
55
|
+
|
|
56
|
+
agent = FunctionAgent(tools=tools, llm=llm) # llm: any LlamaIndex LLM with tool calling
|
|
57
|
+
response = await agent.run("What's new on news.ycombinator.com?")
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Tools
|
|
61
|
+
|
|
62
|
+
| Function | Price per call | What it does |
|
|
63
|
+
|---|---|---|
|
|
64
|
+
| `fetch(url)` | $0.001 | URL to readable text (no JavaScript) |
|
|
65
|
+
| `render(url, wait_for=None)` | $0.005 | JavaScript-rendered page to text |
|
|
66
|
+
| `extract(url, type=None)` | $0.008 | Typed JSON from 17 supported sites |
|
|
67
|
+
| `research(query, max_results=None)` | $0.02 | Web search plus source texts for citations |
|
|
68
|
+
| `parse(url, format=None)` | $0.005 | Office document or PDF at a URL to Markdown (no OCR) |
|
|
69
|
+
| `domains_check(domains)` | $0.003 | Domain availability |
|
|
70
|
+
| `domains_suggest(keywords, tlds=None, max_check=None)` | $0.003 | Generate and check domain names from keywords |
|
|
71
|
+
|
|
72
|
+
Supported `extract` sites: twitter, coingecko, github, news, npm, pypi, crates, hackernews, youtube, wikipedia, producthunt, imdb, amazon, crunchbase, sec-edgar, zillow, redfin.
|
|
73
|
+
|
|
74
|
+
## Output and errors
|
|
75
|
+
|
|
76
|
+
Functions return a string. Fetch and render return the page title, URL and text. Parse returns Markdown. The other functions return the API response as JSON. Output is cut at `max_chars` (default 10,000).
|
|
77
|
+
|
|
78
|
+
SDK errors (`PaymentError`, `RateLimitError`, `ApiError`, `NetworkError`, all subclasses of `clawfetch.ClawFetchError`) are raised unchanged.
|
|
79
|
+
|
|
80
|
+
## License
|
|
81
|
+
|
|
82
|
+
MIT
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# llama-index-tools-clawfetch
|
|
2
|
+
|
|
3
|
+
LlamaIndex tool spec for [ClawFetch](https://api.clawfetch.ai), a pay-per-call web intelligence API for AI agents. Each call is paid with an x402 micropayment in USDC on Base.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install llama-index-tools-clawfetch
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Wallet
|
|
12
|
+
|
|
13
|
+
You need a Base wallet private key that holds USDC. There is no API key and no signup. Payments are gasless EIP-3009 transfers signed locally by the [`clawfetch`](https://pypi.org/project/clawfetch/) SDK, so the wallet does not need ETH. Use a dedicated wallet with a small balance.
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
export CLAWFETCH_PRIVATE_KEY=0x...
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Quickstart
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
from llama_index.tools.clawfetch import ClawFetchToolSpec
|
|
23
|
+
|
|
24
|
+
spec = ClawFetchToolSpec() # reads CLAWFETCH_PRIVATE_KEY
|
|
25
|
+
# or: ClawFetchToolSpec(private_key="0x...", max_chars=8000)
|
|
26
|
+
|
|
27
|
+
print(spec.fetch("https://example.com"))
|
|
28
|
+
|
|
29
|
+
tools = spec.to_tool_list()
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
To use the tools with an agent:
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
from llama_index.core.agent.workflow import FunctionAgent
|
|
36
|
+
|
|
37
|
+
agent = FunctionAgent(tools=tools, llm=llm) # llm: any LlamaIndex LLM with tool calling
|
|
38
|
+
response = await agent.run("What's new on news.ycombinator.com?")
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Tools
|
|
42
|
+
|
|
43
|
+
| Function | Price per call | What it does |
|
|
44
|
+
|---|---|---|
|
|
45
|
+
| `fetch(url)` | $0.001 | URL to readable text (no JavaScript) |
|
|
46
|
+
| `render(url, wait_for=None)` | $0.005 | JavaScript-rendered page to text |
|
|
47
|
+
| `extract(url, type=None)` | $0.008 | Typed JSON from 17 supported sites |
|
|
48
|
+
| `research(query, max_results=None)` | $0.02 | Web search plus source texts for citations |
|
|
49
|
+
| `parse(url, format=None)` | $0.005 | Office document or PDF at a URL to Markdown (no OCR) |
|
|
50
|
+
| `domains_check(domains)` | $0.003 | Domain availability |
|
|
51
|
+
| `domains_suggest(keywords, tlds=None, max_check=None)` | $0.003 | Generate and check domain names from keywords |
|
|
52
|
+
|
|
53
|
+
Supported `extract` sites: twitter, coingecko, github, news, npm, pypi, crates, hackernews, youtube, wikipedia, producthunt, imdb, amazon, crunchbase, sec-edgar, zillow, redfin.
|
|
54
|
+
|
|
55
|
+
## Output and errors
|
|
56
|
+
|
|
57
|
+
Functions return a string. Fetch and render return the page title, URL and text. Parse returns Markdown. The other functions return the API response as JSON. Output is cut at `max_chars` (default 10,000).
|
|
58
|
+
|
|
59
|
+
SDK errors (`PaymentError`, `RateLimitError`, `ApiError`, `NetworkError`, all subclasses of `clawfetch.ClawFetchError`) are raised unchanged.
|
|
60
|
+
|
|
61
|
+
## License
|
|
62
|
+
|
|
63
|
+
MIT
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""ClawFetch tool spec — pay-per-call web intelligence for AI agents (x402, USDC on Base)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
from typing import Any, List, Optional
|
|
8
|
+
|
|
9
|
+
from clawfetch import ClawFetch
|
|
10
|
+
from llama_index.core.tools.tool_spec.base import BaseToolSpec
|
|
11
|
+
|
|
12
|
+
DEFAULT_MAX_CHARS = 10_000
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _truncate(text: str, max_chars: int) -> str:
|
|
16
|
+
if len(text) <= max_chars:
|
|
17
|
+
return text
|
|
18
|
+
return text[:max_chars] + f"\n...[truncated {len(text) - max_chars} chars]"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _page(result: dict) -> str:
|
|
22
|
+
return f"# {result.get('title') or ''}\nURL: {result.get('url') or ''}\n\n{result.get('text') or ''}"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ClawFetchToolSpec(BaseToolSpec):
|
|
26
|
+
"""ClawFetch web tools. Each call is paid in USDC on Base via x402.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
private_key: Base wallet private key holding USDC. Defaults to the
|
|
30
|
+
``CLAWFETCH_PRIVATE_KEY`` environment variable.
|
|
31
|
+
client: An existing ``clawfetch.ClawFetch`` client (overrides private_key).
|
|
32
|
+
max_chars: Maximum characters returned by each tool call.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
spec_functions = [
|
|
36
|
+
"fetch",
|
|
37
|
+
"render",
|
|
38
|
+
"extract",
|
|
39
|
+
"research",
|
|
40
|
+
"parse",
|
|
41
|
+
"domains_check",
|
|
42
|
+
"domains_suggest",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
private_key: Optional[str] = None,
|
|
48
|
+
client: Optional[ClawFetch] = None,
|
|
49
|
+
max_chars: int = DEFAULT_MAX_CHARS,
|
|
50
|
+
) -> None:
|
|
51
|
+
if client is None:
|
|
52
|
+
key = private_key or os.environ.get("CLAWFETCH_PRIVATE_KEY")
|
|
53
|
+
if not key:
|
|
54
|
+
raise ValueError("Pass private_key=..., set CLAWFETCH_PRIVATE_KEY, or pass client=ClawFetch(...).")
|
|
55
|
+
client = ClawFetch(private_key=key)
|
|
56
|
+
self.client = client
|
|
57
|
+
self.max_chars = max_chars
|
|
58
|
+
|
|
59
|
+
def _json(self, result: Any) -> str:
|
|
60
|
+
return _truncate(json.dumps(result, ensure_ascii=False), self.max_chars)
|
|
61
|
+
|
|
62
|
+
def fetch(self, url: str) -> str:
|
|
63
|
+
"""Fetch a web page and return its title, URL and readable text. No JavaScript execution; use render for JS-heavy pages. Costs $0.001 USDC per call.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
url: Absolute http(s) URL of the page to fetch.
|
|
67
|
+
"""
|
|
68
|
+
return _truncate(_page(self.client.fetch(url, max_chars=self.max_chars)), self.max_chars)
|
|
69
|
+
|
|
70
|
+
def render(self, url: str, wait_for: Optional[str] = None) -> str:
|
|
71
|
+
"""Render a JavaScript-heavy page in a headless browser and return its visible text. Use when fetch returns empty or incomplete content. Costs $0.005 USDC per call.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
url: Absolute http(s) URL of the page.
|
|
75
|
+
wait_for: Optional CSS selector to wait for before reading the page.
|
|
76
|
+
"""
|
|
77
|
+
result = self.client.render(url, max_chars=self.max_chars, wait_for=wait_for)
|
|
78
|
+
return _truncate(_page(result), self.max_chars)
|
|
79
|
+
|
|
80
|
+
def extract(self, url: str, type: Optional[str] = None) -> str:
|
|
81
|
+
"""Extract typed structured data (JSON) from a page on a supported site: twitter, coingecko, github, news, npm, pypi, crates, hackernews, youtube, wikipedia, producthunt, imdb, amazon, crunchbase, sec-edgar, zillow, redfin. Costs $0.008 USDC per call.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
url: URL on a supported site.
|
|
85
|
+
type: Optional extractor name from the list above; auto-detected from the URL when omitted.
|
|
86
|
+
"""
|
|
87
|
+
return self._json(self.client.extract(url, type=type))
|
|
88
|
+
|
|
89
|
+
def research(self, query: str, max_results: Optional[int] = None) -> str:
|
|
90
|
+
"""Search the web for a query and return the text of the top sources with their URLs (for citations). Costs $0.02 USDC per call.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
query: Research question or topic.
|
|
94
|
+
max_results: Number of sources to read (1-10, default 5).
|
|
95
|
+
"""
|
|
96
|
+
return self._json(self.client.research(query, max_results=max_results))
|
|
97
|
+
|
|
98
|
+
def parse(self, url: str, format: Optional[str] = None) -> str:
|
|
99
|
+
"""Convert an office document or PDF at a URL (docx, pptx, xlsx, pdf, odt, rtf, epub, csv, ...) to Markdown. No OCR: scanned PDFs fail. Costs $0.005 USDC per call.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
url: Absolute http(s) URL of the document.
|
|
103
|
+
format: Optional format override (doc, docx, odt, pdf, ppt, pptx, rtf, epub, xlsx, ods, odp, csv).
|
|
104
|
+
"""
|
|
105
|
+
return _truncate(self.client.parse(url, format=format).get("markdown") or "", self.max_chars)
|
|
106
|
+
|
|
107
|
+
def domains_check(self, domains: List[str]) -> str:
|
|
108
|
+
"""Check whether specific domain names are available to register. Returns JSON with a result per domain (available true/false, or null with an error). Costs $0.003 USDC per call.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
domains: Fully qualified domain names, e.g. ["acme.ai", "acme.com"] (max 20).
|
|
112
|
+
"""
|
|
113
|
+
return self._json(self.client.domains_check(domains))
|
|
114
|
+
|
|
115
|
+
def domains_suggest(
|
|
116
|
+
self,
|
|
117
|
+
keywords: List[str],
|
|
118
|
+
tlds: Optional[List[str]] = None,
|
|
119
|
+
max_check: Optional[int] = None,
|
|
120
|
+
) -> str:
|
|
121
|
+
"""Generate domain names from keywords and check which are available. Returns JSON with the available domains and every checked result. Costs $0.003 USDC per call.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
keywords: Keywords to combine into domain names, e.g. ["agent", "grid"].
|
|
125
|
+
tlds: TLDs to try without dots, e.g. ["com", "ai"]. Default: com, ai, io.
|
|
126
|
+
max_check: How many generated names to check (1-50, default 30).
|
|
127
|
+
"""
|
|
128
|
+
return self._json(self.client.domains_suggest(keywords, tlds=tlds, max_check=max_check))
|
|
File without changes
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "llama-index-tools-clawfetch"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "llama-index tools clawfetch integration — pay-per-call web intelligence for AI agents (x402, USDC on Base)"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
requires-python = ">=3.10"
|
|
13
|
+
keywords = ["llama-index", "clawfetch", "x402", "web-scraping", "ai-agents", "tools"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
19
|
+
]
|
|
20
|
+
dependencies = [
|
|
21
|
+
"clawfetch>=0.2.2,<0.3",
|
|
22
|
+
"llama-index-core>=0.12,<0.15",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[project.optional-dependencies]
|
|
26
|
+
test = ["pytest>=8"]
|
|
27
|
+
|
|
28
|
+
[project.urls]
|
|
29
|
+
Homepage = "https://api.clawfetch.ai"
|
|
30
|
+
|
|
31
|
+
[tool.hatch.build.targets.sdist]
|
|
32
|
+
include = ["llama_index/", "tests/", "README.md", "CHANGELOG.md", "LICENSE"]
|
|
33
|
+
|
|
34
|
+
[tool.hatch.build.targets.wheel]
|
|
35
|
+
include = ["llama_index/"]
|
|
36
|
+
|
|
37
|
+
[tool.llamahub]
|
|
38
|
+
contains_example = false
|
|
39
|
+
import_path = "llama_index.tools.clawfetch"
|
|
40
|
+
|
|
41
|
+
[tool.llamahub.class_authors]
|
|
42
|
+
ClawFetchToolSpec = "clawfetch"
|
|
43
|
+
|
|
44
|
+
[tool.pytest.ini_options]
|
|
45
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Unit tests — the ClawFetch client is mocked: no network, no payments."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from unittest.mock import MagicMock
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
from clawfetch import ClawFetch
|
|
8
|
+
from llama_index.core.tools import FunctionTool
|
|
9
|
+
from llama_index.core.tools.tool_spec.base import BaseToolSpec
|
|
10
|
+
|
|
11
|
+
from llama_index.tools.clawfetch import ClawFetchToolSpec
|
|
12
|
+
|
|
13
|
+
DUMMY_KEY = "0x0000000000000000000000000000000000000000000000000000000000000001"
|
|
14
|
+
PAGE = {"url": "https://a.b", "title": "T", "text": "hello"}
|
|
15
|
+
|
|
16
|
+
# (spec function, call kwargs, sdk method, expected args, expected kwargs, sdk result, expected output)
|
|
17
|
+
CASES = [
|
|
18
|
+
("fetch", {"url": "https://a.b"}, "fetch", ("https://a.b",), {"max_chars": 10_000},
|
|
19
|
+
PAGE, "# T\nURL: https://a.b\n\nhello"),
|
|
20
|
+
("render", {"url": "https://a.b", "wait_for": "#app"}, "render", ("https://a.b",),
|
|
21
|
+
{"max_chars": 10_000, "wait_for": "#app"}, PAGE, "# T\nURL: https://a.b\n\nhello"),
|
|
22
|
+
("extract", {"url": "https://github.com/a/b", "type": "github"}, "extract", ("https://github.com/a/b",),
|
|
23
|
+
{"type": "github"}, {"type": "github", "stars": 1}, json.dumps({"type": "github", "stars": 1})),
|
|
24
|
+
("research", {"query": "x402"}, "research", ("x402",), {"max_results": None},
|
|
25
|
+
{"query": "x402", "sources": 0, "results": []}, json.dumps({"query": "x402", "sources": 0, "results": []})),
|
|
26
|
+
("parse", {"url": "https://a.b/x.docx"}, "parse", ("https://a.b/x.docx",), {"format": None},
|
|
27
|
+
{"markdown": "# Doc", "format": "docx", "chars": 5}, "# Doc"),
|
|
28
|
+
("domains_check", {"domains": ["a.ai"]}, "domains_check", (["a.ai"],), {},
|
|
29
|
+
{"checked": 1, "results": [{"domain": "a.ai", "available": None, "error": "timeout"}]},
|
|
30
|
+
json.dumps({"checked": 1, "results": [{"domain": "a.ai", "available": None, "error": "timeout"}]})),
|
|
31
|
+
("domains_suggest", {"keywords": ["agent"], "tlds": ["ai"], "max_check": 5}, "domains_suggest",
|
|
32
|
+
(["agent"],), {"tlds": ["ai"], "max_check": 5},
|
|
33
|
+
{"keywords": ["agent"], "available": ["agent.ai"]}, json.dumps({"keywords": ["agent"], "available": ["agent.ai"]})),
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@pytest.mark.parametrize("fn,call_kwargs,method,args,kwargs,result,expected", CASES)
|
|
38
|
+
def test_spec_function_calls_sdk_and_formats(fn, call_kwargs, method, args, kwargs, result, expected):
|
|
39
|
+
client = MagicMock(spec=ClawFetch)
|
|
40
|
+
getattr(client, method).return_value = result
|
|
41
|
+
spec = ClawFetchToolSpec(client=client)
|
|
42
|
+
assert getattr(spec, fn)(**call_kwargs) == expected
|
|
43
|
+
getattr(client, method).assert_called_once_with(*args, **kwargs)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@pytest.mark.parametrize("fn,call_kwargs,method,args,kwargs,result,expected", CASES)
|
|
47
|
+
def test_function_tool_call_through_llama_index(fn, call_kwargs, method, args, kwargs, result, expected):
|
|
48
|
+
client = MagicMock(spec=ClawFetch)
|
|
49
|
+
getattr(client, method).return_value = result
|
|
50
|
+
tools = {t.metadata.name: t for t in ClawFetchToolSpec(client=client).to_tool_list()}
|
|
51
|
+
out = tools[fn].call(**call_kwargs)
|
|
52
|
+
assert out.content == expected
|
|
53
|
+
getattr(client, method).assert_called_once_with(*args, **kwargs)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_tool_list_metadata():
|
|
57
|
+
tools = ClawFetchToolSpec(client=MagicMock(spec=ClawFetch)).to_tool_list()
|
|
58
|
+
assert issubclass(ClawFetchToolSpec, BaseToolSpec)
|
|
59
|
+
assert [t.metadata.name for t in tools] == [
|
|
60
|
+
"fetch", "render", "extract", "research", "parse", "domains_check", "domains_suggest",
|
|
61
|
+
]
|
|
62
|
+
for t in tools:
|
|
63
|
+
assert isinstance(t, FunctionTool)
|
|
64
|
+
assert "$0." in t.metadata.description and "USDC" in t.metadata.description
|
|
65
|
+
schema = tools[6].metadata.get_parameters_dict()
|
|
66
|
+
assert schema["required"] == ["keywords"]
|
|
67
|
+
assert schema["properties"]["keywords"]["type"] == "array"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_output_truncated_to_max_chars():
|
|
71
|
+
client = MagicMock(spec=ClawFetch)
|
|
72
|
+
client.parse.return_value = {"markdown": "x" * 50}
|
|
73
|
+
assert ClawFetchToolSpec(client=client, max_chars=10).parse("https://a.b/x.pdf") == "x" * 10 + "\n...[truncated 40 chars]"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_from_private_key_and_env(monkeypatch):
|
|
77
|
+
assert isinstance(ClawFetchToolSpec(private_key=DUMMY_KEY).client, ClawFetch)
|
|
78
|
+
monkeypatch.setenv("CLAWFETCH_PRIVATE_KEY", DUMMY_KEY)
|
|
79
|
+
assert isinstance(ClawFetchToolSpec().client, ClawFetch)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_missing_key_raises(monkeypatch):
|
|
83
|
+
monkeypatch.delenv("CLAWFETCH_PRIVATE_KEY", raising=False)
|
|
84
|
+
with pytest.raises(ValueError, match="CLAWFETCH_PRIVATE_KEY"):
|
|
85
|
+
ClawFetchToolSpec()
|