blopus 0.3.2__tar.gz → 0.3.3__tar.gz
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.
- {blopus-0.3.2 → blopus-0.3.3}/PKG-INFO +1 -1
- {blopus-0.3.2 → blopus-0.3.3}/blopus/_async.py +6 -0
- {blopus-0.3.2 → blopus-0.3.3}/blopus/_common.py +11 -0
- {blopus-0.3.2 → blopus-0.3.3}/blopus/_version.py +1 -1
- {blopus-0.3.2 → blopus-0.3.3}/blopus/cli.py +23 -1
- {blopus-0.3.2 → blopus-0.3.3}/blopus/client.py +6 -0
- {blopus-0.3.2 → blopus-0.3.3}/blopus/crewai.py +11 -2
- {blopus-0.3.2 → blopus-0.3.3}/blopus/llamaindex.py +2 -1
- {blopus-0.3.2 → blopus-0.3.3}/blopus/models.py +10 -0
- {blopus-0.3.2 → blopus-0.3.3}/blopus.egg-info/PKG-INFO +1 -1
- {blopus-0.3.2 → blopus-0.3.3}/pyproject.toml +1 -1
- {blopus-0.3.2 → blopus-0.3.3}/LICENSE +0 -0
- {blopus-0.3.2 → blopus-0.3.3}/README.md +0 -0
- {blopus-0.3.2 → blopus-0.3.3}/blopus/__init__.py +0 -0
- {blopus-0.3.2 → blopus-0.3.3}/blopus/exceptions.py +0 -0
- {blopus-0.3.2 → blopus-0.3.3}/blopus/langchain.py +0 -0
- {blopus-0.3.2 → blopus-0.3.3}/blopus/mcp.py +0 -0
- {blopus-0.3.2 → blopus-0.3.3}/blopus.egg-info/SOURCES.txt +0 -0
- {blopus-0.3.2 → blopus-0.3.3}/blopus.egg-info/dependency_links.txt +0 -0
- {blopus-0.3.2 → blopus-0.3.3}/blopus.egg-info/entry_points.txt +0 -0
- {blopus-0.3.2 → blopus-0.3.3}/blopus.egg-info/requires.txt +0 -0
- {blopus-0.3.2 → blopus-0.3.3}/blopus.egg-info/top_level.txt +0 -0
- {blopus-0.3.2 → blopus-0.3.3}/setup.cfg +0 -0
- {blopus-0.3.2 → blopus-0.3.3}/tests/test_client.py +0 -0
- {blopus-0.3.2 → blopus-0.3.3}/tests/test_common.py +0 -0
- {blopus-0.3.2 → blopus-0.3.3}/tests/test_mcp.py +0 -0
|
@@ -93,6 +93,9 @@ class AsyncBlopus:
|
|
|
93
93
|
include_excerpt: bool = False,
|
|
94
94
|
excerpt_chars: Optional[int] = None,
|
|
95
95
|
news_only: bool = False,
|
|
96
|
+
recency: Optional[str] = None,
|
|
97
|
+
include_content: bool = False,
|
|
98
|
+
content_chars: Optional[int] = None,
|
|
96
99
|
) -> SearchResponse:
|
|
97
100
|
"""Run a web search. Always costs 1 credit regardless of params.
|
|
98
101
|
|
|
@@ -120,6 +123,9 @@ class AsyncBlopus:
|
|
|
120
123
|
include_excerpt=include_excerpt,
|
|
121
124
|
excerpt_chars=excerpt_chars,
|
|
122
125
|
news_only=news_only,
|
|
126
|
+
recency=recency,
|
|
127
|
+
include_content=include_content,
|
|
128
|
+
content_chars=content_chars,
|
|
123
129
|
)
|
|
124
130
|
return SearchResponse.from_dict(await self._post("/v1/search", body))
|
|
125
131
|
|
|
@@ -110,6 +110,9 @@ def build_search_body(
|
|
|
110
110
|
include_excerpt: bool = False,
|
|
111
111
|
excerpt_chars: Optional[int] = None,
|
|
112
112
|
news_only: bool = False,
|
|
113
|
+
recency: Optional[str] = None,
|
|
114
|
+
include_content: bool = False,
|
|
115
|
+
content_chars: Optional[int] = None,
|
|
113
116
|
) -> Dict[str, Any]:
|
|
114
117
|
"""Assemble the JSON body for ``POST /v1/search`` (omitting unset optionals)."""
|
|
115
118
|
body: Dict[str, Any] = {
|
|
@@ -132,6 +135,14 @@ def build_search_body(
|
|
|
132
135
|
body["include_excerpt"] = True
|
|
133
136
|
if excerpt_chars is not None:
|
|
134
137
|
body["excerpt_chars"] = excerpt_chars
|
|
138
|
+
# recency changes RANKING; freshness FILTERS. Sent only when set, so the server
|
|
139
|
+
# default ("normal") stays authoritative.
|
|
140
|
+
if recency is not None:
|
|
141
|
+
body["recency"] = recency
|
|
142
|
+
if include_content:
|
|
143
|
+
body["include_content"] = True
|
|
144
|
+
if content_chars is not None:
|
|
145
|
+
body["content_chars"] = content_chars
|
|
135
146
|
if news_only:
|
|
136
147
|
# only send it when true — an older gateway would 422 on an unknown field
|
|
137
148
|
body["news_only"] = True
|
|
@@ -175,6 +175,12 @@ def _cmd_search(args: argparse.Namespace) -> int:
|
|
|
175
175
|
language=args.language,
|
|
176
176
|
offset=args.offset,
|
|
177
177
|
include_excerpt=args.include_excerpt,
|
|
178
|
+
excerpt_chars=args.excerpt_chars,
|
|
179
|
+
start_date=args.start_date,
|
|
180
|
+
end_date=args.end_date,
|
|
181
|
+
recency=args.recency,
|
|
182
|
+
include_content=args.include_content,
|
|
183
|
+
content_chars=args.content_chars,
|
|
178
184
|
)
|
|
179
185
|
if args.json:
|
|
180
186
|
print(json.dumps(res.raw, indent=2, ensure_ascii=False))
|
|
@@ -184,7 +190,9 @@ def _cmd_search(args: argparse.Namespace) -> int:
|
|
|
184
190
|
for i, r in enumerate(res.results, 1):
|
|
185
191
|
print(f"{i}. {r.title}")
|
|
186
192
|
print(f" {r.url}")
|
|
187
|
-
if r.
|
|
193
|
+
if r.content:
|
|
194
|
+
print(f" {r.content}")
|
|
195
|
+
elif r.snippet:
|
|
188
196
|
print(f" {r.snippet}")
|
|
189
197
|
print()
|
|
190
198
|
if res.remaining_quota is not None:
|
|
@@ -298,6 +306,20 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
298
306
|
help="Skip N results for pagination (0-200).")
|
|
299
307
|
s.add_argument("--include-excerpt", action="store_true",
|
|
300
308
|
help="Return a longer excerpt per result (skips a separate fetch).")
|
|
309
|
+
s.add_argument("--recency", default=None, choices=["normal", "relaxed", "off"],
|
|
310
|
+
help="Ranking preference, not a filter. 'off' for timeless questions "
|
|
311
|
+
"where the best answer may be months old.")
|
|
312
|
+
s.add_argument("--start-date", default=None, metavar="DATE",
|
|
313
|
+
help="Window start: YYYY-MM-DD or epoch seconds.")
|
|
314
|
+
s.add_argument("--end-date", default=None, metavar="DATE",
|
|
315
|
+
help="Window end: YYYY-MM-DD or epoch seconds.")
|
|
316
|
+
s.add_argument("--excerpt-chars", type=int, default=None, metavar="N",
|
|
317
|
+
help="Excerpt length when --include-excerpt is set.")
|
|
318
|
+
s.add_argument("--include-content", action="store_true",
|
|
319
|
+
help="Return full text inline instead of a snippet. Costs the same as "
|
|
320
|
+
"the search, so it beats fetching each result separately.")
|
|
321
|
+
s.add_argument("--content-chars", type=int, default=None, metavar="N",
|
|
322
|
+
help="Cap inline content length per result.")
|
|
301
323
|
s.add_argument("--json", action="store_true", help="Emit raw JSON instead of text.")
|
|
302
324
|
s.set_defaults(func=_cmd_search)
|
|
303
325
|
|
|
@@ -105,6 +105,9 @@ class Blopus:
|
|
|
105
105
|
include_excerpt: bool = False,
|
|
106
106
|
excerpt_chars: Optional[int] = None,
|
|
107
107
|
news_only: bool = False,
|
|
108
|
+
recency: Optional[str] = None,
|
|
109
|
+
include_content: bool = False,
|
|
110
|
+
content_chars: Optional[int] = None,
|
|
108
111
|
) -> SearchResponse:
|
|
109
112
|
"""Run a web search. Always costs 1 credit regardless of params.
|
|
110
113
|
|
|
@@ -132,6 +135,9 @@ class Blopus:
|
|
|
132
135
|
include_excerpt=include_excerpt,
|
|
133
136
|
excerpt_chars=excerpt_chars,
|
|
134
137
|
news_only=news_only,
|
|
138
|
+
recency=recency,
|
|
139
|
+
include_content=include_content,
|
|
140
|
+
content_chars=content_chars,
|
|
135
141
|
)
|
|
136
142
|
return SearchResponse.from_dict(self._post("/v1/search", body))
|
|
137
143
|
|
|
@@ -34,8 +34,16 @@ def BlopusSearchTool(
|
|
|
34
34
|
client: Optional[Blopus] = None,
|
|
35
35
|
count: int = 10,
|
|
36
36
|
freshness: str = "all",
|
|
37
|
+
news_only: bool = False,
|
|
38
|
+
**search_kwargs,
|
|
37
39
|
):
|
|
38
|
-
"""Return a CrewAI ``BaseTool`` backed by ``Blopus.search``.
|
|
40
|
+
"""Return a CrewAI ``BaseTool`` backed by ``Blopus.search``.
|
|
41
|
+
|
|
42
|
+
``news_only`` and any other :meth:`Blopus.search` keyword are accepted so this
|
|
43
|
+
adapter is not the odd one out — the LangChain and LlamaIndex wrappers already
|
|
44
|
+
take them, and a parameter missing from one framework's wrapper is a parameter
|
|
45
|
+
that framework's users cannot reach.
|
|
46
|
+
"""
|
|
39
47
|
BaseTool, BaseModel, Field = _requirements()
|
|
40
48
|
_client = client or Blopus(api_key=api_key)
|
|
41
49
|
|
|
@@ -51,7 +59,8 @@ def BlopusSearchTool(
|
|
|
51
59
|
args_schema: type = _Schema
|
|
52
60
|
|
|
53
61
|
def _run(self, query: str) -> str:
|
|
54
|
-
res = _client.search(query, count=count, freshness=freshness
|
|
62
|
+
res = _client.search(query, count=count, freshness=freshness,
|
|
63
|
+
news_only=news_only, **search_kwargs)
|
|
55
64
|
if not res.results:
|
|
56
65
|
return "No results."
|
|
57
66
|
return "\n".join(
|
|
@@ -39,6 +39,7 @@ def BlopusToolSpec(
|
|
|
39
39
|
count: int = 10,
|
|
40
40
|
freshness: str = "all",
|
|
41
41
|
news_only: bool = False,
|
|
42
|
+
**search_kwargs,
|
|
42
43
|
):
|
|
43
44
|
"""Return a LlamaIndex tool spec exposing Blopus ``search`` and ``fetch``.
|
|
44
45
|
|
|
@@ -55,7 +56,7 @@ def BlopusToolSpec(
|
|
|
55
56
|
def blopus_search(self, query: str) -> List[dict]:
|
|
56
57
|
"""Search the web. Returns a list of {title, url, snippet, score} results."""
|
|
57
58
|
res = _client.search(query, count=count, freshness=freshness,
|
|
58
|
-
news_only=news_only)
|
|
59
|
+
news_only=news_only, **search_kwargs)
|
|
59
60
|
return [
|
|
60
61
|
{"title": r.title, "url": r.url, "snippet": r.snippet, "score": r.score}
|
|
61
62
|
for r in res.results
|
|
@@ -41,6 +41,12 @@ class SearchResult:
|
|
|
41
41
|
age_seconds: Optional[int] = None
|
|
42
42
|
language: Optional[str] = None
|
|
43
43
|
score: Optional[float] = None
|
|
44
|
+
#: Crawl time. ``published_at`` falls back to this when the article date is unknown.
|
|
45
|
+
fetched_at: Optional[int] = None
|
|
46
|
+
#: How many near-identical pages were collapsed into this hit.
|
|
47
|
+
duplicate_count: int = 0
|
|
48
|
+
#: Full text. Present only when the request set ``include_content``.
|
|
49
|
+
content: Optional[str] = None
|
|
44
50
|
raw: Dict[str, Any] = field(default_factory=dict, repr=False)
|
|
45
51
|
|
|
46
52
|
@classmethod
|
|
@@ -56,6 +62,10 @@ class SearchResult:
|
|
|
56
62
|
age_seconds=_as_int(d.get("age_seconds")),
|
|
57
63
|
language=d.get("language"),
|
|
58
64
|
score=_as_float(d.get("score")),
|
|
65
|
+
fetched_at=_as_int(d.get("fetched_at")),
|
|
66
|
+
duplicate_count=_as_int(d.get("duplicate_count")) or 0,
|
|
67
|
+
# only present when include_content was requested
|
|
68
|
+
content=d.get("content"),
|
|
59
69
|
raw=d,
|
|
60
70
|
)
|
|
61
71
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|