tool: add web_search via Brave Search API
ober
806d510435f929c7baaffec3efa6a5cdca58fc09
--- a/src/jcode/tool/web.ss +++ b/src/jcode/tool/web.ss @@ -4,7 +4,9 @@ (import :std/net/request :std/text/json + :std/misc/string :jcode/core/log + :jcode/core/config :jcode/tool/registry) (def logger (make-logger "tool.web")) @@ -13,7 +15,11 @@ (register-tool! "fetch" "Fetch a URL via HTTP/HTTPS. Supports GET and POST. Returns HTTP status and response body." (make-fetch-schema) - handle-fetch)) + handle-fetch) + (register-tool! "web_search" + "Search the web via Brave Search. Returns a ranked list of {title, url, snippet}. Use this when you need fresh information or do not know the URL; follow up with fetch on a specific result URL." + (make-search-schema) + handle-search)) (def (make-fetch-schema) (let ((schema (make-hash-table)) @@ -69,3 +75,98 @@ (text (request-text resp))) (request-close resp) (format "Status: ~a\n~a" status text))) + +;; ---- web_search (Brave Search API) ---- + +(def (make-search-schema) + (let ((schema (make-hash-table)) + (props (make-hash-table))) + (hash-put! schema "type" "object") + (hash-put! schema "required" '("query")) + (let ((q-p (make-hash-table))) + (hash-put! q-p "type" "string") + (hash-put! q-p "description" "Search query.") + (hash-put! props "query" q-p)) + (let ((c-p (make-hash-table))) + (hash-put! c-p "type" "integer") + (hash-put! c-p "description" "Number of results to return (1-20, default 5).") + (hash-put! props "count" c-p)) + (hash-put! schema "properties" props) + schema)) + +(def (brave-api-key) + (or (getenv "BRAVE_API_KEY") + (config-ref "brave_api_key") + (config-ref "providers" "brave" "api_key"))) + +(def (handle-search args) + (let* ((query (hash-ref args "query" #f)) + (count (let ((c (hash-ref args "count" #f))) + (cond + ((or (not c) (eq? c (void))) 5) + ((number? c) (max 1 (min 20 (exact c)))) + ((string? c) (max 1 (min 20 (string->number c)))) + (else 5)))) + (key (brave-api-key))) + (unless query (error 'web_search "Missing required parameter: query")) + (unless key + (error 'web_search + "No Brave Search API key. Set BRAVE_API_KEY env var or add 'brave_api_key' to jcode.json.")) + (log-info logger "web_search" `((q . ,query) (count . ,count))) + (try + (brave-search query count key) + (catch (e) + (format "Error: ~a" (err->string e)))))) + +(def (brave-search query count key) + (let* ((url (string-append + "https://api.search.brave.com/res/v1/web/search?q=" + (url-encode query) + "&count=" (number->string count))) + (headers `(("X-Subscription-Token" . ,key) + ("Accept" . "application/json"))) + (resp (http-get url headers #f)) + (status (request-status resp)) + (text (request-text resp))) + (request-close resp) + (cond + ((not (= status 200)) + (format "Brave Search HTTP ~a\n~a" status text)) + (else (format-brave-results query text))))) + +(def (format-brave-results query body) + (let* ((root (string->json-object body)) + (web (and (hash-table? root) (hash-get root "web"))) + (results (and (hash-table? web) (hash-get web "results")))) + (cond + ((or (not results) (null? results)) + (format "No results for: ~a" query)) + (else + (let loop ((rs results) (i 1) (acc '())) + (cond + ((null? rs) + (string-append + (format "Brave Search: ~a (~a results)\n\n" + query (length results)) + (string-join (reverse acc) "\n\n"))) + (else + (let* ((r (car rs)) + (title (or (hash-get r "title") "")) + (url (or (hash-get r "url") "")) + (snip (or (hash-get r "description") ""))) + (loop (cdr rs) (+ i 1) + (cons (format "~a. ~a\n ~a\n ~a" + i title url (strip-tags snip)) + acc)))))))))) + +(def (strip-tags s) + ;; Brave snippets contain <strong>...</strong> markup; flatten for plain text. + (let loop ((chars (string->list s)) (out '()) (in-tag? #f)) + (cond + ((null? chars) (list->string (reverse out))) + ((and (not in-tag?) (char=? (car chars) #\<)) + (loop (cdr chars) out #t)) + ((and in-tag? (char=? (car chars) #\>)) + (loop (cdr chars) out #f)) + (in-tag? (loop (cdr chars) out #t)) + (else (loop (cdr chars) (cons (car chars) out) #f)))))