-
Notifications
You must be signed in to change notification settings - Fork 453
Expand file tree
/
Copy pathcrw_search.py
More file actions
230 lines (196 loc) · 9.02 KB
/
Copy pathcrw_search.py
File metadata and controls
230 lines (196 loc) · 9.02 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
from __future__ import annotations
from typing import Any, Optional, Dict, List, Type
import json
import os
from pydantic import ConfigDict, Field
from firecrawl import AsyncFirecrawlApp
from dotenv import load_dotenv
load_dotenv()
from src.tool.default_tools.search.types import SearchItem, SearchToolArgs
from src.tool.types import Tool, ToolResponse, ToolExtra
from src.logger import logger
from src.registry import TOOL
# Default fastCRW cloud base URL. fastCRW is a Firecrawl-compatible web scraper
# (single binary; self-host or cloud), so the official Firecrawl client can be
# pointed at it by overriding the base URL. Set CRW_API_URL to use a self-hosted
# server (e.g. http://localhost:3000).
DEFAULT_CRW_API_URL = "https://fastcrw.com/api"
@TOOL.register_module(force=True)
class CrwSearch(Tool):
"""Tool that queries the fastCRW search engine.
fastCRW is a Firecrawl-compatible web data engine (single binary; self-host
or cloud). It exposes the same /v1/search API as Firecrawl, so the official
Firecrawl client is reused with the fastCRW base URL.
Example usages:
.. code-block:: python
# basic usage
tool = CrwSearch()
.. code-block:: python
# with custom search kwargs
tool = CrwSearch.from_search_kwargs({"limit": 5})
"""
model_config = ConfigDict(arbitrary_types_allowed=True, extra="allow")
name: str = "crw_search"
description: str = (
"a search engine. "
"useful for when you need to answer questions about current events."
" input should be a search query."
)
metadata: Dict[str, Any] = Field(default={}, description="The metadata of the tool")
api_key: Optional[str] = Field(default=None, description="fastCRW API key")
api_url: Optional[str] = Field(default=None, description="fastCRW base URL (override for self-host)")
def __init__(self, **kwargs):
"""Initialize the CrwSearch tool."""
# Set api_key from environment if not provided
super().__init__(**kwargs)
self.api_key = os.getenv("CRW_API_KEY")
self.api_url = os.getenv("CRW_API_URL", DEFAULT_CRW_API_URL)
@classmethod
def from_search_kwargs(cls, search_kwargs: dict, **kwargs: Any) -> CrwSearch:
"""Create a tool from search kwargs.
Args:
search_kwargs: Any additional kwargs to pass to the search function.
**kwargs: Any additional kwargs to pass to the tool.
Returns:
A tool.
"""
return cls(search_kwargs=search_kwargs, **kwargs)
async def _search_crw(self,
query: str,
num_results: int = 10,
filter_year: Optional[int] = 2025) -> List[SearchItem]:
"""
Perform a fastCRW search using the provided parameters.
Returns a list of SearchItem objects.
"""
if not self.api_key:
raise ValueError("CRW_API_KEY environment variable is required")
results = []
app = AsyncFirecrawlApp(api_key=self.api_key, api_url=self.api_url)
search_kwargs = {
"query": query,
"limit": num_results,
}
# Add date filter if year is valid (1900-2100)
# Handle None case (when explicitly passed from caller)
if filter_year is None:
filter_year = 2025 # Use default if None
if 1900 <= filter_year <= 2100:
search_kwargs["tbs"] = f"cdr:1,cd_min:01/01/{filter_year},cd_max:12/31/{filter_year}"
else:
logger.warning(f"Invalid filter_year: {filter_year}. Expected 1900-2100. Ignoring date filter.")
try:
response = await app.search(**search_kwargs)
except Exception as e:
logger.error(f"fastCRW API call failed: {e}")
return results
# Check if response and response.web exist and are not None
if response is None:
logger.warning("fastCRW search returned None response")
return results
# Log response structure for debugging
logger.debug(f"fastCRW response type: {type(response)}")
logger.debug(f"fastCRW response attributes: {dir(response) if hasattr(response, '__dict__') else 'N/A'}")
# Check for different possible response formats
web_results = None
# Try to access web results from response object
if hasattr(response, 'web') and response.web is not None:
web_results = response.web
elif hasattr(response, 'data') and response.data is not None:
# Some API versions might use 'data' instead of 'web'
web_results = response.data
elif hasattr(response, 'results') and response.results is not None:
# Try 'results' attribute
web_results = response.results
elif isinstance(response, dict):
# Response might be a dict
web_results = response.get('web') or response.get('data') or response.get('results')
elif isinstance(response, list):
# Response might be a list directly
web_results = response
else:
# Try to convert response to dict if it's a Pydantic model
try:
if hasattr(response, 'model_dump'):
response_dict = response.model_dump()
web_results = response_dict.get('web') or response_dict.get('data') or response_dict.get('results')
elif hasattr(response, 'dict'):
response_dict = response.dict()
web_results = response_dict.get('web') or response_dict.get('data') or response_dict.get('results')
except Exception:
pass
if web_results is None:
# Log full response structure for debugging
logger.warning(
f"fastCRW search response has no accessible results. "
f"Response type: {type(response)}, Response: {str(response)[:200]}"
)
# Try to log all attributes
if hasattr(response, '__dict__'):
logger.debug(f"Response attributes: {list(response.__dict__.keys())}")
elif hasattr(response, '__fields__'):
logger.debug(f"Response fields: {list(response.__fields__.keys())}")
return results
# Safely iterate over web_results
try:
for item in web_results:
if item is None:
continue
# Handle both object and dict formats
if isinstance(item, dict):
title = item.get('title', '') or ""
url = item.get('url', '') or ""
description = item.get('description', '') or item.get('snippet', '') or ""
else:
title = getattr(item, 'title', None) or ""
url = getattr(item, 'url', None) or ""
description = getattr(item, 'description', None) or getattr(item, 'snippet', None) or ""
if url: # Only add items with valid URLs
results.append(SearchItem(
title=title,
url=url,
description=description
))
except (TypeError, AttributeError) as e:
logger.error(f"Error iterating over fastCRW search results: {e}, web_results type: {type(web_results)}")
return results
return results
async def __call__(
self,
query: str,
num_results: Optional[int] = 5,
country: Optional[str] = "us",
lang: Optional[str] = "en",
filter_year: Optional[int] = 2025,
**kwargs
) -> ToolResponse:
"""
fastCRW search tool.
Args:
query (str): The query to search for.
num_results (Optional[int]): The number of search results to return.
country (Optional[str]): The country to search in.
lang (Optional[str]): The language to search in.
filter_year (int): The year to filter results by. Defaults to 2025.
"""
try:
# Perform search
search_items = await self._search_crw(query, num_results=num_results, filter_year=filter_year)
# Format results as JSON string
results_json = json.dumps([{
"title": item.title,
"url": item.url,
"description": item.description or ""
} for item in search_items], ensure_ascii=False, indent=4)
message = f"fastCRW search results for query: {query}\n\n{results_json}"
return ToolResponse(success=True, message=message, extra=ToolExtra(
data={
"query": query,
"num_results": len(search_items),
"search_items": search_items,
"engine": "crw"
}
))
except Exception as e:
logger.error(f"Error in fastCRW search: {e}")
return ToolResponse(success=False, message=f"Error in fastCRW search: {str(e)}")