ask-web-fetch 0.2.0 → 0.3.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 33eb9a14775223cfaf64bab5b01544c8eb79bb9f4b28c2c8a414bd579116e1e7
4
- data.tar.gz: b75238a2a93d2f04c8d7327a993c2a097ab09df3169ce005e67c32b4d25ba00f
3
+ metadata.gz: efd68d22906c21155fe37405f68ef05185732ff6f1ce39767e50c87e4d535994
4
+ data.tar.gz: 7d06e49acbc527f06badb034933d792203910637c0e2671f133ec2fc159be41d
5
5
  SHA512:
6
- metadata.gz: 380b32b742ef62153c8def11dc619e133d1230d7ec39c73b7787448550d5d3f8b37ee9fbe7b5e09a886ba29e7cbfefbdbe0f73d4999255bd4cd19202c03f12a8
7
- data.tar.gz: a0f520160d28b978adae2a52414b2625cb88f57bec7a1cab5139277db7f5d639410889915e7a2fbfebf803082cdb5470a48b145ace49776237a91ab8be302e4d
6
+ metadata.gz: 7736807de8ff9c7b1bad298af12abf67af77f254090532f39f080d0dd98c2bb3a7fed60aac5cf8bbeaeb977b03d3d38900525a4a85b012dc94ae171f18f38b3e
7
+ data.tar.gz: 2edae586d7baeb9114e29d23540ccbaa331e920a8fe83b7acea12c612b8491116fc635c948d437a157c3193b90d63b97e84857b7714f73ff5f6d19d6c907472b
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+ require 'uri'
5
+ require 'json'
6
+ require_relative '../backend'
7
+
8
+ module Ask
9
+ module WebFetch
10
+ module Backends
11
+ # Self-hosted Crawl4AI (https://docs.crawl4ai.com) — a headless
12
+ # Chromium crawler that renders JavaScript and returns clean markdown.
13
+ # Runs as its own Docker service (default http://localhost:11235), the
14
+ # same self-hosted pattern as ask-web-search's SearXNG. No API key;
15
+ # configure via CRAWL4AI_URL (and CRAWL4AI_TOKEN for 0.9+ JWT-protected
16
+ # servers).
17
+ #
18
+ # Kept FIRST in the default chain: when the service is present it
19
+ # handles the JS-rendered pages the Local backend can't. When it isn't
20
+ # configured — or is unreachable — it fails fast and the chain falls
21
+ # through to Local, with Jina as the last resort.
22
+ class Crawl4Ai < Backend
23
+ DEFAULT_URL = 'http://localhost:11235'
24
+ OPEN_TIMEOUT = 5
25
+ READ_TIMEOUT = 30
26
+ CRAWL_TIMEOUT = 60
27
+
28
+ class << self
29
+ attr_writer :url, :token
30
+
31
+ def url
32
+ @url || ENV['CRAWL4AI_URL']
33
+ end
34
+
35
+ def token
36
+ @token || ENV['CRAWL4AI_TOKEN']
37
+ end
38
+
39
+ # Presence = configuration. The tool's default chain only includes
40
+ # this backend when CRAWL4AI_URL is set, so consumers without a
41
+ # Crawl4AI service see zero behavior change (Local -> Jina).
42
+ def configured?
43
+ !url.to_s.empty?
44
+ end
45
+ end
46
+
47
+ def fetch(url)
48
+ raise FetchError, 'Crawl4AI not configured (set CRAWL4AI_URL)' if self.class.url.to_s.empty?
49
+
50
+ body = crawl(url)
51
+ raise FetchError, "challenge page at #{url}" if challenge_page?(body)
52
+
53
+ page = to_page(body, url)
54
+ raise EmptyContentError, "no readable content at #{url}" unless usable_content?(page[:content])
55
+
56
+ page
57
+ rescue Net::OpenTimeout, Net::ReadTimeout, Errno::ECONNREFUSED,
58
+ Errno::ECONNRESET, SocketError, URI::InvalidURIError => e
59
+ raise FetchError, "Crawl4AI #{e.class}: #{e.message}"
60
+ end
61
+
62
+ private
63
+
64
+ def crawl(url)
65
+ uri = URI("#{self.class.url.chomp('/')}/crawl")
66
+ http = Net::HTTP.new(uri.host, uri.port)
67
+ http.use_ssl = uri.scheme == 'https'
68
+ http.open_timeout = OPEN_TIMEOUT
69
+ http.read_timeout = READ_TIMEOUT
70
+
71
+ req = Net::HTTP::Post.new(uri)
72
+ req['Content-Type'] = 'application/json'
73
+ req['Accept'] = 'application/json'
74
+ req['User-Agent'] = USER_AGENT
75
+ req['Authorization'] = "Bearer #{self.class.token}" if self.class.token
76
+ req.body = JSON.generate(
77
+ urls: [url],
78
+ crawler_config: { cache_mode: 'bypass', timeout: CRAWL_TIMEOUT }
79
+ )
80
+
81
+ res = http.request(req)
82
+ case res.code
83
+ when '200'
84
+ res.body.to_s
85
+ when '401', '403'
86
+ raise FetchError, "Crawl4AI auth error (#{res.code})"
87
+ else
88
+ raise FetchError, "Crawl4AI returned #{res.code}"
89
+ end
90
+ end
91
+
92
+ # The /crawl response is {success:, results: [CrawlResult...]} where
93
+ # each result carries markdown (fit_markdown preferred, raw_markdown
94
+ # fallback) and metadata (title, description, ...).
95
+ def to_page(body, url)
96
+ data = JSON.parse(body)
97
+ result = Array(data['results']).first || {}
98
+ if result['success'] == false
99
+ raise FetchError, "Crawl4AI crawl failed: #{result['error_message'] || 'unknown error'}"
100
+ end
101
+
102
+ markdown = result.dig('markdown', 'fit_markdown').to_s
103
+ markdown = result.dig('markdown', 'raw_markdown').to_s if markdown.strip.empty?
104
+ { title: result.dig('metadata', 'title'), content: markdown }
105
+ rescue JSON::ParserError => e
106
+ raise FetchError, "Crawl4AI bad JSON response: #{e.message}"
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
@@ -3,22 +3,34 @@
3
3
  require 'ask-tools'
4
4
  require_relative '../web_fetch/backend'
5
5
  require_relative '../web_fetch/backends/local'
6
+ require_relative '../web_fetch/backends/crawl4ai'
6
7
  require_relative '../web_fetch/backends/jina'
7
8
 
8
9
  module Ask
9
10
  module Tools
10
11
  # Fetches a URL and returns its content as clean markdown for LLM
11
12
  # consumption. Tries each configured backend in order and returns the
12
- # first success, so a blocked or JS-rendered page falls through from the
13
- # local fetcher to Jina Reader.
13
+ # first success.
14
+ #
15
+ # Chain: Crawl4AI first (self-hosted headless-Chromium renderer; used
16
+ # when the CRAWL4AI_URL service is present, fails fast when it isn't),
17
+ # then the local fetcher, with Jina Reader as the last resort.
14
18
  class WebFetch < Ask::Tool
15
19
  DEFAULT_MAX_CHARS = 20_000
16
20
 
17
- # Backend chain, tried in order. Swap or extend for future backends
18
- # (e.g. a self-hosted crawler); each must subclass
19
- # Ask::WebFetch::Backend and implement #fetch(url).
21
+ # Backend chain, tried in order. Crawl4AI leads when configured
22
+ # (CRAWL4AI_URL), so a present self-hosted renderer is preferred;
23
+ # otherwise Local, with Jina as the last resort. Swap or extend for
24
+ # future backends; each must subclass Ask::WebFetch::Backend and
25
+ # implement #fetch(url).
20
26
  def self.backends
21
- @backends ||= [Ask::WebFetch::Backends::Local, Ask::WebFetch::Backends::Jina]
27
+ @backends ||= begin
28
+ chain = [Ask::WebFetch::Backends::Local, Ask::WebFetch::Backends::Jina]
29
+ if Ask::WebFetch::Backends::Crawl4Ai.configured?
30
+ chain.unshift(Ask::WebFetch::Backends::Crawl4Ai)
31
+ end
32
+ chain
33
+ end
22
34
  end
23
35
 
24
36
  class << self
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Ask
4
4
  module WebFetch
5
- VERSION = '0.2.0'
5
+ VERSION = '0.3.0'
6
6
  end
7
7
  end
data/lib/ask/web_fetch.rb CHANGED
@@ -3,5 +3,6 @@
3
3
  require_relative 'web_fetch/version'
4
4
  require_relative 'web_fetch/backend'
5
5
  require_relative 'web_fetch/backends/local'
6
+ require_relative 'web_fetch/backends/crawl4ai'
6
7
  require_relative 'web_fetch/backends/jina'
7
8
  require_relative 'web_fetch/tool'
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.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kaka Ruto
@@ -122,6 +122,7 @@ files:
122
122
  - lib/ask-web-fetch.rb
123
123
  - lib/ask/web_fetch.rb
124
124
  - lib/ask/web_fetch/backend.rb
125
+ - lib/ask/web_fetch/backends/crawl4ai.rb
125
126
  - lib/ask/web_fetch/backends/jina.rb
126
127
  - lib/ask/web_fetch/backends/local.rb
127
128
  - lib/ask/web_fetch/tool.rb