ask-web-fetch 0.5.0 → 0.5.1
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.
- checksums.yaml +4 -4
- data/README.md +18 -3
- data/lib/ask/web_fetch/backend.rb +6 -1
- data/lib/ask/web_fetch/backends/crawl4ai.rb +5 -0
- data/lib/ask/web_fetch/backends/jina.rb +9 -3
- data/lib/ask/web_fetch/markdown.rb +6 -0
- data/lib/ask/web_fetch/noise_filter.rb +143 -0
- data/lib/ask/web_fetch/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0f49a52ecb57c57ee29ce475ad791f70d30fe52946c94c6bd6842dcccc477392
|
|
4
|
+
data.tar.gz: b67e97adb54db5b6a775d1ece43df51c0e5443a6ef9e2cd3a17d547af5f7d1dc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 28dd03ea3e7aae930925ea2d582ff074beba7d3030d762830024e41506a23fd671bb166fa5809417a824e8d2c1729679d00f3d710fa82de05b2ca41038dd39de
|
|
7
|
+
data.tar.gz: 84815a2d6504c81df9d81aab2fd679d5bb334f64893b05264cc4185be0e7d7439f357461494c65811cbdb16e0e4548b0b2db290f52b747412678eb953c5f40b3
|
data/README.md
CHANGED
|
@@ -26,6 +26,14 @@ first success:
|
|
|
26
26
|
headless Chromium, so it renders JS pages the Local backend can't. Free
|
|
27
27
|
without a key (~20 req/min per IP); set `JINA_API_KEY` for higher limits.
|
|
28
28
|
|
|
29
|
+
Every backend's markdown runs through a shared cleanup (`Markdown.clean`):
|
|
30
|
+
decorative symbol noise — the long, letter-free, repetitive character
|
|
31
|
+
streams some pages render as animated backgrounds or section dividers — is
|
|
32
|
+
stripped, and whitespace is normalized. The filter is conservative: code
|
|
33
|
+
blocks, tables, headings, blockquotes, inline code, and short ASCII-art
|
|
34
|
+
fragments always survive. Tunable via `Ask::WebFetch::NoiseFilter.filter(
|
|
35
|
+
markdown, min_length:, max_entropy:)`.
|
|
36
|
+
|
|
29
37
|
The tool falls back automatically: if Crawl4AI is absent or fails, Local is
|
|
30
38
|
tried (blocked, timeout, non-HTML, anti-bot challenge, or a JS page with no
|
|
31
39
|
server-side content), then Jina. If every backend fails (rate limit, access
|
|
@@ -68,9 +76,13 @@ Ask::Tools::WebFetch.backends = [MyBackend, Ask::WebFetch::Backends::Local]
|
|
|
68
76
|
|
|
69
77
|
`#fetch` must return `{ title: String|nil, content: String }` and raise
|
|
70
78
|
`Ask::WebFetch::FetchError` (hard failure) or `EmptyContentError` (page
|
|
71
|
-
fetched but nothing usable).
|
|
72
|
-
|
|
73
|
-
|
|
79
|
+
fetched but nothing usable). Run the returned markdown through
|
|
80
|
+
`Ask::WebFetch::Markdown.clean` (backends that convert HTML get this from
|
|
81
|
+
`Markdown.generate`; backends fed pre-converted markdown must call it
|
|
82
|
+
explicitly) so the shared noise removal and whitespace normalization apply
|
|
83
|
+
everywhere. The chain then handles ordering and fallback for you. For
|
|
84
|
+
tests, `Ask::Tools::WebFetch.backends = [...]` can be swapped and
|
|
85
|
+
restored.
|
|
74
86
|
|
|
75
87
|
## Installation
|
|
76
88
|
|
|
@@ -125,6 +137,9 @@ No configuration required for the default chain. Optional knobs:
|
|
|
125
137
|
no content unless Crawl4AI is configured — set `CRAWL4AI_URL` to handle
|
|
126
138
|
them with a self-hosted renderer.
|
|
127
139
|
- Some sites block non-browser requests regardless of User-Agent.
|
|
140
|
+
- Symbol streams that a converter merges *into* a content line (rather
|
|
141
|
+
than leaving them as their own lines) are out of scope for the
|
|
142
|
+
markdown-level NoiseFilter — that would need a DOM-level pass.
|
|
128
143
|
|
|
129
144
|
## Full documentation
|
|
130
145
|
|
|
@@ -32,7 +32,12 @@ module Ask
|
|
|
32
32
|
# 2. #fetch must return { title: String|nil, content: String }
|
|
33
33
|
# 3. #fetch must raise FetchError (hard failure) or
|
|
34
34
|
# EmptyContentError (page fetched but nothing usable) on failure
|
|
35
|
-
# 4.
|
|
35
|
+
# 4. run the page's markdown through Ask::WebFetch::Markdown.clean
|
|
36
|
+
# before returning it — backends that hold HTML get this from
|
|
37
|
+
# Markdown.generate, backends fed pre-converted markdown (Jina,
|
|
38
|
+
# Crawl4AI) must call it explicitly so the shared noise removal
|
|
39
|
+
# and whitespace normalization apply everywhere
|
|
40
|
+
# 5. register the class in Ask::Tools::WebFetch.backends
|
|
36
41
|
#
|
|
37
42
|
# The tool tries each backend in order and returns the first success.
|
|
38
43
|
class Backend
|
|
@@ -4,6 +4,7 @@ require 'net/http'
|
|
|
4
4
|
require 'uri'
|
|
5
5
|
require 'json'
|
|
6
6
|
require_relative '../backend'
|
|
7
|
+
require_relative '../markdown'
|
|
7
8
|
|
|
8
9
|
module Ask
|
|
9
10
|
module WebFetch
|
|
@@ -115,6 +116,10 @@ module Ask
|
|
|
115
116
|
|
|
116
117
|
markdown = result.dig('markdown', 'fit_markdown').to_s
|
|
117
118
|
markdown = result.dig('markdown', 'raw_markdown').to_s if markdown.strip.empty?
|
|
119
|
+
# The service's markdown bypasses Markdown.generate, so it runs
|
|
120
|
+
# through the same shared clean (noise removal + whitespace) as
|
|
121
|
+
# the converting backends.
|
|
122
|
+
markdown = Markdown.clean(markdown)
|
|
118
123
|
{
|
|
119
124
|
title: result.dig('metadata', 'title'),
|
|
120
125
|
description: result.dig('metadata', 'description') ||
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require 'net/http'
|
|
4
4
|
require 'uri'
|
|
5
5
|
require_relative '../backend'
|
|
6
|
+
require_relative '../markdown'
|
|
6
7
|
|
|
7
8
|
module Ask
|
|
8
9
|
module WebFetch
|
|
@@ -33,11 +34,16 @@ module Ask
|
|
|
33
34
|
when '200'
|
|
34
35
|
body = res.body.to_s
|
|
35
36
|
raise FetchError, 'challenge page from Jina' if challenge_page?(body)
|
|
36
|
-
raise EmptyContentError, 'empty response from Jina' unless usable_content?(body)
|
|
37
37
|
|
|
38
38
|
# Jina only sees rendered markdown — outlinks come from its
|
|
39
|
-
# links, resolved against the requested URL.
|
|
40
|
-
|
|
39
|
+
# links, resolved against the requested URL. Content runs
|
|
40
|
+
# through the same Markdown.clean as the converting backends,
|
|
41
|
+
# so decorative symbol noise is stripped here too; a page
|
|
42
|
+
# whose only "content" was noise falls through as empty.
|
|
43
|
+
content = Markdown.clean(body)
|
|
44
|
+
raise EmptyContentError, 'empty response from Jina' unless usable_content?(content)
|
|
45
|
+
|
|
46
|
+
{ title: nil, description: nil, content: content, outlinks: markdown_outlinks(body, url) }
|
|
41
47
|
when '429'
|
|
42
48
|
raise ServerError, 'rate limited by Jina (429)'
|
|
43
49
|
when '401', '403'
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require 'nokogiri'
|
|
4
4
|
require 'reverse_markdown'
|
|
5
5
|
require 'uri'
|
|
6
|
+
require_relative 'noise_filter'
|
|
6
7
|
|
|
7
8
|
module Ask
|
|
8
9
|
module WebFetch
|
|
@@ -126,7 +127,12 @@ module Ask
|
|
|
126
127
|
[parts.join, references.join]
|
|
127
128
|
end
|
|
128
129
|
|
|
130
|
+
# Shared post-conversion cleanup: drop decorative symbol noise
|
|
131
|
+
# (NoiseFilter), then normalize whitespace. Every backend's content
|
|
132
|
+
# runs through this — Local and Browser via generate, Jina and
|
|
133
|
+
# Crawl4AI explicitly — so the same noise rules apply to all.
|
|
129
134
|
def clean(markdown)
|
|
135
|
+
markdown = NoiseFilter.filter(markdown)
|
|
130
136
|
markdown.gsub(/[ \t]+\n/, "\n")
|
|
131
137
|
.gsub(/\n{3,}/, "\n\n")
|
|
132
138
|
.strip
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ask
|
|
4
|
+
module WebFetch
|
|
5
|
+
# Strips decorative symbol noise from markdown: the long, letter-free,
|
|
6
|
+
# repetitive character streams pages render as animated backgrounds,
|
|
7
|
+
# marquees and section dividers — e.g. Hugging Face's storage page
|
|
8
|
+
# ships a "+ = · ( ~ @ # % & * ? / : ; < > [ ] { } | ^ $ !" stream as
|
|
9
|
+
# its page background. Runs on CONVERTED markdown, so every backend
|
|
10
|
+
# benefits: Local and Browser already convert through Markdown, and
|
|
11
|
+
# Jina and Crawl4AI hand the gem pre-converted markdown — the
|
|
12
|
+
# DOM-level ContentFilter never sees either case.
|
|
13
|
+
#
|
|
14
|
+
# Conservative by design. A line is dropped only when ALL hold:
|
|
15
|
+
#
|
|
16
|
+
# * it is long enough to matter (>= min_length characters)
|
|
17
|
+
# * it contains no letters or digits at all
|
|
18
|
+
# * it is repetitive — at least two distinct characters, with a
|
|
19
|
+
# distinct/length ratio below max_entropy (a repeated stream,
|
|
20
|
+
# not prose punctuation)
|
|
21
|
+
# * it is not markdown structure: fenced or indented code, table
|
|
22
|
+
# rows, headings, blockquotes, inline code, raw HTML, math
|
|
23
|
+
#
|
|
24
|
+
# Short decorative fragments (an ASCII-art header like "*****"),
|
|
25
|
+
# single-character runs ("-----" dividers), and everything containing
|
|
26
|
+
# words survive. Lines whose only content is invisible characters
|
|
27
|
+
# (zero-width spaces, combining marks) are always dropped — they carry
|
|
28
|
+
# nothing. Blank lines are untouched.
|
|
29
|
+
class NoiseFilter
|
|
30
|
+
# Longest line that is never touched, whatever its contents.
|
|
31
|
+
DEFAULT_MIN_LENGTH = 32
|
|
32
|
+
|
|
33
|
+
# Highest distinct-chars/length ratio a line may have and still
|
|
34
|
+
# count as repetitive. Below this the line reads as a repeated
|
|
35
|
+
# stream; above it, as prose punctuation (kept).
|
|
36
|
+
DEFAULT_MAX_ENTROPY = 0.3
|
|
37
|
+
|
|
38
|
+
# Characters that never render: zero-width space/joiner and bidi
|
|
39
|
+
# controls, the BOM, and combining marks.
|
|
40
|
+
INVISIBLE_RE = /[\u200B-\u200F\uFEFF\u2060\u00AD\p{Mn}]/.freeze
|
|
41
|
+
|
|
42
|
+
# A line starting with one of these is structure, not noise:
|
|
43
|
+
# headings, blockquotes, inline code, raw HTML, math, table rows.
|
|
44
|
+
STRUCTURE_PREFIX_RE = /\A[#>`<$|]/.freeze
|
|
45
|
+
|
|
46
|
+
# GFM table separator rows — "| --- | --- |" or the pipe-only
|
|
47
|
+
# "--- | ---" variant — are dashes, pipes, colons and spaces only.
|
|
48
|
+
# Kept as structure; the noise streams this filter targets always
|
|
49
|
+
# mix in other symbol types (+ = · ~ @ # % …), which this narrow
|
|
50
|
+
# pattern cannot match, so it is safe to exempt the whole class.
|
|
51
|
+
TABLE_SEPARATOR_RE = /\A\|?[\s\-:|]+\|?\z/.freeze
|
|
52
|
+
|
|
53
|
+
# Fenced code opener/closer: three or more backticks or tildes,
|
|
54
|
+
# optionally with an info string.
|
|
55
|
+
FENCE_RE = /\A(?:`{3,}|~{3,})/.freeze
|
|
56
|
+
|
|
57
|
+
class << self
|
|
58
|
+
# Returns +markdown+ with decorative noise lines removed. The
|
|
59
|
+
# options override the conservative defaults.
|
|
60
|
+
def filter(markdown, min_length: DEFAULT_MIN_LENGTH, max_entropy: DEFAULT_MAX_ENTROPY)
|
|
61
|
+
new(min_length: min_length, max_entropy: max_entropy).filter(markdown)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def initialize(min_length: DEFAULT_MIN_LENGTH, max_entropy: DEFAULT_MAX_ENTROPY)
|
|
66
|
+
@min_length = min_length
|
|
67
|
+
@max_entropy = max_entropy
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def filter(markdown)
|
|
71
|
+
out = +''
|
|
72
|
+
in_fence = false
|
|
73
|
+
in_indented_code = false
|
|
74
|
+
prev_blank = false
|
|
75
|
+
|
|
76
|
+
markdown.each_line do |line|
|
|
77
|
+
stripped = line.strip
|
|
78
|
+
|
|
79
|
+
# Fenced code: flip on any fence opener/closer, then pass the
|
|
80
|
+
# whole block through untouched — code may legitimately be
|
|
81
|
+
# nothing but symbols.
|
|
82
|
+
if stripped.match?(FENCE_RE)
|
|
83
|
+
in_fence = !in_fence
|
|
84
|
+
out << line
|
|
85
|
+
next
|
|
86
|
+
end
|
|
87
|
+
if in_fence
|
|
88
|
+
out << line
|
|
89
|
+
next
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Indented code (GFM-ish: 4+ leading spaces, ends at a blank
|
|
93
|
+
# line). Passed through untouched for the same reason.
|
|
94
|
+
if in_indented_code && stripped.empty?
|
|
95
|
+
in_indented_code = false
|
|
96
|
+
out << line
|
|
97
|
+
next
|
|
98
|
+
end
|
|
99
|
+
indented = line.start_with?(' ', "\t")
|
|
100
|
+
if indented && (in_indented_code || prev_blank)
|
|
101
|
+
in_indented_code = true
|
|
102
|
+
out << line
|
|
103
|
+
next
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
prev_blank = stripped.empty?
|
|
107
|
+
out << line unless noise_line?(stripped)
|
|
108
|
+
end
|
|
109
|
+
out
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
private
|
|
113
|
+
|
|
114
|
+
def noise_line?(stripped)
|
|
115
|
+
# Blank lines are structure — never touched.
|
|
116
|
+
return false if stripped.empty?
|
|
117
|
+
|
|
118
|
+
# Nothing but invisible characters renders as blank: pure waste.
|
|
119
|
+
return true if stripped.gsub(INVISIBLE_RE, '').empty?
|
|
120
|
+
|
|
121
|
+
# Words and numbers are content, whatever surrounds them.
|
|
122
|
+
return false if stripped.match?(/[A-Za-z0-9]/)
|
|
123
|
+
|
|
124
|
+
# Structural markers and anything too short to matter survive
|
|
125
|
+
# even when symbol-only.
|
|
126
|
+
return false if stripped.match?(STRUCTURE_PREFIX_RE)
|
|
127
|
+
return false if stripped.length < @min_length
|
|
128
|
+
|
|
129
|
+
# Table separator rows ("--- | ---") are structure even without a
|
|
130
|
+
# leading pipe — dash/pipe/colon-only lines are dividers either
|
|
131
|
+
# way, and real noise streams never match their narrow alphabet.
|
|
132
|
+
return false if stripped.match?(TABLE_SEPARATOR_RE)
|
|
133
|
+
|
|
134
|
+
chars = stripped.gsub(/\s/, '')
|
|
135
|
+
distinct = chars.chars.uniq.length
|
|
136
|
+
return false if distinct < 2
|
|
137
|
+
return false if distinct.fdiv(chars.length) >= @max_entropy
|
|
138
|
+
|
|
139
|
+
true
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ask-web-fetch
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.5.
|
|
4
|
+
version: 0.5.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kaka Ruto
|
|
@@ -146,6 +146,7 @@ files:
|
|
|
146
146
|
- lib/ask/web_fetch/content_filter.rb
|
|
147
147
|
- lib/ask/web_fetch/http.rb
|
|
148
148
|
- lib/ask/web_fetch/markdown.rb
|
|
149
|
+
- lib/ask/web_fetch/noise_filter.rb
|
|
149
150
|
- lib/ask/web_fetch/tool.rb
|
|
150
151
|
- lib/ask/web_fetch/version.rb
|
|
151
152
|
homepage: https://github.com/ask-rb/ask-web-fetch
|