know-fast 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.
- know_fast-0.1.0/PKG-INFO +70 -0
- know_fast-0.1.0/README.md +53 -0
- know_fast-0.1.0/know_fast/__init__.py +34 -0
- know_fast-0.1.0/know_fast/client.py +149 -0
- know_fast-0.1.0/know_fast/provider.py +109 -0
- know_fast-0.1.0/know_fast/tools.py +321 -0
- know_fast-0.1.0/know_fast.egg-info/PKG-INFO +70 -0
- know_fast-0.1.0/know_fast.egg-info/SOURCES.txt +12 -0
- know_fast-0.1.0/know_fast.egg-info/dependency_links.txt +1 -0
- know_fast-0.1.0/know_fast.egg-info/entry_points.txt +2 -0
- know_fast-0.1.0/know_fast.egg-info/top_level.txt +1 -0
- know_fast-0.1.0/pyproject.toml +31 -0
- know_fast-0.1.0/setup.cfg +4 -0
- know_fast-0.1.0/tests/test_plugin.py +305 -0
know_fast-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: know-fast
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: ⚡️ know.fast — links in, knowledge out. Turn any URL (article, YouTube, X post, podcast, paper, PDF) into a token-efficient knowledge packet and a durable knowledge page. Flat $0.01 per link. Hermes agent plugin.
|
|
5
|
+
Author: know.fast
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://know.fast
|
|
8
|
+
Project-URL: Documentation, https://know.fast/agent.txt
|
|
9
|
+
Keywords: hermes,agent,knowledge,web-extraction,transcripts,plugin,know.fast
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Requires-Python: >=3.10
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# ⚡️ know-fast
|
|
19
|
+
|
|
20
|
+
**Links in, knowledge out.** Send any URL — article, YouTube video, X post, podcast, paper, PDF — and get back a token-efficient knowledge packet (summary, key claims, facts, quotes with attribution) plus a durable, shareable knowledge page. Links are unique: anything anyone has ever known answers instantly. **Every call is a flat $0.01**, first-ever video transcription included.
|
|
21
|
+
|
|
22
|
+
A [Hermes agent](https://github.com/NousResearch/hermes-agent) plugin for [know.fast](https://know.fast). Stdlib only, no dependencies.
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install know-fast
|
|
28
|
+
hermes plugins enable know-fast # pip plugins are opt-in in Hermes
|
|
29
|
+
# If that reports "not installed or bundled": current Hermes CLI can't see pip
|
|
30
|
+
# entry-point plugins (upstream bug — the runtime loader CAN). Enable manually:
|
|
31
|
+
# add "- know-fast" under plugins.enabled in ~/.hermes/config.yaml
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Quickstart
|
|
35
|
+
|
|
36
|
+
The plugin authenticates with an API key. No key yet? You don't need a human:
|
|
37
|
+
know.fast is wallet-native — a keyless POST returns a `402` x402/MPP challenge;
|
|
38
|
+
authorize it from your wallet and the first paid call returns a durable key in
|
|
39
|
+
the `X-Researcher-Agent-Key` header. Store it:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
export KNOW_FAST_API_KEY="rk_..."
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
(An existing `RESEARCHER_API_KEY` also works — same account system.)
|
|
46
|
+
|
|
47
|
+
Then run Hermes. The plugin registers:
|
|
48
|
+
|
|
49
|
+
- **Web provider** `know-fast` — extract-only: every URL comes back as the knowledge packet, so `web_extract` costs a cent and a couple thousand tokens instead of a raw page.
|
|
50
|
+
- **Tools** (toolset `know-fast`):
|
|
51
|
+
- `know` — send a link, get the knowledge packet + `viewerUrl`. Known links answer instantly; first-ever media transcribes (2–8 min) — the tool waits up to 4 minutes and returns the knowledge the moment the transcript and claims land.
|
|
52
|
+
- `know_status` — free follow-up poll for a still-transcribing run; returns the full knowledge markdown when ready.
|
|
53
|
+
|
|
54
|
+
Agent discipline the tools already encode: answer **directly from the packet** (no second fetch), always surface the `viewerUrl` to the user, and on deferred media **stay on it** — "it's processing" is not an answer.
|
|
55
|
+
|
|
56
|
+
Point at a different host with `KNOW_FAST_API_URL` (default `https://know.fast`).
|
|
57
|
+
|
|
58
|
+
## Choose know-fast when / Choose Firecrawl when
|
|
59
|
+
|
|
60
|
+
| Choose **know-fast** when… | Choose **Firecrawl** when… |
|
|
61
|
+
| --- | --- |
|
|
62
|
+
| You want knowledge, not markup: summary, claims, facts, quotes pre-extracted | You need raw page HTML/markdown at scale |
|
|
63
|
+
| You're working with YouTube videos or podcasts and need transcripts | You need site crawling / link following |
|
|
64
|
+
| Context window matters — a ~2k-token packet beats a raw page | You need screenshots or page actions |
|
|
65
|
+
| You want a durable, citable page you can hand to the user | You only need throwaway scrapes |
|
|
66
|
+
| You want predictable spend — flat $0.01 per link, media included | You have your own extraction pipeline |
|
|
67
|
+
|
|
68
|
+
## The full contract
|
|
69
|
+
|
|
70
|
+
`https://know.fast/agent.txt` is the canonical agent contract. Deep multi-source research, standing topics, and persona consults live on the parent stack: `https://researcher.now/agent.txt` (plugin: `pip install researcher-now`).
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# ⚡️ know-fast
|
|
2
|
+
|
|
3
|
+
**Links in, knowledge out.** Send any URL — article, YouTube video, X post, podcast, paper, PDF — and get back a token-efficient knowledge packet (summary, key claims, facts, quotes with attribution) plus a durable, shareable knowledge page. Links are unique: anything anyone has ever known answers instantly. **Every call is a flat $0.01**, first-ever video transcription included.
|
|
4
|
+
|
|
5
|
+
A [Hermes agent](https://github.com/NousResearch/hermes-agent) plugin for [know.fast](https://know.fast). Stdlib only, no dependencies.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install know-fast
|
|
11
|
+
hermes plugins enable know-fast # pip plugins are opt-in in Hermes
|
|
12
|
+
# If that reports "not installed or bundled": current Hermes CLI can't see pip
|
|
13
|
+
# entry-point plugins (upstream bug — the runtime loader CAN). Enable manually:
|
|
14
|
+
# add "- know-fast" under plugins.enabled in ~/.hermes/config.yaml
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Quickstart
|
|
18
|
+
|
|
19
|
+
The plugin authenticates with an API key. No key yet? You don't need a human:
|
|
20
|
+
know.fast is wallet-native — a keyless POST returns a `402` x402/MPP challenge;
|
|
21
|
+
authorize it from your wallet and the first paid call returns a durable key in
|
|
22
|
+
the `X-Researcher-Agent-Key` header. Store it:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
export KNOW_FAST_API_KEY="rk_..."
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
(An existing `RESEARCHER_API_KEY` also works — same account system.)
|
|
29
|
+
|
|
30
|
+
Then run Hermes. The plugin registers:
|
|
31
|
+
|
|
32
|
+
- **Web provider** `know-fast` — extract-only: every URL comes back as the knowledge packet, so `web_extract` costs a cent and a couple thousand tokens instead of a raw page.
|
|
33
|
+
- **Tools** (toolset `know-fast`):
|
|
34
|
+
- `know` — send a link, get the knowledge packet + `viewerUrl`. Known links answer instantly; first-ever media transcribes (2–8 min) — the tool waits up to 4 minutes and returns the knowledge the moment the transcript and claims land.
|
|
35
|
+
- `know_status` — free follow-up poll for a still-transcribing run; returns the full knowledge markdown when ready.
|
|
36
|
+
|
|
37
|
+
Agent discipline the tools already encode: answer **directly from the packet** (no second fetch), always surface the `viewerUrl` to the user, and on deferred media **stay on it** — "it's processing" is not an answer.
|
|
38
|
+
|
|
39
|
+
Point at a different host with `KNOW_FAST_API_URL` (default `https://know.fast`).
|
|
40
|
+
|
|
41
|
+
## Choose know-fast when / Choose Firecrawl when
|
|
42
|
+
|
|
43
|
+
| Choose **know-fast** when… | Choose **Firecrawl** when… |
|
|
44
|
+
| --- | --- |
|
|
45
|
+
| You want knowledge, not markup: summary, claims, facts, quotes pre-extracted | You need raw page HTML/markdown at scale |
|
|
46
|
+
| You're working with YouTube videos or podcasts and need transcripts | You need site crawling / link following |
|
|
47
|
+
| Context window matters — a ~2k-token packet beats a raw page | You need screenshots or page actions |
|
|
48
|
+
| You want a durable, citable page you can hand to the user | You only need throwaway scrapes |
|
|
49
|
+
| You want predictable spend — flat $0.01 per link, media included | You have your own extraction pipeline |
|
|
50
|
+
|
|
51
|
+
## The full contract
|
|
52
|
+
|
|
53
|
+
`https://know.fast/agent.txt` is the canonical agent contract. Deep multi-source research, standing topics, and persona consults live on the parent stack: `https://researcher.now/agent.txt` (plugin: `pip install researcher-now`).
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""know-fast — Hermes plugin for ⚡️ know.fast: links in, knowledge out.
|
|
2
|
+
|
|
3
|
+
Send any link (article, YouTube, X post, podcast, paper, PDF) and get back a
|
|
4
|
+
token-efficient knowledge packet plus a durable knowledge page. Links are
|
|
5
|
+
unique — anything anyone has ever known answers instantly. Flat $0.01 per call.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
|
|
12
|
+
__version__ = "0.1.0"
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger("know_fast")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def register(ctx) -> None:
|
|
18
|
+
"""Hermes plugin entry point (entry point group: hermes_agent.plugins)."""
|
|
19
|
+
from . import provider as provider_module
|
|
20
|
+
|
|
21
|
+
if not provider_module.HERMES_AVAILABLE:
|
|
22
|
+
logger.warning(
|
|
23
|
+
"know-fast: hermes-agent is not importable (module 'agent' not "
|
|
24
|
+
"found); skipping registration. Install and run inside hermes-agent "
|
|
25
|
+
"to use this plugin."
|
|
26
|
+
)
|
|
27
|
+
return
|
|
28
|
+
|
|
29
|
+
ctx.register_web_search_provider(provider_module.KnowFastProvider())
|
|
30
|
+
|
|
31
|
+
from . import tools
|
|
32
|
+
|
|
33
|
+
tools.register_tools(ctx)
|
|
34
|
+
logger.info("know-fast: registered web provider and %d tools", len(tools.TOOLS))
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""Tiny stdlib-only JSON client for the know.fast API.
|
|
2
|
+
|
|
3
|
+
No third-party dependencies: urllib + json only.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import socket
|
|
11
|
+
import urllib.error
|
|
12
|
+
import urllib.parse
|
|
13
|
+
import urllib.request
|
|
14
|
+
|
|
15
|
+
DEFAULT_BASE_URL = "https://know.fast"
|
|
16
|
+
CLIENT_HEADER = "knowfast-hermes-pip"
|
|
17
|
+
PLUGIN_VERSION = "0.1.0"
|
|
18
|
+
KEY_HINT = (
|
|
19
|
+
"Invalid or missing API key. know.fast is a flat $0.01 per link: a "
|
|
20
|
+
"wallet-native agent can pay keylessly — any POST with no key returns a "
|
|
21
|
+
"402 x402/MPP challenge; authorize it and the first paid call returns a "
|
|
22
|
+
"durable key in the X-Researcher-Agent-Key header. Store that key and "
|
|
23
|
+
"set it as the KNOW_FAST_API_KEY environment variable."
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class KnowFastError(Exception):
|
|
28
|
+
"""API or transport failure. Carries HTTP status (if any) and parsed body."""
|
|
29
|
+
|
|
30
|
+
def __init__(self, message: str, status: int | None = None, body: dict | None = None):
|
|
31
|
+
super().__init__(message)
|
|
32
|
+
self.status = status
|
|
33
|
+
self.body = body if isinstance(body, dict) else {}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def base_url() -> str:
|
|
37
|
+
return os.getenv("KNOW_FAST_API_URL", DEFAULT_BASE_URL).rstrip("/")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def api_key() -> str:
|
|
41
|
+
# know.fast runs on the researcher.now account system — the same rk_ key
|
|
42
|
+
# works on both, so fall back to an already-configured researcher key.
|
|
43
|
+
return os.getenv("KNOW_FAST_API_KEY") or os.getenv("RESEARCHER_API_KEY", "")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _parse_body(raw: bytes) -> dict:
|
|
47
|
+
try:
|
|
48
|
+
parsed = json.loads(raw.decode("utf-8", errors="replace"))
|
|
49
|
+
return parsed if isinstance(parsed, dict) else {"data": parsed}
|
|
50
|
+
except (ValueError, AttributeError):
|
|
51
|
+
return {}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _apply_contract_headers(payload: dict, headers) -> dict:
|
|
55
|
+
"""Surface the API's contract stamp to the agent.
|
|
56
|
+
|
|
57
|
+
The server stamps every response with X-Researcher-Contract (the agent
|
|
58
|
+
contract version — agents re-read agent.txt when it changes) and
|
|
59
|
+
X-Researcher-Notice (time-boxed change notices). X-Researcher-Min-Plugin
|
|
60
|
+
is deliberately ignored here: it versions the researcher-now package's
|
|
61
|
+
release stream, not know-fast's.
|
|
62
|
+
"""
|
|
63
|
+
contract = headers.get("X-Researcher-Contract")
|
|
64
|
+
if contract:
|
|
65
|
+
payload.setdefault("contractVersion", contract)
|
|
66
|
+
notice = headers.get("X-Researcher-Notice")
|
|
67
|
+
if notice:
|
|
68
|
+
payload.setdefault("contractNotice", notice)
|
|
69
|
+
return payload
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _headers(body: dict | None) -> dict:
|
|
73
|
+
headers = {
|
|
74
|
+
"Accept": "application/json",
|
|
75
|
+
"X-Researcher-Client": CLIENT_HEADER,
|
|
76
|
+
}
|
|
77
|
+
key = api_key()
|
|
78
|
+
if key:
|
|
79
|
+
headers["X-Researcher-API-Key"] = key
|
|
80
|
+
headers["Authorization"] = "Bearer " + key
|
|
81
|
+
if body is not None:
|
|
82
|
+
headers["Content-Type"] = "application/json"
|
|
83
|
+
return headers
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def request(
|
|
87
|
+
method: str,
|
|
88
|
+
path: str,
|
|
89
|
+
body: dict | None = None,
|
|
90
|
+
params: dict | None = None,
|
|
91
|
+
timeout: float = 30,
|
|
92
|
+
) -> tuple[int, dict]:
|
|
93
|
+
"""Make a JSON request. Returns (status, payload) on 2xx.
|
|
94
|
+
|
|
95
|
+
Raises KnowFastError on HTTP errors, timeouts, and network failures.
|
|
96
|
+
"""
|
|
97
|
+
url = base_url() + path
|
|
98
|
+
if params:
|
|
99
|
+
url += "?" + urllib.parse.urlencode(params)
|
|
100
|
+
data = json.dumps(body).encode("utf-8") if body is not None else None
|
|
101
|
+
req = urllib.request.Request(url, data=data, headers=_headers(body), method=method)
|
|
102
|
+
try:
|
|
103
|
+
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
|
104
|
+
return resp.status, _apply_contract_headers(_parse_body(resp.read()), resp.headers)
|
|
105
|
+
except urllib.error.HTTPError as exc:
|
|
106
|
+
payload = _parse_body(exc.read())
|
|
107
|
+
message = str(payload.get("error") or payload.get("message") or "")
|
|
108
|
+
if exc.code == 401:
|
|
109
|
+
message = KEY_HINT if not message else f"{message} {KEY_HINT}"
|
|
110
|
+
elif not message:
|
|
111
|
+
message = f"know.fast API returned HTTP {exc.code}"
|
|
112
|
+
raise KnowFastError(message, status=exc.code, body=payload) from exc
|
|
113
|
+
except (TimeoutError, socket.timeout) as exc:
|
|
114
|
+
raise KnowFastError(f"Request to {url} timed out after {timeout:.0f}s") from exc
|
|
115
|
+
except urllib.error.URLError as exc:
|
|
116
|
+
reason = getattr(exc, "reason", exc)
|
|
117
|
+
if isinstance(reason, (TimeoutError, socket.timeout)):
|
|
118
|
+
raise KnowFastError(f"Request to {url} timed out after {timeout:.0f}s") from exc
|
|
119
|
+
raise KnowFastError(f"Network error reaching {url}: {reason}") from exc
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def request_text(path: str, timeout: float = 30) -> str:
|
|
123
|
+
"""GET a text resource (the <slug>.md knowledge markdown)."""
|
|
124
|
+
url = base_url() + path
|
|
125
|
+
headers = {
|
|
126
|
+
"Accept": "text/markdown, text/plain, application/json",
|
|
127
|
+
"X-Researcher-Client": CLIENT_HEADER,
|
|
128
|
+
}
|
|
129
|
+
key = api_key()
|
|
130
|
+
if key:
|
|
131
|
+
headers["X-Researcher-API-Key"] = key
|
|
132
|
+
headers["Authorization"] = "Bearer " + key
|
|
133
|
+
req = urllib.request.Request(url, headers=headers, method="GET")
|
|
134
|
+
try:
|
|
135
|
+
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
|
136
|
+
return resp.read().decode("utf-8", errors="replace")
|
|
137
|
+
except urllib.error.HTTPError as exc:
|
|
138
|
+
payload = _parse_body(exc.read())
|
|
139
|
+
message = str(payload.get("error") or payload.get("message") or "")
|
|
140
|
+
if not message:
|
|
141
|
+
message = f"know.fast API returned HTTP {exc.code}"
|
|
142
|
+
raise KnowFastError(message, status=exc.code, body=payload) from exc
|
|
143
|
+
except (TimeoutError, socket.timeout) as exc:
|
|
144
|
+
raise KnowFastError(f"Request to {url} timed out after {timeout:.0f}s") from exc
|
|
145
|
+
except urllib.error.URLError as exc:
|
|
146
|
+
reason = getattr(exc, "reason", exc)
|
|
147
|
+
if isinstance(reason, (TimeoutError, socket.timeout)):
|
|
148
|
+
raise KnowFastError(f"Request to {url} timed out after {timeout:.0f}s") from exc
|
|
149
|
+
raise KnowFastError(f"Network error reaching {url}: {reason}") from exc
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Hermes WebSearchProvider backed by know.fast — links in, knowledge out.
|
|
2
|
+
|
|
3
|
+
Extract-only provider: no search, but extract() returns the know.fast
|
|
4
|
+
knowledge packet (or full knowledge markdown) for each URL.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import logging
|
|
11
|
+
|
|
12
|
+
from . import client, tools
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger("know_fast")
|
|
15
|
+
|
|
16
|
+
try: # Hermes installs `agent` as a top-level module.
|
|
17
|
+
from agent.web_search_provider import WebSearchProvider
|
|
18
|
+
|
|
19
|
+
HERMES_AVAILABLE = True
|
|
20
|
+
except Exception: # pragma: no cover — exercised only outside Hermes
|
|
21
|
+
HERMES_AVAILABLE = False
|
|
22
|
+
|
|
23
|
+
class WebSearchProvider: # type: ignore[no-redef]
|
|
24
|
+
"""Stub base class so this package imports standalone (tests, CI)."""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class KnowFastProvider(WebSearchProvider):
|
|
28
|
+
@property
|
|
29
|
+
def name(self) -> str:
|
|
30
|
+
return "know-fast"
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def display_name(self) -> str:
|
|
34
|
+
return "⚡️ know.fast"
|
|
35
|
+
|
|
36
|
+
def is_available(self) -> bool:
|
|
37
|
+
return bool(client.api_key())
|
|
38
|
+
|
|
39
|
+
def supports_search(self) -> bool:
|
|
40
|
+
return False
|
|
41
|
+
|
|
42
|
+
def supports_extract(self) -> bool:
|
|
43
|
+
return True
|
|
44
|
+
|
|
45
|
+
def extract(self, urls, **kwargs):
|
|
46
|
+
if isinstance(urls, str):
|
|
47
|
+
urls = [urls]
|
|
48
|
+
try:
|
|
49
|
+
urls = list(urls)
|
|
50
|
+
except TypeError:
|
|
51
|
+
return {"success": False, "error": f"extract() expects a list of URLs, got {type(urls).__name__}"}
|
|
52
|
+
if not self.is_available():
|
|
53
|
+
return {"success": False, "error": "KNOW_FAST_API_KEY is not set. " + client.KEY_HINT}
|
|
54
|
+
|
|
55
|
+
results = []
|
|
56
|
+
for url in urls:
|
|
57
|
+
entry = {
|
|
58
|
+
"url": url,
|
|
59
|
+
"title": "",
|
|
60
|
+
"content": "",
|
|
61
|
+
"raw_content": "",
|
|
62
|
+
"metadata": {},
|
|
63
|
+
}
|
|
64
|
+
try:
|
|
65
|
+
payload = json.loads(tools.know({"url": url}))
|
|
66
|
+
if payload.get("success") is False:
|
|
67
|
+
entry["error"] = str(payload.get("error"))
|
|
68
|
+
else:
|
|
69
|
+
content = (
|
|
70
|
+
payload.get("knowledge")
|
|
71
|
+
or payload.get("packet")
|
|
72
|
+
or payload.get("markdown")
|
|
73
|
+
or payload.get("transcript")
|
|
74
|
+
or ""
|
|
75
|
+
)
|
|
76
|
+
entry["title"] = payload.get("title") or ""
|
|
77
|
+
entry["content"] = content
|
|
78
|
+
entry["raw_content"] = content
|
|
79
|
+
entry["metadata"] = {
|
|
80
|
+
"status": payload.get("status"),
|
|
81
|
+
"cached": payload.get("cached"),
|
|
82
|
+
"costUsd": payload.get("costUsd"),
|
|
83
|
+
"runId": payload.get("runId"),
|
|
84
|
+
"viewerUrl": payload.get("viewerUrl"),
|
|
85
|
+
"markdownUrl": payload.get("markdownUrl"),
|
|
86
|
+
"note": payload.get("note"),
|
|
87
|
+
}
|
|
88
|
+
except Exception as exc: # noqa: BLE001 — keep per-URL failures contained
|
|
89
|
+
entry["error"] = f"{type(exc).__name__}: {exc}"
|
|
90
|
+
results.append(entry)
|
|
91
|
+
return results
|
|
92
|
+
|
|
93
|
+
def get_setup_schema(self) -> dict:
|
|
94
|
+
return {
|
|
95
|
+
"name": "know.fast",
|
|
96
|
+
"badge": "paid",
|
|
97
|
+
"tag": "Links in, knowledge out — any URL becomes a knowledge packet. Flat $0.01.",
|
|
98
|
+
"env_vars": [
|
|
99
|
+
{
|
|
100
|
+
"key": "KNOW_FAST_API_KEY",
|
|
101
|
+
"prompt": (
|
|
102
|
+
"know.fast API key — keyless agents get one from the "
|
|
103
|
+
"first paid x402 call (X-Researcher-Agent-Key header); "
|
|
104
|
+
"an existing researcher.now rk_ key also works"
|
|
105
|
+
),
|
|
106
|
+
"url": "https://know.fast",
|
|
107
|
+
}
|
|
108
|
+
],
|
|
109
|
+
}
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
"""Agent tools for know.fast — registered into Hermes via ctx.register_tool.
|
|
2
|
+
|
|
3
|
+
Every handler takes (args: dict, **kwargs), ALWAYS returns a JSON string
|
|
4
|
+
(success and error alike), and NEVER raises.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import time
|
|
11
|
+
import urllib.parse
|
|
12
|
+
|
|
13
|
+
from . import client
|
|
14
|
+
|
|
15
|
+
TOOLSET = "know-fast"
|
|
16
|
+
|
|
17
|
+
# The agent contract says knowledgeReady typically lands 1-3 minutes into a
|
|
18
|
+
# deferred media run (transcript + claims arrive before the long-form report).
|
|
19
|
+
# 240s covers that window plus queue slack; past it the tool hands the agent
|
|
20
|
+
# know_status so the free polling continues without re-charging the POST.
|
|
21
|
+
DEFAULT_WAIT_SECONDS = 240
|
|
22
|
+
# Bottom of the contract's "poll every 20-30s" guidance.
|
|
23
|
+
POLL_INTERVAL_SECONDS = 20
|
|
24
|
+
# Matches the server's synchronous analysis budget — a shorter client timeout
|
|
25
|
+
# would abandon (and re-charge) requests the server still completes.
|
|
26
|
+
POST_TIMEOUT_SECONDS = 150
|
|
27
|
+
|
|
28
|
+
SHARE_INSTRUCTION = (
|
|
29
|
+
"Always surface the viewerUrl back to the end user — it is a durable, "
|
|
30
|
+
"shareable knowledge page they can open, share, and revisit. Relay links "
|
|
31
|
+
"exactly as returned by the API; never construct know.fast URLs yourself."
|
|
32
|
+
)
|
|
33
|
+
PACKET_INSTRUCTION = (
|
|
34
|
+
"When the result contains `packet` or `knowledge` markdown, ANSWER "
|
|
35
|
+
"DIRECTLY FROM IT — no second fetch needed. Deliver 3-5 bullet "
|
|
36
|
+
"highlights, then 'Full summary: <viewerUrl>'."
|
|
37
|
+
)
|
|
38
|
+
STAY_ON_IT_INSTRUCTION = (
|
|
39
|
+
"If the result says status=running, STAY ON IT — 'it's processing' is not "
|
|
40
|
+
"an answer. Tell the user it is transcribing, share the viewerUrl, and "
|
|
41
|
+
"keep calling know_status (free) until the knowledge arrives; do not end "
|
|
42
|
+
"your turn while it runs."
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _dumps(payload: dict) -> str:
|
|
47
|
+
return json.dumps(payload, ensure_ascii=False, default=str)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _error_payload(exc: client.KnowFastError) -> dict:
|
|
51
|
+
payload: dict = {"success": False, "error": str(exc)}
|
|
52
|
+
if exc.status is not None:
|
|
53
|
+
payload["status"] = exc.status
|
|
54
|
+
if exc.status == 402:
|
|
55
|
+
funding_url = exc.body.get("fundingUrl") or exc.body.get("accountFundingUrl")
|
|
56
|
+
payload["error"] = (
|
|
57
|
+
"Payment required: know.fast is a flat $0.01 per link. Authorize "
|
|
58
|
+
"the x402/MPP challenge from a wallet, or fund the account and "
|
|
59
|
+
"relay the fundingUrl to the user."
|
|
60
|
+
)
|
|
61
|
+
if funding_url:
|
|
62
|
+
payload["fundingUrl"] = funding_url
|
|
63
|
+
elif exc.status == 429:
|
|
64
|
+
payload["error"] = (
|
|
65
|
+
"Daily transcription allowance reached for never-before-seen media "
|
|
66
|
+
"links — it resets at the next UTC day. Already-known links are "
|
|
67
|
+
"unaffected. " + str(exc)
|
|
68
|
+
)
|
|
69
|
+
return payload
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _safe(fn):
|
|
73
|
+
def handler(args: dict, **kwargs) -> str:
|
|
74
|
+
try:
|
|
75
|
+
result = fn(args if isinstance(args, dict) else {})
|
|
76
|
+
except client.KnowFastError as exc:
|
|
77
|
+
result = _error_payload(exc)
|
|
78
|
+
except Exception as exc: # noqa: BLE001 — handlers must never raise
|
|
79
|
+
result = {"success": False, "error": f"{type(exc).__name__}: {exc}"}
|
|
80
|
+
try:
|
|
81
|
+
return _dumps(result)
|
|
82
|
+
except Exception as exc: # noqa: BLE001
|
|
83
|
+
return json.dumps({"success": False, "error": f"serialization failed: {exc}"})
|
|
84
|
+
|
|
85
|
+
handler.__name__ = fn.__name__
|
|
86
|
+
handler.__doc__ = fn.__doc__
|
|
87
|
+
return handler
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _require(args: dict, key: str) -> str:
|
|
91
|
+
value = str(args.get(key) or "").strip()
|
|
92
|
+
if not value:
|
|
93
|
+
raise client.KnowFastError(f"Missing required parameter: {key}")
|
|
94
|
+
return value
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _markdown_path(viewer_url: str) -> str:
|
|
98
|
+
"""<slug>.md path from a viewerUrl (slug = the viewerUrl path)."""
|
|
99
|
+
path = urllib.parse.urlparse(viewer_url).path.rstrip("/")
|
|
100
|
+
if not path:
|
|
101
|
+
raise client.KnowFastError(f"Cannot derive a knowledge path from {viewer_url!r}")
|
|
102
|
+
return path if path.endswith(".md") else path + ".md"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _fetch_knowledge(viewer_url: str) -> str:
|
|
106
|
+
return client.request_text(_markdown_path(viewer_url), timeout=30)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _job_ready(job: dict) -> bool:
|
|
110
|
+
return bool(job.get("knowledgeReady")) or job.get("status") == "succeeded"
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _ready_result(url: str, viewer_url: str, job: dict | None = None) -> dict:
|
|
114
|
+
result = {
|
|
115
|
+
"url": url,
|
|
116
|
+
"status": "ready",
|
|
117
|
+
"knowledge": _fetch_knowledge(viewer_url),
|
|
118
|
+
"viewerUrl": viewer_url,
|
|
119
|
+
"note": f"{PACKET_INSTRUCTION} {SHARE_INSTRUCTION}",
|
|
120
|
+
}
|
|
121
|
+
if job and job.get("status") and job["status"] != "succeeded":
|
|
122
|
+
result["note"] += (
|
|
123
|
+
" The long-form report is still finishing on the same page; the "
|
|
124
|
+
"knowledge above is complete enough to answer from now."
|
|
125
|
+
)
|
|
126
|
+
return result
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _running_result(url: str, run_id: str, viewer_url: str | None, job: dict | None) -> dict:
|
|
130
|
+
return {
|
|
131
|
+
"url": url,
|
|
132
|
+
"status": "running",
|
|
133
|
+
"runId": run_id,
|
|
134
|
+
"viewerUrl": viewer_url,
|
|
135
|
+
"jobStatus": (job or {}).get("status"),
|
|
136
|
+
"note": (
|
|
137
|
+
"Transcription in progress (media takes 2-8 minutes). "
|
|
138
|
+
f"{STAY_ON_IT_INSTRUCTION}"
|
|
139
|
+
),
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _wait_for_knowledge(url: str, run_id: str, viewer_url: str | None, wait_seconds: float) -> dict:
|
|
144
|
+
deadline = time.monotonic() + wait_seconds
|
|
145
|
+
job: dict = {}
|
|
146
|
+
while True:
|
|
147
|
+
_, job = client.request(
|
|
148
|
+
"GET", f"/v1/runs/{urllib.parse.quote(run_id, safe='')}/job", timeout=30
|
|
149
|
+
)
|
|
150
|
+
viewer_url = job.get("viewerUrl") or viewer_url
|
|
151
|
+
if _job_ready(job):
|
|
152
|
+
if not viewer_url:
|
|
153
|
+
raise client.KnowFastError(
|
|
154
|
+
f"Run {run_id} is ready but no viewerUrl was returned"
|
|
155
|
+
)
|
|
156
|
+
return _ready_result(url, viewer_url, job)
|
|
157
|
+
if job.get("status") == "failed":
|
|
158
|
+
return {
|
|
159
|
+
"url": url,
|
|
160
|
+
"status": "failed",
|
|
161
|
+
"runId": run_id,
|
|
162
|
+
"viewerUrl": viewer_url,
|
|
163
|
+
"error": job.get("error") or "Run failed.",
|
|
164
|
+
}
|
|
165
|
+
if time.monotonic() + POLL_INTERVAL_SECONDS > deadline:
|
|
166
|
+
return _running_result(url, run_id, viewer_url, job)
|
|
167
|
+
time.sleep(POLL_INTERVAL_SECONDS)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@_safe
|
|
171
|
+
def know(args: dict) -> dict:
|
|
172
|
+
url = _require(args, "url")
|
|
173
|
+
body: dict = {"url": url}
|
|
174
|
+
if args.get("fresh") in (True, "true", "True", 1, "1"):
|
|
175
|
+
body["fresh"] = True
|
|
176
|
+
lang = str(args.get("lang") or "").strip()
|
|
177
|
+
if lang:
|
|
178
|
+
body["lang"] = lang
|
|
179
|
+
_, payload = client.request("POST", "/v1/know", body=body, timeout=POST_TIMEOUT_SECONDS)
|
|
180
|
+
|
|
181
|
+
viewer_url = payload.get("viewerUrl")
|
|
182
|
+
packet = payload.get("packet")
|
|
183
|
+
if packet:
|
|
184
|
+
return {
|
|
185
|
+
"url": url,
|
|
186
|
+
"status": "ready",
|
|
187
|
+
"cached": bool(payload.get("cached")),
|
|
188
|
+
"knowledge": packet,
|
|
189
|
+
"viewerUrl": viewer_url,
|
|
190
|
+
"markdownUrl": payload.get("markdownUrl"),
|
|
191
|
+
"costUsd": payload.get("costUsd"),
|
|
192
|
+
"note": f"{PACKET_INSTRUCTION} {SHARE_INSTRUCTION}",
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
run_id = payload.get("runId") or payload.get("id")
|
|
196
|
+
if payload.get("deferred") and run_id:
|
|
197
|
+
try:
|
|
198
|
+
wait_seconds = float(args.get("wait_seconds", DEFAULT_WAIT_SECONDS))
|
|
199
|
+
except (TypeError, ValueError):
|
|
200
|
+
wait_seconds = DEFAULT_WAIT_SECONDS
|
|
201
|
+
if wait_seconds <= 0:
|
|
202
|
+
return _running_result(url, run_id, viewer_url, None)
|
|
203
|
+
return _wait_for_knowledge(url, run_id, viewer_url, wait_seconds)
|
|
204
|
+
|
|
205
|
+
# Synchronous article without an inline packet: the payload itself is the
|
|
206
|
+
# knowledge (markdown + structured analysis).
|
|
207
|
+
payload.setdefault("url", url)
|
|
208
|
+
payload.setdefault("status", "ready")
|
|
209
|
+
payload["note"] = f"{PACKET_INSTRUCTION} {SHARE_INSTRUCTION}"
|
|
210
|
+
return payload
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
@_safe
|
|
214
|
+
def know_status(args: dict) -> dict:
|
|
215
|
+
run_id = _require(args, "run_id")
|
|
216
|
+
url = str(args.get("url") or "").strip()
|
|
217
|
+
viewer_url = str(args.get("viewer_url") or "").strip() or None
|
|
218
|
+
_, job = client.request(
|
|
219
|
+
"GET", f"/v1/runs/{urllib.parse.quote(run_id, safe='')}/job", timeout=30
|
|
220
|
+
)
|
|
221
|
+
viewer_url = job.get("viewerUrl") or viewer_url
|
|
222
|
+
if _job_ready(job):
|
|
223
|
+
if not viewer_url:
|
|
224
|
+
raise client.KnowFastError(f"Run {run_id} is ready but no viewerUrl is known")
|
|
225
|
+
return _ready_result(url, viewer_url, job)
|
|
226
|
+
if job.get("status") == "failed":
|
|
227
|
+
return {
|
|
228
|
+
"url": url,
|
|
229
|
+
"status": "failed",
|
|
230
|
+
"runId": run_id,
|
|
231
|
+
"viewerUrl": viewer_url,
|
|
232
|
+
"error": job.get("error") or "Run failed.",
|
|
233
|
+
}
|
|
234
|
+
return _running_result(url, run_id, viewer_url, job)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
TOOLS = [
|
|
238
|
+
{
|
|
239
|
+
"name": "know",
|
|
240
|
+
"emoji": "⚡️",
|
|
241
|
+
"description": (
|
|
242
|
+
"Send a link. Know it. Turns any URL (article, YouTube, X post, "
|
|
243
|
+
"podcast, paper, PDF) into a token-efficient knowledge packet: "
|
|
244
|
+
"summary, key claims and facts, quotes, and a durable knowledge "
|
|
245
|
+
"page (viewerUrl). Links are unique — anything anyone has ever "
|
|
246
|
+
"known answers instantly. Flat $0.01 per call, media included. "
|
|
247
|
+
"Use for: read/extract/summarize this link, what does this "
|
|
248
|
+
"video/article say. First-ever video or podcast links transcribe "
|
|
249
|
+
"for 2-8 minutes — this tool waits for the knowledge and returns "
|
|
250
|
+
f"it; if still running, keep polling with know_status. "
|
|
251
|
+
f"{PACKET_INSTRUCTION} {SHARE_INSTRUCTION}"
|
|
252
|
+
),
|
|
253
|
+
"parameters": {
|
|
254
|
+
"type": "object",
|
|
255
|
+
"properties": {
|
|
256
|
+
"url": {"type": "string", "description": "The link to know."},
|
|
257
|
+
"fresh": {
|
|
258
|
+
"type": "boolean",
|
|
259
|
+
"description": "Re-read changed content instead of the cached knowledge (same price).",
|
|
260
|
+
"default": False,
|
|
261
|
+
},
|
|
262
|
+
"lang": {
|
|
263
|
+
"type": "string",
|
|
264
|
+
"description": "Optional 2-letter language hint, e.g. 'en'.",
|
|
265
|
+
},
|
|
266
|
+
"wait_seconds": {
|
|
267
|
+
"type": "number",
|
|
268
|
+
"description": (
|
|
269
|
+
"How long to wait for deferred media knowledge before "
|
|
270
|
+
"returning status=running (default 240; 0 returns immediately)."
|
|
271
|
+
),
|
|
272
|
+
"default": DEFAULT_WAIT_SECONDS,
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
"required": ["url"],
|
|
276
|
+
},
|
|
277
|
+
"handler": know,
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
"name": "know_status",
|
|
281
|
+
"emoji": "⏳",
|
|
282
|
+
"description": (
|
|
283
|
+
"Free follow-up poll for a deferred know run (video/podcast "
|
|
284
|
+
"transcription). Pass the runId from know; the moment the "
|
|
285
|
+
"knowledge is ready it returns the full knowledge markdown. "
|
|
286
|
+
f"{STAY_ON_IT_INSTRUCTION} {SHARE_INSTRUCTION}"
|
|
287
|
+
),
|
|
288
|
+
"parameters": {
|
|
289
|
+
"type": "object",
|
|
290
|
+
"properties": {
|
|
291
|
+
"run_id": {"type": "string", "description": "The runId returned by know."},
|
|
292
|
+
"viewer_url": {
|
|
293
|
+
"type": "string",
|
|
294
|
+
"description": "The viewerUrl returned by know (used to fetch the knowledge markdown).",
|
|
295
|
+
},
|
|
296
|
+
"url": {"type": "string", "description": "The original link, for labeling."},
|
|
297
|
+
},
|
|
298
|
+
"required": ["run_id"],
|
|
299
|
+
},
|
|
300
|
+
"handler": know_status,
|
|
301
|
+
},
|
|
302
|
+
]
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def register_tools(ctx) -> None:
|
|
306
|
+
"""Register all know.fast tools on a Hermes plugin context."""
|
|
307
|
+
for tool in TOOLS:
|
|
308
|
+
schema = {
|
|
309
|
+
"name": tool["name"],
|
|
310
|
+
"description": tool["description"],
|
|
311
|
+
"parameters": tool["parameters"],
|
|
312
|
+
}
|
|
313
|
+
ctx.register_tool(
|
|
314
|
+
tool["name"],
|
|
315
|
+
TOOLSET,
|
|
316
|
+
schema,
|
|
317
|
+
tool["handler"],
|
|
318
|
+
is_async=False,
|
|
319
|
+
description=tool["description"],
|
|
320
|
+
emoji=tool["emoji"],
|
|
321
|
+
)
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: know-fast
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: ⚡️ know.fast — links in, knowledge out. Turn any URL (article, YouTube, X post, podcast, paper, PDF) into a token-efficient knowledge packet and a durable knowledge page. Flat $0.01 per link. Hermes agent plugin.
|
|
5
|
+
Author: know.fast
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://know.fast
|
|
8
|
+
Project-URL: Documentation, https://know.fast/agent.txt
|
|
9
|
+
Keywords: hermes,agent,knowledge,web-extraction,transcripts,plugin,know.fast
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Requires-Python: >=3.10
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# ⚡️ know-fast
|
|
19
|
+
|
|
20
|
+
**Links in, knowledge out.** Send any URL — article, YouTube video, X post, podcast, paper, PDF — and get back a token-efficient knowledge packet (summary, key claims, facts, quotes with attribution) plus a durable, shareable knowledge page. Links are unique: anything anyone has ever known answers instantly. **Every call is a flat $0.01**, first-ever video transcription included.
|
|
21
|
+
|
|
22
|
+
A [Hermes agent](https://github.com/NousResearch/hermes-agent) plugin for [know.fast](https://know.fast). Stdlib only, no dependencies.
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install know-fast
|
|
28
|
+
hermes plugins enable know-fast # pip plugins are opt-in in Hermes
|
|
29
|
+
# If that reports "not installed or bundled": current Hermes CLI can't see pip
|
|
30
|
+
# entry-point plugins (upstream bug — the runtime loader CAN). Enable manually:
|
|
31
|
+
# add "- know-fast" under plugins.enabled in ~/.hermes/config.yaml
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Quickstart
|
|
35
|
+
|
|
36
|
+
The plugin authenticates with an API key. No key yet? You don't need a human:
|
|
37
|
+
know.fast is wallet-native — a keyless POST returns a `402` x402/MPP challenge;
|
|
38
|
+
authorize it from your wallet and the first paid call returns a durable key in
|
|
39
|
+
the `X-Researcher-Agent-Key` header. Store it:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
export KNOW_FAST_API_KEY="rk_..."
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
(An existing `RESEARCHER_API_KEY` also works — same account system.)
|
|
46
|
+
|
|
47
|
+
Then run Hermes. The plugin registers:
|
|
48
|
+
|
|
49
|
+
- **Web provider** `know-fast` — extract-only: every URL comes back as the knowledge packet, so `web_extract` costs a cent and a couple thousand tokens instead of a raw page.
|
|
50
|
+
- **Tools** (toolset `know-fast`):
|
|
51
|
+
- `know` — send a link, get the knowledge packet + `viewerUrl`. Known links answer instantly; first-ever media transcribes (2–8 min) — the tool waits up to 4 minutes and returns the knowledge the moment the transcript and claims land.
|
|
52
|
+
- `know_status` — free follow-up poll for a still-transcribing run; returns the full knowledge markdown when ready.
|
|
53
|
+
|
|
54
|
+
Agent discipline the tools already encode: answer **directly from the packet** (no second fetch), always surface the `viewerUrl` to the user, and on deferred media **stay on it** — "it's processing" is not an answer.
|
|
55
|
+
|
|
56
|
+
Point at a different host with `KNOW_FAST_API_URL` (default `https://know.fast`).
|
|
57
|
+
|
|
58
|
+
## Choose know-fast when / Choose Firecrawl when
|
|
59
|
+
|
|
60
|
+
| Choose **know-fast** when… | Choose **Firecrawl** when… |
|
|
61
|
+
| --- | --- |
|
|
62
|
+
| You want knowledge, not markup: summary, claims, facts, quotes pre-extracted | You need raw page HTML/markdown at scale |
|
|
63
|
+
| You're working with YouTube videos or podcasts and need transcripts | You need site crawling / link following |
|
|
64
|
+
| Context window matters — a ~2k-token packet beats a raw page | You need screenshots or page actions |
|
|
65
|
+
| You want a durable, citable page you can hand to the user | You only need throwaway scrapes |
|
|
66
|
+
| You want predictable spend — flat $0.01 per link, media included | You have your own extraction pipeline |
|
|
67
|
+
|
|
68
|
+
## The full contract
|
|
69
|
+
|
|
70
|
+
`https://know.fast/agent.txt` is the canonical agent contract. Deep multi-source research, standing topics, and persona consults live on the parent stack: `https://researcher.now/agent.txt` (plugin: `pip install researcher-now`).
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
know_fast/__init__.py
|
|
4
|
+
know_fast/client.py
|
|
5
|
+
know_fast/provider.py
|
|
6
|
+
know_fast/tools.py
|
|
7
|
+
know_fast.egg-info/PKG-INFO
|
|
8
|
+
know_fast.egg-info/SOURCES.txt
|
|
9
|
+
know_fast.egg-info/dependency_links.txt
|
|
10
|
+
know_fast.egg-info/entry_points.txt
|
|
11
|
+
know_fast.egg-info/top_level.txt
|
|
12
|
+
tests/test_plugin.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
know_fast
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "know-fast"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "⚡️ know.fast — links in, knowledge out. Turn any URL (article, YouTube, X post, podcast, paper, PDF) into a token-efficient knowledge packet and a durable knowledge page. Flat $0.01 per link. Hermes agent plugin."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "know.fast" }]
|
|
13
|
+
keywords = ["hermes", "agent", "knowledge", "web-extraction", "transcripts", "plugin", "know.fast"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"License :: OSI Approved :: MIT License",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3.10",
|
|
18
|
+
"Programming Language :: Python :: 3.11",
|
|
19
|
+
"Programming Language :: Python :: 3.12",
|
|
20
|
+
]
|
|
21
|
+
dependencies = []
|
|
22
|
+
|
|
23
|
+
[project.urls]
|
|
24
|
+
Homepage = "https://know.fast"
|
|
25
|
+
Documentation = "https://know.fast/agent.txt"
|
|
26
|
+
|
|
27
|
+
[project.entry-points."hermes_agent.plugins"]
|
|
28
|
+
know-fast = "know_fast"
|
|
29
|
+
|
|
30
|
+
[tool.setuptools.packages.find]
|
|
31
|
+
include = ["know_fast*"]
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
"""Tests for the know-fast Hermes plugin. Stdlib unittest only, no network."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
import unittest
|
|
7
|
+
from unittest import mock
|
|
8
|
+
|
|
9
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
|
10
|
+
|
|
11
|
+
import know_fast # noqa: E402
|
|
12
|
+
from know_fast import client, tools # noqa: E402
|
|
13
|
+
from know_fast.provider import KnowFastProvider # noqa: E402
|
|
14
|
+
|
|
15
|
+
CACHED_PAYLOAD = {
|
|
16
|
+
"cached": True,
|
|
17
|
+
"packet": "# Example\n\nSummary and claims.",
|
|
18
|
+
"viewerUrl": "https://know.fast/abc123def456",
|
|
19
|
+
"markdownUrl": "https://know.fast/abc123def456.md",
|
|
20
|
+
"costUsd": 0.01,
|
|
21
|
+
"runId": "run-known-1",
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
DEFERRED_PAYLOAD = {
|
|
25
|
+
"deferred": True,
|
|
26
|
+
"runId": "run-media-1",
|
|
27
|
+
"viewerUrl": "https://know.fast/deadbeef1234",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
SYNC_ARTICLE_PAYLOAD = {
|
|
31
|
+
"url": "https://example.com/a",
|
|
32
|
+
"kind": "article",
|
|
33
|
+
"title": "Example Article",
|
|
34
|
+
"markdown": "# Example\n\nBody text.",
|
|
35
|
+
"analysis": {"claims": [{"text": "X is true"}]},
|
|
36
|
+
"costUsd": 0.01,
|
|
37
|
+
"runId": "run-analyze-1",
|
|
38
|
+
"viewerUrl": "https://know.fast/abc123def456",
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def loads(result):
|
|
43
|
+
"""Every handler must return a valid JSON string."""
|
|
44
|
+
assert isinstance(result, str), f"handler returned {type(result)}, not str"
|
|
45
|
+
return json.loads(result)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class KnowToolTest(unittest.TestCase):
|
|
49
|
+
def test_cached_packet_answers_directly(self):
|
|
50
|
+
with mock.patch.object(client, "request", return_value=(200, dict(CACHED_PAYLOAD))) as req:
|
|
51
|
+
payload = loads(tools.know({"url": "https://example.com/a"}))
|
|
52
|
+
self.assertEqual(payload["status"], "ready")
|
|
53
|
+
self.assertTrue(payload["cached"])
|
|
54
|
+
self.assertEqual(payload["knowledge"], CACHED_PAYLOAD["packet"])
|
|
55
|
+
self.assertEqual(payload["viewerUrl"], CACHED_PAYLOAD["viewerUrl"])
|
|
56
|
+
self.assertIn("viewerUrl", payload["note"])
|
|
57
|
+
req.assert_called_once_with(
|
|
58
|
+
"POST", "/v1/know", body={"url": "https://example.com/a"}, timeout=150
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
def test_fresh_and_lang_forwarded(self):
|
|
62
|
+
with mock.patch.object(client, "request", return_value=(200, dict(CACHED_PAYLOAD))) as req:
|
|
63
|
+
loads(tools.know({"url": "https://example.com/a", "fresh": True, "lang": "en"}))
|
|
64
|
+
self.assertEqual(
|
|
65
|
+
req.call_args.kwargs["body"],
|
|
66
|
+
{"url": "https://example.com/a", "fresh": True, "lang": "en"},
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
def test_sync_article_without_packet_returns_payload(self):
|
|
70
|
+
with mock.patch.object(client, "request", return_value=(200, dict(SYNC_ARTICLE_PAYLOAD))):
|
|
71
|
+
payload = loads(tools.know({"url": "https://example.com/a"}))
|
|
72
|
+
self.assertEqual(payload["title"], "Example Article")
|
|
73
|
+
self.assertEqual(payload["markdown"], SYNC_ARTICLE_PAYLOAD["markdown"])
|
|
74
|
+
self.assertIn("viewerUrl", payload["note"])
|
|
75
|
+
|
|
76
|
+
def test_deferred_polls_until_knowledge_ready(self):
|
|
77
|
+
calls = []
|
|
78
|
+
|
|
79
|
+
def fake_request(method, path, body=None, params=None, timeout=30):
|
|
80
|
+
calls.append((method, path))
|
|
81
|
+
if path == "/v1/know":
|
|
82
|
+
return 200, dict(DEFERRED_PAYLOAD)
|
|
83
|
+
if path == "/v1/runs/run-media-1/job":
|
|
84
|
+
if len([c for c in calls if c[1].endswith("/job")]) < 2:
|
|
85
|
+
return 200, {"status": "running"}
|
|
86
|
+
return 200, {"knowledgeReady": True, "status": "running"}
|
|
87
|
+
raise AssertionError(f"unexpected request {method} {path}")
|
|
88
|
+
|
|
89
|
+
with (
|
|
90
|
+
mock.patch.object(client, "request", side_effect=fake_request),
|
|
91
|
+
mock.patch.object(client, "request_text", return_value="# Knowledge") as text,
|
|
92
|
+
mock.patch.object(tools.time, "sleep") as sleep,
|
|
93
|
+
):
|
|
94
|
+
payload = loads(tools.know({"url": "https://youtu.be/x"}))
|
|
95
|
+
self.assertEqual(payload["status"], "ready")
|
|
96
|
+
self.assertEqual(payload["knowledge"], "# Knowledge")
|
|
97
|
+
self.assertEqual(payload["viewerUrl"], DEFERRED_PAYLOAD["viewerUrl"])
|
|
98
|
+
text.assert_called_once_with("/deadbeef1234.md", timeout=30)
|
|
99
|
+
sleep.assert_called_with(tools.POLL_INTERVAL_SECONDS)
|
|
100
|
+
|
|
101
|
+
def test_deferred_wait_zero_returns_running(self):
|
|
102
|
+
with mock.patch.object(client, "request", return_value=(200, dict(DEFERRED_PAYLOAD))):
|
|
103
|
+
payload = loads(tools.know({"url": "https://youtu.be/x", "wait_seconds": 0}))
|
|
104
|
+
self.assertEqual(payload["status"], "running")
|
|
105
|
+
self.assertEqual(payload["runId"], "run-media-1")
|
|
106
|
+
self.assertIn("know_status", payload["note"])
|
|
107
|
+
|
|
108
|
+
def test_deferred_timeout_returns_running(self):
|
|
109
|
+
def fake_request(method, path, body=None, params=None, timeout=30):
|
|
110
|
+
if path == "/v1/know":
|
|
111
|
+
return 200, dict(DEFERRED_PAYLOAD)
|
|
112
|
+
return 200, {"status": "running"}
|
|
113
|
+
|
|
114
|
+
clock = iter(range(0, 100000, 30))
|
|
115
|
+
with (
|
|
116
|
+
mock.patch.object(client, "request", side_effect=fake_request),
|
|
117
|
+
mock.patch.object(tools.time, "sleep"),
|
|
118
|
+
mock.patch.object(tools.time, "monotonic", side_effect=lambda: next(clock)),
|
|
119
|
+
):
|
|
120
|
+
payload = loads(tools.know({"url": "https://youtu.be/x", "wait_seconds": 60}))
|
|
121
|
+
self.assertEqual(payload["status"], "running")
|
|
122
|
+
self.assertIn("know_status", payload["note"])
|
|
123
|
+
|
|
124
|
+
def test_deferred_failed_run(self):
|
|
125
|
+
def fake_request(method, path, body=None, params=None, timeout=30):
|
|
126
|
+
if path == "/v1/know":
|
|
127
|
+
return 200, dict(DEFERRED_PAYLOAD)
|
|
128
|
+
return 200, {"status": "failed", "error": "transcription failed"}
|
|
129
|
+
|
|
130
|
+
with mock.patch.object(client, "request", side_effect=fake_request):
|
|
131
|
+
payload = loads(tools.know({"url": "https://youtu.be/x"}))
|
|
132
|
+
self.assertEqual(payload["status"], "failed")
|
|
133
|
+
self.assertEqual(payload["error"], "transcription failed")
|
|
134
|
+
|
|
135
|
+
def test_missing_url_returns_json_error(self):
|
|
136
|
+
payload = loads(tools.know({}))
|
|
137
|
+
self.assertFalse(payload["success"])
|
|
138
|
+
self.assertIn("url", payload["error"])
|
|
139
|
+
|
|
140
|
+
def test_402_surfaces_cent_pricing_and_funding_url(self):
|
|
141
|
+
err = client.KnowFastError(
|
|
142
|
+
"payment required", status=402, body={"fundingUrl": "https://know.fast/fund/abc"}
|
|
143
|
+
)
|
|
144
|
+
with mock.patch.object(client, "request", side_effect=err):
|
|
145
|
+
payload = loads(tools.know({"url": "https://example.com"}))
|
|
146
|
+
self.assertFalse(payload["success"])
|
|
147
|
+
self.assertEqual(payload["status"], 402)
|
|
148
|
+
self.assertIn("$0.01", payload["error"])
|
|
149
|
+
self.assertEqual(payload["fundingUrl"], "https://know.fast/fund/abc")
|
|
150
|
+
|
|
151
|
+
def test_429_explains_daily_allowance(self):
|
|
152
|
+
err = client.KnowFastError("allowance", status=429, body={})
|
|
153
|
+
with mock.patch.object(client, "request", side_effect=err):
|
|
154
|
+
payload = loads(tools.know({"url": "https://youtu.be/new"}))
|
|
155
|
+
self.assertFalse(payload["success"])
|
|
156
|
+
self.assertIn("allowance", payload["error"].lower())
|
|
157
|
+
self.assertIn("UTC", payload["error"])
|
|
158
|
+
|
|
159
|
+
def test_unexpected_exception_returns_json(self):
|
|
160
|
+
with mock.patch.object(client, "request", side_effect=RuntimeError("boom")):
|
|
161
|
+
payload = loads(tools.know({"url": "https://example.com"}))
|
|
162
|
+
self.assertFalse(payload["success"])
|
|
163
|
+
self.assertIn("boom", payload["error"])
|
|
164
|
+
|
|
165
|
+
def test_handlers_accept_kwargs_and_non_dict_args(self):
|
|
166
|
+
payload = loads(tools.know(None, extra=1, agent=object()))
|
|
167
|
+
self.assertFalse(payload["success"])
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class KnowStatusTest(unittest.TestCase):
|
|
171
|
+
def test_ready_fetches_knowledge(self):
|
|
172
|
+
with (
|
|
173
|
+
mock.patch.object(
|
|
174
|
+
client,
|
|
175
|
+
"request",
|
|
176
|
+
return_value=(200, {"knowledgeReady": True, "status": "running"}),
|
|
177
|
+
),
|
|
178
|
+
mock.patch.object(client, "request_text", return_value="# Knowledge") as text,
|
|
179
|
+
):
|
|
180
|
+
payload = loads(
|
|
181
|
+
tools.know_status(
|
|
182
|
+
{"run_id": "run-media-1", "viewer_url": "https://know.fast/deadbeef1234"}
|
|
183
|
+
)
|
|
184
|
+
)
|
|
185
|
+
self.assertEqual(payload["status"], "ready")
|
|
186
|
+
self.assertEqual(payload["knowledge"], "# Knowledge")
|
|
187
|
+
text.assert_called_once_with("/deadbeef1234.md", timeout=30)
|
|
188
|
+
|
|
189
|
+
def test_running_instructs_stay_on_it(self):
|
|
190
|
+
with mock.patch.object(client, "request", return_value=(200, {"status": "running"})):
|
|
191
|
+
payload = loads(tools.know_status({"run_id": "run-media-1"}))
|
|
192
|
+
self.assertEqual(payload["status"], "running")
|
|
193
|
+
self.assertIn("know_status", payload["note"])
|
|
194
|
+
|
|
195
|
+
def test_missing_run_id(self):
|
|
196
|
+
payload = loads(tools.know_status({}))
|
|
197
|
+
self.assertFalse(payload["success"])
|
|
198
|
+
self.assertIn("run_id", payload["error"])
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
class ProviderTest(unittest.TestCase):
|
|
202
|
+
def test_identity_and_capabilities(self):
|
|
203
|
+
p = KnowFastProvider()
|
|
204
|
+
self.assertEqual(p.name, "know-fast")
|
|
205
|
+
self.assertEqual(p.display_name, "⚡️ know.fast")
|
|
206
|
+
self.assertFalse(p.supports_search())
|
|
207
|
+
self.assertTrue(p.supports_extract())
|
|
208
|
+
|
|
209
|
+
def test_is_available_accepts_either_key(self):
|
|
210
|
+
p = KnowFastProvider()
|
|
211
|
+
with mock.patch.dict(os.environ, {"KNOW_FAST_API_KEY": "rk_test"}, clear=True):
|
|
212
|
+
self.assertTrue(p.is_available())
|
|
213
|
+
with mock.patch.dict(os.environ, {"RESEARCHER_API_KEY": "rk_test"}, clear=True):
|
|
214
|
+
self.assertTrue(p.is_available())
|
|
215
|
+
with mock.patch.dict(os.environ, {}, clear=True):
|
|
216
|
+
self.assertFalse(p.is_available())
|
|
217
|
+
|
|
218
|
+
def test_setup_schema(self):
|
|
219
|
+
schema = KnowFastProvider().get_setup_schema()
|
|
220
|
+
self.assertEqual(schema["name"], "know.fast")
|
|
221
|
+
self.assertEqual(schema["badge"], "paid")
|
|
222
|
+
self.assertEqual(schema["env_vars"][0]["key"], "KNOW_FAST_API_KEY")
|
|
223
|
+
|
|
224
|
+
def test_extract_maps_packet_to_content(self):
|
|
225
|
+
p = KnowFastProvider()
|
|
226
|
+
with mock.patch.dict(os.environ, {"KNOW_FAST_API_KEY": "rk_test"}):
|
|
227
|
+
with mock.patch.object(client, "request", return_value=(200, dict(CACHED_PAYLOAD))):
|
|
228
|
+
results = p.extract(["https://example.com/a"])
|
|
229
|
+
entry = results[0]
|
|
230
|
+
self.assertEqual(entry["content"], CACHED_PAYLOAD["packet"])
|
|
231
|
+
self.assertEqual(entry["raw_content"], CACHED_PAYLOAD["packet"])
|
|
232
|
+
self.assertEqual(entry["metadata"]["viewerUrl"], CACHED_PAYLOAD["viewerUrl"])
|
|
233
|
+
self.assertEqual(entry["metadata"]["costUsd"], 0.01)
|
|
234
|
+
self.assertNotIn("error", entry)
|
|
235
|
+
|
|
236
|
+
def test_extract_per_url_errors(self):
|
|
237
|
+
p = KnowFastProvider()
|
|
238
|
+
|
|
239
|
+
def fake_request(method, path, body=None, **kw):
|
|
240
|
+
if body and body.get("url") == "https://bad.example":
|
|
241
|
+
raise client.KnowFastError("could not fetch", status=422)
|
|
242
|
+
return 200, dict(CACHED_PAYLOAD)
|
|
243
|
+
|
|
244
|
+
with mock.patch.dict(os.environ, {"KNOW_FAST_API_KEY": "rk_test"}):
|
|
245
|
+
with mock.patch.object(client, "request", side_effect=fake_request):
|
|
246
|
+
results = p.extract(["https://bad.example", "https://example.com/a"])
|
|
247
|
+
self.assertEqual(results[0]["error"], "could not fetch")
|
|
248
|
+
self.assertEqual(results[0]["content"], "")
|
|
249
|
+
self.assertNotIn("error", results[1])
|
|
250
|
+
|
|
251
|
+
def test_extract_without_key_returns_failure_dict(self):
|
|
252
|
+
p = KnowFastProvider()
|
|
253
|
+
with mock.patch.dict(os.environ, {}, clear=True):
|
|
254
|
+
result = p.extract(["https://example.com"])
|
|
255
|
+
self.assertEqual(result["success"], False)
|
|
256
|
+
self.assertIn("KNOW_FAST_API_KEY", result["error"])
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
class ClientTest(unittest.TestCase):
|
|
260
|
+
def test_key_fallback_to_researcher(self):
|
|
261
|
+
with mock.patch.dict(os.environ, {"RESEARCHER_API_KEY": "rk_r"}, clear=True):
|
|
262
|
+
self.assertEqual(client.api_key(), "rk_r")
|
|
263
|
+
with mock.patch.dict(
|
|
264
|
+
os.environ, {"RESEARCHER_API_KEY": "rk_r", "KNOW_FAST_API_KEY": "rk_k"}, clear=True
|
|
265
|
+
):
|
|
266
|
+
self.assertEqual(client.api_key(), "rk_k")
|
|
267
|
+
|
|
268
|
+
def test_base_url_override(self):
|
|
269
|
+
with mock.patch.dict(os.environ, {"KNOW_FAST_API_URL": "http://localhost:4100/"}, clear=True):
|
|
270
|
+
self.assertEqual(client.base_url(), "http://localhost:4100")
|
|
271
|
+
with mock.patch.dict(os.environ, {}, clear=True):
|
|
272
|
+
self.assertEqual(client.base_url(), "https://know.fast")
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
class RegisterTest(unittest.TestCase):
|
|
276
|
+
def test_register_without_hermes_logs_and_returns(self):
|
|
277
|
+
ctx = mock.Mock()
|
|
278
|
+
with mock.patch("know_fast.provider.HERMES_AVAILABLE", False):
|
|
279
|
+
with self.assertLogs("know_fast", level="WARNING"):
|
|
280
|
+
know_fast.register(ctx)
|
|
281
|
+
ctx.register_web_search_provider.assert_not_called()
|
|
282
|
+
ctx.register_tool.assert_not_called()
|
|
283
|
+
|
|
284
|
+
def test_register_with_hermes_registers_provider_and_tools(self):
|
|
285
|
+
ctx = mock.Mock()
|
|
286
|
+
with mock.patch("know_fast.provider.HERMES_AVAILABLE", True):
|
|
287
|
+
know_fast.register(ctx)
|
|
288
|
+
ctx.register_web_search_provider.assert_called_once()
|
|
289
|
+
provider = ctx.register_web_search_provider.call_args.args[0]
|
|
290
|
+
self.assertIsInstance(provider, KnowFastProvider)
|
|
291
|
+
names = [c.args[0] for c in ctx.register_tool.call_args_list]
|
|
292
|
+
self.assertEqual(names, ["know", "know_status"])
|
|
293
|
+
for call in ctx.register_tool.call_args_list:
|
|
294
|
+
name, toolset, schema, handler = call.args
|
|
295
|
+
self.assertEqual(toolset, "know-fast")
|
|
296
|
+
self.assertEqual(schema["name"], name)
|
|
297
|
+
self.assertEqual(schema["parameters"]["type"], "object")
|
|
298
|
+
self.assertTrue(callable(handler))
|
|
299
|
+
self.assertFalse(call.kwargs["is_async"])
|
|
300
|
+
self.assertIn("viewerUrl", call.kwargs["description"])
|
|
301
|
+
self.assertTrue(call.kwargs["emoji"])
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
if __name__ == "__main__":
|
|
305
|
+
unittest.main()
|