fastaf-sdk 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.
- fastaf_sdk-0.1.0/PKG-INFO +80 -0
- fastaf_sdk-0.1.0/README.md +60 -0
- fastaf_sdk-0.1.0/pyproject.toml +28 -0
- fastaf_sdk-0.1.0/src/fastaf/__init__.py +4 -0
- fastaf_sdk-0.1.0/src/fastaf/client.py +76 -0
- fastaf_sdk-0.1.0/src/fastaf/tools.py +39 -0
- fastaf_sdk-0.1.0/tests/conftest.py +5 -0
- fastaf_sdk-0.1.0/tests/test_client.py +78 -0
- fastaf_sdk-0.1.0/tests/test_tools.py +48 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: fastaf-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: FastAF API client and optional agent tools
|
|
5
|
+
Project-URL: Homepage, https://fastaf.co
|
|
6
|
+
Project-URL: Repository, https://github.com/fastaf-co/fastaf-sdk
|
|
7
|
+
Author-email: FastAF <dev@fastaf.co>
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Requires-Dist: httpx<1,>=0.27
|
|
10
|
+
Provides-Extra: crewai
|
|
11
|
+
Requires-Dist: crewai<2,>=1; extra == 'crewai'
|
|
12
|
+
Provides-Extra: langchain
|
|
13
|
+
Requires-Dist: langchain-core<2,>=1; extra == 'langchain'
|
|
14
|
+
Provides-Extra: llamaindex
|
|
15
|
+
Requires-Dist: llama-index-core<0.15,>=0.14; extra == 'llamaindex'
|
|
16
|
+
Provides-Extra: test
|
|
17
|
+
Requires-Dist: build<2,>=1; extra == 'test'
|
|
18
|
+
Requires-Dist: pytest<10,>=8; extra == 'test'
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# fastaf-sdk
|
|
22
|
+
|
|
23
|
+
[FastAF](https://fastaf.co) · Docs: https://fastaf.co/docs/
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
Distribution name: **fastaf-sdk**. Import: **fastaf**. The unrelated `fastaf`
|
|
27
|
+
PyPI distribution is not this SDK. Nothing here has been published.
|
|
28
|
+
|
|
29
|
+
Python 3.10+. Local installation (from this directory):
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
pip install -e .
|
|
33
|
+
# Install only integrations you use:
|
|
34
|
+
pip install -e '.[langchain,llamaindex,crewai]'
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
import os
|
|
39
|
+
from fastaf import FastAF, FastAFError
|
|
40
|
+
|
|
41
|
+
with FastAF(os.environ["FASTAF_API_KEY"]) as client:
|
|
42
|
+
data = client.search("best espresso machine", results=5)
|
|
43
|
+
markdown = client.request("scrape", {"url": "https://example.com", "formats": "markdown"})
|
|
44
|
+
data = client.request("serp", {"query": "coffee"}, method="POST")
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`request(endpoint, params)` covers the live `/v1/*` endpoints. Returns decoded
|
|
48
|
+
JSON, text, or bytes for images. Keys stay in Authorization headers. Errors
|
|
49
|
+
raise `FastAFError` with `status`, `body`, and `headers` (including Retry-After).
|
|
50
|
+
Network/timeout exceptions remain httpx exceptions. Default timeout: 60 seconds;
|
|
51
|
+
override `timeout=30`. There are no automatic retries, redirects or x402 payments.
|
|
52
|
+
A 402 on keyed REST means insufficient credits, not permission to spend a wallet.
|
|
53
|
+
Close the client or use its context manager. Do not put keys in browser code.
|
|
54
|
+
|
|
55
|
+
## Agent tools
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from fastaf.tools import langchain_tool, llamaindex_tool, crewai_tool
|
|
59
|
+
|
|
60
|
+
# Use inside the client's lifetime. Each accepts the same FastAF client.
|
|
61
|
+
search = langchain_tool(client) # StructuredTool
|
|
62
|
+
search = llamaindex_tool(client) # FunctionTool
|
|
63
|
+
search = crewai_tool(client) # CrewAI tool
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Tools accept `query` and `results` (1-10, default 5). They return JSON strings;
|
|
67
|
+
credentials are not part of agent-visible schemas. Each call consumes API
|
|
68
|
+
credits, so configure agent step limits. Integrations import lazily.
|
|
69
|
+
|
|
70
|
+
## Tests and local packaging
|
|
71
|
+
|
|
72
|
+
```sh
|
|
73
|
+
pip install -e '.[test,langchain,llamaindex,crewai]'
|
|
74
|
+
pytest
|
|
75
|
+
python -m build
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Tests use httpx.MockTransport, never real keys or billable calls. Without optional
|
|
79
|
+
frameworks, their integration tests skip; install all extras to run the full suite.
|
|
80
|
+
The private distribution classifier deliberately blocks uploading to PyPI.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# fastaf-sdk
|
|
2
|
+
|
|
3
|
+
[FastAF](https://fastaf.co) · Docs: https://fastaf.co/docs/
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
Distribution name: **fastaf-sdk**. Import: **fastaf**. The unrelated `fastaf`
|
|
7
|
+
PyPI distribution is not this SDK. Nothing here has been published.
|
|
8
|
+
|
|
9
|
+
Python 3.10+. Local installation (from this directory):
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
pip install -e .
|
|
13
|
+
# Install only integrations you use:
|
|
14
|
+
pip install -e '.[langchain,llamaindex,crewai]'
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
import os
|
|
19
|
+
from fastaf import FastAF, FastAFError
|
|
20
|
+
|
|
21
|
+
with FastAF(os.environ["FASTAF_API_KEY"]) as client:
|
|
22
|
+
data = client.search("best espresso machine", results=5)
|
|
23
|
+
markdown = client.request("scrape", {"url": "https://example.com", "formats": "markdown"})
|
|
24
|
+
data = client.request("serp", {"query": "coffee"}, method="POST")
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
`request(endpoint, params)` covers the live `/v1/*` endpoints. Returns decoded
|
|
28
|
+
JSON, text, or bytes for images. Keys stay in Authorization headers. Errors
|
|
29
|
+
raise `FastAFError` with `status`, `body`, and `headers` (including Retry-After).
|
|
30
|
+
Network/timeout exceptions remain httpx exceptions. Default timeout: 60 seconds;
|
|
31
|
+
override `timeout=30`. There are no automatic retries, redirects or x402 payments.
|
|
32
|
+
A 402 on keyed REST means insufficient credits, not permission to spend a wallet.
|
|
33
|
+
Close the client or use its context manager. Do not put keys in browser code.
|
|
34
|
+
|
|
35
|
+
## Agent tools
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from fastaf.tools import langchain_tool, llamaindex_tool, crewai_tool
|
|
39
|
+
|
|
40
|
+
# Use inside the client's lifetime. Each accepts the same FastAF client.
|
|
41
|
+
search = langchain_tool(client) # StructuredTool
|
|
42
|
+
search = llamaindex_tool(client) # FunctionTool
|
|
43
|
+
search = crewai_tool(client) # CrewAI tool
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Tools accept `query` and `results` (1-10, default 5). They return JSON strings;
|
|
47
|
+
credentials are not part of agent-visible schemas. Each call consumes API
|
|
48
|
+
credits, so configure agent step limits. Integrations import lazily.
|
|
49
|
+
|
|
50
|
+
## Tests and local packaging
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
pip install -e '.[test,langchain,llamaindex,crewai]'
|
|
54
|
+
pytest
|
|
55
|
+
python -m build
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Tests use httpx.MockTransport, never real keys or billable calls. Without optional
|
|
59
|
+
frameworks, their integration tests skip; install all extras to run the full suite.
|
|
60
|
+
The private distribution classifier deliberately blocks uploading to PyPI.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27,<2"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "fastaf-sdk"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "FastAF API client and optional agent tools"
|
|
9
|
+
authors = [{name = "FastAF", email = "dev@fastaf.co"}]
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
dependencies = ["httpx>=0.27,<1"]
|
|
13
|
+
|
|
14
|
+
[project.optional-dependencies]
|
|
15
|
+
langchain = ["langchain-core>=1,<2"]
|
|
16
|
+
llamaindex = ["llama-index-core>=0.14,<0.15"]
|
|
17
|
+
crewai = ["crewai>=1,<2"]
|
|
18
|
+
test = ["pytest>=8,<10", "build>=1,<2"]
|
|
19
|
+
|
|
20
|
+
[tool.hatch.build.targets.wheel]
|
|
21
|
+
packages = ["src/fastaf"]
|
|
22
|
+
|
|
23
|
+
[tool.pytest.ini_options]
|
|
24
|
+
testpaths = ["tests"]
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
Homepage = "https://fastaf.co"
|
|
28
|
+
Repository = "https://github.com/fastaf-co/fastaf-sdk"
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import Any, Mapping
|
|
5
|
+
from urllib.parse import urlsplit
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
Params = Mapping[str, str | int | float | bool | None]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class FastAFError(Exception):
|
|
13
|
+
"""HTTP failure, including quota 402; no automatic retry or payment."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, response: httpx.Response) -> None:
|
|
16
|
+
super().__init__(f"FastAF request failed ({response.status_code})")
|
|
17
|
+
self.status = response.status_code
|
|
18
|
+
self.body = _decode(response)
|
|
19
|
+
self.headers = response.headers
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _decode(response: httpx.Response) -> Any:
|
|
23
|
+
content_type = response.headers.get("content-type", "").split(";")[0].strip()
|
|
24
|
+
if content_type == "application/json" or content_type.endswith("+json"):
|
|
25
|
+
try:
|
|
26
|
+
return response.json()
|
|
27
|
+
except ValueError:
|
|
28
|
+
return response.text
|
|
29
|
+
if content_type.startswith("image/") or content_type == "application/octet-stream":
|
|
30
|
+
return response.content
|
|
31
|
+
return response.text
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class FastAF:
|
|
35
|
+
"""Synchronous httpx client. Owns its connection pool; use as a context manager.
|
|
36
|
+
|
|
37
|
+
Optional transports support mocking/proxies without changing authentication.
|
|
38
|
+
Redirects are not followed and failed calls are never automatically retried.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(self, api_key: str, *, base_url: str = "https://api.fastaf.co",
|
|
42
|
+
timeout: float = 60.0, transport: httpx.BaseTransport | None = None) -> None:
|
|
43
|
+
if not api_key or re.search(r"\s", api_key):
|
|
44
|
+
raise ValueError("A nonempty API key is required")
|
|
45
|
+
url = urlsplit(base_url)
|
|
46
|
+
if (url.scheme not in {"https", "http"} or not url.hostname or url.username or url.password
|
|
47
|
+
or url.path not in {"", "/"} or url.query or url.fragment):
|
|
48
|
+
raise ValueError("base_url must be an HTTP(S) origin without credentials, path, query or fragment")
|
|
49
|
+
if timeout <= 0:
|
|
50
|
+
raise ValueError("timeout must be positive")
|
|
51
|
+
self._http = httpx.Client(base_url=base_url, headers={"Authorization": f"Bearer {api_key}"},
|
|
52
|
+
timeout=timeout, transport=transport, follow_redirects=False)
|
|
53
|
+
|
|
54
|
+
def request(self, endpoint: str, params: Params | None = None, *, method: str = "GET") -> Any:
|
|
55
|
+
if not re.fullmatch(r"[a-z][a-z0-9_-]*", endpoint):
|
|
56
|
+
raise ValueError("endpoint must be a single endpoint name, not a URL or path")
|
|
57
|
+
if method not in {"GET", "POST"}:
|
|
58
|
+
raise ValueError("Use GET or POST")
|
|
59
|
+
values = {key: value for key, value in (params or {}).items() if value is not None}
|
|
60
|
+
kwargs = {"params": values} if method == "GET" else {"json": values}
|
|
61
|
+
response = self._http.request(method, f"/v1/{endpoint}", **kwargs)
|
|
62
|
+
if not response.is_success:
|
|
63
|
+
raise FastAFError(response)
|
|
64
|
+
return _decode(response)
|
|
65
|
+
|
|
66
|
+
def search(self, query: str, **params: str | int | float | bool | None) -> Any:
|
|
67
|
+
return self.request("serp", {**params, "query": query})
|
|
68
|
+
|
|
69
|
+
def close(self) -> None:
|
|
70
|
+
self._http.close()
|
|
71
|
+
|
|
72
|
+
def __enter__(self) -> FastAF:
|
|
73
|
+
return self
|
|
74
|
+
|
|
75
|
+
def __exit__(self, *_: Any) -> None:
|
|
76
|
+
self.close()
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Optional framework factories. Credentials live in closures, never tool schemas."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
from typing import TYPE_CHECKING, Callable
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from .client import FastAF
|
|
9
|
+
|
|
10
|
+
DESCRIPTION = "Search the public web with FastAF for source links and snippets. Each call spends API credits."
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _search(client: FastAF) -> Callable:
|
|
14
|
+
def fastaf_search(query: str, results: int = 5) -> str:
|
|
15
|
+
"""Search the public web. query: nonempty search text; results: 1-10."""
|
|
16
|
+
if not query.strip() or len(query) > 2000:
|
|
17
|
+
raise ValueError("query must contain 1-2000 characters")
|
|
18
|
+
if isinstance(results, bool) or not isinstance(results, int) or not 1 <= results <= 10:
|
|
19
|
+
raise ValueError("results must be an integer from 1 to 10")
|
|
20
|
+
return json.dumps(client.search(query, results=results), ensure_ascii=False)
|
|
21
|
+
return fastaf_search
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def langchain_tool(client: FastAF):
|
|
25
|
+
"""Requires fastaf-sdk[langchain]."""
|
|
26
|
+
from langchain_core.tools import StructuredTool
|
|
27
|
+
return StructuredTool.from_function(_search(client), name="fastaf_search", description=DESCRIPTION)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def llamaindex_tool(client: FastAF):
|
|
31
|
+
"""Requires fastaf-sdk[llamaindex]."""
|
|
32
|
+
from llama_index.core.tools import FunctionTool
|
|
33
|
+
return FunctionTool.from_defaults(fn=_search(client), name="fastaf_search", description=DESCRIPTION)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def crewai_tool(client: FastAF):
|
|
37
|
+
"""Requires fastaf-sdk[crewai]."""
|
|
38
|
+
from crewai.tools import tool
|
|
39
|
+
return tool("fastaf_search")(_search(client))
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from fastaf import FastAF, FastAFError
|
|
7
|
+
|
|
8
|
+
KEY = "test-key-not-a-secret"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_get_encoding_and_auth():
|
|
12
|
+
def handler(request):
|
|
13
|
+
assert request.url.path == "/v1/serp"
|
|
14
|
+
assert request.url.params["query"] == "coffee & tè"
|
|
15
|
+
assert request.url.params["html"] == "false"
|
|
16
|
+
assert "omit" not in request.url.params
|
|
17
|
+
assert request.headers["Authorization"] == f"Bearer {KEY}"
|
|
18
|
+
assert request.extensions["timeout"]["read"] == 60
|
|
19
|
+
return httpx.Response(200, json={"ok": True})
|
|
20
|
+
with FastAF(KEY, transport=httpx.MockTransport(handler)) as client:
|
|
21
|
+
assert client.search("coffee & tè", html=False, omit=None) == {"ok": True}
|
|
22
|
+
assert client._http.is_closed
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_post_text():
|
|
26
|
+
def handler(request):
|
|
27
|
+
assert request.method == "POST"
|
|
28
|
+
assert not request.url.query
|
|
29
|
+
assert json.loads(request.content) == {"url": "https://example.com", "dynamic": True}
|
|
30
|
+
return httpx.Response(200, text="# Title", headers={"Content-Type": "text/markdown"})
|
|
31
|
+
with FastAF(KEY, transport=httpx.MockTransport(handler)) as client:
|
|
32
|
+
assert client.request("scrape", {"url": "https://example.com", "dynamic": True}, method="POST") == "# Title"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_binary():
|
|
36
|
+
with FastAF(KEY, transport=httpx.MockTransport(lambda _: httpx.Response(200, content=b"png", headers={"Content-Type": "image/png"}))) as client:
|
|
37
|
+
assert client.request("screenshot") == b"png"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@pytest.mark.parametrize("status", [302, 401, 402, 429, 503])
|
|
41
|
+
def test_errors_and_no_retry_or_redirect(status):
|
|
42
|
+
calls = []
|
|
43
|
+
def handler(request):
|
|
44
|
+
calls.append(request)
|
|
45
|
+
return httpx.Response(status, json={"error": "test"}, headers={"Retry-After": "3", "Location": "https://evil.test"})
|
|
46
|
+
with FastAF(KEY, transport=httpx.MockTransport(handler)) as client:
|
|
47
|
+
with pytest.raises(FastAFError) as error:
|
|
48
|
+
client.search("coffee")
|
|
49
|
+
assert error.value.status == status
|
|
50
|
+
assert error.value.body == {"error": "test"}
|
|
51
|
+
assert error.value.headers["Retry-After"] == "3"
|
|
52
|
+
assert KEY not in str(error.value)
|
|
53
|
+
assert len(calls) == 1
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@pytest.mark.parametrize("endpoint", ["../usage", "/serp", "https://evil.test", "serp?x=y", "serp#x"])
|
|
57
|
+
def test_endpoint_injection(endpoint):
|
|
58
|
+
with FastAF(KEY, transport=httpx.MockTransport(lambda _: pytest.fail("must not fetch"))) as client:
|
|
59
|
+
with pytest.raises(ValueError):
|
|
60
|
+
client.request(endpoint)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@pytest.mark.parametrize("base", ["https://user:pass@example.com", "https://example.com/path", "https://example.com?key=x", "file:///tmp"])
|
|
64
|
+
def test_origin_injection(base):
|
|
65
|
+
with pytest.raises(ValueError):
|
|
66
|
+
FastAF(KEY, base_url=base)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_timeout_and_invalid_json():
|
|
70
|
+
def timeout(request):
|
|
71
|
+
raise httpx.ReadTimeout("mock timeout", request=request)
|
|
72
|
+
with FastAF(KEY, transport=httpx.MockTransport(timeout)) as client:
|
|
73
|
+
with pytest.raises(httpx.ReadTimeout):
|
|
74
|
+
client.search("coffee")
|
|
75
|
+
with FastAF(KEY, transport=httpx.MockTransport(lambda _: httpx.Response(502, text="not-json", headers={"Content-Type": "application/json"}))) as client:
|
|
76
|
+
with pytest.raises(FastAFError) as error:
|
|
77
|
+
client.search("coffee")
|
|
78
|
+
assert error.value.body == "not-json"
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import httpx
|
|
3
|
+
import pytest
|
|
4
|
+
from fastaf import FastAF
|
|
5
|
+
from fastaf.tools import _search, langchain_tool, llamaindex_tool, crewai_tool
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@pytest.fixture
|
|
9
|
+
def client():
|
|
10
|
+
def handler(request):
|
|
11
|
+
assert request.url.path == "/v1/serp"
|
|
12
|
+
assert request.url.params["query"] == "coffee"
|
|
13
|
+
assert request.url.params["results"] == "5"
|
|
14
|
+
return httpx.Response(200, json={"organic_results": []})
|
|
15
|
+
with FastAF("test-key", transport=httpx.MockTransport(handler)) as client:
|
|
16
|
+
yield client
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_search_validation(client):
|
|
20
|
+
search = _search(client)
|
|
21
|
+
for query, results in [("", 5), ("coffee", 0), ("coffee", 11), ("coffee", True)]:
|
|
22
|
+
with pytest.raises(ValueError):
|
|
23
|
+
search(query, results)
|
|
24
|
+
assert json.loads(search("coffee")) == {"organic_results": []}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_langchain(client):
|
|
28
|
+
pytest.importorskip("langchain_core")
|
|
29
|
+
tool = langchain_tool(client)
|
|
30
|
+
assert tool.name == "fastaf_search"
|
|
31
|
+
assert json.loads(tool.invoke({"query": "coffee"})) == {"organic_results": []}
|
|
32
|
+
assert "api_key" not in tool.args
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_llamaindex(client):
|
|
36
|
+
pytest.importorskip("llama_index.core")
|
|
37
|
+
tool = llamaindex_tool(client)
|
|
38
|
+
assert tool.metadata.name == "fastaf_search"
|
|
39
|
+
assert json.loads(tool.call(query="coffee").content) == {"organic_results": []}
|
|
40
|
+
assert "api_key" not in tool.metadata.get_parameters_dict()["properties"]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_crewai(client):
|
|
44
|
+
pytest.importorskip("crewai")
|
|
45
|
+
tool = crewai_tool(client)
|
|
46
|
+
assert tool.name == "fastaf_search"
|
|
47
|
+
assert json.loads(tool.run(query="coffee")) == {"organic_results": []}
|
|
48
|
+
assert "api_key" not in tool.args_schema.model_json_schema()["properties"]
|