@allansantos-dev/smart-tool 0.9.7 → 0.9.9
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.
- package/CHANGELOG.md +21 -0
- package/NOTICE +1 -0
- package/README.md +1 -1
- package/install.py +17 -0
- package/install_runtime.py +81 -32
- package/package.json +1 -1
- package/smart_tool_daemon.py +39 -1
- package/version.py +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.9.9 - beta
|
|
4
|
+
|
|
5
|
+
- `web_fetch` renders JavaScript pages with [Moli](https://github.com/lexmount/moli) first (a headless browser built
|
|
6
|
+
for agents: layout and paint only when a page needs them; 43 MB download, SHA-256 checked) and starts Chromium
|
|
7
|
+
through Crawl4AI only when Moli fails or comes back thin. On 21 pages HTTP could not read: Moli 1.6 s and 90 MB
|
|
8
|
+
peak (median) against 4.2 s and 581 MB; Moli read 14, Chromium 16, both together 17 (only Chromium read YouTube,
|
|
9
|
+
Google Maps and Airbnb). With several agent sessions reading at once, measured on N simultaneous rendered reads:
|
|
10
|
+
aggregate peak memory 448 → 35 MB (1), 1,788 → 194 MB (3), 2,901 → 634 MB (5), 4.4-4.7 GB → 1.0-1.4 GB (10), and
|
|
11
|
+
the median read time halved (10 at once: 5.0 s → 2.5 s). Pages only Chromium reads now take Moli's 1-4 s more.
|
|
12
|
+
The `web_fetch` metric records which engine read the page and Moli's error when it fell back.
|
|
13
|
+
|
|
14
|
+
## 0.9.8 - beta
|
|
15
|
+
|
|
16
|
+
- An update could keep running the previous code: npm dates every file in the package 1985-10-26, the installer kept
|
|
17
|
+
that date when copying, and Python reuses a compiled `.pyc` when the source's date and size match, so a file changed
|
|
18
|
+
without changing size (a version bump, a one-character fix) went on running the old bytecode. Reproduced on a real
|
|
19
|
+
reinstall: `version.py` said 0.9.6 and the daemon served 0.9.8. Copied and restored files now get the current time,
|
|
20
|
+
and the program's `__pycache__` folders are cleared after copying and after a rollback (two copies within the same
|
|
21
|
+
second keep the same date). The update to this version already clears any stale bytecode left by earlier ones.
|
|
22
|
+
Reported by the agent-launcher session, which had the same bug.
|
|
23
|
+
|
|
3
24
|
## 0.9.7 - beta
|
|
4
25
|
|
|
5
26
|
The hook decides code searches with a fixed rule instead of the router model.
|
package/NOTICE
CHANGED
|
@@ -15,6 +15,7 @@ Python packages (installation venv, from PyPI)
|
|
|
15
15
|
|
|
16
16
|
Browser runtime (web_adapters/browser/.venv, from PyPI and the projects' download servers)
|
|
17
17
|
- camoufox Python package (MIT) and the Camoufox browser, a Firefox fork (MPL-2.0), fetched by `camoufox fetch`
|
|
18
|
+
- Moli (Apache-2.0 OR MIT), https://github.com/lexmount/moli: renders JavaScript pages for web_fetch; downloaded at install time from its GitHub release (SHA-256 checked), with its own license files
|
|
18
19
|
- crawl4ai (Apache-2.0), https://github.com/unclecode/crawl4ai: page rendering is powered by Crawl4AI
|
|
19
20
|
- Playwright (Apache-2.0) and the Chromium build it installs (BSD-style, see https://www.chromium.org)
|
|
20
21
|
|
package/README.md
CHANGED
|
@@ -146,4 +146,4 @@ user` / `codex mcp remove smart-tool`, then delete `%LOCALAPPDATA%\Programs\Smar
|
|
|
146
146
|
|
|
147
147
|
Apache License 2.0. See `LICENSE` and `NOTICE` for third-party components.
|
|
148
148
|
|
|
149
|
-
Page rendering is powered by [Crawl4AI](https://github.com/unclecode/crawl4ai) (Apache-2.0).
|
|
149
|
+
Page rendering is powered by [Moli](https://github.com/lexmount/moli) (Apache-2.0 / MIT), with [Crawl4AI](https://github.com/unclecode/crawl4ai) (Apache-2.0) as its fallback.
|
package/install.py
CHANGED
|
@@ -197,6 +197,7 @@ def copy_files(target, files, backup, changed, removed):
|
|
|
197
197
|
changed.append(rel)
|
|
198
198
|
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
199
199
|
shutil.copy2(source, dest)
|
|
200
|
+
os.utime(dest)
|
|
200
201
|
for rel in sorted(previous - set(files)):
|
|
201
202
|
if (target / rel).is_file():
|
|
202
203
|
(backup / rel).parent.mkdir(parents=True, exist_ok=True)
|
|
@@ -210,16 +211,31 @@ def copy_files(target, files, backup, changed, removed):
|
|
|
210
211
|
indent=1), encoding="utf-8")
|
|
211
212
|
|
|
212
213
|
|
|
214
|
+
def clear_bytecode(target):
|
|
215
|
+
"""Deletes the program's __pycache__ folders (the virtual environment's are left alone). npm writes the same
|
|
216
|
+
mtime (1985-10-26) into every tarball entry, and Python's bytecode check compares only the source's mtime and
|
|
217
|
+
size: a file changed without changing size kept running the old code after an update (reproduced on 2026-10-10:
|
|
218
|
+
version.py said 0.9.6, the daemon served 0.9.8)."""
|
|
219
|
+
for current, dirs, _files in os.walk(target):
|
|
220
|
+
dirs[:] = [d for d in dirs if d != ".venv"]
|
|
221
|
+
if "__pycache__" in dirs:
|
|
222
|
+
shutil.rmtree(Path(current) / "__pycache__", ignore_errors=True)
|
|
223
|
+
dirs.remove("__pycache__")
|
|
224
|
+
|
|
225
|
+
|
|
213
226
|
def restore(target, backup, changed, removed):
|
|
214
227
|
for rel in changed:
|
|
215
228
|
if (backup / rel).is_file():
|
|
216
229
|
shutil.copy2(backup / rel, target / rel)
|
|
230
|
+
os.utime(target / rel)
|
|
217
231
|
else:
|
|
218
232
|
(target / rel).unlink(missing_ok=True)
|
|
219
233
|
for rel in removed:
|
|
220
234
|
if (backup / rel).is_file():
|
|
221
235
|
(target / rel).parent.mkdir(parents=True, exist_ok=True)
|
|
222
236
|
shutil.move(backup / rel, target / rel)
|
|
237
|
+
os.utime(target / rel)
|
|
238
|
+
clear_bytecode(target)
|
|
223
239
|
if (backup / MANIFEST).is_file():
|
|
224
240
|
shutil.copy2(backup / MANIFEST, target / MANIFEST)
|
|
225
241
|
else:
|
|
@@ -319,6 +335,7 @@ def main():
|
|
|
319
335
|
changed, removed, frozen, deps_touched = [], [], None, False
|
|
320
336
|
try:
|
|
321
337
|
copy_files(target, files, backup, changed, removed)
|
|
338
|
+
clear_bytecode(target)
|
|
322
339
|
print(f"Files changed: {len(changed)}; removed as obsolete: {len(removed)}")
|
|
323
340
|
if not args.skip_deps:
|
|
324
341
|
frozen = freeze(python)
|
package/install_runtime.py
CHANGED
|
@@ -31,6 +31,14 @@ TYPESCRIPT_PARSER = NODE_DIR / "node_modules" / "typescript" / "lib" / "typescri
|
|
|
31
31
|
JAVA_PARSER = NODE_DIR / 'node_modules' / 'java-parser' / 'src' / 'index.js'
|
|
32
32
|
ANGULAR_PARSER = NODE_DIR / 'node_modules' / '@angular' / 'compiler' / 'fesm2022' / 'compiler.mjs'
|
|
33
33
|
NODE_RUNTIME = NODE_DIR / ".node-runtime"
|
|
34
|
+
# Moli renders the JavaScript pages web_fetch cannot read over HTTP; Chromium (Crawl4AI) stays as its fallback.
|
|
35
|
+
# Measured on 2026-10-10 over 21 such pages: 1.6 s and 90 MB peak (median) against 4.2 s and 581 MB for Chromium.
|
|
36
|
+
MOLI_VERSION = "1.1.15"
|
|
37
|
+
MOLI_FOLDER = f"moli-v{MOLI_VERSION}-x86_64-pc-windows-msvc"
|
|
38
|
+
MOLI_URL = f"https://github.com/lexmount/moli/releases/download/v{MOLI_VERSION}/moli-x86_64-pc-windows-msvc.zip"
|
|
39
|
+
MOLI_SHA256 = "64c00ce02d6db8e1c22c55e88fbcaaf3e7517e930f24c25ff46a457b421040c8"
|
|
40
|
+
MOLI_DIR = BROWSER_DIR / "moli"
|
|
41
|
+
MOLI_EXE = MOLI_DIR / "moli.exe"
|
|
34
42
|
|
|
35
43
|
|
|
36
44
|
def _node_supported(node):
|
|
@@ -58,6 +66,49 @@ def _node_commands():
|
|
|
58
66
|
return (node, npm) if node and npm and _node_supported(node) else (None, None)
|
|
59
67
|
|
|
60
68
|
|
|
69
|
+
def _download_verified(url, expected_sha256, path):
|
|
70
|
+
"""Downloads url to path and fails when its SHA-256 differs from the published one."""
|
|
71
|
+
digest = hashlib.sha256()
|
|
72
|
+
with urllib.request.urlopen(url, timeout=60) as response, path.open("wb") as out:
|
|
73
|
+
while block := response.read(1024 * 1024):
|
|
74
|
+
digest.update(block)
|
|
75
|
+
out.write(block)
|
|
76
|
+
if digest.hexdigest().lower() != expected_sha256.lower():
|
|
77
|
+
raise RuntimeError(f"SHA-256 of {url} does not match the published checksum.")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _extract_folder(zip_path, top, destination):
|
|
81
|
+
"""Extracts the files under the archive's single top folder into destination, refusing any other path."""
|
|
82
|
+
destination.mkdir()
|
|
83
|
+
with zipfile.ZipFile(zip_path) as zf:
|
|
84
|
+
for member in zf.infolist():
|
|
85
|
+
parts = Path(member.filename.replace("\\", "/")).parts
|
|
86
|
+
if parts == (top,) and member.is_dir():
|
|
87
|
+
continue
|
|
88
|
+
if len(parts) < 2 or parts[0] != top or ".." in parts:
|
|
89
|
+
raise RuntimeError(f"{zip_path.name} contains an unexpected path: {member.filename}")
|
|
90
|
+
target = destination.joinpath(*parts[1:])
|
|
91
|
+
if member.is_dir():
|
|
92
|
+
target.mkdir(parents=True, exist_ok=True)
|
|
93
|
+
else:
|
|
94
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
95
|
+
with zf.open(member) as source, target.open("wb") as out:
|
|
96
|
+
shutil.copyfileobj(source, out)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _replace_folder(unpacked, final, temp_dir):
|
|
100
|
+
"""Swaps final for unpacked, putting the previous folder back if the swap fails."""
|
|
101
|
+
previous = temp_dir / "previous"
|
|
102
|
+
if final.exists():
|
|
103
|
+
os.replace(final, previous)
|
|
104
|
+
try:
|
|
105
|
+
os.replace(unpacked, final)
|
|
106
|
+
except OSError:
|
|
107
|
+
if previous.exists() and not final.exists():
|
|
108
|
+
os.replace(previous, final)
|
|
109
|
+
raise
|
|
110
|
+
|
|
111
|
+
|
|
61
112
|
def _install_node():
|
|
62
113
|
"""Baixa o Node LTS oficial e confere SHA-256 antes de extrair."""
|
|
63
114
|
print("Downloading Node LTS for the Smart Tool web runtime...", flush=True)
|
|
@@ -79,43 +130,38 @@ def _install_node():
|
|
|
79
130
|
with tempfile.TemporaryDirectory(prefix="node-setup-", dir=NODE_DIR) as temp:
|
|
80
131
|
temp_dir = Path(temp)
|
|
81
132
|
zip_path = temp_dir / archive
|
|
82
|
-
|
|
83
|
-
with urllib.request.urlopen(base + archive, timeout=60) as response, zip_path.open("wb") as out:
|
|
84
|
-
while block := response.read(1024 * 1024):
|
|
85
|
-
digest.update(block)
|
|
86
|
-
out.write(block)
|
|
87
|
-
if digest.hexdigest().lower() != expected.lower():
|
|
88
|
-
raise RuntimeError("Node SHA-256 checksum does not match the official index.")
|
|
133
|
+
_download_verified(base + archive, expected, zip_path)
|
|
89
134
|
unpacked = temp_dir / "runtime"
|
|
90
|
-
unpacked
|
|
91
|
-
with zipfile.ZipFile(zip_path) as zf:
|
|
92
|
-
for member in zf.infolist():
|
|
93
|
-
parts = Path(member.filename.replace("\\", "/")).parts
|
|
94
|
-
if parts == (f"node-{version}-win-x64",) and member.is_dir():
|
|
95
|
-
continue
|
|
96
|
-
if len(parts) < 2 or parts[0] != f"node-{version}-win-x64" or ".." in parts:
|
|
97
|
-
raise RuntimeError("Node archive contains an unexpected path.")
|
|
98
|
-
destination = unpacked.joinpath(*parts[1:])
|
|
99
|
-
if member.is_dir():
|
|
100
|
-
destination.mkdir(parents=True, exist_ok=True)
|
|
101
|
-
else:
|
|
102
|
-
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
103
|
-
with zf.open(member) as source, destination.open("wb") as target:
|
|
104
|
-
shutil.copyfileobj(source, target)
|
|
135
|
+
_extract_folder(zip_path, f"node-{version}-win-x64", unpacked)
|
|
105
136
|
if not (unpacked / "node.exe").is_file() or not (unpacked / "npm.cmd").is_file():
|
|
106
137
|
raise RuntimeError("Node package does not contain the expected executables.")
|
|
107
|
-
|
|
108
|
-
if NODE_RUNTIME.exists():
|
|
109
|
-
os.replace(NODE_RUNTIME, previous)
|
|
110
|
-
try:
|
|
111
|
-
os.replace(unpacked, NODE_RUNTIME)
|
|
112
|
-
except OSError:
|
|
113
|
-
if previous.exists() and not NODE_RUNTIME.exists():
|
|
114
|
-
os.replace(previous, NODE_RUNTIME)
|
|
115
|
-
raise
|
|
138
|
+
_replace_folder(unpacked, NODE_RUNTIME, temp_dir)
|
|
116
139
|
print(f"Node {version} installed inside Smart Tool.")
|
|
117
140
|
|
|
118
141
|
|
|
142
|
+
def _moli_ready():
|
|
143
|
+
try:
|
|
144
|
+
return MOLI_EXE.is_file() and (MOLI_DIR / "VERSION").read_text(encoding="utf-8").strip() == MOLI_VERSION
|
|
145
|
+
except OSError:
|
|
146
|
+
return False
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _install_moli():
|
|
150
|
+
"""Downloads the pinned Moli release from GitHub, checks its SHA-256 and unpacks it into web_adapters/browser."""
|
|
151
|
+
print(f"Downloading Moli {MOLI_VERSION} (structured-first headless browser, 43 MB)...", flush=True)
|
|
152
|
+
BROWSER_DIR.mkdir(parents=True, exist_ok=True)
|
|
153
|
+
with tempfile.TemporaryDirectory(prefix="moli-setup-", dir=BROWSER_DIR) as temp:
|
|
154
|
+
temp_dir = Path(temp)
|
|
155
|
+
zip_path = temp_dir / "moli.zip"
|
|
156
|
+
_download_verified(MOLI_URL, MOLI_SHA256, zip_path)
|
|
157
|
+
unpacked = temp_dir / "moli"
|
|
158
|
+
_extract_folder(zip_path, MOLI_FOLDER, unpacked)
|
|
159
|
+
if not (unpacked / "moli.exe").is_file():
|
|
160
|
+
raise RuntimeError("The Moli package does not contain moli.exe.")
|
|
161
|
+
_replace_folder(unpacked, MOLI_DIR, temp_dir)
|
|
162
|
+
print(f"Moli {MOLI_VERSION} installed inside Smart Tool.")
|
|
163
|
+
|
|
164
|
+
|
|
119
165
|
def _run(args, *, cwd=None, label="Web dependency", idle_s=600, stream=False, attempts=1):
|
|
120
166
|
"""Runs a step with UTF-8 output and closed stdin (a prompt fails at once instead of hanging); it is stopped only
|
|
121
167
|
after idle_s without any output, and stream shows the output live."""
|
|
@@ -197,6 +243,7 @@ def _check():
|
|
|
197
243
|
"browser_packages": False,
|
|
198
244
|
"camoufox_browser": False,
|
|
199
245
|
"chromium_browser": False,
|
|
246
|
+
"moli_browser": _moli_ready(),
|
|
200
247
|
}
|
|
201
248
|
if status["browser_python"]:
|
|
202
249
|
try:
|
|
@@ -286,10 +333,12 @@ def install(reuse_browser_runtime=None):
|
|
|
286
333
|
idle_s=180, stream=True, attempts=3)
|
|
287
334
|
_run([str(BROWSER_PYTHON), "-m", "playwright", "install", "chromium"],
|
|
288
335
|
label="Chromium browser", idle_s=180, stream=True, attempts=3)
|
|
336
|
+
if not _moli_ready():
|
|
337
|
+
_install_moli()
|
|
289
338
|
status = _check()
|
|
290
339
|
if not all(status.values()):
|
|
291
340
|
raise RuntimeError(f"Incomplete web runtime: {status}")
|
|
292
|
-
print("Smart Tool runtime installed: Node, open-websearch, TypeScript, Java, Angular, Camoufox and
|
|
341
|
+
print("Smart Tool runtime installed: Node, open-websearch, TypeScript, Java, Angular, Camoufox, Crawl4AI and Moli.")
|
|
293
342
|
|
|
294
343
|
|
|
295
344
|
def main():
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@allansantos-dev/smart-tool",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.9",
|
|
4
4
|
"description": "Local MCP server that gives coding agents (Claude Code, Codex) cheaper, sharper tools than their built-in search. Windows.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Allan Santos",
|
package/smart_tool_daemon.py
CHANGED
|
@@ -37,6 +37,7 @@ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
|
37
37
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
38
38
|
import capabilities
|
|
39
39
|
import client_hooks
|
|
40
|
+
import install_runtime
|
|
40
41
|
import integration_metrics
|
|
41
42
|
import config
|
|
42
43
|
import freshness
|
|
@@ -3187,17 +3188,52 @@ def _tool_result(text, is_error=False):
|
|
|
3187
3188
|
|
|
3188
3189
|
WEB_FETCH_TIMEOUT_S = 90
|
|
3189
3190
|
WEB_FETCH_RENDER_TIMEOUT_S = 60
|
|
3191
|
+
MOLI_RENDER_TIMEOUT_S = 25
|
|
3192
|
+
# Engine of the last render on this request's thread: web_fetch.read_page calls _render_page on the handler's thread.
|
|
3193
|
+
_RENDER = threading.local()
|
|
3190
3194
|
WEB_FETCH_VECTOR_ROOT = os.path.join(paths.DATA_DIR, "web-fetch")
|
|
3191
3195
|
|
|
3192
3196
|
|
|
3197
|
+
def _moli_markdown(url, timeout):
|
|
3198
|
+
"""Markdown of a JavaScript page rendered by Moli (layout and paint only when the page needs them)."""
|
|
3199
|
+
if not install_runtime.MOLI_EXE.is_file():
|
|
3200
|
+
raise RuntimeError(f"Moli is not installed ({install_runtime.MOLI_EXE}); reinstall Smart Tool.")
|
|
3201
|
+
done = subprocess.run([str(install_runtime.MOLI_EXE), "fetch", "--dump", "markdown", url], capture_output=True,
|
|
3202
|
+
timeout=timeout, creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0))
|
|
3203
|
+
if done.returncode:
|
|
3204
|
+
raise RuntimeError(f"moli exited {done.returncode}: {done.stderr.decode('utf-8', 'replace').strip()[-300:]}")
|
|
3205
|
+
return done.stdout.decode("utf-8", "replace")
|
|
3206
|
+
|
|
3207
|
+
|
|
3193
3208
|
def _render_page(url, deadline):
|
|
3209
|
+
"""Moli first; Chromium through Crawl4AI only when Moli fails or comes back thin. Measured on 2026-10-10 over 21
|
|
3210
|
+
pages HTTP could not read: Moli 1.6 s and 90 MB peak (median), Chromium 4.2 s and 581 MB; Moli read 14, Chromium
|
|
3211
|
+
16, both together 17 (only Chromium read YouTube, Google Maps and Airbnb). The engine that answered goes to the
|
|
3212
|
+
web_fetch metric through _RENDER."""
|
|
3194
3213
|
left = deadline - time.monotonic()
|
|
3195
3214
|
if left < 5:
|
|
3196
3215
|
return ""
|
|
3216
|
+
_RENDER.engine, _RENDER.moli_error = None, None
|
|
3217
|
+
try:
|
|
3218
|
+
markdown = _moli_markdown(url, min(MOLI_RENDER_TIMEOUT_S, left - 2))
|
|
3219
|
+
except (OSError, RuntimeError, subprocess.TimeoutExpired) as exc:
|
|
3220
|
+
markdown, _RENDER.moli_error = "", f"{type(exc).__name__}: {exc}"[:200]
|
|
3221
|
+
_log(f"web_fetch Moli render failed, trying Chromium: {_RENDER.moli_error}")
|
|
3222
|
+
if not web_fetch.thin(markdown):
|
|
3223
|
+
_RENDER.engine = "moli"
|
|
3224
|
+
return markdown
|
|
3225
|
+
left = deadline - time.monotonic()
|
|
3226
|
+
if left < 5:
|
|
3227
|
+
return markdown
|
|
3197
3228
|
raw = _run_search_script(CAMOUFOX_VENV_PYTHON, CRAWL4AI_SEARCH_SCRIPT, [url, "--max-pages", "1"],
|
|
3198
3229
|
min(WEB_FETCH_RENDER_TIMEOUT_S, left))
|
|
3199
3230
|
pages = raw.get("pages") if isinstance(raw, dict) else None
|
|
3200
|
-
|
|
3231
|
+
chromium = (pages[0].get("markdown") or "") if pages else ""
|
|
3232
|
+
if len(chromium.strip()) > len(markdown.strip()):
|
|
3233
|
+
_RENDER.engine = "chromium"
|
|
3234
|
+
return chromium
|
|
3235
|
+
_RENDER.engine = "moli"
|
|
3236
|
+
return markdown
|
|
3201
3237
|
|
|
3202
3238
|
|
|
3203
3239
|
def _web_fetch_embed(texts, deadline):
|
|
@@ -3243,6 +3279,7 @@ def _handle_web_fetch(arguments):
|
|
|
3243
3279
|
if problem:
|
|
3244
3280
|
raise RuntimeError(f"Gateway model unavailable ({problem}).")
|
|
3245
3281
|
deadline = started + WEB_FETCH_TIMEOUT_S
|
|
3282
|
+
_RENDER.engine, _RENDER.moli_error = None, None
|
|
3246
3283
|
page = web_fetch.read_page(url, deadline, _render_page)
|
|
3247
3284
|
selected, pieces = web_fetch.select(page["text"], prompt, lambda texts: _web_fetch_embed(texts, deadline))
|
|
3248
3285
|
text = web_fetch.answer(config.load_config()["router_model"], prompt, {**page, "text": selected}, deadline)
|
|
@@ -3252,6 +3289,7 @@ def _handle_web_fetch(arguments):
|
|
|
3252
3289
|
raise RuntimeError(f"web_fetch failed: {str(exc).rstrip('.')}. The native WebFetch is allowed for this URL for "
|
|
3253
3290
|
f"{web_fetch.FAILED_TTL_S // 60} min.") from None
|
|
3254
3291
|
_metric("web_fetch", cached=page["cached"], rendered=page["rendered"], render_error=page.get("render_error"),
|
|
3292
|
+
render_engine=_RENDER.engine if page["rendered"] else None, moli_error=_RENDER.moli_error,
|
|
3255
3293
|
page_chars=page["chars"], read_chars=len(selected), pieces=pieces,
|
|
3256
3294
|
answer_chars=len(text), elapsed_s=round(time.monotonic() - started, 2))
|
|
3257
3295
|
origin = "from cache" if page["cached"] else "rendered in the browser" if page["rendered"] else "downloaded now"
|
package/version.py
CHANGED