agent_search_gateway.providers.web.firecrawl
Firecrawl v2 search and scrape adapter.
1"""Firecrawl v2 search and scrape adapter.""" 2 3from ...errors import ExecutionFailure 4from ...observability import SecretValue 5from ...providers.contracts import KeywordSearchHit, URLFetchCandidate 6from ...url_normalization import NormalizedURL 7from .common import ( 8 JsonRequester, 9 endpoint, 10 failure, 11 non_empty_string, 12 optional_string, 13 require_list, 14 require_object, 15) 16 17 18def _v2_endpoint(api_url: str, suffix: str) -> str: 19 base = api_url.rstrip("/") 20 if base.endswith("/v2"): 21 return endpoint(base, suffix) 22 return endpoint(base, f"/v2/{suffix.lstrip('/')}") 23 24 25def _page_body( 26 result: dict[str, object], 27 provider: str, 28 stage: str, 29) -> URLFetchCandidate: 30 markdown = optional_string(result.get("markdown"), provider, stage, "markdown") 31 raw_html = optional_string(result.get("rawHtml"), provider, stage, "rawHtml") 32 raw = raw_html or markdown 33 if not raw.strip(): 34 raise failure(provider, stage, "page body is empty") 35 return URLFetchCandidate(raw_content=raw, content=markdown) 36 37 38class FirecrawlAdapter: 39 def __init__( 40 self, 41 *, 42 name: str, 43 api_url: str, 44 secret: SecretValue, 45 http_executor: JsonRequester, 46 ) -> None: 47 self.name = name 48 self._api_url = api_url 49 self._secret = secret 50 self._http = http_executor 51 52 @property 53 def _headers(self) -> dict[str, str]: 54 return {"Authorization": f"Bearer {self._secret.reveal()}"} 55 56 async def search(self, query: str) -> list[KeywordSearchHit]: 57 payload = await self._http.request_json( 58 "POST", 59 _v2_endpoint(self._api_url, "search"), 60 stage="search", 61 headers=self._headers, 62 json_body={ 63 "query": query, 64 "sources": ["web"], 65 "scrapeOptions": {"formats": ["markdown", "rawHtml"]}, 66 }, 67 ) 68 root = self._successful_root(payload, "search") 69 data = require_object(root.get("data"), self.name, "search", "data") 70 web = require_list(data.get("web"), self.name, "search", "data.web") 71 hits: list[KeywordSearchHit] = [] 72 for item in web: 73 try: 74 result = require_object(item, self.name, "search", "result") 75 url = non_empty_string(result.get("url"), self.name, "search", "result.url") 76 markdown = optional_string( 77 result.get("markdown"), self.name, "search", "result.markdown" 78 ) 79 raw_html = optional_string( 80 result.get("rawHtml"), self.name, "search", "result.rawHtml" 81 ) 82 hits.append( 83 KeywordSearchHit( 84 url=url, 85 title=optional_string( 86 result.get("title"), self.name, "search", "result.title" 87 ), 88 snippet=optional_string( 89 result.get("description"), 90 self.name, 91 "search", 92 "result.description", 93 ), 94 raw_content=raw_html or markdown, 95 content=markdown, 96 ) 97 ) 98 except ExecutionFailure: 99 continue 100 return hits 101 102 async def fetch(self, url: NormalizedURL) -> URLFetchCandidate: 103 payload = await self._http.request_json( 104 "POST", 105 _v2_endpoint(self._api_url, "scrape"), 106 stage="fetch", 107 headers=self._headers, 108 json_body={"url": str(url), "formats": ["markdown", "rawHtml"]}, 109 ) 110 root = self._successful_root(payload, "fetch") 111 data = require_object(root.get("data"), self.name, "fetch", "data") 112 return _page_body(data, self.name, "fetch") 113 114 def _successful_root(self, payload: object, stage: str) -> dict[str, object]: 115 root = require_object(payload, self.name, stage, "response") 116 if root.get("success") is not True: 117 raise failure(self.name, stage, "provider reported failure") 118 return root
class
FirecrawlAdapter:
39class FirecrawlAdapter: 40 def __init__( 41 self, 42 *, 43 name: str, 44 api_url: str, 45 secret: SecretValue, 46 http_executor: JsonRequester, 47 ) -> None: 48 self.name = name 49 self._api_url = api_url 50 self._secret = secret 51 self._http = http_executor 52 53 @property 54 def _headers(self) -> dict[str, str]: 55 return {"Authorization": f"Bearer {self._secret.reveal()}"} 56 57 async def search(self, query: str) -> list[KeywordSearchHit]: 58 payload = await self._http.request_json( 59 "POST", 60 _v2_endpoint(self._api_url, "search"), 61 stage="search", 62 headers=self._headers, 63 json_body={ 64 "query": query, 65 "sources": ["web"], 66 "scrapeOptions": {"formats": ["markdown", "rawHtml"]}, 67 }, 68 ) 69 root = self._successful_root(payload, "search") 70 data = require_object(root.get("data"), self.name, "search", "data") 71 web = require_list(data.get("web"), self.name, "search", "data.web") 72 hits: list[KeywordSearchHit] = [] 73 for item in web: 74 try: 75 result = require_object(item, self.name, "search", "result") 76 url = non_empty_string(result.get("url"), self.name, "search", "result.url") 77 markdown = optional_string( 78 result.get("markdown"), self.name, "search", "result.markdown" 79 ) 80 raw_html = optional_string( 81 result.get("rawHtml"), self.name, "search", "result.rawHtml" 82 ) 83 hits.append( 84 KeywordSearchHit( 85 url=url, 86 title=optional_string( 87 result.get("title"), self.name, "search", "result.title" 88 ), 89 snippet=optional_string( 90 result.get("description"), 91 self.name, 92 "search", 93 "result.description", 94 ), 95 raw_content=raw_html or markdown, 96 content=markdown, 97 ) 98 ) 99 except ExecutionFailure: 100 continue 101 return hits 102 103 async def fetch(self, url: NormalizedURL) -> URLFetchCandidate: 104 payload = await self._http.request_json( 105 "POST", 106 _v2_endpoint(self._api_url, "scrape"), 107 stage="fetch", 108 headers=self._headers, 109 json_body={"url": str(url), "formats": ["markdown", "rawHtml"]}, 110 ) 111 root = self._successful_root(payload, "fetch") 112 data = require_object(root.get("data"), self.name, "fetch", "data") 113 return _page_body(data, self.name, "fetch") 114 115 def _successful_root(self, payload: object, stage: str) -> dict[str, object]: 116 root = require_object(payload, self.name, stage, "response") 117 if root.get("success") is not True: 118 raise failure(self.name, stage, "provider reported failure") 119 return root
FirecrawlAdapter( *, name: str, api_url: str, secret: agent_search_gateway.observability.SecretValue, http_executor: agent_search_gateway.providers.web.common.JsonRequester)
async def
search( self, query: str) -> list[agent_search_gateway.providers.contracts.KeywordSearchHit]:
57 async def search(self, query: str) -> list[KeywordSearchHit]: 58 payload = await self._http.request_json( 59 "POST", 60 _v2_endpoint(self._api_url, "search"), 61 stage="search", 62 headers=self._headers, 63 json_body={ 64 "query": query, 65 "sources": ["web"], 66 "scrapeOptions": {"formats": ["markdown", "rawHtml"]}, 67 }, 68 ) 69 root = self._successful_root(payload, "search") 70 data = require_object(root.get("data"), self.name, "search", "data") 71 web = require_list(data.get("web"), self.name, "search", "data.web") 72 hits: list[KeywordSearchHit] = [] 73 for item in web: 74 try: 75 result = require_object(item, self.name, "search", "result") 76 url = non_empty_string(result.get("url"), self.name, "search", "result.url") 77 markdown = optional_string( 78 result.get("markdown"), self.name, "search", "result.markdown" 79 ) 80 raw_html = optional_string( 81 result.get("rawHtml"), self.name, "search", "result.rawHtml" 82 ) 83 hits.append( 84 KeywordSearchHit( 85 url=url, 86 title=optional_string( 87 result.get("title"), self.name, "search", "result.title" 88 ), 89 snippet=optional_string( 90 result.get("description"), 91 self.name, 92 "search", 93 "result.description", 94 ), 95 raw_content=raw_html or markdown, 96 content=markdown, 97 ) 98 ) 99 except ExecutionFailure: 100 continue 101 return hits
async def
fetch( self, url: agent_search_gateway.url_normalization.NormalizedURL) -> agent_search_gateway.providers.contracts.URLFetchCandidate:
103 async def fetch(self, url: NormalizedURL) -> URLFetchCandidate: 104 payload = await self._http.request_json( 105 "POST", 106 _v2_endpoint(self._api_url, "scrape"), 107 stage="fetch", 108 headers=self._headers, 109 json_body={"url": str(url), "formats": ["markdown", "rawHtml"]}, 110 ) 111 root = self._successful_root(payload, "fetch") 112 data = require_object(root.get("data"), self.name, "fetch", "data") 113 return _page_body(data, self.name, "fetch")